@atlaskit/editor-plugin-list
Version:
List plugin for @atlaskit/editor-core
291 lines (284 loc) • 13.4 kB
JavaScript
"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;
}