@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
285 lines (271 loc) • 16.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isHeaderRowRequired = isHeaderRowRequired;
exports.unwrapContentFromTable = exports.transformSliceToRemoveOpenTable = exports.transformSliceToRemoveNestedTables = exports.transformSliceToFixHardBreakProblemOnCopyFromCell = exports.transformSliceToCorrectEmptyTableCells = exports.transformSliceTableLayoutDefaultToCenter = exports.removeTableFromLastChild = exports.removeTableFromFirstChild = void 0;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _nesting = require("@atlaskit/editor-common/nesting");
var _utils = require("@atlaskit/editor-common/utils");
var _model = require("@atlaskit/editor-prosemirror/model");
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
var _editorTables = require("@atlaskit/editor-tables");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _pluginFactory = require("../plugin-factory");
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
// lifts up the content of each cell, returning an array of nodes
var unwrapContentFromTable = exports.unwrapContentFromTable = function unwrapContentFromTable(maybeTable) {
var schema = maybeTable.type.schema;
if (maybeTable.type === schema.nodes.table) {
var content = [];
var _schema$nodes = schema.nodes,
tableCell = _schema$nodes.tableCell,
tableHeader = _schema$nodes.tableHeader;
maybeTable.descendants(function (maybeCell) {
if (maybeCell.type === tableCell || maybeCell.type === tableHeader) {
content.push.apply(content, (0, _toConsumableArray2.default)((0, _utils2.flatten)(maybeCell, false).map(function (child) {
return child.node;
})));
return false;
}
return true;
});
return content;
}
return maybeTable;
};
// Flattens nested tables after a given nesting depth
// If this looks familiar, it's a heavily modified version of `mapFragment` which has been
// adjusted to support tracking nesting depth. This wasn't possible by using `mapFragment` directly
var _unwrapNestedTables = function unwrapNestedTables(content, schema, unwrapNestDepth) {
var currentNestDepth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var flattenNested = function flattenNested(node, tableDepth) {
if (node.type === schema.nodes.table) {
if (tableDepth >= unwrapNestDepth) {
return unwrapContentFromTable(node);
}
return node;
}
return node;
};
var children = [];
for (var i = 0, size = content.childCount; i < size; i++) {
var node = content.child(i);
var transformed = node.isLeaf ? flattenNested(node, currentNestDepth) : flattenNested(node.copy(_model.Fragment.fromArray(_unwrapNestedTables(node.content, schema, unwrapNestDepth, node.type === schema.nodes.table ? currentNestDepth + 1 : currentNestDepth))), currentNestDepth);
if (transformed) {
if (Array.isArray(transformed)) {
children.push.apply(children, (0, _toConsumableArray2.default)(transformed));
} else {
children.push(transformed);
}
}
}
return children;
};
var removeTableFromFirstChild = exports.removeTableFromFirstChild = function removeTableFromFirstChild(node, i) {
return i === 0 ? unwrapContentFromTable(node) : node;
};
var removeTableFromLastChild = exports.removeTableFromLastChild = function removeTableFromLastChild(node, i, fragment) {
return i === fragment.childCount - 1 ? unwrapContentFromTable(node) : node;
};
var transformSliceToRemoveNestedTables = exports.transformSliceToRemoveNestedTables = function transformSliceToRemoveNestedTables(slice, schema, selection) {
var _schema$nodes2 = schema.nodes,
table = _schema$nodes2.table,
tableCell = _schema$nodes2.tableCell,
tableHeader = _schema$nodes2.tableHeader;
var openEnd = slice.openEnd;
var newFragment = (0, _utils.flatmap)(slice.content, function (node, i, fragment) {
var _slice$content$firstC, _slice$content$lastCh;
// We allow default nesting of 2 to support
// two levels of nesting in nodes that support table nesting already such as layoutSection and expands
var allowedTableNesting = 2;
var isCellSelection = selection instanceof _editorTables.CellSelection;
var isPasteInTable = (0, _utils2.hasParentNodeOfType)([table, tableCell, tableHeader])(selection);
var isPasteInNestedTable = (0, _nesting.getParentOfTypeCount)(schema.nodes.table)(selection.$from) > 1;
var isPasteFullTableInsideEmptyCellEnabled = (0, _platformFeatureFlags.fg)('platform_editor_paste_full_table_inside_empty_cell');
// Pasted content only contains a table and no other content
var isCellPaste = isPasteInTable && slice.content.childCount === 1 && ((_slice$content$firstC = slice.content.firstChild) === null || _slice$content$firstC === void 0 ? void 0 : _slice$content$firstC.type) === table && (!isPasteFullTableInsideEmptyCellEnabled || slice.openStart !== 0 && slice.openEnd !== 0);
// however if pasted content is a table, allow just one level
if (node.type === schema.nodes.table) {
allowedTableNesting = 1;
// if paste is inside a table, allow no further nesting
if (isPasteInTable) {
allowedTableNesting = 0;
}
// unless we are pasting inside a nested table, then bounce back to 1 level
// because editor-plugin-paste will lift the table to the parent table (just below it)
if (isPasteInNestedTable) {
allowedTableNesting = 1;
}
// paste of table cells into a table cell - content is spread across multiple cells
// by editor-tables so needs to be treated a little differently
if (isCellPaste || isCellSelection) {
allowedTableNesting = 1;
if (isPasteInNestedTable) {
allowedTableNesting = 0;
}
}
}
// Prevent invalid openEnd after pasting tables with a selection that ends inside a nested table cell.
// If the slice ends with a selection that ends inside a nested table, and we paste inside a table we
// need to adjust the openEnd because it is no longer correct. If we don't, Prosemirror fires an exception
// because it iterates to a non-existent depth and the transform will not be applied
if (slice.openEnd >= 7 &&
// depth of a nested table cell
slice.content.childCount > 1 && ((_slice$content$lastCh = slice.content.lastChild) === null || _slice$content$lastCh === void 0 ? void 0 : _slice$content$lastCh.type) === table && isPasteInTable) {
// re-point the slice's openEnd to non-nested table cell depth
openEnd = 4;
}
if (isCellSelection && !isCellPaste) {
// if pasting into a cell selection, we need to flatten the parent table as well
return _unwrapNestedTables(_model.Fragment.fromArray([node]), schema, allowedTableNesting);
} else {
// after we've worked out what the allowed nesting depth is, unwrap nested tables
var newChildren = _unwrapNestedTables(node.content, schema, allowedTableNesting);
return node.copy(_model.Fragment.fromArray(newChildren));
}
});
return new _model.Slice(newFragment, slice.openStart, openEnd);
};
/**
* When we copy from a table cell with a hardBreak at the end,
* the slice generated will come with a hardBreak outside of the table.
* This code will look for that pattern and fix it.
*/
var transformSliceToFixHardBreakProblemOnCopyFromCell = exports.transformSliceToFixHardBreakProblemOnCopyFromCell = function transformSliceToFixHardBreakProblemOnCopyFromCell(slice, schema) {
var _schema$nodes3 = schema.nodes,
paragraph = _schema$nodes3.paragraph,
table = _schema$nodes3.table,
hardBreak = _schema$nodes3.hardBreak;
var emptyParagraphNode = paragraph.createAndFill();
var hardBreakNode = hardBreak === null || hardBreak === void 0 ? void 0 : hardBreak.createAndFill();
var paragraphNodeSize = emptyParagraphNode ? emptyParagraphNode.nodeSize : 0;
var hardBreakNodeSize = hardBreakNode ? hardBreakNode.nodeSize : 0;
var paragraphWithHardBreakSize = paragraphNodeSize + hardBreakNodeSize;
if (slice.content.childCount === 2 && slice.content.firstChild && slice.content.lastChild && slice.content.firstChild.type === table && slice.content.lastChild.type === paragraph && slice.content.lastChild.nodeSize === paragraphWithHardBreakSize) {
var nodes = unwrapContentFromTable(slice.content.firstChild);
if (nodes instanceof Array) {
return new _model.Slice(_model.Fragment.from(
// keep only the content and discard the hardBreak
nodes[0]), slice.openStart, slice.openEnd);
}
}
return slice;
};
var isNodeSingleCellTable = function isNodeSingleCellTable(node, schema) {
var _node$firstChild, _node$firstChild$firs, _node$firstChild$firs2;
return node.childCount === 1 && ((_node$firstChild = node.firstChild) === null || _node$firstChild === void 0 ? void 0 : _node$firstChild.type) === schema.nodes.tableRow && node.firstChild.childCount === 1 && (((_node$firstChild$firs = node.firstChild.firstChild) === null || _node$firstChild$firs === void 0 ? void 0 : _node$firstChild$firs.type) === schema.nodes.tableCell || ((_node$firstChild$firs2 = node.firstChild.firstChild) === null || _node$firstChild$firs2 === void 0 ? void 0 : _node$firstChild$firs2.type) === schema.nodes.tableHeader);
};
var isFragmentSingleCellTable = function isFragmentSingleCellTable(fragment, schema) {
return fragment.childCount === 1 && fragment.firstChild !== null && isNodeSingleCellTable(fragment.firstChild, schema);
};
var containsNonTableBlockChildren = function containsNonTableBlockChildren(fragment, schema) {
var containsNonTableBlock = false;
var _schema$nodes4 = schema.nodes,
table = _schema$nodes4.table,
tableCell = _schema$nodes4.tableCell,
tableHeader = _schema$nodes4.tableHeader;
fragment.forEach(function (node) {
if (node.isBlock && ![table, tableCell, tableHeader].includes(node.type)) {
containsNonTableBlock = true;
}
});
return containsNonTableBlock;
};
var transformSliceToRemoveOpenTable = exports.transformSliceToRemoveOpenTable = function transformSliceToRemoveOpenTable(slice, schema) {
var _slice$content$firstC4;
// Case 1: A slice of a block selection inside a nested table
// Prosemirror wraps nested block selections in their respective tables
// We are using `safeInsert` to paste nested tables, so we do not want to preserve this wrapping
// slice starts and ends inside a nested table at the same depth
if (slice.openStart >= 7 && slice.openEnd >= 7) {
var cleaned = slice;
var descendedDepth = 0;
var tableDepthDecrement = 2;
// if the slice is a single cell table and contains cells with single cell tables, descend into it until we find textblock children
if (isFragmentSingleCellTable(slice.content, schema)) {
var _slice$content$firstC2;
(_slice$content$firstC2 = slice.content.firstChild) === null || _slice$content$firstC2 === void 0 || _slice$content$firstC2.descendants(function (node) {
if (isNodeSingleCellTable(node, schema)) {
descendedDepth += tableDepthDecrement;
} else if (node.type === schema.nodes.table) {
return false;
} else if (containsNonTableBlockChildren(node.content, schema)) {
descendedDepth += tableDepthDecrement;
// create a new slice with the content of non-table block children and the depth of the nested tables subtracted
cleaned = new _model.Slice(node.content, slice.openStart - descendedDepth - tableDepthDecrement, slice.openEnd - descendedDepth - tableDepthDecrement);
return false;
}
});
}
if (!cleaned.eq(slice)) {
return cleaned;
}
}
// Case 2: A slice entirely within a single CELL
if (
// starts and ends inside of a cell
slice.openStart >= 4 && slice.openEnd >= 4 &&
// slice is a table node
slice.content.childCount === 1 &&
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
slice.content.firstChild.type === schema.nodes.table) {
var _slice$content$firstC3;
// we're removing the table, tableRow and tableCell reducing the open depth by 3
var depthDecrement = 3;
// prosemirror-view has a bug that it duplicates table entry when selecting multiple paragraphs in a table cell.
// https://github.com/ProseMirror/prosemirror/issues/1270
// The structure becomes
// table(genuine) > tableRow(genuine) > table(duplicated) > tableRow(duplicated) > tableCell/tableHeader(genuine) > contents(genuine)
// As we are removing wrapping table anyway, we keep duplicated table and tableRow for simplicity
var _cleaned = slice;
if (((_slice$content$firstC3 = slice.content.firstChild) === null || _slice$content$firstC3 === void 0 || (_slice$content$firstC3 = _slice$content$firstC3.content) === null || _slice$content$firstC3 === void 0 || (_slice$content$firstC3 = _slice$content$firstC3.firstChild) === null || _slice$content$firstC3 === void 0 || (_slice$content$firstC3 = _slice$content$firstC3.content) === null || _slice$content$firstC3 === void 0 || (_slice$content$firstC3 = _slice$content$firstC3.firstChild) === null || _slice$content$firstC3 === void 0 ? void 0 : _slice$content$firstC3.type) === schema.nodes.table) {
_cleaned = new _model.Slice(slice.content.firstChild.content.firstChild.content, slice.openStart - 2, slice.openEnd - 2);
}
return new _model.Slice((0, _utils.flatmap)(_cleaned.content, unwrapContentFromTable), _cleaned.openStart - depthDecrement, _cleaned.openEnd - depthDecrement);
}
// Case 3: A slice starting within a CELL and ending outside the table
if (
// starts inside of a cell but ends outside of the starting table
slice.openStart >= 4 &&
// slice starts from a table node (and spans across more than one node)
slice.content.childCount > 1 && ((_slice$content$firstC4 = slice.content.firstChild) === null || _slice$content$firstC4 === void 0 ? void 0 : _slice$content$firstC4.type) === schema.nodes.table) {
// repoint the slice's cutting depth so that cell content where the slice starts
// does not get lifted out of the cell on paste
return new _model.Slice(slice.content, 1, slice.openEnd);
}
return slice;
};
var transformSliceToCorrectEmptyTableCells = exports.transformSliceToCorrectEmptyTableCells = function transformSliceToCorrectEmptyTableCells(slice, schema) {
var _schema$nodes5 = schema.nodes,
tableCell = _schema$nodes5.tableCell,
tableHeader = _schema$nodes5.tableHeader;
return (0, _utils.mapSlice)(slice, function (node) {
if (node && (node.type === tableCell || node.type === tableHeader) && !node.content.childCount) {
return node.type.createAndFill(node.attrs) || node;
}
return node;
});
};
function isHeaderRowRequired(state) {
var tableState = (0, _pluginFactory.getPluginState)(state);
return tableState && tableState.pluginConfig.isHeaderRowRequired;
}
var transformSliceTableLayoutDefaultToCenter = exports.transformSliceTableLayoutDefaultToCenter = function transformSliceTableLayoutDefaultToCenter(slice, schema) {
var table = schema.nodes.table;
var children = [];
(0, _utils.mapChildren)(slice.content, function (node) {
if (node.type === table && node.attrs.layout === 'default') {
children.push(table.createChecked(_objectSpread(_objectSpread({}, node.attrs), {}, {
layout: 'center'
}), node.content, node.marks));
} else {
children.push(node);
}
});
return new _model.Slice(_model.Fragment.fromArray(children), slice.openStart, slice.openEnd);
};