UNPKG

@atlaskit/editor-plugin-list

Version:

List plugin for @atlaskit/editor-core

202 lines (187 loc) 8.18 kB
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); } }); }