UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

260 lines (253 loc) • 9.56 kB
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findTable, isTableSelected } from '@atlaskit/editor-tables/utils'; import { isListNode } from '../utils'; import { GapCursorSelection } from './gap-cursor/selection'; export var isSelectionAtStartOfNode = function isSelectionAtStartOfNode($pos, parentNode) { if (!parentNode) { return false; } for (var i = $pos.depth + 1; i > 0; i--) { var node = $pos.node(i); if (node && node.eq(parentNode)) { break; } if (i > 1 && $pos.before(i) !== $pos.before(i - 1) + 1) { return false; } } return true; }; export var isSelectionAtEndOfNode = function isSelectionAtEndOfNode($pos, parentNode) { if (!parentNode) { return false; } for (var i = $pos.depth + 1; i > 0; i--) { var node = $pos.node(i); if (node && node.eq(parentNode)) { break; } if (i > 1 && $pos.after(i) !== $pos.after(i - 1) - 1) { return false; } } return true; }; export function atTheEndOfDoc(state) { var selection = state.selection, doc = state.doc; return doc.nodeSize - selection.$to.pos - 2 === selection.$to.depth; } export function atTheBeginningOfDoc(state) { var selection = state.selection; return selection.$from.pos === selection.$from.depth; } export function atTheEndOfBlock(state) { var selection = state.selection; var $to = selection.$to; if (selection instanceof GapCursorSelection) { return false; } if (selection instanceof NodeSelection && selection.node.isBlock) { return true; } return endPositionOfParent($to) === $to.pos + 1; } export function atTheBeginningOfBlock(state) { var selection = state.selection; return selectionIsAtTheBeginningOfBlock(selection); } export function selectionIsAtTheBeginningOfBlock(selection) { var $from = selection.$from; if (selection instanceof GapCursorSelection) { return false; } if (selection instanceof NodeSelection && selection.node.isBlock) { return true; } return startPositionOfParent($from) === $from.pos; } export function startPositionOfParent(resolvedPos) { return resolvedPos.start(resolvedPos.depth); } export function endPositionOfParent(resolvedPos) { return resolvedPos.end(resolvedPos.depth) + 1; } /** * * @param $anchor Resolved selection anchor position * @param $head Resolved selection head position * @returns An expanded selection encompassing surrounding nodes. Hoists up to the shared depth anchor/head depths differ. */ export var expandSelectionBounds = function expandSelectionBounds($anchor, $head) { var sharedDepth = $anchor.sharedDepth($head.pos) + 1; var $from = $anchor.min($head); var $to = $anchor.max($head); var fromDepth = $from.depth; var toDepth = $to.depth; var selectionStart; var selectionEnd; var selectionHasGrandparent = toDepth > 1 && fromDepth > 1; var selectionIsAcrossDiffParents = selectionHasGrandparent && !$to.parent.isTextblock && !$to.sameParent($from); var selectionIsAcrossTextBlocksWithDiffParents = selectionHasGrandparent && $to.parent.isTextblock && $to.before(toDepth - 1) !== $from.before(fromDepth - 1); if (toDepth > fromDepth) { selectionStart = $from.before(sharedDepth); selectionEnd = $to.after(sharedDepth); } else if (toDepth < fromDepth) { selectionStart = $from.before(sharedDepth); selectionEnd = $to.after(sharedDepth); } else if (selectionIsAcrossDiffParents || selectionIsAcrossTextBlocksWithDiffParents) { // when selection from/to share same depth with different parents, hoist up the selection to the parent of the highest depth in the selection selectionStart = $from.before(sharedDepth); selectionEnd = $to.after(sharedDepth); } else if (!$from.node().inlineContent) { // when selection might be a Node selection, return what was passed in return { $anchor: $anchor, $head: $head }; } else { selectionStart = fromDepth ? $from.before() : $from.pos; selectionEnd = toDepth ? $to.after() : $to.pos; } var $expandedFrom = $anchor.doc.resolve(selectionStart); var $expandedTo = $anchor.doc.resolve(selectionEnd); return { $anchor: $anchor === $from ? $expandedFrom : $expandedTo, $head: $head === $to ? $expandedTo : $expandedFrom }; }; /** * Delete what is selected in the given transaction. * @param tr the transaction to delete the selection from * @param selectionToUse optional selection to delete instead of the transaction's current selection * @returns the updated transaction */ export var deleteSelectedRange = function deleteSelectedRange(tr, selectionToUse) { var selection = selectionToUse || tr.selection; var from = selection.$from.pos; var to = selection.$to.pos; if (selection instanceof TextSelection) { // Expand just the from position to the start of the block // This ensures entire paragraphs are deleted instead of just // the text content, which avoids leaving an empty line behind var expanded = expandToBlockRange(selection.$from, selection.$to); from = expanded.$from.pos; } else if (isTableSelected(selection)) { var table = findTable(selection); if (table) { from = table.pos; to = table.pos + table.node.nodeSize; } } tr.deleteRange(from, to); return tr; }; var getDefaultPredicate = function getDefaultPredicate(_ref) { var nodes = _ref.nodes; var requiresFurtherExpansion = new Set([nodes.bulletList, nodes.orderedList, nodes.taskList, nodes.listItem, nodes.taskItem, nodes.tableHeader, nodes.tableRow, nodes.tableCell, nodes.table, nodes.mediaGroup, nodes.mediaSingle]); return function (node) { return !requiresFurtherExpansion.has(node.type); }; }; /** * This expands the given $from and $to resolved positions to the block boundaries * spanning all nodes in the range up to the nearest common ancestor. * * By default, it will further expand the range when encountering specific node types * that require full block selection (like lists and tables). A custom predicate * can be provided to modify this behavior. * * @param $from The resolved start position * @param $to The resolved end position * @param predicate A predicate to determine if parent node is acceptable (see prosemirror-model/blockRange) * @returns An object containing the expanded $from and $to resolved positions */ export var expandToBlockRange = function expandToBlockRange($from, $to) { var predicate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getDefaultPredicate($from.doc.type.schema); var range = $from.blockRange($to, predicate); if (!range) { return { $from: $from, $to: $to }; } return { $from: $from.doc.resolve(range.start), $to: $to.doc.resolve(range.end), range: range }; }; /** * Expands a given selection to a block range, considering specific node types that require expansion. * * E.g. if the selection starts/ends at list items or table cells, the selection will be expanded * to encompass the entire list or table. * * Used mostly for block menu / drag handle related selections, where we want to ensure the selection * being acted upon covers the entire block range selected by the user. * * @param selection The selection to expand * @returns The expanded selection */ export var expandSelectionToBlockRange = function expandSelectionToBlockRange(_ref2) { var $from = _ref2.$from, $to = _ref2.$to; return expandToBlockRange($from, $to); }; export var isMultiBlockRange = function isMultiBlockRange(range) { if (range.endIndex - range.startIndex <= 1) { return false; // At most one child } // Count block nodes in the range, return true if more than one var blockCount = 0; for (var i = range.startIndex; i < range.endIndex; i++) { if (range.parent.child(i).isBlock) { blockCount++; } if (blockCount > 1) { return true; } } return false; }; /** * Determines if a selection contains multiple block nodes. */ export function isMultiBlockSelection(selection) { var _expandSelectionToBlo = expandSelectionToBlockRange(selection), range = _expandSelectionToBlo.range; if (!range) { return false; } return isMultiBlockRange(range); } /** * Extracts the source nodes from a selection range. * * This function expands the given selection to its block range boundaries and returns * an array of the nodes contained within that range. It handles special cases like * list nodes, where the slice positions are adjusted to include the list wrapper. * * @param tr - The transaction containing the document * @param selection - The selection to extract nodes from * @returns An array of ProseMirror nodes within the expanded selection range * * @example * ```typescript * const selection = tr.selection; * const nodes = getSourceNodesFromSelectionRange(tr, selection); * // nodes will contain all block-level nodes in the selection * ``` */ export function getSourceNodesFromSelectionRange(tr, selection) { var _expandSelectionToBlo2 = expandSelectionToBlockRange(selection), $from = _expandSelectionToBlo2.$from, $to = _expandSelectionToBlo2.$to; var selectedParent = $from.parent; var isList = isListNode(selectedParent); var sliceStart = isList ? $from.pos - 1 : $from.pos; var sliceEnd = isList ? $to.pos + 1 : $to.pos; var slice = tr.doc.slice(sliceStart, sliceEnd); return _toConsumableArray(slice.content.content); }