Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/editor/src/bundle/MarkupEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export const MarkupEditorView = memo<MarkupEditorViewProps>((props) => {
hiddenActionsConfig={hiddenActionsConfig}
stickyToolbar={stickyToolbar}
toolbarConfig={toolbarConfig}
toolbarFocus={() => editor.focus()}
settingsVisible={settingsVisible}
className={b('toolbar', [toolbarClassName])}
toolbarDisplay={toolbarDisplay}
Expand Down
83 changes: 58 additions & 25 deletions packages/editor/src/bundle/ToolbarView.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import {useLayoutEffect, useRef} from 'react';
import {type ComponentProps, useCallback, useLayoutEffect, useMemo, useRef} from 'react';

import type {QAProps} from '@gravity-ui/uikit';
import {useUpdate} from 'react-use';

import {LAYOUT} from 'src/common/layout';
import {typedMemo} from 'src/react-utils/memo';
import {EventEmitter} from 'src/utils';

import type {ClassNameProps} from '../classname';
import {i18n} from '../i18n/menubar';
import {useSticky} from '../react-utils/useSticky';
import {FlexToolbar, type ToolbarData, type ToolbarDisplay, type ToolbarItemData} from '../toolbar';
import {
FlexToolbar,
type FlexToolbarProps,
type ToolbarData,
type ToolbarDisplay,
type ToolbarItemData,
ToolbarProvider,
} from '../toolbar';

import type {EditorInt} from './Editor';
import {stickyCn} from './sticky';
import type {MarkdownEditorMode} from './types';

const MemoizedFlexibleToolbar = typedMemo(FlexToolbar);

type ToolbarProviderValue = NonNullable<ComponentProps<typeof ToolbarProvider>['value']>;

export type ToolbarViewProps<T> = ClassNameProps &
QAProps & {
editor: EditorInt;
editorMode: MarkdownEditorMode;
toolbarEditor: T;
toolbarFocus: () => void;
toolbarConfig: ToolbarData<T>;
settingsVisible?: boolean;
hiddenActionsConfig?: ToolbarItemData<T>[];
Expand All @@ -32,7 +43,6 @@ export function ToolbarView<T>({
editor,
editorMode,
toolbarEditor,
toolbarFocus,
toolbarConfig,
toolbarDisplay,
hiddenActionsConfig,
Expand All @@ -42,19 +52,40 @@ export function ToolbarView<T>({
stickyToolbar,
qa,
}: ToolbarViewProps<T>) {
const rerender = useUpdate();
useLayoutEffect(() => {
editor.on('rerender-toolbar', rerender);
return () => {
editor.off('rerender-toolbar', rerender);
};
}, [editor, rerender]);

const wrapperRef = useRef<HTMLDivElement>(null);
const isStickyActive = useSticky(wrapperRef) && stickyToolbar;

const mobile = editor.mobile;

const clickHandle = useCallback<NonNullable<FlexToolbarProps<T>['onClick']>>(
(id, attrs) => editor.emit('toolbar-action', {id, attrs, editorMode}),
[editor, editorMode],
);

const toolbarProviderValue = useMemo(
() =>
({
editor: toolbarEditor,
eventBus: new EventEmitter<{update: null}>(),
}) satisfies ToolbarProviderValue,
[toolbarEditor],
);

const handleFocus = useCallback(() => {
editor.focus();
}, [editor]);

useLayoutEffect(() => {
const onRerender = () => {
toolbarProviderValue.eventBus.emit('update', null);
};

editor.on('rerender-toolbar', onRerender);
return () => {
editor.off('rerender-toolbar', onRerender);
};
}, [editor, toolbarProviderValue]);

return (
<div
data-qa={qa}
Expand All @@ -69,18 +100,20 @@ export function ToolbarView<T>({
)}
data-layout={LAYOUT.STICKY_TOOLBAR}
>
<FlexToolbar
data={toolbarConfig}
hiddenActions={hiddenActionsConfig}
editor={toolbarEditor}
focus={toolbarFocus}
dotsTitle={i18n('more_action')}
onClick={(id, attrs) => editor.emit('toolbar-action', {id, attrs, editorMode})}
display={toolbarDisplay}
disableTooltip={mobile}
disableHotkey={mobile}
disablePreview={mobile}
/>
<ToolbarProvider value={toolbarProviderValue}>
<MemoizedFlexibleToolbar
data={toolbarConfig}
hiddenActions={hiddenActionsConfig}
editor={toolbarEditor}
focus={handleFocus}
dotsTitle={i18n('more_action')}
onClick={clickHandle}
display={toolbarDisplay}
disableTooltip={mobile}
disableHotkey={mobile}
disablePreview={mobile}
/>
</ToolbarProvider>
{children}
</div>
);
Expand Down
7 changes: 2 additions & 5 deletions packages/editor/src/bundle/WysiwygEditorView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {memo} from 'react';

import type {QAProps} from '@gravity-ui/uikit';

import {type ClassNameProps, cn} from '../classname';
Expand Down Expand Up @@ -30,7 +28,7 @@ export type WysiwygEditorViewProps = ClassNameProps &
toolbarDisplay?: ToolbarDisplay;
};

export const WysiwygEditorView = memo<WysiwygEditorViewProps>((props) => {
export const WysiwygEditorView: React.FC<WysiwygEditorViewProps> = (props) => {
const {
editor,
autofocus,
Expand Down Expand Up @@ -69,7 +67,6 @@ export const WysiwygEditorView = memo<WysiwygEditorViewProps>((props) => {
toolbarEditor={editor}
stickyToolbar={stickyToolbar}
toolbarConfig={toolbarConfig}
toolbarFocus={() => editor.focus()}
hiddenActionsConfig={hiddenActionsConfig}
settingsVisible={settingsVisible}
className={b('toolbar', [toolbarClassName])}
Expand All @@ -83,5 +80,5 @@ export const WysiwygEditorView = memo<WysiwygEditorViewProps>((props) => {
</WysiwygEditorComponent>
</div>
);
});
};
WysiwygEditorView.displayName = 'MarkdownWysiwgEditorView';
1 change: 1 addition & 0 deletions packages/editor/src/bundle/config/markup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export const mToolbarConfig: MToolbarData = [
{
id: 'colorify',
type: ToolbarDataType.ReactComponent,
noRerenderOnUpdate: true, // static state in markup mode
component: MToolbarColors,
width: 42,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/bundle/config/wysiwyg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ export const wToolbarConfig: WToolbarData = [
id: 'colorify',
type: ToolbarDataType.ReactComponent,
component: WToolbarColors,
noRerenderOnUpdate: true, // WToolbarColors uses toolbar context to update its own state
width: 42,
},
wLinkItemData,
Expand Down Expand Up @@ -521,6 +522,7 @@ export const wSelectionMenuConfig: SelectionContextConfig = [
id: 'colorify',
type: ToolbarDataType.ReactComponent,
component: WToolbarColors,
noRerenderOnUpdate: true, // WToolbarColors uses toolbar context to update its own state
props: {disablePortal: true} satisfies Partial<WToolbarColorsProps>,
width: 42,
},
Expand Down
59 changes: 56 additions & 3 deletions packages/editor/src/bundle/toolbar/wysiwyg/WToolbarColors.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import {useEffect, useState} from 'react';

import {useLatest} from 'react-use';

import type {ActionStorage} from '#core';
import {isEqual} from 'src/lodash';
import {useToolbarContext} from 'src/toolbar/context';

import {ToolbarColors, type ToolbarColorsProps} from '../custom/ToolbarColors';
import type {WToolbarBaseProps} from '../types';

Expand All @@ -10,13 +18,13 @@ export const WToolbarColors: React.FC<WToolbarColorsProps> = ({
focus,
onClick,
}) => {
const {active, enabled, currentColor} = useColorsState(editor);
const action = editor.actions.colorify;
const currentColor = action.meta();

return (
<ToolbarColors
active={action.isActive()}
enable={action.isEnable()}
active={active}
enable={enabled}
currentColor={currentColor}
exec={(color) => {
action.run({color: color === currentColor ? '' : color});
Expand All @@ -29,3 +37,48 @@ export const WToolbarColors: React.FC<WToolbarColorsProps> = ({
/>
);
};

type ColorsState = {
active: boolean;
enabled: boolean;
currentColor: string;
};

function useColorsState(editor: ActionStorage): ColorsState {
const action = editor.actions.colorify;

const context = useToolbarContext();

const [state, setState] = useState<ColorsState>({
active: false,
enabled: true,
currentColor: '',
});
const stateRef = useLatest(state);

useEffect(() => {
if (!context) return undefined;

const onUpdate = () => {
const newState = {
active: action.isActive(),
enabled: action.isEnable(),
currentColor: action.meta(),
};

if (!isEqual(stateRef.current, newState)) {
setState(newState);
}
};

onUpdate();

context.eventBus.on('update', onUpdate);

return () => {
context.eventBus.off('update', onUpdate);
};
}, [action, context, stateRef]);

return state;
}
2 changes: 2 additions & 0 deletions packages/editor/src/modules/toolbars/items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -776,10 +776,12 @@ export const colorifyItemView: ToolbarItemView<ToolbarDataType.ReactComponent> =
};
export const colorifyItemWysiwyg: ToolbarItemWysiwyg<ToolbarDataType.ReactComponent> = {
component: WToolbarColors,
noRerenderOnUpdate: true, // WToolbarColors uses toolbar context to update its own state
width: 42,
};
export const colorifyItemMarkup: ToolbarItemMarkup<ToolbarDataType.ReactComponent> = {
component: MToolbarColors,
noRerenderOnUpdate: true, // static state in markup mode
width: 42,
};

Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/modules/toolbars/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type ToolbarItemEditor<T, E> = Partial<EditorActions<E>> & {
: T extends ToolbarDataType.ReactComponent
? {
width: number;
noRerenderOnUpdate?: boolean;
component: React.ComponentType<ToolbarBaseProps<E>>;
}
: {});
Expand Down
9 changes: 9 additions & 0 deletions packages/editor/src/react-utils/memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// hack to allow using generic components with memo

import {memo} from 'react';

// DO NOT TRY TO PASS GENERIC PARAMETERS TO THIS FUNCTION!
export const typedMemo: <Props extends object, Return extends React.ReactNode>(
Component: (props: Props) => Return,
compare?: (prevProps: Props, newProps: Props) => boolean,
) => (props: Props) => Return = memo;
49 changes: 28 additions & 21 deletions packages/editor/src/toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Fragment} from 'react';
import {cn} from '../classname';

import {ToolbarButtonGroup} from './ToolbarGroup';
import {ToolbarWrapToContext} from './ToolbarRerender';
import type {ToolbarBaseProps, ToolbarData} from './types';

import './Toolbar.scss';
Expand All @@ -15,6 +16,10 @@ export type ToolbarProps<E> = ToolbarBaseProps<E> & {
data: ToolbarData<E>;
};

/**
The component is not memoized. To optimize number of rerenders,
memoize component yourself and wrap it in a toolbar context (use ToolbarProvider component).
*/
export function Toolbar<E>({
editor,
data,
Expand All @@ -28,26 +33,28 @@ export function Toolbar<E>({
disableTooltip,
}: ToolbarProps<E>) {
return (
<div className={b({display}, [className])} data-qa={qa}>
{data.map<React.ReactNode>((group, index) => {
const isLastGroup = index === data.length - 1;

return (
<Fragment key={index}>
<ToolbarButtonGroup
data={group}
editor={editor}
focus={focus}
onClick={onClick}
className={b('group')}
disableHotkey={disableHotkey}
disablePreview={disablePreview}
disableTooltip={disableTooltip}
/>
{isLastGroup || <div className={b('group-separator')} />}
</Fragment>
);
})}
</div>
<ToolbarWrapToContext editor={editor}>
<div className={b({display}, [className])} data-qa={qa}>
{data.map<React.ReactNode>((group, index) => {
const isLastGroup = index === data.length - 1;

return (
<Fragment key={index}>
<ToolbarButtonGroup
data={group}
editor={editor}
focus={focus}
onClick={onClick}
className={b('group')}
disableHotkey={disableHotkey}
disablePreview={disablePreview}
disableTooltip={disableTooltip}
/>
{isLastGroup || <div className={b('group-separator')} />}
</Fragment>
);
})}
</div>
</ToolbarWrapToContext>
);
}
7 changes: 3 additions & 4 deletions packages/editor/src/toolbar/ToolbarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {i18n} from '../i18n/common';
import {isFunction} from '../lodash';

import {ToolbarTooltipDelay} from './const';
import {useActionState} from './hooks';
import type {ToolbarBaseProps, ToolbarItemData} from './types';

import './ToolbarButton.scss';
Expand Down Expand Up @@ -111,10 +112,8 @@ export const ToolbarButtonView = forwardRef<HTMLButtonElement, ToolbarButtonView
);

export function ToolbarButton<E>(props: ToolbarButtonProps<E>) {
const {id, editor, focus, isActive, isEnable, exec, onClick} = props;

const active = isActive(editor);
const enabled = isEnable(editor);
const {id, editor, focus, exec, onClick} = props;
const {active, enabled} = useActionState(editor, props);

const handleClick = () => {
focus();
Expand Down
Loading
Loading