UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

196 lines (185 loc) 8.77 kB
import { expandToBlockRange, isMultiBlockRange } from '@atlaskit/editor-common/selection'; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { getTableSelectionClosesToPos } from '@atlaskit/editor-tables/utils'; import { getBlockControlsMeta, key } from '../main'; import { newGetSelection } from './getSelection'; export var getMultiSelectionIfPosInside = function getMultiSelectionIfPosInside(api, pos, tr) { var _api$blockControls, _pluginState$multiSel, _tr$getMeta; var pluginState = api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.sharedState.currentState(); // With move nodes shortcut, we expand selection and move node within one transaction, // Hence we also look for `multiSelectDnD` in transaction meta var multiSelectDnD = (_pluginState$multiSel = pluginState === null || pluginState === void 0 ? void 0 : pluginState.multiSelectDnD) !== null && _pluginState$multiSel !== void 0 ? _pluginState$multiSel : tr === null || tr === void 0 || (_tr$getMeta = tr.getMeta(key)) === null || _tr$getMeta === void 0 ? void 0 : _tr$getMeta.multiSelectDnD; if (multiSelectDnD && multiSelectDnD.anchor >= 0 && multiSelectDnD.head >= 0) { var multiFrom = Math.min(multiSelectDnD.anchor, multiSelectDnD.head); var multiTo = Math.max(multiSelectDnD.anchor, multiSelectDnD.head); // We subtract one as the handle position is before the node return pos >= multiFrom - 1 && pos < multiTo ? { anchor: multiSelectDnD.anchor, head: multiSelectDnD.head } : {}; } return {}; }; /** * Given a handle position, returns the from and to positions of the selected content. * If the handle position is not in a multi-selection, it returns the node's from and to positions. * * @param handlePos The position of the handle * @param tr The transaction to use for position calculations * @param api The BlockControlsPlugin API for accessing shared state * @returns from and to positions of the selected content (after expansion) */ export var getSelectedSlicePosition = function getSelectedSlicePosition(handlePos, tr, api) { var _activeNode$nodeSize; var _getMultiSelectionIfP = getMultiSelectionIfPosInside(api, handlePos, tr), anchor = _getMultiSelectionIfP.anchor, head = _getMultiSelectionIfP.head; var inSelection = anchor !== undefined && head !== undefined; var from = inSelection ? Math.min(anchor || 0, head || 0) : handlePos; var activeNode = tr.doc.nodeAt(handlePos); var activeNodeEndPos = handlePos + ((_activeNode$nodeSize = activeNode === null || activeNode === void 0 ? void 0 : activeNode.nodeSize) !== null && _activeNode$nodeSize !== void 0 ? _activeNode$nodeSize : 1); var to = inSelection ? Math.max(anchor || 0, head || 0) : activeNodeEndPos; return { from: from, to: to }; }; /** * Takes a position and expands the selection to encompass the node at that position. Ignores empty or out of range selections. * Ignores positions that are in text blocks (i.e. not start of a node) * @returns TextSelection if expanded, otherwise returns Selection that was passed in. */ export var expandSelectionHeadToNodeAtPos = function expandSelectionHeadToNodeAtPos(selection, nodePos) { var doc = selection.$anchor.doc; if (nodePos < 0 || nodePos > doc.nodeSize - 2 || selection.empty) { return selection; } var $pos = doc.resolve(nodePos); var node = $pos.nodeAfter; if ($pos.node().isTextblock || !node) { return selection; } var $newHead = nodePos < selection.anchor ? $pos : doc.resolve(node.nodeSize + nodePos); var textSelection = new TextSelection(selection.$anchor, $newHead); return textSelection; }; /** * This swaps the anchor/head for NodeSelections when its anchor > pos. * This is because NodeSelection always has an anchor at the start of the node, * which may not align with the existing selection. */ export var alignAnchorHeadInDirectionOfPos = function alignAnchorHeadInDirectionOfPos(selection, pos) { return selection instanceof NodeSelection && Math.max(pos, selection.anchor) === selection.anchor ? new TextSelection(selection.$head, selection.$anchor) : selection; }; /** * This maps a preserved selection through a transaction, expanding text selections to block boundaries. * * @param selection The existing preserved selection to map * @param tr The transaction to map through * @returns The mapped selection or undefined if mapping is not possible */ export var mapPreservedSelection = function mapPreservedSelection(selection, tr) { var _ref = getBlockControlsMeta(tr) || {}, preservedSelectionMapping = _ref.preservedSelectionMapping; var mapping = preservedSelectionMapping || tr.mapping; var from = mapping.map(selection.from); var to = mapping.map(selection.to); var isSelectionEmpty = from === to; var wasSelectionEmpty = selection.from === selection.to; if (isSelectionEmpty && !wasSelectionEmpty) { // If selection has become empty i.e. content has been deleted, stop preserving return undefined; } return createPreservedSelection(tr.doc.resolve(from), tr.doc.resolve(to)); }; var isInsideEmptyTextblock = function isInsideEmptyTextblock($pos) { return $pos.parent.isTextblock && $pos.parent.content.size === 0; }; var isAtEndOfParent = function isAtEndOfParent($pos) { return $pos.parentOffset === $pos.parent.content.size; }; var isAtStartOfParent = function isAtStartOfParent($pos) { return $pos.parentOffset === 0; }; /** * Adjust selection bounds to exclude nodes where the selection only touches * the edge position without selecting any content. * * Exception: Don't adjust if the selection is inside an empty textblock, * as we want to include empty paragraphs in block operations. * * @param $from The resolved position of the start of the selection * @param $to The resolved position of the end of the selection * @returns Adjusted $from and $to positions */ export var adjustSelectionBoundsForEdgePositions = function adjustSelectionBoundsForEdgePositions($from, $to) { var doc = $from.doc; // Walk $from forward while at end of ancestors var adjustedFrom = $from; if (!isInsideEmptyTextblock($from)) { while (adjustedFrom.depth > 0 && isAtEndOfParent(adjustedFrom)) { var nextPos = adjustedFrom.after(); if (nextPos > doc.content.size || nextPos === adjustedFrom.pos) { break; } adjustedFrom = doc.resolve(nextPos); } } // Walk $to backward while at start of ancestors var adjustedTo = $to; if (!isInsideEmptyTextblock($to)) { while (adjustedTo.depth > 0 && isAtStartOfParent(adjustedTo)) { var prevPos = adjustedTo.before(); if (prevPos < 0 || prevPos === adjustedTo.pos) { break; } adjustedTo = doc.resolve(prevPos); } } return { $from: adjustedFrom, $to: adjustedTo }; }; /** * Creates a preserved selection which is expanded to block boundaries. * * Will return the correct type of selection based on the nodes contained within the * expanded selection range. * * If the selection becomes empty or invalid, it returns undefined. * * @param $from The resolved position of the start of the selection * @param $to The resolved position of the end of the selection * @returns A Selection or undefined if selection is invalid */ export var createPreservedSelection = function createPreservedSelection($from, $to) { var _doc$nodeAt; var doc = $from.doc; var isCollapsed = $from.pos === $to.pos; var adjusted = isCollapsed ? { $from: $from, $to: $to } : adjustSelectionBoundsForEdgePositions($from, $to); if (!isCollapsed && adjusted.$from.pos >= adjusted.$to.pos) { return undefined; } // expand the selection range to block boundaries, so selection always includes whole nodes var expanded = expandToBlockRange(adjusted.$from, adjusted.$to); // stop preserving if selection becomes invalid if (expanded.$from.pos < 0 || expanded.$to.pos > doc.content.size || expanded.$from.pos >= expanded.$to.pos) { return undefined; } // If multiple blocks selected, create TextSelection from start of first node to end of last node if (expanded.range && isMultiBlockRange(expanded.range)) { return new TextSelection(expanded.$from, expanded.$to); } var nodeType = (_doc$nodeAt = doc.nodeAt(expanded.$from.pos)) === null || _doc$nodeAt === void 0 || (_doc$nodeAt = _doc$nodeAt.type) === null || _doc$nodeAt === void 0 ? void 0 : _doc$nodeAt.name; if (!nodeType) { return undefined; } if (nodeType === 'table') { return getTableSelectionClosesToPos($from); } return newGetSelection(doc, false, expanded.$from.pos) || undefined; };