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 1bc5ad834..da851aa96 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 (