@atlaskit/editor-plugin-toolbar
Version:
Toolbar plugin for @atlaskit/editor-core
236 lines (235 loc) • 10.3 kB
JavaScript
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
};
};