mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-12 02:03:59 +02:00
Feature/slate selectors (#4841)
* Added a redux inspired slate selector * added changeset
This commit is contained in:
@@ -4,6 +4,10 @@ import { ReactEditor } from '../plugin/react-editor'
|
||||
import { FocusedContext } from '../hooks/use-focused'
|
||||
import { EditorContext } from '../hooks/use-slate-static'
|
||||
import { SlateContext } from '../hooks/use-slate'
|
||||
import {
|
||||
getSelectorContext,
|
||||
SlateSelectorContext,
|
||||
} from '../hooks/use-slate-selector'
|
||||
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
|
||||
@@ -38,9 +42,15 @@ export const Slate = (props: {
|
||||
return [editor]
|
||||
})
|
||||
|
||||
const {
|
||||
selectorContext,
|
||||
onChange: handleSelectorChange,
|
||||
} = getSelectorContext(editor)
|
||||
|
||||
const onContextChange = useCallback(() => {
|
||||
onChange(editor.children)
|
||||
setContext([editor])
|
||||
handleSelectorChange(editor)
|
||||
}, [onChange])
|
||||
|
||||
EDITOR_TO_ON_CHANGE.set(editor, onContextChange)
|
||||
@@ -76,12 +86,14 @@ export const Slate = (props: {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SlateContext.Provider value={context}>
|
||||
<EditorContext.Provider value={editor}>
|
||||
<FocusedContext.Provider value={isFocused}>
|
||||
{children}
|
||||
</FocusedContext.Provider>
|
||||
</EditorContext.Provider>
|
||||
</SlateContext.Provider>
|
||||
<SlateSelectorContext.Provider value={selectorContext}>
|
||||
<SlateContext.Provider value={context}>
|
||||
<EditorContext.Provider value={editor}>
|
||||
<FocusedContext.Provider value={isFocused}>
|
||||
{children}
|
||||
</FocusedContext.Provider>
|
||||
</EditorContext.Provider>
|
||||
</SlateContext.Provider>
|
||||
</SlateSelectorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
139
packages/slate-react/src/hooks/use-slate-selector.tsx
Normal file
139
packages/slate-react/src/hooks/use-slate-selector.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Editor } from 'slate'
|
||||
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'
|
||||
|
||||
function isError(error: any): error is Error {
|
||||
return error instanceof Error
|
||||
}
|
||||
|
||||
type EditorChangeHandler = (editor: Editor) => void
|
||||
/**
|
||||
* A React context for sharing the editor selector context in a way to control rerenders
|
||||
*/
|
||||
|
||||
export const SlateSelectorContext = createContext<{
|
||||
getSlate: () => Editor
|
||||
addEventListener: (callback: EditorChangeHandler) => () => void
|
||||
}>({} as any)
|
||||
|
||||
const refEquality = (a: any, b: any) => a === b
|
||||
|
||||
/**
|
||||
* use redux style selectors to prevent rerendering on every keystroke.
|
||||
* Bear in mind rerendering can only prevented if the returned value is a value type or for reference types (e.g. objects and arrays) add a custom equality function.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const isSelectionActive = useSlateSelector(editor => Boolean(editor.selection));
|
||||
* ```
|
||||
*/
|
||||
export function useSlateSelector<T>(
|
||||
selector: (editor: Editor) => T,
|
||||
equalityFn: (a: T, b: T) => boolean = refEquality
|
||||
) {
|
||||
const [, forceRender] = useReducer(s => s + 1, 0)
|
||||
const context = useContext(SlateSelectorContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
`The \`useSlateSelector\` hook must be used inside the <Slate> component's context.`
|
||||
)
|
||||
}
|
||||
const { getSlate, addEventListener } = context
|
||||
|
||||
const latestSubscriptionCallbackError = useRef<Error | undefined>()
|
||||
const latestSelector = useRef<(editor: Editor) => T>(() => null as any)
|
||||
const latestSelectedState = useRef<T>((null as any) as T)
|
||||
let selectedState: T
|
||||
|
||||
try {
|
||||
if (
|
||||
selector !== latestSelector.current ||
|
||||
latestSubscriptionCallbackError.current
|
||||
) {
|
||||
selectedState = selector(getSlate())
|
||||
} else {
|
||||
selectedState = latestSelectedState.current
|
||||
}
|
||||
} catch (err) {
|
||||
if (latestSubscriptionCallbackError.current && isError(err)) {
|
||||
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
latestSelector.current = selector
|
||||
latestSelectedState.current = selectedState
|
||||
latestSubscriptionCallbackError.current = undefined
|
||||
})
|
||||
|
||||
useIsomorphicLayoutEffect(
|
||||
() => {
|
||||
function checkForUpdates() {
|
||||
try {
|
||||
const newSelectedState = latestSelector.current(getSlate())
|
||||
|
||||
if (equalityFn(newSelectedState, latestSelectedState.current)) {
|
||||
return
|
||||
}
|
||||
|
||||
latestSelectedState.current = newSelectedState
|
||||
} catch (err) {
|
||||
// we ignore all errors here, since when the component
|
||||
// is re-rendered, the selectors are called again, and
|
||||
// will throw again, if neither props nor store state
|
||||
// changed
|
||||
latestSubscriptionCallbackError.current = err
|
||||
}
|
||||
|
||||
forceRender()
|
||||
}
|
||||
|
||||
const unsubscribe = addEventListener(checkForUpdates)
|
||||
|
||||
checkForUpdates()
|
||||
|
||||
return () => unsubscribe()
|
||||
},
|
||||
// don't rerender on equalityFn change since we want to be able to define it inline
|
||||
[addEventListener, getSlate]
|
||||
)
|
||||
|
||||
return selectedState
|
||||
}
|
||||
|
||||
/**
|
||||
* Create selector context with editor updating on every editor change
|
||||
*/
|
||||
export function getSelectorContext(editor: Editor) {
|
||||
const eventListeners = useRef<EditorChangeHandler[]>([]).current
|
||||
const slateRef = useRef<{
|
||||
editor: Editor
|
||||
}>({
|
||||
editor,
|
||||
}).current
|
||||
const onChange = useCallback((editor: Editor) => {
|
||||
slateRef.editor = editor
|
||||
eventListeners.forEach((listener: EditorChangeHandler) => listener(editor))
|
||||
}, [])
|
||||
|
||||
const selectorContext = useMemo(() => {
|
||||
return {
|
||||
getSlate: () => slateRef.editor,
|
||||
addEventListener: (callback: EditorChangeHandler) => {
|
||||
eventListeners.push(callback)
|
||||
return () => {
|
||||
eventListeners.splice(eventListeners.indexOf(callback), 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
}, [eventListeners, slateRef])
|
||||
return { selectorContext, onChange }
|
||||
}
|
@@ -24,6 +24,7 @@ export { useFocused } from './hooks/use-focused'
|
||||
export { useReadOnly } from './hooks/use-read-only'
|
||||
export { useSelected } from './hooks/use-selected'
|
||||
export { useSlate } from './hooks/use-slate'
|
||||
export { useSlateSelector } from './hooks/use-slate-selector'
|
||||
|
||||
// Plugin
|
||||
export { ReactEditor } from './plugin/react-editor'
|
||||
|
Reference in New Issue
Block a user