Skip to content

Commit 2d59e03

Browse files
committed
Add initial patterns support
1 parent 9e94f49 commit 2d59e03

File tree

15 files changed

+795
-3
lines changed

15 files changed

+795
-3
lines changed

ios/Demo-iOS/Sources/Views/AppRootView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ struct AppRootView: View {
7474

7575
let updatedConfiguration = EditorConfigurationBuilder()
7676
.setShouldUseThemeStyles(canUseEditorStyles)
77-
.setShouldUsePlugins(canUsePlugins)
77+
.setShouldUsePlugins(false)
7878
.setSiteUrl(config.siteUrl)
7979
.setSiteApiRoot(config.siteApiRoot)
8080
.setAuthHeader(config.authHeader)

ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct EditorJSMessage {
7070

7171
struct ShowBlockInserterBody: Decodable {
7272
let sections: [BlockInserterSection]
73+
let patterns: [PatternType]
7374
}
7475

7576
struct LogMessage: Decodable {

ios/Sources/GutenbergKit/Sources/EditorViewController.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,23 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
253253
// MARK: - Internal (Block Inserter)
254254

255255
private func showBlockInserter(data: EditorJSMessage.ShowBlockInserterBody) {
256+
// Configure pattern preview loader with the WebView
257+
PatternPreviewLoader.shared.configure(webView: webView)
258+
256259
let context = MediaPickerPresentationContext()
257260

258261
let host = UIHostingController(rootView: NavigationStack {
259262
BlockInserterView(
260263
sections: data.sections,
264+
patterns: data.patterns,
261265
mediaPicker: mediaPicker,
262266
presentationContext: context,
263267
onBlockSelected: { [weak self] block in
264268
self?.insertBlockFromInserter(block.id)
265269
},
270+
onPatternSelected: { [weak self] patternName in
271+
self?.insertPatternFromInserter(patternName)
272+
},
266273
onMediaSelected: {
267274
print("insert media:", $0)
268275
}
@@ -278,6 +285,11 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
278285
evaluate("window.blockInserter.insertBlock('\(blockID)')")
279286
}
280287

288+
private func insertPatternFromInserter(_ patternName: String) {
289+
let escapedName = patternName.replacingOccurrences(of: "'", with: "\\'")
290+
evaluate("window.blockInserter.insertPattern('\(escapedName)')")
291+
}
292+
281293
private func openMediaLibrary(_ config: OpenMediaLibraryAction) {
282294
delegate?.editor(self, didRequestMediaFromSiteMediaLibrary: config)
283295
}

ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import UIKit
44

55
struct BlockInserterView: View {
66
let sections: [BlockInserterSection]
7+
let patterns: [PatternType]
78
let mediaPicker: MediaPickerController?
89
let presentationContext: MediaPickerPresentationContext
910
let onBlockSelected: (BlockType) -> Void
11+
let onPatternSelected: (String) -> Void
1012
let onMediaSelected: ([MediaInfo]) -> Void
1113

1214
@StateObject private var viewModel: BlockInserterViewModel
@@ -15,18 +17,23 @@ struct BlockInserterView: View {
1517
private let maxSelectionCount = 10
1618

1719
@Environment(\.dismiss) private var dismiss
20+
@State private var showingPatterns = false
1821

1922
init(
2023
sections: [BlockInserterSection],
24+
patterns: [PatternType] = [],
2125
mediaPicker: MediaPickerController?,
2226
presentationContext: MediaPickerPresentationContext,
2327
onBlockSelected: @escaping (BlockType) -> Void,
28+
onPatternSelected: @escaping (String) -> Void,
2429
onMediaSelected: @escaping ([MediaInfo]) -> Void
2530
) {
2631
self.sections = sections
32+
self.patterns = patterns
2733
self.mediaPicker = mediaPicker
2834
self.presentationContext = presentationContext
2935
self.onBlockSelected = onBlockSelected
36+
self.onPatternSelected = onPatternSelected
3037
self.onMediaSelected = onMediaSelected
3138

3239
let viewModel = BlockInserterViewModel(sections: sections)
@@ -42,6 +49,17 @@ struct BlockInserterView: View {
4249
.toolbar {
4350
toolbar
4451
}
52+
.sheet(isPresented: $showingPatterns) {
53+
NavigationStack {
54+
PatternsView(
55+
patterns: patterns,
56+
onPatternSelected: { patternName in
57+
showingPatterns = false
58+
insertPattern(patternName)
59+
}
60+
)
61+
}
62+
}
4563
}
4664

4765
private var content: some View {
@@ -71,6 +89,13 @@ struct BlockInserterView: View {
7189
}
7290

7391
ToolbarItemGroup(placement: .topBarTrailing) {
92+
Button {
93+
showingPatterns = true
94+
} label: {
95+
Image(systemName: "square.grid.2x2")
96+
}
97+
.tint(Color.primary)
98+
7499
if let mediaPicker {
75100
MediaPickerMenu(picker: mediaPicker, context: presentationContext) {
76101
dismiss()
@@ -86,6 +111,11 @@ struct BlockInserterView: View {
86111
dismiss()
87112
onBlockSelected(block)
88113
}
114+
115+
private func insertPattern(_ patternName: String) {
116+
dismiss()
117+
onPatternSelected(patternName)
118+
}
89119
}
90120

91121
// MARK: - Preview
@@ -102,6 +132,9 @@ struct BlockInserterView: View {
102132
onBlockSelected: {
103133
print("block selected: \($0.name)")
104134
},
135+
onPatternSelected: {
136+
print("pattern selected: \($0)")
137+
},
105138
onMediaSelected: {
106139
print("media selected: \($0)")
107140
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
3+
struct PatternType: Decodable, Identifiable {
4+
let id: String
5+
let name: String
6+
let title: String
7+
let description: String?
8+
let category: String?
9+
let keywords: [String]?
10+
let content: String
11+
let patternType: PatternSource
12+
let syncStatus: SyncStatus?
13+
let viewportWidth: Int
14+
15+
enum PatternSource: String, Decodable {
16+
case user
17+
case theme
18+
case directory
19+
}
20+
21+
enum SyncStatus: String, Decodable {
22+
case synced = "fully"
23+
case unsynced
24+
}
25+
}
26+
27+
extension PatternType: Searchable {
28+
/// Sets the searchable fields in the order of priority
29+
func searchableFields() -> [SearchableField] {
30+
var fields: [SearchableField] = []
31+
32+
fields.append(SearchableField(content: title, weight: 10.0, allowFuzzyMatch: true))
33+
fields.append(SearchableField(content: name, weight: 8.0, allowFuzzyMatch: false))
34+
35+
(keywords ?? []).forEach { keyword in
36+
fields.append(SearchableField(content: keyword, weight: 5.0, allowFuzzyMatch: true))
37+
}
38+
39+
if let description, !description.isEmpty {
40+
fields.append(SearchableField(content: description, weight: 2.0, allowFuzzyMatch: false))
41+
}
42+
43+
if let category, !category.isEmpty {
44+
fields.append(SearchableField(content: category, weight: 2.0, allowFuzzyMatch: true))
45+
}
46+
47+
return fields
48+
}
49+
}
50+
51+
struct PatternSection: Identifiable, Decodable {
52+
var id: String { category }
53+
let category: String
54+
let name: String
55+
let patterns: [PatternType]
56+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import SwiftUI
2+
3+
struct PatternItemView: View {
4+
let pattern: PatternType
5+
let onSelected: () -> Void
6+
7+
@State private var previewImage: UIImage?
8+
@State private var isLoadingPreview = false
9+
@State private var previewError = false
10+
11+
var body: some View {
12+
Button(action: onSelected) {
13+
VStack(alignment: .leading, spacing: 8) {
14+
preview
15+
16+
Text(pattern.title)
17+
.font(.subheadline)
18+
.fontWeight(.medium)
19+
.lineLimit(2)
20+
.multilineTextAlignment(.leading)
21+
.foregroundStyle(Color.primary)
22+
.frame(maxWidth: .infinity, alignment: .leading)
23+
24+
if let description = pattern.description, !description.isEmpty {
25+
Text(description)
26+
.font(.caption)
27+
.lineLimit(2)
28+
.foregroundStyle(Color.secondary)
29+
.frame(maxWidth: .infinity, alignment: .leading)
30+
}
31+
}
32+
.padding(12)
33+
.background(Color(uiColor: .secondarySystemBackground))
34+
.cornerRadius(12)
35+
}
36+
.buttonStyle(.plain)
37+
}
38+
39+
@ViewBuilder
40+
private var preview: some View {
41+
if let previewImage {
42+
Image(uiImage: previewImage)
43+
.resizable()
44+
.aspectRatio(contentMode: .fit)
45+
.frame(maxWidth: .infinity)
46+
.frame(height: 120)
47+
.background(Color.white)
48+
.cornerRadius(8)
49+
.clipped()
50+
} else if isLoadingPreview {
51+
RoundedRectangle(cornerRadius: 8)
52+
.fill(Color(uiColor: .tertiarySystemBackground))
53+
.frame(height: 120)
54+
.overlay {
55+
ProgressView()
56+
}
57+
} else if previewError {
58+
RoundedRectangle(cornerRadius: 8)
59+
.fill(Color(uiColor: .tertiarySystemBackground))
60+
.frame(height: 120)
61+
.overlay {
62+
VStack(spacing: 4) {
63+
Image(systemName: "square.grid.2x2")
64+
.font(.title2)
65+
.foregroundStyle(Color.secondary)
66+
Text("Preview unavailable")
67+
.font(.caption2)
68+
.foregroundStyle(Color.secondary)
69+
}
70+
}
71+
} else {
72+
RoundedRectangle(cornerRadius: 8)
73+
.fill(Color(uiColor: .tertiarySystemBackground))
74+
.frame(height: 120)
75+
.task {
76+
await loadPreview()
77+
}
78+
}
79+
}
80+
81+
private func loadPreview() async {
82+
guard !isLoadingPreview, previewImage == nil, !previewError else {
83+
return
84+
}
85+
86+
isLoadingPreview = true
87+
defer { isLoadingPreview = false }
88+
89+
do {
90+
let image = try await PatternPreviewLoader.shared.loadPreview(for: pattern)
91+
previewImage = image
92+
} catch {
93+
print("Failed to load pattern preview: \(error)")
94+
previewError = true
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)