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 / cell / index.tsx

index.tsx in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/blocks/cell/index.tsx

751 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { CSSProperties, ReactNode, useState } from "react";
2 import {
3 BlockControls,
4 InnerBlocks,
5 InspectorControls,
6 store as blockEditorStore,
7 useBlockProps,
8 useInnerBlocksProps,
9 } from "@wordpress/block-editor";
10 import { BlockEditProps, registerBlockType } from "@wordpress/blocks";
11 import { useDispatch, useRegistry, useSelect } from "@wordpress/data";
12 import {
13 PanelBody,
14 SelectControl,
15 ToggleControl,
16 ToolbarDropdownMenu,
17 } from "@wordpress/components";
18 import { __ } from "@wordpress/i18n";
19 import {
20 table as tableIcon,
21 tableColumnAfter,
22 tableColumnBefore,
23 tableColumnDelete,
24 tableRowAfter,
25 tableRowBefore,
26 tableRowDelete,
27 } from "@wordpress/icons";
28 import blockIcon from "@tableberg/shared/icons/tableberg";
29
30 import metadata from "./block.json";
31 import {
32 ColorControl,
33 BorderControl,
34 SpacingControlSingle,
35 } from "@tableberg/components";
36 import LockedControl from "../../components/LockedControl";
37 import { UpsellEnhancedModal } from "../../components/UpsellModal";
38
39 import { useTableStore } from "../../store";
40 import { isProAvailable } from "../../pro-status";
41 import {
42 deleteColumn,
43 deleteRow,
44 getCellContext,
45 insertColumn,
46 insertRow,
47 mergeCells,
48 splitCell,
49 } from "../table/table-ops";
50 import { cellColumn, GridRow } from "../table/grid-model";
51 import { CellDimensionsControls } from "./dimensions";
52 import {
53 CellColumnBackgroundControls,
54 ColumnBackgroundContext,
55 } from "./row-column-background";
56 import {
57 CellColumnBorderControls,
58 ColumnBorderContext,
59 } from "./column-border";
60
61 import {
62 Border,
63 CellKey,
64 getCellKey,
65 RibbonConfig,
66 Span,
67 TableCellStylesType,
68 TableConfig,
69 } from "../../attributes";
70
71 const EMPTY_BORDER: Border = { top: "", right: "", bottom: "", left: "" };
72
73 export interface CellBlockAttrs {
74 span?: Span;
75 styles?: Partial<TableCellStylesType>;
76 ribbon?: RibbonConfig;
77 className?: string;
78 }
79
80 const ELEMENT_BLOCKS = [
81 "tableberg/text",
82 "tableberg/button",
83 "tableberg/image",
84 "tableberg/list",
85 "tableberg/styled-list",
86 "tableberg/icon",
87 "tableberg/star-rating",
88 "tableberg/custom-html",
89 ];
90
91 /**
92 * Whether this cell sits in the header or footer row, derived from the cell's
93 * position in the block tree + the table config provided via block context.
94 */
95 function useRowType(
96 clientId: string,
97 tableConfig: TableConfig | undefined
98 ): "header" | "footer" | "body" {
99 return useSelect(
100 select => {
101 if (!tableConfig?.headerEnabled && !tableConfig?.footerEnabled) {
102 return "body";
103 }
104
105 const be = select(blockEditorStore) as any;
106 const rowClientId = be.getBlockRootClientId(clientId);
107 if (!rowClientId) {
108 return "body";
109 }
110 const tableClientId = be.getBlockRootClientId(rowClientId);
111 const rowIndex = be.getBlockIndex(rowClientId);
112 const rowCount = be.getBlockCount(tableClientId);
113
114 if (tableConfig.headerEnabled && rowIndex === 0) {
115 return "header";
116 }
117 if (tableConfig.footerEnabled && rowIndex === rowCount - 1) {
118 return "footer";
119 }
120 return "body";
121 },
122 [clientId, tableConfig?.headerEnabled, tableConfig?.footerEnabled]
123 );
124 }
125
126 type RowBackgroundStyleKey =
127 | "headerBackgroundColor"
128 | "footerBackgroundColor"
129 | "evenRowBackgroundColor"
130 | "oddRowBackgroundColor";
131
132 /**
133 * Which of the table's row-position background defaults applies to this
134 * cell: header/footer rows take their own colour, other rows alternate
135 * even/odd by position among the data rows — header/footer don't consume a
136 * slot in that count, matching `TableRenderer.php`'s equivalent resolution.
137 */
138 function useRowBackgroundStyleKey(
139 clientId: string,
140 tableConfig: TableConfig | undefined
141 ): RowBackgroundStyleKey {
142 return useSelect(
143 select => {
144 const be = select(blockEditorStore) as any;
145 const rowClientId = be.getBlockRootClientId(clientId);
146 if (!rowClientId) {
147 return "oddRowBackgroundColor";
148 }
149 const tableClientId = be.getBlockRootClientId(rowClientId);
150 const rowIndex = be.getBlockIndex(rowClientId);
151 const rowCount = be.getBlockCount(tableClientId);
152
153 if (tableConfig?.headerEnabled && rowIndex === 0) {
154 return "headerBackgroundColor";
155 }
156 if (tableConfig?.footerEnabled && rowIndex === rowCount - 1) {
157 return "footerBackgroundColor";
158 }
159
160 const dataRowPosition = tableConfig?.headerEnabled
161 ? rowIndex - 1
162 : rowIndex;
163
164 return dataRowPosition % 2 === 0
165 ? "oddRowBackgroundColor"
166 : "evenRowBackgroundColor";
167 },
168 [clientId, tableConfig?.headerEnabled, tableConfig?.footerEnabled]
169 );
170 }
171
172 /** Shape of the entries the pro plugin adds to this menu. */
173 export interface CellToolbarControl {
174 icon: unknown;
175 title: string;
176 onClick: (context: {
177 registry: unknown;
178 tableClientId: string;
179 rowIndex: number;
180 column: number;
181 }) => void;
182 }
183
184 function CellTableToolbar({
185 clientId,
186 hasSpan,
187 // Injected by the pro plugin; empty when pro is not installed.
188 proToolbarControls = [],
189 }: {
190 clientId: string;
191 hasSpan: boolean;
192 proToolbarControls?: CellToolbarControl[];
193 }) {
194 const registry = useRegistry() as any;
195 const isPro = isProAvailable();
196 const [showDuplicateUpsell, setShowDuplicateUpsell] = useState(false);
197
198 // Cells for a merge: the modifier-click marquee (works across rows) or
199 // the native sibling multi-select (same-row shift selection).
200 const marqueeCellIds = useTableStore(state => state.nativeSelectedCells);
201 const setNativeSelectedCells = useTableStore(
202 state => state.setNativeSelectedCells
203 );
204 const nativeMultiSelectIds = useSelect(select => {
205 const be = select(blockEditorStore) as any;
206 const ids: string[] = be.getMultiSelectedBlockClientIds();
207 return ids.filter(
208 id => be.getBlockName(id) === "tableberg/cell"
209 );
210 }, []);
211 const selectedCellIds =
212 marqueeCellIds.length > 1 ? marqueeCellIds : nativeMultiSelectIds;
213
214 const withContext = (
215 fn: (
216 ctx: NonNullable<ReturnType<typeof getCellContext>>
217 ) => void
218 ) => () => {
219 const ctx = getCellContext(registry, clientId);
220 if (ctx && ctx.column !== null) {
221 fn(ctx);
222 }
223 };
224
225 const controls = [
226 {
227 icon: tableRowBefore,
228 title: __("Insert row above", "tableberg"),
229 onClick: withContext(ctx =>
230 insertRow(registry, ctx.tableClientId, ctx.rowIndex)
231 ),
232 },
233 {
234 icon: tableRowAfter,
235 title: __("Insert row below", "tableberg"),
236 onClick: withContext(ctx =>
237 insertRow(registry, ctx.tableClientId, ctx.rowIndex + 1)
238 ),
239 },
240 {
241 icon: tableRowDelete,
242 title: __("Delete row", "tableberg"),
243 onClick: withContext(ctx =>
244 deleteRow(registry, ctx.tableClientId, ctx.rowIndex)
245 ),
246 },
247 {
248 icon: tableColumnBefore,
249 title: __("Insert column before", "tableberg"),
250 onClick: withContext(ctx =>
251 insertColumn(registry, ctx.tableClientId, ctx.column!)
252 ),
253 },
254 {
255 icon: tableColumnAfter,
256 title: __("Insert column after", "tableberg"),
257 onClick: withContext(ctx => {
258 const cell = ctx.rows
259 .flat()
260 .find(c => c.id === clientId);
261 insertColumn(
262 registry,
263 ctx.tableClientId,
264 ctx.column! + (cell?.colSpan ?? 1)
265 );
266 }),
267 },
268 {
269 icon: tableColumnDelete,
270 title: __("Delete column", "tableberg"),
271 onClick: withContext(ctx =>
272 deleteColumn(registry, ctx.tableClientId, ctx.column!)
273 ),
274 },
275 // Duplicate row/column and the ribbon are pro features: pro supplies
276 // these entries and free only resolves the grid position for them.
277 // Without pro they are replaced by upsell entries below.
278 ...proToolbarControls.map(control => ({
279 icon: control.icon as any,
280 title: control.title,
281 onClick: withContext(ctx =>
282 control.onClick({
283 registry,
284 tableClientId: ctx.tableClientId,
285 rowIndex: ctx.rowIndex,
286 column: ctx.column!,
287 })
288 ),
289 })),
290 ...(proToolbarControls.length === 0 && !isPro
291 ? [
292 {
293 icon: tableRowAfter,
294 title: __("Duplicate row (Pro)", "tableberg"),
295 onClick: () => setShowDuplicateUpsell(true),
296 },
297 {
298 icon: tableColumnAfter,
299 title: __("Duplicate column (Pro)", "tableberg"),
300 onClick: () => setShowDuplicateUpsell(true),
301 },
302 ]
303 : []),
304 ...(selectedCellIds.length > 1
305 ? [
306 {
307 icon: tableIcon,
308 title: __("Merge cells", "tableberg"),
309 onClick: withContext(ctx => {
310 const merged = mergeCells(
311 registry,
312 ctx.tableClientId,
313 selectedCellIds
314 );
315 if (merged) {
316 setNativeSelectedCells([]);
317 }
318 }),
319 },
320 ]
321 : []),
322 ...(hasSpan
323 ? [
324 {
325 icon: tableIcon,
326 title: __("Split cell", "tableberg"),
327 onClick: withContext(ctx =>
328 splitCell(registry, ctx.tableClientId, clientId)
329 ),
330 },
331 ]
332 : []),
333 ];
334
335 return (
336 <>
337 <BlockControls group="block">
338 <ToolbarDropdownMenu
339 icon={tableIcon}
340 label={__("Edit table", "tableberg")}
341 controls={controls}
342 />
343 </BlockControls>
344 {showDuplicateUpsell && (
345 <UpsellEnhancedModal
346 onClose={() => setShowDuplicateUpsell(false)}
347 selected="duplicate-row-col"
348 />
349 )}
350 </>
351 );
352 }
353
354 // Per-cell style overrides (the table-wide defaults live on the table
355 // block's sidebar; these win over them for this one cell).
356 function CellInspectorControls({
357 attributes,
358 setAttributes,
359 defaultElementGap,
360 // Controls injected by the pro plugin; null when pro is not installed.
361 CellBackgroundControl = null,
362 CellRibbonPanel = null,
363 CellBorderControl = null,
364 CellEmptyControl = null,
365 }: Pick<BlockEditProps<CellBlockAttrs>, "attributes" | "setAttributes"> & {
366 defaultElementGap?: string;
367 CellBackgroundControl?: ReactNode;
368 CellRibbonPanel?: ReactNode;
369 CellBorderControl?: ReactNode;
370 CellEmptyControl?: ReactNode;
371 }) {
372 const styles = attributes.styles ?? {};
373 const update = (patch: Partial<TableCellStylesType>) =>
374 setAttributes({ styles: { ...styles, ...patch } });
375
376 const ribbon = attributes.ribbon;
377
378 const isPro = isProAvailable();
379
380 const alignmentOptions = [
381 { label: __("Default", "tableberg"), value: "" },
382 { label: __("Top", "tableberg"), value: "top" },
383 { label: __("Middle", "tableberg"), value: "middle" },
384 { label: __("Bottom", "tableberg"), value: "bottom" },
385 ];
386
387 return (
388 <>
389 {/*
390 * The colour control renders a ToolsPanel item, so it only shows
391 * up inside a ToolsPanel. The editor's own "Color" group is one;
392 * a plain PanelBody is not, and the control silently renders
393 * nothing there.
394 */}
395 <InspectorControls group="color">
396 {/*
397 * A background on one cell is a pro feature: pro hands the
398 * real control down as a prop. Free has no implementation of
399 * its own, so without pro the dead placeholder below is all
400 * there is.
401 */}
402 {CellBackgroundControl && CellBackgroundControl}
403 {!CellBackgroundControl && !isPro && (
404 <LockedControl isEnhanced selected="cell-bg">
405 <ColorControl
406 label={__("Background Color", "tableberg")}
407 value=""
408 onChange={() => null}
409 onDeselect={() => null}
410 />
411 </LockedControl>
412 )}
413 </InspectorControls>
414
415 {/*
416 * A border on one cell is a pro feature: pro hands the real
417 * control down as a prop. Free has no implementation of its
418 * own, so without pro the dead placeholder below is all there
419 * is. Same "Border" group (Styles tab) the row/column border
420 * controls use.
421 */}
422 <InspectorControls group="border">
423 {CellBorderControl && CellBorderControl}
424 {!CellBorderControl && !isPro && (
425 <LockedControl isEnhanced selected="cell-border">
426 <BorderControl
427 label={__("Border", "tableberg")}
428 value={EMPTY_BORDER}
429 hasValue={() => false}
430 onChange={() => null}
431 onDeselect={() => null}
432 />
433 </LockedControl>
434 )}
435 </InspectorControls>
436
437 <InspectorControls>
438 <PanelBody title={__("Cell Settings", "tableberg")}>
439 <SpacingControlSingle
440 label={__("Block Spacing", "tableberg")}
441 value={
442 styles.elementGap ??
443 defaultElementGap ??
444 "var(--wp--preset--spacing--20)"
445 }
446 onChange={elementGap => update({ elementGap })}
447 style={{ marginBottom: "24px" }}
448 />
449 {isPro ? (
450 <SelectControl
451 label={__("Vertical Alignment", "tableberg")}
452 value={(styles.verticalAlign as string) ?? ""}
453 options={alignmentOptions}
454 onChange={verticalAlign =>
455 update({
456 verticalAlign: (verticalAlign ||
457 undefined) as TableCellStylesType["verticalAlign"],
458 })
459 }
460 />
461 ) : (
462 <LockedControl isEnhanced selected="cell-bg">
463 <SelectControl
464 label={__("Vertical Alignment", "tableberg")}
465 value=""
466 options={alignmentOptions}
467 onChange={() => null}
468 />
469 </LockedControl>
470 )}
471 {/*
472 * Emptying a cell (keeping its background/border but
473 * leaving its content out on the frontend only) is a pro
474 * feature: pro hands the toggle down. Free has no
475 * implementation of its own.
476 */}
477 {CellEmptyControl}
478 {!CellEmptyControl && !isPro && (
479 <LockedControl isEnhanced selected="cell-empty">
480 <ToggleControl
481 checked={false}
482 label={__("Empty Cell", "tableberg")}
483 onChange={() => null}
484 />
485 </LockedControl>
486 )}
487 </PanelBody>
488 {/*
489 * The ribbon is a pro feature: pro hands the whole panel down.
490 * Free has no ribbon implementation of its own.
491 */}
492 {CellRibbonPanel}
493 {!CellRibbonPanel && !isPro && (
494 <PanelBody title={__("Ribbon", "tableberg")}>
495 <LockedControl isEnhanced selected="ribbon">
496 <ToggleControl
497 checked={false}
498 label={__("Enable Ribbon", "tableberg")}
499 onChange={() => null}
500 />
501 </LockedControl>
502 </PanelBody>
503 )}
504 </InspectorControls>
505 </>
506 );
507 }
508
509 function CellEdit(
510 props: BlockEditProps<CellBlockAttrs> & {
511 context?: Record<string, unknown>;
512 // Injected by the pro plugin's editor.BlockEdit wrapper.
513 CellBackgroundControl?: ReactNode;
514 CellRibbonPanel?: ReactNode;
515 CellRibbonOverlay?: ReactNode;
516 CellBorderControl?: ReactNode;
517 CellEmptyControl?: ReactNode;
518 CellEmptyBadge?: ReactNode;
519 cellStyles?: CSSProperties;
520 cellProToolbarControls?: CellToolbarControl[];
521 ProColumnBackgroundContent?: (
522 ctx: ColumnBackgroundContext
523 ) => ReactNode;
524 ProColumnBorderContent?: (ctx: ColumnBorderContext) => ReactNode;
525 }
526 ) {
527 const { attributes, clientId, context, isSelected } = props;
528 const proExtensionActive = props.cellStyles !== undefined;
529 const tableConfig = context?.["tableberg/tableConfig"] as
530 | TableConfig
531 | undefined;
532
533 // Modifier-click marquee: Cmd/Ctrl+click toggles cells into a cross-row
534 // selection for merging; a plain click anywhere clears it.
535 const marqueeCellIds = useTableStore(state => state.nativeSelectedCells);
536 const setNativeSelectedCells = useTableStore(
537 state => state.setNativeSelectedCells
538 );
539 const { selectBlock } = useDispatch(blockEditorStore) as any;
540 const isInMarquee = marqueeCellIds.includes(clientId);
541
542 const onCellMouseDown = (event: React.MouseEvent) => {
543 if (event.metaKey || event.ctrlKey || event.shiftKey) {
544 // Capture-phase: keep Gutenberg's own modifier multi-select from
545 // hijacking the marquee.
546 event.preventDefault();
547 event.stopPropagation();
548 event.nativeEvent.stopImmediatePropagation?.();
549
550 const next = isInMarquee
551 ? marqueeCellIds.filter(id => id !== clientId)
552 : [...marqueeCellIds, clientId];
553 setNativeSelectedCells(next);
554 // Keep a cell block selected so the merge toolbar is reachable.
555 selectBlock(clientId);
556 return;
557 }
558
559 if (marqueeCellIds.length > 0) {
560 setNativeSelectedCells([]);
561 }
562 };
563 const cellDefaults = context?.["tableberg/cellDefaults"] as
564 | { styles: TableCellStylesType }
565 | undefined;
566 // A pro attribute of the parent row block, read via context since a
567 // cell has no direct reference to its row. Only relevant here to know
568 // whether to skip the table-wide default below.
569 const rowBackgroundColor = proExtensionActive
570 ? (context?.["tableberg/rowBackgroundColor"] as string | undefined)
571 : undefined;
572
573 const rowType = useRowType(clientId, tableConfig);
574 const Tag = rowType === "body" ? "td" : "th";
575 const rowBackgroundStyleKey = useRowBackgroundStyleKey(
576 clientId,
577 tableConfig
578 );
579
580 // Minimal cell styling for the editor MVP: defaults cascade + per-cell
581 // overrides. Full parity (borders, radius, orientation) lands in Phase 4.
582 const defaults = cellDefaults?.styles;
583 const styles = attributes.styles;
584 const cellStyle: CSSProperties = {
585 paddingTop: styles?.padding?.top ?? defaults?.padding?.top,
586 paddingRight: styles?.padding?.right ?? defaults?.padding?.right,
587 paddingBottom: styles?.padding?.bottom ?? defaults?.padding?.bottom,
588 paddingLeft: styles?.padding?.left ?? defaults?.padding?.left,
589 // The per-cell background is a pro attribute pro renders through
590 // `cellStyles` below; free only knows the table-wide defaults. Skip
591 // all of them when the row has its own colour — a cell painting a
592 // table default over itself would otherwise always hide the
593 // `<tr>`'s. A cell/column colour (below, via `cellStyles`) still
594 // wins over either one, applied after this. Header/footer/even/odd
595 // take priority over the plain common default when set.
596 backgroundColor: rowBackgroundColor
597 ? undefined
598 : (defaults?.[rowBackgroundStyleKey] ||
599 defaults?.backgroundColor ||
600 undefined),
601 // A cell's own vertical alignment is pro (the frontend only applies it
602 // through pro's licensed filter), so without pro show the table default.
603 verticalAlign:
604 ((isProAvailable() ? styles?.verticalAlign : undefined) as
605 | CSSProperties["verticalAlign"]
606 | undefined) ??
607 (defaults?.verticalAlign as CSSProperties["verticalAlign"]),
608 // The per-cell/column border is a pro attribute pro renders through
609 // `cellStyles` below; free only knows the table-wide default.
610 borderTop: defaults?.border?.top || undefined,
611 borderRight: defaults?.border?.right || undefined,
612 borderBottom: defaults?.border?.bottom || undefined,
613 borderLeft: defaults?.border?.left || undefined,
614 // Per-cell radius only; the table-level radius is clipped on the
615 // wrapper (border-radius on collapsed cells is ignored by browsers).
616 borderTopLeftRadius: styles?.borderRadius?.topLeft,
617 borderTopRightRadius: styles?.borderRadius?.topRight,
618 borderBottomRightRadius: styles?.borderRadius?.bottomRight,
619 borderBottomLeftRadius: styles?.borderRadius?.bottomLeft,
620 };
621
622 // Elements layout inside the cell (orientation / gap / wrap), same
623 // cascade: per-cell override wins over the table-wide defaults. Applied
624 // to an inner wrapper — flex directly on the td would break the table
625 // row layout (display: table-cell is lost).
626 const orientation =
627 styles?.orientation ??
628 (proExtensionActive ? defaults?.orientation : undefined);
629 const elementGap = styles?.elementGap ?? defaults?.elementGap;
630 const wrap =
631 styles?.wrap ?? (proExtensionActive ? defaults?.wrap : undefined);
632 const contentStyle: CSSProperties = {
633 display: "flex",
634 flexDirection: orientation === "horizontal" ? "row" : "column",
635 gap: elementGap || undefined,
636 };
637 if (orientation === "horizontal") {
638 contentStyle.alignItems = "center";
639 contentStyle.flexWrap = wrap === "nowrap" ? "nowrap" : "wrap";
640 }
641
642 if (isInMarquee) {
643 cellStyle.boxShadow =
644 "inset 0 0 0 2px var(--wp-admin-theme-color, #3858e9)";
645 }
646
647 Object.assign(cellStyle, props.cellStyles ?? {});
648
649 const blockProps = useBlockProps({ style: cellStyle });
650 const innerBlocksProps = useInnerBlocksProps(
651 {
652 className: "tableberg-cell-content",
653 style: contentStyle,
654 },
655 {
656 allowedBlocks: ELEMENT_BLOCKS,
657 template: [["tableberg/text", {}]],
658 }
659 );
660
661 // Sticky first column (pro): the first cell block of each row sticks to
662 // the left while the canvas scrolls horizontally.
663 const stickyFirstCol =
664 proExtensionActive && !!tableConfig?.stickyFirstCol;
665 const isFirstColCell = useSelect(
666 select => {
667 if (!stickyFirstCol) {
668 return false;
669 }
670 const be = select(blockEditorStore) as any;
671 return be.getBlockIndex(clientId) === 0;
672 },
673 [clientId, stickyFirstCol]
674 );
675 if (isFirstColCell) {
676 cellStyle.position = "sticky";
677 cellStyle.left = 0;
678 cellStyle.zIndex = 1;
679 if (!cellStyle.backgroundColor) {
680 cellStyle.backgroundColor = "#fff";
681 }
682 }
683
684 const hasSpan =
685 (attributes.span?.rowSpan ?? 1) > 1 ||
686 (attributes.span?.colSpan ?? 1) > 1;
687
688 return (
689 <>
690 {isSelected && (
691 <CellTableToolbar
692 clientId={clientId}
693 hasSpan={hasSpan}
694 proToolbarControls={props.cellProToolbarControls}
695 />
696 )}
697 {isSelected && (
698 <CellInspectorControls
699 attributes={attributes}
700 setAttributes={props.setAttributes}
701 defaultElementGap={defaults?.elementGap}
702 CellBackgroundControl={props.CellBackgroundControl}
703 CellRibbonPanel={props.CellRibbonPanel}
704 CellBorderControl={props.CellBorderControl}
705 CellEmptyControl={props.CellEmptyControl}
706 />
707 )}
708 {isSelected && <CellDimensionsControls clientId={clientId} />}
709 {isSelected && (
710 <CellColumnBackgroundControls
711 clientId={clientId}
712 cellDefaultsBackgroundColor={
713 defaults?.backgroundColor ?? ""
714 }
715 ProColumnBackgroundContent={
716 props.ProColumnBackgroundContent
717 }
718 />
719 )}
720 {isSelected && (
721 <CellColumnBorderControls
722 clientId={clientId}
723 cellDefaultsBorder={defaults?.border ?? EMPTY_BORDER}
724 ProColumnBorderContent={props.ProColumnBorderContent}
725 />
726 )}
727
728 <Tag
729 {...blockProps}
730 rowSpan={attributes.span?.rowSpan}
731 colSpan={attributes.span?.colSpan}
732 onMouseDownCapture={onCellMouseDown}
733 style={(blockProps as { style?: CSSProperties }).style}
734 >
735 <div {...innerBlocksProps} />
736 {props.CellRibbonOverlay}
737 {props.CellEmptyBadge}
738 </Tag>
739 </>
740 );
741 }
742
743 export function registerNativeCellBlock() {
744 registerBlockType(metadata.name, {
745 ...(metadata as any),
746 icon: blockIcon,
747 edit: CellEdit,
748 save: () => <InnerBlocks.Content />,
749 });
750 }
751