UNPKG

@atlaskit/editor-plugin-selection-extension

Version:

editor-plugin-selection-extension plugin for @atlaskit/editor-core

270 lines (254 loc) 9.24 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { logException } from '@atlaskit/editor-common/monitoring'; import { Fragment } from '@atlaskit/editor-prosemirror/model'; var LIST_ITEM_TYPES = new Set(['taskItem', 'decisionItem', 'listItem']); var LIST_NODE_TYPES = new Set(['taskList', 'bulletList', 'orderedList', 'decisionList']); /** * Build a JSON pointer path for a node within the selectedNode structure. * @param pathIndices - Array of content indices representing the path to the node */ var buildJsonPointer = function buildJsonPointer(pathIndices) { return pathIndices.map(function (index) { return "/content/".concat(index); }).join(''); }; /** * Build selection ranges for multi-node selections. * This function traverses the selectedNode and creates JSON pointer-based ranges * that describe what parts of the selectedNode are included in the selection. */ export var buildSelectionRanges = function buildSelectionRanges(selectedNode, selectedNodePos, $from, $to) { var selectionStart = $from.pos; var selectionEnd = $to.pos; var tokenOffset = selectedNode.type.name === 'doc' ? 0 : 1; var nodeStart = selectedNodePos + tokenOffset; var nodeEnd = selectedNodePos + selectedNode.nodeSize - tokenOffset; // If selection spans entire content, return undefined (complete block selection) if (selectionStart <= nodeStart && selectionEnd >= nodeEnd) { return undefined; } var selectionRanges = []; // Traverse the selectedNode and find all nodes/text within the selection range var _traverse = function traverse(node, nodePos, path) { var nodeEndPos = nodePos + node.nodeSize; // Skip nodes completely before or after selection if (nodeEndPos <= selectionStart || nodePos >= selectionEnd) { return; } if (node.isText) { var _node$text; var textStart = nodePos; var textEnd = nodePos + (((_node$text = node.text) === null || _node$text === void 0 ? void 0 : _node$text.length) || 0); var rangeStart = Math.max(textStart, selectionStart); var rangeEnd = Math.min(textEnd, selectionEnd); if (rangeStart < rangeEnd) { var pointer = "".concat(buildJsonPointer(path), "/text"); selectionRanges.push({ start: { pointer: pointer, position: rangeStart - textStart }, end: { pointer: pointer, position: rangeEnd - textStart } }); } } else if (node.content.size > 0) { var contentStart = nodePos + 1; var contentEnd = nodeEndPos - 1; var isWholeBlockSelected = node.isBlock && !node.isTextblock && !LIST_NODE_TYPES.has(node.type.name) && selectionStart <= contentStart && selectionEnd >= contentEnd; if (isWholeBlockSelected) { var _pointer = buildJsonPointer(path); selectionRanges.push({ start: { pointer: _pointer }, end: { pointer: _pointer } }); } else { // Traverse children for textblocks, lists, or partial selections var _childPos = nodePos + 1; for (var i = 0; i < node.content.childCount; i++) { _traverse(node.content.child(i), _childPos, [].concat(_toConsumableArray(path), [i])); _childPos += node.content.child(i).nodeSize; } } } else if (nodePos >= selectionStart && nodeEndPos <= selectionEnd) { // Handle leaf nodes (e.g., hardBreak, image) var _pointer2 = buildJsonPointer(path); selectionRanges.push({ start: { pointer: _pointer2 }, end: { pointer: _pointer2 } }); } }; // Traverse each child of the selectedNode var childPos = nodeStart; for (var i = 0; i < selectedNode.content.childCount; i++) { var child = selectedNode.content.child(i); _traverse(child, childPos, [i]); childPos += child.nodeSize; } return selectionRanges.length > 0 ? selectionRanges : undefined; }; /** Find the depth of the deepest common ancestor node. */ var getCommonAncestorDepth = function getCommonAncestorDepth($from, $to) { var minDepth = Math.min($from.depth, $to.depth); for (var d = 0; d <= minDepth; d++) { if ($from.node(d) !== $to.node(d)) { return d - 1; } } return minDepth; }; /** * Find the closest parent container node that contains the selection. * - For lists: returns the topmost list (to handle nested lists) * - For other containers returns the closest one * Returns the parent and its position. */ export var getCommonParentContainer = function getCommonParentContainer($from, $to) { var commonDepth = getCommonAncestorDepth($from, $to); // Single pass: look for topmost list OR first non-list parent var topMostList = null; var topMostListPos = -1; var firstNonListParent = null; var firstNonListParentPos = -1; for (var depth = commonDepth; depth > 0; depth--) { var node = $from.node(depth); if (LIST_NODE_TYPES.has(node.type.name)) { // Keep updating to find the topmost list (last one found going upward) topMostList = node; topMostListPos = $from.before(depth); } else if (!firstNonListParent && node.type.name !== 'doc') { // Only capture the first (innermost) non-list parent firstNonListParent = node; firstNonListParentPos = $from.before(depth); } } // Return topmost list if found, else first non-list parent if (topMostList) { return { node: topMostList, pos: topMostListPos }; } return { node: firstNonListParent, pos: firstNonListParentPos }; }; /** * Wraps nodes in a doc fragment if there are multiple nodes */ export var wrapNodesInDoc = function wrapNodesInDoc(schema, nodes) { if (nodes.length === 0) { return schema.nodes.doc.createChecked({}, Fragment.empty); } // Single node: return unwrapped if (nodes.length === 1) { return nodes[0]; } // For multiple nodes, wrap in doc try { return schema.node('doc', null, Fragment.from(nodes)); } catch (error) { logException(error, { location: 'editor-plugin-selection-extension' }); return schema.nodes.doc.createChecked({}, Fragment.empty); } }; export var getSelectionInfoFromSameNode = function getSelectionInfoFromSameNode(selection) { var $from = selection.$from, $to = selection.$to; return { selectedNode: $from.node(), selectionRanges: [{ start: { pointer: "/content/".concat($from.index(), "/text"), position: $from.parentOffset }, end: { pointer: "/content/".concat($from.index(), "/text"), position: $to.parentOffset } }], nodePos: $from.before() }; }; export var getSelectionInfo = function getSelectionInfo(selection, schema) { var $from = selection.$from, $to = selection.$to; // For same parent selections but not the r, check for parent container if ($from.parent === $to.parent && $from.depth > 0) { var _getCommonParentConta = getCommonParentContainer($from, $to), parentNode = _getCommonParentConta.node, parentNodePos = _getCommonParentConta.pos; if (parentNode) { var _selectionRanges = buildSelectionRanges(parentNode, parentNodePos, $from, $to); return { selectedNode: parentNode, nodePos: parentNodePos, selectionRanges: _selectionRanges }; } var nodePos = $from.before(); var _selectionRanges2 = buildSelectionRanges($from.node(), nodePos, $from, $to); return { selectedNode: $from.node(), nodePos: nodePos, selectionRanges: _selectionRanges2 }; } // find the common ancestor var range = $from.blockRange($to); if (!range) { return { selectedNode: $from.node(), nodePos: $from.depth > 0 ? $from.before() : $from.pos }; } if (range.parent.type.name !== 'doc') { // For lists, find topmost list parent; otherwise use immediate parent if (LIST_NODE_TYPES.has(range.parent.type.name) || LIST_ITEM_TYPES.has(range.parent.type.name)) { var _getCommonParentConta2 = getCommonParentContainer($from, $to), topList = _getCommonParentConta2.node, topListPos = _getCommonParentConta2.pos; if (topList) { var _selectionRanges3 = buildSelectionRanges(topList, topListPos, $from, $to); return { selectedNode: topList, nodePos: topListPos, selectionRanges: _selectionRanges3 }; } } var _nodePos = range.depth > 0 ? $from.before(range.depth) : 0; var _selectionRanges4 = buildSelectionRanges(range.parent, _nodePos, $from, $to); return { selectedNode: range.parent, nodePos: _nodePos, selectionRanges: _selectionRanges4 }; } // Extract complete nodes within the block range var nodes = []; for (var i = range.startIndex; i < range.endIndex; i++) { nodes.push(range.parent.child(i)); } var selectedNode = wrapNodesInDoc(schema, nodes); var selectionRanges = buildSelectionRanges(selectedNode, range.start, $from, $to); return { selectedNode: selectedNode, nodePos: range.start, selectionRanges: selectionRanges }; };