@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
196 lines (185 loc) • 8.77 kB
JavaScript
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;
};