@atlaskit/editor-plugin-selection
Version:
Selection plugin for @atlaskit/editor-core
260 lines (249 loc) • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.shouldSkipGapCursor = exports.setSelectionTopLevelBlocks = exports.setCursorForTopLevelBlocks = exports.hasGapCursorPlugin = exports.deleteNode = exports.arrow = void 0;
var _selection = require("@atlaskit/editor-common/selection");
var _utils = require("@atlaskit/editor-common/utils");
var _whitespace = require("@atlaskit/editor-common/whitespace");
var _state = require("@atlaskit/editor-prosemirror/state");
var _utils2 = require("@atlaskit/editor-prosemirror/utils");
var _gapCursorPluginKey = require("../gap-cursor-plugin-key");
var _direction = require("./direction");
var _utils3 = require("./utils");
var shouldSkipGapCursor = exports.shouldSkipGapCursor = function shouldSkipGapCursor(direction, state, $pos) {
var _$pos$nodeBefore;
var doc = state.doc,
schema = state.schema;
switch (direction) {
case _direction.Direction.UP:
if ((0, _selection.atTheBeginningOfDoc)(state)) {
return false;
}
return (0, _utils.isPositionNearTableRow)($pos, schema, 'before') || (0, _utils3.isTextBlockNearPos)(doc, schema, $pos, -1) || (0, _utils.isNodeBeforeMediaNode)($pos, state);
case _direction.Direction.DOWN:
return (
// end of a paragraph
(0, _selection.atTheEndOfDoc)(state) || (0, _utils3.isTextBlockNearPos)(doc, schema, $pos, 1) || (0, _utils.isPositionNearTableRow)($pos, schema, 'after') || ((_$pos$nodeBefore = $pos.nodeBefore) === null || _$pos$nodeBefore === void 0 ? void 0 : _$pos$nodeBefore.type.name) === 'text' && !$pos.nodeAfter
);
default:
return false;
}
};
// These cases should be handled using the handleMediaGapCursor function
function shouldHandleMediaGapCursor(dir, state) {
var _selection$$from$node;
var selection = state.selection;
var upArrowFromGapCursorIntoMedia = selection instanceof _selection.GapCursorSelection && dir === _direction.Direction.UP && selection.$from.nodeBefore && (0, _utils.isMediaNode)(selection.$from.nodeBefore);
var downArrowFromGapCursorIntoMediaGroup = selection instanceof _selection.GapCursorSelection && dir === _direction.Direction.DOWN && ((_selection$$from$node = selection.$from.nodeAfter) === null || _selection$$from$node === void 0 ? void 0 : _selection$$from$node.type.name) === 'mediaGroup';
return upArrowFromGapCursorIntoMedia || downArrowFromGapCursorIntoMediaGroup;
}
// Handle media gap cursor for up/down arrow into media nodes
// Should check this case by using shouldHandleMediaGapCursor first
function handleMediaGapCursor(dir, state) {
var selection = state.selection,
tr = state.tr;
var $pos = (0, _direction.isBackward)(dir) ? selection.$from : selection.$to;
if (dir === _direction.Direction.UP && selection.$from.nodeBefore && (0, _utils.isMediaNode)(selection.$from.nodeBefore)) {
var _tr$doc$nodeAt;
var nodeBeforePos = (0, _utils2.findPositionOfNodeBefore)(tr.selection);
if (nodeBeforePos && selection.side === 'right' && ((_tr$doc$nodeAt = tr.doc.nodeAt(nodeBeforePos)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type.name) === 'mediaSingle') {
tr.setSelection(new _state.NodeSelection(tr.doc.resolve(nodeBeforePos))).scrollIntoView();
} else if (nodeBeforePos || nodeBeforePos === 0) {
tr.setSelection(new _selection.GapCursorSelection(tr.doc.resolve(nodeBeforePos), _selection.Side.LEFT)).scrollIntoView();
}
}
if (dir === _direction.Direction.DOWN && selection.$from.nodeAfter) {
var nodeAfterPos = selection.side === 'right' ? $pos.pos : $pos.pos + selection.$from.nodeAfter.nodeSize;
if (nodeAfterPos) {
tr.setSelection(new _selection.GapCursorSelection(tr.doc.resolve(nodeAfterPos), _selection.Side.LEFT)).scrollIntoView();
}
}
return tr;
}
var arrow = exports.arrow = function arrow(dir, endOfTextblock) {
return function (state, dispatch, view) {
var doc = state.doc,
selection = state.selection,
tr = state.tr;
var $pos = (0, _direction.isBackward)(dir) ? selection.$from : selection.$to;
var mustMove = selection.empty;
// start from text selection
if (selection instanceof _state.TextSelection) {
// if cursor is in the middle of a text node, do nothing
if (!endOfTextblock || !endOfTextblock(dir.toString())) {
return false;
}
// UP/DOWN jumps to the nearest texblock skipping gapcursor whenever possible
if (shouldSkipGapCursor(dir, state, $pos)) {
return false;
}
// otherwise resolve previous/next position
$pos = doc.resolve((0, _direction.isBackward)(dir) ? $pos.before() : $pos.after());
mustMove = false;
}
if (selection instanceof _state.NodeSelection) {
if (selection.node.isInline) {
return false;
}
if (dir === _direction.Direction.UP && !(0, _selection.atTheBeginningOfDoc)(state) && !(0, _utils.isNodeBeforeMediaNode)($pos, state) || dir === _direction.Direction.DOWN) {
// We dont add gap cursor on node selections going up and down
// Except we do if we're going up for a block node which is the
// first node in the document OR the node before is a media node
return false;
}
}
// Handle media gap cursor for up/down arrow into media nodes
if (shouldHandleMediaGapCursor(dir, state)) {
var updatedTr = handleMediaGapCursor(dir, state);
if (dispatch) {
dispatch(updatedTr);
}
return true;
}
// when jumping between block nodes at the same depth, we need to reverse cursor without changing ProseMirror position
if (selection instanceof _selection.GapCursorSelection &&
// next node allow gap cursor position
(0, _selection.isValidTargetNode)((0, _direction.isBackward)(dir) ? $pos.nodeBefore : $pos.nodeAfter) && (
// gap cursor changes block node
(0, _direction.isBackward)(dir) && selection.side === _selection.Side.LEFT || (0, _direction.isForward)(dir) && selection.side === _selection.Side.RIGHT)) {
// reverse cursor position
if (dispatch) {
dispatch(tr.setSelection(new _selection.GapCursorSelection($pos, selection.side === _selection.Side.RIGHT ? _selection.Side.LEFT : _selection.Side.RIGHT)).scrollIntoView());
}
return true;
}
if (view) {
var domAtPos = view.domAtPos.bind(view);
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
var target = (0, _utils2.findDomRefAtPos)($pos.pos, domAtPos);
if (target && target.textContent === _whitespace.ZERO_WIDTH_SPACE) {
return false;
}
}
var nextSelection = _selection.GapCursorSelection.findFrom($pos, (0, _direction.isBackward)(dir) ? -1 : 1, mustMove);
if (!nextSelection) {
return false;
}
if (!(0, _selection.isValidTargetNode)((0, _direction.isForward)(dir) ? nextSelection.$from.nodeBefore : nextSelection.$from.nodeAfter)) {
// reverse cursor position
if (dispatch) {
dispatch(tr.setSelection(new _selection.GapCursorSelection(nextSelection.$from, (0, _direction.isForward)(dir) ? _selection.Side.LEFT : _selection.Side.RIGHT)).scrollIntoView());
}
return true;
}
if (dispatch) {
dispatch(tr.setSelection(nextSelection).scrollIntoView());
}
return true;
};
};
var deleteNode = exports.deleteNode = function deleteNode(dir) {
return function (state, dispatch) {
if (state.selection instanceof _selection.GapCursorSelection) {
var _state$selection = state.selection,
$from = _state$selection.$from,
$anchor = _state$selection.$anchor;
var tr = state.tr;
if ((0, _direction.isBackward)(dir)) {
if (state.selection.side === 'left') {
tr.setSelection(new _selection.GapCursorSelection($anchor, _selection.Side.RIGHT));
if (dispatch) {
dispatch(tr);
}
return true;
}
tr = (0, _utils2.removeNodeBefore)(state.tr);
} else if ($from.nodeAfter) {
tr = tr.delete($from.pos, $from.pos + $from.nodeAfter.nodeSize);
}
if (dispatch) {
dispatch(tr.setSelection(_state.Selection.near(tr.doc.resolve(tr.mapping.map(state.selection.$from.pos)))).scrollIntoView());
}
return true;
}
return false;
};
};
// This function captures clicks outside of the ProseMirror contentEditable area
// see also description of "handleClick" in gap-cursor pm-plugin
var captureCursorCoords = function captureCursorCoords(event, editorRef, posAtCoords, tr) {
var rect = editorRef.getBoundingClientRect();
// capture clicks before the first block element
if (event.clientY < rect.top) {
return {
position: 0,
side: _selection.Side.LEFT
};
}
if (rect.left > 0) {
// calculate start position of a node that is vertically at the same level
var coords = posAtCoords({
left: rect.left,
top: event.clientY
});
if (coords && coords.inside > -1) {
var $from = tr.doc.resolve(coords.inside);
var start = $from.before(1);
var side = event.clientX < rect.left ? _selection.Side.LEFT : _selection.Side.RIGHT;
var position;
if (side === _selection.Side.LEFT) {
position = start;
} else {
var node = tr.doc.nodeAt(start);
if (node) {
position = start + node.nodeSize;
}
}
return {
position: position,
side: side
};
}
}
return null;
};
var setSelectionTopLevelBlocks = exports.setSelectionTopLevelBlocks = function setSelectionTopLevelBlocks(tr, event, editorRef, posAtCoords, editorFocused) {
var cursorCoords = captureCursorCoords(event, editorRef, posAtCoords, tr);
if (!cursorCoords) {
return;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var $pos = cursorCoords.position !== undefined ? tr.doc.resolve(cursorCoords.position) : null;
if ($pos === null) {
return;
}
var isGapCursorAllowed = cursorCoords.side === _selection.Side.LEFT ? (0, _selection.isValidTargetNode)($pos.nodeAfter) : (0, _selection.isValidTargetNode)($pos.nodeBefore);
if (isGapCursorAllowed && _selection.GapCursorSelection.valid($pos)) {
// this forces PM to re-render the decoration node if we change the side of the gap cursor, it doesn't do it by default
if (tr.selection instanceof _selection.GapCursorSelection) {
tr.setSelection(_state.Selection.near($pos));
} else {
tr.setSelection(new _selection.GapCursorSelection($pos, cursorCoords.side));
}
}
// try to set text selection if the editor isnt focused
// if the editor is focused, we are most likely dragging a selection outside.
else if (editorFocused === false) {
var selectionTemp = _state.Selection.findFrom($pos, cursorCoords.side === _selection.Side.LEFT ? 1 : -1, true);
if (selectionTemp) {
tr.setSelection(selectionTemp);
}
}
};
var setCursorForTopLevelBlocks = exports.setCursorForTopLevelBlocks = function setCursorForTopLevelBlocks(event, editorRef, posAtCoords, editorFocused) {
return function (state, dispatch) {
var tr = state.tr;
setSelectionTopLevelBlocks(tr, event, editorRef, posAtCoords, editorFocused);
if (tr.selectionSet && dispatch) {
dispatch(tr);
return true;
}
return false;
};
};
var hasGapCursorPlugin = exports.hasGapCursorPlugin = function hasGapCursorPlugin(state) {
return Boolean(_gapCursorPluginKey.gapCursorPluginKey.get(state));
};