From 85a5fd8774831fa99aab8e10f032609507eb0b81 Mon Sep 17 00:00:00 2001 From: Jake Donham Date: Fri, 15 Oct 2021 17:38:58 -0700 Subject: [PATCH 1/4] defer native events internally to Editable --- .../slate-react/src/components/editable.tsx | 18 +++++-- packages/slate-react/src/plugin/with-react.ts | 30 ----------- packages/slate-react/src/utils/native.ts | 52 ------------------- 3 files changed, 14 insertions(+), 86 deletions(-) delete mode 100644 packages/slate-react/src/utils/native.ts diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index a9f0f20fef..18239fc5c9 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -48,7 +48,8 @@ import { PLACEHOLDER_SYMBOL, EDITOR_TO_WINDOW, } from '../utils/weak-maps' -import { asNative, flushNativeEvents } from '../utils/native' + +type DeferredOperation = { type: 'insertText'; text: string } const Children = (props: Parameters[0]) => ( {useChildren(props)} @@ -124,6 +125,7 @@ export const Editable = (props: EditableProps) => { // Rerender editor when composition status changed const [isComposing, setIsComposing] = useState(false) const ref = useRef(null) + const deferredOperations = useRef([]) // Update internal state on each render. IS_READ_ONLY.set(editor, readOnly) @@ -433,8 +435,9 @@ export const Editable = (props: EditableProps) => { // Only insertText operations use the native functionality, for now. // Potentially expand to single character deletes, as well. if (native) { - asNative(editor, () => Editor.insertText(editor, data), { - onFlushed: () => event.preventDefault(), + deferredOperations.current.push({ + type: 'insertText', + text: data, }) } else { Editor.insertText(editor, data) @@ -622,7 +625,14 @@ export const Editable = (props: EditableProps) => { // and we can correctly compare DOM text values in components // to stop rendering, so that browser functions like autocorrect // and spellcheck work as expected. - flushNativeEvents(editor) + for (const op of deferredOperations.current) { + switch (op.type) { + case 'insertText': + Editor.insertText(editor, op.text) + break + } + } + deferredOperations.current = [] }, [])} onBlur={useCallback( (event: React.FocusEvent) => { diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts index d73609731f..e2fbd57ffb 100644 --- a/packages/slate-react/src/plugin/with-react.ts +++ b/packages/slate-react/src/plugin/with-react.ts @@ -8,11 +8,6 @@ import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY, } from '../utils/weak-maps' -import { - AS_NATIVE, - NATIVE_OPERATIONS, - flushNativeEvents, -} from '../utils/native' import { isDOMText, getPlainText, @@ -66,31 +61,6 @@ export const withReact = (editor: T) => { } e.apply = (op: Operation) => { - // if we're NOT an insert_text and there's a queue - // of native events, bail out and flush the queue. - // otherwise transforms as part of this cycle will - // be incorrect. - // - // This is needed as overriden operations (e.g. `insertText`) - // can call additional transforms, which will need accurate - // content, and will be called _before_ `onInput` is fired. - if (op.type !== 'insert_text') { - AS_NATIVE.set(editor, false) - flushNativeEvents(editor) - } - - // If we're in native mode, queue the operation - // and it will be applied later. - if (AS_NATIVE.get(editor)) { - const nativeOps = NATIVE_OPERATIONS.get(editor) - if (nativeOps) { - nativeOps.push(op) - } else { - NATIVE_OPERATIONS.set(editor, [op]) - } - return - } - const matches: [Path, Key][] = [] switch (op.type) { diff --git a/packages/slate-react/src/utils/native.ts b/packages/slate-react/src/utils/native.ts deleted file mode 100644 index 12a2857d66..0000000000 --- a/packages/slate-react/src/utils/native.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Editor, Operation } from 'slate' - -export const AS_NATIVE: WeakMap = new WeakMap() -export const NATIVE_OPERATIONS: WeakMap = new WeakMap() - -/** - * `asNative` queues operations as native, meaning native browser events will - * not have been prevented, and we need to flush the operations - * after the native events have propogated to the DOM. - * @param {Editor} editor - Editor on which the operations are being applied - * @param {callback} fn - Function containing .exec calls which will be queued as native - */ -export const asNative = ( - editor: Editor, - fn: () => void, - { onFlushed }: { onFlushed?: () => void } = {} -) => { - const isNative = AS_NATIVE.get(editor) - - AS_NATIVE.set(editor, true) - try { - fn() - } finally { - if (isNative !== undefined) { - AS_NATIVE.set(editor, isNative) - } - } - - if (!NATIVE_OPERATIONS.get(editor)) { - onFlushed?.() - } -} - -/** - * `flushNativeEvents` applies any queued native events. - * @param {Editor} editor - Editor on which the operations are being applied - */ -export const flushNativeEvents = (editor: Editor) => { - const nativeOps = NATIVE_OPERATIONS.get(editor) - - // Clear list _before_ applying, as we might flush - // events in each op, as well. - NATIVE_OPERATIONS.delete(editor) - - if (nativeOps) { - Editor.withoutNormalizing(editor, () => { - nativeOps.forEach(op => { - editor.apply(op) - }) - }) - } -} From 6be79cd0f5b75c64cbba85d6d4ff8e69f46b27a1 Mon Sep 17 00:00:00 2001 From: Jake Donham Date: Fri, 15 Oct 2021 17:43:02 -0700 Subject: [PATCH 2/4] add changeset --- .changeset/many-baboons-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-baboons-stare.md diff --git a/.changeset/many-baboons-stare.md b/.changeset/many-baboons-stare.md new file mode 100644 index 0000000000..04ef868908 --- /dev/null +++ b/.changeset/many-baboons-stare.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +defer native events within Editable to avoid bugs with Editor From f76803a9b74f734c7a8e5b757329025dd213e333 Mon Sep 17 00:00:00 2001 From: Jake Donham Date: Sun, 17 Oct 2021 15:02:57 -0700 Subject: [PATCH 3/4] suggestions to make DeferredOperation a closure instead of an object type Co-authored-by: Nemanja Tosic --- packages/slate-react/src/components/editable.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 18239fc5c9..49b721811f 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -49,7 +49,7 @@ import { EDITOR_TO_WINDOW, } from '../utils/weak-maps' -type DeferredOperation = { type: 'insertText'; text: string } +type DeferredOperation = () => void; const Children = (props: Parameters[0]) => ( {useChildren(props)} @@ -435,9 +435,7 @@ export const Editable = (props: EditableProps) => { // Only insertText operations use the native functionality, for now. // Potentially expand to single character deletes, as well. if (native) { - deferredOperations.current.push({ - type: 'insertText', - text: data, + deferredOperations.current.push(() => Editor.insertText(editor, data) }) } else { Editor.insertText(editor, data) From a8c431e7025f04bbc478f3a5ad718ba060095855 Mon Sep 17 00:00:00 2001 From: Jake Donham Date: Sun, 17 Oct 2021 15:07:48 -0700 Subject: [PATCH 4/4] fix misapplied suggestion --- packages/slate-react/src/components/editable.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 49b721811f..f0c45fe62f 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -49,7 +49,7 @@ import { EDITOR_TO_WINDOW, } from '../utils/weak-maps' -type DeferredOperation = () => void; +type DeferredOperation = () => void const Children = (props: Parameters[0]) => ( {useChildren(props)} @@ -435,8 +435,9 @@ export const Editable = (props: EditableProps) => { // Only insertText operations use the native functionality, for now. // Potentially expand to single character deletes, as well. if (native) { - deferredOperations.current.push(() => Editor.insertText(editor, data) - }) + deferredOperations.current.push(() => + Editor.insertText(editor, data) + ) } else { Editor.insertText(editor, data) } @@ -624,11 +625,7 @@ export const Editable = (props: EditableProps) => { // to stop rendering, so that browser functions like autocorrect // and spellcheck work as expected. for (const op of deferredOperations.current) { - switch (op.type) { - case 'insertText': - Editor.insertText(editor, op.text) - break - } + op() } deferredOperations.current = [] }, [])}