UNPKG

@atlaskit/editor-plugin-selection

Version:

Selection plugin for @atlaskit/editor-core

171 lines (170 loc) 8.1 kB
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { GapCursorSelection, Side as GapCursorSide, hideCaretModifier, JSON_ID, setGapCursorAtPos, Side } from '@atlaskit/editor-common/selection'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { findPositionOfNodeBefore } from '@atlaskit/editor-prosemirror/utils'; import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { selectionPluginKey } from '../types'; import { gapCursorPluginKey } from './gap-cursor-plugin-key'; import { deleteNode } from './gap-cursor/actions'; import { Direction } from './gap-cursor/direction'; import { getLayoutModeFromTargetNode, isIgnoredClick } from './gap-cursor/utils'; import { toDOM } from './gap-cursor/utils/place-gap-cursor'; const plugin = new SafePlugin({ key: gapCursorPluginKey, state: { init: () => ({ selectionIsGapCursor: false, displayGapCursor: true, hideCursor: false }), apply: (tr, pluginState, _oldState, newState) => { var _meta$displayGapCurso, _selectionMeta$hideCu; const meta = tr.getMeta(gapCursorPluginKey); const selectionMeta = tr.getMeta(selectionPluginKey); const selectionIsGapCursor = newState.selection instanceof GapCursorSelection; return { selectionIsGapCursor, // only attempt to hide gap cursor if selection is gap cursor displayGapCursor: selectionIsGapCursor ? (_meta$displayGapCurso = meta === null || meta === void 0 ? void 0 : meta.displayGapCursor) !== null && _meta$displayGapCurso !== void 0 ? _meta$displayGapCurso : pluginState.displayGapCursor : true, // track hideCursor state from selection plugin hideCursor: (_selectionMeta$hideCu = selectionMeta === null || selectionMeta === void 0 ? void 0 : selectionMeta.hideCursor) !== null && _selectionMeta$hideCu !== void 0 ? _selectionMeta$hideCu : pluginState.hideCursor }; } }, view: view => { /** * If the selection is at the beginning of a document and is a NodeSelection, * convert to a GapCursor selection. This is to stop users accidentally replacing * the first node of a document by accident. */ if (view.state.selection.anchor === 0 && view.state.selection instanceof NodeSelection) { // This is required otherwise the dispatch doesn't trigger in the correct place window.requestAnimationFrame(() => { view.dispatch(view.state.tr.setSelection(new GapCursorSelection(view.state.doc.resolve(0), GapCursorSide.LEFT))); }); } return { update(view) { if (editorExperiment('platform_synced_block', true)) { // Caret visibility now handled directly via CSS selector in gapCursorStyles.ts return; } const { selectionIsGapCursor } = gapCursorPluginKey.getState(view.state); /** * Starting with prosemirror-view 1.19.4, cursor wrapper that previously was hiding cursor doesn't exist: * https://github.com/ProseMirror/prosemirror-view/commit/4a56bc7b7e61e96ef879d1dae1014ede0fc09e43 * * Because it was causing issues with RTL: https://github.com/ProseMirror/prosemirror/issues/948 * * This is the work around which uses `caret-color: transparent` in order to hide regular caret, * when gap cursor is visible. * * Browser support is pretty good: https://caniuse.com/#feat=css-caret-color */ view.dom.classList.toggle(hideCaretModifier, selectionIsGapCursor); } }; }, props: { decorations: editorState => { const { doc, selection } = editorState; const { displayGapCursor, hideCursor } = gapCursorPluginKey.getState(editorState); if (selection instanceof GapCursorSelection && displayGapCursor && !hideCursor) { const { $from, side } = selection; // render decoration DOM node always to the left of the target node even if selection points to the right // otherwise positioning of the right gap cursor is a nightmare when the target node has a nodeView with vertical margins let position = selection.head; const isRightCursor = side === Side.RIGHT; if (isRightCursor && $from.nodeBefore) { const nodeBeforeStart = findPositionOfNodeBefore(selection); if (typeof nodeBeforeStart === 'number') { position = nodeBeforeStart; } } const node = isRightCursor ? $from.nodeBefore : $from.nodeAfter; const layoutMode = node && getLayoutModeFromTargetNode(node); return DecorationSet.create(doc, [Decoration.widget(position, toDOM, { key: `${JSON_ID}-${side}-${layoutMode}`, // position === 0: if gap cursor at start of document, render it on the left side of the selection to enable pasting (otherwise Chrome doesn't pick up the paste event) side: layoutMode || position === 0 ? -1 : 0 })]); } return null; }, // render gap cursor only when its valid createSelectionBetween(view, $anchor, $head) { if (view && view.state && view.state.selection instanceof CellSelection) { // Do not show GapCursor when there is a CellSection happening return null; } if ($anchor.pos === $head.pos && GapCursorSelection.valid($head)) { return new GapCursorSelection($head); } return null; }, handleClick(view, nodePos, event) { var _$pos$parent; const posAtCoords = view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!posAtCoords || isIgnoredClick(event.target instanceof HTMLElement ? event.target : null)) { return false; } const isInsideTheTarget = posAtCoords.pos === posAtCoords.inside; if (isInsideTheTarget) { return false; } const leftSideOffsetX = 20; const side = event.offsetX > leftSideOffsetX ? Side.RIGHT : Side.LEFT; const $pos = view.state.doc.resolve(nodePos); // In the new prosemirror-view posAtCoords is not returning a precise value for our media nodes if (((_$pos$parent = $pos.parent) === null || _$pos$parent === void 0 ? void 0 : _$pos$parent.type.name) === 'mediaSingle') { const $insidePos = view.state.doc.resolve(Math.max(posAtCoords.inside, 0)); // We don't have GapCursors problems when the node target is inside the root level if ($insidePos.depth <= 1) { return false; } const mediaGapCursor = !$pos.nodeBefore ? $pos.before() : $pos.after(); return setGapCursorAtPos(mediaGapCursor, side)(view.state, view.dispatch); } const docSize = view.state.doc.content.size; const nodeInside = posAtCoords.inside < 0 || posAtCoords.inside > docSize ? null : view.state.doc.nodeAt(posAtCoords.inside); if (nodeInside !== null && nodeInside !== void 0 && nodeInside.isAtom) { return false; } return setGapCursorAtPos(nodePos, side)(view.state, view.dispatch); }, handleDOMEvents: { /** * Android composition events aren't handled well by Prosemirror * We've added a couple of beforeinput hooks to help PM out when trying to delete * certain nodes. We can remove these when PM has better composition support. * @see https://github.com/ProseMirror/prosemirror/issues/543 */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any beforeinput: (view, event) => { if (event.inputType === 'deleteContentBackward' && view.state.selection instanceof GapCursorSelection) { event.preventDefault(); return deleteNode(Direction.BACKWARD)(view.state, view.dispatch); } return false; } } } }); export default plugin;