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

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

801 lines 26.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { CSSProperties, useState, MouseEvent } from "react";
2 import {
3 store as blockEditorStore,
4 useBlockProps,
5 } from "@wordpress/block-editor";
6 import { useDispatch } from "@wordpress/data";
7 import { __ } from "@wordpress/i18n";
8 import {
9 Span,
10 CellElement,
11 CellKey,
12 attrDefaults,
13 parseCellKey,
14 } from "../../attributes";
15 import { useTableStore } from "../../store";
16 import { useBlockCardUpdateShim } from "../../hooks/block-editor-compat";
17 import {
18 elementAlignmentToJustifyContent,
19 getElementAlignment,
20 getUniformElementsAlignment,
21 } from "../../alignment";
22 import {
23 TextElement,
24 ButtonElement,
25 ImageElement,
26 ListElement,
27 createElement,
28 } from "../../elements";
29 import CellInserter, {
30 getCellInserterItems,
31 } from "../../components/cell-inserter";
32 import classNames from "classnames";
33 import { renderExtendedElement } from "../../extensions";
34 import { renderExtendedCellRibbon } from "../../ribbon-extensions";
35 import { isProAvailable } from "../../pro-status";
36
37 const cellDefaultsStyles = attrDefaults.cellDefaults.styles;
38
39 const VERTICAL_ALIGN_TO_FLEX: Record<
40 "top" | "middle" | "bottom",
41 CSSProperties["alignItems"]
42 > = {
43 top: "flex-start",
44 middle: "center",
45 bottom: "flex-end",
46 };
47
48 type WrapperBlockProps = {
49 className?: string;
50 style?: CSSProperties;
51 [key: string]: unknown;
52 };
53 type BlockEditorActions = {
54 selectBlock: (clientId: string, initialPosition?: 0 | -1 | null) => void;
55 };
56
57 function getTableClientIdFromElement(element: HTMLElement): string | null {
58 const tableBlock = element.closest<HTMLElement>(
59 ".tableberg-editor-shell[id^='block-']"
60 );
61
62 return tableBlock?.id.replace(/^block-/, "") || null;
63 }
64
65 function selectTableBlockFromElement(
66 element: HTMLElement,
67 selectBlock: BlockEditorActions["selectBlock"]
68 ) {
69 const tableClientId = getTableClientIdFromElement(element);
70 if (tableClientId) {
71 selectBlock(tableClientId);
72 }
73 }
74
75 function mergeWrapperBlockProps(
76 blockProps: WrapperBlockProps | undefined,
77 className: string | undefined,
78 style: CSSProperties
79 ): WrapperBlockProps {
80 const mergedClassName = classNames(className, blockProps?.className);
81
82 return {
83 ...(blockProps || {}),
84 ...(mergedClassName ? { className: mergedClassName } : {}),
85 style: {
86 ...((blockProps?.style as CSSProperties | undefined) || {}),
87 ...style,
88 },
89 };
90 }
91
92 export function Cell({
93 span,
94 cellCoords,
95 }: {
96 span: Span;
97 cellCoords: CellKey;
98 }) {
99 const [row, column] = parseCellKey(cellCoords);
100 const getCellStyleState = useTableStore(state => state.getCellStyle);
101 const cellStyles = getCellStyleState(cellCoords);
102 const isPro = isProAvailable();
103 const innerBorderType = useTableStore(state =>
104 isPro ? state.table.innerBorderType || "" : ""
105 );
106 const totalRows = useTableStore(state => state.table.rows);
107 const totalCols = useTableStore(state => state.table.cols);
108
109 const cellStyleCss: {
110 position: CSSProperties["position"];
111 top?: string;
112 zIndex?: CSSProperties["zIndex"];
113
114 boxShadow?: string;
115 backgroundColor?: string;
116
117 paddingTop?: string;
118 paddingRight?: string;
119 paddingBottom?: string;
120 paddingLeft?: string;
121
122 borderTop?: string;
123 borderRight?: string;
124 borderBottom?: string;
125 borderLeft?: string;
126
127 borderTopLeftRadius?: string;
128 borderTopRightRadius?: string;
129 borderBottomLeftRadius?: string;
130 borderBottomRightRadius?: string;
131
132 verticalAlign?: CSSProperties["verticalAlign"];
133
134 height?: string;
135 minHeight?: string;
136
137 width?: string;
138 minWidth?: string;
139 } = {
140 position: "relative",
141 };
142
143 const cellGlobalPadding = useTableStore(
144 state => state.cellDefaults.styles.padding
145 );
146 (function getCellPadding() {
147 let padding:
148 | {
149 top: string;
150 right: string;
151 bottom: string;
152 left: string;
153 }
154 | undefined;
155
156 padding = cellGlobalPadding;
157 if (cellStyles?.padding) {
158 padding = cellStyles?.padding;
159 }
160
161 cellStyleCss.paddingTop = padding?.top;
162 cellStyleCss.paddingRight = padding?.right;
163 cellStyleCss.paddingBottom = padding?.bottom;
164 cellStyleCss.paddingLeft = padding?.left;
165 })();
166
167 const cellGlobalBorder = useTableStore(
168 state => state.cellDefaults.styles.border
169 );
170 (function getCellBorder() {
171 let border:
172 | {
173 top: string;
174 right: string;
175 bottom: string;
176 left: string;
177 }
178 | undefined;
179
180 border = cellGlobalBorder;
181 if (cellStyles?.border) {
182 border = cellStyles?.border;
183 }
184
185 cellStyleCss.borderTop = border?.top;
186 cellStyleCss.borderRight = border?.right;
187 cellStyleCss.borderBottom = border?.bottom;
188 cellStyleCss.borderLeft = border?.left;
189 })();
190
191 // These Pro modes render inner separators only. The separate table
192 // border control owns the outside rectangle.
193 if (innerBorderType === "row") {
194 cellStyleCss.borderLeft = undefined;
195 cellStyleCss.borderRight = undefined;
196 if (row === 0) {
197 cellStyleCss.borderTop = undefined;
198 }
199 if (row + span.rowSpan >= totalRows) {
200 cellStyleCss.borderBottom = undefined;
201 }
202 } else if (innerBorderType === "col") {
203 cellStyleCss.borderTop = undefined;
204 cellStyleCss.borderBottom = undefined;
205 if (column === 0) {
206 cellStyleCss.borderLeft = undefined;
207 }
208 if (column + span.colSpan >= totalCols) {
209 cellStyleCss.borderRight = undefined;
210 }
211 }
212
213 (function getCellBorderRadius() {
214 // The common/table border radius is rendered on the table wrapper, so
215 // here we only honour a per-cell override (rounds that single cell).
216 const override = cellStyles?.borderRadius;
217
218 cellStyleCss.borderTopLeftRadius = override?.topLeft;
219 cellStyleCss.borderTopRightRadius = override?.topRight;
220 cellStyleCss.borderBottomRightRadius = override?.bottomRight;
221 cellStyleCss.borderBottomLeftRadius = override?.bottomLeft;
222 })();
223
224 const cellDefaultStyles = useTableStore(state => state.cellDefaults.styles);
225 const headerEnabled = useTableStore(state => state.table.headerEnabled);
226 const footerEnabled = useTableStore(state => state.table.footerEnabled);
227
228 let rowPositionBackground = "";
229 if (headerEnabled && row === 0) {
230 rowPositionBackground = cellDefaultStyles.headerBackgroundColor;
231 } else if (footerEnabled && row === totalRows - 1) {
232 rowPositionBackground = cellDefaultStyles.footerBackgroundColor;
233 } else {
234 const dataRowPosition = headerEnabled ? row - 1 : row;
235 rowPositionBackground =
236 dataRowPosition % 2 === 0
237 ? cellDefaultStyles.oddRowBackgroundColor
238 : cellDefaultStyles.evenRowBackgroundColor;
239 }
240
241 cellStyleCss.backgroundColor =
242 rowPositionBackground || cellDefaultStyles.backgroundColor;
243 if (cellStyles?.backgroundColor) {
244 cellStyleCss.backgroundColor = cellStyles?.backgroundColor;
245 }
246
247 const cellGlobalOrientation = useTableStore(
248 state => state.cellDefaults.styles.orientation
249 );
250 const cellOrientation =
251 cellStyles?.orientation ||
252 (isPro ? cellGlobalOrientation : undefined) ||
253 cellDefaultsStyles.orientation;
254
255 const cellGlobalWrap = useTableStore(
256 state => state.cellDefaults.styles.wrap
257 );
258 const cellWrap =
259 cellStyles?.wrap ||
260 (isPro ? cellGlobalWrap : undefined) ||
261 cellDefaultsStyles.wrap;
262
263 const cellGlobalElementGap = useTableStore(
264 state => state.cellDefaults.styles.elementGap
265 );
266 const cellElementGap =
267 cellStyles?.elementGap ??
268 cellGlobalElementGap ??
269 cellDefaultsStyles.elementGap;
270
271 const cellGlobalVerticalAlign = useTableStore(
272 state => state.cellDefaults.styles.verticalAlign
273 );
274 const cellVerticalAlign =
275 cellStyles?.verticalAlign ||
276 cellGlobalVerticalAlign ||
277 cellDefaultsStyles.verticalAlign;
278 cellStyleCss.verticalAlign = cellVerticalAlign;
279
280 const isCurrentCellSelected = useTableStore(state =>
281 state.selectedCells.includes(cellCoords)
282 );
283
284 if (isCurrentCellSelected) {
285 cellStyleCss.boxShadow = "inset 0 0 0 2px var(--wp-admin-theme-color)";
286 }
287
288 const selectedCellBlockProps = useBlockProps();
289
290 const tableRows = useTableStore(state => state.table.rows);
291 const isFirstRow = row === 0;
292 const isLastRow = row === tableRows - 1;
293
294 const stickyHeader = useTableStore(
295 state => state.table.stickyHeader ?? false
296 );
297 const isHeaderCell = isFirstRow && headerEnabled;
298 const isStickyHeaderCell = isPro && stickyHeader && isHeaderCell;
299
300 if (isStickyHeaderCell) {
301 cellStyleCss.position = "sticky";
302 cellStyleCss.top = "0";
303 cellStyleCss.zIndex = 2;
304
305 if (!cellStyleCss.backgroundColor) {
306 cellStyleCss.backgroundColor = "#fff";
307 }
308 }
309
310 const sortPreviewMode = useTableStore(state => state.sortPreviewMode);
311 const tableEditPreview = useTableStore(state => state.tableEditPreview);
312 const previewSortColumn = useTableStore(state => state.previewSortColumn);
313 const previewSortOrder = useTableStore(state => state.previewSortOrder);
314 const togglePreviewSort = useTableStore(state => state.togglePreviewSort);
315 const columns = useTableStore(state => state.columns);
316 const rows = useTableStore(state => state.rows);
317 const table = useTableStore(state => state.table);
318 const fixedColumnWidths = useTableStore(
319 state => state.table.fixedColumnWidths ?? true
320 );
321 const isSortable = isPro && isHeaderCell && !!columns[column]?.sortable;
322 const isSorted = previewSortColumn === column;
323
324 const columnWidth =
325 span.colSpan > 1
326 ? undefined
327 : fixedColumnWidths && table.cols > 0
328 ? `${100 / table.cols}%`
329 : columns[column]?.width;
330
331 if (columnWidth) {
332 cellStyleCss.width = columnWidth;
333 cellStyleCss.minWidth = columnWidth;
334 }
335
336 const rowHeight = span.rowSpan > 1 ? undefined : rows[row]?.height;
337
338 if (rowHeight) {
339 cellStyleCss.height = rowHeight;
340 cellStyleCss.minHeight = rowHeight;
341 }
342
343 let Tag: "td" | "th" = "td";
344 if (isHeaderCell) {
345 Tag = "th";
346 }
347 if (isLastRow && footerEnabled) {
348 Tag = "th";
349 }
350
351 const cells = useTableStore(state => state.cells);
352 const addElementToCell = useTableStore(state => state.addElementToCell);
353 const getCellRibbon = useTableStore(state => state.getCellRibbon);
354 const cellRibbon = getCellRibbon(cellCoords);
355
356 const setSelectedCells = useTableStore(state => state.setSelectedCells);
357 const addSelectedCells = useTableStore(state => state.addSelectedCells);
358 const clearSelectedElement = useTableStore(
359 state => state.clearSelectedElement
360 );
361 const { selectBlock } = useDispatch(
362 blockEditorStore
363 ) as unknown as BlockEditorActions;
364
365 const updateBlockCard = useBlockCardUpdateShim();
366
367 const cellElements = cells[cellCoords]?.elements || [];
368 const cellClassName = cells[cellCoords]?.className || "";
369
370 const [isHovered, setIsHovered] = useState(false);
371 const [isInserterOpen, setIsInserterOpen] = useState(false);
372 const showInserter =
373 !sortPreviewMode &&
374 (isHovered || isCurrentCellSelected || isInserterOpen);
375
376 const handleCellClick = (e: MouseEvent<HTMLTableCellElement>) => {
377 if (sortPreviewMode) {
378 if (isHeaderCell && isSortable) {
379 togglePreviewSort(column);
380 }
381 return;
382 }
383
384 const { ctrlKey, metaKey, shiftKey } = e;
385
386 if (ctrlKey || metaKey || shiftKey) {
387 // Building a multi-cell selection: drop any single-element
388 // selection so the cell selection drives the merge controls.
389 selectTableBlockFromElement(e.currentTarget, selectBlock);
390 clearSelectedElement();
391 addSelectedCells([cellCoords]);
392 updateBlockCard(
393 e.currentTarget,
394 "Multiple Cells",
395 "Tableberg Cells: Individual cells of a tableberg table"
396 );
397 return;
398 }
399
400 selectTableBlockFromElement(e.currentTarget, selectBlock);
401 clearSelectedElement();
402 setSelectedCells([cellCoords]);
403 updateBlockCard(
404 e.currentTarget,
405 "Cell",
406 "Tableberg Cell: Individual cell of a tableberg table"
407 );
408 };
409
410 const renderSortIndicator = () => {
411 if (!isHeaderCell || !isSortable) {
412 return null;
413 }
414
415 if (sortPreviewMode) {
416 return (
417 <span
418 className={classNames("tableberg-sort-indicator", {
419 "tableberg-sort-indicator--active": isSorted,
420 })}
421 style={{
422 userSelect: "none",
423 }}
424 title={
425 isSorted
426 ? __("Click to change sort order", "tableberg")
427 : __("Click to sort by this column", "tableberg")
428 }
429 >
430 {isSorted
431 ? previewSortOrder === "asc"
432 ? "\u25B2"
433 : "\u25BC"
434 : "\u25B2\u25BC"}
435 </span>
436 );
437 }
438
439 return (
440 <span
441 className="tableberg-sort-indicator"
442 style={{
443 color: "#a0a0a0",
444 userSelect: "none",
445 }}
446 title={__(
447 "Sorting enabled. Use Preview Sorting to test.",
448 "tableberg"
449 )}
450 >
451 {"\u25B2\u25BC"}
452 </span>
453 );
454 };
455
456 const sortableStyles: CSSProperties =
457 sortPreviewMode && isHeaderCell && isSortable
458 ? { cursor: "pointer" }
459 : {};
460
461 const previewRowIndex =
462 tableEditPreview && tableEditPreview.target === "row"
463 ? tableEditPreview.index
464 : null;
465 const previewColumnIndex =
466 tableEditPreview && tableEditPreview.target === "column"
467 ? tableEditPreview.index
468 : null;
469 const isPreviewDelete = tableEditPreview?.operation === "delete";
470 const isPreviewDuplicate = tableEditPreview?.operation === "duplicate";
471 const isPreviewInsertOrDuplicate =
472 tableEditPreview?.operation === "insert" ||
473 tableEditPreview?.operation === "duplicate";
474
475 const rowStart = row;
476 const rowEnd = row + span.rowSpan - 1;
477 const colStart = column;
478 const colEnd = column + span.colSpan - 1;
479
480 const intersectsPreviewRow =
481 previewRowIndex !== null &&
482 previewRowIndex >= 0 &&
483 previewRowIndex < table.rows &&
484 previewRowIndex >= rowStart &&
485 previewRowIndex <= rowEnd;
486
487 const intersectsPreviewColumn =
488 previewColumnIndex !== null &&
489 previewColumnIndex >= 0 &&
490 previewColumnIndex < table.cols &&
491 previewColumnIndex >= colStart &&
492 previewColumnIndex <= colEnd;
493
494 const duplicateSourceRowIndex =
495 isPreviewDuplicate && previewRowIndex !== null
496 ? Math.max(0, Math.min(previewRowIndex - 1, table.rows - 1))
497 : null;
498
499 const duplicateSourceColumnIndex =
500 isPreviewDuplicate && previewColumnIndex !== null
501 ? Math.max(0, Math.min(previewColumnIndex - 1, table.cols - 1))
502 : null;
503 const cellInserterItems = getCellInserterItems();
504
505 const intersectsDuplicateSourceRow =
506 duplicateSourceRowIndex !== null &&
507 duplicateSourceRowIndex >= rowStart &&
508 duplicateSourceRowIndex <= rowEnd;
509
510 const intersectsDuplicateSourceColumn =
511 duplicateSourceColumnIndex !== null &&
512 duplicateSourceColumnIndex >= colStart &&
513 duplicateSourceColumnIndex <= colEnd;
514
515 const previewInsertRowAtBoundary =
516 isPreviewInsertOrDuplicate &&
517 previewRowIndex !== null &&
518 previewRowIndex >= 0 &&
519 previewRowIndex < table.rows &&
520 rowStart === previewRowIndex;
521
522 const previewInsertColumnAtBoundary =
523 isPreviewInsertOrDuplicate &&
524 previewColumnIndex !== null &&
525 previewColumnIndex >= 0 &&
526 previewColumnIndex < table.cols &&
527 colStart === previewColumnIndex;
528
529 const previewInsertRowAtEnd =
530 isPreviewInsertOrDuplicate &&
531 previewRowIndex === table.rows &&
532 rowEnd === table.rows - 1;
533
534 const previewInsertColumnAtEnd =
535 isPreviewInsertOrDuplicate &&
536 previewColumnIndex === table.cols &&
537 colEnd === table.cols - 1;
538
539 const mergedCellBlockProps = mergeWrapperBlockProps(
540 isCurrentCellSelected ? selectedCellBlockProps : undefined,
541 classNames(cellClassName, {
542 "is-selected": isCurrentCellSelected,
543 "tableberg-sortable-header": isHeaderCell && isSortable,
544 "tableberg-cell-preview-delete-row":
545 isPreviewDelete && intersectsPreviewRow,
546 "tableberg-cell-preview-delete-column":
547 isPreviewDelete && intersectsPreviewColumn,
548 "tableberg-cell-preview-duplicate-source-row":
549 isPreviewDuplicate && intersectsDuplicateSourceRow,
550 "tableberg-cell-preview-duplicate-source-column":
551 isPreviewDuplicate && intersectsDuplicateSourceColumn,
552 "tableberg-cell-preview-insert-row-boundary":
553 previewInsertRowAtBoundary,
554 "tableberg-cell-preview-insert-column-boundary":
555 previewInsertColumnAtBoundary,
556 "tableberg-cell-preview-insert-row-end": previewInsertRowAtEnd,
557 "tableberg-cell-preview-insert-column-end":
558 previewInsertColumnAtEnd,
559 }),
560 { ...cellStyleCss, ...sortableStyles }
561 );
562
563 return (
564 <Tag
565 {...mergedCellBlockProps}
566 rowSpan={span.rowSpan}
567 colSpan={span.colSpan}
568 data-sortable={
569 isPro && isHeaderCell ? columns[column]?.sortable : undefined
570 }
571 onMouseEnter={() => setIsHovered(true)}
572 onMouseLeave={() => setIsHovered(false)}
573 onClick={handleCellClick}
574 >
575 <CellElementRenderer
576 cellElements={cellElements}
577 cellCoords={cellCoords}
578 orientation={cellOrientation}
579 elementGap={cellElementGap}
580 wrap={cellWrap}
581 verticalAlign={cellVerticalAlign}
582 />
583 {renderSortIndicator()}
584 {cellRibbon && renderExtendedCellRibbon(cellRibbon, cellCoords)}
585 {showInserter && (
586 <CellInserter
587 items={cellInserterItems}
588 onSelect={item => {
589 const newElement = createElement(item.name);
590 if (newElement) {
591 addElementToCell(cellCoords, newElement);
592 }
593 }}
594 onOpenChange={setIsInserterOpen}
595 />
596 )}
597 </Tag>
598 );
599 }
600
601 interface CellElementRendererProps {
602 cellElements: CellElement[];
603 cellCoords: CellKey;
604 orientation: "vertical" | "horizontal";
605 elementGap: string;
606 wrap: "wrap" | "nowrap";
607 verticalAlign: "top" | "middle" | "bottom";
608 }
609
610 function CellElementRenderer({
611 cellElements,
612 cellCoords,
613 orientation,
614 elementGap,
615 wrap,
616 verticalAlign,
617 }: CellElementRendererProps) {
618 const isElementSelected = useTableStore(state => state.isElementSelected);
619 const selectedElementBlockProps = useBlockProps();
620
621 const isHorizontal = orientation === "horizontal";
622 const implicitAlignment = getUniformElementsAlignment(cellElements);
623 const elementsStyle: CSSProperties = {
624 display: "flex",
625 flexDirection: isHorizontal ? "row" : "column",
626 justifyContent: isHorizontal
627 ? elementAlignmentToJustifyContent(implicitAlignment || "left")
628 : VERTICAL_ALIGN_TO_FLEX[verticalAlign],
629 alignItems: isHorizontal
630 ? VERTICAL_ALIGN_TO_FLEX[verticalAlign]
631 : "stretch",
632 gap: elementGap,
633 flexWrap: wrap,
634 };
635
636 return (
637 <div
638 className={classNames("tableberg-cell-elements", {
639 "tableberg-cell-elements-horizontal": isHorizontal,
640 })}
641 style={elementsStyle}
642 >
643 {cellElements.map((element, index) => {
644 const isSelected = isElementSelected(cellCoords, index);
645
646 let elementComponent: React.ReactNode;
647 switch (element.name) {
648 case "text":
649 elementComponent = (
650 <TextElement
651 key={index}
652 attributes={element.attributes}
653 bindings={element.bindings}
654 cellCoords={cellCoords}
655 elementIndex={index}
656 />
657 );
658 break;
659 case "button":
660 elementComponent = (
661 <ButtonElement
662 key={index}
663 attributes={element.attributes}
664 bindings={element.bindings}
665 cellCoords={cellCoords}
666 elementIndex={index}
667 />
668 );
669 break;
670 case "image":
671 elementComponent = (
672 <ImageElement
673 key={index}
674 attributes={element.attributes}
675 bindings={element.bindings}
676 cellCoords={cellCoords}
677 elementIndex={index}
678 />
679 );
680 break;
681 case "list":
682 elementComponent = (
683 <ListElement
684 key={index}
685 attributes={element.attributes}
686 bindings={element.bindings}
687 cellCoords={cellCoords}
688 elementIndex={index}
689 />
690 );
691 break;
692 default:
693 elementComponent = renderExtendedElement(
694 element,
695 cellCoords,
696 index
697 );
698 break;
699 }
700
701 return (
702 <CellElementBlockWrapper
703 key={index}
704 cellCoords={cellCoords}
705 element={element}
706 elementIndex={index}
707 isHorizontal={isHorizontal}
708 isSelected={isSelected}
709 selectedBlockProps={selectedElementBlockProps}
710 >
711 {elementComponent}
712 </CellElementBlockWrapper>
713 );
714 })}
715 </div>
716 );
717 }
718
719 function CellElementBlockWrapper({
720 children,
721 cellCoords,
722 element,
723 elementIndex,
724 isHorizontal,
725 isSelected,
726 selectedBlockProps,
727 }: {
728 children: React.ReactNode;
729 cellCoords: CellKey;
730 element: CellElement;
731 elementIndex: number;
732 isHorizontal: boolean;
733 isSelected: boolean;
734 selectedBlockProps: WrapperBlockProps;
735 }) {
736 const sortPreviewMode = useTableStore(state => state.sortPreviewMode);
737 const setSelectedElement = useTableStore(state => state.setSelectedElement);
738 const addSelectedCells = useTableStore(state => state.addSelectedCells);
739 const clearSelectedElement = useTableStore(
740 state => state.clearSelectedElement
741 );
742 const updateBlockCard = useBlockCardUpdateShim();
743 const { selectBlock } = useDispatch(
744 blockEditorStore
745 ) as unknown as BlockEditorActions;
746
747 const mergedElementBlockProps = mergeWrapperBlockProps(
748 isSelected ? selectedBlockProps : undefined,
749 classNames("tableberg-cell-element", {
750 "is-selected": isSelected,
751 }),
752 {
753 display: "flex",
754 justifyContent: elementAlignmentToJustifyContent(
755 getElementAlignment(element)
756 ),
757 width: isHorizontal ? undefined : "100%",
758 }
759 );
760
761 // Modifier+click on element content should select the whole cell for
762 // merging instead of the element. Handle it in the capture phase so the
763 // element's own click handler (which stops propagation) never runs.
764 const handleClickCapture = (e: MouseEvent<HTMLDivElement>) => {
765 if (e.ctrlKey || e.metaKey || e.shiftKey) {
766 e.preventDefault();
767 e.stopPropagation();
768 selectTableBlockFromElement(e.currentTarget, selectBlock);
769 clearSelectedElement();
770 addSelectedCells([cellCoords]);
771 return;
772 }
773
774 if (sortPreviewMode) {
775 return;
776 }
777
778 selectTableBlockFromElement(e.currentTarget, selectBlock);
779 setSelectedElement(cellCoords, elementIndex);
780 updateBlockCard(
781 e.currentTarget,
782 element.name,
783 `A ${element.name} element within a Tableberg cell`
784 );
785 };
786
787 const handleClick = (e: MouseEvent<HTMLDivElement>) => {
788 e.stopPropagation();
789 };
790
791 return (
792 <div
793 {...mergedElementBlockProps}
794 onClickCapture={handleClickCapture}
795 onClick={handleClick}
796 >
797 {children}
798 </div>
799 );
800 }
801