mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-24 16:02:55 +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:
5
.changeset/many-baboons-stare.md
Normal file
5
.changeset/many-baboons-stare.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate-react': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
defer native events within Editable to avoid bugs with Editor
|
@@ -48,7 +48,8 @@ import {
|
|||||||
PLACEHOLDER_SYMBOL,
|
PLACEHOLDER_SYMBOL,
|
||||||
EDITOR_TO_WINDOW,
|
EDITOR_TO_WINDOW,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import { asNative, flushNativeEvents } from '../utils/native'
|
|
||||||
|
type DeferredOperation = () => void
|
||||||
|
|
||||||
const Children = (props: Parameters<typeof useChildren>[0]) => (
|
const Children = (props: Parameters<typeof useChildren>[0]) => (
|
||||||
<React.Fragment>{useChildren(props)}</React.Fragment>
|
<React.Fragment>{useChildren(props)}</React.Fragment>
|
||||||
@@ -124,6 +125,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
// Rerender editor when composition status changed
|
// Rerender editor when composition status changed
|
||||||
const [isComposing, setIsComposing] = useState(false)
|
const [isComposing, setIsComposing] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const deferredOperations = useRef<DeferredOperation[]>([])
|
||||||
|
|
||||||
// Update internal state on each render.
|
// Update internal state on each render.
|
||||||
IS_READ_ONLY.set(editor, readOnly)
|
IS_READ_ONLY.set(editor, readOnly)
|
||||||
@@ -433,9 +435,9 @@ export const Editable = (props: EditableProps) => {
|
|||||||
// Only insertText operations use the native functionality, for now.
|
// Only insertText operations use the native functionality, for now.
|
||||||
// Potentially expand to single character deletes, as well.
|
// Potentially expand to single character deletes, as well.
|
||||||
if (native) {
|
if (native) {
|
||||||
asNative(editor, () => Editor.insertText(editor, data), {
|
deferredOperations.current.push(() =>
|
||||||
onFlushed: () => event.preventDefault(),
|
Editor.insertText(editor, data)
|
||||||
})
|
)
|
||||||
} else {
|
} else {
|
||||||
Editor.insertText(editor, data)
|
Editor.insertText(editor, data)
|
||||||
}
|
}
|
||||||
@@ -622,7 +624,10 @@ export const Editable = (props: EditableProps) => {
|
|||||||
// and we can correctly compare DOM text values in components
|
// and we can correctly compare DOM text values in components
|
||||||
// to stop rendering, so that browser functions like autocorrect
|
// to stop rendering, so that browser functions like autocorrect
|
||||||
// and spellcheck work as expected.
|
// and spellcheck work as expected.
|
||||||
flushNativeEvents(editor)
|
for (const op of deferredOperations.current) {
|
||||||
|
op()
|
||||||
|
}
|
||||||
|
deferredOperations.current = []
|
||||||
}, [])}
|
}, [])}
|
||||||
onBlur={useCallback(
|
onBlur={useCallback(
|
||||||
(event: React.FocusEvent<HTMLDivElement>) => {
|
(event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
@@ -8,11 +8,6 @@ import {
|
|||||||
EDITOR_TO_ON_CHANGE,
|
EDITOR_TO_ON_CHANGE,
|
||||||
NODE_TO_KEY,
|
NODE_TO_KEY,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import {
|
|
||||||
AS_NATIVE,
|
|
||||||
NATIVE_OPERATIONS,
|
|
||||||
flushNativeEvents,
|
|
||||||
} from '../utils/native'
|
|
||||||
import {
|
import {
|
||||||
isDOMText,
|
isDOMText,
|
||||||
getPlainText,
|
getPlainText,
|
||||||
@@ -66,31 +61,6 @@ export const withReact = <T extends Editor>(editor: T) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.apply = (op: Operation) => {
|
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][] = []
|
const matches: [Path, Key][] = []
|
||||||
|
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
|
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user