UNPKG

@atlaskit/editor-plugin-selection-extension

Version:

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

327 lines (320 loc) 13.6 kB
import React from 'react'; import { isSSR } from '@atlaskit/editor-common/core-utils'; import { selectionExtensionMessages } from '@atlaskit/editor-common/messages'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { usePluginStateEffect } from '@atlaskit/editor-common/use-plugin-state-effect'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { insertAdfAtEndOfDoc } from './pm-plugins/actions/insertAdfAtEndOfDoc'; import { replaceWithAdf } from './pm-plugins/actions/replaceWithAdf'; import { createPlugin, selectionExtensionPluginKey } from './pm-plugins/main'; import { getFragmentInfoFromSelection, getFragmentInfoFromSelectionNew, getSelectionAdfInfo, getSelectionAdfInfoNew, getSelectionTextInfo } from './pm-plugins/utils'; import { SelectionExtensionActionTypes } from './types'; import { SelectionExtensionComponentWrapper } from './ui/extension/SelectionExtensionComponentWrapper'; import { getMenuItemExtensions, getToolbarItemExtensions } from './ui/extensions'; import { LegacyPrimaryToolbarComponent } from './ui/LegacyToolbarComponent'; import { selectionToolbar } from './ui/selectionToolbar'; import { getToolbarComponents } from './ui/toolbar-components'; import { registerBlockMenuItems } from './ui/utils/registerBlockMenuItems'; export const selectionExtensionPlugin = ({ api, config }) => { const editorViewRef = {}; let cachedSelection; let cachedOverflowMenuOptions; const isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar); const { extensionList = [], extensions = {} } = config || {}; const { firstParty = [], external = [] } = extensions || {}; if (!isToolbarAIFCEnabled) { const primaryToolbarItemExtensions = getToolbarItemExtensions(extensionList, 'primaryToolbar'); if (primaryToolbarItemExtensions !== null && primaryToolbarItemExtensions !== void 0 && primaryToolbarItemExtensions.length) { var _api$primaryToolbar, _api$primaryToolbar$a; api === null || api === void 0 ? void 0 : (_api$primaryToolbar = api.primaryToolbar) === null || _api$primaryToolbar === void 0 ? void 0 : (_api$primaryToolbar$a = _api$primaryToolbar.actions) === null || _api$primaryToolbar$a === void 0 ? void 0 : _api$primaryToolbar$a.registerComponent({ name: 'selectionExtension', component: () => /*#__PURE__*/React.createElement(LegacyPrimaryToolbarComponent, { primaryToolbarItemExtensions: primaryToolbarItemExtensions }) }); } } if (editorExperiment('platform_editor_block_menu', true, { exposure: true })) { registerBlockMenuItems({ extensionList, api, editorViewRef }); } return { name: 'selectionExtension', getSharedState(editorState) { if (!editorState) { return null; } return selectionExtensionPluginKey.getState(editorState) || null; }, commands: { setActiveExtension: extension => ({ tr }) => { return tr.setMeta(selectionExtensionPluginKey, { type: 'set-active-extension', extension }); }, clearActiveExtension: () => ({ tr }) => { return tr.setMeta(selectionExtensionPluginKey, { type: 'clear-active-extension' }); } }, actions: { replaceWithAdf: nodeAdf => { if (!editorViewRef.current) { return { status: 'failed-to-replace' }; } const { state, dispatch } = editorViewRef.current; return replaceWithAdf(nodeAdf, api)(state, dispatch); }, insertAdfAtEndOfDoc: nodeAdf => { if (!editorViewRef.current) { return { status: 'failed' }; } const { state, dispatch } = editorViewRef.current; return insertAdfAtEndOfDoc(nodeAdf)(state, dispatch); }, getSelectionAdf: () => { if (!editorViewRef.current) { return null; } const { state } = editorViewRef.current; if (editorExperiment('platform_editor_block_menu', true, { exposure: true })) { var _api$blockControls, _api$blockControls$sh; const selection = (api === null || api === void 0 ? void 0 : (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 ? void 0 : (_api$blockControls$sh = _api$blockControls.sharedState.currentState()) === null || _api$blockControls$sh === void 0 ? void 0 : _api$blockControls$sh.preservedSelection) || state.selection; return getSelectionAdfInfoNew(selection); } const { selectionRanges, selectedNodeAdf } = getSelectionAdfInfo(state); return { selectedNodeAdf, selectionRanges }; }, getDocumentFromSelection: () => { if (!editorViewRef.current) { return null; } const { state } = editorViewRef.current; if (editorExperiment('platform_editor_block_menu', true, { exposure: true })) { var _api$blockControls2, _api$blockControls2$s; const selection = (api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : (_api$blockControls2$s = _api$blockControls2.sharedState.currentState()) === null || _api$blockControls2$s === void 0 ? void 0 : _api$blockControls2$s.preservedSelection) || state.selection; return getFragmentInfoFromSelectionNew(selection); } const { selectedNodeAdf } = getFragmentInfoFromSelection(state); return { selectedNodeAdf }; } }, usePluginHook: () => { usePluginStateEffect(api, ['selection'], () => { if (isSSR()) { return; } if (isToolbarAIFCEnabled) { var _api$toolbar; api === null || api === void 0 ? void 0 : (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 ? void 0 : _api$toolbar.actions.registerComponents(getToolbarComponents({ api, config }), true); } }); }, contentComponent: ({ editorView }) => { var _api$analytics; if (!editorView || isSSR()) { return null; } return /*#__PURE__*/React.createElement(SelectionExtensionComponentWrapper, { editorView: editorView, api: api, editorAnalyticsAPI: api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions }); }, pluginsOptions: { selectionToolbar: isToolbarAIFCEnabled ? undefined : (state, intl) => { var _api$editorViewMode, _api$editorViewMode$s; if (!config) { return; } const { pageModes } = config; // Extensions Config Validation // Check whether plugin contains any selection extensions if (!(firstParty !== null && firstParty !== void 0 && firstParty.length) && !(external !== null && external !== void 0 && external.length) && !(extensionList !== null && extensionList !== void 0 && extensionList.length)) { return; } // Content Mode Validation // Check if pageModes is provided and matches against current content mode // This will eventually transition from mode to viewMode const editorViewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode; if (pageModes) { // Early Exit: consumer has set pageModes but editorViewMode is undefined if (!editorViewMode) { return; } // Simplify traversion of pageModes which can be string or array of strings const showOnModesCollection = Array.isArray(pageModes) ? pageModes : [pageModes]; // Early Exit: consumer has set pageModes but current editorViewMode is not in the collection if (!showOnModesCollection.includes(editorViewMode)) { return; } } // Active Extension // Check if there is an active extension and hide the selection extension dropdown const selectionExtensionState = selectionExtensionPluginKey.getState(state); if (selectionExtensionState !== null && selectionExtensionState !== void 0 && selectionExtensionState.activeExtension) { return; } const handleOnExtensionClick = view => extension => { var _extension$onClick, _api$core; const selection = getSelectionTextInfo(view, api); if (extension.component) { api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : api.selectionExtension.commands.setActiveExtension({ extension, selection })); } const { selectedNodeAdf, selectionRanges, selectedNode, nodePos } = getSelectionAdfInfo(view.state); const onClickCallbackOptions = { selectedNodeAdf, selectionRanges }; (_extension$onClick = extension.onClick) === null || _extension$onClick === void 0 ? void 0 : _extension$onClick.call(extension, onClickCallbackOptions); api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({ tr }) => { tr.setMeta(selectionExtensionPluginKey, { type: SelectionExtensionActionTypes.SET_SELECTED_NODE, selectedNode, nodePos }); return tr; }); }; const convertExtensionToDropdownMenuItem = (extension, rank) => { var _extension$isDisabled; const disabled = (extension === null || extension === void 0 ? void 0 : extension.isDisabled) instanceof Function ? extension === null || extension === void 0 ? void 0 : (_extension$isDisabled = extension.isDisabled) === null || _extension$isDisabled === void 0 ? void 0 : _extension$isDisabled.call(extension, { selection: editorViewRef.current ? getSelectionTextInfo(editorViewRef.current, api) : undefined }) : extension === null || extension === void 0 ? void 0 : extension.isDisabled; return { title: extension.name, icon: extension.icon ? /*#__PURE__*/React.createElement(extension.icon, { label: '' }) : undefined, disabled, rank, onClick: () => { editorViewRef.current && handleOnExtensionClick(editorViewRef.current)(extension); return true; } }; }; const getFirstPartyExtensions = extensions => { return extensions.map(extension => { return convertExtensionToDropdownMenuItem(extension, 30); }); }; // Add a heading to the external extensions const getExternalExtensions = extensions => { let externalExtensions = []; if (extensions !== null && extensions !== void 0 && extensions.length) { externalExtensions = extensions.map(extension => { return convertExtensionToDropdownMenuItem(extension); }); const externalExtensionsHeading = { type: 'overflow-dropdown-heading', title: intl.formatMessage(selectionExtensionMessages.externalExtensionsHeading) }; externalExtensions.unshift(externalExtensionsHeading); } return externalExtensions; }; // NEXT PR: Make sure we cache the whole generated selection toolbar // also debug this to make sure it's actually preventing unnecessary re-renders / work if (cachedOverflowMenuOptions && state.selection.eq(cachedSelection)) { return selectionToolbar({ overflowOptions: cachedOverflowMenuOptions, extensionList }); } const allFirstParty = [...firstParty, ...getMenuItemExtensions(extensionList, 'first-party')]; const allExternal = [...external, ...getMenuItemExtensions(extensionList, 'external')]; const groupedExtensionsArray = [...getFirstPartyExtensions(allFirstParty), ...getExternalExtensions(allExternal)]; cachedOverflowMenuOptions = groupedExtensionsArray; cachedSelection = state.selection; return selectionToolbar({ overflowOptions: cachedOverflowMenuOptions, extensionList }); } }, pmPlugins: () => [{ name: 'selectionExtension', plugin: () => createPlugin() }, { name: 'selectionExtensionGetEditorViewReferencePlugin', plugin: () => { return new SafePlugin({ view: editorView => { editorViewRef.current = editorView; return { destroy: () => { delete editorViewRef.current; } }; } }); } }] }; };