1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-22 15:02:51 +02:00

defer native events within Editable to avoid bugs with Editor (#4605)

* defer native events internally to Editable

* add changeset

* suggestions to make DeferredOperation a closure instead of an object type

Co-authored-by: Nemanja Tosic <netosic90@gmail.com>

* fix misapplied suggestion

Co-authored-by: Nemanja Tosic <netosic90@gmail.com>
This commit is contained in:
Jake Donham
2021-10-18 13:05:16 -07:00
committed by GitHub
parent f1607da4ad
commit 87ab2efa41
4 changed files with 15 additions and 87 deletions

View File

@@ -48,7 +48,8 @@ import {
PLACEHOLDER_SYMBOL,
EDITOR_TO_WINDOW,
} from '../utils/weak-maps'
import { asNative, flushNativeEvents } from '../utils/native'
type DeferredOperation = () => void
const Children = (props: Parameters<typeof useChildren>[0]) => (
<React.Fragment>{useChildren(props)}</React.Fragment>
@@ -124,6 +125,7 @@ export const Editable = (props: EditableProps) => {
// Rerender editor when composition status changed
const [isComposing, setIsComposing] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const deferredOperations = useRef<DeferredOperation[]>([])
// Update internal state on each render.
IS_READ_ONLY.set(editor, readOnly)
@@ -433,9 +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(() =>
Editor.insertText(editor, data)
)
} else {
Editor.insertText(editor, data)
}
@@ -622,7 +624,10 @@ 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) {
op()
}
deferredOperations.current = []
}, [])}
onBlur={useCallback(
(event: React.FocusEvent<HTMLDivElement>) => {

View File

@@ -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 = <T extends Editor>(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) {

View File

@@ -1,52 +0,0 @@
import { Editor, Operation } from 'slate'
export const AS_NATIVE: WeakMap<Editor, boolean> = new WeakMap()
export const NATIVE_OPERATIONS: WeakMap<Editor, Operation[]> = 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)
})
})
}
}