Skip to content

Commit

Permalink
Merge pull request #2214 from epam/improve-editor-focus
Browse files Browse the repository at this point in the history
[PlateEditor] Improve editor focus
  • Loading branch information
AlekseyManetov authored May 6, 2024
2 parents 03ff2a0 + d838116 commit 3f47089
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 87 deletions.
2 changes: 1 addition & 1 deletion uui-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@udecode/plate-reset-node": "31.0.0",
"@udecode/plate-resizable": "31.0.0",
"@udecode/plate-serializer-docx": "31.4.1",
"@udecode/plate-serializer-html": "31.1.0",
"@udecode/plate-serializer-html": "31.4.4",
"@udecode/plate-serializer-md": "31.4.0",
"@udecode/plate-table": "31.4.1",
"@udecode/slate": "31.0.0",
Expand Down
163 changes: 85 additions & 78 deletions uui-editor/src/SlateEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, {
FocusEventHandler, Fragment, KeyboardEventHandler, useMemo, useRef,
FocusEventHandler, forwardRef, Fragment, KeyboardEventHandler, memo, useCallback, useMemo, useRef,
} from 'react';
import {
IEditable, IHasCX, IHasRawProps, cx, useForceUpdate, uuiMod,
IEditable, IHasCX, IHasRawProps, cx, uuiMod, useForceUpdate,
} from '@epam/uui-core';
import { ScrollBars } from '@epam/uui';
import {
Plate, PlateContent, PlateEditor, PlatePlugin, Value, createPlugins, useEditorState, useEventEditorSelectors,
Plate, PlateContent, PlateEditor, PlatePlugin, Value, createPlugins, useComposedRef,
} from '@udecode/plate-common';

import { createPlateUI } from './components';
Expand All @@ -18,13 +18,16 @@ import { defaultPlugins } from './defaultPlugins';

import css from './SlateEditor.module.scss';
import { isEditorValueEmpty } from './helpers';
import { useFocusEvents } from './plugins/eventEditorPlugin/eventEditorPlugin';

const basePlugins: PlatePlugin[] = [
...baseMarksPlugin(),
...defaultPlugins,
];

