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 / table / controls.tsx

controls.tsx in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/blocks/table/controls.tsx

1,578 lines 62.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress Imports
3 */
4 import { __, sprintf } from "@wordpress/i18n";
5 import {
6 positionLeft,
7 positionCenter,
8 positionRight,
9 cog,
10 styles as stylesIcon,
11 caption as captionIcon,
12 table,
13 tableRowBefore,
14 arrowDown,
15 arrowRight,
16 arrowUp,
17 justifyLeft,
18 justifyCenter,
19 justifyRight,
20 } from "@wordpress/icons";
21 import {
22 InspectorControls,
23 BlockControls,
24 FontSizePicker,
25 ColorPalette,
26 useBlockEditContext,
27 store as blockEditorStore,
28 } from "@wordpress/block-editor";
29 import {
30 ToggleControl,
31 __experimentalToolsPanel as ToolsPanel,
32 __experimentalToolsPanelItem as ToolsPanelItem,
33 __experimentalToggleGroupControl as ToggleGroupControl,
34 __experimentalToggleGroupControlOption as ToggleGroupControlOption,
35 __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon,
36 PanelBody,
37 ToolbarButton,
38 ToolbarGroup,
39 SelectControl,
40 Button,
41 Dropdown,
42 TabPanel,
43 } from "@wordpress/components";
44 import { ReactNode, useState } from "react";
45 /**
46 * Internal Imports
47 */
48 import { useDispatch, useRegistry, useSelect } from "@wordpress/data";
49 import {
50 SpacingControl,
51 SpacingControlSingle,
52 ColorControl,
53 ToolbarWithDropdown,
54 SizeControl,
55 BorderControl,
56 BorderRadiusControl,
57 } from "@tableberg/components";
58 import LockedControl from "../../components/LockedControl";
59 import { useTableStore } from "../../store";
60 import TablebergIcon from "@tableberg/shared/icons/tableberg";
61 import { DuplicateRowIcon } from "@tableberg/shared/icons/enhancements";
62 import { areAllMergeable, mergeCells } from "../../merge";
63 import {
64 attrDefaults,
65 Cell,
66 CellKey,
67 CellSpacing,
68 PaginationConfig,
69 SortableType,
70 TableAlignment,
71 } from "../../attributes";
72 import { hasSortableColumns, tableHasMergedCells } from "../../sorting";
73 import { tableHasRowSpanningCells } from "../../pagination";
74 import { useBackgroundColorHelpers, useCellStyleControl } from "../../hooks";
75 import { ResponsiveControl } from "../../components/ResponsiveControl";
76 import {
77 ElementAlignment,
78 getUniformElementsAlignment,
79 setElementAlignment,
80 } from "../../alignment";
81 import { isProAvailable } from "../../pro-status";
82 import { UpsellEnhancedModal } from "../../components/UpsellModal";
83 import { AdvancedCustomClassControl } from "../../components/AdvancedCustomClassControl";
84
85 const cellDefaultsStyles = attrDefaults.cellDefaults.styles;
86
87 const TABLE_ALIGNMENT_TOOLBAR_CONTROLS: {
88 value: TableAlignment;
89 icon: JSX.Element;
90 title: string;
91 }[] = [
92 {
93 value: "left",
94 icon: positionLeft,
95 title: __("Align left", "tableberg"),
96 },
97 {
98 value: "center",
99 icon: positionCenter,
100 title: __("Align center", "tableberg"),
101 },
102 {
103 value: "right",
104 icon: positionRight,
105 title: __("Align right", "tableberg"),
106 },
107 ];
108
109 type TableWidthMode = "auto" | "fixed" | "wide" | "full";
110
111 type SidebarTab = "settings" | "styles" | "datatable";
112
113 const DEFAULT_FIXED_TABLE_WIDTH = "350px";
114
115 const getCellEntries = (cells: Record<CellKey, Cell>) =>
116 Object.entries(cells).map(
117 ([key, cell]) => [key as CellKey, cell.elements || []] as const
118 );
119
120 const resetToolsPanelFilters = (filters: (() => unknown)[] = []) => {
121 filters.forEach(filter => {
122 filter();
123 });
124 };
125
126 const TABLE_WIDTH_PRESET_VALUES = ["auto", "wide", "full"] as const;
127
128 const isTableWidthPreset = (
129 value: string
130 ): value is (typeof TABLE_WIDTH_PRESET_VALUES)[number] =>
131 (TABLE_WIDTH_PRESET_VALUES as readonly string[]).includes(value);
132
133 const CELL_ORIENTATION_OPTIONS = [
134 {
135 value: "vertical",
136 icon: arrowDown,
137 label: __("Vertical", "tableberg"),
138 },
139 {
140 value: "horizontal",
141 icon: arrowRight,
142 label: __("Horizontal", "tableberg"),
143 },
144 ] as const;
145
146 const CELL_ALIGNMENT_OPTIONS = [
147 {
148 value: "left",
149 icon: justifyLeft,
150 label: __("Left", "tableberg"),
151 },
152 {
153 value: "center",
154 icon: justifyCenter,
155 label: __("Center", "tableberg"),
156 },
157 {
158 value: "right",
159 icon: justifyRight,
160 label: __("Right", "tableberg"),
161 },
162 ] as const;
163
164 const CELL_VERTICAL_ALIGN_OPTIONS = [
165 {
166 value: "top",
167 icon: arrowUp,
168 label: __("Top", "tableberg"),
169 },
170 {
171 value: "middle",
172 icon: positionCenter,
173 label: __("Middle", "tableberg"),
174 },
175 {
176 value: "bottom",
177 icon: arrowDown,
178 label: __("Bottom", "tableberg"),
179 },
180 ] as const;
181
182 type FourSidesSpacing = {
183 top: string;
184 right: string;
185 bottom: string;
186 left: string;
187 };
188
189 const tableConfigDefaults = attrDefaults.table;
190
191 function getDefaultCellSpacing(): CellSpacing {
192 const defaultCellSpacing = tableConfigDefaults.cellSpacing!!;
193
194 return {
195 horizontal: defaultCellSpacing.horizontal,
196 vertical: defaultCellSpacing.vertical,
197 };
198 }
199
200 function toCellSpacingPaddingValue(cellSpacing: CellSpacing) {
201 return {
202 top: cellSpacing.vertical,
203 right: cellSpacing.horizontal,
204 bottom: cellSpacing.vertical,
205 left: cellSpacing.horizontal,
206 };
207 }
208
209 function fromCellSpacingPaddingValue(
210 newPadding: FourSidesSpacing,
211 previousCellSpacing: CellSpacing,
212 defaultCellSpacing: CellSpacing
213 ) {
214 const previousPadding = toCellSpacingPaddingValue(previousCellSpacing);
215
216 let horizontal = previousCellSpacing.horizontal;
217 if (newPadding.right !== previousPadding.right) {
218 horizontal = newPadding.right;
219 } else if (newPadding.left !== previousPadding.left) {
220 horizontal = newPadding.left;
221 }
222
223 let vertical = previousCellSpacing.vertical;
224 if (newPadding.top !== previousPadding.top) {
225 vertical = newPadding.top;
226 } else if (newPadding.bottom !== previousPadding.bottom) {
227 vertical = newPadding.bottom;
228 }
229
230 return {
231 horizontal: horizontal || defaultCellSpacing.horizontal,
232 vertical: vertical || defaultCellSpacing.vertical,
233 };
234 }
235
236 interface ElementFontOptionControlProps {
237 label: string;
238 onSelect: (value: string) => void;
239 onReset: () => void;
240 }
241
242 function ElementFontColorOptionControl({
243 label,
244 onSelect,
245 onReset,
246 }: ElementFontOptionControlProps) {
247 return (
248 <div className="tableberg-element-font-option">
249 <Dropdown
250 className="block-editor-tools-panel-color-gradient-settings__dropdown"
251 popoverProps={{ placement: "bottom-start" }}
252 renderToggle={({ isOpen, onToggle }) => (
253 <Button
254 __next40pxDefaultSize
255 onClick={onToggle}
256 aria-expanded={isOpen}
257 className={`block-editor-panel-color-gradient-settings__dropdown tableberg-element-font-option-toggle${
258 isOpen ? " is-open" : ""
259 }`}
260 >
261 <span className="tableberg-element-font-option-label">
262 {label}
263 </span>
264 </Button>
265 )}
266 renderContent={({ onClose }) => (
267 <div className="tableberg-element-font-option-popover">
268 <ColorPalette
269 value={undefined}
270 clearable={false}
271 onChange={newValue => {
272 if (!newValue) {
273 return;
274 }
275
276 onSelect(newValue);
277 onClose();
278 }}
279 />
280 <Button
281 __next40pxDefaultSize
282 className="components-circular-option-picker__clear"
283 variant="tertiary"
284 onClick={() => {
285 onReset();
286 onClose();
287 }}
288 >
289 {__("Reset", "tableberg")}
290 </Button>
291 </div>
292 )}
293 />
294 </div>
295 );
296 }
297
298 function ElementFontSizeOptionControl({
299 label,
300 onSelect,
301 onReset,
302 }: ElementFontOptionControlProps) {
303 return (
304 <div className="tableberg-element-font-option">
305 <Dropdown
306 className="block-editor-tools-panel-color-gradient-settings__dropdown"
307 popoverProps={{ placement: "bottom-start" }}
308 renderToggle={({ isOpen, onToggle }) => (
309 <Button
310 __next40pxDefaultSize
311 onClick={onToggle}
312 aria-expanded={isOpen}
313 className={`block-editor-panel-color-gradient-settings__dropdown tableberg-element-font-option-toggle${
314 isOpen ? " is-open" : ""
315 }`}
316 >
317 <span className="tableberg-element-font-option-label">
318 {label}
319 </span>
320 </Button>
321 )}
322 renderContent={({ onClose }) => (
323 <div
324 className="tableberg-element-font-option-popover tableberg-element-font-option-popover--font-size"
325 onKeyDown={event => {
326 if (event.key === "Enter") {
327 onClose();
328 }
329 }}
330 >
331 <FontSizePicker
332 value={undefined}
333 withReset={false}
334 onChange={fontSize => {
335 if (!fontSize) {
336 return;
337 }
338
339 onSelect(String(fontSize));
340 }}
341 />
342 <Button
343 __next40pxDefaultSize
344 className="components-circular-option-picker__clear"
345 variant="tertiary"
346 onClick={() => {
347 onReset();
348 onClose();
349 }}
350 >
351 {__("Reset to default", "tableberg")}
352 </Button>
353 </div>
354 )}
355 />
356 </div>
357 );
358 }
359
360 /** Everything pro's column-sorting UI needs, gathered from the store. */
361 export interface ColumnSortingContext {
362 columnNumbers: number[];
363 getColumnConfig: (column: number) => ColumnConfig | undefined;
364 isColumnSortableAllowed: (column: number) => boolean;
365 setColumnSortable: (
366 column: number,
367 sortable: SortableType | undefined
368 ) => void;
369 hasSortableCols: boolean;
370 sortPreviewMode: boolean;
371 enterSortPreviewMode: () => void;
372 }
373
374 function ColumnSortingControl({
375 ProColumnSortingContent,
376 }: {
377 // Injected by the pro plugin; undefined when pro is not installed. A
378 // render function rather than a plain node: the merged-cells check and
379 // the live preview toggle both need store state pro cannot reach from
380 // outside (it runs above TableStoreProvider), so free gathers the
381 // context and pro only renders from it.
382 ProColumnSortingContent?: (ctx: ColumnSortingContext) => ReactNode;
383 }) {
384 const cells = useTableStore(state => state.cells);
385 const columns = useTableStore(state => state.columns);
386 const tableConfig = useTableStore(state => state.table);
387 const sortPreviewMode = useTableStore(state => state.sortPreviewMode);
388 const getColumnConfig = useTableStore(state => state.getColumnConfig);
389 const setColumnSortable = useTableStore(state => state.setColumnSortable);
390 const isColumnSortableAllowed = useTableStore(
391 state => state.isColumnSortableAllowed
392 );
393 const enterSortPreviewMode = useTableStore(
394 state => state.enterSortPreviewMode
395 );
396
397 const { headerEnabled } = tableConfig;
398
399 if (!headerEnabled) {
400 return (
401 <InspectorControls>
402 <PanelBody title={__("Column Sorting", "tableberg")}>
403 <p style={{ color: "#757575" }}>
404 {__(
405 "To enable column sorting, make the top row a header in the Header & Footer Settings panel.",
406 "tableberg"
407 )}
408 </p>
409 </PanelBody>
410 </InspectorControls>
411 );
412 }
413
414 if (tableHasMergedCells(cells)) {
415 return (
416 <InspectorControls>
417 <PanelBody title={__("Column Sorting", "tableberg")}>
418 <p style={{ color: "#757575" }}>
419 {__(
420 "Column sorting is not available for tables with merged cells.",
421 "tableberg"
422 )}
423 </p>
424 </PanelBody>
425 </InspectorControls>
426 );
427 }
428
429 const columnNumbers = Array.from(
430 { length: tableConfig.cols },
431 (_, index) => index
432 );
433 const hasSortableCols = hasSortableColumns(columns);
434
435 if (!ProColumnSortingContent) {
436 return (
437 <InspectorControls>
438 <LockedControl isEnhanced selected="sorting">
439 <PanelBody title={__("Column Sorting", "tableberg")}>
440 {columnNumbers.map(column => (
441 <ToggleControl
442 key={column}
443 checked={false}
444 label={sprintf(
445 __("Column %d", "tableberg"),
446 column + 1
447 )}
448 onChange={() => null}
449 />
450 ))}
451 </PanelBody>
452 </LockedControl>
453 </InspectorControls>
454 );
455 }
456
457 return (
458 <InspectorControls>
459 <PanelBody
460 title={__("Column Sorting", "tableberg")}
461 initialOpen={hasSortableCols}
462 >
463 {ProColumnSortingContent({
464 columnNumbers,
465 getColumnConfig,
466 isColumnSortableAllowed,
467 setColumnSortable,
468 hasSortableCols,
469 sortPreviewMode,
470 enterSortPreviewMode,
471 })}
472 </PanelBody>
473 </InspectorControls>
474 );
475 }
476
477 /** Everything pro's pagination UI needs, gathered from the store. */
478 export interface PaginationContext {
479 paginationConfig: PaginationConfig;
480 updatePaginationConfig: (patch: Partial<PaginationConfig>) => void;
481 }
482
483 function PaginationControl({
484 ProPaginationContent,
485 }: {
486 // Injected by the pro plugin; undefined when pro is not installed. A
487 // render function rather than a plain node: the row-span check needs
488 // block-tree `cells` pro cannot reach from outside (it runs above
489 // TableStoreProvider), and setPaginationConfig also clamps the store's
490 // ephemeral currentPage, so free gathers the context and pro only
491 // renders from it.
492 ProPaginationContent?: (ctx: PaginationContext) => ReactNode;
493 }) {
494 const cells = useTableStore(state => state.cells);
495 const paginationConfig = useTableStore(state => state.table.pagination!);
496 const setPaginationConfig = useTableStore(
497 state => state.setPaginationConfig
498 );
499
500 if (!ProPaginationContent) {
501 return (
502 <InspectorControls>
503 <LockedControl isEnhanced selected="pagination">
504 <PanelBody title={__("Pagination", "tableberg")}>
505 <ToggleControl
506 checked={false}
507 label={__("Enable Pagination", "tableberg")}
508 onChange={() => null}
509 />
510 </PanelBody>
511 </LockedControl>
512 </InspectorControls>
513 );
514 }
515
516 if (tableHasRowSpanningCells(cells)) {
517 return (
518 <InspectorControls>
519 <PanelBody title={__("Pagination", "tableberg")}>
520 <p style={{ color: "#757575" }}>
521 {__(
522 "Pagination is not available for tables with row-spanning cells (cells that span multiple rows).",
523 "tableberg"
524 )}
525 </p>
526 </PanelBody>
527 </InspectorControls>
528 );
529 }
530
531 return (
532 <InspectorControls>
533 <PanelBody title={__("Pagination", "tableberg")}>
534 {ProPaginationContent({
535 paginationConfig,
536 updatePaginationConfig: setPaginationConfig,
537 })}
538 </PanelBody>
539 </InspectorControls>
540 );
541 }
542
543 /**
544 * Only the table-wide switch lives here. The per-column width and per-row
545 * height are set on the cell you select, from the cell block's sidebar —
546 * that is where the selection actually is in the block editor.
547 */
548 function ColumnAndRowDimensionsControl() {
549 const tableConfig = useTableStore(state => state.table);
550 const updateTableConfig = useTableStore(state => state.updateTable);
551
552 if (tableConfig.cols < 1 && tableConfig.rows < 1) {
553 return null;
554 }
555
556 return (
557 <InspectorControls>
558 <PanelBody title={__("Column & Row Dimensions", "tableberg")}>
559 <ToggleControl
560 checked={tableConfig.fixedColumnWidths ?? true}
561 label={__("Equal width columns", "tableberg")}
562 onChange={(enabled: boolean) => {
563 updateTableConfig({ fixedColumnWidths: enabled });
564 }}
565 />
566 <p style={{ marginBottom: 0, color: "#757575" }}>
567 {__(
568 "Select a cell to set the width of its column and the height of its row.",
569 "tableberg"
570 )}
571 </p>
572 </PanelBody>
573 </InspectorControls>
574 );
575 }
576
577 interface TablebergControlsProps {
578 // Injected by the pro plugin via NativeTableEdit's generic Pro*-prop
579 // forwarding (same mechanism the element bridge uses for cell elements).
580 // Undefined when pro is not installed.
581 ProStickyHeaderControl?: ReactNode;
582 ProStickyFirstColControl?: ReactNode;
583 ProCellOrientationControl?: ReactNode;
584 ProBorderModeControls?: ReactNode;
585 ProColumnSortingContent?: (ctx: ColumnSortingContext) => ReactNode;
586 ProSearchControl?: ReactNode;
587 ProPaginationContent?: (ctx: PaginationContext) => ReactNode;
588 }
589
590 function TablebergControls({
591 ProStickyHeaderControl,
592 ProStickyFirstColControl,
593 ProCellOrientationControl,
594 ProBorderModeControls,
595 ProColumnSortingContent,
596 ProSearchControl,
597 ProPaginationContent,
598 }: TablebergControlsProps = {}) {
599 const isPro = isProAvailable();
600 const { clientId } = useBlockEditContext();
601 const tableConfig = useTableStore(state => state.table);
602 const cells = useTableStore(state => state.cells);
603 const updateConfig = useTableStore(state => state.updateTable);
604 const setCells = useTableStore(state => state.setCells);
605 const selectedCells = useTableStore(state => state.selectedCells);
606 const tableState = useTableStore(state => state);
607 const sortPreviewMode = useTableStore(state => state.sortPreviewMode);
608 const selectedRibbonCell = useTableStore(state => state.selectedRibbonCell);
609 const showCaption = useTableStore(state => state.showCaption);
610 const setShowCaption = useTableStore(state => state.setShowCaption);
611 const showRowColumnControls = useTableStore(
612 state => state.showRowColumnControls
613 );
614 const showDuplicateMoveControls = useTableStore(
615 state => state.showDuplicateMoveControls
616 );
617 const toggleRowColumnControls = useTableStore(
618 state => state.toggleRowColumnControls
619 );
620 const toggleDuplicateMoveControls = useTableStore(
621 state => state.toggleDuplicateMoveControls
622 );
623 const caption = tableConfig.caption || "";
624 const [activeTab, setActiveTab] = useState<SidebarTab>("settings");
625 const [showDuplicateMoveUpsell, setShowDuplicateMoveUpsell] =
626 useState(false);
627
628 const isRibbonSelected = selectedRibbonCell !== null;
629 const tableAlignment: TableAlignment = tableConfig.tableAlignment || "left";
630 const tableWidth = (tableConfig.tableWidth || "auto").trim();
631 const tableWidthMode: TableWidthMode = isTableWidthPreset(tableWidth)
632 ? tableWidth
633 : "fixed";
634 const isFixedWidthMode = tableWidthMode === "fixed";
635 const fixedTableWidthValue = isFixedWidthMode
636 ? tableWidth || DEFAULT_FIXED_TABLE_WIDTH
637 : DEFAULT_FIXED_TABLE_WIDTH;
638 const defaultCellSpacing = getDefaultCellSpacing();
639 const cellSpacing = tableConfig.cellSpacing || defaultCellSpacing;
640 const horizontalCellSpacing =
641 cellSpacing.horizontal || defaultCellSpacing.horizontal;
642 const verticalCellSpacing =
643 cellSpacing.vertical || defaultCellSpacing.vertical;
644 const defaultTableBorder = tableConfigDefaults.tableBorder!!;
645 const tableBorder = tableConfig.tableBorder || defaultTableBorder;
646
647 const tableBorderControlProps = {
648 label: __("Table Border", "tableberg"),
649 value: tableBorder,
650 hasValue: () =>
651 !!tableBorder.top ||
652 !!tableBorder.right ||
653 !!tableBorder.bottom ||
654 !!tableBorder.left,
655 onChange: (newBorder: typeof tableBorder) => {
656 updateConfig({ tableBorder: newBorder });
657 },
658 onDeselect: () => {
659 updateConfig({ tableBorder: { ...defaultTableBorder } });
660 },
661 };
662
663 const defaultBlockMargin = tableConfigDefaults.margin!!;
664 const blockMargin = tableConfig.margin || defaultBlockMargin;
665 const blockMarginControlProps = {
666 label: __("Block Margin", "tableberg"),
667 value: blockMargin,
668 hasValue: () =>
669 !!blockMargin.top ||
670 !!blockMargin.right ||
671 !!blockMargin.bottom ||
672 !!blockMargin.left,
673 onChange: (newMargin: typeof blockMargin) => {
674 updateConfig({ margin: newMargin });
675 },
676 onDeselect: () => {
677 updateConfig({ margin: { ...defaultBlockMargin } });
678 },
679 };
680
681 const defaultBlockPadding = tableConfigDefaults.padding!!;
682 const blockPadding = tableConfig.padding || defaultBlockPadding;
683 const blockPaddingControlProps = {
684 label: __("Block Padding", "tableberg"),
685 value: blockPadding,
686 hasValue: () =>
687 !!blockPadding.top ||
688 !!blockPadding.right ||
689 !!blockPadding.bottom ||
690 !!blockPadding.left,
691 onChange: (newPadding: typeof blockPadding) => {
692 updateConfig({ padding: newPadding });
693 },
694 onDeselect: () => {
695 updateConfig({ padding: { ...defaultBlockPadding } });
696 },
697 };
698
699 const borderControlProps = useCellStyleControl({
700 styleKey: "border",
701 defaultValue: cellDefaultsStyles.border,
702 label: __("Cell Border", "tableberg"),
703 labelSelected: __("Common Cell Border", "tableberg"),
704 hasValue: border =>
705 !!border.top || !!border.right || !!border.bottom || !!border.left,
706 });
707
708 const borderRadiusControlProps = useCellStyleControl({
709 styleKey: "borderRadius",
710 defaultValue: cellDefaultsStyles.borderRadius,
711 label: __("Table Border Radius", "tableberg"),
712 labelSelected: __("Cell Border Radius", "tableberg"),
713 hasValue: borderRadius =>
714 !!borderRadius.topLeft ||
715 !!borderRadius.topRight ||
716 !!borderRadius.bottomRight ||
717 !!borderRadius.bottomLeft,
718 });
719
720 const paddingControlProps = useCellStyleControl({
721 styleKey: "padding",
722 defaultValue: cellDefaultsStyles.padding,
723 hasValue: padding =>
724 !!padding.top ||
725 !!padding.right ||
726 !!padding.bottom ||
727 !!padding.left,
728 label: __("Common Cell Padding", "tableberg"),
729 labelSelected: __("Cell Padding", "tableberg"),
730 });
731
732 const backgroundColorControlProps = useCellStyleControl({
733 styleKey: "backgroundColor",
734 defaultValue: cellDefaultsStyles.backgroundColor,
735 hasValue: bg => !!bg,
736 label: __("Common Cell Background Color", "tableberg"),
737 labelSelected: __("Cell Background Color", "tableberg"),
738 });
739
740 const elementGapControlProps = useCellStyleControl({
741 styleKey: "elementGap",
742 defaultValue: cellDefaultsStyles.elementGap,
743 hasValue: elementGap => elementGap !== cellDefaultsStyles.elementGap,
744 label: __("Common Element Spacing", "tableberg"),
745 labelSelected: __("Element Spacing", "tableberg"),
746 });
747
748 const selectedCellKeys = new Set(selectedCells);
749 const cellEntries = getCellEntries(cells);
750
751 const scopedCellEntries =
752 selectedCells.length > 0
753 ? cellEntries.filter(([cellKey]) => selectedCellKeys.has(cellKey))
754 : cellEntries;
755
756 const scopedElements = scopedCellEntries.flatMap(
757 ([, elements]) => elements
758 );
759
760 const fontOptionsRegistry = useRegistry() as any;
761
762 // Element alignment lives on the element BLOCKS in the tree now.
763 const forEachElementBlock = (
764 visit: (elBlock: any, dispatch: any) => void
765 ) => {
766 const be = fontOptionsRegistry.select(blockEditorStore);
767 const beDispatch = fontOptionsRegistry.dispatch(blockEditorStore);
768 const rowBlocks = be.getBlock(clientId)?.innerBlocks ?? [];
769
770 fontOptionsRegistry.batch(() => {
771 for (const rowBlock of rowBlocks) {
772 for (const cellBlock of rowBlock.innerBlocks ?? []) {
773 for (const elBlock of cellBlock.innerBlocks ?? []) {
774 visit(elBlock, beDispatch);
775 }
776 }
777 }
778 });
779 };
780
781 const elementsAlignmentValue = (() => {
782 const be = fontOptionsRegistry.select(blockEditorStore);
783 const rowBlocks = be.getBlock(clientId)?.innerBlocks ?? [];
784 let uniform: ElementAlignment | undefined;
785 for (const rowBlock of rowBlocks) {
786 for (const cellBlock of rowBlock.innerBlocks ?? []) {
787 for (const elBlock of cellBlock.innerBlocks ?? []) {
788 const align = elBlock.attributes?.align as
789 | ElementAlignment
790 | undefined;
791 if (!align) {
792 continue;
793 }
794 if (uniform === undefined) {
795 uniform = align;
796 } else if (uniform !== align) {
797 return undefined;
798 }
799 }
800 }
801 }
802 return uniform;
803 })();
804
805 const elementsAlignmentControlProps = {
806 label: __("Common Elements Alignment", "tableberg"),
807 value: elementsAlignmentValue,
808 onChange: (newAlignment: ElementAlignment) => {
809 forEachElementBlock((elBlock, dispatch) => {
810 dispatch.updateBlockAttributes(elBlock.clientId, {
811 align: newAlignment,
812 });
813 });
814 },
815 };
816
817
818 // Bulk-updates every text/list ELEMENT BLOCK in the table's tree (the
819 // legacy store path below only worked when cell content lived in attrs).
820 const updateElementFontOptions = (updates: {
821 textColor?: string;
822 linkColor?: string;
823 fontSize?: string;
824 }) => {
825 const be = fontOptionsRegistry.select(blockEditorStore);
826 const beDispatch = fontOptionsRegistry.dispatch(blockEditorStore);
827 const rowBlocks = be.getBlock(clientId)?.innerBlocks ?? [];
828
829 fontOptionsRegistry.batch(() => {
830 for (const rowBlock of rowBlocks) {
831 for (const cellBlock of rowBlock.innerBlocks ?? []) {
832 for (const elBlock of cellBlock.innerBlocks ?? []) {
833 if (
834 elBlock.name !== "tableberg/text" &&
835 elBlock.name !== "tableberg/list"
836 ) {
837 continue;
838 }
839 beDispatch.updateBlockAttributes(elBlock.clientId, {
840 styles: {
841 ...(elBlock.attributes?.styles ?? {}),
842 ...(updates.textColor !== undefined
843 ? { textColor: updates.textColor }
844 : {}),
845 ...(updates.linkColor !== undefined
846 ? { linkColor: updates.linkColor }
847 : {}),
848 ...(updates.fontSize !== undefined
849 ? { fontSize: updates.fontSize }
850 : {}),
851 },
852 });
853 }
854 }
855 }
856 });
857 };
858
859 const legacyUpdateElementFontOptions = (updates: {
860 textColor?: string;
861 linkColor?: string;
862 fontSize?: string;
863 }) => {
864 const nextCells = Object.fromEntries(
865 cellEntries.map(([key, elements]) => {
866 if (selectedCells.length > 0 && !selectedCellKeys.has(key)) {
867 return [key, cells[key]];
868 }
869
870 return [
871 key,
872 {
873 ...(cells[key] || {}),
874 elements: elements.map(element => {
875 if (
876 element.name === "text" ||
877 element.name === "list"
878 ) {
879 return {
880 ...element,
881 attributes: {
882 ...element.attributes,
883 styles: {
884 ...element.attributes.styles,
885 ...(updates.textColor !== undefined
886 ? {
887 textColor:
888 updates.textColor,
889 }
890 : {}),
891 ...(updates.linkColor !== undefined
892 ? {
893 linkColor:
894 updates.linkColor,
895 }
896 : {}),
897 ...(updates.fontSize !== undefined
898 ? { fontSize: updates.fontSize }
899 : {}),
900 },
901 },
902 };
903 }
904
905 if (element.name === "star-rating") {
906 return {
907 ...element,
908 attributes: {
909 ...element.attributes,
910 ...(updates.textColor !== undefined
911 ? {
912 reviewTextColor:
913 updates.textColor,
914 }
915 : {}),
916 ...(updates.linkColor !== undefined
917 ? {
918 reviewTextLinkColor:
919 updates.linkColor,
920 }
921 : {}),
922 ...(updates.fontSize !== undefined
923 ? {
924 reviewTextFontSize:
925 updates.fontSize,
926 }
927 : {}),
928 },
929 };
930 }
931
932 return element;
933 }),
934 },
935 ];
936 })
937 ) as Record<CellKey, Cell>;
938
939 setCells(nextCells);
940 };
941
942 const verticalAlignControlProps = useCellStyleControl({
943 styleKey: "verticalAlign",
944 defaultValue: cellDefaultsStyles.verticalAlign,
945 hasValue: verticalAlign =>
946 verticalAlign !== cellDefaultsStyles.verticalAlign,
947 label: __("Common Elements Vertical Alignment", "tableberg"),
948 labelSelected: __("Elements Vertical Alignment", "tableberg"),
949 });
950
951 const {
952 headerBackgroundColorControl,
953 evenRowBackgroundColorControl,
954 oddRowBackgroundColorControl,
955 footerBackgroundColorControl,
956 } = useBackgroundColorHelpers();
957
958 if (sortPreviewMode) {
959 return null;
960 }
961
962 return (
963 <>
964 <BlockControls>
965 <ToolbarWithDropdown
966 title={__("Align table", "tableberg")}
967 value={tableAlignment}
968 controls={TABLE_ALIGNMENT_TOOLBAR_CONTROLS}
969 disabled={!isFixedWidthMode}
970 onChange={(newAlignment?: string) => {
971 if (!isFixedWidthMode || !newAlignment) {
972 return;
973 }
974
975 updateConfig({
976 tableAlignment: newAlignment as TableAlignment,
977 });
978 }}
979 />
980 <ToolbarGroup>
981 <ToolbarButton
982 onClick={() => {
983 const nextShowCaption = !showCaption;
984 setShowCaption(nextShowCaption);
985
986 if (!nextShowCaption && caption.trim() !== "") {
987 updateConfig({ caption: "" });
988 }
989 }}
990 icon={captionIcon}
991 isPressed={showCaption}
992 label={
993 showCaption
994 ? __("Remove caption", "tableberg")
995 : __("Add caption", "tableberg")
996 }
997 />
998 <ToolbarButton
999 onClick={() => {
1000 mergeCells(selectedCells, tableState);
1001 }}
1002 title="Merge Cells"
1003 icon={table}
1004 disabled={
1005 !areAllMergeable(selectedCells, tableState.cells)
1006 }
1007 />
1008 </ToolbarGroup>
1009 </BlockControls>
1010
1011 <InspectorControls>
1012 <TabPanel
1013 className="tableberg-sidebar-tabs"
1014 initialTabName="settings"
1015 onSelect={selectedTab => {
1016 if (
1017 selectedTab === "settings" ||
1018 selectedTab === "styles" ||
1019 selectedTab === "datatable"
1020 ) {
1021 setActiveTab(selectedTab);
1022 }
1023 }}
1024 tabs={[
1025 {
1026 name: "settings",
1027 title: __("Settings", "tableberg"),
1028 icon: cog,
1029 },
1030 {
1031 name: "styles",
1032 title: __("Styles", "tableberg"),
1033 icon: stylesIcon,
1034 },
1035 {
1036 name: "datatable",
1037 title: __("Datatable", "tableberg"),
1038 icon: table,
1039 },
1040 ]}
1041 >
1042 {() => null}
1043 </TabPanel>
1044 </InspectorControls>
1045
1046 {selectedCells.length === 0 ? (
1047 <AdvancedCustomClassControl
1048 label={__("Additional CSS class(es)", "tableberg")}
1049 value={tableConfig.className}
1050 onChange={className => {
1051 updateConfig({
1052 className: className || "",
1053 });
1054 }}
1055 />
1056 ) : (
1057 <AdvancedCustomClassControl
1058 label={__("Additional CSS class(es)", "tableberg")}
1059 value={
1060 selectedCells.every(
1061 key =>
1062 (cells[key]?.className || "") ===
1063 (cells[selectedCells[0]]?.className || "")
1064 )
1065 ? cells[selectedCells[0]]?.className
1066 : ""
1067 }
1068 onChange={className => {
1069 const nextClassName = className || "";
1070
1071 setCells(
1072 Object.fromEntries(
1073 Object.entries(cells).map(([key, cell]) => {
1074 if (
1075 !selectedCells.includes(key as CellKey)
1076 ) {
1077 return [key, cell];
1078 }
1079
1080 return [
1081 key,
1082 {
1083 ...cell,
1084 className: nextClassName,
1085 },
1086 ];
1087 })
1088 ) as Record<CellKey, Cell>
1089 );
1090 }}
1091 />
1092 )}
1093
1094 {activeTab === "settings" && (
1095 <>
1096 <InspectorControls>
1097 <PanelBody title={__("Table Width", "tableberg")}>
1098 <ToggleGroupControl
1099 label={__("Width Mode", "tableberg")}
1100 value={tableWidthMode}
1101 onChange={(
1102 newValue: string | number | undefined
1103 ) => {
1104 if (
1105 typeof newValue !== "string" ||
1106 !newValue
1107 ) {
1108 return;
1109 }
1110
1111 if (newValue === "fixed") {
1112 updateConfig({
1113 tableAlignment,
1114 tableWidth:
1115 tableWidthMode === "fixed"
1116 ? tableWidth
1117 : DEFAULT_FIXED_TABLE_WIDTH,
1118 });
1119 return;
1120 }
1121
1122 if (
1123 newValue === "auto" ||
1124 newValue === "wide" ||
1125 newValue === "full"
1126 ) {
1127 updateConfig({
1128 tableAlignment,
1129 tableWidth: newValue,
1130 });
1131 }
1132 }}
1133 isBlock
1134 >
1135 <ToggleGroupControlOption
1136 value="auto"
1137 label={__("Auto", "tableberg")}
1138 />
1139 <ToggleGroupControlOption
1140 value="fixed"
1141 label={__("Fixed", "tableberg")}
1142 />
1143 <ToggleGroupControlOption
1144 value="wide"
1145 label={__("Wide", "tableberg")}
1146 />
1147 <ToggleGroupControlOption
1148 value="full"
1149 label={__("Full", "tableberg")}
1150 />
1151 </ToggleGroupControl>
1152
1153 {isFixedWidthMode && (
1154 <SizeControl
1155 label={__("Table Width", "tableberg")}
1156 value={fixedTableWidthValue}
1157 onChange={newValue => {
1158 updateConfig({
1159 tableWidth: newValue
1160 ? newValue.trim()
1161 : DEFAULT_FIXED_TABLE_WIDTH,
1162 });
1163 }}
1164 />
1165 )}
1166 </PanelBody>
1167 </InspectorControls>
1168
1169 <InspectorControls>
1170 <PanelBody title={__("Cell Elements", "tableberg")}>
1171 {/*
1172 * Horizontal cell layout — and the wrap choice
1173 * that only matters with it — is a pro feature:
1174 * pro hands the whole control down as one prop.
1175 * Free has no implementation of its own, so
1176 * without pro the locked placeholder below is
1177 * all there is.
1178 */}
1179 {ProCellOrientationControl ?? (
1180 <LockedControl
1181 isEnhanced
1182 selected="cell-orientation"
1183 >
1184 <ToggleGroupControl
1185 __nextHasNoMarginBottom
1186 label={__(
1187 "Common Elements Orientation",
1188 "tableberg"
1189 )}
1190 value="vertical"
1191 isBlock
1192 onChange={() => null}
1193 >
1194 {CELL_ORIENTATION_OPTIONS.map(
1195 ({ value, icon, label }) => (
1196 <ToggleGroupControlOptionIcon
1197 key={value}
1198 value={value}
1199 icon={icon}
1200 label={label}
1201 />
1202 )
1203 )}
1204 </ToggleGroupControl>
1205 </LockedControl>
1206 )}
1207
1208 <SpacingControlSingle
1209 label={elementGapControlProps.label}
1210 value={elementGapControlProps.value}
1211 onChange={value => {
1212 elementGapControlProps.onChange(value);
1213 }}
1214 />
1215
1216 <ToggleGroupControl
1217 __nextHasNoMarginBottom
1218 label={elementsAlignmentControlProps.label}
1219 value={elementsAlignmentControlProps.value}
1220 isBlock
1221 onChange={newAlignment => {
1222 if (
1223 newAlignment !== "left" &&
1224 newAlignment !== "center" &&
1225 newAlignment !== "right"
1226 ) {
1227 return;
1228 }
1229
1230 elementsAlignmentControlProps.onChange(
1231 newAlignment
1232 );
1233 }}
1234 >
1235 {CELL_ALIGNMENT_OPTIONS.map(
1236 ({ value, icon, label }) => (
1237 <ToggleGroupControlOptionIcon
1238 key={value}
1239 value={value}
1240 icon={icon}
1241 label={label}
1242 />
1243 )
1244 )}
1245 </ToggleGroupControl>
1246
1247 <ToggleGroupControl
1248 __nextHasNoMarginBottom
1249 label={verticalAlignControlProps.label}
1250 value={verticalAlignControlProps.value}
1251 isBlock
1252 onChange={newVerticalAlign => {
1253 if (
1254 newVerticalAlign !== "top" &&
1255 newVerticalAlign !== "middle" &&
1256 newVerticalAlign !== "bottom"
1257 ) {
1258 return;
1259 }
1260
1261 verticalAlignControlProps.onChange(
1262 newVerticalAlign
1263 );
1264 }}
1265 >
1266 {CELL_VERTICAL_ALIGN_OPTIONS.map(
1267 ({ value, icon, label }) => (
1268 <ToggleGroupControlOptionIcon
1269 key={value}
1270 value={value}
1271 icon={icon}
1272 label={label}
1273 />
1274 )
1275 )}
1276 </ToggleGroupControl>
1277 </PanelBody>
1278 </InspectorControls>
1279
1280 <InspectorControls>
1281 <PanelBody
1282 title={__("Element Font Options", "tableberg")}
1283 >
1284 <div className="tableberg-element-font-options">
1285 <ElementFontColorOptionControl
1286 label={__(
1287 "Set all elements' text color",
1288 "tableberg"
1289 )}
1290 onSelect={textColor => {
1291 updateElementFontOptions({
1292 textColor,
1293 });
1294 }}
1295 onReset={() => {
1296 updateElementFontOptions({
1297 textColor: "#000000",
1298 });
1299 }}
1300 />
1301 <ElementFontColorOptionControl
1302 label={__(
1303 "Set all elements' link color",
1304 "tableberg"
1305 )}
1306 onSelect={linkColor => {
1307 updateElementFontOptions({
1308 linkColor,
1309 });
1310 }}
1311 onReset={() => {
1312 updateElementFontOptions({
1313 linkColor: "",
1314 });
1315 }}
1316 />
1317 <ElementFontSizeOptionControl
1318 label={__(
1319 "Set all elements' font size",
1320 "tableberg"
1321 )}
1322 onSelect={fontSize => {
1323 updateElementFontOptions({
1324 fontSize,
1325 });
1326 }}
1327 onReset={() => {
1328 updateElementFontOptions({
1329 fontSize: "1.38rem",
1330 });
1331 }}
1332 />
1333 </div>
1334 </PanelBody>
1335 </InspectorControls>
1336
1337 <InspectorControls>
1338 <PanelBody
1339 title={__("Header & Footer Settings", "tableberg")}
1340 >
1341 <ToggleControl
1342 checked={tableConfig.headerEnabled}
1343 label={__("Make Top Row Header", "tableberg")}
1344 onChange={(headerEnabled: boolean) => {
1345 updateConfig({ headerEnabled });
1346 }}
1347 />
1348 <ToggleControl
1349 checked={tableConfig.footerEnabled}
1350 label={__(
1351 "Make Bottom Row Footer",
1352 "tableberg"
1353 )}
1354 onChange={(footerEnabled: boolean) => {
1355 updateConfig({ footerEnabled });
1356 }}
1357 />
1358 {/*
1359 * Sticky header/first column are pro features:
1360 * pro hands the real toggles down as props. Free
1361 * has no implementation of its own, so without
1362 * pro the locked placeholders below are all
1363 * there is.
1364 */}
1365 {ProStickyHeaderControl ?? (
1366 <LockedControl
1367 isEnhanced
1368 selected="sticky-top-row"
1369 >
1370 <ToggleControl
1371 checked={false}
1372 label={__("Sticky Header", "tableberg")}
1373 help={__(
1374 "Available in Tableberg Pro.",
1375 "tableberg"
1376 )}
1377 onChange={() => null}
1378 />
1379 </LockedControl>
1380 )}
1381 {ProStickyFirstColControl ?? (
1382 <LockedControl
1383 isEnhanced
1384 selected="sticky-first-col"
1385 >
1386 <ToggleControl
1387 checked={false}
1388 label={__(
1389 "Sticky First Column",
1390 "tableberg"
1391 )}
1392 help={__(
1393 "Available in Tableberg Pro.",
1394 "tableberg"
1395 )}
1396 onChange={() => null}
1397 />
1398 </LockedControl>
1399 )}
1400 </PanelBody>
1401 </InspectorControls>
1402
1403 <ColumnAndRowDimensionsControl />
1404 <ResponsiveControl />
1405 </>
1406 )}
1407
1408 {activeTab === "styles" && (
1409 <>
1410 <InspectorControls>
1411 <ToolsPanel
1412 label={__("Colors", "tableberg")}
1413 resetAll={resetToolsPanelFilters}
1414 panelId={clientId}
1415 className="tableberg-colors-tools-panel"
1416 style={{ gap: "0" }}
1417 >
1418 {selectedCells.length > 0 && !isPro ? (
1419 <LockedControl isEnhanced selected="cell-bg">
1420 <ColorControl
1421 {...backgroundColorControlProps}
1422 />
1423 </LockedControl>
1424 ) : (
1425 <ColorControl
1426 {...backgroundColorControlProps}
1427 />
1428 )}
1429 {headerBackgroundColorControl && (
1430 <ColorControl
1431 {...headerBackgroundColorControl}
1432 />
1433 )}
1434 <ColorControl {...evenRowBackgroundColorControl} />
1435 <ColorControl {...oddRowBackgroundColorControl} />
1436 {footerBackgroundColorControl && (
1437 <ColorControl
1438 {...footerBackgroundColorControl}
1439 />
1440 )}
1441 </ToolsPanel>
1442 </InspectorControls>
1443
1444 <InspectorControls>
1445 <ToolsPanel
1446 label={__("Dimensions", "tableberg")}
1447 resetAll={resetToolsPanelFilters}
1448 panelId={clientId}
1449 >
1450 <div className="tableberg-border-mode-controls">
1451 {ProBorderModeControls ?? (
1452 <>
1453 <LockedControl
1454 isEnhanced
1455 selected="row-only-border"
1456 >
1457 <ToggleControl
1458 checked={false}
1459 label={__(
1460 "Row Only Border",
1461 "tableberg"
1462 )}
1463 onChange={() => null}
1464 />
1465 </LockedControl>
1466 <LockedControl
1467 isEnhanced
1468 selected="column-only-border"
1469 >
1470 <ToggleControl
1471 checked={false}
1472 label={__(
1473 "Column Only Border",
1474 "tableberg"
1475 )}
1476 onChange={() => null}
1477 />
1478 </LockedControl>
1479 </>
1480 )}
1481 </div>
1482 <BorderControl {...tableBorderControlProps} />
1483 <BorderControl {...borderControlProps} />
1484 <BorderRadiusControl
1485 {...borderRadiusControlProps}
1486 />
1487 <SpacingControl {...paddingControlProps} />
1488 <SpacingControl
1489 label={__("Cell Spacing", "tableberg")}
1490 value={toCellSpacingPaddingValue({
1491 horizontal: horizontalCellSpacing,
1492 vertical: verticalCellSpacing,
1493 })}
1494 hasValue={() =>
1495 horizontalCellSpacing !==
1496 defaultCellSpacing.horizontal ||
1497 verticalCellSpacing !==
1498 defaultCellSpacing.vertical
1499 }
1500 onChange={newValue => {
1501 updateConfig({
1502 cellSpacing:
1503 fromCellSpacingPaddingValue(
1504 newValue,
1505 {
1506 horizontal:
1507 horizontalCellSpacing,
1508 vertical:
1509 verticalCellSpacing,
1510 },
1511 defaultCellSpacing
1512 ),
1513 });
1514 }}
1515 onDeselect={() => {
1516 updateConfig({
1517 cellSpacing: { ...defaultCellSpacing },
1518 });
1519 }}
1520 />
1521 <SpacingControl {...blockMarginControlProps} />
1522 <SpacingControl {...blockPaddingControlProps} />
1523 </ToolsPanel>
1524 </InspectorControls>
1525 </>
1526 )}
1527
1528 {activeTab === "datatable" && (
1529 <>
1530 <ColumnSortingControl
1531 ProColumnSortingContent={ProColumnSortingContent}
1532 />
1533 <PaginationControl
1534 ProPaginationContent={ProPaginationContent}
1535 />
1536 {ProSearchControl ?? (
1537 <InspectorControls>
1538 <LockedControl isEnhanced selected="search">
1539 <ToolsPanel
1540 label={__("Search", "tableberg")}
1541 resetAll={() => null}
1542 >
1543 <ToolsPanelItem
1544 label={__(
1545 "Enable Search",
1546 "tableberg"
1547 )}
1548 hasValue={() => false}
1549 onDeselect={() => null}
1550 isShownByDefault
1551 >
1552 <ToggleControl
1553 checked={false}
1554 label={__(
1555 "Enable Search",
1556 "tableberg"
1557 )}
1558 onChange={() => null}
1559 />
1560 </ToolsPanelItem>
1561 </ToolsPanel>
1562 </LockedControl>
1563 </InspectorControls>
1564 )}
1565 </>
1566 )}
1567
1568 {showDuplicateMoveUpsell && (
1569 <UpsellEnhancedModal
1570 onClose={() => setShowDuplicateMoveUpsell(false)}
1571 selected="duplicate-row-col"
1572 />
1573 )}
1574 </>
1575 );
1576 }
1577 export default TablebergControls;
1578