@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
506 lines (499 loc) • 19.2 kB
JavaScript
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);
};