UNPKG

@atlaskit/editor-plugin-list

Version:

List plugin for @atlaskit/editor-core

291 lines (284 loc) 13.4 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.applyListNormalisationFixes = applyListNormalisationFixes; exports.liftFollowingList = liftFollowingList; exports.liftNodeSelectionList = liftNodeSelectionList; exports.liftTextSelectionList = liftTextSelectionList; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _utils = require("@atlaskit/editor-common/utils"); var _model = require("@atlaskit/editor-prosemirror/model"); var _state = require("@atlaskit/editor-prosemirror/state"); var _transform = require("@atlaskit/editor-prosemirror/transform"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _indentation = require("./utils/indentation"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function liftListItem(selection, tr) { var $from = selection.$from, $to = selection.$to; var nodeType = tr.doc.type.schema.nodes.listItem; var range = $from.blockRange($to, function (node) { return !!node.childCount && !!node.firstChild && node.firstChild.type === nodeType; }); if (!range || range.depth < 2 || $from.node(range.depth - 1).type !== nodeType) { return tr; } var end = range.end; var endOfList = $to.end(range.depth); if (end < endOfList) { tr.step(new _transform.ReplaceAroundStep(end - 1, endOfList, end, endOfList, new _model.Slice(_model.Fragment.from(nodeType.create(undefined, range.parent.copy())), 1, 0), 1, true)); range = new _model.NodeRange(tr.doc.resolve($from.pos), tr.doc.resolve(endOfList), range.depth); } return tr.lift(range, (0, _transform.liftTarget)(range)).scrollIntoView(); } // Function will lift list item following selection to level-1. function liftFollowingList(from, to, rootListDepth, tr) { var listItem = tr.doc.type.schema.nodes.listItem; var lifted = false; tr.doc.nodesBetween(from, to, function (node, pos) { if (!lifted && node.type === listItem && pos > from) { lifted = true; var listDepth = rootListDepth + 3; while (listDepth > rootListDepth + 2) { var start = tr.doc.resolve(tr.mapping.map(pos)); listDepth = start.depth; var end = tr.doc.resolve(tr.mapping.map(pos + node.textContent.length)); var sel = new _state.TextSelection(start, end); tr = liftListItem(sel, tr); } } }); return tr; } function liftNodeSelectionList(selection, tr) { var from = selection.from; var listItem = tr.doc.type.schema.nodes.listItem; var mappedPosition = tr.mapping.map(from); var nodeAtPos = tr.doc.nodeAt(mappedPosition); var start = tr.doc.resolve(mappedPosition); if ((start === null || start === void 0 ? void 0 : start.parent.type) !== listItem) { return tr; } var end = tr.doc.resolve(mappedPosition + ((nodeAtPos === null || nodeAtPos === void 0 ? void 0 : nodeAtPos.nodeSize) || 1)); var range = start.blockRange(end); if (range) { var _liftTarget = (0, _indentation.getListLiftTarget)(start); tr.lift(range, _liftTarget); } return tr; } // The function will list paragraphs in selection out to level 1 below root list. function liftTextSelectionList(selection, tr) { var from = selection.from, to = selection.to; var paragraph = tr.doc.type.schema.nodes.paragraph; var listCol = []; tr.doc.nodesBetween(from, to, function (node, pos) { if (node.type === paragraph) { listCol.push({ node: node, pos: pos }); } }); for (var i = listCol.length - 1; i >= 0; i--) { var _paragraph = listCol[i]; var start = tr.doc.resolve(tr.mapping.map(_paragraph.pos)); if (start.depth > 0) { var end = void 0; if (_paragraph.node.textContent && _paragraph.node.textContent.length > 0) { end = tr.doc.resolve(tr.mapping.map(_paragraph.pos + _paragraph.node.textContent.length)); } else { end = tr.doc.resolve(tr.mapping.map(_paragraph.pos + 1)); } var range = start.blockRange(end); if (range) { tr.lift(range, (0, _indentation.getListLiftTarget)(start)); } } } return tr; } /** * Finds the top-level list nodes (bulletList/orderedList) that contain the positions * affected by the given transactions. Returns a map of list node position → list node, * so callers can scan only the affected subtrees rather than the entire document. */ function getAffectedListsFromTransactions(transactions, doc, schema) { var _schema$nodes = schema.nodes, bulletList = _schema$nodes.bulletList, orderedList = _schema$nodes.orderedList; var listTypes = [bulletList, orderedList].filter(Boolean); if (listTypes.length === 0) { return new Map(); } var result = new Map(); var _iterator = _createForOfIteratorHelper(transactions), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var tr = _step.value; var _iterator2 = _createForOfIteratorHelper(tr.steps), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var step = _step2.value; // ReplaceStep and ReplaceAroundStep both have from/to — other step types are skipped. if (!(step instanceof _transform.ReplaceStep) && !(step instanceof _transform.ReplaceAroundStep)) { continue; } // Check both the start and end of each changed range, mapped to post-transaction positions. for (var _i = 0, _arr = [step.from, step.to]; _i < _arr.length; _i++) { var rawPos = _arr[_i]; var mappedPos = Math.min(tr.mapping.map(rawPos), doc.content.size - 1); var $pos = doc.resolve(mappedPos); // Walk ancestors from inner to outer, recording the outermost list node. // Once we find a list and then exit list structure (hit a non-list ancestor), // break early — prevents container nodes (e.g. panel) from causing us to // return an outer list that is in a different structural context. // $pos.node(depth) is O(1) array access. var rootListPos = null; var rootListNode = null; for (var depth = $pos.depth; depth >= 0; depth--) { var node = $pos.node(depth); if (listTypes.includes(node.type)) { rootListPos = $pos.before(depth); rootListNode = node; } else if (rootListNode !== null && node.type !== schema.nodes.listItem) { // We've exited the list structure — stop walking. break; } } if (rootListPos !== null && rootListNode !== null) { result.set(rootListPos, rootListNode); } } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return result; } /** * Applies list normalisation fixes to the given transaction for all affected list subtrees. * Processes nodes in reverse document order so that position offsets from insertions/joins * do not affect earlier positions. * * When platform_editor_flexible_list_indentation is off: inserts an empty paragraph before any listItem whose * first child is a list node, and merges adjacent same-type list nodes within a listItem. * When platform_editor_flexible_list_indentation is on: only merges adjacent same-type list nodes. */ function applyListNormalisationFixes(_ref) { var tr = _ref.tr, transactions = _ref.transactions, doc = _ref.doc, schema = _ref.schema; var affectedLists = getAffectedListsFromTransactions(transactions, doc, schema); if (affectedLists.size === 0) { return tr; } var _schema$nodes2 = schema.nodes, listItem = _schema$nodes2.listItem, paragraph = _schema$nodes2.paragraph, bulletList = _schema$nodes2.bulletList, orderedList = _schema$nodes2.orderedList, taskList = _schema$nodes2.taskList; if (!listItem) { return tr; } var nestedListTypes = [bulletList, orderedList, taskList].filter(Boolean); // Process lists in reverse position order so fixes at higher positions // don't shift the positions of fixes at lower positions. var sortedEntries = (0, _toConsumableArray2.default)(affectedLists.entries()).sort(function (_ref2, _ref3) { var _ref4 = (0, _slicedToArray2.default)(_ref2, 1), posA = _ref4[0]; var _ref5 = (0, _slicedToArray2.default)(_ref3, 1), posB = _ref5[0]; return posB - posA; }); var _iterator3 = _createForOfIteratorHelper(sortedEntries), _step3; try { var _loop = function _loop() { var _step3$value = (0, _slicedToArray2.default)(_step3.value, 1), listPos = _step3$value[0]; // Re-resolve the list node from the current transaction doc (post-paste state), // as the original listNode snapshot may be stale after the paste transaction. var mappedListPos = tr.mapping.map(listPos); var currentListNode = tr.doc.nodeAt(mappedListPos); if (!currentListNode) { return 1; // continue } // Collect all listItem positions at all depths in document order, then process in // reverse so that fixes at higher positions don't shift positions of lower ones. var listItemPositions = []; currentListNode.descendants(function (node, offsetPos) { if (node.type === listItem) { listItemPositions.push(mappedListPos + 1 + offsetPos); } return true; }); for (var i = listItemPositions.length - 1; i >= 0; i--) { var mappedPos = tr.mapping.map(listItemPositions[i]); var node = tr.doc.nodeAt(mappedPos); if (!node || node.type !== listItem) { continue; } // Merge adjacent same-type list nodes (highest boundary first within the listItem). for (var j = node.childCount - 1; j > 0; j--) { var child = node.child(j); var prevChild = node.child(j - 1); if ((0, _utils.isListNode)(child) && child.type === prevChild.type) { var offset = 1; // +1 for listItem opening token for (var k = 0; k < j; k++) { offset += node.child(k).nodeSize; } try { tr.join(mappedPos + offset); } catch (e) { // join may fail if position is invalid after earlier transforms — skip // eslint-disable-next-line no-console console.warn('[editor-plugin-list] applyListNormalisationFixes: unexpected join failure', e); } } } // Insert empty paragraph before a list-type first child when _indentation is off. // Only list types (bulletList, orderedList, taskList) are invalid as a first child — // other non-paragraph types (mediaSingle, codeBlock, extension) are valid per the schema. if (paragraph && !(0, _expValEquals.expValEquals)('platform_editor_flexible_list_indentation', 'isEnabled', true)) { // Re-map position after any join steps that may have been added above. var remappedPos = tr.mapping.map(listItemPositions[i]); var currentNode = tr.doc.nodeAt(remappedPos); var firstChild = currentNode === null || currentNode === void 0 ? void 0 : currentNode.firstChild; if (firstChild && nestedListTypes.includes(firstChild.type)) { var emptyParagraph = paragraph.createAndFill(); if (emptyParagraph) { tr.insert(remappedPos + 1, emptyParagraph); } } } } }; for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { if (_loop()) continue; } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } return tr; }