snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
187
packages/react/src/useEditorState.ts
Normal file
187
packages/react/src/useEditorState.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useDebugValue, useEffect, useLayoutEffect, useState } from 'react'
|
||||
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector.js'
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
||||
|
||||
export type EditorStateSnapshot<TEditor extends Editor | null = Editor | null> = {
|
||||
editor: TEditor
|
||||
transactionNumber: number
|
||||
}
|
||||
|
||||
export type UseEditorStateOptions<
|
||||
TSelectorResult,
|
||||
TEditor extends Editor | null = Editor | null,
|
||||
> = {
|
||||
/**
|
||||
* The editor instance.
|
||||
*/
|
||||
editor: TEditor
|
||||
/**
|
||||
* A selector function to determine the value to compare for re-rendering.
|
||||
*/
|
||||
selector: (context: EditorStateSnapshot<TEditor>) => TSelectorResult
|
||||
/**
|
||||
* A custom equality function to determine if the editor should re-render.
|
||||
* @default `deepEqual` from `fast-deep-equal`
|
||||
*/
|
||||
equalityFn?: (a: TSelectorResult, b: TSelectorResult | null) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* To synchronize the editor instance with the component state,
|
||||
* we need to create a separate instance that is not affected by the component re-renders.
|
||||
*/
|
||||
class EditorStateManager<TEditor extends Editor | null = Editor | null> {
|
||||
private transactionNumber = 0
|
||||
|
||||
private lastTransactionNumber = 0
|
||||
|
||||
private lastSnapshot: EditorStateSnapshot<TEditor>
|
||||
|
||||
private editor: TEditor
|
||||
|
||||
private subscribers = new Set<() => void>()
|
||||
|
||||
constructor(initialEditor: TEditor) {
|
||||
this.editor = initialEditor
|
||||
this.lastSnapshot = { editor: initialEditor, transactionNumber: 0 }
|
||||
|
||||
this.getSnapshot = this.getSnapshot.bind(this)
|
||||
this.getServerSnapshot = this.getServerSnapshot.bind(this)
|
||||
this.watch = this.watch.bind(this)
|
||||
this.subscribe = this.subscribe.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current editor instance.
|
||||
*/
|
||||
getSnapshot(): EditorStateSnapshot<TEditor> {
|
||||
if (this.transactionNumber === this.lastTransactionNumber) {
|
||||
return this.lastSnapshot
|
||||
}
|
||||
this.lastTransactionNumber = this.transactionNumber
|
||||
this.lastSnapshot = { editor: this.editor, transactionNumber: this.transactionNumber }
|
||||
return this.lastSnapshot
|
||||
}
|
||||
|
||||
/**
|
||||
* Always disable the editor on the server-side.
|
||||
*/
|
||||
getServerSnapshot(): EditorStateSnapshot<null> {
|
||||
return { editor: null, transactionNumber: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the editor instance's changes.
|
||||
*/
|
||||
subscribe(callback: () => void): () => void {
|
||||
this.subscribers.add(callback)
|
||||
return () => {
|
||||
this.subscribers.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the editor instance for changes.
|
||||
*/
|
||||
watch(nextEditor: Editor | null): undefined | (() => void) {
|
||||
this.editor = nextEditor as TEditor
|
||||
|
||||
if (this.editor) {
|
||||
/**
|
||||
* This will force a re-render when the editor state changes.
|
||||
* This is to support things like `editor.can().toggleBold()` in components that `useEditor`.
|
||||
* This could be more efficient, but it's a good trade-off for now.
|
||||
*/
|
||||
// Changes can emit both events, so notify only once
|
||||
let lastTransaction: unknown
|
||||
const fn = (props?: { transaction?: unknown }) => {
|
||||
if (props?.transaction !== undefined && props.transaction === lastTransaction) {
|
||||
return
|
||||
}
|
||||
lastTransaction = props?.transaction
|
||||
|
||||
this.transactionNumber += 1
|
||||
this.subscribers.forEach(callback => callback())
|
||||
}
|
||||
|
||||
const currentEditor = this.editor
|
||||
|
||||
currentEditor.on('transaction', fn)
|
||||
currentEditor.on('update', fn)
|
||||
return () => {
|
||||
currentEditor.off('transaction', fn)
|
||||
currentEditor.off('update', fn)
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook allows you to watch for changes on the editor instance.
|
||||
* It will allow you to select a part of the editor state and re-render the component when it changes.
|
||||
* @example
|
||||
* ```tsx
|
||||
* const editor = useEditor({...options})
|
||||
* const { currentSelection } = useEditorState({
|
||||
* editor,
|
||||
* selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
|
||||
* })
|
||||
*/
|
||||
export function useEditorState<TSelectorResult>(
|
||||
options: UseEditorStateOptions<TSelectorResult, Editor>,
|
||||
): TSelectorResult
|
||||
/**
|
||||
* This hook allows you to watch for changes on the editor instance.
|
||||
* It will allow you to select a part of the editor state and re-render the component when it changes.
|
||||
* @example
|
||||
* ```tsx
|
||||
* const editor = useEditor({...options})
|
||||
* const { currentSelection } = useEditorState({
|
||||
* editor,
|
||||
* selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
|
||||
* })
|
||||
*/
|
||||
export function useEditorState<TSelectorResult>(
|
||||
options: UseEditorStateOptions<TSelectorResult, Editor | null>,
|
||||
): TSelectorResult | null
|
||||
|
||||
/**
|
||||
* This hook allows you to watch for changes on the editor instance.
|
||||
* It will allow you to select a part of the editor state and re-render the component when it changes.
|
||||
* @example
|
||||
* ```tsx
|
||||
* const editor = useEditor({...options})
|
||||
* const { currentSelection } = useEditorState({
|
||||
* editor,
|
||||
* selector: snapshot => ({ currentSelection: snapshot.editor.state.selection }),
|
||||
* })
|
||||
*/
|
||||
export function useEditorState<TSelectorResult>(
|
||||
options:
|
||||
| UseEditorStateOptions<TSelectorResult, Editor>
|
||||
| UseEditorStateOptions<TSelectorResult, Editor | null>,
|
||||
): TSelectorResult | null {
|
||||
const [editorStateManager] = useState(() => new EditorStateManager(options.editor))
|
||||
|
||||
// Using the `useSyncExternalStore` hook to sync the editor instance with the component state
|
||||
const selectedState = useSyncExternalStoreWithSelector(
|
||||
editorStateManager.subscribe,
|
||||
editorStateManager.getSnapshot,
|
||||
editorStateManager.getServerSnapshot,
|
||||
options.selector as UseEditorStateOptions<TSelectorResult, Editor | null>['selector'],
|
||||
options.equalityFn ?? deepEqual,
|
||||
)
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
return editorStateManager.watch(options.editor)
|
||||
}, [options.editor, editorStateManager])
|
||||
|
||||
useDebugValue(selectedState)
|
||||
|
||||
return selectedState
|
||||
}
|
||||
Reference in New Issue
Block a user