UNPKG

@atlaskit/editor-plugin-selection

Version:

Selection plugin for @atlaskit/editor-core

396 lines (376 loc) 18 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.isSelectionAtStartOfParentNode = exports.isSelectionAtEndOfParentNode = exports.isSelectionAtEndOfLayoutColumn = exports.isSelectableContainerNode = exports.isSelectableChildNode = exports.isPanelOrExpandNode = exports.isListItemWithinContainerNotAtEnd = exports.isLayoutColumnNode = exports.isContainerNode = exports.getNodesToDecorateFromSelection = exports.getDecorations = exports.findTopLevelList = exports.findSelectableContainerParent = exports.findSelectableContainerBefore = exports.findSelectableContainerAfter = exports.findLastChildNodeToSelect = exports.findFirstChildNodeToSelect = void 0; exports.shouldRecalcDecorations = shouldRecalcDecorations; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _selection = require("@atlaskit/editor-common/selection"); var _utils = require("@atlaskit/editor-common/utils"); var _state = require("@atlaskit/editor-prosemirror/state"); var _utils2 = require("@atlaskit/editor-prosemirror/utils"); var _view = require("@atlaskit/editor-prosemirror/view"); var _editorSharedStyles = require("@atlaskit/editor-shared-styles"); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); var _types = require("../types"); var _hideCursorDecoration = require("./cursor/ui/hide-cursor-decoration"); var getDecorations = exports.getDecorations = function getDecorations(tr, manualSelection, hideCursor, blockSelection) { var selection = tr.selection; var decorations = []; if (hideCursor) { decorations.push((0, _hideCursorDecoration.createHideCursorDecoration)()); } if (selection instanceof _state.NodeSelection) { decorations.push(_view.Decoration.node(selection.from, selection.to, { class: _editorSharedStyles.akEditorSelectedNodeClassName })); return _view.DecorationSet.create(tr.doc, decorations); } if (selection instanceof _state.TextSelection || selection instanceof _state.AllSelection) { if (manualSelection && manualSelection.anchor >= 0 && manualSelection.head >= 0 && manualSelection.anchor <= tr.doc.nodeSize && manualSelection.head <= tr.doc.nodeSize) { selection = _state.TextSelection.create(tr.doc, manualSelection.anchor, manualSelection.head); } // Only apply node decorations when there is an active block selection. // When there is no block selection, text selections should use native browser selection appearance. if (!(0, _experiments.editorExperiment)('platform_editor_block_menu', true, { exposure: true }) || blockSelection) { var selectionDecorations = getNodesToDecorateFromSelection(selection, tr.doc).map(function (_ref) { var node = _ref.node, pos = _ref.pos; return _view.Decoration.node(pos, pos + node.nodeSize, { class: _editorSharedStyles.akEditorSelectedNodeClassName }); }); decorations.push.apply(decorations, (0, _toConsumableArray2.default)(selectionDecorations)); } return _view.DecorationSet.create(tr.doc, decorations); } return decorations.length > 0 ? _view.DecorationSet.create(tr.doc, decorations) : _view.DecorationSet.empty; }; var topLevelBlockNodesThatHaveSelectionStyles = ['table', 'panel', 'expand', 'layoutSection', 'decisionList', 'decisionItem', 'codeBlock']; /** * Use `getNodesToDecorateFromSelection` to collect and return * a list of nodes within the Selection that should have Selection * decorations applied. This allows selection styles to be added to * nested nodes. It will ignore text nodes as decorations are * applied natively and also ignore nodes that don't completely * sit within the given `Selection`. */ var getNodesToDecorateFromSelection = exports.getNodesToDecorateFromSelection = function getNodesToDecorateFromSelection(selection, doc) { var nodes = []; if (selection.from !== selection.to) { var from = selection.from, to = selection.to; doc.nodesBetween(from, to, function (node, pos) { var withinSelection = from <= pos && pos + node.nodeSize <= to; // The reason we need to check for these nodes is to stop // traversing their children if they are within a selection - // this is to prevent selection styles from being added to // the children as well as the parent node. // Example scenario is if an entire table has been selected // we should not traverse its children so we can apply the // selection styles to the table. But if an entire tableRow // has been selected (but the parent table has not) we should // traverse it as it could contain other nodes that need // selection styles. I couldn’t see a clear way to differentiate // without explicitly stating which nodes should be traversed // and which shouldn’t. var isTopLevelNodeThatHasSelectionStyles = topLevelBlockNodesThatHaveSelectionStyles.includes(node.type.name); // If the node is a top-level block node and completely sits within // the selection, we do not recurse it's children to prevent selection // styles being added to its child nodes. The expected behaviour // is that selection styles are only added to the parent. if (node && withinSelection && isTopLevelNodeThatHasSelectionStyles) { nodes.push({ node: node, pos: pos }); return false; // Otherwise we recurse the children and return them so we can apply // selection styles. Text is handled by the browser. } else if (node && withinSelection && !node.isText) { nodes.push({ node: node, pos: pos }); } return true; }); } return nodes; }; function shouldRecalcDecorations(_ref2) { var oldEditorState = _ref2.oldEditorState, newEditorState = _ref2.newEditorState; var oldSelection = oldEditorState.selection; var newSelection = newEditorState.selection; var oldPluginState = _types.selectionPluginKey.getState(oldEditorState); var newPluginState = _types.selectionPluginKey.getState(newEditorState); if (!oldPluginState || !newPluginState) { return false; } // If selection is unchanged, no need to recalculate if (oldSelection.eq(newSelection)) { // We need this special case for NodeSelection, as Prosemirror still thinks the // selections are equal when the node has changed if (oldSelection instanceof _state.NodeSelection && newSelection instanceof _state.NodeSelection) { var oldDecorations = oldPluginState.decorationSet.find(); var newDecorations = newPluginState.decorationSet.find(); // There might not be old or new decorations if the node selection is for a text node // This wouldn't have happened intentionally, but we need to handle this case regardless if (oldDecorations.length > 0 && newDecorations.length > 0) { return !oldDecorations[0].eq(newDecorations[0]); } return !(oldDecorations.length === 0 && newDecorations.length === 0); } return false; } // There's no point updating decorations if going from one standard TextSelection to another if (oldSelection instanceof _state.TextSelection && newSelection instanceof _state.TextSelection && oldSelection.from === oldSelection.to && newSelection.from === newSelection.to) { return false; } return true; } var isSelectableContainerNode = exports.isSelectableContainerNode = function isSelectableContainerNode(node) { return !!(node && !node.isAtom && _state.NodeSelection.isSelectable(node)); }; var isSelectableChildNode = exports.isSelectableChildNode = function isSelectableChildNode(node) { return !!(node && (node.isText || (0, _utils.isEmptyParagraph)(node) || _state.NodeSelection.isSelectable(node))); }; /** * Finds closest parent node that is a selectable block container node * If it finds a parent that is not selectable but supports gap cursor, will * return undefined */ var findSelectableContainerParent = exports.findSelectableContainerParent = function findSelectableContainerParent(selection) { var foundNodeThatSupportsGapCursor = false; var selectableNode = (0, _utils2.findParentNode)(function (node) { var isSelectable = isSelectableContainerNode(node); if (!isSelectable && !(0, _selection.isIgnored)(node)) { foundNodeThatSupportsGapCursor = true; } return isSelectable; })(selection); if (!foundNodeThatSupportsGapCursor) { return selectableNode; } }; /** * Finds node before that is a selectable block container node, starting * from $pos.depth + 1 and working in * If it finds a node that is not selectable but supports gap cursor, will * return undefined */ var findSelectableContainerBefore = exports.findSelectableContainerBefore = function findSelectableContainerBefore($pos, doc) { // prosemirror just returns the same pos from Selection.findFrom when // parent.inlineContent is true, so we move position back one here // to counteract that if ($pos.parent.inlineContent && isSelectableContainerNode($pos.parent)) { $pos = doc.resolve($pos.start() - 1); } var selectionBefore = _state.Selection.findFrom($pos, -1); if (selectionBefore) { var $selectionBefore = doc.resolve(selectionBefore.from); for (var i = $pos.depth + 1; i <= $selectionBefore.depth; i++) { var node = $selectionBefore.node(i); if (isSelectableContainerNode(node)) { return { node: node, pos: $selectionBefore.start(i) - 1 }; } if (i > $pos.depth + 1 && !(0, _selection.isIgnored)(node)) { return; } } /** * Stick to the default left selection behaviour, * useful for mediaSingleWithCaption */ if (selectionBefore instanceof _state.NodeSelection && _state.NodeSelection.isSelectable(selectionBefore.node)) { return { node: selectionBefore.node, pos: selectionBefore.from }; } } }; /** * Finds node after that is a selectable block container node, starting * from $pos.depth + 1 and working in * If it finds a node that is not selectable but supports gap cursor, will * return undefined */ var findSelectableContainerAfter = exports.findSelectableContainerAfter = function findSelectableContainerAfter($pos, doc) { var selectionAfter = _state.Selection.findFrom($pos, 1); if (selectionAfter) { var $selectionAfter = doc.resolve(selectionAfter.from); for (var i = $pos.depth + 1; i <= $selectionAfter.depth; i++) { var node = $selectionAfter.node(i); if (isSelectableContainerNode(node)) { return { node: node, pos: $selectionAfter.start(i) - 1 }; } if (i > $pos.depth + 1 && !(0, _selection.isIgnored)(node)) { return; } } } }; /** * Finds first child node that is a selectable block container node OR that * supports gap cursor */ var findFirstChildNodeToSelect = exports.findFirstChildNodeToSelect = function findFirstChildNodeToSelect(parent) { return (0, _utils2.flatten)(parent).find(function (child) { return isSelectableChildNode(child.node) || !(0, _selection.isIgnored)(child.node); }); }; /** * Finds last child node that is a selectable block container node OR that * supports gap cursor */ var findLastChildNodeToSelect = exports.findLastChildNodeToSelect = function findLastChildNodeToSelect(parent) { var child; parent.descendants(function (node, pos) { if (isSelectableChildNode(node) || !(0, _selection.isIgnored)(node)) { child = { node: node, pos: pos }; return false; } }); if (child) { return child; } }; var isSelectionAtStartOfParentNode = exports.isSelectionAtStartOfParentNode = function isSelectionAtStartOfParentNode($pos, selection) { var _findSelectableContai; return (0, _selection.isSelectionAtStartOfNode)($pos, (_findSelectableContai = findSelectableContainerParent(selection)) === null || _findSelectableContai === void 0 ? void 0 : _findSelectableContai.node); }; var isSelectionAtEndOfParentNode = exports.isSelectionAtEndOfParentNode = function isSelectionAtEndOfParentNode($pos, selection) { // If the current position is at the end of its parent node's content. var isAtTheEndOfCurrentLevel = $pos.parent.content.size === $pos.parentOffset; if (!isAtTheEndOfCurrentLevel) { return false; } // If at the root or parent is selectable, we're at the end if ($pos.depth === 0 || _state.NodeSelection.isSelectable($pos.parent)) { return isAtTheEndOfCurrentLevel; } // Handle lists: if in a list inside container and not at the end, return false if ((0, _utils2.hasParentNode)(_utils.isListItemNode)(selection) && isListItemWithinContainerNotAtEnd($pos, selection)) { return false; } // Handle layout columns: if another node follows, not at end if (isSelectionAtEndOfLayoutColumn($pos)) { return false; } // Default: if at end of parent's parent var $after = $pos.doc.resolve($pos.after()); return $after.parent.content.size === $after.parentOffset; }; /** * Determines if the current selection is inside a list item within a container and not at the end of the parent list. * * This is useful for handling edge cases where the selection is within a list inside a container structure, * and we need to know if the selection is not at the logical end of the parent list node. */ var isListItemWithinContainerNotAtEnd = exports.isListItemWithinContainerNotAtEnd = function isListItemWithinContainerNotAtEnd($pos, selection) { var isInContainerNode = (0, _utils2.hasParentNode)(isContainerNode)(selection); if (!isInContainerNode) { return false; } var parentList = (0, _utils2.findParentNodeClosestToPos)($pos, _utils.isListItemNode); if (!parentList) { return false; } var $parentListPos = $pos.doc.resolve(parentList.pos); var topLevelList = findTopLevelList($pos); if (!topLevelList) { return false; } var $afterTopLevelList = $pos.doc.resolve(topLevelList.pos + topLevelList.node.nodeSize); var nodeAfterTopLevelList = $afterTopLevelList.nodeAfter; var grandParentList = (0, _utils2.findParentNodeClosestToPos)($pos.doc.resolve($parentListPos.before()), _utils.isListItemNode); var grandParentListPos = grandParentList ? $pos.doc.resolve(grandParentList.pos) : undefined; var isLastListItemInParent = // Check if the current list item is the last child in its parent list $parentListPos.index() === $parentListPos.parent.childCount - 1 && ( // Check if there is no grandparent list, or if the grandparent list item is also the last child in its parent !grandParentList || grandParentListPos && grandParentListPos.index() === grandParentListPos.parent.childCount - 1); if (!isLastListItemInParent || nodeAfterTopLevelList) { return true; } return false; }; /** * Determines if the given node is a Container (layoutColumn, panel, expand) node. */ var isContainerNode = exports.isContainerNode = function isContainerNode(node) { var _node$type; var _ref3 = (node === null || node === void 0 || (_node$type = node.type) === null || _node$type === void 0 || (_node$type = _node$type.schema) === null || _node$type === void 0 ? void 0 : _node$type.nodes) || {}, layoutColumn = _ref3.layoutColumn, panel = _ref3.panel, expand = _ref3.expand; return Boolean(node && node.type && [panel, expand, layoutColumn].includes(node.type)); }; /** * Finds the top-level List ancestor of the given position. * Returns the node and its position if found, otherwise returns undefined. */ var findTopLevelList = exports.findTopLevelList = function findTopLevelList(pos) { var _pos$doc$type$schema$ = pos.doc.type.schema.nodes, bulletList = _pos$doc$type$schema$.bulletList, orderedList = _pos$doc$type$schema$.orderedList; var currentDepth = pos.depth; var topLevelList; while (currentDepth > 0) { var node = pos.node(currentDepth); if ([bulletList, orderedList].includes(node.type)) { topLevelList = { node: node, pos: pos.before(currentDepth) }; } currentDepth--; } return topLevelList; }; /** * Determines whether the current selection position is at the end of a layout column node. */ var isSelectionAtEndOfLayoutColumn = exports.isSelectionAtEndOfLayoutColumn = function isSelectionAtEndOfLayoutColumn($pos) { var layoutColumnParent = (0, _utils2.findParentNodeClosestToPos)($pos, isLayoutColumnNode); if (!layoutColumnParent) { return false; } var panelOrExpandParent = (0, _utils2.findParentNodeClosestToPos)($pos, isPanelOrExpandNode); if (panelOrExpandParent && panelOrExpandParent.pos > layoutColumnParent.pos) { return false; } var afterPos = layoutColumnParent.pos + layoutColumnParent.node.nodeSize; var $after = $pos.doc.resolve(afterPos); return Boolean($after.nodeAfter); }; /** * Determines if the given node is a LayoutColumn node. */ var isLayoutColumnNode = exports.isLayoutColumnNode = function isLayoutColumnNode(node) { var _node$type2; var _ref4 = (node === null || node === void 0 || (_node$type2 = node.type) === null || _node$type2 === void 0 || (_node$type2 = _node$type2.schema) === null || _node$type2 === void 0 ? void 0 : _node$type2.nodes) || {}, layoutColumn = _ref4.layoutColumn; return Boolean(node && node.type && node.type === layoutColumn); }; var isPanelOrExpandNode = exports.isPanelOrExpandNode = function isPanelOrExpandNode(node) { var _node$type3; var _ref5 = (node === null || node === void 0 || (_node$type3 = node.type) === null || _node$type3 === void 0 || (_node$type3 = _node$type3.schema) === null || _node$type3 === void 0 ? void 0 : _node$type3.nodes) || {}, panel = _ref5.panel, expand = _ref5.expand; return Boolean(node && node.type && (node.type === panel || node.type === expand)); };