PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / blocks / cell / column-border.tsx

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

203 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ReactNode } from "react";
2 import {
3 InspectorControls,
4 store as blockEditorStore,
5 } from "@wordpress/block-editor";
6 import { useDispatch, useRegistry, useSelect } from "@wordpress/data";
7 import { __ } from "@wordpress/i18n";
8 import { BorderControl } from "@tableberg/components";
9
10 import LockedControl from "../../components/LockedControl";
11 import { Border } from "../../attributes";
12 import { buildOccupancy, cellColumn, GridRow } from "../table/grid-model";
13
14 const EMPTY_BORDER: Border = { top: "", right: "", bottom: "", left: "" };
15
16 const hasBorder = (border: Border) =>
17 !!border.top || !!border.right || !!border.bottom || !!border.left;
18
19 export interface ColumnBorderContext {
20 columnBorderControlProps: {
21 label: string;
22 value: Border;
23 hasValue: () => boolean;
24 onChange: (newBorder: Border) => void;
25 onDeselect: () => void;
26 };
27 }
28
29 /**
30 * Column border — bulk-apply the picked border to every cell in the
31 * selected cell's column, mirroring `CellColumnBackgroundControls` (and,
32 * further back, `CellDimensionsControls`'s column width): a column has no
33 * block of its own, so this is set from whichever cell you have selected.
34 * Writes each target cell's own `border` attribute — the same one the
35 * individual per-cell border control uses, with the same legacy
36 * `styles.border` fallback (tables where it lived before it got its own
37 * attribute). Deliberately not `styles.border` directly: that key is read
38 * unconditionally by free's own rendering (the table-wide default's
39 * fallback target), so anything written there keeps showing even with pro
40 * switched off — moving it to a dedicated pro-only attribute is what makes
41 * it disappear correctly, the same fix already applied to backgroundColor.
42 *
43 * The border wraps the column as one block, not each cell individually:
44 * every cell gets the left/right sides, but only the topmost cell gets the
45 * top and only the bottommost gets the bottom — otherwise cells in the
46 * middle of the column would each draw a border against their neighbours.
47 */
48 export function CellColumnBorderControls({
49 clientId,
50 cellDefaultsBorder,
51 // Injected by the pro plugin; undefined when pro is not installed. A
52 // render function rather than a plain node: resolving the column's
53 // sibling cells and bulk-writing their styles needs the block registry,
54 // which pro's editor.BlockEdit HOC can't reach from outside the tree.
55 ProColumnBorderContent,
56 }: {
57 clientId: string;
58 cellDefaultsBorder: Border;
59 ProColumnBorderContent?: (ctx: ColumnBorderContext) => ReactNode;
60 }) {
61 const { updateBlockAttributes } = useDispatch(blockEditorStore) as any;
62 const registry = useRegistry() as any;
63
64 const info = useSelect(
65 select => {
66 const be = select(blockEditorStore) as any;
67 const rowClientId = be.getBlockRootClientId(clientId);
68 const tableClientId = rowClientId
69 ? be.getBlockRootClientId(rowClientId)
70 : null;
71
72 if (!rowClientId || !tableClientId) {
73 return null;
74 }
75
76 const rows: GridRow[] = (
77 be.getBlock(tableClientId)?.innerBlocks ?? []
78 )
79 .filter((b: any) => b.name === "tableberg/row")
80 .map((rowBlock: any) =>
81 rowBlock.innerBlocks
82 .filter((b: any) => b.name === "tableberg/cell")
83 .map((cellBlock: any) => ({
84 id: cellBlock.clientId,
85 rowSpan: cellBlock.attributes?.span?.rowSpan ?? 1,
86 colSpan: cellBlock.attributes?.span?.colSpan ?? 1,
87 }))
88 );
89
90 const column = cellColumn(rows, clientId);
91 let columnCellIds: string[] = [];
92 if (column !== null) {
93 const occupancy = buildOccupancy(rows);
94 const seen = new Set<string>();
95 for (let r = 0; r < rows.length; r++) {
96 const id = occupancy.matrix[r]?.[column];
97 if (id && !seen.has(id)) {
98 seen.add(id);
99 columnCellIds.push(id);
100 }
101 }
102 }
103
104 const getResolvedBorder = (id: string): Border => {
105 const cellAttrs = be.getBlockAttributes(id);
106 // New attribute first, legacy `styles.border` fallback for
107 // cells edited before it existed — same resolution the
108 // individual cell border control uses.
109 const cellBorder = (cellAttrs?.border ??
110 cellAttrs?.styles?.border) as Border | undefined;
111 return cellBorder && hasBorder(cellBorder)
112 ? cellBorder
113 : cellDefaultsBorder;
114 };
115
116 // A column is one rectangular block: only its outermost edges
117 // carry the border. Top comes from the first cell, bottom from
118 // the last — the cells in between share no border with each
119 // other, so their own top/bottom don't factor in here.
120 let columnBorder: Border | undefined;
121 if (columnCellIds.length > 0) {
122 const resolved = columnCellIds.map(getResolvedBorder);
123 const first = resolved[0];
124 const last = resolved[resolved.length - 1];
125 const sidesConsistent = resolved.every(
126 border =>
127 border.left === first.left &&
128 border.right === first.right
129 );
130 columnBorder = sidesConsistent
131 ? {
132 top: first.top,
133 right: first.right,
134 bottom: last.bottom,
135 left: first.left,
136 }
137 : undefined;
138 }
139
140 return { columnCellIds, columnBorder };
141 },
142 [
143 clientId,
144 cellDefaultsBorder.top,
145 cellDefaultsBorder.right,
146 cellDefaultsBorder.bottom,
147 cellDefaultsBorder.left,
148 ]
149 );
150
151 if (!info) {
152 return null;
153 }
154
155 const applyToCells = (newBorder: Border) => {
156 const be = registry.select(blockEditorStore);
157 const total = info.columnCellIds.length;
158 registry.batch(() => {
159 info.columnCellIds.forEach((id, index) => {
160 const currentStyles = be.getBlockAttributes(id)?.styles ?? {};
161 const { border: _legacy, ...styles } = currentStyles;
162 const isFirst = index === 0;
163 const isLast = index === total - 1;
164 updateBlockAttributes(id, {
165 // Left/right run the full column, but top/bottom only
166 // belong to the outermost cells — an interior cell
167 // shares its neighbours' edges, not its own.
168 border: {
169 top: isFirst ? newBorder.top : "",
170 right: newBorder.right,
171 bottom: isLast ? newBorder.bottom : "",
172 left: newBorder.left,
173 },
174 styles,
175 });
176 });
177 });
178 };
179
180 const columnBorder = info.columnBorder ?? EMPTY_BORDER;
181 const context: ColumnBorderContext = {
182 columnBorderControlProps: {
183 label: __("Column Border", "tableberg"),
184 value: columnBorder,
185 hasValue: () => hasBorder(columnBorder),
186 onChange: (newBorder: Border) => applyToCells(newBorder),
187 onDeselect: () => applyToCells(EMPTY_BORDER),
188 },
189 };
190
191 return (
192 <InspectorControls group="border">
193 {ProColumnBorderContent ? (
194 ProColumnBorderContent(context)
195 ) : (
196 <LockedControl isEnhanced selected="col-border">
197 <BorderControl {...context.columnBorderControlProps} />
198 </LockedControl>
199 )}
200 </InspectorControls>
201 );
202 }
203