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