@atlaskit/editor-plugin-selection-toolbar
Version:
@atlaskit/editor-plugin-selection-toolbar for @atlaskit/editor-core
392 lines (388 loc) • 21.2 kB
JavaScript
import React from 'react';
import { bind } from 'bind-event-listener';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { calculateToolbarPositionAboveSelection, calculateToolbarPositionOnCellSelection, calculateToolbarPositionTrackHead } from '@atlaskit/editor-common/utils';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { fg } from '@atlaskit/platform-feature-flags';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { setToolbarDocking, toggleToolbar, updateToolbarDocking, forceToolbarDockingWithoutAnalytics } from './pm-plugins/commands';
import { selectionToolbarPluginKey } from './pm-plugins/plugin-key';
import { PageVisibilityWatcher } from './ui/PageVisibilityWatcher';
import { getPinOptionToolbarConfig } from './ui/pin-toolbar-config';
import { PrimaryToolbarComponent } from './ui/PrimaryToolbarComponent';
import { getToolbarComponents } from './ui/toolbar-components';
const getToolbarDocking = (contextualFormattingEnabled, userPreferencesProvider) => {
if (contextualFormattingEnabled && editorExperiment('platform_editor_controls', 'variant1')) {
var _userPreferencesProvi;
return (_userPreferencesProvi = userPreferencesProvider === null || userPreferencesProvider === void 0 ? void 0 : userPreferencesProvider.getPreference('toolbarDockingInitialPosition')) !== null && _userPreferencesProvi !== void 0 ? _userPreferencesProvi : 'none';
}
return 'top';
};
const getToolbarDockingV2 = (contextualFormattingEnabled, dockingPreference) => {
if (contextualFormattingEnabled && editorExperiment('platform_editor_controls', 'variant1')) {
return dockingPreference !== null && dockingPreference !== void 0 ? dockingPreference : 'none';
}
return 'top';
};
export const selectionToolbarPlugin = ({
api,
config
}) => {
const __selectionToolbarHandlers = [];
let primaryToolbarComponent;
const isToolbarAIFCEnabled = Boolean(api === null || api === void 0 ? void 0 : api.toolbar);
const {
userPreferencesProvider,
contextualFormattingEnabled,
disablePin
} = config;
if (isToolbarAIFCEnabled) {
var _api$toolbar;
/**
* If toolbar is set to always-pinned or always-inline, there is no control over toolbar placement
*/
if ((api === null || api === void 0 ? void 0 : (_api$toolbar = api.toolbar) === null || _api$toolbar === void 0 ? void 0 : _api$toolbar.actions.contextualFormattingMode()) === 'controlled') {
var _api$toolbar2;
api === null || api === void 0 ? void 0 : (_api$toolbar2 = api.toolbar) === null || _api$toolbar2 === void 0 ? void 0 : _api$toolbar2.actions.registerComponents(getToolbarComponents(api, true, disablePin));
}
} else {
if (editorExperiment('platform_editor_controls', 'variant1', {
exposure: true
})) {
var _api$primaryToolbar;
primaryToolbarComponent = ({
disabled
}) => {
return /*#__PURE__*/React.createElement(PrimaryToolbarComponent, {
api: api,
disabled: disabled
});
};
api === null || api === void 0 ? void 0 : (_api$primaryToolbar = api.primaryToolbar) === null || _api$primaryToolbar === void 0 ? void 0 : _api$primaryToolbar.actions.registerComponent({
name: 'pinToolbar',
component: primaryToolbarComponent
});
}
}
let previousToolbarDocking = (userPreferencesProvider === null || userPreferencesProvider === void 0 ? void 0 : userPreferencesProvider.getPreference('toolbarDockingInitialPosition')) || null;
let isPreferenceInitialized = false;
return {
name: 'selectionToolbar',
actions: {
suppressToolbar: () => {
var _api$core$actions$exe;
return (_api$core$actions$exe = api === null || api === void 0 ? void 0 : api.core.actions.execute(toggleToolbar({
hide: true
}))) !== null && _api$core$actions$exe !== void 0 ? _api$core$actions$exe : false;
},
unsuppressToolbar: () => {
var _api$core$actions$exe2;
return (_api$core$actions$exe2 = api === null || api === void 0 ? void 0 : api.core.actions.execute(toggleToolbar({
hide: false
}))) !== null && _api$core$actions$exe2 !== void 0 ? _api$core$actions$exe2 : false;
},
setToolbarDocking: toolbarDocking => {
var _api$core$actions$exe4, _api$analytics;
if (fg('platform_editor_use_preferences_plugin')) {
var _api$core$actions$exe3, _api$userPreferences;
return (_api$core$actions$exe3 = api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : (_api$userPreferences = api.userPreferences) === null || _api$userPreferences === void 0 ? void 0 : _api$userPreferences.actions.updateUserPreference('toolbarDockingPosition', toolbarDocking))) !== null && _api$core$actions$exe3 !== void 0 ? _api$core$actions$exe3 : false;
}
return (_api$core$actions$exe4 = api === null || api === void 0 ? void 0 : api.core.actions.execute(setToolbarDocking({
toolbarDocking,
userPreferencesProvider,
editorAnalyticsApi: api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions
}))) !== null && _api$core$actions$exe4 !== void 0 ? _api$core$actions$exe4 : false;
},
forceToolbarDockingWithoutAnalytics: toolbarDocking => {
var _api$core$actions$exe6;
if (fg('platform_editor_use_preferences_plugin')) {
var _api$core$actions$exe5, _api$userPreferences2;
return (_api$core$actions$exe5 = api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : (_api$userPreferences2 = api.userPreferences) === null || _api$userPreferences2 === void 0 ? void 0 : _api$userPreferences2.actions.updateUserPreference('toolbarDockingPosition', toolbarDocking))) !== null && _api$core$actions$exe5 !== void 0 ? _api$core$actions$exe5 : false;
}
return (_api$core$actions$exe6 = api === null || api === void 0 ? void 0 : api.core.actions.execute(forceToolbarDockingWithoutAnalytics({
toolbarDocking,
userPreferencesProvider
}))) !== null && _api$core$actions$exe6 !== void 0 ? _api$core$actions$exe6 : false;
},
refreshToolbarDocking: () => {
if (userPreferencesProvider) {
var _api$core$actions$exe7;
const userToolbarDockingPref = getToolbarDocking(contextualFormattingEnabled, userPreferencesProvider);
return (_api$core$actions$exe7 = api === null || api === void 0 ? void 0 : api.core.actions.execute(updateToolbarDocking({
toolbarDocking: userToolbarDockingPref
}))) !== null && _api$core$actions$exe7 !== void 0 ? _api$core$actions$exe7 : false;
}
return false;
}
},
getSharedState(editorState) {
if (!editorState) {
return;
}
return selectionToolbarPluginKey.getState(editorState);
},
pmPlugins(selectionToolbarHandlers) {
var _api$userPreferences3, _api$userPreferences4, _api$userPreferences5;
if (selectionToolbarHandlers) {
__selectionToolbarHandlers.push(...selectionToolbarHandlers);
}
const initialToolbarDocking = fg('platform_editor_use_preferences_plugin') ? getToolbarDockingV2(contextualFormattingEnabled, api === null || api === void 0 ? void 0 : (_api$userPreferences3 = api.userPreferences) === null || _api$userPreferences3 === void 0 ? void 0 : (_api$userPreferences4 = _api$userPreferences3.sharedState.currentState()) === null || _api$userPreferences4 === void 0 ? void 0 : (_api$userPreferences5 = _api$userPreferences4.preferences) === null || _api$userPreferences5 === void 0 ? void 0 : _api$userPreferences5.toolbarDockingPosition) : getToolbarDocking(contextualFormattingEnabled, userPreferencesProvider);
return [{
name: 'selection-tracker',
plugin: () => {
return new SafePlugin({
key: selectionToolbarPluginKey,
state: {
init() {
return {
selectionStable: false,
hide: false,
toolbarDocking: initialToolbarDocking
};
},
apply(tr, pluginState) {
const meta = tr.getMeta(selectionToolbarPluginKey);
let newPluginState = pluginState;
if (meta) {
return {
...newPluginState,
...meta
};
}
if (editorExperiment('platform_editor_block_menu', true)) {
var _api$userIntent, _api$userIntent$share;
const isBlockMenuOpen = (api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : (_api$userIntent$share = _api$userIntent.sharedState.currentState()) === null || _api$userIntent$share === void 0 ? void 0 : _api$userIntent$share.currentUserIntent) === 'blockMenuOpen';
newPluginState = {
...newPluginState,
isBlockMenuOpen
};
}
// if the toolbarDockingInitialPosition preference has changed
// update the toolbarDocking state
if (!previousToolbarDocking) {
// we currently only check for the initial value
const toolbarDockingPreference = userPreferencesProvider === null || userPreferencesProvider === void 0 ? void 0 : userPreferencesProvider.getPreference('toolbarDockingInitialPosition');
if (toolbarDockingPreference && toolbarDockingPreference !== previousToolbarDocking) {
previousToolbarDocking = toolbarDockingPreference;
const userToolbarDockingPref = getToolbarDocking(contextualFormattingEnabled, userPreferencesProvider);
if (pluginState.toolbarDocking !== userToolbarDockingPref) {
return {
...newPluginState,
toolbarDocking: userToolbarDockingPref
};
}
}
}
return newPluginState;
}
},
view(view) {
const unbind = bind(view.root, {
type: 'mouseup',
listener: event => {
var _api$editorViewMode;
// 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';
const target = event.target;
if (target && target instanceof Element) {
const isRovoChangeToneButton = target.tagName === 'BUTTON' && hasNestedSpanWithText(target, 'Change tone') || target.getAttribute('aria-label') === 'Change tone' || target.innerHTML === 'Change tone';
const isRovoTranslateButton = target.tagName === 'BUTTON' && hasNestedSpanWithText(target, 'Translate options') || target.getAttribute('aria-label') === 'Translate options' || target.innerHTML === 'Translate options';
if (isRovoChangeToneButton || isRovoTranslateButton) {
return null;
}
}
view.dispatch(view.state.tr.setMeta(selectionToolbarPluginKey, {
selectionStable: !isViewModeEnabled ? view.hasFocus() : true
}));
}
});
const unbindEditorViewFocus = bind(view.dom, {
type: 'focus',
listener: () => {
view.dispatch(view.state.tr.setMeta(selectionToolbarPluginKey, {
selectionStable: true
}));
}
});
return {
destroy() {
unbind();
unbindEditorViewFocus();
}
};
},
appendTransaction(_transactions, _oldState, newState) {
if (fg('platform_editor_use_preferences_plugin')) {
return null;
}
if (!isPreferenceInitialized && editorExperiment('platform_editor_controls', 'variant1')) {
const toolbarDockingPreference = userPreferencesProvider === null || userPreferencesProvider === void 0 ? void 0 : userPreferencesProvider.getPreference('toolbarDockingInitialPosition');
if (toolbarDockingPreference !== undefined) {
var _api$analytics2;
isPreferenceInitialized = true;
const userToolbarDockingPref = getToolbarDocking(contextualFormattingEnabled, userPreferencesProvider);
const tr = newState.tr;
api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.attachAnalyticsEvent({
action: ACTION.INITIALISED,
actionSubject: ACTION_SUBJECT.USER_PREFERENCES,
actionSubjectId: ACTION_SUBJECT_ID.SELECTION_TOOLBAR_PREFERENCES,
attributes: {
toolbarDocking: userToolbarDockingPref
},
eventType: EVENT_TYPE.OPERATIONAL
})(tr);
return tr;
}
}
return null;
},
props: {
handleDOMEvents: {
mousedown: view => {
view.dispatch(view.state.tr.setMeta(selectionToolbarPluginKey, {
selectionStable: false
}));
return false;
}
}
}
});
}
}];
},
pluginsOptions: isToolbarAIFCEnabled ? {} : {
floatingToolbar(state, intl, providerFactory) {
const {
selectionStable,
hide,
toolbarDocking,
isBlockMenuOpen
} = selectionToolbarPluginKey.getState(state);
const isCellSelection = ('$anchorCell' in state.selection);
const isEditorControlsEnabled = editorExperiment('platform_editor_controls', 'variant1');
if (state.selection.empty || !selectionStable || hide || state.selection instanceof NodeSelection ||
// $anchorCell is only available in CellSelection, this check is to
// avoid importing CellSelection from @atlaskit/editor-tables
isCellSelection && !isEditorControlsEnabled // for Editor Controls we want to show the toolbar on CellSelection
) {
// If there is no active selection, or the selection is not stable, or the selection is a node selection,
// do not show the toolbar.
return;
}
if (isCellSelection && isEditorControlsEnabled) {
var _api$blockControls, _api$blockControls$sh;
const isSelectedViaDragHandle = 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.isSelectedViaDragHandle;
if (isSelectedViaDragHandle) {
return;
}
}
if (isBlockMenuOpen && isEditorControlsEnabled && editorExperiment('platform_editor_block_menu', true)) {
// If the block menu is open, do not show the selection toolbar.
return;
}
// Resolve the selectionToolbarHandlers to a list of SelectionToolbarGroups
// and filter out any handlers which returned undefined
const resolved = __selectionToolbarHandlers.map(selectionToolbarHandler => selectionToolbarHandler(state, intl, providerFactory)).filter(resolved => resolved !== undefined);
// Sort the groups by rank
// This is intended to allow different plugins to control the order of the groups
// they add to the selection toolbar.
// ie. if you want to have your plugin's group appear first, set rank to -10 if there is currently another
// plugin you expect to be run at the same time as with an rank of -9
resolved.sort(({
rank: rankA = 0
}, {
rank: rankB = 0
}) => {
if (rankA < rankB) {
return 1;
}
return -1;
});
const items = [];
// This flattens the groups passed into the floating toolbar into a single list of items
for (let i = 0; i < resolved.length; i++) {
var _resolved$i;
// add a seperator icon after each group except the last
if (Array.isArray((_resolved$i = resolved[i]) === null || _resolved$i === void 0 ? void 0 : _resolved$i.items)) {
items.push(...resolved[i].items);
}
if (editorExperiment('platform_editor_controls', 'variant1')) {
if (resolved[i] && resolved[i + 1]) {
var _resolved;
if (((_resolved = resolved[i + 1]) === null || _resolved === void 0 ? void 0 : _resolved.pluginName) === 'annotation') {
items.push({
type: 'separator',
fullHeight: true
});
}
}
} else {
if (i !== resolved.length - 1) {
items.push({
type: 'separator'
});
}
}
}
if (items.length > 0 && contextualFormattingEnabled && isEditorControlsEnabled) {
var _api$userPreferences6, _api$userPreferences7, _api$userPreferences8;
const toolbarDockingPref = api !== null && api !== void 0 && api.userPreferences && fg('platform_editor_use_preferences_plugin') ? api === null || api === void 0 ? void 0 : (_api$userPreferences6 = api.userPreferences) === null || _api$userPreferences6 === void 0 ? void 0 : (_api$userPreferences7 = _api$userPreferences6.sharedState.currentState()) === null || _api$userPreferences7 === void 0 ? void 0 : (_api$userPreferences8 = _api$userPreferences7.preferences) === null || _api$userPreferences8 === void 0 ? void 0 : _api$userPreferences8.toolbarDockingPosition : toolbarDocking;
items.push(...getPinOptionToolbarConfig({
api,
toolbarDocking: toolbarDockingPref,
intl
}));
}
let onPositionCalculated;
const toolbarTitle = 'Selection toolbar';
if (isCellSelection && isEditorControlsEnabled) {
onPositionCalculated = calculateToolbarPositionOnCellSelection(toolbarTitle);
} else {
const calcToolbarPosition = config.preferenceToolbarAboveSelection ? calculateToolbarPositionAboveSelection : calculateToolbarPositionTrackHead;
onPositionCalculated = calcToolbarPosition(toolbarTitle);
}
const nodeType = getSelectionNodeTypes(state);
return {
title: 'Selection toolbar',
nodeType: nodeType,
items: items,
...(isEditorControlsEnabled && {
scrollable: true
}),
onPositionCalculated
};
}
},
contentComponent: editorExperiment('platform_editor_controls', 'variant1') && !fg('platform_editor_use_preferences_plugin') && fg('platform_editor_user_preferences_provider_update') ? () => /*#__PURE__*/React.createElement(PageVisibilityWatcher, {
api: api,
userPreferencesProvider: userPreferencesProvider
}) : undefined,
primaryToolbarComponent: !(api !== null && api !== void 0 && api.primaryToolbar) && editorExperiment('platform_editor_controls', 'variant1', {
exposure: true
}) ? primaryToolbarComponent : undefined
};
};
function getSelectionNodeTypes(state) {
const selectionNodeTypes = [];
state.doc.nodesBetween(state.selection.from, state.selection.to, node => {
if (selectionNodeTypes.indexOf(node.type) !== 0) {
selectionNodeTypes.push(node.type);
}
});
return selectionNodeTypes;
}
const hasNestedSpanWithText = (element, text) => {
if (element.tagName === 'SPAN' && element.innerHTML === text) {
return true;
}
for (const child of Array.from(element.children)) {
if (hasNestedSpanWithText(child, text)) {
return true;
}
}
return false;
};