UNPKG

@atlaskit/editor-plugin-selection

Version:

Selection plugin for @atlaskit/editor-core

260 lines (249 loc) 11.6 kB
"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)); };