@atlaskit/editor-plugin-toolbar
Version:
Toolbar plugin for @atlaskit/editor-core
240 lines (239 loc) • 12 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
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) {
var selection = editorState.selection;
if (selection instanceof NodeSelection) {
return {
node: selection.node,
pos: selection.from,
nodeType: selection.node.type.name,
marks: selection.node.marks.map(function (mark) {
return "".concat(mark.type.name, ":").concat(JSON.stringify(mark.attrs));
})
};
}
var nodes = editorState.schema.nodes;
var 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(function (mark) {
return "".concat(mark.type.name, ":").concat(JSON.stringify(mark.attrs));
})
};
}
var 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(function (mark) {
return "".concat(mark.type.name, ":").concat(JSON.stringify(mark.attrs));
})
};
}
var $pos = selection.$from;
return {
node: $pos.parent,
pos: $pos.pos,
nodeType: $pos.parent.type.name,
marks: $pos.marks().map(function (mark) {
return "".concat(mark.type.name, ":").concat(JSON.stringify(mark.attrs));
})
};
}
export var toolbarPlugin = function toolbarPlugin(_ref) {
var api = _ref.api,
_ref$config = _ref.config,
config = _ref$config === void 0 ? {
disableSelectionToolbar: false,
disableSelectionToolbarWhenPinned: false
} : _ref$config;
var refs = {};
var disableSelectionToolbar = config.disableSelectionToolbar,
disableSelectionToolbarWhenPinned = config.disableSelectionToolbarWhenPinned,
_config$contextualFor = config.contextualFormattingEnabled,
contextualFormattingEnabled = _config$contextualFor === void 0 ? 'always-pinned' : _config$contextualFor,
breakpointPreset = config.breakpointPreset;
var registry = createComponentRegistry();
registry.register(getToolbarComponents(contextualFormattingEnabled, api, breakpointPreset));
return {
name: 'toolbar',
actions: {
registerComponents: function registerComponents(toolbarComponents) {
var replaceItems = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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: function getComponents() {
return config !== null && config !== void 0 && config.componentFilter ? registry.components.filter(config.componentFilter) : registry.components;
},
contextualFormattingMode: function contextualFormattingMode() {
var _config$contextualFor2;
// 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).
var override = config === null || config === void 0 || (_config$contextualFor2 = config.contextualFormattingModeOverride) === null || _config$contextualFor2 === void 0 ? void 0 : _config$contextualFor2.call(config);
if (override) {
return override;
}
return contextualFormattingEnabled !== null && contextualFormattingEnabled !== void 0 ? contextualFormattingEnabled : 'always-pinned';
},
getBreakpointPreset: function getBreakpointPreset() {
return breakpointPreset;
}
},
getSharedState: function getSharedState(editorState) {
if (!editorState) {
return undefined;
}
return editorToolbarPluginKey.getState(editorState);
},
pmPlugins: function pmPlugins() {
return [{
name: 'editor-toolbar-selection',
plugin: function plugin() {
// Tracks mouse-down state to prevent the focus event (first page load)
// from prematurely showing the toolbar mid-drag
var mouseState = {
isMouseDown: false
};
return new SafePlugin({
key: editorToolbarPluginKey,
state: {
init: function init(_, editorState) {
return {
shouldShowToolbar: false,
selectedNode: getSelectedNode(editorState)
};
},
apply: function apply(tr, pluginState, _, newState) {
var meta = tr.getMeta(editorToolbarPluginKey);
var newPluginState = _objectSpread({}, pluginState);
var shouldUpdateNode = tr.docChanged || tr.selectionSet;
if (shouldUpdateNode) {
var newSelectedNode = getSelectedNode(newState);
var oldNode = pluginState.selectedNode;
var 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 = _objectSpread(_objectSpread({}, newPluginState), meta);
}
return newPluginState;
}
},
view: function view(_view) {
var unbind = bind(_view.root, {
type: 'mouseup',
listener: function listener(ev) {
var _api$editorViewMode;
mouseState.isMouseDown = false;
var event = ev;
var isInToolbar = isEventInContainer(event, DEFAULT_POPUP_SELECTORS.toolbarContainer);
var 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.
var editorViewModePlugin = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.currentState();
var 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
}));
}
});
var unbindEditorViewFocus = bind(_view.dom, {
type: 'focus',
listener: function 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: function destroy() {
unbind();
unbindEditorViewFocus();
}
};
},
props: {
handleDOMEvents: {
mousedown: function mousedown(view) {
mouseState.isMouseDown = true;
view.dispatch(view.state.tr.setMeta(editorToolbarPluginKey, {
shouldShowToolbar: false
}));
return false;
}
}
}
});
}
}].concat(_toConsumableArray(!disableSelectionToolbar && expValEquals('platform_editor_experience_tracking', 'isEnabled', true) ? [{
name: 'selectionToolbarOpenExperience',
plugin: function plugin() {
return getSelectionToolbarOpenExperiencePlugin({
refs: refs,
dispatchAnalyticsEvent: function dispatchAnalyticsEvent(payload) {
var _api$analytics;
return api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 ? void 0 : _api$analytics.fireAnalyticsEvent(payload);
}
});
}
}] : []));
},
contentComponent: !disableSelectionToolbar ? function (_ref2) {
var editorView = _ref2.editorView,
popupsMountPoint = _ref2.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
};
};