@atlaskit/editor-plugin-list
Version:
List plugin for @atlaskit/editor-core
202 lines (187 loc) • 8.18 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';
/**
* 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);
}
/**
* Flatten a root list into a flat array of content-bearing items
* and simultaneously determine which elements intersect the user's selection.
*
* Delegates to the shared `flattenListLike` with list-specific callbacks.
* Selection intersection is checked against each item's content-only
* span (excluding nested lists).
*/
export function flattenList(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);
}
});
}