# tableberg/1.2.0/src/blocks/cell/column-border.tsx

Tableberg – Simple Gutenberg Table Block, version 1.2.0. 285 lines.

- Page: https://pluginprobe.com/plugins/tableberg/1.2.0/code/src/blocks/cell/column-border.tsx
- Raw: https://pluginprobe.com/plugins/tableberg/1.2.0/raw/src/blocks/cell/column-border.tsx
- Modified: 2026-09-30T16:56:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/tableberg/1.2.0/code/src/blocks/cell/column-border.tsx#L10-L20`.

```tsx
import { ReactNode } from "react";
import {
    InspectorControls,
    store as blockEditorStore,
} from "@wordpress/block-editor";
import { useDispatch, useRegistry, useSelect } from "@wordpress/data";
import { __ } from "@wordpress/i18n";
import { BorderControl, BorderRadiusControl } from "@tableberg/components";

import LockedControl from "../../components/LockedControl";
import { Border, Corners } from "../../attributes";
import { buildOccupancy, cellColumn, GridRow } from "../table/grid-model";

const EMPTY_BORDER: Border = { top: "", right: "", bottom: "", left: "" };

const EMPTY_CORNERS: Corners = {
    topLeft: "",
    topRight: "",
    bottomRight: "",
    bottomLeft: "",
};

const hasBorder = (border: Border) =>
    !!border.top || !!border.right || !!border.bottom || !!border.left;

const hasCorners = (corners: Corners) =>
    !!corners.topLeft ||
    !!corners.topRight ||
    !!corners.bottomRight ||
    !!corners.bottomLeft;

export interface ColumnBorderContext {
    columnBorderControlProps: {
        label: string;
        value: Border;
        hasValue: () => boolean;
        onChange: (newBorder: Border) => void;
        onDeselect: () => void;
    };
    columnRadiusControlProps: {
        label: string;
        value: Corners;
        hasValue: () => boolean;
        onChange: (newRadius: Corners) => void;
        onDeselect: () => void;
    };
}

/**
 * Column border — bulk-apply the picked border to every cell in the
 * selected cell's column, mirroring `CellColumnBackgroundControls` (and,
 * further back, `CellDimensionsControls`'s column width): a column has no
 * block of its own, so this is set from whichever cell you have selected.
 * Writes each target cell's own `border` attribute — the same one the
 * individual per-cell border control uses, with the same legacy
 * `styles.border` fallback (tables where it lived before it got its own
 * attribute). Deliberately not `styles.border` directly: that key is read
 * unconditionally by free's own rendering (the table-wide default's
 * fallback target), so anything written there keeps showing even with pro
 * switched off — moving it to a dedicated pro-only attribute is what makes
 * it disappear correctly, the same fix already applied to backgroundColor.
 *
 * The border wraps the column as one block, not each cell individually:
 * every cell gets the left/right sides, but only the topmost cell gets the
 * top and only the bottommost gets the bottom — otherwise cells in the
 * middle of the column would each draw a border against their neighbours.
 */
export function CellColumnBorderControls({
    clientId,
    cellDefaultsBorder,
    // Injected by the pro plugin; undefined when pro is not installed. A
    // render function rather than a plain node: resolving the column's
    // sibling cells and bulk-writing their styles needs the block registry,
    // which pro's editor.BlockEdit HOC can't reach from outside the tree.
    ProColumnBorderContent,
}: {
    clientId: string;
    cellDefaultsBorder: Border;
    ProColumnBorderContent?: (ctx: ColumnBorderContext) => ReactNode;
}) {
    const { updateBlockAttributes } = useDispatch(blockEditorStore) as any;
    const registry = useRegistry() as any;

    const info = useSelect(
        select => {
            const be = select(blockEditorStore) as any;
            const rowClientId = be.getBlockRootClientId(clientId);
            const tableClientId = rowClientId
                ? be.getBlockRootClientId(rowClientId)
                : null;

            if (!rowClientId || !tableClientId) {
                return null;
            }

            const rows: GridRow[] = (
                be.getBlock(tableClientId)?.innerBlocks ?? []
            )
                .filter((b: any) => b.name === "tableberg/row")
                .map((rowBlock: any) =>
                    rowBlock.innerBlocks
                        .filter((b: any) => b.name === "tableberg/cell")
                        .map((cellBlock: any) => ({
                            id: cellBlock.clientId,
                            rowSpan: cellBlock.attributes?.span?.rowSpan ?? 1,
                            colSpan: cellBlock.attributes?.span?.colSpan ?? 1,
                        }))
                );

            const column = cellColumn(rows, clientId);
            let columnCellIds: string[] = [];
            if (column !== null) {
                const occupancy = buildOccupancy(rows);
                const seen = new Set<string>();
                for (let r = 0; r < rows.length; r++) {
                    const id = occupancy.matrix[r]?.[column];
                    if (id && !seen.has(id)) {
                        seen.add(id);
                        columnCellIds.push(id);
                    }
                }
            }

            const getResolvedBorder = (id: string): Border => {
                const cellAttrs = be.getBlockAttributes(id);
                // New attribute first, legacy `styles.border` fallback for
                // cells edited before it existed — same resolution the
                // individual cell border control uses.
                const cellBorder = (cellAttrs?.border ??
                    cellAttrs?.styles?.border) as Border | undefined;
                return cellBorder && hasBorder(cellBorder)
                    ? cellBorder
                    : cellDefaultsBorder;
            };

            // A column is one rectangular block: only its outermost edges
            // carry the border. Top comes from the first cell, bottom from
            // the last — the cells in between share no border with each
            // other, so their own top/bottom don't factor in here.
            let columnBorder: Border | undefined;
            if (columnCellIds.length > 0) {
                const resolved = columnCellIds.map(getResolvedBorder);
                const first = resolved[0];
                const last = resolved[resolved.length - 1];
                const sidesConsistent = resolved.every(
                    border =>
                        border.left === first.left &&
                        border.right === first.right
                );
                columnBorder = sidesConsistent
                    ? {
                          top: first.top,
                          right: first.right,
                          bottom: last.bottom,
                          left: first.left,
                      }
                    : undefined;
            }

            // Same idea for the corners: a column is rounded at its own
            // four corners, so the top two come from the first cell and the
            // bottom two from the last.
            let columnRadius: Corners | undefined;
            if (columnCellIds.length > 0) {
                const radii = columnCellIds.map(
                    id =>
                        (be.getBlockAttributes(id)?.styles?.borderRadius ??
                            EMPTY_CORNERS) as Corners
                );
                const first = radii[0];
                const last = radii[radii.length - 1];
                columnRadius = {
                    topLeft: first.topLeft ?? "",
                    topRight: first.topRight ?? "",
                    bottomRight: last.bottomRight ?? "",
                    bottomLeft: last.bottomLeft ?? "",
                };
            }

            return { columnCellIds, columnBorder, columnRadius };
        },
        [
            clientId,
            cellDefaultsBorder.top,
            cellDefaultsBorder.right,
            cellDefaultsBorder.bottom,
            cellDefaultsBorder.left,
        ]
    );

    if (!info) {
        return null;
    }

    const applyToCells = (newBorder: Border) => {
        const be = registry.select(blockEditorStore);
        const total = info.columnCellIds.length;
        registry.batch(() => {
            info.columnCellIds.forEach((id, index) => {
                const currentStyles = be.getBlockAttributes(id)?.styles ?? {};
                const { border: _legacy, ...styles } = currentStyles;
                const isFirst = index === 0;
                const isLast = index === total - 1;
                updateBlockAttributes(id, {
                    // Left/right run the full column, but top/bottom only
                    // belong to the outermost cells — an interior cell
                    // shares its neighbours' edges, not its own.
                    border: {
                        top: isFirst ? newBorder.top : "",
                        right: newBorder.right,
                        bottom: isLast ? newBorder.bottom : "",
                        left: newBorder.left,
                    },
                    styles,
                });
            });
        });
    };

    // Only the column's outermost cells are rounded; an interior cell has
    // no corner of the column to round.
    const applyRadiusToCells = (newRadius: Corners) => {
        const be = registry.select(blockEditorStore);
        const total = info.columnCellIds.length;

        registry.batch(() => {
            info.columnCellIds.forEach((id, index) => {
                const styles = be.getBlockAttributes(id)?.styles ?? {};
                const isFirst = index === 0;
                const isLast = index === total - 1;

                updateBlockAttributes(id, {
                    styles: {
                        ...styles,
                        borderRadius: {
                            topLeft: isFirst ? newRadius.topLeft : "",
                            topRight: isFirst ? newRadius.topRight : "",
                            bottomRight: isLast ? newRadius.bottomRight : "",
                            bottomLeft: isLast ? newRadius.bottomLeft : "",
                        },
                    },
                });
            });
        });
    };

    const columnBorder = info.columnBorder ?? EMPTY_BORDER;
    const columnRadius = info.columnRadius ?? EMPTY_CORNERS;
    const context: ColumnBorderContext = {
        columnBorderControlProps: {
            label: __("Column Border", "tableberg"),
            value: columnBorder,
            hasValue: () => hasBorder(columnBorder),
            onChange: (newBorder: Border) => applyToCells(newBorder),
            onDeselect: () => applyToCells(EMPTY_BORDER),
        },
        columnRadiusControlProps: {
            label: __("Column Border Radius", "tableberg"),
            value: columnRadius,
            hasValue: () => hasCorners(columnRadius),
            onChange: (newRadius: Corners) => applyRadiusToCells(newRadius),
            onDeselect: () => applyRadiusToCells(EMPTY_CORNERS),
        },
    };

    return (
        <InspectorControls group="border">
            {ProColumnBorderContent ? (
                ProColumnBorderContent(context)
            ) : (
                <>
                    <LockedControl isEnhanced selected="col-border">
                        <BorderControl {...context.columnBorderControlProps} />
                    </LockedControl>
                    <LockedControl isEnhanced selected="col-border-radius">
                        <BorderRadiusControl
                            {...context.columnRadiusControlProps}
                        />
                    </LockedControl>
                </>
            )}
        </InspectorControls>
    );
}

```
