@atlaskit/editor-plugin-floating-toolbar
Version:
Floating toolbar plugin for @atlaskit/editor-core
592 lines (578 loc) • 25.9 kB
JavaScript
import React from 'react';
import camelCase from 'lodash/camelCase';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, CONTENT_COMPONENT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { isSSR } from '@atlaskit/editor-common/core-utils';
import { ErrorBoundary } from '@atlaskit/editor-common/error-boundary';
import {
// Deprecated API - Look at removing this sometime in the future
useSharedPluginState } from '@atlaskit/editor-common/hooks';
import { WithProviders } from '@atlaskit/editor-common/provider-factory';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check';
import { Popup } from '@atlaskit/editor-common/ui';
import { AllSelection, PluginKey, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findDomRefAtPos, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { copyNode } from './pm-plugins/commands';
import forceFocusPlugin, { forceFocusSelector } from './pm-plugins/force-focus';
import { hideConfirmDialog } from './pm-plugins/toolbar-data/commands';
import { createPlugin as floatingToolbarDataPluginFactory } from './pm-plugins/toolbar-data/plugin';
import { pluginKey as dataPluginKey } from './pm-plugins/toolbar-data/plugin-key';
import { findNode } from './pm-plugins/utils';
import { ConfirmationModal } from './ui/ConfirmationModal';
import Toolbar from './ui/Toolbar';
import { consolidateOverflowDropdownItems } from './ui/utils';
const SUPPRESS_TOOLBAR_USER_INTENTS = ['dragging', 'tableContextualMenuPopupOpen', 'tableDragMenuPopupOpen', 'commenting', 'resizing', 'blockMenuOpen', 'statusPickerOpen'];
// TODO: AFP-2532 - Fix automatic suppressions below
export const getRelevantConfig = (selection, configs) => {
// node selections always take precedence, see if
let configPair;
configs.find(config => {
const node = findSelectedNodeOfType(config.nodeType)(selection);
if (node) {
configPair = {
node: node.node,
pos: node.pos,
config
};
}
return !!node;
});
if (configPair) {
return configPair;
}
// create mapping of node type name to configs
const configByNodeType = {};
configs.forEach(config => {
if (Array.isArray(config.nodeType)) {
config.nodeType.forEach(nodeType => {
configByNodeType[nodeType.name] = config;
});
} else {
configByNodeType[config.nodeType.name] = config;
}
});
// search up the tree from selection
const {
$from
} = selection;
for (let i = $from.depth; i > 0; i--) {
const node = $from.node(i);
const matchedConfig = configByNodeType[node.type.name];
if (matchedConfig) {
return {
config: matchedConfig,
node: node,
pos: $from.pos
};
}
}
// if it is AllSelection (can be result of Cmd+A) - use first node
if (selection instanceof AllSelection) {
const docNode = $from.node(0);
let matchedConfig = null;
const firstChild = findNode(docNode, node => {
matchedConfig = configByNodeType[node.type.name];
return !!matchedConfig;
});
if (firstChild && matchedConfig) {
return {
config: matchedConfig,
node: firstChild,
pos: $from.pos
};
}
}
return;
};
const getDomRefFromSelection = (view, dispatchAnalyticsEvent) => {
try {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
return findDomRefAtPos(view.state.selection.from, view.domAtPos.bind(view));
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error) {
// eslint-disable-next-line no-console
console.warn(error);
if (dispatchAnalyticsEvent) {
const payload = {
action: ACTION.ERRORED,
actionSubject: ACTION_SUBJECT.CONTENT_COMPONENT,
eventType: EVENT_TYPE.OPERATIONAL,
attributes: {
component: CONTENT_COMPONENT.FLOATING_TOOLBAR,
selection: view.state.selection.toJSON(),
position: view.state.selection.from,
docSize: view.state.doc.nodeSize,
error: error.toString(),
// @ts-expect-error - Object literal may only specify known properties, 'errorStack' does not exist in type
// This error was introduced after upgrading to TypeScript 5
errorStack: error.stack || undefined
}
};
dispatchAnalyticsEvent(payload);
}
}
};
function filterUndefined(x) {
return !!x;
}
export const floatingToolbarPlugin = ({
api
}) => {
return {
name: 'floatingToolbar',
pmPlugins(floatingToolbarHandlers = []) {
const plugins = [{
// Should be after all toolbar plugins
name: 'floatingToolbar',
plugin: ({
providerFactory,
getIntl
}) => floatingToolbarPluginFactory({
api,
floatingToolbarHandlers,
providerFactory,
getIntl
})
}, {
name: 'floatingToolbarData',
plugin: ({
dispatch
}) => floatingToolbarDataPluginFactory(dispatch)
}, {
name: 'forceFocus',
plugin: () => forceFocusPlugin()
}];
return plugins;
},
actions: {
forceFocusSelector
},
commands: {
copyNode: (nodeType, inputMethod) => {
var _api$analytics;
return copyNode(nodeType, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions, inputMethod);
}
},
getSharedState(editorState) {
var _api$interaction, _api$interaction$shar, _pluginKey$getState$g, _pluginKey$getState, _pluginKey$getState$g2;
if (!editorState) {
return undefined;
}
const interactionState = api === null || api === void 0 ? void 0 : (_api$interaction = api.interaction) === null || _api$interaction === void 0 ? void 0 : (_api$interaction$shar = _api$interaction.sharedState.currentState()) === null || _api$interaction$shar === void 0 ? void 0 : _api$interaction$shar.interactionState;
const configWithNodeInfo = interactionState !== 'hasNotHadInteraction' ? (_pluginKey$getState$g = (_pluginKey$getState = pluginKey.getState(editorState)) === null || _pluginKey$getState === void 0 ? void 0 : (_pluginKey$getState$g2 = _pluginKey$getState.getConfigWithNodeInfo) === null || _pluginKey$getState$g2 === void 0 ? void 0 : _pluginKey$getState$g2.call(_pluginKey$getState, editorState)) !== null && _pluginKey$getState$g !== void 0 ? _pluginKey$getState$g : undefined : undefined;
return {
configWithNodeInfo,
floatingToolbarData: dataPluginKey.getState(editorState)
};
},
contentComponent({
popupsMountPoint,
popupsBoundariesElement,
popupsScrollableElement,
editorView,
providerFactory,
dispatchAnalyticsEvent
}) {
if (!editorView) {
return null;
}
return /*#__PURE__*/React.createElement(ContentComponent, {
editorView: editorView,
pluginInjectionApi: api,
popupsMountPoint: popupsMountPoint,
popupsBoundariesElement: popupsBoundariesElement,
popupsScrollableElement: popupsScrollableElement,
providerFactory: providerFactory,
dispatchAnalyticsEvent: dispatchAnalyticsEvent
});
}
};
};
/**
* React component that renders the floating toolbar UI for the editor.
*
* This component manages the display of floating toolbars based on the current editor state,
* selection, and configuration. It handles visibility conditions, positioning, toolbar items
* consolidation, and confirmation dialogs.
*
* @param props - Component properties
* @param props.pluginInjectionApi - Plugin injection API for accessing other plugin states
* @param props.editorView - ProseMirror EditorView instance
* @param props.popupsMountPoint - DOM element where popups should be mounted
* @param props.popupsBoundariesElement - Element that defines popup boundaries
* @param props.popupsScrollableElement - Scrollable container element for popups
* @param props.providerFactory - Factory for creating various providers
* @param props.dispatchAnalyticsEvent - Function to dispatch analytics events
* @returns JSX element representing the floating toolbar or null if not visible
*/
export function ContentComponent({
pluginInjectionApi,
editorView,
popupsMountPoint,
popupsBoundariesElement,
popupsScrollableElement,
providerFactory,
dispatchAnalyticsEvent
}) {
var _configWithNodeInfo$c, _configWithNodeInfo$c2, _items, _pluginInjectionApi$c, _pluginInjectionApi$d, _confirmButtonItem, _confirmButtonItem2, _confirmButtonItem3;
const {
floatingToolbarState,
editorDisabledState,
editorViewModeState,
userIntentState
} = useSharedPluginState(pluginInjectionApi, ['floatingToolbar', 'editorDisabled', 'editorViewMode', 'userIntent']);
const {
configWithNodeInfo,
floatingToolbarData
} = floatingToolbarState !== null && floatingToolbarState !== void 0 ? floatingToolbarState : {};
if (isSSR()) {
return null;
}
if (!configWithNodeInfo || !configWithNodeInfo.config || typeof ((_configWithNodeInfo$c = configWithNodeInfo.config) === null || _configWithNodeInfo$c === void 0 ? void 0 : _configWithNodeInfo$c.visible) !== 'undefined' && !((_configWithNodeInfo$c2 = configWithNodeInfo.config) !== null && _configWithNodeInfo$c2 !== void 0 && _configWithNodeInfo$c2.visible)) {
return null;
}
if (userIntentState !== null && userIntentState !== void 0 && userIntentState.currentUserIntent && SUPPRESS_TOOLBAR_USER_INTENTS.includes(userIntentState === null || userIntentState === void 0 ? void 0 : userIntentState.currentUserIntent)) {
return null;
}
const {
config,
node
} = configWithNodeInfo;
// When the new inline editor-toolbar is enabled, suppress floating toolbar for text selections.
if (Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar)) {
const selection = editorView.state.selection;
const isCellSelection = '$anchorCell' in selection && !selection.empty;
const isTextSelected = selection instanceof TextSelection && !selection.empty;
// don't dismiss table toolbar when a cell selection is caused by clicking the drag handle (which has it's own userIntent)
if (isTextSelected && config.className !== 'hyperlink-floating-toolbar' || isCellSelection && (userIntentState === null || userIntentState === void 0 ? void 0 : userIntentState.currentUserIntent) === 'default') {
return null;
}
}
let {
items
} = config;
const {
groupLabel
} = config;
const {
title,
getDomRef = getDomRefFromSelection,
align = 'center',
className = '',
height,
width,
zIndex,
offset = [0, 12],
forcePlacement,
preventPopupOverflow,
onPositionCalculated,
absoluteOffset = {
top: 0,
left: 0,
right: 0,
bottom: 0
},
focusTrap,
mediaAssistiveMessage = '',
stick = true
} = config;
const targetRef = getDomRef(editorView, dispatchAnalyticsEvent);
const isEditorDisabled = editorDisabledState && editorDisabledState.editorDisabled;
const isInViewMode = (editorViewModeState === null || editorViewModeState === void 0 ? void 0 : editorViewModeState.mode) === 'view';
if (!targetRef || isEditorDisabled && !isInViewMode) {
return null;
}
// TODO: MODES-3950 - Update this view mode specific logic once we refactor view mode.
// We should inverse the responsibility here: A blacklist for toolbar items in view mode, rather than this white list.
// Also consider moving this logic to the more specific toolbar plugins (media and selection).
const iterableItems = Array.isArray(items) ? items : (_items = items) === null || _items === void 0 ? void 0 : _items(node);
if (isInViewMode) {
// Typescript note: Not all toolbar item types have the `supportsViewMode` prop.
const toolbarItemViewModeProp = 'supportsViewMode';
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- Ignored via go/ees017 (to be fixed)
items = iterableItems.filter(item => toolbarItemViewModeProp in item && !!item[toolbarItemViewModeProp]);
}
if (areToolbarFlagsEnabled(Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar))) {
var _items2;
// Consolidate floating toolbar items
const toolbarItemsArray = Array.isArray(items) ? items : (_items2 = items) === null || _items2 === void 0 ? void 0 : _items2(node);
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- Ignored via go/ees017 (to be fixed)
const overflowDropdownItems = toolbarItemsArray.filter(item => item.type === 'overflow-dropdown');
if (overflowDropdownItems.length > 1) {
const consolidatedOverflowDropdown = consolidateOverflowDropdownItems(overflowDropdownItems);
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- Ignored via go/ees017 (to be fixed)
const otherItems = toolbarItemsArray.filter(item => item.type !== 'overflow-dropdown');
if (otherItems.length > 0) {
// remove the last separators
while (((_otherItems$at = otherItems.at(-1)) === null || _otherItems$at === void 0 ? void 0 : _otherItems$at.type) === 'separator') {
var _otherItems$at;
otherItems.pop();
}
}
items = [...otherItems, {
type: 'separator',
fullHeight: true,
supportsViewMode: true
}, consolidatedOverflowDropdown];
}
// Apply analytics to dropdown
if (overflowDropdownItems.length > 0 && dispatchAnalyticsEvent) {
var _items3;
const currentItems = Array.isArray(items) ? items : (_items3 = items) === null || _items3 === void 0 ? void 0 : _items3(node);
const updatedItems = currentItems.map(item => {
if (item.type !== 'overflow-dropdown') {
return item;
}
const originalOnClick = item.onClick;
return {
...item,
onClick: () => {
var _pluginInjectionApi$e, _pluginInjectionApi$e2;
const editorContentMode = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e = pluginInjectionApi.editorViewMode) === null || _pluginInjectionApi$e === void 0 ? void 0 : (_pluginInjectionApi$e2 = _pluginInjectionApi$e.sharedState.currentState()) === null || _pluginInjectionApi$e2 === void 0 ? void 0 : _pluginInjectionApi$e2.mode;
dispatchAnalyticsEvent({
action: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.BUTTON,
actionSubjectId: ACTION_SUBJECT_ID.FLOATING_TOOLBAR_OVERFLOW,
eventType: EVENT_TYPE.UI,
attributes: {
editorContentMode
}
});
// Call original onClick if it exists
originalOnClick === null || originalOnClick === void 0 ? void 0 : originalOnClick();
}
};
});
items = updatedItems;
}
}
let customPositionCalculation;
const toolbarItems = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.copyButton) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.actions.processCopyButtonItems(editorView.state)(Array.isArray(items) ? items : items(node), pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$d = pluginInjectionApi.decorations) === null || _pluginInjectionApi$d === void 0 ? void 0 : _pluginInjectionApi$d.actions.hoverDecoration);
if (onPositionCalculated) {
customPositionCalculation = nextPos => {
return onPositionCalculated(editorView, nextPos);
};
}
const dispatchCommand = fn => fn && fn(editorView.state, editorView.dispatch, editorView);
// Confirm dialog
let confirmButtonItem;
const {
confirmDialogForItem,
confirmDialogForItemOption
} = floatingToolbarData || {};
const matchingItem = confirmDialogForItem ? toolbarItems === null || toolbarItems === void 0 ? void 0 : toolbarItems[confirmDialogForItem] : undefined;
if ((matchingItem === null || matchingItem === void 0 ? void 0 : matchingItem.type) === 'button') {
confirmButtonItem = matchingItem;
}
if ((matchingItem === null || matchingItem === void 0 ? void 0 : matchingItem.type) === 'overflow-dropdown' && confirmDialogForItemOption !== undefined) {
const matchingItemOption = matchingItem.options[confirmDialogForItemOption];
// OverflowDropdownOption is the only member of the union that does not have a 'type' property
if (!('type' in matchingItemOption)) {
confirmButtonItem = matchingItemOption;
}
}
const scrollable = config.scrollable;
const confirmDialogOptions = typeof ((_confirmButtonItem = confirmButtonItem) === null || _confirmButtonItem === void 0 ? void 0 : _confirmButtonItem.confirmDialog) === 'function' ? (_confirmButtonItem2 = confirmButtonItem) === null || _confirmButtonItem2 === void 0 ? void 0 : _confirmButtonItem2.confirmDialog() : (_confirmButtonItem3 = confirmButtonItem) === null || _confirmButtonItem3 === void 0 ? void 0 : _confirmButtonItem3.confirmDialog;
return /*#__PURE__*/React.createElement(ErrorBoundary, {
component: ACTION_SUBJECT.FLOATING_TOOLBAR_PLUGIN,
componentId: camelCase(title),
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
fallbackComponent: null
}, /*#__PURE__*/React.createElement(Popup, {
ariaLabel: title,
role: 'toolbar',
offset: offset,
target: targetRef,
alignY: "bottom",
forcePlacement: forcePlacement,
fitHeight: height,
fitWidth: width,
absoluteOffset: absoluteOffset,
alignX: align,
stick: stick,
zIndex: zIndex,
mountTo: popupsMountPoint,
boundariesElement: popupsBoundariesElement,
scrollableElement: popupsScrollableElement,
onPositionCalculated: customPositionCalculation
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
,
style: scrollable ? {
maxWidth: '100%'
} : {},
focusTrap: focusTrap,
preventOverflow: preventPopupOverflow
}, /*#__PURE__*/React.createElement(WithProviders, {
providerFactory: providerFactory
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
providers: ['extensionProvider']
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
renderNode: providers => {
return /*#__PURE__*/React.createElement(Toolbar, {
target: targetRef
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
,
items: toolbarItems,
groupLabel: groupLabel,
node: node,
dispatchCommand: dispatchCommand,
editorView: editorView
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: className
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
focusEditor: () => editorView.focus(),
providerFactory: providerFactory,
popupsMountPoint: popupsMountPoint,
popupsBoundariesElement: popupsBoundariesElement,
popupsScrollableElement: popupsScrollableElement,
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
extensionsProvider: providers.extensionProvider,
scrollable: scrollable,
api: pluginInjectionApi,
mediaAssistiveMessage: mediaAssistiveMessage
});
}
})), /*#__PURE__*/React.createElement(ConfirmationModal, {
testId: "ak-floating-toolbar-confirmation-modal",
options: confirmDialogOptions
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onConfirm: (isChecked = false) => {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!!confirmDialogOptions.onConfirm) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dispatchCommand(confirmDialogOptions.onConfirm(isChecked));
} else {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dispatchCommand(confirmButtonItem.onClick);
}
}
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onClose: () => {
dispatchCommand(hideConfirmDialog());
// Need to set focus to Editor here,
// As when the Confirmation dialog pop up, and user interacts with the dialog, Editor loses focus.
// So when Confirmation dialog is closed, Editor does not have the focus, then cursor goes to the position 1 of the doc,
// instead of the cursor position before the dialog pop up.
if (!editorView.hasFocus()) {
editorView.focus();
}
}
}));
}
/**
*
* ProseMirror Plugin
*
*/
// We throttle update of this plugin with RAF.
// So from other plugins you will always get the previous state.
export const pluginKey = new PluginKey('floatingToolbarPluginKey');
/**
* Clean up floating toolbar configs from undesired properties.
*/
function sanitizeFloatingToolbarConfig(config) {
// Cleanup from non existing node types
if (Array.isArray(config.nodeType)) {
return {
...config,
nodeType: config.nodeType.filter(filterUndefined)
};
}
return config;
}
/**
* Creates a floating toolbar plugin for the ProseMirror editor.
*
* This factory function creates a SafePlugin that manages floating toolbars in the editor.
* It processes an array of floating toolbar handlers and determines which toolbar configuration
* should be active based on the current editor state and selection.
*
* @param options - Configuration object for the floating toolbar plugin
* @param options.floatingToolbarHandlers - Array of handlers that return toolbar configurations
* @param options.getIntl - Function that returns the IntlShape instance for internationalization
* @param options.providerFactory - Factory for creating various providers used by the editor
* @returns A SafePlugin instance that manages floating toolbar state and behavior
*/
export function floatingToolbarPluginFactory(options) {
const {
floatingToolbarHandlers,
providerFactory,
getIntl,
api
} = options;
const intl = getIntl();
const getConfigWithNodeInfo = editorState => {
let activeConfigs = [];
for (let index = 0; index < floatingToolbarHandlers.length; index++) {
const handler = floatingToolbarHandlers[index];
const config = handler(editorState, intl, providerFactory, activeConfigs);
if (config) {
var _api$userIntent, _api$userIntent$share;
if (SUPPRESS_TOOLBAR_USER_INTENTS.includes((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) || '')) {
activeConfigs = undefined;
break;
}
activeConfigs.push(sanitizeFloatingToolbarConfig(config));
}
}
const relevantConfig = activeConfigs && getRelevantConfig(editorState.selection, activeConfigs);
return relevantConfig;
};
const getIsToolbarSuppressed = () => {
return false;
};
const apply = () => {
const newPluginState = {
getConfigWithNodeInfo
};
return newPluginState;
};
return new SafePlugin({
key: pluginKey,
state: {
init: () => {
return {
getConfigWithNodeInfo
};
},
apply: expValEquals('platform_editor_lovability_suppress_toolbar_event', 'isEnabled', true) ? (_tr, _pluginState, __oldEditorState) => {
const suppressedToolbar = getIsToolbarSuppressed();
const newPluginState = {
getConfigWithNodeInfo,
suppressedToolbar
};
return newPluginState;
} : apply
},
view: expValEquals('platform_editor_lovability_suppress_toolbar_event', 'isEnabled', true) ? () => {
return {
update: (view, prevState) => {
const pluginState = pluginKey.getState(view.state);
const prevPluginState = pluginKey.getState(prevState);
if (pluginState !== null && pluginState !== void 0 && pluginState.suppressedToolbar && !(prevPluginState !== null && prevPluginState !== void 0 && prevPluginState.suppressedToolbar)) {
var _api$analytics2, _api$analytics2$actio;
api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : (_api$analytics2$actio = _api$analytics2.actions) === null || _api$analytics2$actio === void 0 ? void 0 : _api$analytics2$actio.fireAnalyticsEvent({
action: ACTION.SUPPRESSED,
actionSubject: ACTION_SUBJECT.FLOATING_TOOLBAR_PLUGIN,
actionSubjectId: ACTION_SUBJECT_ID.FLOATING_TOOLBAR,
eventType: EVENT_TYPE.TRACK
});
}
}
};
} : undefined
});
}