UNPKG

@atlaskit/editor-plugin-base

Version:

Base plugin for @atlaskit/editor-core

190 lines (187 loc) 8.84 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.inlineCursorTargetStateKey = exports.default = void 0; var _browser = require("@atlaskit/editor-common/browser"); var _safePlugin = require("@atlaskit/editor-common/safe-plugin"); var _utils = require("@atlaskit/editor-common/utils"); var _whitespace = require("@atlaskit/editor-common/whitespace"); var _state = require("@atlaskit/editor-prosemirror/state"); var _view = require("@atlaskit/editor-prosemirror/view"); var inlineCursorTargetStateKey = exports.inlineCursorTargetStateKey = new _state.PluginKey('inlineCursorTargetPlugin'); var isInlineNodeView = function isInlineNodeView(node) { /** * If inlineNodeView is a hardbreak we don't want to add decorations * as it breaks soft line breaks for Japanese/Chinese keyboards. */ var isHardBreak = (node === null || node === void 0 ? void 0 : node.type) === (node === null || node === void 0 ? void 0 : node.type.schema.nodes.hardBreak); return node && node.type.isInline && !node.type.isText && !isHardBreak; }; var _default = exports.default = function _default() { return new _safePlugin.SafePlugin({ key: inlineCursorTargetStateKey, state: { init: function init() { return { cursorTarget: undefined }; }, apply: function apply(tr) { var selection = tr.selection, doc = tr.doc; var $from = selection.$from, $to = selection.$to; // In Safari, if the cursor target is to the right of the cursor it will block selections // made with shift + arrowRight and vice versa for shift + arrowLeft. This is due to a // contenteditable bug in safari, where editable elements block the selection but we need // the cursor target to be editable for the following: // - Ability to navigate with down/up arrows when between inline nodes // - Ability to navigate with down/up arrows when between the start of a paragraph & an inline node // - Ability to click and drag to select an inline node if it is the first node // To prevent blocking the selection, we check handleDOMEvents and add meta to // the transaction to prevent the plugin from making cursor target decorations. var safariShiftSelection = tr.getMeta(inlineCursorTargetStateKey); if (selection && (0, _utils.isTextSelection)(selection) && !safariShiftSelection) { var hasInlineNodeViewAfter = isInlineNodeView($from.nodeAfter); var hasInlineNodeViewBefore = isInlineNodeView($from.nodeBefore); var isAtStartAndInlineNodeViewAfter = $from.parentOffset === 0 && isInlineNodeView($from.nodeAfter); var isAtEndAndInlineNodeViewBefore = doc.resolve($from.pos).node().lastChild === $from.nodeBefore && isInlineNodeView($from.nodeBefore); var createWidget = function createWidget(side) { var node = document.createElement('span'); node.contentEditable = 'true'; node.setAttribute('aria-hidden', 'true'); node.appendChild(document.createTextNode(_whitespace.ZERO_WIDTH_SPACE)); node.className = 'cursor-target'; var $from = selection.$from; var rightPosition = $from.pos; var leftPosition = $from.posAtIndex(Math.max($from.index() - 1, 0)); var widgetPos = side === 'left' ? leftPosition : rightPosition; return _view.Decoration.widget(widgetPos, node, { raw: true, key: 'inlineCursor' // eslint-disable-next-line @typescript-eslint/no-explicit-any }); }; // Create editable decoration widgets around the current inline node to allow proper cursor navigation. if ((hasInlineNodeViewAfter || isAtEndAndInlineNodeViewBefore) && (hasInlineNodeViewBefore || isAtStartAndInlineNodeViewAfter)) { return { cursorTarget: { decorations: [createWidget('left'), createWidget('right')], positions: { from: $from.pos, to: $to.pos } } }; } } return { cursorTarget: undefined }; } }, props: { decorations: function decorations(state) { var doc = state.doc; var _ref = inlineCursorTargetStateKey.getState(state), cursorTarget = _ref.cursorTarget; if (cursorTargetHasValidDecorations(cursorTarget)) { return _view.DecorationSet.create(doc, cursorTarget.decorations); } return null; }, handleDOMEvents: { // Workaround to prevent the decorations created by the plugin from // blocking shift + arrow left/right selections in safari. When // a shift + arrow left/right event is detected, send meta data to the // plugin to prevent it from creating decorations. // TODO: ED-26959 - We may be able to remove this when playing the following ticket: // https://product-fabric.atlassian.net/browse/ED-14938 keydown: function keydown(view, event) { var browser = (0, _browser.getBrowserInfo)(); if (browser.safari && event instanceof KeyboardEvent && event.shiftKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { view.dispatch(view.state.tr.setMeta(inlineCursorTargetStateKey, { cursorTarget: undefined })); } return false; }, // Check the DOM to see if there are inline cursor targets // after a composition event ends. If so, manually insert the // event data in order to prevent contents ending up inside // of the cursor target decorations. compositionend: function compositionend(view, incorrectlyTypedEvent) { // This is typed by the prosemirror definitions as Event, // this type is incorrect, and it is actually an InputEvent var event = incorrectlyTypedEvent; var state = view.state; var _ref2 = inlineCursorTargetStateKey.getState(state), cursorTarget = _ref2.cursorTarget; if (cursorTarget !== undefined) { handleTextInputInsideCursorTargetDecoration({ event: event, cursorTarget: cursorTarget, view: view }); return true; } return false; }, // Check the DOM to see if there are inline cursor targets // before any input event. If so, manually insert the // event data in order to prevent contents ending up inside // of the cursor target decorations. beforeinput: function beforeinput(view, incorrectlyTypedEvent) { // This is typed by the prosemirror definitions as Event, // this type is incorrect, and it is actually an InputEvent var event = incorrectlyTypedEvent; var state = view.state; var _ref3 = inlineCursorTargetStateKey.getState(state), cursorTarget = _ref3.cursorTarget; if (!event.isComposing && cursorTarget !== undefined) { handleTextInputInsideCursorTargetDecoration({ event: event, cursorTarget: cursorTarget, view: view }); return true; } return false; } } } }); }; function cursorTargetHasValidDecorations(cursorTarget) { if (!cursorTarget || // Decorations can end up as null when the decorations prop is // called after the decorations have been removed from the dom. // https://github.com/ProseMirror/prosemirror-view/blob/8f0d313a6389b86a335274fba36534ba1cb21f12/src/decoration.js#L30 cursorTarget.decorations.includes(null)) { return false; } return true; } function handleTextInputInsideCursorTargetDecoration(_ref4) { var event = _ref4.event, view = _ref4.view, cursorTarget = _ref4.cursorTarget; event.stopPropagation(); event.preventDefault(); var content = event.data || ''; // ensure any custom handleTextInput handlers are called for the input event // ie. type ahead, emoji shortcuts. var defaultTransaction = function defaultTransaction() { return view.state.tr.insertText(content, cursorTarget.positions.from, cursorTarget.positions.to); }; var potentiallyHandleByHandleTextInput = view.someProp('handleTextInput', function (f) { return f(view, cursorTarget.positions.from, cursorTarget.positions.to, content, defaultTransaction); }); if (potentiallyHandleByHandleTextInput) { // if a handleTextInput handler has handled the event, we don't want to // manually update the document. return; } view.dispatch(defaultTransaction()); }