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

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

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