diff --git a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift index febb2dce9..cd3934565 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift @@ -67,6 +67,6 @@ struct EditorJSMessage { } struct ShowBlockInserterBody: Decodable { - let blocks: [EditorBlock] + let sections: [BlockInserterSection] } } diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 8eaec687f..e52a6a938 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();") @@ -245,16 +251,16 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro // MARK: - Internal (Block Inserter) - private func showBlockInserter(blocks: [EditorBlock]) { + private func showBlockInserter(data: EditorJSMessage.ShowBlockInserterBody) { let context = MediaPickerPresentationContext() let host = UIHostingController(rootView: NavigationStack { BlockInserterView( - blocks: blocks, + sections: data.sections, mediaPicker: mediaPicker, presentationContext: context, - onBlockSelected: { - print("insert blocks:", $0) + onBlockSelected: { [weak self] block in + self?.insertBlockFromInserter(block.id) }, onMediaSelected: { print("insert media:", $0) @@ -267,6 +273,10 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro present(host, animated: true) } + private func insertBlockFromInserter(_ blockID: String) { + evaluate("window.blockInserter.insertBlock('\(blockID)')") + } + private func openMediaLibrary(_ config: OpenMediaLibraryAction) { delegate?.editor(self, didRequestMediaFromSiteMediaLibrary: config) } @@ -309,7 +319,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro delegate?.editor(self, didLogException: editorException) case .showBlockInserter: let body = try message.decode(EditorJSMessage.ShowBlockInserterBody.self) - showBlockInserter(blocks: body.blocks) + showBlockInserter(data: body) case .openMediaLibrary: let config = try message.decode(OpenMediaLibraryAction.self) openMediaLibrary(config) 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..62b3dc0a7 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 @@ -29,6 +29,7 @@ struct BlockInserterBlockView: View { .padding(.horizontal, 4) } .buttonStyle(.plain) + .disabled(block.isDisabled) .frame(maxWidth: .infinity, alignment: .center) .contextMenu { Button { @@ -57,7 +58,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 4b4d4b905..d69a39d92 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterSectionView.swift @@ -1,38 +1,55 @@ import SwiftUI -struct BlockInserterSection: Identifiable { +struct BlockInserterSection: Identifiable, Decodable { var id: String { category } let category: String - let name: String - let blocks: [EditorBlock] + let name: String? + 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 + @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) { - if section.category != "text" { - Text(section.name) + if let name = section.name { + Text(name) .font(.headline) .foregroundStyle(Color.secondary) .padding(.leading, padding) .frame(maxWidth: .infinity, alignment: .leading) } grid + if hasMoreBlocks { + toggleButton + } } - .padding(.top, section.category != "text" ? 20 : 24) + .padding(.top, section.name != nil ? 20 : 24) .padding(.bottom, 10) .cardStyle() } 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+PreviewData.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView+PreviewData.swift index d6c025aa6..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,11 @@ #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", description: "Start with the basic building block of all narrative.", @@ -12,7 +13,8 @@ extension EditorBlock { keywords: ["text", "paragraph"], icon: paragraphSVG ), - EditorBlock( + BlockType( + id: "core/heading", name: "core/heading", title: "Heading", description: "Introduce new sections and organize content to help visitors find what they need.", @@ -20,7 +22,8 @@ extension EditorBlock { keywords: ["title", "heading"], icon: headingSVG ), - EditorBlock( + BlockType( + id: "core/list", name: "core/list", title: "List", description: "Create a bulleted or numbered list.", @@ -28,7 +31,8 @@ extension EditorBlock { keywords: ["bullet", "number", "list"], icon: listSVG ), - EditorBlock( + BlockType( + id: "core/quote", name: "core/quote", title: "Quote", description: "Give quoted text visual emphasis.", @@ -36,7 +40,8 @@ extension EditorBlock { keywords: ["quote", "citation"], icon: quoteSVG ), - EditorBlock( + BlockType( + id: "core/code", name: "core/code", title: "Code", description: "Display code snippets that respect your spacing and tabs.", @@ -44,7 +49,8 @@ extension EditorBlock { keywords: ["code", "programming"], icon: codeSVG ), - EditorBlock( + BlockType( + id: "core/preformatted", name: "core/preformatted", title: "Preformatted", description: "Add text that respects your spacing and tabs, and also allows styling.", @@ -52,7 +58,8 @@ extension EditorBlock { keywords: ["preformatted", "monospace"], icon: nil ), - EditorBlock( + BlockType( + id: "core/pullquote", name: "core/pullquote", title: "Pullquote", description: "Give special visual emphasis to a quote from your text.", @@ -60,7 +67,8 @@ extension EditorBlock { keywords: ["pullquote", "quote"], icon: quoteSVG ), - EditorBlock( + BlockType( + id: "core/verse", name: "core/verse", title: "Verse", description: "Insert poetry. Use special spacing formats. Or quote song lyrics.", @@ -68,7 +76,8 @@ extension EditorBlock { keywords: ["poetry", "verse"], icon: nil ), - EditorBlock( + BlockType( + id: "core/table", name: "core/table", title: "Table", description: "Create structured content in rows and columns to display information.", @@ -78,7 +87,8 @@ extension EditorBlock { ), // Media blocks - EditorBlock( + BlockType( + id: "core/image", name: "core/image", title: "Image", description: "Insert an image to make a visual statement.", @@ -86,7 +96,8 @@ extension EditorBlock { keywords: ["photo", "picture"], icon: imageSVG ), - EditorBlock( + BlockType( + id: "core/gallery", name: "core/gallery", title: "Gallery", description: "Display multiple images in a rich gallery.", @@ -94,7 +105,8 @@ extension EditorBlock { keywords: ["images", "photos"], icon: imageSVG ), - EditorBlock( + BlockType( + id: "core/audio", name: "core/audio", title: "Audio", description: "Embed a simple audio player.", @@ -102,7 +114,8 @@ extension EditorBlock { keywords: ["music", "sound", "podcast"], icon: nil ), - EditorBlock( + BlockType( + id: "core/video", name: "core/video", title: "Video", description: "Embed a video from your media library or upload a new one.", @@ -110,7 +123,8 @@ extension EditorBlock { keywords: ["movie", "film"], icon: videoSVG ), - EditorBlock( + BlockType( + id: "core/cover", name: "core/cover", title: "Cover", description: "Add an image or video with a text overlay.", @@ -118,7 +132,8 @@ extension EditorBlock { keywords: ["banner", "hero", "cover"], icon: nil ), - EditorBlock( + BlockType( + id: "core/file", name: "core/file", title: "File", description: "Add a link to a downloadable file.", @@ -126,7 +141,8 @@ extension EditorBlock { keywords: ["download", "pdf", "document"], icon: nil ), - EditorBlock( + BlockType( + id: "core/media-text", name: "core/media-text", title: "Media & Text", description: "Set media and words side-by-side for a richer layout.", @@ -136,7 +152,8 @@ extension EditorBlock { ), // Design blocks - EditorBlock( + BlockType( + id: "core/columns", name: "core/columns", title: "Columns", description: "Display content in multiple columns.", @@ -144,7 +161,8 @@ extension EditorBlock { keywords: ["layout", "columns"], icon: nil ), - EditorBlock( + BlockType( + id: "core/group", name: "core/group", title: "Group", description: "Gather blocks in a container.", @@ -152,7 +170,8 @@ extension EditorBlock { keywords: ["container", "wrapper", "group"], icon: nil ), - EditorBlock( + BlockType( + id: "core/separator", name: "core/separator", title: "Separator", description: "Create a break between ideas or sections.", @@ -160,7 +179,8 @@ extension EditorBlock { keywords: ["divider", "hr"], icon: nil ), - EditorBlock( + BlockType( + id: "core/spacer", name: "core/spacer", title: "Spacer", description: "Add white space between blocks.", @@ -168,7 +188,8 @@ extension EditorBlock { keywords: ["space", "gap"], icon: nil ), - EditorBlock( + BlockType( + id: "core/buttons", name: "core/buttons", title: "Buttons", description: "Prompt visitors to take action with a group of button-style links.", @@ -176,7 +197,8 @@ extension EditorBlock { keywords: ["button", "link", "cta"], icon: buttonSVG ), - EditorBlock( + BlockType( + id: "core/more", name: "core/more", title: "More", description: "Content before this block will be shown in the excerpt on your archives page.", @@ -186,14 +208,16 @@ extension EditorBlock { ), // Widget blocks - EditorBlock( + BlockType( + id: "core/search", name: "core/search", title: "Search", description: "Help visitors find your content.", category: "widgets", - keywords: ["find", "search"], + keywords: ["find", "search"] ), - EditorBlock( + BlockType( + id: "core/archives", name: "core/archives", title: "Archives", description: "Display a date archive of your posts.", @@ -201,7 +225,8 @@ extension EditorBlock { keywords: ["archive", "history"], icon: nil ), - EditorBlock( + BlockType( + id: "core/categories", name: "core/categories", title: "Categories", description: "Display a list of all categories.", @@ -211,7 +236,8 @@ extension EditorBlock { ), // Embed blocks - EditorBlock( + BlockType( + id: "core-embed/youtube", name: "core-embed/youtube", title: "YouTube", description: "Embed a YouTube video.", @@ -219,7 +245,8 @@ extension EditorBlock { keywords: ["video", "youtube"], icon: nil ), - EditorBlock( + BlockType( + id: "core-embed/twitter", name: "core-embed/twitter", title: "Twitter", description: "Embed a tweet.", @@ -227,7 +254,8 @@ extension EditorBlock { keywords: ["tweet", "twitter"], icon: nil ), - EditorBlock( + BlockType( + id: "core-embed/vimeo", name: "core-embed/vimeo", title: "Vimeo", description: "Embed a Vimeo video.", @@ -235,7 +263,8 @@ extension EditorBlock { keywords: ["video", "vimeo"], icon: nil ), - EditorBlock( + BlockType( + id: "core-embed/instagram", name: "core-embed/instagram", title: "Instagram", description: "Embed an Instagram post.", @@ -245,7 +274,8 @@ extension EditorBlock { ), // Additional common blocks - EditorBlock( + BlockType( + id: "core/html", name: "core/html", title: "Custom HTML", description: "Add custom HTML code and preview it as you edit.", @@ -253,7 +283,8 @@ extension EditorBlock { keywords: ["html", "code", "custom"], icon: codeSVG ), - EditorBlock( + BlockType( + id: "core/shortcode", name: "core/shortcode", title: "Shortcode", description: "Insert additional custom elements with WordPress shortcodes.", @@ -261,7 +292,8 @@ extension EditorBlock { keywords: ["shortcode", "custom"], icon: nil ), - EditorBlock( + BlockType( + id: "core/social-links", name: "core/social-links", title: "Social Icons", description: "Display icons linking to your social media profiles.", diff --git a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift index 50b376d74..663c98c54 100644 --- a/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockInserterView.swift @@ -3,9 +3,10 @@ import PhotosUI import UIKit struct BlockInserterView: View { + let sections: [BlockInserterSection] let mediaPicker: MediaPickerController? let presentationContext: MediaPickerPresentationContext - let onBlockSelected: (EditorBlock) -> Void + let onBlockSelected: (BlockType) -> Void let onMediaSelected: ([MediaInfo]) -> Void @StateObject private var viewModel: BlockInserterViewModel @@ -16,18 +17,20 @@ struct BlockInserterView: View { @Environment(\.dismiss) private var dismiss init( - blocks: [EditorBlock], + sections: [BlockInserterSection], mediaPicker: MediaPickerController?, presentationContext: MediaPickerPresentationContext, - onBlockSelected: @escaping (EditorBlock) -> Void, + onBlockSelected: @escaping (BlockType) -> Void, onMediaSelected: @escaping ([MediaInfo]) -> Void ) { + self.sections = sections self.mediaPicker = mediaPicker self.presentationContext = presentationContext self.onBlockSelected = onBlockSelected self.onMediaSelected = onMediaSelected - self._viewModel = StateObject(wrappedValue: BlockInserterViewModel(blocks: blocks)) + let viewModel = BlockInserterViewModel(sections: sections) + self._viewModel = StateObject(wrappedValue: viewModel) } var body: some View { @@ -79,7 +82,7 @@ struct BlockInserterView: View { // MARK: - Actions - private func insertBlock(_ block: EditorBlock) { + private func insertBlock(_ block: BlockType) { dismiss() onBlockSelected(block) } @@ -91,7 +94,9 @@ struct BlockInserterView: View { #Preview { NavigationStack { BlockInserterView( - blocks: EditorBlock.mocks, + 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 934e981bd..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: [EditorBlock] private let allSections: [BlockInserterSection] private var cancellables = Set() - init(blocks: [EditorBlock]) { - let blocks = blocks.filter { $0.name != "core/missing" } - - self.blocks = blocks - - self.allSections = BlockInserterViewModel.createSections(from: blocks) - self.sections = allSections + init(sections: [BlockInserterSection]) { + self.allSections = sections + self.sections = sections setupSearchObserver() } - + private func setupSearchObserver() { $searchText .debounce(for: .milliseconds(200), scheduler: RunLoop.main) @@ -36,7 +31,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, @@ -46,98 +41,4 @@ class BlockInserterViewModel: ObservableObject { } } } - - private static func createSections(from blocks: [EditorBlock]) -> [BlockInserterSection] { - let blocks = Dictionary(grouping: blocks) { - $0.category?.lowercased() ?? "common" - } - - var sections: [BlockInserterSection] = [] - - let categories = Constants.orderedCategories - - // Add known categories in a predefined order - for (category, name) in categories { - if let blocks = blocks[category] { - let sortedBlocks = orderBlocks(blocks, category: category) - sections.append(BlockInserterSection(category: category, name: name, blocks: sortedBlocks)) - } - } - - // Add any remaining categories - for (category, blocks) in blocks { - 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: [EditorBlock], category: String) -> [EditorBlock] { - 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: [EditorBlock], order: [String]) -> [EditorBlock] { - var orderedBlocks: [EditorBlock] = [] - - // 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/ios/Sources/GutenbergKit/Sources/EditorTypes.swift b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift similarity index 68% rename from ios/Sources/GutenbergKit/Sources/EditorTypes.swift rename to ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift index b3766b378..d616ddf8f 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorTypes.swift +++ b/ios/Sources/GutenbergKit/Sources/Views/BlockInserter/BlockType.swift @@ -1,23 +1,23 @@ import Foundation -struct EditorBlock: Decodable, Identifiable { - var id: String { name } - +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 + /// YouTube, Vimeo, etc.). + let id: String let name: String let title: String? let description: String? let category: String? let keywords: [String]? var icon: String? + var frecency: Double = 0.0 + var isDisabled = false + 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] = [] diff --git a/src/components/editor-toolbar/index.jsx b/src/components/editor-toolbar/index.jsx index 7c73eca82..81a0a0734 100644 --- a/src/components/editor-toolbar/index.jsx +++ b/src/components/editor-toolbar/index.jsx @@ -17,7 +17,7 @@ import { ToolbarButton, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { close, cog, plus } from '@wordpress/icons'; +import { close, cog } from '@wordpress/icons'; import clsx from 'clsx'; import { store as editorStore } from '@wordpress/editor'; @@ -27,7 +27,8 @@ import { store as editorStore } from '@wordpress/editor'; import './style.scss'; import { useModalize } from './use-modalize'; import { useModalDialogState } from '../editor/use-modal-dialog-state'; -import { showBlockInserter, getGBKit } from '../../utils/bridge'; +import { getGBKit } from '../../utils/bridge'; +import NativeBlockInserterButton from '../native-block-inserter-button'; /** * Renders the editor toolbar containing block-related actions. @@ -81,17 +82,7 @@ const EditorToolbar = ( { className } ) => { const classes = clsx( 'gutenberg-kit-editor-toolbar', className ); const addBlockButton = enableNativeBlockInserter ? ( - { - if ( isInserterOpened ) { - setIsInserterOpened( false ); - } - showBlockInserter(); - } } - className="gutenberg-kit-add-block-button" - /> + ) : ( { + const { getSelectedBlockClientId, getBlockRootClientId, getBlock } = + select( blockEditorStore ); + const clientId = getSelectedBlockClientId(); + // Get the parent block's client ID + const parentClientId = clientId + ? getBlockRootClientId( clientId ) + : null; + // Get the parent block object to extract its name + const parentBlock = parentClientId + ? getBlock( parentClientId ) + : null; + return { + selectedBlockClientId: clientId, + destinationBlockName: parentBlock?.name || null, + }; + }, + [] + ); + + // When cursor is in title, selectedBlockClientId is null. + // Use undefined to insert at the beginning of content. + const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { + rootClientId: selectedBlockClientId ?? undefined, + isAppender: false, + selectBlockOnInsert: true, + } ); + + const [ inserterItems, categories, , onSelectItem ] = useBlockTypesState( + destinationRootClientId, + onInsertBlocks, + false // isQuick + ); + + // 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 = { + sections, + insertBlock: ( blockId ) => { + const item = inserterItems.find( ( i ) => i.id === blockId ); + if ( ! item ) { + debug( + `Block with ID "${ blockId }" not found in inserter items` + ); + return false; + } + + try { + // Use the hook's onSelectItem which handles all insertion logic + onSelectItem( item ); + return true; + } catch ( error ) { + debug( 'Failed to insert block:', error ); + return false; + } + }, + }; + + return () => { + delete window.blockInserter; + }; + }, [ sections, inserterItems, categories, onSelectItem ] ); + + return ( +