PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.2.0
Tableberg – Simple Gutenberg Table Block v1.2.0
1.2.0 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 All 43 releases
tableberg / src / blocks / element-bridge.tsx

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

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