diff --git a/packages/editor/src/extensions/behavior/SelectionContext/TextSelectionTooltip.tsx b/packages/editor/src/extensions/behavior/SelectionContext/TextSelectionTooltip.tsx new file mode 100644 index 000000000..bc0dded6b --- /dev/null +++ b/packages/editor/src/extensions/behavior/SelectionContext/TextSelectionTooltip.tsx @@ -0,0 +1,93 @@ +import {useEffect, useMemo, useState} from 'react'; + +import {Popup, type PopupPlacement, type PopupProps, sp} from '@gravity-ui/uikit'; + +import type {ActionStorage} from '#core'; +import type {EditorView} from '#pm/view'; +import {isFunction} from 'src/lodash'; +import {typedMemo} from 'src/react-utils/memo'; +import {Toolbar, type ToolbarData, type ToolbarProps} from 'src/toolbar'; +import {ToolbarWrapToContext} from 'src/toolbar/ToolbarRerender'; + +import type {ContextConfig} from './types'; + +const ToolbarMemoized = typedMemo(Toolbar); +const KEY_SEP = '|||'; + +export type TextSelectionTooltipProps = Pick< + ToolbarProps, + 'onClick' | 'editor' | 'focus' +> & { + config: ContextConfig; + editorView: EditorView; + popupPlacement: PopupPlacement; + popupAnchor: PopupProps['anchorElement']; + popupOnOpenChange: PopupProps['onOpenChange']; +}; + +export const TextSelectionTooltip: React.FC = + function TextSelectionTooltip({ + popupAnchor, + popupPlacement, + popupOnOpenChange, + + config, + focus, + editor, + onClick, + editorView, + }) { + const [conditionKey, setConditionKey] = useState(() => + calcConditionKey(config, editor, editorView), + ); + + useEffect(() => { + const newKey = calcConditionKey(config, editor, editorView); + if (conditionKey !== newKey) setConditionKey(newKey); + }); + + const toolbarData = useMemo>(() => { + const results = conditionKey.split(KEY_SEP); + let idx = 0; + return config + .map((groupData) => groupData.filter(() => results[idx++] === 'true')) + .filter((groupData) => Boolean(groupData.length)); + }, [config, conditionKey]); + + return ( + + + + + + ); + }; + +function calcConditionKey( + config: ContextConfig, + editor: ActionStorage, + editorView: EditorView, +): string { + return config + .flatMap((groupData) => + groupData.map((item) => { + const {condition} = item; + if (condition === 'enabled') return item.isEnable(editor); + if (isFunction(condition)) return condition(editorView.state); + return true; + }), + ) + .join(KEY_SEP); +} diff --git a/packages/editor/src/extensions/behavior/SelectionContext/index.ts b/packages/editor/src/extensions/behavior/SelectionContext/index.ts index 973083e9a..0d900a404 100644 --- a/packages/editor/src/extensions/behavior/SelectionContext/index.ts +++ b/packages/editor/src/extensions/behavior/SelectionContext/index.ts @@ -66,6 +66,7 @@ class SelectionTooltip implements PluginSpec { private destroyed = false; private tooltip: TooltipView; + private editorView: EditorView | null = null; private hideTimeoutRef: ReturnType | null = null; private _isMousePressed = false; @@ -76,7 +77,13 @@ class SelectionTooltip implements PluginSpec { logger: Logger2.ILogger, options: SelectionContextOptions, ) { - this.tooltip = new TooltipView(actions, menuConfig, logger, options); + this.tooltip = new TooltipView(actions, menuConfig, logger, { + ...options, + onPopupOpenChange: (_open, _event, reason) => { + if (reason !== 'escape-key' && this.editorView) + this.scheduleTooltipHiding(this.editorView); + }, + }); } get key(): PluginKey { @@ -140,6 +147,8 @@ class SelectionTooltip implements PluginSpec { } private update(view: EditorView, prevState?: TinyState) { + this.editorView = view; + if (this._isMousePressed) return; this.cancelTooltipHiding(); @@ -185,11 +194,7 @@ class SelectionTooltip implements PluginSpec { return; } - this.tooltip.show(view, { - onOpenChange: (_open, _event, reason) => { - if (reason !== 'escape-key') this.scheduleTooltipHiding(view); - }, - }); + this.tooltip.show(view); } private scheduleTooltipHiding(view: EditorView) { diff --git a/packages/editor/src/extensions/behavior/SelectionContext/tooltip.tsx b/packages/editor/src/extensions/behavior/SelectionContext/tooltip.tsx index 6b6c7a582..e146cd1fa 100644 --- a/packages/editor/src/extensions/behavior/SelectionContext/tooltip.tsx +++ b/packages/editor/src/extensions/behavior/SelectionContext/tooltip.tsx @@ -1,56 +1,23 @@ import type {VirtualElement} from '@floating-ui/react'; -import {Popup, type PopupPlacement, type PopupProps} from '@gravity-ui/uikit'; -import type {EditorState} from 'prosemirror-state'; +import type {PopupPlacement, PopupProps} from '@gravity-ui/uikit'; import type {EditorView} from 'prosemirror-view'; import type {ActionStorage} from '../../../core'; -import {isFunction} from '../../../lodash'; import {type Logger2, globalLogger} from '../../../logger'; import {ErrorLoggerBoundary} from '../../../react-utils/ErrorBoundary'; -import {Toolbar} from '../../../toolbar'; -import type { - ToolbarButtonPopupData, - ToolbarGroupItemData, - ToolbarProps, - ToolbarSingleItemData, -} from '../../../toolbar'; import {type RendererItem, getReactRendererFromState} from '../ReactRenderer'; -type SelectionTooltipBaseProps = { - show?: boolean; - poppupProps: PopupProps; -}; -type SelectionTooltipProps = SelectionTooltipBaseProps & ToolbarProps; - -const SelectionTooltip: React.FC = ({ - show, - poppupProps, - ...toolbarProps -}) => { - if (!show) return null; - return ( - - - - ); -}; +import {TextSelectionTooltip} from './TextSelectionTooltip'; +import type {ContextConfig} from './types'; -export type ContextGroupItemData = - | (ToolbarGroupItemData & { - condition?: (state: EditorState) => void; - }) - | ((ToolbarSingleItemData | ToolbarButtonPopupData) & { - condition?: 'enabled'; - }); - -export type ContextGroupData = ContextGroupItemData[]; -export type ContextConfig = ContextGroupData[]; +export type {ContextGroupItemData, ContextGroupData, ContextConfig} from './types'; export type TooltipViewParams = { /** @default 'bottom' */ placement?: 'top' | 'bottom'; /** @default false */ flip?: boolean; + onPopupOpenChange: PopupProps['onOpenChange']; }; export class TooltipView { @@ -60,9 +27,11 @@ export class TooltipView { private readonly actions: ActionStorage; private readonly menuConfig: ContextConfig; private readonly placement: PopupPlacement; + private readonly onPopupOpenChange: PopupProps['onOpenChange']; private view!: EditorView; - private baseProps: SelectionTooltipBaseProps = {show: false, poppupProps: {}}; + private visible = false; + private anchor: PopupProps['anchorElement'] = undefined; private _tooltipRenderItem: RendererItem | null = null; constructor( @@ -75,24 +44,20 @@ export class TooltipView { this.actions = actions; this.menuConfig = menuConfig; - const {flip, placement = 'bottom'} = params; + const {flip, placement = 'bottom', onPopupOpenChange} = params; this.placement = flip ? placement : [placement]; + this.onPopupOpenChange = onPopupOpenChange; } get isTooltipOpen(): boolean { return this.#isTooltipOpen; } - show(view: EditorView, popupProps?: PopupProps) { + show(view: EditorView) { this.view = view; this.#isTooltipOpen = true; - this.baseProps = { - show: true, - poppupProps: { - ...popupProps, - ...this.calcPosition(view), - }, - }; + this.visible = true; + this.anchor ??= this.createVirtualElement(view); this.renderPopup(); } @@ -100,10 +65,11 @@ export class TooltipView { this.view = view; // do not rerender popup if it is already hidden - if (!this.#isTooltipOpen && !this.baseProps.show) return; + if (!this.#isTooltipOpen && !this.visible) return; this.#isTooltipOpen = false; - this.baseProps = {show: false, poppupProps: {}}; + this.visible = false; + this.anchor = undefined; this.renderPopup(); } @@ -112,38 +78,11 @@ export class TooltipView { this._tooltipRenderItem = null; } - private getSelectionTooltipProps(): SelectionTooltipProps { - return { - ...this.baseProps, - qa: 'g-md-toolbar-selection', - focus: () => this.view.focus(), - data: this.getFilteredConfig(), - editor: this.actions, - onClick: (id) => { - globalLogger.action({mode: 'wysiwyg', source: 'context-menu', action: id}); - this.logger.action({source: 'context-menu', action: id}); - }, - }; - } - - private getFilteredConfig(): ContextConfig { - return this.baseProps.show - ? this.menuConfig - .map((groupData) => - groupData.filter((item) => { - const {condition} = item; - if (condition === 'enabled') { - return item.isEnable(this.actions); - } - if (isFunction(condition)) { - return condition(this.view.state); - } - return true; - }), - ) - .filter((groupData) => Boolean(groupData.length)) - : []; - } + private readonly handleFocus = () => this.view.focus(); + private readonly handleClick = (id: string) => { + globalLogger.action({mode: 'wysiwyg', source: 'context-menu', action: id}); + this.logger.action({source: 'context-menu', action: id}); + }; private renderPopup() { this.tooltipRenderItem.rerender(); @@ -152,17 +91,29 @@ export class TooltipView { private get tooltipRenderItem() { if (!this._tooltipRenderItem) { const reactRenderer = getReactRendererFromState(this.view.state); - this._tooltipRenderItem = reactRenderer.createItem('selection_context', () => ( - - - - )); + this._tooltipRenderItem = reactRenderer.createItem('selection_context', () => { + if (!this.visible) return null; + return ( + + + + ); + }); } return this._tooltipRenderItem; } - private calcPosition(view: EditorView): PopupProps { - const virtualElem: VirtualElement = { + private createVirtualElement(view: EditorView): VirtualElement { + return { getBoundingClientRect() { // These are in screen coordinates const start = view.coordsAtPos(view.state.selection.from); @@ -188,10 +139,5 @@ export class TooltipView { }; }, }; - - return { - placement: this.placement, - anchorElement: virtualElem, - }; } } diff --git a/packages/editor/src/extensions/behavior/SelectionContext/types.ts b/packages/editor/src/extensions/behavior/SelectionContext/types.ts new file mode 100644 index 000000000..e6dbd1316 --- /dev/null +++ b/packages/editor/src/extensions/behavior/SelectionContext/types.ts @@ -0,0 +1,18 @@ +import type {ActionStorage} from '#core'; +import type {EditorState} from '#pm/state'; +import type { + ToolbarButtonPopupData, + ToolbarGroupItemData, + ToolbarSingleItemData, +} from 'src/toolbar'; + +export type ContextGroupItemData = + | (ToolbarGroupItemData & { + condition?: (state: EditorState) => void; + }) + | ((ToolbarSingleItemData | ToolbarButtonPopupData) & { + condition?: 'enabled'; + }); + +export type ContextGroupData = ContextGroupItemData[]; +export type ContextConfig = ContextGroupData[];