import React from 'react'; import { EditorState } from 'prosemirror-state'; import { getExtensionKeyAndNodeKey } from '@atlaskit/editor-common/extensions'; import { getPluginState } from './pm-plugins/main'; import { getSelectedExtension } from './utils'; import WithEditorActions from '../../ui/WithEditorActions'; import ConfigPanelLoader from '../../ui/ConfigPanel/ConfigPanelLoader'; import { duplicateSelection } from '../../utils/selection'; import { clearEditingContext, forceAutoSave, updateState } from './commands'; import { buildExtensionNode } from './actions'; import type { EditorView } from 'prosemirror-view'; import type { ContentNodeWithPos } from 'prosemirror-utils'; import type { ExtensionState } from './types'; import { SaveIndicator } from './ui/SaveIndicator/SaveIndicator'; const areParametersEqual = ( firstParameters: any, secondParameters: any, ): boolean => { if ( typeof firstParameters === 'object' && typeof secondParameters === 'object' && firstParameters !== null && secondParameters !== null ) { const firstKeys = Object.keys(firstParameters); const secondKeys = Object.keys(secondParameters); return ( firstKeys.length === secondKeys.length && firstKeys.every((key) => firstParameters[key] === secondParameters[key]) ); } return firstParameters === secondParameters; }; export const getContextPanel = (allowAutoSave?: boolean) => ( state: EditorState, ) => { const nodeWithPos = getSelectedExtension(state, true); // Adding checks to bail out early if (!nodeWithPos) { return; } const extensionState = getPluginState(state); const { autoSaveResolve, showContextPanel, extensionProvider, processParametersBefore, processParametersAfter, } = extensionState; if ( extensionState && showContextPanel && extensionProvider && processParametersAfter ) { const { extensionType, extensionKey, parameters } = nodeWithPos.node.attrs; const [extKey, nodeKey] = getExtensionKeyAndNodeKey( extensionKey, extensionType, ); const configParams = processParametersBefore ? processParametersBefore(parameters || {}) : parameters; return ( {({ onSaveStarted, onSaveEnded }) => { return ( { const editorView = actions._privateGetEditorView(); if (!editorView) { return null; } return ( { await onChangeAction( editorView, updatedParameters, parameters, nodeWithPos, onSaveStarted, ); onSaveEnded(); if (autoSaveResolve) { autoSaveResolve(); } if (!allowAutoSave) { clearEditingContext( editorView.state, editorView.dispatch, ); } }} onCancel={async () => { if (allowAutoSave) { await new Promise((resolve) => { forceAutoSave(resolve)( editorView.state, editorView.dispatch, ); }); } clearEditingContext( editorView.state, editorView.dispatch, ); }} /> ); }} /> ); }} ); } }; export async function onChangeAction( editorView: EditorView, updatedParameters: object = {}, oldParameters: object = {}, nodeWithPos: ContentNodeWithPos, onSaving?: () => void, ) { // WARNING: editorView.state stales quickly, do not unpack const { processParametersAfter, processParametersBefore } = getPluginState( editorView.state, ) as ExtensionState; if (!processParametersAfter) { return; } const unwrappedOldParameters = processParametersBefore ? processParametersBefore(oldParameters) : oldParameters; // todo: update to only check parameters which are in the manifest's field definitions if (areParametersEqual(unwrappedOldParameters, updatedParameters)) { return; } if (onSaving) { onSaving(); } const key = Date.now(); const { positions: previousPositions } = getPluginState( editorView.state, ) as ExtensionState; await updateState({ positions: { ...previousPositions, [key]: nodeWithPos.pos, }, })(editorView.state, editorView.dispatch); // WARNING: after this, editorView.state may have changed const newParameters = await processParametersAfter(updatedParameters); const { positions } = getPluginState(editorView.state) as ExtensionState; if (!positions) { return; } if (!(key in positions)) { return; } const { node } = nodeWithPos; const newNode = buildExtensionNode( nodeWithPos.node.toJSON().type, editorView.state.schema, { ...node.attrs, parameters: { ...oldParameters, ...newParameters, }, }, node.content, node.marks, ); if (!newNode) { return; } const positionUpdated = positions[key]; const transaction = editorView.state.tr.replaceWith( positionUpdated, positionUpdated + newNode.nodeSize, newNode, ); // Ensure we preserve the selection, tr.replaceWith causes it to be lost in some cases // when replacing the node const { selection: prevSelection } = editorView.state; if (!prevSelection.eq(transaction.selection)) { const selection = duplicateSelection(prevSelection, transaction.doc); if (selection) { transaction.setSelection(selection); } } const positionsLess: Record = { ...(getPluginState(editorView.state) as ExtensionState).positions, }; delete positionsLess[key]; await updateState({ positions: positionsLess, })(editorView.state, editorView.dispatch); editorView.dispatch(transaction); }