@atlaskit/editor-plugin-selection-extension
Version:
editor-plugin-selection-extension plugin for @atlaskit/editor-core
277 lines (260 loc) • 9.77 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.wrapNodesInDoc = exports.getSelectionInfoFromSameNode = exports.getSelectionInfo = exports.getCommonParentContainer = exports.buildSelectionRanges = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _monitoring = require("@atlaskit/editor-common/monitoring");
var _model = require("@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.
*/
var buildSelectionRanges = exports.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((0, _toConsumableArray2.default)(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.
*/
var getCommonParentContainer = exports.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
*/
var wrapNodesInDoc = exports.wrapNodesInDoc = function wrapNodesInDoc(schema, nodes) {
if (nodes.length === 0) {
return schema.nodes.doc.createChecked({}, _model.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, _model.Fragment.from(nodes));
} catch (error) {
(0, _monitoring.logException)(error, {
location: 'editor-plugin-selection-extension'
});
return schema.nodes.doc.createChecked({}, _model.Fragment.empty);
}
};
var getSelectionInfoFromSameNode = exports.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()
};
};
var getSelectionInfo = exports.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
};
};