@atlaskit/editor-plugin-extension
Version:
editor-plugin-extension plugin for @atlaskit/editor-core
314 lines (309 loc) • 16.7 kB
JavaScript
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _typeof from "@babel/runtime/helpers/typeof";
import _regeneratorRuntime from "@babel/runtime/regenerator";
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { GapCursorSelection, createSelectionClickHandler, isSelectionAtEndOfNode, isSelectionAtStartOfNode } from '@atlaskit/editor-common/selection';
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfTypeClosestToPos, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { clearEditingContext, updateState } from '../editor-commands/commands';
import { lazyExtensionNodeView } from '../nodeviews/lazyExtension';
import { createPluginState, getPluginState } from './plugin-factory';
import { pluginKey } from './plugin-key';
import { updateEditButton } from './update-edit-button';
import { getSelectedDomElement, getSelectedExtension } from './utils';
var shouldShowEditButton = function shouldShowEditButton(extensionHandler, extensionProvider) {
var usesLegacyMacroBrowser = !extensionHandler && !extensionProvider || typeof extensionHandler === 'function';
var usesModernUpdateMethod = _typeof(extensionHandler) === 'object' && typeof extensionHandler.update === 'function';
if (usesLegacyMacroBrowser || usesModernUpdateMethod) {
return true;
}
return false;
};
var getUpdateExtensionPromise = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(view, extensionHandler, extensionProvider) {
var updateMethod;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!(extensionHandler && _typeof(extensionHandler) === 'object')) {
_context.next = 4;
break;
}
return _context.abrupt("return", extensionHandler.update);
case 4:
if (!extensionProvider) {
_context.next = 10;
break;
}
_context.next = 7;
return updateEditButton(view, extensionProvider);
case 7:
updateMethod = _context.sent;
if (!updateMethod) {
_context.next = 10;
break;
}
return _context.abrupt("return", updateMethod);
case 10:
throw new Error('No update method available');
case 11:
case "end":
return _context.stop();
}
}, _callee);
}));
return function getUpdateExtensionPromise(_x, _x2, _x3) {
return _ref.apply(this, arguments);
};
}();
export var createExtensionProviderHandler = function createExtensionProviderHandler(view) {
return /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(name, provider) {
var extensionProvider;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
if (!(name === 'extensionProvider' && provider)) {
_context2.next = 13;
break;
}
_context2.prev = 1;
_context2.next = 4;
return provider;
case 4:
extensionProvider = _context2.sent;
updateState({
extensionProvider: extensionProvider
})(view.state, view.dispatch);
_context2.next = 8;
return updateEditButton(view, extensionProvider);
case 8:
_context2.next = 13;
break;
case 10:
_context2.prev = 10;
_context2.t0 = _context2["catch"](1);
updateState({
extensionProvider: undefined
})(view.state, view.dispatch);
case 13:
case "end":
return _context2.stop();
}
}, _callee2, null, [[1, 10]]);
}));
return function (_x4, _x5) {
return _ref2.apply(this, arguments);
};
}();
};
export var handleUpdate = function handleUpdate(_ref3) {
var view = _ref3.view,
prevState = _ref3.prevState,
domAtPos = _ref3.domAtPos,
extensionHandlers = _ref3.extensionHandlers,
applyChange = _ref3.applyChange;
var state = view.state,
dispatch = view.dispatch;
var _getPluginState = getPluginState(state),
element = _getPluginState.element,
localId = _getPluginState.localId,
extensionProvider = _getPluginState.extensionProvider,
showContextPanel = _getPluginState.showContextPanel,
showEditButton = _getPluginState.showEditButton;
// This fetches the selected extension node, either by keyboard selection or click for all types of extensions
var selectedExtension = getSelectedExtension(state, true);
if (!selectedExtension) {
if (showContextPanel) {
clearEditingContext(applyChange)(state, dispatch);
}
return;
}
var node = selectedExtension.node;
var newElement = getSelectedDomElement(state.schema, domAtPos, selectedExtension);
// In some cases, showEditButton can be stale and the edit button doesn't show - @see ED-15285
// To be safe, we update the showEditButton state here
var shouldUpdateEditButton = !showEditButton && extensionProvider && element === newElement && !getSelectedExtension(prevState, true);
var isNewNodeSelected = node.attrs.localId ? localId !== node.attrs.localId :
// This is the current assumption and it's wrong but we are keeping it
// as fallback in case we need to turn off `allowLocalIdGeneration`
element !== newElement;
if (isNewNodeSelected || shouldUpdateEditButton) {
if (showContextPanel) {
clearEditingContext(applyChange)(state, dispatch);
return;
}
var extensionType = node.attrs.extensionType;
var extensionHandler = extensionHandlers[extensionType];
// showEditButton might change async based on results from extension providers
var _showEditButton = shouldShowEditButton(extensionHandler, extensionProvider);
var updateExtension = getUpdateExtensionPromise(view, extensionHandler, extensionProvider).catch(function () {
// do nothing;
});
updateState({
localId: node.attrs.localId,
showContextPanel: false,
element: newElement,
showEditButton: _showEditButton,
updateExtension: updateExtension
})(state, dispatch);
}
// New DOM element doesn't necessarily mean it's a new Node
else if (element !== newElement) {
updateState({
element: newElement
})(state, dispatch);
}
return true;
};
export var createPlugin = function createPlugin(dispatch, providerFactory, extensionHandlers, portalProviderAPI, eventDispatcher, pluginInjectionApi) {
var _featureFlags$macroIn;
var useLongPressSelection = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : false;
var options = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : {};
var featureFlags = arguments.length > 8 ? arguments[8] : undefined;
var __rendererExtensionOptions = arguments.length > 9 ? arguments[9] : undefined;
var state = createPluginState(dispatch, {
showEditButton: false,
showContextPanel: false
});
var extensionNodeViewOptions = {
appearance: options.appearance,
getExtensionHeight: options.getExtensionHeight
};
var macroInteractionDesignFeatureFlags = {
showMacroInteractionDesignUpdates: (_featureFlags$macroIn = featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.macroInteractionUpdates) !== null && _featureFlags$macroIn !== void 0 ? _featureFlags$macroIn : false
};
var showLivePagesBodiedMacrosRendererView = __rendererExtensionOptions === null || __rendererExtensionOptions === void 0 ? void 0 : __rendererExtensionOptions.isAllowedToUseRendererView;
return new SafePlugin({
state: state,
view: function view(editorView) {
var domAtPos = editorView.domAtPos.bind(editorView);
var extensionProviderHandler = createExtensionProviderHandler(editorView);
providerFactory.subscribe('extensionProvider', extensionProviderHandler);
return {
update: function update(view, prevState) {
var _pluginInjectionApi$c;
handleUpdate({
view: view,
prevState: prevState,
domAtPos: domAtPos,
extensionHandlers: extensionHandlers,
applyChange: pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$c = pluginInjectionApi.contextPanel) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.actions.applyChange
});
},
destroy: function destroy() {
providerFactory.unsubscribe('extensionProvider', extensionProviderHandler);
}
};
},
key: pluginKey,
props: {
handleDOMEvents: {
/**
* ED-18072 - Cannot shift + arrow past bodied extension if it is not empty.
* This code is to handle the case where the selection starts inside or on the node and the user is trying to shift + arrow.
* For other part of the solution see code in: packages/editor/editor-core/src/plugins/selection/pm-plugins/events/keydown.ts
*/
keydown: function keydown(view, event) {
if (event instanceof KeyboardEvent && event.shiftKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
var _view$state = view.state,
schema = _view$state.schema,
selection = _view$state.selection,
$head = _view$state.selection.$head,
doc = _view$state.doc,
tr = _view$state.tr;
var bodiedExtension = schema.nodes.bodiedExtension;
if (selection instanceof TextSelection || selection instanceof NodeSelection) {
var maybeBodiedExtension = selection instanceof TextSelection ? findParentNodeOfTypeClosestToPos($head, bodiedExtension) : findSelectedNodeOfType(bodiedExtension)(selection);
if (maybeBodiedExtension) {
var end = maybeBodiedExtension.pos + maybeBodiedExtension.node.nodeSize;
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft' && isSelectionAtStartOfNode($head, maybeBodiedExtension === null || maybeBodiedExtension === void 0 ? void 0 : maybeBodiedExtension.node)) {
var anchor = end + 1;
// an offset is used here so that left arrow selects the first character before the node (consistent with arrow right)
var headOffset = event.key === 'ArrowLeft' ? -1 : 0;
var head = maybeBodiedExtension.pos + headOffset;
var newSelection = TextSelection.create(doc, Math.max(anchor, selection.anchor), head);
view.dispatch(tr.setSelection(newSelection));
return true;
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight' && isSelectionAtEndOfNode($head, maybeBodiedExtension === null || maybeBodiedExtension === void 0 ? void 0 : maybeBodiedExtension.node)) {
var _anchor = maybeBodiedExtension.pos - 1;
var _head = end + 1;
var _newSelection = TextSelection.create(doc, Math.min(_anchor, selection.anchor), _head);
view.dispatch(tr.setSelection(_newSelection));
return true;
}
}
}
}
// Handle non shift key case for MBE
if (event instanceof KeyboardEvent && !event.shiftKey && event.key === 'ArrowLeft') {
var _view$state2 = view.state,
_schema = _view$state2.schema,
_selection = _view$state2.selection,
_$head = _view$state2.selection.$head,
_doc = _view$state2.doc,
_tr = _view$state2.tr;
var _schema$nodes = _schema.nodes,
multiBodiedExtension = _schema$nodes.multiBodiedExtension,
extensionFrame = _schema$nodes.extensionFrame,
paragraph = _schema$nodes.paragraph;
if (_selection instanceof GapCursorSelection || _selection instanceof TextSelection && _$head.parent.type === paragraph) {
var maybeMultiBodiedExtension = findParentNodeOfTypeClosestToPos(_$head, multiBodiedExtension);
if (maybeMultiBodiedExtension) {
var _tr$doc$nodeAt;
/* In case of gap cursor, we need to decrement the position by 1 as we need to check the node at previous position
* In case of text selection, we need to decrement the position by 2 as we need to jump back twice, once from text node and then its parent paragraph node
*/
var previousPositionDecrement = _selection instanceof GapCursorSelection ? 1 : 2;
if (((_tr$doc$nodeAt = _tr.doc.nodeAt(_$head.pos - previousPositionDecrement)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type) === extensionFrame) {
var _newSelection2 = TextSelection.create(_doc, _tr.doc.resolve(_$head.pos - previousPositionDecrement).start(_$head.depth - previousPositionDecrement));
view.dispatch(_tr.setSelection(_newSelection2));
}
}
}
}
return false;
}
},
nodeViews: {
// WARNING: referentiality-plugin also creates these nodeviews
extension: lazyExtensionNodeView('extension', portalProviderAPI, eventDispatcher, providerFactory, extensionHandlers, extensionNodeViewOptions, pluginInjectionApi, macroInteractionDesignFeatureFlags),
// WARNING: referentiality-plugin also creates these nodeviews
bodiedExtension: lazyExtensionNodeView('bodiedExtension', portalProviderAPI, eventDispatcher, providerFactory, extensionHandlers, extensionNodeViewOptions, pluginInjectionApi, macroInteractionDesignFeatureFlags, showLivePagesBodiedMacrosRendererView, __rendererExtensionOptions === null || __rendererExtensionOptions === void 0 ? void 0 : __rendererExtensionOptions.showUpdated1PBodiedExtensionUI, __rendererExtensionOptions === null || __rendererExtensionOptions === void 0 ? void 0 : __rendererExtensionOptions.rendererExtensionHandlers),
// WARNING: referentiality-plugin also creates these nodeviews
inlineExtension: lazyExtensionNodeView('inlineExtension', portalProviderAPI, eventDispatcher, providerFactory, extensionHandlers, extensionNodeViewOptions, pluginInjectionApi, macroInteractionDesignFeatureFlags),
multiBodiedExtension: lazyExtensionNodeView('multiBodiedExtension', portalProviderAPI, eventDispatcher, providerFactory, extensionHandlers, extensionNodeViewOptions, pluginInjectionApi, macroInteractionDesignFeatureFlags)
},
createSelectionBetween: function createSelectionBetween(view, anchor, head) {
var _view$state3 = view.state,
schema = _view$state3.schema,
doc = _view$state3.doc;
var multiBodiedExtension = schema.nodes.multiBodiedExtension;
var isAnchorInMBE = findParentNodeOfTypeClosestToPos(anchor, multiBodiedExtension);
var isHeadInMBE = findParentNodeOfTypeClosestToPos(head, multiBodiedExtension);
if (isAnchorInMBE !== undefined && isHeadInMBE === undefined) {
// Anchor is in MBE, where user started selecting within MBE and then moved outside
var newSelection = TextSelection.create(doc, isAnchorInMBE.pos < head.pos ? isAnchorInMBE.pos : isAnchorInMBE.pos + isAnchorInMBE.node.nodeSize,
// isAnchorInMBE.pos < head.pos represents downwards selection
head.pos);
return newSelection;
}
if (isAnchorInMBE === undefined && isHeadInMBE !== undefined) {
// Head is in MBE, where user started selecting outside MBE and then moved inside
var _newSelection3 = TextSelection.create(doc, anchor.pos, isHeadInMBE.pos < anchor.pos ? isHeadInMBE.pos : isHeadInMBE.pos + isHeadInMBE.node.nodeSize // isHeadInMBE.pos < anchor.pos represents upwards selection
);
return _newSelection3;
}
return null;
},
handleClickOn: createSelectionClickHandler(['extension', 'bodiedExtension', 'multiBodiedExtension'], function (target) {
return !target.closest('.extension-non-editable-area') && (!target.closest('.extension-content') || !!target.closest('.extension-container'));
},
// It's to enable nested extensions selection
{
useLongPressSelection: useLongPressSelection
})
}
});
};