mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-31 10:51:44 +02:00
Experimental chunking optimisation and other performance improvements (#5871)
* Chunking optimization * Fix comments * Remove redundant `insertionsMinusRemovals` variable * Fix typo * Unblock Netlify builds * Add placeholder * Upgrade Playwright (fixes crash when debugging) * Fix `autoFocus` not working * Fix huge document test * Fix the previous issue without changing `useSlateSelector` * Retry `test:integration` * Re-implement `useSlateWithV` * Retry `test:integration` * Update docs * Update JS examples to match TS examples * Upload Playwright's `test-results` directory in CI to access traces * Change trace mode to `retain-on-first-failure` * Fix: `Locator.fill(text)` is flaky on Editable * Add changesets * Increase minimum `slate-dom` version * Update changeset * Update 09-performance.md * Deprecate the `useSlateWithV` hook * Fix errors and improve clarity in 09-performance.md * Minimum `slate-dom` version is now 0.116 * Update `yarn.lock`
This commit is contained in:
@@ -16,9 +16,9 @@ import {
|
||||
Element,
|
||||
Node,
|
||||
NodeEntry,
|
||||
Range,
|
||||
Transforms,
|
||||
createEditor,
|
||||
DecoratedRange,
|
||||
} from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
import {
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
Slate,
|
||||
useSlate,
|
||||
useSlateStatic,
|
||||
withReact,
|
||||
} from 'slate-react'
|
||||
@@ -48,13 +47,12 @@ const CodeLineType = 'code-line'
|
||||
const CodeHighlightingExample = () => {
|
||||
const [editor] = useState(() => withHistory(withReact(createEditor())))
|
||||
|
||||
const decorate = useDecorate(editor)
|
||||
const decorate = useDecorate()
|
||||
const onKeyDown = useOnKeydown(editor)
|
||||
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<ExampleToolbar />
|
||||
<SetNodeToDecorations />
|
||||
<Editable
|
||||
decorate={decorate}
|
||||
renderElement={ElementWrapper}
|
||||
@@ -166,48 +164,27 @@ const renderLeaf = (props: RenderLeafProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const useDecorate = (editor: CustomEditor) => {
|
||||
return useCallback(
|
||||
([node, path]: NodeEntry) => {
|
||||
if (Element.isElement(node) && node.type === CodeLineType) {
|
||||
const ranges = editor.nodeToDecorations?.get(node) || []
|
||||
return ranges
|
||||
}
|
||||
const useDecorate = () => {
|
||||
return useCallback(([node, path]: NodeEntry) => {
|
||||
if (Element.isElement(node) && node.type === CodeBlockType) {
|
||||
return decorateCodeBlock([node, path])
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
[editor.nodeToDecorations]
|
||||
)
|
||||
return []
|
||||
}, [])
|
||||
}
|
||||
|
||||
interface TokenRange extends Range {
|
||||
token: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type EditorWithDecorations = CustomEditor & {
|
||||
nodeToDecorations: Map<Element, TokenRange[]>
|
||||
}
|
||||
|
||||
const getChildNodeToDecorations = ([
|
||||
const decorateCodeBlock = ([
|
||||
block,
|
||||
blockPath,
|
||||
]: NodeEntry<CodeBlockElement>): Map<Element, TokenRange[]> => {
|
||||
const nodeToDecorations = new Map<Element, TokenRange[]>()
|
||||
|
||||
]: NodeEntry<CodeBlockElement>): DecoratedRange[] => {
|
||||
const text = block.children.map(line => Node.string(line)).join('\n')
|
||||
const language = block.language
|
||||
const tokens = Prism.tokenize(text, Prism.languages[language])
|
||||
const tokens = Prism.tokenize(text, Prism.languages[block.language])
|
||||
const normalizedTokens = normalizeTokens(tokens) // make tokens flat and grouped by line
|
||||
const blockChildren = block.children as Element[]
|
||||
const decorations: DecoratedRange[] = []
|
||||
|
||||
for (let index = 0; index < normalizedTokens.length; index++) {
|
||||
const tokens = normalizedTokens[index]
|
||||
const element = blockChildren[index]
|
||||
|
||||
if (!nodeToDecorations.has(element)) {
|
||||
nodeToDecorations.set(element, [])
|
||||
}
|
||||
|
||||
let start = 0
|
||||
for (const token of tokens) {
|
||||
@@ -219,41 +196,19 @@ const getChildNodeToDecorations = ([
|
||||
const end = start + length
|
||||
|
||||
const path = [...blockPath, index, 0]
|
||||
const range = {
|
||||
|
||||
decorations.push({
|
||||
anchor: { path, offset: start },
|
||||
focus: { path, offset: end },
|
||||
token: true,
|
||||
...Object.fromEntries(token.types.map(type => [type, true])),
|
||||
}
|
||||
|
||||
nodeToDecorations.get(element)!.push(range)
|
||||
})
|
||||
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
return nodeToDecorations
|
||||
}
|
||||
|
||||
// precalculate editor.nodeToDecorations map to use it inside decorate function then
|
||||
const SetNodeToDecorations = () => {
|
||||
const editor = useSlate() as EditorWithDecorations
|
||||
|
||||
const blockEntries = Array.from(
|
||||
Editor.nodes<CodeBlockElement>(editor, {
|
||||
at: [],
|
||||
mode: 'highest',
|
||||
match: n => Element.isElement(n) && n.type === CodeBlockType,
|
||||
})
|
||||
)
|
||||
|
||||
const nodeToDecorations = mergeMaps(
|
||||
...blockEntries.map(getChildNodeToDecorations)
|
||||
)
|
||||
|
||||
editor.nodeToDecorations = nodeToDecorations
|
||||
|
||||
return null
|
||||
return decorations
|
||||
}
|
||||
|
||||
const useOnKeydown = (editor: CustomEditor) => {
|
||||
@@ -306,18 +261,6 @@ const LanguageSelect = (props: LanguageSelectProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const mergeMaps = <K, V>(...maps: Map<K, V>[]) => {
|
||||
const map = new Map<K, V>()
|
||||
|
||||
for (const m of maps) {
|
||||
for (const item of m) {
|
||||
map.set(...item)
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
const toChildren = (content: string): CustomText[] => [{ text: content }]
|
||||
const toCodeLines = (content: string): CodeLineElement[] =>
|
||||
content
|
||||
|
@@ -117,6 +117,7 @@ const UrlInput = ({ url, onChange }: UrlInputProps) => {
|
||||
const [value, setValue] = React.useState(url)
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{
|
||||
|
@@ -1,54 +1,520 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { createEditor, Descendant } from 'slate'
|
||||
import { Editable, RenderElementProps, Slate, withReact } from 'slate-react'
|
||||
|
||||
import React, {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createEditor as slateCreateEditor, Descendant, Editor } from 'slate'
|
||||
import {
|
||||
CustomEditor,
|
||||
HeadingElement,
|
||||
ParagraphElement,
|
||||
} from './custom-types.d'
|
||||
Editable,
|
||||
RenderElementProps,
|
||||
RenderChunkProps,
|
||||
Slate,
|
||||
withReact,
|
||||
useSelected,
|
||||
} from 'slate-react'
|
||||
|
||||
const HEADINGS = 100
|
||||
const PARAGRAPHS = 7
|
||||
const initialValue: Descendant[] = []
|
||||
import { HeadingElement, ParagraphElement } from './custom-types.d'
|
||||
|
||||
for (let h = 0; h < HEADINGS; h++) {
|
||||
const heading: HeadingElement = {
|
||||
type: 'heading-one',
|
||||
children: [{ text: faker.lorem.sentence() }],
|
||||
const SUPPORTS_EVENT_TIMING =
|
||||
typeof window !== 'undefined' && 'PerformanceEventTiming' in window
|
||||
|
||||
const SUPPORTS_LOAF_TIMING =
|
||||
typeof window !== 'undefined' &&
|
||||
'PerformanceLongAnimationFrameTiming' in window
|
||||
|
||||
interface Config {
|
||||
blocks: number
|
||||
chunking: boolean
|
||||
chunkSize: number
|
||||
chunkDivs: boolean
|
||||
chunkOutlines: boolean
|
||||
contentVisibilityMode: 'none' | 'element' | 'chunk'
|
||||
showSelectedHeadings: boolean
|
||||
}
|
||||
|
||||
const blocksOptions = [
|
||||
2, 1000, 2500, 5000, 7500, 10000, 15000, 20000, 25000, 30000, 40000, 50000,
|
||||
100000, 200000,
|
||||
]
|
||||
|
||||
const chunkSizeOptions = [3, 10, 100, 1000]
|
||||
|
||||
const searchParams =
|
||||
typeof document === 'undefined'
|
||||
? null
|
||||
: new URLSearchParams(document.location.search)
|
||||
|
||||
const parseNumber = (key: string, defaultValue: number) =>
|
||||
parseInt(searchParams?.get(key) ?? '', 10) || defaultValue
|
||||
|
||||
const parseBoolean = (key: string, defaultValue: boolean) => {
|
||||
const value = searchParams?.get(key)
|
||||
if (value) return value === 'true'
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const parseEnum = <T extends string>(
|
||||
key: string,
|
||||
options: T[],
|
||||
defaultValue: T
|
||||
): T => {
|
||||
const value = searchParams?.get(key) as T | null | undefined
|
||||
if (value && options.includes(value)) return value
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const initialConfig: Config = {
|
||||
blocks: parseNumber('blocks', 10000),
|
||||
chunking: parseBoolean('chunking', true),
|
||||
chunkSize: parseNumber('chunk_size', 1000),
|
||||
chunkDivs: parseBoolean('chunk_divs', true),
|
||||
chunkOutlines: parseBoolean('chunk_outlines', false),
|
||||
contentVisibilityMode: parseEnum(
|
||||
'content_visibility',
|
||||
['none', 'element', 'chunk'],
|
||||
'chunk'
|
||||
),
|
||||
showSelectedHeadings: parseBoolean('selected_headings', false),
|
||||
}
|
||||
|
||||
const setSearchParams = (config: Config) => {
|
||||
if (searchParams) {
|
||||
searchParams.set('blocks', config.blocks.toString())
|
||||
searchParams.set('chunking', config.chunking ? 'true' : 'false')
|
||||
searchParams.set('chunk_size', config.chunkSize.toString())
|
||||
searchParams.set('chunk_divs', config.chunkDivs ? 'true' : 'false')
|
||||
searchParams.set('chunk_outlines', config.chunkOutlines ? 'true' : 'false')
|
||||
searchParams.set('content_visibility', config.contentVisibilityMode)
|
||||
searchParams.set(
|
||||
'selected_headings',
|
||||
config.showSelectedHeadings ? 'true' : 'false'
|
||||
)
|
||||
history.replaceState({}, '', `?${searchParams.toString()}`)
|
||||
}
|
||||
initialValue.push(heading)
|
||||
}
|
||||
|
||||
for (let p = 0; p < PARAGRAPHS; p++) {
|
||||
const paragraph: ParagraphElement = {
|
||||
type: 'paragraph',
|
||||
children: [{ text: faker.lorem.paragraph() }],
|
||||
const cachedInitialValue: Descendant[] = []
|
||||
|
||||
const getInitialValue = (blocks: number) => {
|
||||
if (cachedInitialValue.length >= blocks) {
|
||||
return cachedInitialValue.slice(0, blocks)
|
||||
}
|
||||
|
||||
faker.seed(1)
|
||||
|
||||
for (let i = cachedInitialValue.length; i < blocks; i++) {
|
||||
if (i % 100 === 0) {
|
||||
const heading: HeadingElement = {
|
||||
type: 'heading-one',
|
||||
children: [{ text: faker.lorem.sentence() }],
|
||||
}
|
||||
cachedInitialValue.push(heading)
|
||||
} else {
|
||||
const paragraph: ParagraphElement = {
|
||||
type: 'paragraph',
|
||||
children: [{ text: faker.lorem.paragraph() }],
|
||||
}
|
||||
cachedInitialValue.push(paragraph)
|
||||
}
|
||||
initialValue.push(paragraph)
|
||||
}
|
||||
|
||||
return cachedInitialValue.slice()
|
||||
}
|
||||
|
||||
const initialInitialValue =
|
||||
typeof window === 'undefined' ? [] : getInitialValue(initialConfig.blocks)
|
||||
|
||||
const createEditor = (config: Config) => {
|
||||
const editor = withReact(slateCreateEditor())
|
||||
|
||||
editor.getChunkSize = node =>
|
||||
config.chunking && Editor.isEditor(node) ? config.chunkSize : null
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
const HugeDocumentExample = () => {
|
||||
const renderElement = useCallback(
|
||||
(props: RenderElementProps) => <Element {...props} />,
|
||||
[]
|
||||
const [rendering, setRendering] = useState(false)
|
||||
const [config, baseSetConfig] = useState<Config>(initialConfig)
|
||||
const [initialValue, setInitialValue] = useState(initialInitialValue)
|
||||
const [editor, setEditor] = useState(() => createEditor(config))
|
||||
const [editorVersion, setEditorVersion] = useState(0)
|
||||
|
||||
const setConfig = useCallback(
|
||||
(partialConfig: Partial<Config>) => {
|
||||
const newConfig = { ...config, ...partialConfig }
|
||||
|
||||
setRendering(true)
|
||||
baseSetConfig(newConfig)
|
||||
setSearchParams(newConfig)
|
||||
|
||||
setTimeout(() => {
|
||||
setRendering(false)
|
||||
setInitialValue(getInitialValue(newConfig.blocks))
|
||||
setEditor(createEditor(newConfig))
|
||||
setEditorVersion(n => n + 1)
|
||||
})
|
||||
},
|
||||
[config]
|
||||
)
|
||||
const editor = useMemo(() => withReact(createEditor()) as CustomEditor, [])
|
||||
|
||||
const renderElement = useCallback(
|
||||
(props: RenderElementProps) => (
|
||||
<Element
|
||||
{...props}
|
||||
contentVisibility={config.contentVisibilityMode === 'element'}
|
||||
showSelectedHeadings={config.showSelectedHeadings}
|
||||
/>
|
||||
),
|
||||
[config.contentVisibilityMode, config.showSelectedHeadings]
|
||||
)
|
||||
|
||||
const renderChunk = useCallback(
|
||||
(props: RenderChunkProps) => (
|
||||
<Chunk
|
||||
{...props}
|
||||
contentVisibilityLowest={config.contentVisibilityMode === 'chunk'}
|
||||
outline={config.chunkOutlines}
|
||||
/>
|
||||
),
|
||||
[config.contentVisibilityMode, config.chunkOutlines]
|
||||
)
|
||||
|
||||
return (
|
||||
<Slate editor={editor} initialValue={initialValue}>
|
||||
<Editable renderElement={renderElement} spellCheck autoFocus />
|
||||
</Slate>
|
||||
<>
|
||||
<PerformanceControls
|
||||
editor={editor}
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
/>
|
||||
|
||||
{rendering ? (
|
||||
<div>Rendering…</div>
|
||||
) : (
|
||||
<Slate key={editorVersion} editor={editor} initialValue={initialValue}>
|
||||
<Editable
|
||||
placeholder="Enter some text…"
|
||||
renderElement={renderElement}
|
||||
renderChunk={config.chunkDivs ? renderChunk : undefined}
|
||||
spellCheck
|
||||
autoFocus
|
||||
/>
|
||||
</Slate>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Element = ({ attributes, children, element }: RenderElementProps) => {
|
||||
const Chunk = ({
|
||||
attributes,
|
||||
children,
|
||||
lowest,
|
||||
contentVisibilityLowest,
|
||||
outline,
|
||||
}: RenderChunkProps & {
|
||||
contentVisibilityLowest: boolean
|
||||
outline: boolean
|
||||
}) => {
|
||||
const style: CSSProperties = {
|
||||
contentVisibility: contentVisibilityLowest && lowest ? 'auto' : undefined,
|
||||
border: outline ? '1px solid red' : undefined,
|
||||
padding: outline ? 20 : undefined,
|
||||
marginBottom: outline ? 20 : undefined,
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...attributes} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Heading = React.forwardRef<
|
||||
HTMLHeadingElement,
|
||||
React.ComponentProps<'h1'> & { showSelectedHeadings: boolean }
|
||||
>(({ style: styleProp, showSelectedHeadings = false, ...props }, ref) => {
|
||||
// Fine since the editor is remounted if the config changes
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const selected = showSelectedHeadings ? useSelected() : false
|
||||
const style = { ...styleProp, color: selected ? 'green' : undefined }
|
||||
return <h1 ref={ref} {...props} aria-selected={selected} style={style} />
|
||||
})
|
||||
|
||||
const Paragraph = 'p'
|
||||
|
||||
const Element = ({
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
contentVisibility,
|
||||
showSelectedHeadings,
|
||||
}: RenderElementProps & {
|
||||
contentVisibility: boolean
|
||||
showSelectedHeadings: boolean
|
||||
}) => {
|
||||
const style: CSSProperties = {
|
||||
contentVisibility: contentVisibility ? 'auto' : undefined,
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
case 'heading-one':
|
||||
return <h1 {...attributes}>{children}</h1>
|
||||
return (
|
||||
<Heading
|
||||
{...attributes}
|
||||
style={style}
|
||||
showSelectedHeadings={showSelectedHeadings}
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
)
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>
|
||||
return (
|
||||
<Paragraph {...attributes} style={style}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const PerformanceControls = ({
|
||||
editor,
|
||||
config,
|
||||
setConfig,
|
||||
}: {
|
||||
editor: Editor
|
||||
config: Config
|
||||
setConfig: Dispatch<Partial<Config>>
|
||||
}) => {
|
||||
const [configurationOpen, setConfigurationOpen] = useState(true)
|
||||
const [keyPressDurations, setKeyPressDurations] = useState<number[]>([])
|
||||
const [lastLongAnimationFrameDuration, setLastLongAnimationFrameDuration] =
|
||||
useState<number | null>(null)
|
||||
|
||||
const lastKeyPressDuration: number | null = keyPressDurations[0] ?? null
|
||||
|
||||
const averageKeyPressDuration =
|
||||
keyPressDurations.length === 10
|
||||
? Math.round(keyPressDurations.reduce((total, d) => total + d) / 10)
|
||||
: null
|
||||
|
||||
useEffect(() => {
|
||||
if (!SUPPORTS_EVENT_TIMING) return
|
||||
|
||||
const observer = new PerformanceObserver(list => {
|
||||
list.getEntries().forEach(entry => {
|
||||
if (entry.name === 'keypress') {
|
||||
const duration = Math.round(
|
||||
// @ts-ignore Entry type is missing processingStart and processingEnd
|
||||
entry.processingEnd - entry.processingStart
|
||||
)
|
||||
setKeyPressDurations(durations => [
|
||||
duration,
|
||||
...durations.slice(0, 9),
|
||||
])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-ignore Options type is missing durationThreshold
|
||||
observer.observe({ type: 'event', durationThreshold: 16 })
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!SUPPORTS_LOAF_TIMING) return
|
||||
|
||||
const { apply } = editor
|
||||
let afterOperation = false
|
||||
|
||||
editor.apply = operation => {
|
||||
apply(operation)
|
||||
afterOperation = true
|
||||
}
|
||||
|
||||
const observer = new PerformanceObserver(list => {
|
||||
list.getEntries().forEach(entry => {
|
||||
if (afterOperation) {
|
||||
setLastLongAnimationFrameDuration(Math.round(entry.duration))
|
||||
afterOperation = false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Register the observer for events
|
||||
observer.observe({ type: 'long-animation-frame' })
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div className="performance-controls">
|
||||
<p>
|
||||
<label>
|
||||
Blocks:{' '}
|
||||
<select
|
||||
value={config.blocks}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
blocks: parseInt(event.target.value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
{blocksOptions.map(blocks => (
|
||||
<option key={blocks} value={blocks}>
|
||||
{blocks.toString().replace(/(\d{3})$/, ',$1')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<details
|
||||
open={configurationOpen}
|
||||
onToggle={event => setConfigurationOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary>Configuration</summary>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.chunking}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
chunking: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>{' '}
|
||||
Chunking enabled
|
||||
</label>
|
||||
</p>
|
||||
|
||||
{config.chunking && (
|
||||
<>
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.chunkDivs}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
chunkDivs: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>{' '}
|
||||
Render each chunk as a separate <code><div></code>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
{config.chunkDivs && (
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.chunkOutlines}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
chunkOutlines: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>{' '}
|
||||
Outline each chunk
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p>
|
||||
<label>
|
||||
Chunk size:{' '}
|
||||
<select
|
||||
value={config.chunkSize}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
chunkSize: parseInt(event.target.value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
{chunkSizeOptions.map(chunkSize => (
|
||||
<option key={chunkSize} value={chunkSize}>
|
||||
{chunkSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p>
|
||||
<label>
|
||||
Set <code>content-visibility: auto</code> on:{' '}
|
||||
<select
|
||||
value={config.contentVisibilityMode}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
contentVisibilityMode: event.target.value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="element">Elements</option>
|
||||
{config.chunking && config.chunkDivs && (
|
||||
<option value="chunk">Lowest chunks</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showSelectedHeadings}
|
||||
onChange={event =>
|
||||
setConfig({
|
||||
showSelectedHeadings: event.target.checked,
|
||||
})
|
||||
}
|
||||
/>{' '}
|
||||
Call <code>useSelected</code> in each heading
|
||||
</label>
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Statistics</summary>
|
||||
|
||||
<p>
|
||||
Last keypress (ms):{' '}
|
||||
{SUPPORTS_EVENT_TIMING
|
||||
? lastKeyPressDuration ?? '-'
|
||||
: 'Not supported'}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Average of last 10 keypresses (ms):{' '}
|
||||
{SUPPORTS_EVENT_TIMING
|
||||
? averageKeyPressDuration ?? '-'
|
||||
: 'Not supported'}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Last long animation frame (ms):{' '}
|
||||
{SUPPORTS_LOAF_TIMING
|
||||
? lastLongAnimationFrameDuration ?? '-'
|
||||
: 'Not supported'}
|
||||
</p>
|
||||
|
||||
{SUPPORTS_EVENT_TIMING && lastKeyPressDuration === null && (
|
||||
<p>Events shorter than 16ms may not be detected.</p>
|
||||
)}
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HugeDocumentExample
|
||||
|
@@ -97,7 +97,7 @@ const SearchHighlightingExample = () => {
|
||||
placeholder="Search the text..."
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className={css`
|
||||
padding-left: 2.5em;
|
||||
padding-left: 2.5em !important;
|
||||
width: 100%;
|
||||
`}
|
||||
/>
|
||||
|
Reference in New Issue
Block a user