UNPKG

@atlaskit/editor-plugin-extension

Version:

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

307 lines (296 loc) 11.5 kB
import { validator } from '@atlaskit/adf-utils/validator'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { nodeToJSON } from '@atlaskit/editor-common/utils'; import { JSONTransformer } from '@atlaskit/editor-json-transformer'; import { Fragment, Mark } from '@atlaskit/editor-prosemirror/model'; import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { setTextSelection } from '@atlaskit/editor-prosemirror/utils'; import { setEditingContextToContextPanel } from '../editor-commands/commands'; import { insertMacroFromMacroBrowser } from './macro/actions'; import { pluginKey as macroPluginKey } from './macro/plugin-key'; import { findNodePosWithLocalId, getDataConsumerMark, getNodeTypesReferenced, getSelectedExtension } from './utils'; export const getEditInLegacyMacroBrowser = ({ view, macroProvider, editorAnalyticsAPI }) => { return () => { if (!view) { throw new Error(`Missing view. Can't update without EditorView`); } if (!macroProvider) { throw new Error(`Missing macroProvider. Can't use the macro browser for updates`); } const nodeWithPos = getSelectedExtension(view.state, true); if (!nodeWithPos) { throw new Error(`Missing nodeWithPos. Can't determine position of node`); } insertMacroFromMacroBrowser(editorAnalyticsAPI)(macroProvider, nodeWithPos.node, true)(view); }; }; const extensionAPICallPayload = functionName => ({ action: ACTION.INVOKED, actionSubject: ACTION_SUBJECT.EXTENSION, actionSubjectId: ACTION_SUBJECT_ID.EXTENSION_API, attributes: { functionName }, eventType: EVENT_TYPE.TRACK }); export const createExtensionAPI = options => { const { editorView: { state: { schema } }, editorAnalyticsAPI } = options; const nodes = Object.keys(schema.nodes); const marks = Object.keys(schema.marks); const validate = validator(nodes, marks, { allowPrivateAttributes: true }); /** * Finds the node and its position by `localId`. Throws if the node could not be found. * * @returns {NodeWithPos} */ const ensureNodePosByLocalId = (localId, { opName }) => { // Be extra cautious since 3rd party devs can use regular JS without type safety if (typeof localId !== 'string' || localId === '') { throw new Error(`${opName}(): Invalid localId '${localId}'.`); } // Find the node + position matching the given ID const { editorView: { state } } = options; const nodePos = findNodePosWithLocalId(state, localId); if (!nodePos) { throw new Error(`${opName}(): Could not find node with ID '${localId}'.`); } return nodePos; }; const doc = { insertAfter: (localId, adf, opt) => { try { validate(adf); } catch (e) { throw new Error(`insertAfter(): Invalid ADF given.`); } const nodePos = ensureNodePosByLocalId(localId, { opName: 'insertAfter' }); const { editorView } = options; const { dispatch, state } = editorView; // Validate the given ADF const { tr, schema } = state; const nodeType = schema.nodes[adf.type]; if (!nodeType) { throw new Error(`insertAfter(): Invalid ADF type '${adf.type}'.`); } const fragment = Fragment.fromJSON(schema, adf.content); const marks = (adf.marks || []).map(markEntity => Mark.fromJSON(schema, markEntity)); const newNode = nodeType === null || nodeType === void 0 ? void 0 : nodeType.createChecked(adf.attrs, fragment, marks); if (!newNode) { throw new Error('insertAfter(): Could not create a node for given ADFEntity.'); } const insertPosition = nodePos.pos + nodePos.node.nodeSize; tr.insert(insertPosition, newNode); // Validate if the document is valid at this point try { tr.doc.check(); } catch (err) { throw new Error(`insertAfter(): The given ADFEntity cannot be inserted in the current position.\n${err}`); } // Analytics - tracking the api call const apiCallPayload = extensionAPICallPayload('insertAfter'); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(apiCallPayload)(tr); // Analytics - tracking node types added const nodesAdded = [newNode]; newNode.descendants(node => { nodesAdded.push(node); return true; }); nodesAdded.forEach(node => { const { extensionKey, extensionType } = node.attrs; const dataConsumerMark = getDataConsumerMark(node); const stringIds = (dataConsumerMark === null || dataConsumerMark === void 0 ? void 0 : dataConsumerMark.attrs.sources.map(sourceId => sourceId)) || []; const hasReferentiality = !!dataConsumerMark; const nodeTypesReferenced = hasReferentiality ? getNodeTypesReferenced(stringIds, state) : undefined; // fire off analytics for this ADF const payload = { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, attributes: { nodeType: node.type.name, inputMethod: INPUT_METHOD.EXTENSION_API, hasReferentiality, nodeTypesReferenced, layout: node.attrs.layout, extensionType, extensionKey }, eventType: EVENT_TYPE.TRACK }; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr); }); if (opt) { if (opt.allowSelectionToNewNode) { tr.setSelection(new NodeSelection(tr.doc.resolve(insertPosition))); } else if (opt.allowSelectionNearNewNode) { tr.setSelection(TextSelection.near(tr.doc.resolve(insertPosition))); } } dispatch(tr); }, scrollTo: localId => { const nodePos = ensureNodePosByLocalId(localId, { opName: 'scrollTo' }); // Analytics - tracking the api call const apiCallPayload = extensionAPICallPayload('scrollTo'); const { editorView: { dispatch, state } } = options; let { tr } = state; editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(apiCallPayload)(tr); tr = setTextSelection(nodePos.pos)(tr); tr = tr.scrollIntoView(); dispatch(tr); }, update: (localId, mutationCallback, opts) => { var _changedValues$marks; const { node, pos } = ensureNodePosByLocalId(localId, { opName: 'update' }); const { editorView: { dispatch, state } } = options; const { tr, schema } = state; const changedValues = mutationCallback({ content: nodeToJSON(node).content, attrs: node.attrs, marks: node.marks.map(pmMark => ({ type: pmMark.type.name, attrs: pmMark.attrs })) }); const ensureValidMark = mark => { if (typeof mark !== 'object' || Array.isArray(mark)) { throw new Error(`update(): Invalid mark given.`); } const { parent } = state.doc.resolve(pos); // Ensure that the given mark is present in the schema const markType = schema.marks[mark.type]; if (!markType) { throw new Error(`update(): Invalid ADF mark type '${mark.type}'.`); } if (!parent.type.allowsMarkType(markType)) { throw new Error(`update(): Parent of type '${parent.type.name}' does not allow marks of type '${mark.type}'.`); } return { mark: markType, attrs: mark.attrs }; }; const newMarks = changedValues.hasOwnProperty('marks') ? (_changedValues$marks = changedValues.marks) === null || _changedValues$marks === void 0 ? void 0 : _changedValues$marks.map(ensureValidMark).map(({ mark, attrs }) => mark.create(attrs)) : node.marks; const newContent = changedValues.hasOwnProperty('content') ? Fragment.fromJSON(schema, changedValues.content) : node.content; let newAttrs = changedValues.hasOwnProperty('attrs') ? changedValues.attrs : node.attrs; if (node.type.name === 'multiBodiedExtension') { var _changedValues$attrs, _node$attrs$parameter, _changedValues$attrs2, _changedValues$attrs3; newAttrs = { ...node.attrs, ...changedValues.attrs, parameters: { ...node.attrs.parameters, ...((_changedValues$attrs = changedValues.attrs) === null || _changedValues$attrs === void 0 ? void 0 : _changedValues$attrs.parameters), macroParams: { ...((_node$attrs$parameter = node.attrs.parameters) === null || _node$attrs$parameter === void 0 ? void 0 : _node$attrs$parameter.macroParams), ...((_changedValues$attrs2 = changedValues.attrs) === null || _changedValues$attrs2 === void 0 ? void 0 : (_changedValues$attrs3 = _changedValues$attrs2.parameters) === null || _changedValues$attrs3 === void 0 ? void 0 : _changedValues$attrs3.macroParams) } } }; // console.log('newAttrs', newAttrs); } // Validate if the new attributes, content and marks result in a valid node and adf. try { const newNode = node.type.createChecked(newAttrs, newContent, newMarks); const newNodeAdf = new JSONTransformer().encodeNode(newNode); validate(newNodeAdf); tr.replaceWith(pos, pos + node.nodeSize, newNode); // Keep selection if content does not change if (newContent === node.content) { tr.setSelection(Selection.fromJSON(tr.doc, state.selection.toJSON())); } } catch (err) { throw new Error(`update(): The given ADFEntity cannot be inserted in the current position.\n${err}`); } // Analytics - tracking the api call const apiCallPayload = extensionAPICallPayload('update'); editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(apiCallPayload)(tr); if (typeof (opts === null || opts === void 0 ? void 0 : opts.addToHistory) === 'boolean') { tr.setMeta('addToHistory', opts.addToHistory); } dispatch(tr); } }; return { editInContextPanel: (transformBefore, transformAfter) => { const { editorView } = options; setEditingContextToContextPanel(transformBefore, transformAfter, options.applyChange)(editorView.state, editorView.dispatch, editorView); }, _editInLegacyMacroBrowser: () => { const { editorView } = options; let editInLegacy = options.editInLegacyMacroBrowser; if (!editInLegacy) { const macroState = macroPluginKey.getState(editorView.state); editInLegacy = getEditInLegacyMacroBrowser({ view: options.editorView, macroProvider: (macroState === null || macroState === void 0 ? void 0 : macroState.macroProvider) || undefined, editorAnalyticsAPI }); } editInLegacy(); }, doc }; };