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:
@@ -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>) => {
|
||||
|
@@ -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) {
|
||||
|
@@ -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