UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

504 lines (497 loc) 20.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.updateDecorations = exports.findControlsHoverDecoration = exports.findColumnControlSelectedDecoration = exports.createRowInsertLine = exports.createResizeHandleDecoration = exports.createControlsHoverDecoration = exports.createColumnSelectedDecoration = exports.createColumnLineResize = exports.createColumnInsertLine = exports.createColumnControlsDecoration = exports.createCellHoverDecoration = void 0; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = require("react"); var _reactIntlNext = require("react-intl-next"); var _v = _interopRequireDefault(require("uuid/v4")); var _utils = require("@atlaskit/editor-common/utils"); var _view = require("@atlaskit/editor-prosemirror/view"); var _tableMap = require("@atlaskit/editor-tables/table-map"); var _utils2 = require("@atlaskit/editor-tables/utils"); var _types = require("../../types"); var _ColumnResizeWidget = require("../../ui/ColumnResizeWidget"); // @ts-ignore -- ReadonlyTransaction is a local declaration and will cause a TS2305 error in CCFE typecheck var filterDecorationByKey = function filterDecorationByKey(key, decorationSet) { return decorationSet.find(undefined, undefined, function (spec) { return spec.key.indexOf(key) > -1; }); }; var findColumnControlSelectedDecoration = exports.findColumnControlSelectedDecoration = function findColumnControlSelectedDecoration(decorationSet) { return filterDecorationByKey(_types.TableDecorations.COLUMN_SELECTED, decorationSet); }; var findControlsHoverDecoration = exports.findControlsHoverDecoration = function findControlsHoverDecoration(decorationSet) { return filterDecorationByKey(_types.TableDecorations.ALL_CONTROLS_HOVER, decorationSet); }; var createCellHoverDecoration = exports.createCellHoverDecoration = function createCellHoverDecoration(cells) { return cells.map(function (cell) { return _view.Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: _types.TableCssClassName.HOVERED_CELL_WARNING }, { key: _types.TableDecorations.CELL_CONTROLS_HOVER }); }); }; var createControlsHoverDecoration = exports.createControlsHoverDecoration = function createControlsHoverDecoration(cells, type, tr, isDragAndDropEnable, hoveredIndexes, danger, selected) { var table = (0, _utils2.findTable)(tr.selection); if (!table) { return []; } var map = _tableMap.TableMap.get(table.node); var _cells$reduce = cells.reduce(function (_ref, cell) { var _ref2 = (0, _slicedToArray2.default)(_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 = (0, _slicedToArray2.default)(_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 = (0, _utils2.findTable)(selection); var rect = (0, _utils2.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 = [_types.TableCssClassName.HOVERED_CELL]; if (danger) { classes.push(_types.TableCssClassName.HOVERED_CELL_IN_DANGER); } if (selected) { classes.push(_types.TableCssClassName.SELECTED_CELL); } if (isDragAndDropEnable) { if (type === 'column' || type === 'row') { classes.pop(); classes.push(_types.TableCssClassName.HOVERED_NO_HIGHLIGHT); } } else { classes.push(type === 'column' ? _types.TableCssClassName.HOVERED_COLUMN : type === 'row' ? _types.TableCssClassName.HOVERED_ROW : _types.TableCssClassName.HOVERED_TABLE); } var key; switch (type) { case 'row': key = _types.TableDecorations.ROW_CONTROLS_HOVER; break; case 'column': key = _types.TableDecorations.COLUMN_CONTROLS_HOVER; break; default: key = _types.TableDecorations.TABLE_CONTROLS_HOVER; break; } return _view.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 }); }); }; var createColumnSelectedDecoration = exports.createColumnSelectedDecoration = function createColumnSelectedDecoration(tr) { var selection = tr.selection, doc = tr.doc; var table = (0, _utils2.findTable)(selection); var rect = (0, _utils2.getSelectionRect)(selection); if (!table || !rect) { return []; } var map = _tableMap.TableMap.get(table.node); var cellPositions = map.cellsInRect(rect); return cellPositions.map(function (pos, index) { var cell = doc.nodeAt(pos + table.start); return _view.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: _types.TableCssClassName.COLUMN_SELECTED }, { key: "".concat(_types.TableDecorations.COLUMN_SELECTED, "_").concat(index) }); }); }; var createColumnControlsDecoration = exports.createColumnControlsDecoration = function createColumnControlsDecoration(selection) { var cells = (0, _utils2.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 _view.Decoration.widget(cell.pos + 1, function () { var element = document.createElement('div'); element.classList.add(_types.TableCssClassName.COLUMN_CONTROLS_DECORATIONS); element.dataset.startIndex = "".concat(startIndex); element.dataset.endIndex = "".concat(endIndex); return element; }, { key: "".concat(_types.TableDecorations.COLUMN_CONTROLS_DECORATIONS, "_").concat(endIndex), // this decoration should be the first one, even before gap cursor. side: -100 }); }); }; var updateDecorations = exports.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` */ var createResizeHandleDecoration = exports.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 = (0, _utils2.findTable)(tr.selection); if (!table || !table.node) { return emptyResult; } var map = _tableMap.TableMap.get(table.node); if (!map.width) { return emptyResult; } var createResizerHandleDecoration = function createResizerHandleDecoration(cellColumnPositioning, columnIndex, rowIndex, cellPos, cellNode) { var decorationRenderKey = (0, _v.default)(); var position = cellPos + cellNode.nodeSize - 1; return _view.Decoration.widget(position, function () { var element = document.createElement('div'); nodeViewPortalProviderAPI.render(function () { return /*#__PURE__*/(0, _react.createElement)(_reactIntlNext.RawIntlProvider, { value: getIntl() }, /*#__PURE__*/(0, _react.createElement)(_ColumnResizeWidget.ColumnResizeWidget, { startIndex: cellColumnPositioning.left, endIndex: cellColumnPositioning.right, includeTooltip: includeTooltip })); }, element, decorationRenderKey); return element; }, { key: "".concat(_types.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) { 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 _view.Decoration.node(lastItemPositions.from, lastItemPositions.to, { class: _types.TableCssClassName.LAST_ITEM_IN_CELL }, { key: "".concat(_types.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(_utils.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`. */ var createColumnLineResize = exports.createColumnLineResize = function createColumnLineResize(selection, cellColumnPositioning, isDragAndDropEnabled) { var table = (0, _utils2.findTable)(selection); if (!table || cellColumnPositioning.right === null) { return []; } var columnIndex = cellColumnPositioning.right; var map = _tableMap.TableMap.get(table.node); var isLastColumn = columnIndex === map.width; if (isLastColumn) { columnIndex -= 1; } var decorationClassName = isDragAndDropEnabled ? isLastColumn ? _types.TableCssClassName.WITH_DRAG_RESIZE_LINE_LAST_COLUMN : _types.TableCssClassName.WITH_DRAG_RESIZE_LINE : isLastColumn ? _types.TableCssClassName.WITH_RESIZE_LINE_LAST_COLUMN : _types.TableCssClassName.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 _view.Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(_types.TableDecorations.COLUMN_RESIZING_HANDLE_LINE, "_").concat(cellColumnPositioning.right, "_").concat(index) }); }).filter(_utils.nonNullable); }; var createColumnInsertLine = exports.createColumnInsertLine = function createColumnInsertLine(columnIndex, selection, hasMergedCells) { var table = (0, _utils2.findTable)(selection); if (!table) { return []; } var map = _tableMap.TableMap.get(table.node); var isFirstColumn = columnIndex === 0; var isLastColumn = columnIndex === map.width; if (isLastColumn) { columnIndex -= 1; } var decorationClassName; if (hasMergedCells) { decorationClassName = isFirstColumn ? _types.TableCssClassName.WITH_FIRST_COLUMN_INSERT_LINE_INACTIVE : isLastColumn ? _types.TableCssClassName.WITH_LAST_COLUMN_INSERT_LINE_INACTIVE : _types.TableCssClassName.WITH_COLUMN_INSERT_LINE_INACTIVE; } else { decorationClassName = isFirstColumn ? _types.TableCssClassName.WITH_FIRST_COLUMN_INSERT_LINE : isLastColumn ? _types.TableCssClassName.WITH_LAST_COLUMN_INSERT_LINE : _types.TableCssClassName.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 _view.Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(_types.TableDecorations.COLUMN_INSERT_LINE, "_").concat(index) }); }).filter(_utils.nonNullable); }; var createRowInsertLine = exports.createRowInsertLine = function createRowInsertLine(rowIndex, selection, hasMergedCells) { var table = (0, _utils2.findTable)(selection); if (!table) { return []; } var map = _tableMap.TableMap.get(table.node); var isLastRow = rowIndex === map.height; if (isLastRow) { rowIndex -= 1; } var cells = (0, _utils2.getCellsInRow)(rowIndex)(selection); if (!cells) { return []; } var decorationClassName; if (hasMergedCells) { decorationClassName = isLastRow ? _types.TableCssClassName.WITH_LAST_ROW_INSERT_LINE_INACTIVE : _types.TableCssClassName.WITH_ROW_INSERT_LINE_INACTIVE; } else { decorationClassName = isLastRow ? _types.TableCssClassName.WITH_LAST_ROW_INSERT_LINE : _types.TableCssClassName.WITH_ROW_INSERT_LINE; } return cells.map(function (cell, index) { if (!cell || !cell.node) { return; } return _view.Decoration.node(cell.pos, cell.pos + cell.node.nodeSize, { class: decorationClassName }, { key: "".concat(_types.TableDecorations.ROW_INSERT_LINE, "_").concat(index) }); }).filter(_utils.nonNullable); };