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

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

285 lines 11.2 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, BorderRadiusControl } from "@tableberg/components";
9
10 import LockedControl from "../../components/LockedControl";
11 import { Border, Corners } from "../../attributes";
12 import { buildOccupancy, cellColumn, GridRow } from "../table/grid-model";
13
14 const EMPTY_BORDER: Border = { top: "", right: "", bottom: "", left: "" };
15
16 const EMPTY_CORNERS: Corners = {
17 topLeft: "",
18 topRight: "",
19 bottomRight: "",
20 bottomLeft: "",
21 };
22
23 const hasBorder = (border: Border) =>
24 !!border.top || !!border.right || !!border.bottom || !!border.left;
25
26 const hasCorners = (corners: Corners) =>
27 !!corners.topLeft ||
28 !!corners.topRight ||
29 !!corners.bottomRight ||
30 !!corners.bottomLeft;
31
32 export interface ColumnBorderContext {
33 columnBorderControlProps: {
34 label: string;
35 value: Border;
36 hasValue: () => boolean;
37 onChange: (newBorder: Border) => void;
38 onDeselect: () => void;
39 };
40 columnRadiusControlProps: {
41 label: string;
42 value: Corners;
43 hasValue: () => boolean;
44 onChange: (newRadius: Corners) => void;
45 onDeselect: () => void;
46 };
47 }
48
49 /**
50 * Column border — bulk-apply the picked border to every cell in the
51 * selected cell's column, mirroring `CellColumnBackgroundControls` (and,
52 * further back, `CellDimensionsControls`'s column width): a column has no
53 * block of its own, so this is set from whichever cell you have selected.
54 * Writes each target cell's own `border` attribute — the same one the
55 * individual per-cell border control uses, with the same legacy
56 * `styles.border` fallback (tables where it lived before it got its own
57 * attribute). Deliberately not `styles.border` directly: that key is read
58 * unconditionally by free's own rendering (the table-wide default's
59 * fallback target), so anything written there keeps showing even with pro
60 * switched off — moving it to a dedicated pro-only attribute is what makes
61 * it disappear correctly, the same fix already applied to backgroundColor.
62 *
63 * The border wraps the column as one block, not each cell individually:
64 * every cell gets the left/right sides, but only the topmost cell gets the
65 * top and only the bottommost gets the bottom — otherwise cells in the
66 * middle of the column would each draw a border against their neighbours.
67 */
68 export function CellColumnBorderControls({
69 clientId,
70 cellDefaultsBorder,
71 // Injected by the pro plugin; undefined when pro is not installed. A
72 // render function rather than a plain node: resolving the column's
73 // sibling cells and bulk-writing their styles needs the block registry,
74 // which pro's editor.BlockEdit HOC can't reach from outside the tree.
75 ProColumnBorderContent,
76 }: {
77 clientId: string;
78 cellDefaultsBorder: Border;
79 ProColumnBorderContent?: (ctx: ColumnBorderContext) => ReactNode;
80 }) {
81 const { updateBlockAttributes } = useDispatch(blockEditorStore) as any;
82 const registry = useRegistry() as any;
83
84 const info = useSelect(
85 select => {
86 const be = select(blockEditorStore) as any;
87 const rowClientId = be.getBlockRootClientId(clientId);
88 const tableClientId = rowClientId
89 ? be.getBlockRootClientId(rowClientId)
90 : null;
91
92 if (!rowClientId || !tableClientId) {
93 return null;
94 }
95
96 const rows: GridRow[] = (
97 be.getBlock(tableClientId)?.innerBlocks ?? []
98 )
99 .filter((b: any) => b.name === "tableberg/row")
100 .map((rowBlock: any) =>
101 rowBlock.innerBlocks
102 .filter((b: any) => b.name === "tableberg/cell")
103 .map((cellBlock: any) => ({
104 id: cellBlock.clientId,
105 rowSpan: cellBlock.attributes?.span?.rowSpan ?? 1,
106 colSpan: cellBlock.attributes?.span?.colSpan ?? 1,
107 }))
108 );
109
110 const column = cellColumn(rows, clientId);
111 let columnCellIds: string[] = [];
112 if (column !== null) {
113 const occupancy = buildOccupancy(rows);
114 const seen = new Set<string>();
115 for (let r = 0; r < rows.length; r++) {
116 const id = occupancy.matrix[r]?.[column];
117 if (id && !seen.has(id)) {
118 seen.add(id);
119 columnCellIds.push(id);
120 }
121 }
122 }
123
124 const getResolvedBorder = (id: string): Border => {
125 const cellAttrs = be.getBlockAttributes(id);
126 // New attribute first, legacy `styles.border` fallback for
127 // cells edited before it existed — same resolution the
128 // individual cell border control uses.
129 const cellBorder = (cellAttrs?.border ??
130 cellAttrs?.styles?.border) as Border | undefined;
131 return cellBorder && hasBorder(cellBorder)
132 ? cellBorder
133 : cellDefaultsBorder;
134 };
135
136 // A column is one rectangular block: only its outermost edges
137 // carry the border. Top comes from the first cell, bottom from
138 // the last — the cells in between share no border with each
139 // other, so their own top/bottom don't factor in here.
140 let columnBorder: Border | undefined;
141 if (columnCellIds.length > 0) {
142 const resolved = columnCellIds.map(getResolvedBorder);
143 const first = resolved[0];
144 const last = resolved[resolved.length - 1];
145 const sidesConsistent = resolved.every(
146 border =>
147 border.left === first.left &&
148 border.right === first.right
149 );
150 columnBorder = sidesConsistent
151 ? {
152 top: first.top,
153 right: first.right,
154 bottom: last.bottom,
155 left: first.left,
156 }
157 : undefined;
158 }
159
160 // Same idea for the corners: a column is rounded at its own
161 // four corners, so the top two come from the first cell and the
162 // bottom two from the last.
163 let columnRadius: Corners | undefined;
164 if (columnCellIds.length > 0) {
165 const radii = columnCellIds.map(
166 id =>
167 (be.getBlockAttributes(id)?.styles?.borderRadius ??
168 EMPTY_CORNERS) as Corners
169 );
170 const first = radii[0];
171 const last = radii[radii.length - 1];
172 columnRadius = {
173 topLeft: first.topLeft ?? "",
174 topRight: first.topRight ?? "",
175 bottomRight: last.bottomRight ?? "",
176 bottomLeft: last.bottomLeft ?? "",
177 };
178 }
179
180 return { columnCellIds, columnBorder, columnRadius };
181 },
182 [
183 clientId,
184 cellDefaultsBorder.top,
185 cellDefaultsBorder.right,
186 cellDefaultsBorder.bottom,
187 cellDefaultsBorder.left,
188 ]
189 );
190
191 if (!info) {
192 return null;
193 }
194
195 const applyToCells = (newBorder: Border) => {
196 const be = registry.select(blockEditorStore);
197 const total = info.columnCellIds.length;
198 registry.batch(() => {
199 info.columnCellIds.forEach((id, index) => {
200 const currentStyles = be.getBlockAttributes(id)?.styles ?? {};
201 const { border: _legacy, ...styles } = currentStyles;
202 const isFirst = index === 0;
203 const isLast = index === total - 1;
204 updateBlockAttributes(id, {
205 // Left/right run the full column, but top/bottom only
206 // belong to the outermost cells — an interior cell
207 // shares its neighbours' edges, not its own.
208 border: {
209 top: isFirst ? newBorder.top : "",
210 right: newBorder.right,
211 bottom: isLast ? newBorder.bottom : "",
212 left: newBorder.left,
213 },
214 styles,
215 });
216 });
217 });
218 };
219
220 // Only the column's outermost cells are rounded; an interior cell has
221 // no corner of the column to round.
222 const applyRadiusToCells = (newRadius: Corners) => {
223 const be = registry.select(blockEditorStore);
224 const total = info.columnCellIds.length;
225
226 registry.batch(() => {
227 info.columnCellIds.forEach((id, index) => {
228 const styles = be.getBlockAttributes(id)?.styles ?? {};
229 const isFirst = index === 0;
230 const isLast = index === total - 1;
231
232 updateBlockAttributes(id, {
233 styles: {
234 ...styles,
235 borderRadius: {
236 topLeft: isFirst ? newRadius.topLeft : "",
237 topRight: isFirst ? newRadius.topRight : "",
238 bottomRight: isLast ? newRadius.bottomRight : "",
239 bottomLeft: isLast ? newRadius.bottomLeft : "",
240 },
241 },
242 });
243 });
244 });
245 };
246
247 const columnBorder = info.columnBorder ?? EMPTY_BORDER;
248 const columnRadius = info.columnRadius ?? EMPTY_CORNERS;
249 const context: ColumnBorderContext = {
250 columnBorderControlProps: {
251 label: __("Column Border", "tableberg"),
252 value: columnBorder,
253 hasValue: () => hasBorder(columnBorder),
254 onChange: (newBorder: Border) => applyToCells(newBorder),
255 onDeselect: () => applyToCells(EMPTY_BORDER),
256 },
257 columnRadiusControlProps: {
258 label: __("Column Border Radius", "tableberg"),
259 value: columnRadius,
260 hasValue: () => hasCorners(columnRadius),
261 onChange: (newRadius: Corners) => applyRadiusToCells(newRadius),
262 onDeselect: () => applyRadiusToCells(EMPTY_CORNERS),
263 },
264 };
265
266 return (
267 <InspectorControls group="border">
268 {ProColumnBorderContent ? (
269 ProColumnBorderContent(context)
270 ) : (
271 <>
272 <LockedControl isEnhanced selected="col-border">
273 <BorderControl {...context.columnBorderControlProps} />
274 </LockedControl>
275 <LockedControl isEnhanced selected="col-border-radius">
276 <BorderRadiusControl
277 {...context.columnRadiusControlProps}
278 />
279 </LockedControl>
280 </>
281 )}
282 </InspectorControls>
283 );
284 }
285