@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
260 lines (253 loc) • 9.56 kB
JavaScript
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);
}