interface SlateEditorProps extends IEditable<EditorValue>, IHasCX, IHasRawProps<React.HTMLAttributes<HTMLDivElement>> {
const disabledPlugins = { insertData: true };

interface PlateEditorProps extends IEditable<EditorValue>, IHasCX, IHasRawProps<React.HTMLAttributes<HTMLDivElement>> {
isReadonly?: boolean;
plugins?: any[];
autoFocus?: boolean;
Expand All @@ -39,80 +42,83 @@ interface SlateEditorProps extends IEditable<EditorValue>, IHasCX, IHasRawProps<
toolbarPosition?: 'floating' | 'fixed';
}

interface PlateEditorProps extends SlateEditorProps {
id: string,
}

function Editor(props: PlateEditorProps) {
const editor = useEditorState();
const focusedEditorId = useEventEditorSelectors.focus();
const isFocused = editor.id === focusedEditorId;

const renderEditor = () => (
<Fragment>
<PlateContent
id={ props.id }
autoFocus={ props.autoFocus }
readOnly={ props.isReadonly }
className={ css.editor }
onKeyDown={ props.onKeyDown }
onBlur={ props.onBlur }
onFocus={ props.onFocus }
placeholder={ isEditorValueEmpty(editor.children) ? props.placeholder : undefined }
style={ { minHeight: props.minHeight } }
/>
<Toolbars toolbarPosition={ props.toolbarPosition } />
</Fragment>
);

return (
<div
className={ cx(
'uui-typography',
props.cx,
css.container,
css['mode-' + (props.mode || 'form')],
(!props.isReadonly && isFocused) && uuiMod.focus,
props.isReadonly && uuiMod.readonly,
props.scrollbars && css.withScrollbars,
props.fontSize === '16' ? 'uui-typography-size-16' : 'uui-typography-size-14',
) }
{ ...props.rawProps }
>
{ props.scrollbars
? (
<ScrollBars cx={ css.scrollbars }>
{ renderEditor() }
</ScrollBars>
)
: renderEditor()}
</div>
);
}

function SlateEditor(props: SlateEditorProps) {
const SlateEditor = memo(forwardRef<HTMLDivElement, PlateEditorProps>((props, ref) => {
const currentId = useRef(String(Date.now()));
const editor = useRef<PlateEditor | null>(null);
const editorRef = useRef<PlateEditor | null>(null);
const editableWrapperRef = useRef<HTMLDivElement>();

/** config */
const plugins = useMemo(
() => {
return createPlugins((props.plugins || [paragraphPlugin()]).flat(), { components: createPlateUI() });
},
() => createPlugins((props.plugins || [paragraphPlugin()]).flat(), { components: createPlateUI() }),
[props.plugins],
);

const onChange = (value: Value) => {
if (props.isReadonly) return;
props?.onValueChange(value);
};
/** value */
const value = useMemo(() => { return migrateSchema(props.value); }, [props.value]);
const onChange = useCallback((v: Value) => {
if (props.isReadonly) {
return;
}
props.onValueChange(v);
}, [props.isReadonly, props.onValueChange]);

/** styles */
const contentStyle = useMemo(() => ({ minHeight: props.minHeight }), [props.minHeight]);
const editorWrapperClassNames = useMemo(() => cx(
'uui-typography',
props.cx,
css.container,
css['mode-' + (props.mode || 'form')],
props.isReadonly && uuiMod.readonly,
props.scrollbars && css.withScrollbars,
props.fontSize === '16' ? 'uui-typography-size-16' : 'uui-typography-size-14',
), [props.cx, props.fontSize, props.isReadonly, props.mode, props.scrollbars]);

/** focus management */
/** TODO: move to plate */
useFocusEvents({ editorId: currentId.current, editorWrapperRef: editableWrapperRef, isReadonly: props.isReadonly });
const autoFocusRef = useCallback((node: HTMLDivElement) => {
if (!editableWrapperRef.current && node) {
editableWrapperRef.current = node;

const value = useMemo(() => {
return migrateSchema(props.value);
}, [props.value]);
if (!props.isReadonly && props.autoFocus) {
editableWrapperRef.current.classList.add(uuiMod.focus);
}
}
return editableWrapperRef;
}, [props.autoFocus, props.isReadonly]);

/** render related */
const renderEditable = useCallback(() => {
return (
<Fragment>
<PlateContent
id={ currentId.current }
autoFocus={ props.autoFocus }
readOnly={ props.isReadonly }
className={ css.editor }
onKeyDown={ props.onKeyDown }
onBlur={ props.onBlur }
onFocus={ props.onFocus }
placeholder={ editorRef.current
&& isEditorValueEmpty(editorRef.current.children)
? props.placeholder : undefined }
style={ contentStyle }
/>
<Toolbars toolbarPosition={ props.toolbarPosition } />
</Fragment>
);
}, [props.autoFocus, contentStyle, props.isReadonly, props.onBlur, props.onFocus, props.onKeyDown, props.placeholder, props.toolbarPosition]);

/** could not be memoized, since slate is uncontrolled component */
const editorContent = props.scrollbars
? <ScrollBars cx={ css.scrollbars }>{ renderEditable() }</ScrollBars>
: renderEditable();

/** force update of uncontrolled component */
const forceUpdate = useForceUpdate();
if (value && editor.current?.children && editor.current.children !== value) {
editor.current.children = value;
if (value && editorRef.current && editorRef.current.children !== value) {
editorRef.current.children = value;
forceUpdate();
}

Expand All @@ -122,17 +128,18 @@ function SlateEditor(props: SlateEditorProps) {
initialValue={ value }
plugins={ plugins }
onChange={ onChange }
editorRef={ editor }
// we override plate core insertData plugin
// so, we need to disable default implementation
disableCorePlugins={ { insertData: true } }
editorRef={ editorRef }
disableCorePlugins={ disabledPlugins }
>
<Editor
id={ currentId.current }
{ ...props }
/>
<div
ref={ useComposedRef(autoFocusRef, ref) }
className={ editorWrapperClassNames }
{ ...props.rawProps }
>
{editorContent}
</div>
</Plate>
);
}
}));

export { SlateEditor, basePlugins };
2 changes: 2 additions & 0 deletions uui-editor/src/defaultPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
noteTypes,
} from './plugins';
import { createAutoformatPlugin } from './plugins/autoformatPlugin/autoformatPlugin';
import { createEventEditorPlugin } from './plugins/eventEditorPlugin/eventEditorPlugin';

const resetBlockTypesCommonRule = {
types: [
Expand Down Expand Up @@ -50,4 +51,5 @@ export const defaultPlugins = [
createResetNodePlugin(resetBlockTypePlugin),
createSoftBreakPlugin(),
createAutoformatPlugin(),
createEventEditorPlugin(),
];
2 changes: 1 addition & 1 deletion uui-editor/src/implementation/PositionedToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface ToolbarProps {

export function FloatingToolbar(props: ToolbarProps): any {
const ref = useRef<HTMLElement | null>();
const editor = useEditorState();
const editor = useEditorState(); // TODO: use useEditorRef
const inFocus = useEventEditorSelectors.focus() === editor.id;
const zIndex = useLayer()?.zIndex;

Expand Down
2 changes: 1 addition & 1 deletion uui-editor/src/implementation/StickyToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface SidebarProps {

// eslint-disable-next-line react/function-component-definition
export const StickyToolbar: React.FC<SidebarProps> = ({ isReadonly, children }) => {
const editor = useEditorState();
const editor = useEditorState(); // TODO: use useEditorRef
const isBlockSelected = isBlock(editor, editor.value);
const [isVisible, setIsVisible] = useState(false);
const sidebarRef = useRef<HTMLDivElement>(null);
Expand Down
69 changes: 69 additions & 0 deletions uui-editor/src/plugins/eventEditorPlugin/eventEditorPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createPluginFactory, eventEditorActions, eventEditorSelectors, KEY_EVENT_EDITOR } from '@udecode/plate-core';
import { MutableRefObject, useEffect } from 'react';
import { uuiMod } from '@epam/uui-core';

export const FOCUS_EDITOR_EVENT = 'uui-focus-editor';
export const BLUR_EDITOR_EVENT = 'uui-blur-editor';

// TODO: move to plate
export const createEventEditorPlugin = createPluginFactory({
key: KEY_EVENT_EDITOR,
handlers: {
onFocus: (editor) => () => {
eventEditorActions.focus(editor.id);

document.dispatchEvent(
new CustomEvent(FOCUS_EDITOR_EVENT, {
detail: { id: editor.id },
}),
);
},
onBlur: (editor) => () => {
const focus = eventEditorSelectors.focus();
if (focus === editor.id) {
eventEditorActions.focus(null);
}
eventEditorActions.blur(editor.id);

document.dispatchEvent(
new CustomEvent(BLUR_EDITOR_EVENT, {
detail: { id: editor.id },
}),
);
},
},
});

export const useFocusEvents = ({
editorWrapperRef,
editorId,
isReadonly,
}: {
editorWrapperRef: MutableRefObject<HTMLDivElement>
editorId: string,
isReadonly?: boolean,
}) => {
useEffect(() => {
const onFocusEditor = (event: Event) => {
const id = (event as any).detail.id;
const allowFocus = editorWrapperRef.current && !isReadonly && editorId === id;
if (allowFocus) {
editorWrapperRef.current.classList.add(uuiMod.focus);
}
};
const onBlurEditor = (event: Event) => {
const id = (event as any).detail.id;
if (editorWrapperRef.current && editorId === id) {
editorWrapperRef.current.classList.remove(uuiMod.focus);
}
};

document.addEventListener(FOCUS_EDITOR_EVENT, onFocusEditor);
document.addEventListener(BLUR_EDITOR_EVENT, onBlurEditor);

return () => {
document.removeEventListener(FOCUS_EDITOR_EVENT, onFocusEditor);
document.removeEventListener(BLUR_EDITOR_EVENT, onBlurEditor);
};
}, [editorId, editorWrapperRef, isReadonly]);
};
2 changes: 1 addition & 1 deletion uui-editor/src/plugins/tablePlugin/MergeToolbarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { collapseSelection, useEditorState } from '@udecode/plate-common';
import { mergeTableCells } from '@udecode/plate-table';

export function MergeToolbarContent() {
const editor = useEditorState();
const editor = useEditorState(); // TODO: use useEditorRef

return (
<ToolbarButton
Expand Down
2 changes: 1 addition & 1 deletion uui-editor/src/plugins/tablePlugin/ToolbarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function StyledRemoveTable() {
}

export function TableToolbarContent({ canUnmerge }:{ canUnmerge:boolean }) {
const editor = useEditorState();
const editor = useEditorState(); // TODO: use useEditorRef

const { cell, row } = getTableEntries(editor) || {};
const cellPath = useMemo(() => cell && cell[1], [cell]);
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4461,10 +4461,10 @@
"@udecode/plate-table" "31.4.1"
validator "^13.11.0"

"@udecode/plate-serializer-html@31.1.0":
version "31.1.0"
resolved "https://registry.yarnpkg.com/@udecode/plate-serializer-html/-/plate-serializer-html-31.1.0.tgz#0de99beb121e0a795220cf5cd88ab98b9b9f8bd5"
integrity sha512-UsCdNQiGWeJzr81Nssqvv9ykIdbn1KTl+zBHMESZJ6GLtM2/E0I6I9+hTl0JxcVPUYfJk8bEuN49ewc2pnnpwQ==
"@udecode/plate-serializer-html@31.4.4":
version "31.4.4"
resolved "https://registry.yarnpkg.com/@udecode/plate-serializer-html/-/plate-serializer-html-31.4.4.tgz#2deed14da06c5720c303ffa02af70a742350e516"
integrity sha512-6pwyHJuhQIXFPHrxaezlFr0RNNQGxBtEXJmkriLMYyMtg86yf7d1euHzsypqN9QIrk0I2KRFib/kH1o1E8YyYw==
dependencies:
html-entities "^2.5.2"

Expand Down

0 comments on commit 3f47089

Please sign in to comment.