prosemirror-subtree-window
v0.1.0
Published
Viewport-driven subtree content windowing for ProseMirror editors
Maintainers
Readme
prosemirror-subtree-window
Viewport-driven subtree-content windowing for ProseMirror.
Open the live large-document performance demo
The complete ProseMirror document always remains in EditorState. Eligible
container nodes keep one height-preserving DOM placeholder while they are away
from the viewport; their descendant contentDOM is materialized when they
approach it. Window moves are decoration-only transactions with no steps, so
they do not enter history or change the document.
This is deliberately called subtree-content windowing rather than
virtualization. It leaves one placeholder per unmaterialized node, so DOM cost
is O(widest sibling run), not strictly O(viewport).
Install
npm install prosemirror-subtree-windowThe package is ESM-only. prosemirror-model, prosemirror-state, and
prosemirror-view are peer dependencies.
Use
import { EditorState, NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { createSubtreeWindowing } from "prosemirror-subtree-window";
const windowing = createSubtreeWindowing({
scrollRoot: editorHost,
nodeTypes: ["section"],
minDocumentUnits: 2_000,
});
const state = EditorState.create({
doc,
plugins: [...plugins, windowing.plugin],
});
const view = new EditorView(editorHost, { state });
windowing.setSelection(view, NodeSelection.create(view.state.doc, position), {
scrollIntoView: true,
focus: true,
});The plugin supplies its node views through plugin props and owns its
IntersectionObserver lifecycle. Do not also provide a direct nodeViews
entry for a windowed type: direct EditorView props take precedence over plugin
props. Pass existing node views as baseNodeViews instead; the package wraps
them and forwards their lifecycle hooks when their content is materialized.
Ordinary tr.setSelection(...) transactions are safe too. The plugin
automatically materializes both endpoint paths of every selection, plus the
selected node for a NodeSelection. The explicit API is useful when selection,
scrolling, and focus should be performed together.
Configuration
scrollRoot and nodeTypes are required. The useful optional settings are:
baseNodeViews: existing node views to wrap and provide through the plugin.subtreeUnits(node): size used for eligibility and eager subtree expansion.estimateUnits(node): units used to estimate placeholder height.measurementKey(node): groups nodes whose measured height per unit is shared.createPlaceholder(context): custom placeholder markup.isWindowable(node, pos, doc): restricts eligible container nodes.alwaysRender(node, pos, doc): excludes structural roots or special nodes.minDocumentUnits,minSubtreeUnits, andexpandSubtreeBelow: thresholds.renderMarginandkeepMargin: observer margins. The wider keep margin provides hysteresis.
Defaults use non-text descendant count as the unit, group measurements by node
type, materialize at 150%, and retain materialized content to 400%.
minDocumentUnits and enabled are evaluated when the plugin state is
created. They do not toggle an existing editor when its document later crosses
the threshold. For editors that start small and can grow large, leave
minDocumentUnits at 0 (the default), or recreate the editor state when your
application changes modes.
API
plugin: add it to the editor state.setWindow(view, action): explicitly materialize or release node positions; useful for deterministic drivers and tests.revealPosition(view, pos): materialize a position and its ancestor path.revealRange(view, from, to): materialize both endpoint paths.setSelection(view, selection, options): select with automatic reveal.getState(state): return{ enabled, materializedCount }diagnostics.countDescendants(node): the memoized default sizing helper.
Constraints
- Use block container nodes whose normal rendering exposes a
contentDOM. Inline flow, textblocks, table-layout participants, and nested editors require application-specific evaluation. - Browser find, screen readers, printing, and DOM-only export cannot inspect content that is not currently materialized. Model-driven operations can scan the complete ProseMirror document and call the reveal API before navigating.
- Placeholder accuracy depends on a size unit that correlates with rendered
height. Supply
estimateUnits,measurementKey, andcreatePlaceholderfor schemas whose nodes vary substantially. - Without
IntersectionObserver, the package fails open and renders the normal complete document. - Custom node views using
multiTypeare not supported in this first version.
Performance demo
The repository-only example generates up to 10000 sections in the browser, keeps the editor interactive, and measures the windowed and complete-DOM mounts on the visitor's device.
Run it locally from a repository checkout:
cd example
npm install
npm run dev