From 1c09ea9391f822d865972566dec9d8a62bf7b983 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 27 Oct 2025 16:15:56 -0400 Subject: [PATCH 1/5] Add support for block variants --- .../GutenbergKit/Sources/EditorTypes.swift | 7 ++-- .../Sources/EditorViewController.swift | 6 ++-- .../BlockInserterView+PreviewData.swift | 34 ++++++++++++++++++- .../native-block-inserter-button/index.jsx | 8 ++--- src/utils/blocks.js | 1 + 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/ios/Sources/GutenbergKit/Sources/EditorTypes.swift b/ios/Sources/GutenbergKit/Sources/EditorTypes.swift index e51e91948..9c50ff5d2 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorTypes.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorTypes.swift @@ -1,8 +1,11 @@ import Foundation struct EditorBlock: Decodable, Identifiable { - var id: String { name } - + /// Unique identifier for this block variant. Note that this is NOT the same as `name`. + /// Multiple blocks can share the same `name` but have different `id` values to represent + /// different variants with different initial attributes (e.g., core/embed variants for + /// YouTube, Vimeo, etc.). + let id: String let name: String let title: String? let description: String? diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index daf3e347b..dccbb6edf 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -255,7 +255,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro mediaPicker: mediaPicker, presentationContext: context, onBlockSelected: { [weak self] block in - self?.insertBlockFromInserter(block.name) + self?.insertBlockFromInserter(block.id) }, onMediaSelected: { print("insert media:", $0) @@ -268,8 +268,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro present(host, animated: true) } - private func insertBlockFromInserter(_ blockName: String) { - evaluate("window.blockInserter.insertBlock('\(blockName)')") + private func insertBlockFromInserter(_ blockId: String) { + evaluate("window.blockInserter.insertBlock('\(blockId)')") } private func openMediaLibrary(_ config: OpenMediaLibraryAction) { diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift index d6c025aa6..3e6021ae2 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift @@ -5,6 +5,7 @@ extension EditorBlock { static let mocks: [EditorBlock] = [ // Text blocks EditorBlock( + id: "core/paragraph", name: "core/paragraph", title: "Paragraph", description: "Start with the basic building block of all narrative.", @@ -13,6 +14,7 @@ extension EditorBlock { icon: paragraphSVG ), EditorBlock( + id: "core/heading", name: "core/heading", title: "Heading", description: "Introduce new sections and organize content to help visitors find what they need.", @@ -21,6 +23,7 @@ extension EditorBlock { icon: headingSVG ), EditorBlock( + id: "core/list", name: "core/list", title: "List", description: "Create a bulleted or numbered list.", @@ -29,6 +32,7 @@ extension EditorBlock { icon: listSVG ), EditorBlock( + id: "core/quote", name: "core/quote", title: "Quote", description: "Give quoted text visual emphasis.", @@ -37,6 +41,7 @@ extension EditorBlock { icon: quoteSVG ), EditorBlock( + id: "core/code", name: "core/code", title: "Code", description: "Display code snippets that respect your spacing and tabs.", @@ -45,6 +50,7 @@ extension EditorBlock { icon: codeSVG ), EditorBlock( + id: "core/preformatted", name: "core/preformatted", title: "Preformatted", description: "Add text that respects your spacing and tabs, and also allows styling.", @@ -53,6 +59,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/pullquote", name: "core/pullquote", title: "Pullquote", description: "Give special visual emphasis to a quote from your text.", @@ -61,6 +68,7 @@ extension EditorBlock { icon: quoteSVG ), EditorBlock( + id: "core/verse", name: "core/verse", title: "Verse", description: "Insert poetry. Use special spacing formats. Or quote song lyrics.", @@ -69,6 +77,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/table", name: "core/table", title: "Table", description: "Create structured content in rows and columns to display information.", @@ -79,6 +88,7 @@ extension EditorBlock { // Media blocks EditorBlock( + id: "core/image", name: "core/image", title: "Image", description: "Insert an image to make a visual statement.", @@ -87,6 +97,7 @@ extension EditorBlock { icon: imageSVG ), EditorBlock( + id: "core/gallery", name: "core/gallery", title: "Gallery", description: "Display multiple images in a rich gallery.", @@ -95,6 +106,7 @@ extension EditorBlock { icon: imageSVG ), EditorBlock( + id: "core/audio", name: "core/audio", title: "Audio", description: "Embed a simple audio player.", @@ -103,6 +115,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/video", name: "core/video", title: "Video", description: "Embed a video from your media library or upload a new one.", @@ -111,6 +124,7 @@ extension EditorBlock { icon: videoSVG ), EditorBlock( + id: "core/cover", name: "core/cover", title: "Cover", description: "Add an image or video with a text overlay.", @@ -119,6 +133,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/file", name: "core/file", title: "File", description: "Add a link to a downloadable file.", @@ -127,6 +142,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/media-text", name: "core/media-text", title: "Media & Text", description: "Set media and words side-by-side for a richer layout.", @@ -137,6 +153,7 @@ extension EditorBlock { // Design blocks EditorBlock( + id: "core/columns", name: "core/columns", title: "Columns", description: "Display content in multiple columns.", @@ -145,6 +162,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/group", name: "core/group", title: "Group", description: "Gather blocks in a container.", @@ -153,6 +171,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/separator", name: "core/separator", title: "Separator", description: "Create a break between ideas or sections.", @@ -161,6 +180,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/spacer", name: "core/spacer", title: "Spacer", description: "Add white space between blocks.", @@ -169,6 +189,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/buttons", name: "core/buttons", title: "Buttons", description: "Prompt visitors to take action with a group of button-style links.", @@ -177,6 +198,7 @@ extension EditorBlock { icon: buttonSVG ), EditorBlock( + id: "core/more", name: "core/more", title: "More", description: "Content before this block will be shown in the excerpt on your archives page.", @@ -187,13 +209,15 @@ extension EditorBlock { // Widget blocks EditorBlock( + id: "core/search", name: "core/search", title: "Search", description: "Help visitors find your content.", category: "widgets", - keywords: ["find", "search"], + keywords: ["find", "search"] ), EditorBlock( + id: "core/archives", name: "core/archives", title: "Archives", description: "Display a date archive of your posts.", @@ -202,6 +226,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/categories", name: "core/categories", title: "Categories", description: "Display a list of all categories.", @@ -212,6 +237,7 @@ extension EditorBlock { // Embed blocks EditorBlock( + id: "core-embed/youtube", name: "core-embed/youtube", title: "YouTube", description: "Embed a YouTube video.", @@ -220,6 +246,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core-embed/twitter", name: "core-embed/twitter", title: "Twitter", description: "Embed a tweet.", @@ -228,6 +255,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core-embed/vimeo", name: "core-embed/vimeo", title: "Vimeo", description: "Embed a Vimeo video.", @@ -236,6 +264,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core-embed/instagram", name: "core-embed/instagram", title: "Instagram", description: "Embed an Instagram post.", @@ -246,6 +275,7 @@ extension EditorBlock { // Additional common blocks EditorBlock( + id: "core/html", name: "core/html", title: "Custom HTML", description: "Add custom HTML code and preview it as you edit.", @@ -254,6 +284,7 @@ extension EditorBlock { icon: codeSVG ), EditorBlock( + id: "core/shortcode", name: "core/shortcode", title: "Shortcode", description: "Insert additional custom elements with WordPress shortcodes.", @@ -262,6 +293,7 @@ extension EditorBlock { icon: nil ), EditorBlock( + id: "core/social-links", name: "core/social-links", title: "Social Icons", description: "Display icons linking to your social media profiles.", diff --git a/src/components/native-block-inserter-button/index.jsx b/src/components/native-block-inserter-button/index.jsx index f521204b9..1bc5ad834 100644 --- a/src/components/native-block-inserter-button/index.jsx +++ b/src/components/native-block-inserter-button/index.jsx @@ -89,13 +89,11 @@ export default function NativeBlockInserterButton() { window.blockInserter = { blocks, destinationBlockName, - insertBlock: ( blockName ) => { - const item = inserterItems.find( - ( i ) => i.name === blockName - ); + insertBlock: ( blockId ) => { + const item = inserterItems.find( ( i ) => i.id === blockId ); if ( ! item ) { debug( - `Block "${ blockName }" not found in inserter items` + `Block with id "${ blockId }" not found in inserter items` ); return false; } diff --git a/src/utils/blocks.js b/src/utils/blocks.js index de1e01ac1..cb6676f45 100644 --- a/src/utils/blocks.js +++ b/src/utils/blocks.js @@ -77,6 +77,7 @@ export function getBlockIcon( item ) { export function serializeBlocksForNative( inserterItems ) { return inserterItems.map( ( item ) => { return { + id: item.id, name: item.name, title: item.title, description: item.description, From 96cf898d8f01033ef69b452313a7d61f18802a38 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 27 Oct 2025 16:19:48 -0400 Subject: [PATCH 2/5] Rename BlockType --- .../Sources/EditorJSMessage.swift | 2 +- .../Sources/EditorViewController.swift | 6 ++ .../Sources/Helpers/BlockIconCache.swift | 4 +- .../Views/BlockInserter/BlockIconView.swift | 2 +- .../BlockInserterBlockView.swift | 4 +- .../BlockInserterSectionView.swift | 4 +- .../BlockInserterView+PreviewData.swift | 68 +++++++++---------- .../BlockInserter/BlockInserterView.swift | 12 ++-- .../BlockInserterViewModel.swift | 14 ++-- .../BlockInserter/BlockType.swift} | 10 +-- 10 files changed, 63 insertions(+), 63 deletions(-) rename ios/Sources/GutenbergKit/Sources/{EditorTypes.swift => Views/BlockInserter/BlockType.swift} (86%) diff --git a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift index 894074738..9eceb5076 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift @@ -67,7 +67,7 @@ struct EditorJSMessage { } struct ShowBlockInserterBody: Decodable { - let blocks: [EditorBlock] + let blocks: [BlockType] let destinationBlockName: String? } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index dccbb6edf..7afb42f2f 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -174,6 +174,12 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro try await webView.evaluateJavaScript("editor.getContent();") as! String } + public struct EditorTitleAndContent: Decodable { + public let title: String + public let content: String + public let changed: Bool + } + /// Returns the current editor title and content. public func getTitleAndContent() async throws -> EditorTitleAndContent { let result = try await webView.evaluateJavaScript("editor.getTitleAndContent();") diff --git a/ios/Sources/GutenbergKit/Sources/Helpers/BlockIconCache.swift b/ios/Sources/GutenbergKit/Sources/Helpers/BlockIconCache.swift index 33d1fb1e8..4a7cafd30 100644 --- a/ios/Sources/GutenbergKit/Sources/Helpers/BlockIconCache.swift +++ b/ios/Sources/GutenbergKit/Sources/Helpers/BlockIconCache.swift @@ -5,7 +5,7 @@ import SVGKit final class BlockIconCache: ObservableObject { var icons: [String: Result] = [:] - func getIcon(for block: EditorBlock) -> SVGKImage? { + func getIcon(for block: BlockType) -> SVGKImage? { if let result = icons[block.id] { return try? result.get() } @@ -14,7 +14,7 @@ final class BlockIconCache: ObservableObject { return try? result.get() } - private func _getIcon(for block: EditorBlock) throws -> SVGKImage { + private func _getIcon(for block: BlockType) throws -> SVGKImage { guard let svg = block.icon, !svg.isEmpty, let source = SVGKSourceString.source(fromContentsOf: svg), diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockIconView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockIconView.swift index d59162445..edd845740 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockIconView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockIconView.swift @@ -2,7 +2,7 @@ import SwiftUI import SVGKit struct BlockIconView: View { - let block: EditorBlock + let block: BlockType let size: CGFloat @EnvironmentObject private var cache: BlockIconCache diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift index ed6990f40..e918b2dc2 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift @@ -2,7 +2,7 @@ import SwiftUI import SVGKit struct BlockInserterBlockView: View { - let block: EditorBlock + let block: BlockType let action: () -> Void @State private var isPressed = false @@ -57,7 +57,7 @@ struct BlockInserterBlockView: View { } private struct BlockDetailedView: View { - let block: EditorBlock + let block: BlockType var body: some View { HStack(spacing: 16) { diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift index d39758866..d1db868cc 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift @@ -4,12 +4,12 @@ struct BlockInserterSection: Identifiable { var id: String { category } let category: String let name: String? - let blocks: [EditorBlock] + let blocks: [BlockType] } struct BlockInserterSectionView: View { let section: BlockInserterSection - let onBlockSelected: (EditorBlock) -> Void + let onBlockSelected: (BlockType) -> Void @ScaledMetric(relativeTo: .largeTitle) private var miniumSize = 80 @ScaledMetric(relativeTo: .largeTitle) private var padding = 20 diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift index 3e6021ae2..bad931f7d 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift @@ -1,10 +1,10 @@ #if DEBUG import Foundation -extension EditorBlock { - static let mocks: [EditorBlock] = [ +extension BlockType { + static let mocks: [BlockType] = [ // Text blocks - EditorBlock( + BlockType( id: "core/paragraph", name: "core/paragraph", title: "Paragraph", @@ -13,7 +13,7 @@ extension EditorBlock { keywords: ["text", "paragraph"], icon: paragraphSVG ), - EditorBlock( + BlockType( id: "core/heading", name: "core/heading", title: "Heading", @@ -22,7 +22,7 @@ extension EditorBlock { keywords: ["title", "heading"], icon: headingSVG ), - EditorBlock( + BlockType( id: "core/list", name: "core/list", title: "List", @@ -31,7 +31,7 @@ extension EditorBlock { keywords: ["bullet", "number", "list"], icon: listSVG ), - EditorBlock( + BlockType( id: "core/quote", name: "core/quote", title: "Quote", @@ -40,7 +40,7 @@ extension EditorBlock { keywords: ["quote", "citation"], icon: quoteSVG ), - EditorBlock( + BlockType( id: "core/code", name: "core/code", title: "Code", @@ -49,7 +49,7 @@ extension EditorBlock { keywords: ["code", "programming"], icon: codeSVG ), - EditorBlock( + BlockType( id: "core/preformatted", name: "core/preformatted", title: "Preformatted", @@ -58,7 +58,7 @@ extension EditorBlock { keywords: ["preformatted", "monospace"], icon: nil ), - EditorBlock( + BlockType( id: "core/pullquote", name: "core/pullquote", title: "Pullquote", @@ -67,7 +67,7 @@ extension EditorBlock { keywords: ["pullquote", "quote"], icon: quoteSVG ), - EditorBlock( + BlockType( id: "core/verse", name: "core/verse", title: "Verse", @@ -76,7 +76,7 @@ extension EditorBlock { keywords: ["poetry", "verse"], icon: nil ), - EditorBlock( + BlockType( id: "core/table", name: "core/table", title: "Table", @@ -87,7 +87,7 @@ extension EditorBlock { ), // Media blocks - EditorBlock( + BlockType( id: "core/image", name: "core/image", title: "Image", @@ -96,7 +96,7 @@ extension EditorBlock { keywords: ["photo", "picture"], icon: imageSVG ), - EditorBlock( + BlockType( id: "core/gallery", name: "core/gallery", title: "Gallery", @@ -105,7 +105,7 @@ extension EditorBlock { keywords: ["images", "photos"], icon: imageSVG ), - EditorBlock( + BlockType( id: "core/audio", name: "core/audio", title: "Audio", @@ -114,7 +114,7 @@ extension EditorBlock { keywords: ["music", "sound", "podcast"], icon: nil ), - EditorBlock( + BlockType( id: "core/video", name: "core/video", title: "Video", @@ -123,7 +123,7 @@ extension EditorBlock { keywords: ["movie", "film"], icon: videoSVG ), - EditorBlock( + BlockType( id: "core/cover", name: "core/cover", title: "Cover", @@ -132,7 +132,7 @@ extension EditorBlock { keywords: ["banner", "hero", "cover"], icon: nil ), - EditorBlock( + BlockType( id: "core/file", name: "core/file", title: "File", @@ -141,7 +141,7 @@ extension EditorBlock { keywords: ["download", "pdf", "document"], icon: nil ), - EditorBlock( + BlockType( id: "core/media-text", name: "core/media-text", title: "Media & Text", @@ -152,7 +152,7 @@ extension EditorBlock { ), // Design blocks - EditorBlock( + BlockType( id: "core/columns", name: "core/columns", title: "Columns", @@ -161,7 +161,7 @@ extension EditorBlock { keywords: ["layout", "columns"], icon: nil ), - EditorBlock( + BlockType( id: "core/group", name: "core/group", title: "Group", @@ -170,7 +170,7 @@ extension EditorBlock { keywords: ["container", "wrapper", "group"], icon: nil ), - EditorBlock( + BlockType( id: "core/separator", name: "core/separator", title: "Separator", @@ -179,7 +179,7 @@ extension EditorBlock { keywords: ["divider", "hr"], icon: nil ), - EditorBlock( + BlockType( id: "core/spacer", name: "core/spacer", title: "Spacer", @@ -188,7 +188,7 @@ extension EditorBlock { keywords: ["space", "gap"], icon: nil ), - EditorBlock( + BlockType( id: "core/buttons", name: "core/buttons", title: "Buttons", @@ -197,7 +197,7 @@ extension EditorBlock { keywords: ["button", "link", "cta"], icon: buttonSVG ), - EditorBlock( + BlockType( id: "core/more", name: "core/more", title: "More", @@ -208,7 +208,7 @@ extension EditorBlock { ), // Widget blocks - EditorBlock( + BlockType( id: "core/search", name: "core/search", title: "Search", @@ -216,7 +216,7 @@ extension EditorBlock { category: "widgets", keywords: ["find", "search"] ), - EditorBlock( + BlockType( id: "core/archives", name: "core/archives", title: "Archives", @@ -225,7 +225,7 @@ extension EditorBlock { keywords: ["archive", "history"], icon: nil ), - EditorBlock( + BlockType( id: "core/categories", name: "core/categories", title: "Categories", @@ -236,7 +236,7 @@ extension EditorBlock { ), // Embed blocks - EditorBlock( + BlockType( id: "core-embed/youtube", name: "core-embed/youtube", title: "YouTube", @@ -245,7 +245,7 @@ extension EditorBlock { keywords: ["video", "youtube"], icon: nil ), - EditorBlock( + BlockType( id: "core-embed/twitter", name: "core-embed/twitter", title: "Twitter", @@ -254,7 +254,7 @@ extension EditorBlock { keywords: ["tweet", "twitter"], icon: nil ), - EditorBlock( + BlockType( id: "core-embed/vimeo", name: "core-embed/vimeo", title: "Vimeo", @@ -263,7 +263,7 @@ extension EditorBlock { keywords: ["video", "vimeo"], icon: nil ), - EditorBlock( + BlockType( id: "core-embed/instagram", name: "core-embed/instagram", title: "Instagram", @@ -274,7 +274,7 @@ extension EditorBlock { ), // Additional common blocks - EditorBlock( + BlockType( id: "core/html", name: "core/html", title: "Custom HTML", @@ -283,7 +283,7 @@ extension EditorBlock { keywords: ["html", "code", "custom"], icon: codeSVG ), - EditorBlock( + BlockType( id: "core/shortcode", name: "core/shortcode", title: "Shortcode", @@ -292,7 +292,7 @@ extension EditorBlock { keywords: ["shortcode", "custom"], icon: nil ), - EditorBlock( + BlockType( id: "core/social-links", name: "core/social-links", title: "Social Icons", diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift index dc0d6b3e6..292ce0b32 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift @@ -3,11 +3,11 @@ import PhotosUI import UIKit struct BlockInserterView: View { - let blocks: [EditorBlock] + let blocks: [BlockType] let destinationBlockName: String? let mediaPicker: MediaPickerController? let presentationContext: MediaPickerPresentationContext - let onBlockSelected: (EditorBlock) -> Void + let onBlockSelected: (BlockType) -> Void let onMediaSelected: ([MediaInfo]) -> Void @StateObject private var viewModel: BlockInserterViewModel @@ -18,11 +18,11 @@ struct BlockInserterView: View { @Environment(\.dismiss) private var dismiss init( - blocks: [EditorBlock], + blocks: [BlockType], destinationBlockName: String?, mediaPicker: MediaPickerController?, presentationContext: MediaPickerPresentationContext, - onBlockSelected: @escaping (EditorBlock) -> Void, + onBlockSelected: @escaping (BlockType) -> Void, onMediaSelected: @escaping ([MediaInfo]) -> Void ) { self.blocks = blocks @@ -85,7 +85,7 @@ struct BlockInserterView: View { // MARK: - Actions - private func insertBlock(_ block: EditorBlock) { + private func insertBlock(_ block: BlockType) { dismiss() onBlockSelected(block) } @@ -97,7 +97,7 @@ struct BlockInserterView: View { #Preview { NavigationStack { BlockInserterView( - blocks: EditorBlock.mocks, + blocks: BlockType.mocks, destinationBlockName: nil, mediaPicker: MockMediaPickerController(), presentationContext: MediaPickerPresentationContext(), diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift index a38416611..2b21bea43 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift @@ -7,11 +7,11 @@ class BlockInserterViewModel: ObservableObject { @Published var searchText = "" @Published private(set) var sections: [BlockInserterSection] = [] - private let blocks: [EditorBlock] + private let blocks: [BlockType] private let allSections: [BlockInserterSection] private var cancellables = Set() - init(blocks: [EditorBlock], destinationBlockName: String?) { + init(blocks: [BlockType], destinationBlockName: String?) { let blocks = blocks.filter { $0.name != "core/missing" } self.blocks = blocks @@ -36,7 +36,7 @@ class BlockInserterViewModel: ObservableObject { sections = allSections } else { sections = allSections.compactMap { section in - let filtered = SearchEngine() + let filtered = SearchEngine() .search(query: searchText, in: section.blocks) return filtered.isEmpty ? nil : BlockInserterSection( category: section.category, @@ -47,7 +47,7 @@ class BlockInserterViewModel: ObservableObject { } } - private static func createSections(from blocks: [EditorBlock], destinationBlockName: String?) -> [BlockInserterSection] { + private static func createSections(from blocks: [BlockType], destinationBlockName: String?) -> [BlockInserterSection] { var sections: [BlockInserterSection] = [] // Separate contextual blocks (specifically allowed in current parent block) @@ -93,7 +93,7 @@ class BlockInserterViewModel: ObservableObject { // MARK: Ordering -private func orderBlocks(_ blocks: [EditorBlock], category: String) -> [EditorBlock] { +private func orderBlocks(_ blocks: [BlockType], category: String) -> [BlockType] { switch category { case "text": return _orderBlocks(blocks, order: [ @@ -128,8 +128,8 @@ private func orderBlocks(_ blocks: [EditorBlock], category: String) -> [EditorBl } } -private func _orderBlocks(_ blocks: [EditorBlock], order: [String]) -> [EditorBlock] { - var orderedBlocks: [EditorBlock] = [] +private func _orderBlocks(_ blocks: [BlockType], order: [String]) -> [BlockType] { + var orderedBlocks: [BlockType] = [] // Add blocks in a predefined order for name in order { diff --git a/ios/Sources/GutenbergKit/Sources/EditorTypes.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift similarity index 86% rename from ios/Sources/GutenbergKit/Sources/EditorTypes.swift rename to ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift index 9c50ff5d2..d616ddf8f 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorTypes.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift @@ -1,6 +1,6 @@ import Foundation -struct EditorBlock: Decodable, Identifiable { +struct BlockType: Decodable, Identifiable { /// Unique identifier for this block variant. Note that this is NOT the same as `name`. /// Multiple blocks can share the same `name` but have different `id` values to represent /// different variants with different initial attributes (e.g., core/embed variants for @@ -17,13 +17,7 @@ struct EditorBlock: Decodable, Identifiable { var parents: [String] = [] } -public struct EditorTitleAndContent: Decodable { - public let title: String - public let content: String - public let changed: Bool -} - -extension EditorBlock: Searchable { +extension BlockType: Searchable { /// Sets the searchable fields in the order of priority func searchableFields() -> [SearchableField] { var fields: [SearchableField] = [] From d8503307156d2b2ef48d3307405760da55066eaa Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 27 Oct 2025 16:26:03 -0400 Subject: [PATCH 3/5] Capitlize ID --- ios/Sources/GutenbergKit/Sources/EditorViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 7afb42f2f..373c0924f 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -274,8 +274,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro present(host, animated: true) } - private func insertBlockFromInserter(_ blockId: String) { - evaluate("window.blockInserter.insertBlock('\(blockId)')") + private func insertBlockFromInserter(_ blockID: String) { + evaluate("window.blockInserter.insertBlock('\(blockID)')") } private func openMediaLibrary(_ config: OpenMediaLibraryAction) { From 3ad08f5403f28a9de637ef9ad73bfd667c7ef7e3 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 29 Oct 2025 15:09:57 -0400 Subject: [PATCH 4/5] Update src/components/native-block-inserter-button/index.jsx Co-authored-by: David Calhoun --- src/components/native-block-inserter-button/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/native-block-inserter-button/index.jsx b/src/components/native-block-inserter-button/index.jsx index 1bc5ad834..e0933d17b 100644 --- a/src/components/native-block-inserter-button/index.jsx +++ b/src/components/native-block-inserter-button/index.jsx @@ -93,7 +93,7 @@ export default function NativeBlockInserterButton() { const item = inserterItems.find( ( i ) => i.id === blockId ); if ( ! item ) { debug( - `Block with id "${ blockId }" not found in inserter items` + `Block with ID "${ blockId }" not found in inserter items` ); return false; } From 4b459a7166653a74f49c186e52bb84abe3db8087 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Wed, 29 Oct 2025 15:20:22 -0400 Subject: [PATCH 5/5] Native Inserter: Improve ordering of blocks and more (#206) * Add a note about serializeBlocksForNative * Remove now redundant core/missing check * Move ordering to the JS land * Further refactoring * Pass displayName for Text * Localize categories * Extend most used blocks * Add gbk-most-used as a separate section * Add support for disabling blocks * Collapse long sections * Update the order of embeds * Fix linter errors --- .../Sources/EditorJSMessage.swift | 3 +- .../Sources/EditorViewController.swift | 3 +- .../BlockInserterBlockView.swift | 1 + .../BlockInserterSectionView.swift | 44 +++- .../BlockInserter/BlockInserterView.swift | 16 +- .../BlockInserterViewModel.swift | 122 +-------- .../native-block-inserter-button/index.jsx | 18 +- src/utils/blocks.js | 235 +++++++++++++++++- src/utils/bridge.js | 10 +- 9 files changed, 300 insertions(+), 152 deletions(-) diff --git a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift index 9eceb5076..cd3934565 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift @@ -67,7 +67,6 @@ struct EditorJSMessage { } struct ShowBlockInserterBody: Decodable { - let blocks: [BlockType] - let destinationBlockName: String? + let sections: [BlockInserterSection] } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 373c0924f..e52a6a938 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -256,8 +256,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro let host = UIHostingController(rootView: NavigationStack { BlockInserterView( - blocks: data.blocks, - destinationBlockName: data.destinationBlockName, + sections: data.sections, mediaPicker: mediaPicker, presentationContext: context, onBlockSelected: { [weak self] block in diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift index e918b2dc2..62b3dc0a7 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterBlockView.swift @@ -29,6 +29,7 @@ struct BlockInserterBlockView: View { .padding(.horizontal, 4) } .buttonStyle(.plain) + .disabled(block.isDisabled) .frame(maxWidth: .infinity, alignment: .center) .contextMenu { Button { diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift index d1db868cc..d69a39d92 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct BlockInserterSection: Identifiable { +struct BlockInserterSection: Identifiable, Decodable { var id: String { category } let category: String let name: String? @@ -13,6 +13,20 @@ struct BlockInserterSectionView: View { @ScaledMetric(relativeTo: .largeTitle) private var miniumSize = 80 @ScaledMetric(relativeTo: .largeTitle) private var padding = 20 + @State private var isExpanded = false + + private let initialDisplayCount = 16 + + private var displayedBlocks: [BlockType] { + if !isExpanded && section.blocks.count > initialDisplayCount { + return Array(section.blocks.prefix(initialDisplayCount)) + } + return section.blocks + } + + private var hasMoreBlocks: Bool { + section.blocks.count > initialDisplayCount + } var body: some View { VStack(alignment: .leading, spacing: 20) { @@ -24,6 +38,9 @@ struct BlockInserterSectionView: View { .frame(maxWidth: .infinity, alignment: .leading) } grid + if hasMoreBlocks { + toggleButton + } } .padding(.top, section.name != nil ? 20 : 24) .padding(.bottom, 10) @@ -32,7 +49,7 @@ struct BlockInserterSectionView: View { private var grid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: miniumSize, maximum: miniumSize * 1.5), spacing: 0)]) { - ForEach(section.blocks) { block in + ForEach(displayedBlocks) { block in BlockInserterBlockView(block: block) { onBlockSelected(block) } @@ -40,4 +57,27 @@ struct BlockInserterSectionView: View { } .padding(.horizontal, 12) } + + private var toggleButton: some View { + Button { + withAnimation { + isExpanded.toggle() + } + } label: { + HStack { + // TODO: CMM-874 add localization + Text(isExpanded ? "Show Less" : "Show More") + .font(.subheadline) + .fontWeight(.medium) + .foregroundStyle(Color.secondary) + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundStyle(Color.primary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + } + .buttonStyle(.plain) + .padding(.horizontal, 12) + } } diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift index 292ce0b32..663c98c54 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift @@ -3,8 +3,7 @@ import PhotosUI import UIKit struct BlockInserterView: View { - let blocks: [BlockType] - let destinationBlockName: String? + let sections: [BlockInserterSection] let mediaPicker: MediaPickerController? let presentationContext: MediaPickerPresentationContext let onBlockSelected: (BlockType) -> Void @@ -18,21 +17,19 @@ struct BlockInserterView: View { @Environment(\.dismiss) private var dismiss init( - blocks: [BlockType], - destinationBlockName: String?, + sections: [BlockInserterSection], mediaPicker: MediaPickerController?, presentationContext: MediaPickerPresentationContext, onBlockSelected: @escaping (BlockType) -> Void, onMediaSelected: @escaping ([MediaInfo]) -> Void ) { - self.blocks = blocks - self.destinationBlockName = destinationBlockName + self.sections = sections self.mediaPicker = mediaPicker self.presentationContext = presentationContext self.onBlockSelected = onBlockSelected self.onMediaSelected = onMediaSelected - let viewModel = BlockInserterViewModel(blocks: blocks, destinationBlockName: destinationBlockName) + let viewModel = BlockInserterViewModel(sections: sections) self._viewModel = StateObject(wrappedValue: viewModel) } @@ -97,8 +94,9 @@ struct BlockInserterView: View { #Preview { NavigationStack { BlockInserterView( - blocks: BlockType.mocks, - destinationBlockName: nil, + sections: [ + BlockInserterSection(category: "text", name: "Text", blocks: BlockType.mocks) + ], mediaPicker: MockMediaPickerController(), presentationContext: MediaPickerPresentationContext(), onBlockSelected: { diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift index 2b21bea43..6eb0e80be 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterViewModel.swift @@ -7,21 +7,16 @@ class BlockInserterViewModel: ObservableObject { @Published var searchText = "" @Published private(set) var sections: [BlockInserterSection] = [] - private let blocks: [BlockType] private let allSections: [BlockInserterSection] private var cancellables = Set() - init(blocks: [BlockType], destinationBlockName: String?) { - let blocks = blocks.filter { $0.name != "core/missing" } - - self.blocks = blocks - - self.allSections = BlockInserterViewModel.createSections(from: blocks, destinationBlockName: destinationBlockName) - self.sections = allSections + init(sections: [BlockInserterSection]) { + self.allSections = sections + self.sections = sections setupSearchObserver() } - + private func setupSearchObserver() { $searchText .debounce(for: .milliseconds(200), scheduler: RunLoop.main) @@ -46,113 +41,4 @@ class BlockInserterViewModel: ObservableObject { } } } - - private static func createSections(from blocks: [BlockType], destinationBlockName: String?) -> [BlockInserterSection] { - var sections: [BlockInserterSection] = [] - - // Separate contextual blocks (specifically allowed in current parent block) - // A block is contextual if the destination block name is in its parents array - let contextualBlocks = blocks.filter { block in - guard let destinationBlockName = destinationBlockName else { return false } - return !block.parents.isEmpty && block.parents.contains(destinationBlockName) - } - - // Add contextual section at the top if there are contextual blocks - if !contextualBlocks.isEmpty { - sections.append(BlockInserterSection(category: "gbk-contextual", name: nil, blocks: contextualBlocks)) - } - - // Group regular blocks by category - let blocksByCategory = Dictionary(grouping: blocks) { - $0.category?.lowercased() ?? "common" - } - - let categories = Constants.orderedCategories - - // Add known categories in a predefined order - for (category, name) in categories { - if let blocks = blocksByCategory[category] { - let sortedBlocks = orderBlocks(blocks, category: category) - // Use nil for text category, otherwise use the display name - let displayName = (category == "text" && contextualBlocks.isEmpty) ? nil : name - sections.append(BlockInserterSection(category: category, name: displayName, blocks: sortedBlocks)) - } - } - - // Add any remaining categories - for (category, blocks) in blocksByCategory { - let isStandardCategory = categories.contains { $0.key == category } - if !isStandardCategory { - sections.append(BlockInserterSection(category: category, name: category.capitalized, blocks: blocks)) - } - } - - return sections - } -} - -// MARK: Ordering - -private func orderBlocks(_ blocks: [BlockType], category: String) -> [BlockType] { - switch category { - case "text": - return _orderBlocks(blocks, order: [ - "core/paragraph", - "core/heading", - "core/list", - "core/list-item", - "core/quote", - "core/code", - "core/preformatted", - "core/verse", - "core/table" - ]) - case "media": - return _orderBlocks(blocks, order: [ - "core/image", - "core/video", - "core/gallery", - "core/embed", - "core/audio", - "core/file" - ]) - case "design": - return _orderBlocks(blocks, order: [ - "core/separator", - "core/spacer", - "core/columns", - "core/column" - ]) - default: - return blocks - } -} - -private func _orderBlocks(_ blocks: [BlockType], order: [String]) -> [BlockType] { - var orderedBlocks: [BlockType] = [] - - // Add blocks in a predefined order - for name in order { - if let block = blocks.first(where: { $0.name == name }) { - orderedBlocks.append(block) - } - } - - // Add remaining blocks in their original order - let remainingBlocks = blocks.filter { block in - !order.contains(block.name) - } - - return orderedBlocks + remainingBlocks -} - -private enum Constants { - static let orderedCategories: [(key: String, displayName: String)] = [ - ("text", "Text"), - ("media", "Media"), - ("design", "Design"), - ("widgets", "Widgets"), - ("theme", "Theme"), - ("embed", "Embeds") - ] } diff --git a/src/components/native-block-inserter-button/index.jsx b/src/components/native-block-inserter-button/index.jsx index e0933d17b..2efb15f0e 100644 --- a/src/components/native-block-inserter-button/index.jsx +++ b/src/components/native-block-inserter-button/index.jsx @@ -32,7 +32,7 @@ import useInsertionPoint from '@wordpress/block-editor/build-module/components/i import useBlockTypesState from '@wordpress/block-editor/build-module/components/inserter/hooks/use-block-types-state'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { debug } from '../../utils/logger'; -import { serializeBlocksForNative } from '../../utils/blocks'; +import { preprocessBlockTypesForNativeInserter } from '../../utils/blocks'; import { showBlockInserter } from '../../utils/bridge'; /** @@ -74,21 +74,25 @@ export default function NativeBlockInserterButton() { selectBlockOnInsert: true, } ); - const [ inserterItems, , , onSelectItem ] = useBlockTypesState( + const [ inserterItems, categories, , onSelectItem ] = useBlockTypesState( destinationRootClientId, onInsertBlocks, false // isQuick ); - // Serialize blocks for native consumption - const blocks = serializeBlocksForNative( inserterItems ); + // Preprocess blocks into sections for native consumption + // Categories are passed to get localized category names + const sections = preprocessBlockTypesForNativeInserter( + inserterItems, + destinationBlockName, + categories + ); // Expose the current inserter state globally for native access // This automatically stays in sync with editor state via hooks useEffect( () => { window.blockInserter = { - blocks, - destinationBlockName, + sections, insertBlock: ( blockId ) => { const item = inserterItems.find( ( i ) => i.id === blockId ); if ( ! item ) { @@ -112,7 +116,7 @@ export default function NativeBlockInserterButton() { return () => { delete window.blockInserter; }; - }, [ blocks, destinationBlockName, inserterItems, onSelectItem ] ); + }, [ sections, inserterItems, categories, onSelectItem ] ); return (