UNPKG

@atlaskit/editor-plugin-extension

Version:

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

168 lines (163 loc) 6.5 kB
import assert from 'assert'; import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD, TARGET_SELECTION_SOURCE } from '@atlaskit/editor-common/analytics'; import { normaliseNestedLayout } from '@atlaskit/editor-common/insert'; import { getValidNode } from '@atlaskit/editor-common/validator'; import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findSelectedNodeOfType, replaceParentNodeOfType, replaceSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils'; import { getPluginState as getExtensionPluginState } from '../plugin-factory'; import { pluginKey } from './plugin-key'; export const insertMacroFromMacroBrowser = editorAnalyticsAPI => (macroProvider, macroNode, isEditing) => async view => { if (!macroProvider) { return false; } // opens MacroBrowser for editing "macroNode" if passed in const newMacro = await macroProvider.openMacroBrowser(macroNode); if (newMacro && macroNode) { const { state, dispatch } = view; const currentLayout = macroNode && macroNode.attrs.layout || 'default'; const node = resolveMacro(newMacro, state, { layout: currentLayout }); if (!node) { return false; } const { selection, schema } = state; const { extension, inlineExtension, bodiedExtension, multiBodiedExtension } = schema.nodes; const updateSelectionsByNodeType = nodeType => { // `isEditing` is `false` when we are inserting from insert-block toolbar tr = isEditing ? replaceParentNodeOfType(nodeType, node)(tr) : safeInsert(node)(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)); }; const extensionState = getExtensionPluginState(state); let targetSelectionSource = TARGET_SELECTION_SOURCE.CURRENT_SELECTION; let { tr } = state; const isBodiedExtensionSelected = !!findSelectedNodeOfType([bodiedExtension])(selection); const isMultiBodiedExtensionSelected = !!findSelectedNodeOfType([multiBodiedExtension])(selection); // When it's a bodiedExtension but not selected if (macroNode.type === bodiedExtension && !isBodiedExtensionSelected) { updateSelectionsByNodeType(state.schema.nodes.bodiedExtension); } // When it's a multiBodiedExtension but not selected else if (macroNode.type === multiBodiedExtension && !isMultiBodiedExtensionSelected) { updateSelectionsByNodeType(state.schema.nodes.multiBodiedExtension); } // If any extension is currently selected else if (findSelectedNodeOfType([extension, bodiedExtension, inlineExtension, multiBodiedExtension])(selection)) { tr = replaceSelectedNode(node)(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 + macroNode.nodeSize, node); tr.setSelection(Selection.near(tr.doc.resolve(pos))); targetSelectionSource = TARGET_SELECTION_SOURCE.HTML_ELEMENT; } } // Only scroll if we have anything to update, best to avoid surprise scroll if (dispatch && tr.docChanged) { const { extensionType, extensionKey, layout, localId } = macroNode.attrs; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({ action: ACTION.UPDATED, actionSubject: ACTION_SUBJECT.EXTENSION, actionSubjectId: macroNode.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: isEditing ? INPUT_METHOD.MACRO_BROWSER : INPUT_METHOD.TOOLBAR, extensionType, extensionKey, layout, localId, selection: tr.selection.toJSON(), targetSelectionSource } })(tr); dispatch(tr.scrollIntoView()); } return true; } return false; }; export const resolveMacro = (macro, state, optionalAttrs) => { if (!macro || !state) { return null; } const { schema } = state; const { type, attrs } = getValidNode(macro, schema); let node = null; if (type === 'extension') { node = schema.nodes.extension.create({ ...attrs, ...optionalAttrs }); } else if (type === 'bodiedExtension') { node = schema.nodes.bodiedExtension.create({ ...attrs, ...optionalAttrs }, schema.nodeFromJSON(macro).content); } else if (type === 'inlineExtension') { node = schema.nodes.inlineExtension.create(attrs); } else if (type === 'multiBodiedExtension') { node = schema.nodes.multiBodiedExtension.create({ ...attrs, ...optionalAttrs }, schema.nodeFromJSON(macro).content); } return node && normaliseNestedLayout(state, node); }; // gets the macroProvider from the state and tries to autoConvert a given text export const runMacroAutoConvert = (state, text) => { const macroPluginState = pluginKey.getState(state); const macroProvider = macroPluginState && macroPluginState.macroProvider; if (!macroProvider || !macroProvider.autoConvert) { return null; } const macroAttributes = macroProvider.autoConvert(text); if (!macroAttributes) { return null; } // decides which kind of macro to render (inline|bodied|bodyless) return resolveMacro(macroAttributes, state); }; export const setMacroProvider = provider => async view => { let resolvedProvider; try { resolvedProvider = await provider; assert(resolvedProvider && resolvedProvider.openMacroBrowser, `MacroProvider promise did not resolve to a valid instance of MacroProvider - ${resolvedProvider}`); } catch (err) { resolvedProvider = null; } view.dispatch(view.state.tr.setMeta(pluginKey, { macroProvider: resolvedProvider })); return true; };