UNPKG

@atlaskit/editor-plugin-extension

Version:

editor-plugin-extension plugin for @atlaskit/editor-core

241 lines (239 loc) 8.28 kB
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD, TARGET_SELECTION_SOURCE } from '@atlaskit/editor-common/analytics'; import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findSelectedNodeOfType, replaceParentNodeOfType, replaceSelectedNode } from '@atlaskit/editor-prosemirror/utils'; import { createExtensionAPI, getEditInLegacyMacroBrowser } from '../pm-plugins/extension-api'; import { getPluginState } from '../pm-plugins/plugin-factory'; import { findExtensionWithLocalId } from '../pm-plugins/utils'; export const buildExtensionNode = (type, schema, attrs, content, marks) => { switch (type) { case 'extension': return schema.nodes.extension.createChecked(attrs, content, marks); case 'inlineExtension': return schema.nodes.inlineExtension.createChecked(attrs, content, marks); case 'bodiedExtension': return schema.nodes.bodiedExtension.create(attrs, content, marks); case 'multiBodiedExtension': return schema.nodes.multiBodiedExtension.create(attrs, content, marks); } }; export const performNodeUpdate = editorAnalyticsAPI => (type, newAttrs, content, marks, shouldScrollIntoView) => (_state, _dispatch, view) => { if (!view) { throw Error('EditorView is required to perform node update!'); } // NOTE: `state` and `dispatch` are stale at this point so we need to grab // the latest one from `view` @see HOT-93986 const { state, dispatch } = view; const newNode = buildExtensionNode(type, state.schema, newAttrs, content, marks); if (!newNode) { return false; } const { selection, schema } = state; const { extension, inlineExtension, bodiedExtension, multiBodiedExtension } = schema.nodes; const isBodiedExtensionSelected = !!findSelectedNodeOfType([bodiedExtension])(selection); const isMultiBodiedExtensionSelected = !!findSelectedNodeOfType([multiBodiedExtension])(selection); const extensionState = getPluginState(state); const updateSelectionsByNodeType = nodeType => { // Bodied/MultiBodied extensions can trigger an update when the cursor is inside which means that there is no node selected. // To work around that we replace the parent and create a text selection instead of new node selection tr = replaceParentNodeOfType(nodeType, newNode)(tr); // Replacing selected node doesn't update the selection. `selection.node` still returns the old node tr.setSelection(TextSelection.create(tr.doc, state.selection.anchor)); }; let targetSelectionSource = TARGET_SELECTION_SOURCE.CURRENT_SELECTION; let action = ACTION.UPDATED; let { tr } = state; // When it's a bodiedExtension but not selected if (newNode.type === bodiedExtension && !isBodiedExtensionSelected) { updateSelectionsByNodeType(state.schema.nodes.bodiedExtension); } // When it's a multiBodiedExtension but not selected else if (newNode.type === multiBodiedExtension && !isMultiBodiedExtensionSelected) { updateSelectionsByNodeType(state.schema.nodes.multiBodiedExtension); } // If any extension is currently selected else if (findSelectedNodeOfType([extension, bodiedExtension, inlineExtension, multiBodiedExtension])(selection)) { tr = replaceSelectedNode(newNode)(tr); // Replacing selected node doesn't update the selection. `selection.node` still returns the old node tr.setSelection(NodeSelection.create(tr.doc, tr.mapping.map(state.selection.anchor))); } // When we loose the selection. This usually happens when Synchrony resets or changes // the selection when user is in the middle of updating an extension. else if (extensionState.element) { const pos = view.posAtDOM(extensionState.element, -1); if (pos > -1) { tr = tr.replaceWith(pos, pos + (content.size || 0) + 1, newNode); tr.setSelection(Selection.near(tr.doc.resolve(pos))); targetSelectionSource = TARGET_SELECTION_SOURCE.HTML_ELEMENT; } else { action = ACTION.ERRORED; } } // Only scroll if we have anything to update, best to avoid surprise scroll if (dispatch && tr.docChanged) { const { extensionType, extensionKey, layout, localId } = newNode.attrs; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({ action, actionSubject: ACTION_SUBJECT.EXTENSION, actionSubjectId: newNode.type.name, eventType: EVENT_TYPE.TRACK, attributes: { // @ts-expect-error - Type is not assignable to parameter of type 'AnalyticsEventPayload' // This error was introduced after upgrading to TypeScript 5 inputMethod: INPUT_METHOD.CONFIG_PANEL, extensionType, extensionKey, layout, localId, selection: tr.selection.toJSON(), targetSelectionSource } })(tr); dispatch(shouldScrollIntoView ? tr.scrollIntoView() : tr); } return true; }; const updateExtensionParams = editorAnalyticsAPI => (updateExtension, node, actions) => async (state, dispatch, view) => { const { attrs, type, content, marks } = node.node; if (!state.schema.nodes[type.name]) { return false; } const { parameters } = attrs; try { const newParameters = await updateExtension(parameters, actions); if (newParameters) { const newAttrs = { ...attrs, parameters: { ...parameters, ...newParameters } }; if (type.name === 'multiBodiedExtension') { newAttrs.parameters.macroParams = { ...parameters.macroParams, ...(newParameters === null || newParameters === void 0 ? void 0 : newParameters.macroParams) }; } return performNodeUpdate(editorAnalyticsAPI)(type.name, newAttrs, content, marks, true)(state, dispatch, view); } } catch {} return true; }; export const editExtension = (macroProvider, applyChangeToContextPanel, editorAnalyticsAPI, updateExtension) => (state, dispatch, view) => { if (!view) { return false; } const { localId } = getPluginState(state); const nodeWithPos = findExtensionWithLocalId(state, localId); if (!nodeWithPos) { return false; } const editInLegacyMacroBrowser = getEditInLegacyMacroBrowser({ view, macroProvider: macroProvider || undefined, editorAnalyticsAPI }); if (updateExtension) { updateExtension.then(updateMethod => { if (updateMethod && view) { const actions = createExtensionAPI({ editorView: view, editInLegacyMacroBrowser, applyChange: applyChangeToContextPanel, editorAnalyticsAPI }); updateExtensionParams(editorAnalyticsAPI)(updateMethod, nodeWithPos, actions)(state, dispatch, view); return; } if (!updateMethod && macroProvider) { editInLegacyMacroBrowser(); return; } }); } else { if (!macroProvider) { return false; } editInLegacyMacroBrowser(); } return true; }; export const createEditSelectedExtensionAction = ({ editorViewRef, editorAnalyticsAPI, applyChangeToContextPanel }) => () => { const { current: view } = editorViewRef; if (!view) { return false; } const { updateExtension } = getPluginState(view.state); return editExtension(null, applyChangeToContextPanel, editorAnalyticsAPI, updateExtension)(view.state, view.dispatch, view); }; export const insertOrReplaceExtension = ({ editorView, action, attrs, content, position, size = 0, tr }) => { const newNode = editorView.state.schema.node('extension', attrs, content); if (action === 'insert') { tr = editorView.state.tr.insert(position, newNode); return tr; } else { tr.replaceWith(position, position + size, newNode); return tr; } }; export const insertOrReplaceBodiedExtension = ({ editorView, action, attrs, content, position, size = 0, tr }) => { const newNode = editorView.state.schema.node('bodiedExtension', attrs, content); if (action === 'insert') { tr = editorView.state.tr.insert(position, newNode); return tr; } else { tr.replaceWith(position, position + size, newNode); return tr; } };