UNPKG

@atlaskit/editor-plugin-toolbar

Version:

Toolbar plugin for @atlaskit/editor-core

236 lines (235 loc) 10.3 kB
import React from 'react'; import { bind } from 'bind-event-listener'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { createComponentRegistry } from '@atlaskit/editor-toolbar-model'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { getSelectionToolbarOpenExperiencePlugin } from './pm-plugins/experiences/selection-toolbar-open-experience'; import { editorToolbarPluginKey } from './pm-plugins/plugin-key'; import { DEFAULT_POPUP_SELECTORS } from './ui/consts'; import { SelectionToolbarWithErrorBoundary } from './ui/SelectionToolbar'; import { getToolbarComponents } from './ui/toolbar-components'; import { isEventInContainer } from './ui/utils/toolbar'; function getSelectedNode(editorState) { const { selection } = editorState; if (selection instanceof NodeSelection) { return { node: selection.node, pos: selection.from, nodeType: selection.node.type.name, marks: selection.node.marks.map(mark => `${mark.type.name}:${JSON.stringify(mark.attrs)}`) }; } const { nodes } = editorState.schema; const selectedNode = findSelectedNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.codeBlock])(selection); if (selectedNode) { return { node: selectedNode.node, pos: selectedNode.pos, nodeType: selectedNode.node.type.name, marks: selectedNode.node.marks.map(mark => `${mark.type.name}:${JSON.stringify(mark.attrs)}`) }; } const parentNode = findParentNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.listItem, nodes.taskItem])(selection); if (parentNode) { return { node: parentNode.node, pos: parentNode.pos, nodeType: parentNode.node.type.name, marks: parentNode.node.marks.map(mark => `${mark.type.name}:${JSON.stringify(mark.attrs)}`) }; } const $pos = selection.$from; return { node: $pos.parent, pos: $pos.pos, nodeType: $pos.parent.type.name, marks: $pos.marks().map(mark => `${mark.type.name}:${JSON.stringify(mark.attrs)}`) }; } export const toolbarPlugin = ({ api, config = { disableSelectionToolbar: false, disableSelectionToolbarWhenPinned: false } }) => { const refs = {}; const { disableSelectionToolbar, disableSelectionToolbarWhenPinned, contextualFormattingEnabled = 'always-pinned', breakpointPreset } = config; const registry = createComponentRegistry(); registry.register(getToolbarComponents(contextualFormattingEnabled, api, breakpointPreset)); return { name: 'toolbar', actions: { registerComponents: (toolbarComponents, replaceItems = false) => { if (replaceItems) { registry.safeRegister(toolbarComponents); } else { registry.register(toolbarComponents); } }, // EDITOR-6558: `componentFilter` is evaluated at read time (not at // `registerComponents` time) so it can react to runtime state // (e.g. the current markdown view mode) without forcing every // plugin to re-register its components. Returning a new array on // every call means React-tree consumers re-render whenever the // underlying filter would change. getComponents: () => { return config !== null && config !== void 0 && config.componentFilter ? registry.components.filter(config.componentFilter) : registry.components; }, contextualFormattingMode: () => { var _config$contextualFor; // EDITOR-6558: `contextualFormattingModeOverride` lets a // consumer (Markdown Mode in source/preview view) force the // toolbar to a specific docking position regardless of the // user's saved preference. Used to lock the toolbar to // `always-pinned` while the floating toolbar would be useless // (i.e. when there's no ProseMirror selection to anchor to). const override = config === null || config === void 0 ? void 0 : (_config$contextualFor = config.contextualFormattingModeOverride) === null || _config$contextualFor === void 0 ? void 0 : _config$contextualFor.call(config); if (override) { return override; } return contextualFormattingEnabled !== null && contextualFormattingEnabled !== void 0 ? contextualFormattingEnabled : 'always-pinned'; }, getBreakpointPreset: () => { return breakpointPreset; } }, getSharedState(editorState) { if (!editorState) { return undefined; } return editorToolbarPluginKey.getState(editorState); }, pmPlugins() { return [{ name: 'editor-toolbar-selection', plugin: () => { // Tracks mouse-down state to prevent the focus event (first page load) // from prematurely showing the toolbar mid-drag const mouseState = { isMouseDown: false }; return new SafePlugin({ key: editorToolbarPluginKey, state: { init(_, editorState) { return { shouldShowToolbar: false, selectedNode: getSelectedNode(editorState) }; }, apply(tr, pluginState, _, newState) { const meta = tr.getMeta(editorToolbarPluginKey); let newPluginState = { ...pluginState }; const shouldUpdateNode = tr.docChanged || tr.selectionSet; if (shouldUpdateNode) { const newSelectedNode = getSelectedNode(newState); const oldNode = pluginState.selectedNode; const hasNodeChanged = !oldNode || !newSelectedNode || oldNode.nodeType !== newSelectedNode.nodeType || oldNode.pos !== newSelectedNode.pos || JSON.stringify(oldNode.marks) !== JSON.stringify(newSelectedNode.marks); if (hasNodeChanged) { newPluginState.selectedNode = newSelectedNode; } } if (meta) { newPluginState = { ...newPluginState, ...meta }; } return newPluginState; } }, view(view) { const unbind = bind(view.root, { type: 'mouseup', listener: function (ev) { var _api$editorViewMode; mouseState.isMouseDown = false; const event = ev; const isInToolbar = isEventInContainer(event, DEFAULT_POPUP_SELECTORS.toolbarContainer); const isInPortal = isEventInContainer(event, DEFAULT_POPUP_SELECTORS.portal); // We only want to set selectionStable to true if the editor has focus // to prevent the toolbar from showing when the editor is blurred // due to a click outside the editor. const editorViewModePlugin = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.currentState(); const isViewModeEnabled = (editorViewModePlugin === null || editorViewModePlugin === void 0 ? void 0 : editorViewModePlugin.mode) === 'view'; view.dispatch(view.state.tr.setMeta(editorToolbarPluginKey, { shouldShowToolbar: !isViewModeEnabled ? view.hasFocus() || isInToolbar || isInPortal : true })); } }); const unbindEditorViewFocus = bind(view.dom, { type: 'focus', listener: () => { // On first page load, focus fires after mousedown — skip to // avoid showing the toolbar mid-drag if (mouseState.isMouseDown && fg('platform_editor_fix_toolbar_on_first_highlight')) { return; } view.dispatch(view.state.tr.setMeta(editorToolbarPluginKey, { shouldShowToolbar: true })); } }); return { destroy() { unbind(); unbindEditorViewFocus(); } }; }, props: { handleDOMEvents: { mousedown: view => { mouseState.isMouseDown = true; view.dispatch(view.state.tr.setMeta(editorToolbarPluginKey, { shouldShowToolbar: false })); return false; } } } }); } }, ...(!disableSelectionToolbar && expValEquals('platform_editor_experience_tracking', 'isEnabled', true) ? [{ name: 'selectionToolbarOpenExperience', plugin: () => getSelectionToolbarOpenExperiencePlugin({ refs, dispatchAnalyticsEvent: payload => { var _api$analytics, _api$analytics$action; return api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent(payload); } }) }] : [])]; }, contentComponent: !disableSelectionToolbar ? ({ editorView, popupsMountPoint }) => { refs.popupsMountPoint = popupsMountPoint || undefined; if (!editorView) { return null; } return /*#__PURE__*/React.createElement(SelectionToolbarWithErrorBoundary, { api: api, editorView: editorView, mountPoint: popupsMountPoint, disableSelectionToolbarWhenPinned: disableSelectionToolbarWhenPinned !== null && disableSelectionToolbarWhenPinned !== void 0 ? disableSelectionToolbarWhenPinned : false }); } : undefined }; };