UNPKG

@atlaskit/editor-plugin-selection-toolbar

Version:

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

392 lines (388 loc) 21.2 kB
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; };