UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

206 lines (197 loc) 7.92 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import { TableMap } from '@atlaskit/editor-tables/table-map'; var roundedTableCellEdgeStateCache = new WeakMap(); var addEdgeState = function addEdgeState(edgeStates, offset, edge) { var edgeState = edgeStates.get(offset); if (!edgeState) { edgeState = { reachesTop: false, reachesBottom: false, reachesLeft: false, reachesRight: false }; edgeStates.set(offset, edgeState); } edgeState[edge] = true; }; /** * Returns the rounded-edge membership for a cell offset. * * The first lookup for a table scans only the TableMap's four borders and caches edge cells. * Interior cells have no cache entry and require no DOM attributes. Subsequent cell nodeview * constructors perform an O(1) lookup. The cache is keyed by the immutable table node, so * structural document changes naturally create a fresh entry. */ export var getRoundedTableCellEdgeState = function getRoundedTableCellEdgeState(tableNode, cellOffset) { var edgeStates = roundedTableCellEdgeStateCache.get(tableNode); if (!edgeStates) { var _TableMap$get = TableMap.get(tableNode), width = _TableMap$get.width, height = _TableMap$get.height, map = _TableMap$get.map; edgeStates = new Map(); for (var column = 0; column < width; column++) { addEdgeState(edgeStates, map[column], 'reachesTop'); addEdgeState(edgeStates, map[(height - 1) * width + column], 'reachesBottom'); } for (var row = 0; row < height; row++) { addEdgeState(edgeStates, map[row * width], 'reachesLeft'); addEdgeState(edgeStates, map[row * width + width - 1], 'reachesRight'); } roundedTableCellEdgeStateCache.set(tableNode, edgeStates); } return edgeStates.get(cellOffset); }; var setDataAttr = function setDataAttr(cell, attr, value) { var hasAttr = cell.hasAttribute(attr); if (value && !hasAttr) { cell.setAttribute(attr, 'true'); } else if (!value && hasAttr) { cell.removeAttribute(attr); } }; /** * Refreshes the `data-reaches-{top,bottom,left,right}` attributes used by CSS to round * the table's corner cells (including merged cells that span to an edge). * * Edge membership is derived directly from the table map's border rows/columns: a cell * reaches an edge iff its offset appears in the corresponding border slice (this naturally * accounts for rowspan/colspan cells, whose offset is repeated across every grid slot they * cover). This is O(cells) overall, versus O(cells^2) when calling findCell per cell. */ var refreshRoundedTableEdgeAttrs = function refreshRoundedTableEdgeAttrs(table, tableNode) { try { var tableMap = TableMap.get(tableNode); var width = tableMap.width, height = tableMap.height, map = tableMap.map; var cells = Array.from(table.rows).flatMap(function (row) { return Array.from(row.cells); }); var cellOffsets = Array.from(new Set(map)); var topOffsets = new Set(); var bottomOffsets = new Set(); var leftOffsets = new Set(); var rightOffsets = new Set(); for (var col = 0; col < width; col++) { topOffsets.add(map[col]); bottomOffsets.add(map[(height - 1) * width + col]); } for (var row = 0; row < height; row++) { leftOffsets.add(map[row * width]); rightOffsets.add(map[row * width + width - 1]); } cellOffsets.forEach(function (cellOffset, cellIndex) { var cell = cells[cellIndex]; if (!cell) { return; } setDataAttr(cell, 'data-reaches-top', topOffsets.has(cellOffset)); setDataAttr(cell, 'data-reaches-bottom', bottomOffsets.has(cellOffset)); setDataAttr(cell, 'data-reaches-left', leftOffsets.has(cellOffset)); setDataAttr(cell, 'data-reaches-right', rightOffsets.has(cellOffset)); }); } catch (_unused) { // Table structure can be transient while ProseMirror normalises transactions. // Keep existing edge attrs if the current shape cannot be mapped safely. } }; /** * Builds a lightweight signature of the cells sitting on the table's four borders, using * their immutable ProseMirror node references. Top and bottom rows are visited in full; * interior rows contribute only their first and last cells, making this O(rows + columns). */ var getPerimeterTableEdgeSignature = function getPerimeterTableEdgeSignature(tableNode) { var edgeCells = []; var lastRowIndex = tableNode.childCount - 1; tableNode.forEach(function (row, _rowOffset, rowIndex) { if (rowIndex === 0 || rowIndex === lastRowIndex) { row.forEach(function (cell) { edgeCells.push(cell); }); return; } if (row.firstChild) { edgeCells.push(row.firstChild); } if (row.childCount > 1 && row.lastChild) { edgeCells.push(row.lastChild); } }); return edgeCells; }; var tableEdgeSignaturesDiffer = function tableEdgeSignaturesDiffer(prev, next) { if (!prev || prev.length !== next.length) { return true; } for (var i = 0; i < prev.length; i++) { if (prev[i] !== next[i]) { return true; } } return false; }; /** * Keeps a table's rounded-corner edge attributes in sync as its shape changes. * * Each TableCell node view refreshes its own edge attrs when its cell attrs change. However, * when the table's shape changes (e.g. a new row is inserted below the last row) or cells are * reordered (e.g. dragging a row/column to a new position), ProseMirror may reuse the existing * neighbouring cells as-is, so those cells never get a chance to update their edge attrs on * their own. This controller covers those cases from the table node view. * * Callers are responsible for feature-gating each call site. */ export var RoundedTableEdges = /*#__PURE__*/function () { // References to the previous render's border cells (O(rows + cols), perimeter-sized, not all cells). function RoundedTableEdges(getTableElement, node) { _classCallCheck(this, RoundedTableEdges); this.getTableElement = getTableElement; // Baseline the edge signature so the first content edit doesn't trigger a spurious // refresh — the cells set their own edge attrs on construction. this.prevSignature = getPerimeterTableEdgeSignature(node); } /** * Call from the node view's `update()` after `super.update()`. When the border signature * changes it schedules a refresh of the edge attrs on the next animation frame. */ return _createClass(RoundedTableEdges, [{ key: "handleUpdate", value: function handleUpdate(node) { var nextSignature = getPerimeterTableEdgeSignature(node); var edgesChanged = tableEdgeSignaturesDiffer(this.prevSignature, nextSignature); this.prevSignature = nextSignature; if (edgesChanged) { this.scheduleRefresh(node); } } /** Call from the node view's `destroy()` to cancel any pending refresh. */ }, { key: "destroy", value: function destroy() { if (this.refreshHandle !== undefined) { cancelAnimationFrame(this.refreshHandle); this.refreshHandle = undefined; } } // The refresh runs on the next animation frame because ReactNodeView.update() schedules the // table's React render via the portal provider. If we read `table.rows` synchronously, we'd // still see the previous DOM. }, { key: "scheduleRefresh", value: function scheduleRefresh(node) { var _this = this; if (this.refreshHandle !== undefined) { cancelAnimationFrame(this.refreshHandle); } this.refreshHandle = requestAnimationFrame(function () { _this.refreshHandle = undefined; var table = _this.getTableElement(); if (table instanceof HTMLTableElement) { refreshRoundedTableEdgeAttrs(table, node); } }); } }]); }();