UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

506 lines (499 loc) 19.2 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import { createElement } from 'react'; import { RawIntlProvider } from 'react-intl-next'; // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead import uuid from 'uuid/v4'; import { nonNullable } from '@atlaskit/editor-common/utils'; // @ts-ignore -- ReadonlyTransaction is a local declaration and will cause a TS2305 error in CCFE typecheck import { Decoration } from '@atlaskit/editor-prosemirror/view'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { findTable, getCellsInRow, getSelectionRect } from '@atlaskit/editor-tables/utils'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { TableCssClassName as ClassName, TableDecorations } from '../../types'; import { ColumnResizeWidget } from '../../ui/ColumnResizeWidget'; var filterDecorationByKey = function filterDecorationByKey(key, decorationSet) { return decorationSet.find(undefined, undefined, function (spec) { return spec.key.indexOf(key) > -1; }); }; export var findColumnControlSelectedDecoration = function findColumnControlSelectedDecoration(decorationSet) { return filterDecorationByKey(TableDecorations.COLUMN_SELECTED, decorationSet); }; export var findControlsHoverDecoration = function findControlsHoverDecoration(decorationSet) { return filterDecorationByKey(TableDecorations.ALL_CONTROLS_HOVER, decorationSet); }; export var createCellHoverDecoration = function createCellHoverDecoration(cells) { return cells.map(function (cell) { return Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: ClassName.HOVERED_CELL_WARNING }, { key: TableDecorations.CELL_CONTROLS_HOVER }); }); }; export var createControlsHoverDecoration = function createControlsHoverDecoration(cells, type, tr, isDragAndDropEnable, hoveredIndexes, danger, selected) { var table = findTable(tr.selection); if (!table) { return []; } var map = TableMap.get(table.node); var _cells$reduce = cells.reduce(function (_ref, cell) { var _ref2 = _slicedToArray(_ref, 2), min = _ref2[0], max = _ref2[1]; if (min === null || cell.pos < min) { min = cell.pos; } if (max === null || cell.pos > max) { max = cell.pos; } return [min, max]; }, [null, null]), _cells$reduce2 = _slicedToArray(_cells$reduce, 2), min = _cells$reduce2[0], max = _cells$reduce2[1]; if (min === null || max === null) { return []; } var updatedCells = cells.map(function (x) { return x.pos; }); // ED-15246 fixed trello card table overflow issue // If columns / rows have been merged the hovered selection is different to the actual selection // So If the table cells are in danger we want to create a "rectangle" selection // to match the "clicked" selection if (danger && type !== 'table') { var selection = tr.selection; var _table = findTable(selection); var rect = getSelectionRect(selection); if (_table && rect) { updatedCells = map.cellsInRect(rect).map(function (x) { return x + _table.start; }); } } return updatedCells.map(function (pos) { var cell = tr.doc.nodeAt(pos); var classes = [ClassName.HOVERED_CELL]; if (danger) { classes.push(ClassName.HOVERED_CELL_IN_DANGER); } if (selected) { classes.push(ClassName.SELECTED_CELL); } if (isDragAndDropEnable) { if (type === 'column' || type === 'row') { classes.pop(); classes.push(ClassName.HOVERED_NO_HIGHLIGHT); } } else { classes.push(type === 'column' ? ClassName.HOVERED_COLUMN : type === 'row' ? ClassName.HOVERED_ROW : ClassName.HOVERED_TABLE); } var key; switch (type) { case 'row': key = TableDecorations.ROW_CONTROLS_HOVER; break; case 'column': key = TableDecorations.COLUMN_CONTROLS_HOVER; break; default: key = TableDecorations.TABLE_CONTROLS_HOVER; break; } return Decoration.node(pos, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pos + cell.nodeSize, { class: classes.join(' ') }, { key: key }); }); }; export var createColumnSelectedDecoration = function createColumnSelectedDecoration(tr) { var selection = tr.selection, doc = tr.doc; var table = findTable(selection); var rect = getSelectionRect(selection); if (!table || !rect) { return []; } var map = TableMap.get(table.node); var cellPositions = map.cellsInRect(rect); return cellPositions.map(function (pos, index) { var cell = doc.nodeAt(pos + table.start); return Decoration.node(pos + table.start, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pos + table.start + cell.nodeSize, { class: ClassName.COLUMN_SELECTED }, { key: "".concat(TableDecorations.COLUMN_SELECTED, "_").concat(index) }); }); }; export var createColumnControlsDecoration = function createColumnControlsDecoration(selection) { var cells = getCellsInRow(0)(selection) || []; var index = 0; return cells.map(function (cell) { var colspan = cell.node.attrs.colspan || 1; // It's important these values are scoped locally as the widget callback could be executed anytime in the future // and we want to avoid value leak var startIndex = index; var endIndex = startIndex + colspan; // The next cell start index will commence from the current cell end index. index = endIndex; return Decoration.widget(cell.pos + 1, function () { var element = document.createElement('div'); element.classList.add(ClassName.COLUMN_CONTROLS_DECORATIONS); element.dataset.startIndex = "".concat(startIndex); element.dataset.endIndex = "".concat(endIndex); return element; }, { key: "".concat(TableDecorations.COLUMN_CONTROLS_DECORATIONS, "_").concat(endIndex), // this decoration should be the first one, even before gap cursor. side: -100 }); }); }; export var updateDecorations = function updateDecorations(node, decorationSet, decorations, key) { var filteredDecorations = filterDecorationByKey(key, decorationSet); var decorationSetFiltered = decorationSet.remove(filteredDecorations); return decorationSetFiltered.add(node, decorations); }; var makeArray = function makeArray(n) { return Array.from(Array(n).keys()); }; /* * This function will create two specific decorations for each cell in a column index target, * for example given that table: * * ``` * 0 1 2 3 * _____________________ _______ * | | | | * | B1 | C1 | A1 | * |______|______ ______|______| * | | | | * | B2 | | A2 | * |______ ______| |______| * | | | D1 | | * | B3 | C2 | | A3 | * |______|______|______|______| * ^ ^ ^ ^ * | | | | * | | | | * | | | | * 0 1 3 4 * \ | | / * \ | | / * \ | | / * \ | | / * \ | | / * columnEndIndexTarget === CellColumnPositioning.right * ``` * * When a user wants to resize a cell, * they need to grab and hold the end of that column, * and this will be the `columnEndIndexTarget` using * the CellColumnPositioning interface. * * Let's say the `columnEndIndexTarget.right` is 3, * so this function will return two types of decorations for each cell on that column, * that means 2 `resizerHandle` and 2 `lastCellElement`, * here is the explanation for each one of them : * * - resizerHandle: * * Given the cell C1, this decoration will add a div to create this area * ``` * ▁▁▁▁▁▁▁▁▁▁▁▁▁ * | ▒▒| * | C1 ▒▒| * | ▒▒| * ▔▔▔▔▔▔▔▔▔▔▔▔▔ * ``` * This ▒ represents the area where table resizing will start, * and you can follow that using checking the class name `ClassName.RESIZE_HANDLE_DECORATION` on the code * * - lastCellElementDecoration * * Given the content of the cell C1 * ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ * | | * | _____________ | * | | | | * | | <p> | | * | |_____________| | * | | * | _____________ | * | | | | * | | <media> | | * | |_____________| | * | | * | _____________ | * | | | | * | | <media> | | * | |_____________| | * | | * ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ * Currently, we are removing the margin-bottom from the last media using this kind of CSS rule: * `div:last-of-type`; This is quite unstable, and after we create the `resizerHandle` div, * that logic will apply the margin in the wrong element, to avoid that, * we will add a new class on the last item for each cell, * hence the second media will receive this class `ClassName.LAST_ITEM_IN_CELL` */ export var createResizeHandleDecoration = function createResizeHandleDecoration(tr, rowIndexTarget, columnEndIndexTarget) { var includeTooltip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; var getIntl = arguments.length > 4 ? arguments[4] : undefined; var nodeViewPortalProviderAPI = arguments.length > 5 ? arguments[5] : undefined; var emptyResult = [[], []]; var table = findTable(tr.selection); if (!table || !table.node) { return emptyResult; } var map = TableMap.get(table.node); if (!map.width) { return emptyResult; } var createResizerHandleDecoration = function createResizerHandleDecoration(cellColumnPositioning, columnIndex, rowIndex, cellPos, cellNode) { // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead var decorationRenderKey = uuid(); var position = cellPos + cellNode.nodeSize - 1; return Decoration.widget(position, function () { var element = document.createElement('div'); nodeViewPortalProviderAPI.render(function () { return /*#__PURE__*/createElement(RawIntlProvider, { value: getIntl() }, /*#__PURE__*/createElement(ColumnResizeWidget, { startIndex: cellColumnPositioning.left, endIndex: cellColumnPositioning.right, includeTooltip: includeTooltip })); }, element, decorationRenderKey); return element; }, { key: "".concat(TableDecorations.COLUMN_RESIZING_HANDLE_WIDGET, "_").concat(rowIndex, "_").concat(columnIndex, "_").concat(includeTooltip ? 'with' : 'no', "-tooltip"), destroy: function destroy(_node) { nodeViewPortalProviderAPI.remove(decorationRenderKey); } }); }; var createLastCellElementDecoration = function createLastCellElementDecoration(cellColumnPositioning, cellPos, cellNode) { if (expValEquals('platform_editor_table_remove_last_cell_decoration', 'isEnabled', true)) { // no longer need to add the last cell decoration to override marginBottom as media wrapper doesn't have margin bottom. This will avoid unnecessary decoration computation/mutation and improve performance // consider clean up ClassName.LAST_ITEM_IN_CELL with platform_editor_table_remove_last_cell_decoration experiment return null; } var lastItemPositions; cellNode.forEach(function (childNode, offset, index) { if (index === cellNode.childCount - 1) { var from = offset + cellPos + 1; lastItemPositions = { from: from, to: from + childNode.nodeSize }; } }); if (!lastItemPositions) { return null; } return Decoration.node(lastItemPositions.from, lastItemPositions.to, { class: ClassName.LAST_ITEM_IN_CELL }, { key: "".concat(TableDecorations.LAST_CELL_ELEMENT, "_").concat(cellColumnPositioning.left, "_").concat(cellColumnPositioning.right) }); }; var resizeHandleCellDecorations = []; var lastCellElementsDecorations = []; for (var rowIndex = 0; rowIndex < map.height; rowIndex++) { var seen = {}; if (rowIndex !== rowIndexTarget) { continue; } for (var columnIndex = 0; columnIndex < map.width; columnIndex++) { var cellPosition = map.map[map.width * rowIndex + columnIndex]; if (seen[cellPosition]) { continue; } seen[cellPosition] = true; var cellPos = table.start + cellPosition; var cell = tr.doc.nodeAt(cellPos); if (!cell) { continue; } var colspan = cell.attrs.colspan || 1; var startIndex = columnIndex; var endIndex = colspan + startIndex; if (endIndex !== columnEndIndexTarget.right) { continue; } var resizerHandleDec = createResizerHandleDecoration({ left: startIndex, right: endIndex }, columnIndex, rowIndex, cellPos, cell); var lastCellDec = createLastCellElementDecoration({ left: startIndex, right: endIndex }, cellPos, cell); resizeHandleCellDecorations.push(resizerHandleDec); lastCellElementsDecorations.push(lastCellDec); } } return [resizeHandleCellDecorations, lastCellElementsDecorations.filter(nonNullable)]; }; /* * This function will create a decoration for each cell using the right position on the CellColumnPositioning * for example given that table: * * ``` * 0 1 2 3 <--- column indexes * _____________________ _______ * | | | | * | B1 | C1 | A1 | * |______|______ ______|______| * | | | | * | B2 | D1 | A2 | * |______ ______|______|______| * | | | | * | B3 | C2 | D2 | * |______|______|_____________| * ``` * * and given the left and right represents the C1 cell: * * ``` * left right * 1 3 * | | * | | * | | * _______∨_____________∨_______ * | | | | * | B1 | C1 | A1 | * |______|______ ______|______| * | | | | * | B2 | D1 | A2 | * |______ ______|______|______| * | | | | * | B3 | C2 | D2 | * |______|______|_____________| * ``` * * Taking that table, and the right as parameters, * this function will return two decorations applying a new class `ClassName.WITH_RESIZE_LINE` * only on the cells: `C1` and `D1`. */ export var createColumnLineResize = function createColumnLineResize(selection, cellColumnPositioning, isDragAndDropEnabled) { var table = findTable(selection); if (!table || cellColumnPositioning.right === null) { return []; } var columnIndex = cellColumnPositioning.right; var map = TableMap.get(table.node); var isLastColumn = columnIndex === map.width; if (isLastColumn) { columnIndex -= 1; } var decorationClassName = isDragAndDropEnabled ? isLastColumn ? ClassName.WITH_DRAG_RESIZE_LINE_LAST_COLUMN : ClassName.WITH_DRAG_RESIZE_LINE : isLastColumn ? ClassName.WITH_RESIZE_LINE_LAST_COLUMN : ClassName.WITH_RESIZE_LINE; var cellPositions = makeArray(map.height).map(function (rowIndex) { return map.map[map.width * rowIndex + columnIndex]; }).filter(function (cellPosition, rowIndex) { if (isLastColumn) { return true; // If is the last column no filter applied } var nextPosition = map.map[map.width * rowIndex + columnIndex - 1]; return cellPosition !== nextPosition; // Removed it if next position is merged }); var cells = cellPositions.map(function (pos) { return { pos: pos + table.start, node: table.node.nodeAt(pos) }; }); return cells.map(function (cell, index) { if (!cell || !cell.node) { return; } return Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(TableDecorations.COLUMN_RESIZING_HANDLE_LINE, "_").concat(cellColumnPositioning.right, "_").concat(index) }); }).filter(nonNullable); }; export var createColumnInsertLine = function createColumnInsertLine(columnIndex, selection, hasMergedCells) { var table = findTable(selection); if (!table) { return []; } var map = TableMap.get(table.node); var isFirstColumn = columnIndex === 0; var isLastColumn = columnIndex === map.width; if (isLastColumn) { columnIndex -= 1; } var decorationClassName; if (hasMergedCells) { decorationClassName = isFirstColumn ? ClassName.WITH_FIRST_COLUMN_INSERT_LINE_INACTIVE : isLastColumn ? ClassName.WITH_LAST_COLUMN_INSERT_LINE_INACTIVE : ClassName.WITH_COLUMN_INSERT_LINE_INACTIVE; } else { decorationClassName = isFirstColumn ? ClassName.WITH_FIRST_COLUMN_INSERT_LINE : isLastColumn ? ClassName.WITH_LAST_COLUMN_INSERT_LINE : ClassName.WITH_COLUMN_INSERT_LINE; } var cellPositions = makeArray(map.height).map(function (rowIndex) { return map.map[map.width * rowIndex + columnIndex]; }).filter(function (cellPosition, rowIndex) { if (isLastColumn) { return true; // If is the last column no filter applied } var nextPosition = map.map[map.width * rowIndex + columnIndex - 1]; return cellPosition !== nextPosition; // Removed it if next position is merged }); var cells = cellPositions.map(function (pos) { return { pos: pos + table.start, node: table.node.nodeAt(pos) }; }); return cells.map(function (cell, index) { if (!cell || !cell.node) { return; } return Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(TableDecorations.COLUMN_INSERT_LINE, "_").concat(index) }); }).filter(nonNullable); }; export var createRowInsertLine = function createRowInsertLine(rowIndex, selection, hasMergedCells) { var table = findTable(selection); if (!table) { return []; } var map = TableMap.get(table.node); var isLastRow = rowIndex === map.height; if (isLastRow) { rowIndex -= 1; } var cells = getCellsInRow(rowIndex)(selection); if (!cells) { return []; } var decorationClassName; if (hasMergedCells) { decorationClassName = isLastRow ? ClassName.WITH_LAST_ROW_INSERT_LINE_INACTIVE : ClassName.WITH_ROW_INSERT_LINE_INACTIVE; } else { decorationClassName = isLastRow ? ClassName.WITH_LAST_ROW_INSERT_LINE : ClassName.WITH_ROW_INSERT_LINE; } return cells.map(function (cell, index) { if (!cell || !cell.node) { return; } return Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(TableDecorations.ROW_INSERT_LINE, "_").concat(index) }); }).filter(nonNullable); };