@atlaskit/editor-plugin-list
Version:
List plugin for @atlaskit/editor-core
297 lines (281 loc) • 11.5 kB
JavaScript
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; }
import { buildReplacementFragment as buildReplacementFragmentBase, flattenList as flattenListBase } from '@atlaskit/editor-common/lists';
import { isListItemNode, isListNode } from '@atlaskit/editor-common/utils';
import { fg } from '@atlaskit/platform-feature-flags';
/**
* Returns true if a listItem has at least one non-list child (paragraph, etc.).
*/
function hasContentChildren(listItem) {
return listItem.children.some(function (child) {
return !isListNode(child);
});
}
/**
* Compute the size of non-list (content) children of a listItem, which
* represents the "visible" bounds of the item for selection purposes.
*/
function contentSize(listItem) {
return listItem.children.reduce(function (size, child) {
return size + (isListNode(child) ? 0 : child.nodeSize);
}, 0);
}
/**
* Flattens a list, splitting each list item into one item per run of content.
* An item like `li(p, ul, p, ul)` becomes separate sibling items, so content
* placed between sub-lists keeps its order. Also records which items the
* selection touches.
*/
function flattenListImpl(options) {
var doc = options.doc,
rootListStart = options.rootListStart,
selectionFrom = options.selectionFrom,
selectionTo = options.selectionTo,
indentDelta = options.indentDelta,
maxDepth = options.maxDepth;
var rootList = doc.nodeAt(rootListStart);
if (!rootList) {
return null;
}
var schema = rootList.type.schema;
var items = [];
var startIndex = -1;
var endIndex = -1;
var exceedsMaxDepth = false;
var isPointSelection = selectionFrom === selectionTo;
var pushSegment = function pushSegment(segmentNode, contentStart, contentEnd, depth, parentList) {
var isSelected = isPointSelection ? selectionFrom >= contentStart && selectionFrom <= contentEnd : contentStart < selectionTo && contentEnd > selectionFrom;
var effectiveDepth = depth + (isSelected ? indentDelta : 0);
items.push({
node: segmentNode,
pos: contentStart - 1,
depth: effectiveDepth,
isSelected: isSelected,
listType: parentList.type.name,
parentListAttrs: parentList.attrs
});
if (isSelected) {
var index = items.length - 1;
if (startIndex === -1) {
startIndex = index;
}
endIndex = index;
if (maxDepth != null && effectiveDepth >= maxDepth) {
exceedsMaxDepth = true;
}
}
};
var _walkList = function walkList(listNode, listPos, depth) {
var childPos = listPos + 1;
listNode.forEach(function (listItemNode) {
if (!isListItemNode(listItemNode)) {
childPos += listItemNode.nodeSize;
return;
}
var itemStart = childPos;
var innerPos = itemStart + 1;
// Content collected since the last sub-list. Each run becomes its own item.
var segmentContent = [];
var segmentContentStart = innerPos;
var segmentContentSize = 0;
var emitSegment = function emitSegment(parentList) {
if (segmentContent.length === 0) {
return;
}
var segmentItem = schema.nodes.listItem.create(listItemNode.attrs, segmentContent);
pushSegment(segmentItem, segmentContentStart, segmentContentStart + segmentContentSize, depth, parentList);
};
listItemNode.forEach(function (child) {
if (isListNode(child)) {
// A sub-list ends the current run: emit it, then walk the sub-list deeper.
emitSegment(listNode);
segmentContent = [];
segmentContentSize = 0;
_walkList(child, innerPos, depth + 1);
innerPos += child.nodeSize;
segmentContentStart = innerPos;
} else {
if (segmentContent.length === 0) {
segmentContentStart = innerPos;
}
segmentContent.push(child);
segmentContentSize += child.nodeSize;
innerPos += child.nodeSize;
}
});
// Emit any remaining content after the last sub-list.
emitSegment(listNode);
childPos += listItemNode.nodeSize;
});
};
_walkList(rootList, rootListStart, 0);
if (items.length === 0 || startIndex === -1 || exceedsMaxDepth) {
return null;
}
return {
items: items,
startIndex: startIndex,
endIndex: endIndex
};
}
export function flattenList(options) {
if (fg('platform_editor_flexible_list_normalization_fix')) {
return flattenListImpl(options);
}
return flattenListBase(options, {
isContentNode: function isContentNode(node, parent) {
return isListItemNode(node) && hasContentChildren(node) && isListNode(parent);
},
// +1 shifts from the listItem node boundary to the start of its content children
getSelectionBounds: function getSelectionBounds(node, pos) {
return {
start: pos + 1,
end: pos + 1 + contentSize(node)
};
},
getDepth: function getDepth(resolvedDepth, rootDepth) {
return (resolvedDepth - rootDepth - 1) / 2;
}
});
}
/**
* Extract non-list (content) children from a listItem node.
*/
function extractContentChildren(listItem) {
var children = [];
for (var i = 0; i < listItem.childCount; i++) {
var child = listItem.child(i);
if (!isListNode(child)) {
children.push(child);
}
}
return children;
}
/**
* Rebuild a ProseMirror list tree from a flat array of `FlattenedItem` objects
* using a bottom-up stack approach.
*
* The algorithm tracks open list/listItem wrappers on a stack. As depth
* transitions occur between consecutive elements, wrapper nodes are opened
* (depth increase) or closed (depth decrease).
*/
function rebuildPMList(elements, schema) {
if (elements.length === 0) {
return null;
}
// Each stack frame represents an open list at a given depth.
// items[] accumulates the PMNode children (listItem nodes) for that list.
var stack = [];
function openList(listType, listAttrs) {
stack.push({
listType: listType,
listAttrs: listAttrs,
items: []
});
}
/**
* Close lists on the stack down to `targetDepth`, wrapping each closed
* list into the last listItem of its parent.
*/
function closeToDepth(targetDepth) {
var _loop = function _loop() {
var closed = stack.pop();
if (!closed) {
return 1; // break
}
var listNode = schema.nodes[closed.listType].create(closed.listAttrs, closed.items);
// Attach the closed list to the last listItem on the parent frame
var parentFrame = stack[stack.length - 1];
var lastItem = parentFrame.items[parentFrame.items.length - 1];
if (lastItem) {
// Append the nested list to this listItem's children
var newContent = [];
lastItem.forEach(function (child) {
return newContent.push(child);
});
newContent.push(listNode);
parentFrame.items[parentFrame.items.length - 1] = schema.nodes.listItem.create(lastItem.attrs, newContent);
} else {
// Edge case: no listItem to attach to. Create a wrapper.
var wrapperItem = schema.nodes.listItem.create(null, [listNode]);
parentFrame.items.push(wrapperItem);
}
};
while (stack.length > targetDepth + 1) {
if (_loop()) break;
}
}
// Seed the root list with the first element's parent list attributes
openList(elements[0].listType, elements[0].parentListAttrs);
var _iterator = _createForOfIteratorHelper(elements),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var el = _step.value;
var targetDepth = el.depth;
// Close lists if we're going shallower
if (stack.length > targetDepth + 1) {
closeToDepth(targetDepth);
}
// Open lists if we need to go deeper.
// We do NOT create wrapper listItems here — closeToDepth handles
// creating wrappers that contain only the nested list (no empty paragraph).
// For unselected elements, the list structure already existed so we
// preserve the parent list's attributes. For selected (moved) elements,
// this is a new nesting level so we use null (the localId plugin will
// backfill a fresh UUID).
while (stack.length < targetDepth + 1) {
openList(el.listType, el.isSelected ? null : el.parentListAttrs);
}
// Build the listItem for this element using its content children
var contentChildren = extractContentChildren(el.node);
var listItem = schema.nodes.listItem.create(el.node.attrs, contentChildren);
stack[stack.length - 1].items.push(listItem);
}
// Close all remaining open lists
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
closeToDepth(0);
var root = stack[0];
var rebuilt = schema.nodes[root.listType].create(root.listAttrs, root.items);
// Compute content start offsets by walking the rebuilt tree.
var contentStartOffsets = new Array(elements.length);
var segIdx = 0;
rebuilt.descendants(function (node, pos) {
if (isListItemNode(node) && hasContentChildren(node)) {
// +1 for rebuilt's opening tag, +1 for listItem's opening tag
contentStartOffsets[segIdx] = 1 + pos + 1;
segIdx++;
}
return true;
});
return {
node: rebuilt,
contentStartOffsets: contentStartOffsets
};
}
/**
* Build a replacement Fragment from a flat array of `FlattenedItem` objects.
*
* Elements with depth >= 0 are grouped into consecutive list segments
* and rebuilt via `rebuildPMList`. Elements with depth < 0 (extracted
* past the root) are converted to their content children (paragraphs).
* The result interleaves list nodes and extracted content in document order.
*
* Delegates to the shared `buildReplacementFragment` with list-specific
* rebuild and extraction functions.
*/
export function buildReplacementFragment(elements, schema) {
return buildReplacementFragmentBase({
items: elements,
schema: schema,
rebuildFn: rebuildPMList,
extractContentFn: function extractContentFn(item) {
return extractContentChildren(item.node);
}
});
}