Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions ios/Sources/GutenbergKit/Sources/EditorJSMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ struct EditorJSMessage {
}

struct ShowBlockInserterBody: Decodable {
let blocks: [BlockType]
let destinationBlockName: String?
let sections: [BlockInserterSection]
}
}
3 changes: 1 addition & 2 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct BlockInserterBlockView: View {
.padding(.horizontal, 4)
}
.buttonStyle(.plain)
.disabled(block.isDisabled)
.frame(maxWidth: .infinity, alignment: .center)
.contextMenu {
Button {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

struct BlockInserterSection: Identifiable {
struct BlockInserterSection: Identifiable, Decodable {
var id: String { category }
let category: String
let name: String?
Expand All @@ -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) {
Expand All @@ -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)
Expand All @@ -32,12 +49,35 @@ 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)
}
}
}
.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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyCancellable>()

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)
Expand All @@ -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")
]
}
18 changes: 11 additions & 7 deletions src/components/native-block-inserter-button/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 ) {
Expand All @@ -112,7 +116,7 @@ export default function NativeBlockInserterButton() {
return () => {
delete window.blockInserter;
};
}, [ blocks, destinationBlockName, inserterItems, onSelectItem ] );
}, [ sections, inserterItems, categories, onSelectItem ] );

return (
<Button
Expand Down
Loading