@atlaskit/editor-plugin-base
Version:
Base plugin for @atlaskit/editor-core
190 lines (187 loc) • 8.84 kB
JavaScript
;
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());
}