UNPKG

@atlaskit/editor-plugin-selection

Version:

Selection plugin for @atlaskit/editor-core

191 lines (186 loc) 8.15 kB
import { expandedState, getNextNodeExpandPos } from '@atlaskit/editor-common/expand'; import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure'; /* * The way expand was built, no browser recognize selection on it. * For instance, when a selection going to a "collapsed" expand * the browser will try to send the cursor to inside the expand content (wrong), * this behavior is caused because the expand content is never true hidden * we just set the height to 1px. * * So, we need to capture a possible selection event * when a collapsed expand is the next node in the common depth. * If that is true, we create a new TextSelection and stop the event bubble */ const isCollapsedExpand = (node, { __livePage }) => { let currentExpandedState; if (__livePage && expValEquals('platform_editor_single_player_expand', 'isEnabled', true)) { currentExpandedState = node ? !expandedState.get(node) : undefined; } else if (__livePage) { currentExpandedState = node === null || node === void 0 ? void 0 : node.attrs.__expanded; } else { currentExpandedState = !(node !== null && node !== void 0 && node.attrs.__expanded); } return Boolean(node && ['expand', 'nestedExpand'].includes(node.type.name) && currentExpandedState); }; /** * ED-18072 - Cannot shift + arrow past bodied extension if it is not empty */ const isBodiedExtension = node => { return Boolean(node && ['bodiedExtension'].includes(node.type.name)); }; /** * ED-19861 - [Regression] keyboard selections within action items are unpredicatable * Table was added to the list of problematic nodes because the desired behaviour when Shift+Up from outside the * table is to select the table node itself, rather than the table cell content. Previously this behaviour was handled * in `packages/editor/editor-core/src/plugins/selection/pm-plugins/events/create-selection-between.ts` but there was * a bug in `create-selection-between` which after fixing the bug that code was no longer handling table selection * correctly, so to fix that table was added here. */ const isTable = node => { return Boolean(node && ['table'].includes(node.type.name)); }; const isProblematicNode = (node, { __livePage }) => { return isCollapsedExpand(node, { __livePage }) || isBodiedExtension(node) || isTable(node); }; const findFixedProblematicNodePosition = (doc, $head, direction, { __livePage }) => { if ($head.pos === 0 || $head.depth === 0) { return null; } if (direction === 'up') { const pos = $head.before(); const $posResolved = $head.doc.resolve(pos); const maybeProblematicNode = $posResolved.nodeBefore; if (maybeProblematicNode && isProblematicNode(maybeProblematicNode, { __livePage })) { const nodeSize = maybeProblematicNode.nodeSize; const nodeStartPosition = pos - nodeSize; // ($head.pos - 1) will correspond to (nodeStartPosition + nodeSize) when we are at the start of the text node const isAtEndOfProblematicNode = $head.pos - 1 === nodeStartPosition + nodeSize; if (isAtEndOfProblematicNode) { const startPosNode = Math.max(nodeStartPosition, 0); const $startPosNode = $head.doc.resolve(Math.min(startPosNode, $head.doc.content.size)); return $startPosNode; } } } if (direction === 'down') { const pos = $head.after(); const maybeProblematicNode = doc.nodeAt(pos); if (maybeProblematicNode && isProblematicNode(maybeProblematicNode, { __livePage }) && $head.pos + 1 === pos) { const nodeSize = maybeProblematicNode.nodeSize; const nodePosition = pos + nodeSize; const startPosNode = Math.max(nodePosition, 0); const $startPosNode = $head.doc.resolve(Math.min(startPosNode, $head.doc.content.size)); return $startPosNode; } } return null; }; const isSelectionLineShortcutWhenCursorIsInsideInlineNode = (view, event) => { var _selection$$cursor$no, _selection$$cursor$no2; if (!event.shiftKey || !event.metaKey) { return false; } const selection = view.state.selection; if (!(selection instanceof TextSelection)) { return false; } if (!selection.$cursor) { return false; } const isSelectingInlineNodeForward = event.key === 'ArrowRight' && Boolean((_selection$$cursor$no = selection.$cursor.nodeAfter) === null || _selection$$cursor$no === void 0 ? void 0 : _selection$$cursor$no.isInline); const isSelectingInlineNodeBackward = event.key === 'ArrowLeft' && Boolean((_selection$$cursor$no2 = selection.$cursor.nodeBefore) === null || _selection$$cursor$no2 === void 0 ? void 0 : _selection$$cursor$no2.isInline); return isSelectingInlineNodeForward || isSelectingInlineNodeBackward; }; const isNavigatingVerticallyWhenCursorIsInsideInlineNode = (view, event) => { var _view$state, _selection$$cursor$no3, _selection$$cursor$no4; if (event.shiftKey || event.metaKey) { return false; } const selection = (_view$state = view.state) === null || _view$state === void 0 ? void 0 : _view$state.selection; if (!(selection instanceof TextSelection)) { return false; } if (!selection.$cursor) { return false; } const isNavigatingInlineNodeDownward = event.key === 'ArrowDown' && Boolean((_selection$$cursor$no3 = selection.$cursor.nodeBefore) === null || _selection$$cursor$no3 === void 0 ? void 0 : _selection$$cursor$no3.isInline) && Boolean((_selection$$cursor$no4 = selection.$cursor.nodeAfter) === null || _selection$$cursor$no4 === void 0 ? void 0 : _selection$$cursor$no4.isInline); if (isNavigatingInlineNodeDownward && getNextNodeExpandPos(view, selection) !== undefined && expValEqualsNoExposure('platform_editor_lovability_navigation_fixes', 'isEnabled', true)) { return false; } return isNavigatingInlineNodeDownward; }; export function createOnKeydown({ __livePage = false }) { function onKeydown(view, event) { /* * This workaround is needed for some specific situations. * - expand collapse * - bodied extension */ if (!(event instanceof KeyboardEvent)) { return false; } // Override the default behaviour to make sure that the selection always extends to // the start of the document and not just the first inline position. if (event.shiftKey && event.metaKey && event.key === 'ArrowUp') { const selection = TextSelection.create(view.state.doc, view.state.selection.$anchor.pos, 0); view.dispatch(view.state.tr.setSelection(selection)); event.preventDefault(); return true; } if (isSelectionLineShortcutWhenCursorIsInsideInlineNode(view, event)) { return true; } if (isNavigatingVerticallyWhenCursorIsInsideInlineNode(view, event)) { return true; } if (!event.shiftKey || event.ctrlKey || event.metaKey) { return false; } if (!['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key)) { return false; } const { doc, selection: { $head, $anchor } } = view.state; if (event.key === 'ArrowRight' && $head.nodeAfter || event.key === 'ArrowLeft' && $head.nodeBefore) { return false; } const direction = ['ArrowLeft', 'ArrowUp', 'Home'].includes(event.key) ? 'up' : 'down'; const $fixedProblematicNodePosition = findFixedProblematicNodePosition(doc, $head, direction, { __livePage }); if ($fixedProblematicNodePosition) { // an offset is used here so that left arrow selects the first character before the node (consistent with arrow right) const headOffset = event.key === 'ArrowLeft' ? -1 : 0; const head = $fixedProblematicNodePosition.pos + headOffset; const forcedTextSelection = TextSelection.create(view.state.doc, $anchor.pos, head); const tr = view.state.tr; tr.setSelection(forcedTextSelection); view.dispatch(tr); event.preventDefault(); return true; } return false; } return onKeydown; }