1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-12 10:14:02 +02:00

enable eslint hooks rules (#5363)

This commit is contained in:
Anthony Ciccarello
2023-03-17 10:45:54 -07:00
committed by GitHub
parent 556a4565d2
commit d42cd005db
15 changed files with 264 additions and 195 deletions

View File

@@ -0,0 +1,11 @@
---
'slate-react': minor
---
update dependencies on react hooks to be more senstive to changes
The code should now meet eslint react hook standards
This could result in more renders
closes #3886

View File

@@ -4,7 +4,8 @@
"plugin:import/typescript", "plugin:import/typescript",
"prettier", "prettier",
"prettier/@typescript-eslint", "prettier/@typescript-eslint",
"prettier/react" "prettier/react",
"plugin:react-hooks/recommended"
], ],
"plugins": ["@typescript-eslint", "import", "react", "prettier"], "plugins": ["@typescript-eslint", "import", "react", "prettier"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
@@ -19,7 +20,7 @@
"settings": { "settings": {
"import/extensions": [".js", ".ts", ".jsx", ".tsx"], "import/extensions": [".js", ".ts", ".jsx", ".tsx"],
"react": { "react": {
"version": "detect" "version": "16"
} }
}, },
"env": { "env": {

View File

@@ -74,6 +74,7 @@
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1", "eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.16.0", "eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^4.6.0",
"faker": "^4.1.0", "faker": "^4.1.0",
"image-extensions": "^1.1.0", "image-extensions": "^1.1.0",
"is-hotkey": "^0.1.6", "is-hotkey": "^0.1.6",

View File

@@ -69,6 +69,7 @@ import {
import { RestoreDOM } from './restore-dom/restore-dom' import { RestoreDOM } from './restore-dom/restore-dom'
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager' import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
import { useTrackUserInput } from '../hooks/use-track-user-input' import { useTrackUserInput } from '../hooks/use-track-user-input'
import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager'
type DeferredOperation = () => void type DeferredOperation = () => void
@@ -181,70 +182,82 @@ export const Editable = (props: EditableProps) => {
} }
}, [autoFocus]) }, [autoFocus])
/**
* The AndroidInputManager object has a cyclical dependency on onDOMSelectionChange
*
* It is defined as a reference to simplify hook dependencies and clarify that
* it needs to be initialized.
*/
const androidInputManagerRef = useRef<
AndroidInputManager | null | undefined
>()
// Listen on the native `selectionchange` event to be able to update any time // Listen on the native `selectionchange` event to be able to update any time
// the selection changes. This is required because React's `onSelect` is leaky // the selection changes. This is required because React's `onSelect` is leaky
// and non-standard so it doesn't fire until after a selection has been // and non-standard so it doesn't fire until after a selection has been
// released. This causes issues in situations where another change happens // released. This causes issues in situations where another change happens
// while a selection is being dragged. // while a selection is being dragged.
const onDOMSelectionChange = useCallback( const onDOMSelectionChange = useMemo(
throttle(() => { () =>
if ( throttle(() => {
(IS_ANDROID || !ReactEditor.isComposing(editor)) && const androidInputManager = androidInputManagerRef.current
(!state.isUpdatingSelection || androidInputManager?.isFlushing()) && if (
!state.isDraggingInternally (IS_ANDROID || !ReactEditor.isComposing(editor)) &&
) { (!state.isUpdatingSelection || androidInputManager?.isFlushing()) &&
const root = ReactEditor.findDocumentOrShadowRoot(editor) !state.isDraggingInternally
const { activeElement } = root ) {
const el = ReactEditor.toDOMNode(editor, editor) const root = ReactEditor.findDocumentOrShadowRoot(editor)
const domSelection = root.getSelection() const { activeElement } = root
const el = ReactEditor.toDOMNode(editor, editor)
const domSelection = root.getSelection()
if (activeElement === el) { if (activeElement === el) {
state.latestElement = activeElement state.latestElement = activeElement
IS_FOCUSED.set(editor, true) IS_FOCUSED.set(editor, true)
} else { } else {
IS_FOCUSED.delete(editor) IS_FOCUSED.delete(editor)
} }
if (!domSelection) { if (!domSelection) {
return Transforms.deselect(editor) return Transforms.deselect(editor)
} }
const { anchorNode, focusNode } = domSelection const { anchorNode, focusNode } = domSelection
const anchorNodeSelectable = const anchorNodeSelectable =
ReactEditor.hasEditableTarget(editor, anchorNode) || ReactEditor.hasEditableTarget(editor, anchorNode) ||
ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode) ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode)
const focusNodeSelectable = const focusNodeSelectable =
ReactEditor.hasEditableTarget(editor, focusNode) || ReactEditor.hasEditableTarget(editor, focusNode) ||
ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode) ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode)
if (anchorNodeSelectable && focusNodeSelectable) { if (anchorNodeSelectable && focusNodeSelectable) {
const range = ReactEditor.toSlateRange(editor, domSelection, { const range = ReactEditor.toSlateRange(editor, domSelection, {
exactMatch: false, exactMatch: false,
suppressThrow: true, suppressThrow: true,
}) })
if (range) { if (range) {
if ( if (
!ReactEditor.isComposing(editor) && !ReactEditor.isComposing(editor) &&
!androidInputManager?.hasPendingChanges() && !androidInputManager?.hasPendingChanges() &&
!androidInputManager?.isFlushing() !androidInputManager?.isFlushing()
) { ) {
Transforms.select(editor, range) Transforms.select(editor, range)
} else { } else {
androidInputManager?.handleUserSelect(range) androidInputManager?.handleUserSelect(range)
}
} }
} }
}
// Deselect the editor if the dom selection is not selectable in readonly mode // Deselect the editor if the dom selection is not selectable in readonly mode
if (readOnly && (!anchorNodeSelectable || !focusNodeSelectable)) { if (readOnly && (!anchorNodeSelectable || !focusNodeSelectable)) {
Transforms.deselect(editor) Transforms.deselect(editor)
}
} }
} }, 100),
}, 100), [editor, readOnly, state]
[readOnly]
) )
const scheduleOnDOMSelectionChange = useMemo( const scheduleOnDOMSelectionChange = useMemo(
@@ -252,7 +265,7 @@ export const Editable = (props: EditableProps) => {
[onDOMSelectionChange] [onDOMSelectionChange]
) )
const androidInputManager = useAndroidInputManager({ androidInputManagerRef.current = useAndroidInputManager({
node: ref, node: ref,
onDOMSelectionChange, onDOMSelectionChange,
scheduleOnDOMSelectionChange, scheduleOnDOMSelectionChange,
@@ -278,7 +291,7 @@ export const Editable = (props: EditableProps) => {
if ( if (
!domSelection || !domSelection ||
!ReactEditor.isFocused(editor) || !ReactEditor.isFocused(editor) ||
androidInputManager?.hasPendingAction() androidInputManagerRef.current?.hasPendingAction()
) { ) {
return return
} }
@@ -376,7 +389,8 @@ export const Editable = (props: EditableProps) => {
} }
const newDomRange = setDomSelection() const newDomRange = setDomSelection()
const ensureSelection = androidInputManager?.isFlushing() === 'action' const ensureSelection =
androidInputManagerRef.current?.isFlushing() === 'action'
if (!IS_ANDROID || !ensureSelection) { if (!IS_ANDROID || !ensureSelection) {
setTimeout(() => { setTimeout(() => {
@@ -444,8 +458,8 @@ export const Editable = (props: EditableProps) => {
!isDOMEventHandled(event, propsOnDOMBeforeInput) !isDOMEventHandled(event, propsOnDOMBeforeInput)
) { ) {
// COMPAT: BeforeInput events aren't cancelable on android, so we have to handle them differently using the android input manager. // COMPAT: BeforeInput events aren't cancelable on android, so we have to handle them differently using the android input manager.
if (androidInputManager) { if (androidInputManagerRef.current) {
return androidInputManager.handleDOMBeforeInput(event) return androidInputManagerRef.current.handleDOMBeforeInput(event)
} }
// Some IMEs/Chrome extensions like e.g. Grammarly set the selection immediately before // Some IMEs/Chrome extensions like e.g. Grammarly set the selection immediately before
@@ -699,7 +713,14 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly, propsOnDOMBeforeInput] [
editor,
onDOMSelectionChange,
onUserInput,
propsOnDOMBeforeInput,
readOnly,
scheduleOnDOMSelectionChange,
]
) )
const callbackRef = useCallback( const callbackRef = useCallback(
@@ -728,7 +749,12 @@ export const Editable = (props: EditableProps) => {
ref.current = node ref.current = node
}, },
[ref, onDOMBeforeInput, onDOMSelectionChange, scheduleOnDOMSelectionChange] [
onDOMSelectionChange,
scheduleOnDOMSelectionChange,
editor,
onDOMBeforeInput,
]
) )
// Attach a native DOM event handler for `selectionchange`, because React's // Attach a native DOM event handler for `selectionchange`, because React's
@@ -899,27 +925,30 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly] [attributes.onBeforeInput, editor, readOnly]
)} )}
onInput={useCallback((event: React.FormEvent<HTMLDivElement>) => { onInput={useCallback(
if (isEventHandled(event, attributes.onInput)) { (event: React.FormEvent<HTMLDivElement>) => {
return if (isEventHandled(event, attributes.onInput)) {
} return
}
if (androidInputManager) { if (androidInputManagerRef.current) {
androidInputManager.handleInput() androidInputManagerRef.current.handleInput()
return return
} }
// Flush native operations, as native events will have propogated // Flush native operations, as native events will have propogated
// 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.
for (const op of deferredOperations.current) { for (const op of deferredOperations.current) {
op() op()
} }
deferredOperations.current = [] deferredOperations.current = []
}, [])} },
[attributes.onInput]
)}
onBlur={useCallback( onBlur={useCallback(
(event: React.FocusEvent<HTMLDivElement>) => { (event: React.FocusEvent<HTMLDivElement>) => {
if ( if (
@@ -984,7 +1013,13 @@ export const Editable = (props: EditableProps) => {
IS_FOCUSED.delete(editor) IS_FOCUSED.delete(editor)
}, },
[readOnly, attributes.onBlur] [
readOnly,
state.isUpdatingSelection,
state.latestElement,
editor,
attributes.onBlur,
]
)} )}
onClick={useCallback( onClick={useCallback(
(event: React.MouseEvent<HTMLDivElement>) => { (event: React.MouseEvent<HTMLDivElement>) => {
@@ -1045,7 +1080,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly, attributes.onClick] [editor, attributes.onClick, readOnly]
)} )}
onCompositionEnd={useCallback( onCompositionEnd={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => { (event: React.CompositionEvent<HTMLDivElement>) => {
@@ -1055,7 +1090,7 @@ export const Editable = (props: EditableProps) => {
IS_COMPOSING.set(editor, false) IS_COMPOSING.set(editor, false)
} }
androidInputManager?.handleCompositionEnd(event) androidInputManagerRef.current?.handleCompositionEnd(event)
if ( if (
isEventHandled(event, attributes.onCompositionEnd) || isEventHandled(event, attributes.onCompositionEnd) ||
@@ -1097,7 +1132,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[attributes.onCompositionEnd] [attributes.onCompositionEnd, editor]
)} )}
onCompositionUpdate={useCallback( onCompositionUpdate={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => { (event: React.CompositionEvent<HTMLDivElement>) => {
@@ -1111,12 +1146,12 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[attributes.onCompositionUpdate] [attributes.onCompositionUpdate, editor]
)} )}
onCompositionStart={useCallback( onCompositionStart={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => { (event: React.CompositionEvent<HTMLDivElement>) => {
if (ReactEditor.hasSelectableTarget(editor, event.target)) { if (ReactEditor.hasSelectableTarget(editor, event.target)) {
androidInputManager?.handleCompositionStart(event) androidInputManagerRef.current?.handleCompositionStart(event)
if ( if (
isEventHandled(event, attributes.onCompositionStart) || isEventHandled(event, attributes.onCompositionStart) ||
@@ -1151,7 +1186,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[attributes.onCompositionStart] [attributes.onCompositionStart, editor]
)} )}
onCopy={useCallback( onCopy={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => { (event: React.ClipboardEvent<HTMLDivElement>) => {
@@ -1168,7 +1203,7 @@ export const Editable = (props: EditableProps) => {
) )
} }
}, },
[attributes.onCopy] [attributes.onCopy, editor]
)} )}
onCut={useCallback( onCut={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => { (event: React.ClipboardEvent<HTMLDivElement>) => {
@@ -1198,7 +1233,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly, attributes.onCut] [readOnly, editor, attributes.onCut]
)} )}
onDragOver={useCallback( onDragOver={useCallback(
(event: React.DragEvent<HTMLDivElement>) => { (event: React.DragEvent<HTMLDivElement>) => {
@@ -1216,7 +1251,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[attributes.onDragOver] [attributes.onDragOver, editor]
)} )}
onDragStart={useCallback( onDragStart={useCallback(
(event: React.DragEvent<HTMLDivElement>) => { (event: React.DragEvent<HTMLDivElement>) => {
@@ -1247,7 +1282,7 @@ export const Editable = (props: EditableProps) => {
) )
} }
}, },
[readOnly, attributes.onDragStart] [readOnly, editor, attributes.onDragStart, state]
)} )}
onDrop={useCallback( onDrop={useCallback(
(event: React.DragEvent<HTMLDivElement>) => { (event: React.DragEvent<HTMLDivElement>) => {
@@ -1290,7 +1325,7 @@ export const Editable = (props: EditableProps) => {
state.isDraggingInternally = false state.isDraggingInternally = false
}, },
[readOnly, attributes.onDrop] [readOnly, editor, attributes.onDrop, state]
)} )}
onDragEnd={useCallback( onDragEnd={useCallback(
(event: React.DragEvent<HTMLDivElement>) => { (event: React.DragEvent<HTMLDivElement>) => {
@@ -1308,7 +1343,7 @@ export const Editable = (props: EditableProps) => {
// Note: `onDragEnd` is only called when `onDrop` is not called // Note: `onDragEnd` is only called when `onDrop` is not called
state.isDraggingInternally = false state.isDraggingInternally = false
}, },
[readOnly, attributes.onDragEnd] [readOnly, state, attributes, editor]
)} )}
onFocus={useCallback( onFocus={useCallback(
(event: React.FocusEvent<HTMLDivElement>) => { (event: React.FocusEvent<HTMLDivElement>) => {
@@ -1333,7 +1368,7 @@ export const Editable = (props: EditableProps) => {
IS_FOCUSED.set(editor, true) IS_FOCUSED.set(editor, true)
} }
}, },
[readOnly, attributes.onFocus] [readOnly, state, editor, attributes.onFocus]
)} )}
onKeyDown={useCallback( onKeyDown={useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -1341,7 +1376,7 @@ export const Editable = (props: EditableProps) => {
!readOnly && !readOnly &&
ReactEditor.hasEditableTarget(editor, event.target) ReactEditor.hasEditableTarget(editor, event.target)
) { ) {
androidInputManager?.handleKeyDown(event) androidInputManagerRef.current?.handleKeyDown(event)
const { nativeEvent } = event const { nativeEvent } = event
@@ -1608,7 +1643,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly, attributes.onKeyDown] [readOnly, editor, attributes.onKeyDown]
)} )}
onPaste={useCallback( onPaste={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => { (event: React.ClipboardEvent<HTMLDivElement>) => {
@@ -1634,7 +1669,7 @@ export const Editable = (props: EditableProps) => {
} }
} }
}, },
[readOnly, attributes.onPaste] [readOnly, editor, attributes.onPaste]
)} )}
> >
<Children <Children

View File

@@ -83,7 +83,7 @@ const Leaf = (props: {
return () => { return () => {
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor) EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
} }
}, [placeholderRef, leaf]) }, [placeholderRef, leaf, editor])
let children = ( let children = (
<String isLast={isLast} leaf={leaf} parent={parent} text={text} /> <String isLast={isLast} leaf={leaf} parent={parent} text={text} />

View File

@@ -5,7 +5,7 @@ import { FocusedContext } from '../hooks/use-focused'
import { EditorContext } from '../hooks/use-slate-static' import { EditorContext } from '../hooks/use-slate-static'
import { SlateContext, SlateContextValue } from '../hooks/use-slate' import { SlateContext, SlateContextValue } from '../hooks/use-slate'
import { import {
getSelectorContext, useSelectorContext,
SlateSelectorContext, SlateSelectorContext,
} from '../hooks/use-slate-selector' } from '../hooks/use-slate-selector'
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps' import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
@@ -47,7 +47,7 @@ export const Slate = (props: {
const { const {
selectorContext, selectorContext,
onChange: handleSelectorChange, onChange: handleSelectorChange,
} = getSelectorContext(editor) } = useSelectorContext(editor)
const onContextChange = useCallback(() => { const onContextChange = useCallback(() => {
if (onChange) { if (onChange) {
@@ -59,7 +59,7 @@ export const Slate = (props: {
editor, editor,
})) }))
handleSelectorChange(editor) handleSelectorChange(editor)
}, [onChange]) }, [editor, handleSelectorChange, onChange])
useEffect(() => { useEffect(() => {
EDITOR_TO_ON_CHANGE.set(editor, onContextChange) EDITOR_TO_ON_CHANGE.set(editor, onContextChange)
@@ -68,13 +68,13 @@ export const Slate = (props: {
EDITOR_TO_ON_CHANGE.set(editor, () => {}) EDITOR_TO_ON_CHANGE.set(editor, () => {})
unmountRef.current = true unmountRef.current = true
} }
}, [onContextChange]) }, [editor, onContextChange])
const [isFocused, setIsFocused] = useState(ReactEditor.isFocused(editor)) const [isFocused, setIsFocused] = useState(ReactEditor.isFocused(editor))
useEffect(() => { useEffect(() => {
setIsFocused(ReactEditor.isFocused(editor)) setIsFocused(ReactEditor.isFocused(editor))
}) }, [editor])
useIsomorphicLayoutEffect(() => { useIsomorphicLayoutEffect(() => {
const fn = () => setIsFocused(ReactEditor.isFocused(editor)) const fn = () => setIsFocused(ReactEditor.isFocused(editor))

View File

@@ -22,34 +22,33 @@ const MUTATION_OBSERVER_CONFIG: MutationObserverInit = {
characterData: true, characterData: true,
} }
export function useAndroidInputManager({ export const useAndroidInputManager = !IS_ANDROID
node, ? () => null
...options : ({ node, ...options }: UseAndroidInputManagerOptions) => {
}: UseAndroidInputManagerOptions) { if (!IS_ANDROID) {
if (!IS_ANDROID) { return null
return null }
}
const editor = useSlateStatic() const editor = useSlateStatic()
const isMounted = useIsMounted() const isMounted = useIsMounted()
const [inputManager] = useState(() => const [inputManager] = useState(() =>
createAndroidInputManager({ createAndroidInputManager({
editor, editor,
...options, ...options,
}) })
) )
useMutationObserver( useMutationObserver(
node, node,
inputManager.handleDomMutations, inputManager.handleDomMutations,
MUTATION_OBSERVER_CONFIG MUTATION_OBSERVER_CONFIG
) )
EDITOR_TO_SCHEDULE_FLUSH.set(editor, inputManager.scheduleFlush) EDITOR_TO_SCHEDULE_FLUSH.set(editor, inputManager.scheduleFlush)
if (isMounted) { if (isMounted) {
inputManager.flush() inputManager.flush()
} }
return inputManager return inputManager
} }

View File

@@ -23,5 +23,5 @@ export function useMutationObserver(
mutationObserver.observe(node.current, options) mutationObserver.observe(node.current, options)
return () => mutationObserver.disconnect() return () => mutationObserver.disconnect()
}, []) }, [mutationObserver, node, options])
} }

View File

@@ -112,17 +112,22 @@ export function useSlateSelector<T>(
/** /**
* Create selector context with editor updating on every editor change * Create selector context with editor updating on every editor change
*/ */
export function getSelectorContext(editor: Editor) { export function useSelectorContext(editor: Editor) {
const eventListeners = useRef<EditorChangeHandler[]>([]).current const eventListeners = useRef<EditorChangeHandler[]>([]).current
const slateRef = useRef<{ const slateRef = useRef<{
editor: Editor editor: Editor
}>({ }>({
editor, editor,
}).current }).current
const onChange = useCallback((editor: Editor) => { const onChange = useCallback(
slateRef.editor = editor (editor: Editor) => {
eventListeners.forEach((listener: EditorChangeHandler) => listener(editor)) slateRef.editor = editor
}, []) eventListeners.forEach((listener: EditorChangeHandler) =>
listener(editor)
)
},
[eventListeners, slateRef]
)
const selectorContext = useMemo(() => { const selectorContext = useMemo(() => {
return { return {

View File

@@ -21,7 +21,7 @@ export function useTrackUserInput() {
animationFrameIdRef.current = window.requestAnimationFrame(() => { animationFrameIdRef.current = window.requestAnimationFrame(() => {
receivedUserInput.current = false receivedUserInput.current = false
}) })
}, []) }, [editor])
useEffect(() => () => cancelAnimationFrame(animationFrameIdRef.current), []) useEffect(() => () => cancelAnimationFrame(animationFrameIdRef.current), [])

View File

@@ -1,4 +1,4 @@
import React from 'react' import React, { useEffect } from 'react'
import { createEditor, Element, Transforms } from 'slate' import { createEditor, Element, Transforms } from 'slate'
import { create, act, ReactTestRenderer } from 'react-test-renderer' import { create, act, ReactTestRenderer } from 'react-test-renderer'
import { Slate, withReact, Editable } from '../src' import { Slate, withReact, Editable } from '../src'
@@ -22,7 +22,7 @@ describe('slate-react', () => {
test('should not unmount the node that gets split on a split_node operation', async () => { test('should not unmount the node that gets split on a split_node operation', async () => {
const editor = withReact(createEditor()) const editor = withReact(createEditor())
const value = [{ type: 'block', children: [{ text: 'test' }] }] const value = [{ type: 'block', children: [{ text: 'test' }] }]
const mounts = jest.fn<void, [Element]>() const mounts = jest.fn()
let el: ReactTestRenderer let el: ReactTestRenderer
@@ -30,8 +30,8 @@ describe('slate-react', () => {
el = create( el = create(
<Slate editor={editor} value={value} onChange={() => {}}> <Slate editor={editor} value={value} onChange={() => {}}>
<Editable <Editable
renderElement={({ element, children }) => { renderElement={({ children }) => {
React.useEffect(() => mounts(element), []) useEffect(() => mounts(), [])
return children return children
}} }}
@@ -56,7 +56,7 @@ describe('slate-react', () => {
{ type: 'block', children: [{ text: 'te' }] }, { type: 'block', children: [{ text: 'te' }] },
{ type: 'block', children: [{ text: 'st' }] }, { type: 'block', children: [{ text: 'st' }] },
] ]
const mounts = jest.fn<void, [Element]>() const mounts = jest.fn()
let el: ReactTestRenderer let el: ReactTestRenderer
@@ -64,8 +64,8 @@ describe('slate-react', () => {
el = create( el = create(
<Slate editor={editor} value={value} onChange={() => {}}> <Slate editor={editor} value={value} onChange={() => {}}>
<Editable <Editable
renderElement={({ element, children }) => { renderElement={({ children }) => {
React.useEffect(() => mounts(element), []) useEffect(() => mounts(), [])
return children return children
}} }}

View File

@@ -8,7 +8,7 @@ import 'prismjs/components/prism-python'
import 'prismjs/components/prism-php' import 'prismjs/components/prism-php'
import 'prismjs/components/prism-sql' import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-java' import 'prismjs/components/prism-java'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useState } from 'react'
import { import {
createEditor, createEditor,
Node, Node,
@@ -51,7 +51,7 @@ const CodeHighlightingExample = () => {
<SetNodeToDecorations /> <SetNodeToDecorations />
<Editable <Editable
decorate={decorate} decorate={decorate}
renderElement={renderElement} renderElement={ElementWrapper}
renderLeaf={renderLeaf} renderLeaf={renderLeaf}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
@@ -60,7 +60,7 @@ const CodeHighlightingExample = () => {
) )
} }
const renderElement = (props: RenderElementProps) => { const ElementWrapper = (props: RenderElementProps) => {
const { attributes, children, element } = props const { attributes, children, element } = props
const editor = useSlateStatic() const editor = useSlateStatic()
@@ -161,14 +161,17 @@ const renderLeaf = (props: RenderLeafProps) => {
} }
const useDecorate = (editor: Editor) => { const useDecorate = (editor: Editor) => {
return useCallback(([node, path]) => { return useCallback(
if (Element.isElement(node) && node.type === CodeLineType) { ([node, path]) => {
const ranges = editor.nodeToDecorations.get(node) || [] if (Element.isElement(node) && node.type === CodeLineType) {
return ranges const ranges = editor.nodeToDecorations.get(node) || []
} return ranges
}
return [] return []
}, []) },
[editor.nodeToDecorations]
)
} }
const getChildNodeToDecorations = ([block, blockPath]: NodeEntry< const getChildNodeToDecorations = ([block, blockPath]: NodeEntry<
@@ -220,34 +223,35 @@ const getChildNodeToDecorations = ([block, blockPath]: NodeEntry<
const SetNodeToDecorations = () => { const SetNodeToDecorations = () => {
const editor = useSlate() const editor = useSlate()
useMemo(() => { const blockEntries = Array.from(
const blockEntries = Array.from( Editor.nodes(editor, {
Editor.nodes(editor, { at: [],
at: [], mode: 'highest',
mode: 'highest', match: n => Element.isElement(n) && n.type === CodeBlockType,
match: n => Element.isElement(n) && n.type === CodeBlockType, })
}) )
)
const nodeToDecorations = mergeMaps( const nodeToDecorations = mergeMaps(
...blockEntries.map(getChildNodeToDecorations) ...blockEntries.map(getChildNodeToDecorations)
) )
editor.nodeToDecorations = nodeToDecorations editor.nodeToDecorations = nodeToDecorations
}, [editor.children])
return null return null
} }
const useOnKeydown = (editor: Editor) => { const useOnKeydown = (editor: Editor) => {
const onKeyDown: React.KeyboardEventHandler = useCallback(e => { const onKeyDown: React.KeyboardEventHandler = useCallback(
if (isHotkey('tab', e)) { e => {
// handle tab key, insert spaces if (isHotkey('tab', e)) {
e.preventDefault() // handle tab key, insert spaces
e.preventDefault()
Editor.insertText(editor, ' ') Editor.insertText(editor, ' ')
} }
}, []) },
[editor]
)
return onKeyDown return onKeyDown
} }

View File

@@ -33,38 +33,41 @@ const MarkdownShortcutsExample = () => {
[] []
) )
const handleDOMBeforeInput = useCallback((e: InputEvent) => { const handleDOMBeforeInput = useCallback(
queueMicrotask(() => { (e: InputEvent) => {
const pendingDiffs = ReactEditor.androidPendingDiffs(editor) queueMicrotask(() => {
const pendingDiffs = ReactEditor.androidPendingDiffs(editor)
const scheduleFlush = pendingDiffs?.some(({ diff, path }) => { const scheduleFlush = pendingDiffs?.some(({ diff, path }) => {
if (!diff.text.endsWith(' ')) { if (!diff.text.endsWith(' ')) {
return false return false
} }
const { text } = SlateNode.leaf(editor, path) const { text } = SlateNode.leaf(editor, path)
const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1) const beforeText = text.slice(0, diff.start) + diff.text.slice(0, -1)
if (!(beforeText in SHORTCUTS)) { if (!(beforeText in SHORTCUTS)) {
return return
} }
const blockEntry = Editor.above(editor, { const blockEntry = Editor.above(editor, {
at: path, at: path,
match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n),
})
if (!blockEntry) {
return false
}
const [, blockPath] = blockEntry
return Editor.isStart(editor, Editor.start(editor, path), blockPath)
}) })
if (!blockEntry) {
return false if (scheduleFlush) {
ReactEditor.androidScheduleFlush(editor)
} }
const [, blockPath] = blockEntry
return Editor.isStart(editor, Editor.start(editor, path), blockPath)
}) })
},
if (scheduleFlush) { [editor]
ReactEditor.androidScheduleFlush(editor) )
}
})
}, [])
return ( return (
<Slate editor={editor} value={initialValue}> <Slate editor={editor} value={initialValue}>

View File

@@ -57,7 +57,7 @@ const MentionExample = () => {
} }
} }
}, },
[index, search, target] [chars, editor, index, target]
) )
useEffect(() => { useEffect(() => {

View File

@@ -6769,6 +6769,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-plugin-react-hooks@npm:^4.6.0":
version: 4.6.0
resolution: "eslint-plugin-react-hooks@npm:4.6.0"
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
checksum: 23001801f14c1d16bf0a837ca7970d9dd94e7b560384b41db378b49b6e32dc43d6e2790de1bd737a652a86f81a08d6a91f402525061b47719328f586a57e86c3
languageName: node
linkType: hard
"eslint-plugin-react@npm:^7.16.0": "eslint-plugin-react@npm:^7.16.0":
version: 7.24.0 version: 7.24.0
resolution: "eslint-plugin-react@npm:7.24.0" resolution: "eslint-plugin-react@npm:7.24.0"
@@ -13872,6 +13881,7 @@ resolve@^2.0.0-next.3:
eslint-plugin-import: ^2.18.2 eslint-plugin-import: ^2.18.2
eslint-plugin-prettier: ^3.1.1 eslint-plugin-prettier: ^3.1.1
eslint-plugin-react: ^7.16.0 eslint-plugin-react: ^7.16.0
eslint-plugin-react-hooks: ^4.6.0
faker: ^4.1.0 faker: ^4.1.0
image-extensions: ^1.1.0 image-extensions: ^1.1.0
is-hotkey: ^0.1.6 is-hotkey: ^0.1.6