PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / blocks / element-bridge.tsx

element-bridge.tsx in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/blocks/element-bridge.tsx

247 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ComponentType, useEffect, useMemo, useRef } from "react";
2 import {
3 store as blockEditorStore,
4 useBlockProps,
5 } from "@wordpress/block-editor";
6 import {
7 BlockEditProps,
8 createBlock,
9 registerBlockType,
10 } from "@wordpress/blocks";
11 import { dispatch as dataDispatch, select as dataSelect } from "@wordpress/data";
12 import { create } from "zustand";
13
14 import { CellKey } from "../attributes";
15 import { TableStoreContext } from "../store";
16 import { ElementBindings } from "../dynamic-data/types";
17
18 /**
19 * Runs an existing (store-coupled) element component as a real block's edit.
20 *
21 * The component sees a patched table store whose element-update actions
22 * route into setAttributes, and whose selection mirrors the block's native
23 * selection. Element components only touch a tiny store surface
24 * (sortPreviewMode, isElementSelected, setSelectedElement,
25 * updateCellElement and the selected-element update actions), so the bridge
26 * fakes exactly that.
27 */
28
29 const BRIDGE_COORDS = "0,0" as CellKey;
30
31 export interface BridgedElementProps {
32 attributes: Record<string, unknown>;
33 bindings?: ElementBindings;
34 cellCoords: CellKey;
35 elementIndex: number;
36 }
37
38 interface BridgeRefs {
39 attributes: Record<string, unknown>;
40 setAttributes: (attrs: Record<string, unknown>) => void;
41 isSelected: boolean;
42 clientId: string;
43 elementName: string;
44 }
45
46 function syntheticCells(refs: { current: BridgeRefs }) {
47 const { bindings, ...attributes } = refs.current.attributes;
48 return {
49 [BRIDGE_COORDS]: {
50 elements: [
51 {
52 name: refs.current.elementName,
53 attributes,
54 ...(bindings ? { bindings } : {}),
55 },
56 ],
57 },
58 };
59 }
60
61 function createBridgeStore(refs: { current: BridgeRefs }) {
62 const be = () => dataDispatch(blockEditorStore) as any;
63
64 return create(() => ({
65 // Reads the reused components perform.
66 sortPreviewMode: false,
67 searchTerm: "",
68 table: { search: undefined },
69 cells: syntheticCells(refs),
70
71 isElementSelected: () => refs.current.isSelected,
72 // Native selection is the block's own selection; clicking the
73 // element already selects the block, so these are no-ops.
74 setSelectedElement: () => {},
75 clearSelectedElement: () => {},
76
77 updateCellElement: (
78 _coords: CellKey,
79 _index: number,
80 updates: {
81 attributes?: Record<string, unknown>;
82 bindings?: ElementBindings;
83 }
84 ) => {
85 const next: Record<string, unknown> = {
86 ...(updates.attributes ?? {}),
87 };
88 if (updates.bindings !== undefined) {
89 next.bindings = updates.bindings;
90 }
91 refs.current.setAttributes(next);
92 },
93
94 updateSelectedElementAttrs: (attrs: Record<string, unknown>) => {
95 refs.current.setAttributes(attrs);
96 },
97
98 updateSelectedElementStyles: (styles: Record<string, unknown>) => {
99 refs.current.setAttributes({
100 styles: {
101 ...((refs.current.attributes.styles as object) ?? {}),
102 ...styles,
103 },
104 });
105 },
106
107 updateSelectedElementBindings: (
108 bindings: ElementBindings | undefined
109 ) => {
110 refs.current.setAttributes({ bindings });
111 },
112
113 // Element structure ops route to native block operations.
114 removeElementFromCell: () => {
115 be().removeBlocks([refs.current.clientId]);
116 },
117
118 duplicateElementInCell: () => {
119 const select = dataSelect(blockEditorStore) as any;
120 const parent = select.getBlockRootClientId(refs.current.clientId);
121 be().duplicateBlocks([refs.current.clientId]);
122 void parent;
123 },
124
125 insertElementInCell: (
126 _coords: CellKey,
127 _index: number,
128 element: {
129 name: string;
130 attributes?: Record<string, unknown>;
131 bindings?: ElementBindings;
132 }
133 ) => {
134 const select = dataSelect(blockEditorStore) as any;
135 const parent = select.getBlockRootClientId(refs.current.clientId);
136 if (!parent) {
137 return;
138 }
139 const ownIndex = select.getBlockIndex(refs.current.clientId);
140 const attrs: Record<string, unknown> = {
141 ...(element.attributes ?? {}),
142 };
143 if (element.bindings) {
144 attrs.bindings = element.bindings;
145 }
146 be().insertBlocks(
147 createBlock(`tableberg/${element.name}`, attrs),
148 ownIndex + 1,
149 parent
150 );
151 },
152
153 reorderElementsInCell: () => {
154 // Native List View drag handles element reordering.
155 },
156 }));
157 }
158
159 export interface NativeElementBlockConfig {
160 metadata: {
161 name: string;
162 title: string;
163 [key: string]: unknown;
164 };
165 icon: unknown;
166 /** The existing element component (free or pro). */
167 component: ComponentType<BridgedElementProps>;
168 }
169
170 /**
171 * Wraps a store-coupled element component as a block `edit`. Blocks that own
172 * a block.json use this directly; blocks registered wholly from JS go through
173 * registerNativeElementBlock below.
174 */
175 export function createBridgedElementEdit(
176 blockName: string,
177 Component: ComponentType<BridgedElementProps>
178 ) {
179 const elementName = blockName.replace(/^tableberg\//, "");
180
181 return function BridgedEdit(
182 props: BlockEditProps<Record<string, unknown>> &
183 Record<string, unknown>
184 ) {
185 const { attributes, setAttributes, isSelected, clientId } = props;
186
187 const refs = useRef<BridgeRefs>({
188 attributes,
189 setAttributes,
190 isSelected,
191 clientId,
192 elementName,
193 });
194 refs.current = {
195 attributes,
196 setAttributes,
197 isSelected,
198 clientId,
199 elementName,
200 };
201
202 const bridgeStore = useMemo(() => createBridgeStore(refs), []);
203
204 // Keep the synthetic cells map fresh for components reading it
205 // (e.g. the element options toolbar).
206 useEffect(() => {
207 (bridgeStore as any).setState({ cells: syntheticCells(refs) });
208 }, [attributes, bridgeStore]);
209
210 const blockProps = useBlockProps();
211 const { bindings, ...elementAttributes } = attributes;
212
213 // Anything the pro plugin injected through `editor.BlockEdit` is
214 // passed on to the element component. The block's own props are
215 // left behind — the component works off the element interface.
216 const proProps = Object.fromEntries(
217 Object.entries(props).filter(([key]) => key.startsWith("Pro"))
218 );
219
220 return (
221 <TableStoreContext.Provider value={bridgeStore as any}>
222 <div {...blockProps}>
223 <Component
224 {...proProps}
225 attributes={elementAttributes}
226 bindings={bindings as ElementBindings | undefined}
227 cellCoords={BRIDGE_COORDS}
228 elementIndex={0}
229 />
230 </div>
231 </TableStoreContext.Provider>
232 );
233 };
234 }
235
236 export function registerNativeElementBlock(config: NativeElementBlockConfig) {
237 registerBlockType(config.metadata.name, {
238 ...(config.metadata as any),
239 icon: config.icon as any,
240 edit: createBridgedElementEdit(
241 config.metadata.name,
242 config.component
243 ),
244 save: () => null,
245 } as any);
246 }
247