@rtif-sdk/web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/block-drag-handler.d.ts +189 -0
- package/dist/block-drag-handler.d.ts.map +1 -0
- package/dist/block-drag-handler.js +745 -0
- package/dist/block-drag-handler.js.map +1 -0
- package/dist/block-renderer.d.ts +402 -0
- package/dist/block-renderer.d.ts.map +1 -0
- package/dist/block-renderer.js +424 -0
- package/dist/block-renderer.js.map +1 -0
- package/dist/clipboard.d.ts +178 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +432 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/command-bus.d.ts +113 -0
- package/dist/command-bus.d.ts.map +1 -0
- package/dist/command-bus.js +70 -0
- package/dist/command-bus.js.map +1 -0
- package/dist/composition.d.ts +220 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +271 -0
- package/dist/composition.js.map +1 -0
- package/dist/content-extraction.d.ts +69 -0
- package/dist/content-extraction.d.ts.map +1 -0
- package/dist/content-extraction.js +228 -0
- package/dist/content-extraction.js.map +1 -0
- package/dist/content-handler-file.d.ts +40 -0
- package/dist/content-handler-file.d.ts.map +1 -0
- package/dist/content-handler-file.js +91 -0
- package/dist/content-handler-file.js.map +1 -0
- package/dist/content-handler-image.d.ts +82 -0
- package/dist/content-handler-image.d.ts.map +1 -0
- package/dist/content-handler-image.js +120 -0
- package/dist/content-handler-image.js.map +1 -0
- package/dist/content-handler-url.d.ts +129 -0
- package/dist/content-handler-url.d.ts.map +1 -0
- package/dist/content-handler-url.js +244 -0
- package/dist/content-handler-url.js.map +1 -0
- package/dist/content-handlers.d.ts +67 -0
- package/dist/content-handlers.d.ts.map +1 -0
- package/dist/content-handlers.js +263 -0
- package/dist/content-handlers.js.map +1 -0
- package/dist/content-pipeline.d.ts +383 -0
- package/dist/content-pipeline.d.ts.map +1 -0
- package/dist/content-pipeline.js +232 -0
- package/dist/content-pipeline.js.map +1 -0
- package/dist/cursor-nav.d.ts +149 -0
- package/dist/cursor-nav.d.ts.map +1 -0
- package/dist/cursor-nav.js +230 -0
- package/dist/cursor-nav.js.map +1 -0
- package/dist/cursor-rect.d.ts +65 -0
- package/dist/cursor-rect.d.ts.map +1 -0
- package/dist/cursor-rect.js +98 -0
- package/dist/cursor-rect.js.map +1 -0
- package/dist/drop-indicator.d.ts +108 -0
- package/dist/drop-indicator.d.ts.map +1 -0
- package/dist/drop-indicator.js +236 -0
- package/dist/drop-indicator.js.map +1 -0
- package/dist/editor.d.ts +41 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +710 -0
- package/dist/editor.js.map +1 -0
- package/dist/floating-toolbar.d.ts +93 -0
- package/dist/floating-toolbar.d.ts.map +1 -0
- package/dist/floating-toolbar.js +159 -0
- package/dist/floating-toolbar.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/input-bridge.d.ts +273 -0
- package/dist/input-bridge.d.ts.map +1 -0
- package/dist/input-bridge.js +884 -0
- package/dist/input-bridge.js.map +1 -0
- package/dist/link-popover.d.ts +38 -0
- package/dist/link-popover.d.ts.map +1 -0
- package/dist/link-popover.js +278 -0
- package/dist/link-popover.js.map +1 -0
- package/dist/mark-renderer.d.ts +275 -0
- package/dist/mark-renderer.d.ts.map +1 -0
- package/dist/mark-renderer.js +210 -0
- package/dist/mark-renderer.js.map +1 -0
- package/dist/perf.d.ts +145 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +260 -0
- package/dist/perf.js.map +1 -0
- package/dist/plugin-kit.d.ts +265 -0
- package/dist/plugin-kit.d.ts.map +1 -0
- package/dist/plugin-kit.js +234 -0
- package/dist/plugin-kit.js.map +1 -0
- package/dist/plugins/alignment-plugin.d.ts +68 -0
- package/dist/plugins/alignment-plugin.d.ts.map +1 -0
- package/dist/plugins/alignment-plugin.js +98 -0
- package/dist/plugins/alignment-plugin.js.map +1 -0
- package/dist/plugins/block-utils.d.ts +113 -0
- package/dist/plugins/block-utils.d.ts.map +1 -0
- package/dist/plugins/block-utils.js +191 -0
- package/dist/plugins/block-utils.js.map +1 -0
- package/dist/plugins/blockquote-plugin.d.ts +39 -0
- package/dist/plugins/blockquote-plugin.d.ts.map +1 -0
- package/dist/plugins/blockquote-plugin.js +88 -0
- package/dist/plugins/blockquote-plugin.js.map +1 -0
- package/dist/plugins/bold-plugin.d.ts +37 -0
- package/dist/plugins/bold-plugin.d.ts.map +1 -0
- package/dist/plugins/bold-plugin.js +48 -0
- package/dist/plugins/bold-plugin.js.map +1 -0
- package/dist/plugins/callout-plugin.d.ts +100 -0
- package/dist/plugins/callout-plugin.d.ts.map +1 -0
- package/dist/plugins/callout-plugin.js +200 -0
- package/dist/plugins/callout-plugin.js.map +1 -0
- package/dist/plugins/code-block-plugin.d.ts +62 -0
- package/dist/plugins/code-block-plugin.d.ts.map +1 -0
- package/dist/plugins/code-block-plugin.js +176 -0
- package/dist/plugins/code-block-plugin.js.map +1 -0
- package/dist/plugins/code-plugin.d.ts +37 -0
- package/dist/plugins/code-plugin.d.ts.map +1 -0
- package/dist/plugins/code-plugin.js +48 -0
- package/dist/plugins/code-plugin.js.map +1 -0
- package/dist/plugins/embed-plugin.d.ts +90 -0
- package/dist/plugins/embed-plugin.d.ts.map +1 -0
- package/dist/plugins/embed-plugin.js +147 -0
- package/dist/plugins/embed-plugin.js.map +1 -0
- package/dist/plugins/font-family-plugin.d.ts +58 -0
- package/dist/plugins/font-family-plugin.d.ts.map +1 -0
- package/dist/plugins/font-family-plugin.js +57 -0
- package/dist/plugins/font-family-plugin.js.map +1 -0
- package/dist/plugins/font-size-plugin.d.ts +57 -0
- package/dist/plugins/font-size-plugin.d.ts.map +1 -0
- package/dist/plugins/font-size-plugin.js +56 -0
- package/dist/plugins/font-size-plugin.js.map +1 -0
- package/dist/plugins/heading-plugin.d.ts +52 -0
- package/dist/plugins/heading-plugin.d.ts.map +1 -0
- package/dist/plugins/heading-plugin.js +114 -0
- package/dist/plugins/heading-plugin.js.map +1 -0
- package/dist/plugins/hr-plugin.d.ts +33 -0
- package/dist/plugins/hr-plugin.d.ts.map +1 -0
- package/dist/plugins/hr-plugin.js +75 -0
- package/dist/plugins/hr-plugin.js.map +1 -0
- package/dist/plugins/image-plugin.d.ts +115 -0
- package/dist/plugins/image-plugin.d.ts.map +1 -0
- package/dist/plugins/image-plugin.js +199 -0
- package/dist/plugins/image-plugin.js.map +1 -0
- package/dist/plugins/indent-plugin.d.ts +62 -0
- package/dist/plugins/indent-plugin.d.ts.map +1 -0
- package/dist/plugins/indent-plugin.js +128 -0
- package/dist/plugins/indent-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +45 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +42 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/italic-plugin.d.ts +37 -0
- package/dist/plugins/italic-plugin.d.ts.map +1 -0
- package/dist/plugins/italic-plugin.js +48 -0
- package/dist/plugins/italic-plugin.js.map +1 -0
- package/dist/plugins/link-plugin.d.ts +129 -0
- package/dist/plugins/link-plugin.d.ts.map +1 -0
- package/dist/plugins/link-plugin.js +212 -0
- package/dist/plugins/link-plugin.js.map +1 -0
- package/dist/plugins/list-plugin.d.ts +53 -0
- package/dist/plugins/list-plugin.d.ts.map +1 -0
- package/dist/plugins/list-plugin.js +309 -0
- package/dist/plugins/list-plugin.js.map +1 -0
- package/dist/plugins/mark-utils.d.ts +173 -0
- package/dist/plugins/mark-utils.d.ts.map +1 -0
- package/dist/plugins/mark-utils.js +425 -0
- package/dist/plugins/mark-utils.js.map +1 -0
- package/dist/plugins/mention-plugin.d.ts +191 -0
- package/dist/plugins/mention-plugin.d.ts.map +1 -0
- package/dist/plugins/mention-plugin.js +295 -0
- package/dist/plugins/mention-plugin.js.map +1 -0
- package/dist/plugins/strikethrough-plugin.d.ts +37 -0
- package/dist/plugins/strikethrough-plugin.d.ts.map +1 -0
- package/dist/plugins/strikethrough-plugin.js +48 -0
- package/dist/plugins/strikethrough-plugin.js.map +1 -0
- package/dist/plugins/text-color-plugin.d.ts +57 -0
- package/dist/plugins/text-color-plugin.d.ts.map +1 -0
- package/dist/plugins/text-color-plugin.js +56 -0
- package/dist/plugins/text-color-plugin.js.map +1 -0
- package/dist/plugins/underline-plugin.d.ts +37 -0
- package/dist/plugins/underline-plugin.d.ts.map +1 -0
- package/dist/plugins/underline-plugin.js +48 -0
- package/dist/plugins/underline-plugin.js.map +1 -0
- package/dist/presets.d.ts +95 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +159 -0
- package/dist/presets.js.map +1 -0
- package/dist/renderer.d.ts +125 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +415 -0
- package/dist/renderer.js.map +1 -0
- package/dist/scroll-to-cursor.d.ts +25 -0
- package/dist/scroll-to-cursor.d.ts.map +1 -0
- package/dist/scroll-to-cursor.js +59 -0
- package/dist/scroll-to-cursor.js.map +1 -0
- package/dist/selection-sync.d.ts +159 -0
- package/dist/selection-sync.d.ts.map +1 -0
- package/dist/selection-sync.js +527 -0
- package/dist/selection-sync.js.map +1 -0
- package/dist/shortcut-handler.d.ts +98 -0
- package/dist/shortcut-handler.d.ts.map +1 -0
- package/dist/shortcut-handler.js +155 -0
- package/dist/shortcut-handler.js.map +1 -0
- package/dist/toolbar.d.ts +103 -0
- package/dist/toolbar.d.ts.map +1 -0
- package/dist/toolbar.js +134 -0
- package/dist/toolbar.js.map +1 -0
- package/dist/trigger-manager.d.ts +205 -0
- package/dist/trigger-manager.d.ts.map +1 -0
- package/dist/trigger-manager.js +466 -0
- package/dist/trigger-manager.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine the container element where span children should be placed
|
|
3
|
+
* for a given block element.
|
|
4
|
+
*
|
|
5
|
+
* For composite blocks (e.g., callout) whose renderer defines
|
|
6
|
+
* `getContentContainer`, spans are rendered inside a nested content
|
|
7
|
+
* element rather than directly in the block element. For all other
|
|
8
|
+
* block types the block element itself is the span container.
|
|
9
|
+
*
|
|
10
|
+
* @param blockEl - The block DOM element
|
|
11
|
+
* @param blockType - The RTIF block type string
|
|
12
|
+
* @param blockRenderers - Optional registry of block renderers
|
|
13
|
+
* @returns The element that should contain span children
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const container = getSpanContainer(blockEl, 'callout', registry);
|
|
18
|
+
* // For callout: returns the .rtif-callout-content div
|
|
19
|
+
* // For text: returns blockEl itself
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function getSpanContainer(blockEl, blockType, blockRenderers) {
|
|
23
|
+
if (!blockRenderers)
|
|
24
|
+
return blockEl;
|
|
25
|
+
const renderer = blockRenderers.get(blockType);
|
|
26
|
+
if (renderer?.getContentContainer) {
|
|
27
|
+
return renderer.getContentContainer(blockEl) ?? blockEl;
|
|
28
|
+
}
|
|
29
|
+
return blockEl;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a DOM element for an RTIF span.
|
|
33
|
+
*
|
|
34
|
+
* When a `markRenderers` registry is provided and the span has a mark with a
|
|
35
|
+
* registered {@link SpanReplacer}, the replacer's `createElement()` is called
|
|
36
|
+
* to produce a fully custom element. Otherwise, a default `<span>` is created
|
|
37
|
+
* with mark renderers applied.
|
|
38
|
+
*
|
|
39
|
+
* Non-empty spans contain a single Text node. Empty spans (text === '')
|
|
40
|
+
* contain a `<br>` element so the block has layout height in the browser.
|
|
41
|
+
*
|
|
42
|
+
* @param span - The RTIF span to render
|
|
43
|
+
* @param markRenderers - Optional registry of mark renderers for visual styling
|
|
44
|
+
* @returns An `HTMLElement` with `data-rtif-span` attribute and appropriate child node
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const el = createSpanElement({ text: 'hello' });
|
|
49
|
+
* // <span data-rtif-span>hello</span>
|
|
50
|
+
*
|
|
51
|
+
* const boldEl = createSpanElement(
|
|
52
|
+
* { text: 'bold', marks: { bold: true } },
|
|
53
|
+
* markRegistry,
|
|
54
|
+
* );
|
|
55
|
+
* // <span data-rtif-span class="rtif-bold">bold</span>
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function createSpanElement(span, markRenderers) {
|
|
59
|
+
// Check for span replacer before creating default element
|
|
60
|
+
if (markRenderers && span.marks) {
|
|
61
|
+
for (const markType of Object.keys(span.marks)) {
|
|
62
|
+
if (span.marks[markType] == null)
|
|
63
|
+
continue;
|
|
64
|
+
const replacer = markRenderers.getReplacer(markType);
|
|
65
|
+
if (replacer) {
|
|
66
|
+
const el = replacer.createElement(span.text, span.marks);
|
|
67
|
+
el.setAttribute('data-rtif-span', '');
|
|
68
|
+
return el;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const el = document.createElement('span');
|
|
73
|
+
el.setAttribute('data-rtif-span', '');
|
|
74
|
+
// Apply mark rendering
|
|
75
|
+
if (markRenderers) {
|
|
76
|
+
markRenderers.applyAll(el, span.marks);
|
|
77
|
+
}
|
|
78
|
+
if (span.text === '') {
|
|
79
|
+
el.appendChild(document.createElement('br'));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
el.appendChild(document.createTextNode(span.text));
|
|
83
|
+
}
|
|
84
|
+
return el;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a DOM block element for an RTIF block.
|
|
88
|
+
*
|
|
89
|
+
* Each block renders as a `<div>` with `data-rtif-block` set to the block ID
|
|
90
|
+
* and `data-block-type` set to the block type. Child span elements are created
|
|
91
|
+
* for each span in the block.
|
|
92
|
+
*
|
|
93
|
+
* When a `blockRenderers` registry is provided, the renderer for the block's
|
|
94
|
+
* type is applied to the element (adding CSS classes, ARIA roles, etc.).
|
|
95
|
+
*
|
|
96
|
+
* @param block - The RTIF block to render
|
|
97
|
+
* @param markRenderers - Optional registry of mark renderers for visual styling
|
|
98
|
+
* @param blockRenderers - Optional registry of block renderers for block-type styling
|
|
99
|
+
* @returns A `<div data-rtif-block="{id}" data-block-type="{type}">` element
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const el = createBlockElement({
|
|
104
|
+
* id: 'b1',
|
|
105
|
+
* type: 'text',
|
|
106
|
+
* spans: [{ text: 'Hello world' }],
|
|
107
|
+
* });
|
|
108
|
+
* // <div data-rtif-block="b1" data-block-type="text">
|
|
109
|
+
* // <span data-rtif-span>Hello world</span>
|
|
110
|
+
* // </div>
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function createBlockElement(block, markRenderers, blockRenderers) {
|
|
114
|
+
const el = document.createElement('div');
|
|
115
|
+
el.setAttribute('data-rtif-block', block.id);
|
|
116
|
+
el.setAttribute('data-block-type', block.type);
|
|
117
|
+
if (blockRenderers) {
|
|
118
|
+
blockRenderers.applyToElement(el, block);
|
|
119
|
+
}
|
|
120
|
+
const container = getSpanContainer(el, block.type, blockRenderers);
|
|
121
|
+
for (const span of block.spans) {
|
|
122
|
+
container.appendChild(createSpanElement(span, markRenderers));
|
|
123
|
+
}
|
|
124
|
+
return el;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Find a block DOM element within a root by its RTIF block ID.
|
|
128
|
+
*
|
|
129
|
+
* @param root - The root element to search within
|
|
130
|
+
* @param blockId - The block ID to find (matches `data-rtif-block` attribute)
|
|
131
|
+
* @returns The matching element, or `null` if not found
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const blockEl = findBlockElement(root, 'b1');
|
|
136
|
+
* if (blockEl) {
|
|
137
|
+
* console.log(blockEl.textContent);
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function findBlockElement(root, blockId) {
|
|
142
|
+
return root.querySelector(`[data-rtif-block="${blockId}"]`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Perform a full initial render of an RTIF document into a root element.
|
|
146
|
+
*
|
|
147
|
+
* Clears all existing content in the root, then creates block and span
|
|
148
|
+
* elements for the entire document. Use this for the first render; use
|
|
149
|
+
* {@link reconcile} for subsequent updates.
|
|
150
|
+
*
|
|
151
|
+
* @param root - The root DOM element (typically `contenteditable`)
|
|
152
|
+
* @param doc - The RTIF document to render
|
|
153
|
+
* @param markRenderers - Optional registry of mark renderers for visual styling
|
|
154
|
+
* @param blockRenderers - Optional registry of block renderers for block-type styling
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const root = document.getElementById('editor')!;
|
|
159
|
+
* renderInitial(root, {
|
|
160
|
+
* version: 1,
|
|
161
|
+
* blocks: [{ id: 'b1', type: 'text', spans: [{ text: 'Hello' }] }],
|
|
162
|
+
* });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export function renderInitial(root, doc, markRenderers, blockRenderers) {
|
|
166
|
+
// Clear all existing content without using innerHTML
|
|
167
|
+
while (root.firstChild) {
|
|
168
|
+
root.removeChild(root.firstChild);
|
|
169
|
+
}
|
|
170
|
+
for (const block of doc.blocks) {
|
|
171
|
+
root.appendChild(createBlockElement(block, markRenderers, blockRenderers));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Update the content of a span DOM element to match an RTIF span.
|
|
176
|
+
*
|
|
177
|
+
* Handles the transition between empty spans (`<br>`) and text spans
|
|
178
|
+
* (Text node) efficiently by reusing existing Text nodes when possible.
|
|
179
|
+
*
|
|
180
|
+
* @param spanEl - The existing span DOM element
|
|
181
|
+
* @param span - The RTIF span with the desired content
|
|
182
|
+
*/
|
|
183
|
+
function reconcileSpanContent(spanEl, span) {
|
|
184
|
+
if (span.text === '') {
|
|
185
|
+
// Empty span: must contain a <br>
|
|
186
|
+
while (spanEl.firstChild) {
|
|
187
|
+
spanEl.removeChild(spanEl.firstChild);
|
|
188
|
+
}
|
|
189
|
+
spanEl.appendChild(document.createElement('br'));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Non-empty span: must contain a Text node
|
|
193
|
+
const firstChild = spanEl.firstChild;
|
|
194
|
+
if (firstChild && firstChild.nodeType === Node.TEXT_NODE) {
|
|
195
|
+
// Reuse existing text node, just update value
|
|
196
|
+
firstChild.nodeValue = span.text;
|
|
197
|
+
// Remove any extra children (shouldn't happen, but be safe)
|
|
198
|
+
while (spanEl.childNodes.length > 1) {
|
|
199
|
+
spanEl.removeChild(spanEl.lastChild);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Replace all children with a text node
|
|
204
|
+
while (spanEl.firstChild) {
|
|
205
|
+
spanEl.removeChild(spanEl.firstChild);
|
|
206
|
+
}
|
|
207
|
+
spanEl.appendChild(document.createTextNode(span.text));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reconcile the span children of a block element to match an RTIF block.
|
|
213
|
+
*
|
|
214
|
+
* Compares spans by reference identity to skip unchanged spans. When a span
|
|
215
|
+
* has changed, checks whether marks also changed — if so, replaces the
|
|
216
|
+
* entire span element to reflect new mark styling; otherwise, updates only
|
|
217
|
+
* the text content.
|
|
218
|
+
*
|
|
219
|
+
* @param container - The DOM element containing span children (may be the
|
|
220
|
+
* block element itself or a content container for composite blocks)
|
|
221
|
+
* @param prevBlock - The previous RTIF block state
|
|
222
|
+
* @param nextBlock - The new RTIF block state
|
|
223
|
+
* @param markRenderers - Optional registry of mark renderers
|
|
224
|
+
*/
|
|
225
|
+
function reconcileBlockSpans(container, prevBlock, nextBlock, markRenderers) {
|
|
226
|
+
const prevSpans = prevBlock.spans;
|
|
227
|
+
const nextSpans = nextBlock.spans;
|
|
228
|
+
const spanElements = container.children;
|
|
229
|
+
// Update or replace existing spans
|
|
230
|
+
const minLen = Math.min(prevSpans.length, nextSpans.length);
|
|
231
|
+
for (let i = 0; i < minLen; i++) {
|
|
232
|
+
const prevSpan = prevSpans[i];
|
|
233
|
+
const nextSpan = nextSpans[i];
|
|
234
|
+
// Skip if same reference
|
|
235
|
+
if (prevSpan === nextSpan) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const spanEl = spanElements[i];
|
|
239
|
+
if (spanEl) {
|
|
240
|
+
// If marks changed, try replacer update() before full replacement
|
|
241
|
+
if (prevSpan.marks !== nextSpan.marks) {
|
|
242
|
+
let handled = false;
|
|
243
|
+
if (markRenderers && nextSpan.marks) {
|
|
244
|
+
for (const markType of Object.keys(nextSpan.marks)) {
|
|
245
|
+
if (nextSpan.marks[markType] == null)
|
|
246
|
+
continue;
|
|
247
|
+
const replacer = markRenderers.getReplacer(markType);
|
|
248
|
+
if (replacer?.update) {
|
|
249
|
+
handled = replacer.update(spanEl, nextSpan.text, nextSpan.marks);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!handled) {
|
|
255
|
+
const newSpanEl = createSpanElement(nextSpan, markRenderers);
|
|
256
|
+
container.replaceChild(newSpanEl, spanEl);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Only text changed — check replacer update() for text-only changes too
|
|
261
|
+
let handled = false;
|
|
262
|
+
if (markRenderers && nextSpan.marks) {
|
|
263
|
+
for (const markType of Object.keys(nextSpan.marks)) {
|
|
264
|
+
if (nextSpan.marks[markType] == null)
|
|
265
|
+
continue;
|
|
266
|
+
const replacer = markRenderers.getReplacer(markType);
|
|
267
|
+
if (replacer?.update) {
|
|
268
|
+
handled = replacer.update(spanEl, nextSpan.text, nextSpan.marks);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!handled) {
|
|
274
|
+
reconcileSpanContent(spanEl, nextSpan);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Remove extra spans (next has fewer)
|
|
280
|
+
while (spanElements.length > nextSpans.length) {
|
|
281
|
+
container.removeChild(container.lastElementChild);
|
|
282
|
+
}
|
|
283
|
+
// Add new spans (next has more)
|
|
284
|
+
for (let i = prevSpans.length; i < nextSpans.length; i++) {
|
|
285
|
+
const nextSpan = nextSpans[i];
|
|
286
|
+
container.appendChild(createSpanElement(nextSpan, markRenderers));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Efficiently reconcile the DOM to match a new RTIF document state.
|
|
291
|
+
*
|
|
292
|
+
* Only touches blocks that have changed (detected by object reference
|
|
293
|
+
* comparison). Handles block additions, removals, reordering, and
|
|
294
|
+
* span-level content updates within changed blocks. When a block's type
|
|
295
|
+
* or attrs change (by reference), the entire block element is replaced
|
|
296
|
+
* to ensure block-renderer styling is re-applied cleanly.
|
|
297
|
+
*
|
|
298
|
+
* **Critical**: When `composingBlockId` is set (IME composition active),
|
|
299
|
+
* the block with that ID is skipped entirely to avoid disrupting the
|
|
300
|
+
* browser's native composition rendering.
|
|
301
|
+
*
|
|
302
|
+
* @param root - The root DOM element containing block elements
|
|
303
|
+
* @param prevDoc - The previous RTIF document state
|
|
304
|
+
* @param nextDoc - The new RTIF document state to reconcile toward
|
|
305
|
+
* @param composingBlockId - Block ID currently under IME composition, or `null`
|
|
306
|
+
* @param markRenderers - Optional registry of mark renderers for visual styling
|
|
307
|
+
* @param blockRenderers - Optional registry of block renderers for block-type styling
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```ts
|
|
311
|
+
* // After an engine dispatch produces a new document:
|
|
312
|
+
* reconcile(root, prevDoc, nextDoc, composingBlockId, markRenderers, blockRenderers);
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
export function reconcile(root, prevDoc, nextDoc, composingBlockId, markRenderers, blockRenderers) {
|
|
316
|
+
// Fast path: identical document reference
|
|
317
|
+
if (prevDoc === nextDoc) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Build a lookup of previous blocks by ID for reference comparison
|
|
321
|
+
const prevBlockMap = new Map();
|
|
322
|
+
for (const block of prevDoc.blocks) {
|
|
323
|
+
prevBlockMap.set(block.id, block);
|
|
324
|
+
}
|
|
325
|
+
// Build a map of existing DOM block elements by ID
|
|
326
|
+
const existingElements = new Map();
|
|
327
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
328
|
+
const child = root.children[i];
|
|
329
|
+
const blockId = child.getAttribute('data-rtif-block');
|
|
330
|
+
if (blockId) {
|
|
331
|
+
existingElements.set(blockId, child);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Track which block IDs are in the next document
|
|
335
|
+
const nextBlockIds = new Set();
|
|
336
|
+
// Walk next blocks in order, using a reference node cursor to minimize moves
|
|
337
|
+
let referenceNode = root.firstChild;
|
|
338
|
+
for (const nextBlock of nextDoc.blocks) {
|
|
339
|
+
nextBlockIds.add(nextBlock.id);
|
|
340
|
+
let blockEl = existingElements.get(nextBlock.id) ?? null;
|
|
341
|
+
if (blockEl) {
|
|
342
|
+
// Block element already exists in the DOM
|
|
343
|
+
const prevBlock = prevBlockMap.get(nextBlock.id);
|
|
344
|
+
// Skip composing block entirely
|
|
345
|
+
if (nextBlock.id === composingBlockId) {
|
|
346
|
+
// Just make sure it's in the right position
|
|
347
|
+
if (blockEl !== referenceNode) {
|
|
348
|
+
root.insertBefore(blockEl, referenceNode);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
referenceNode = referenceNode.nextSibling;
|
|
352
|
+
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// Check if block changed by reference
|
|
356
|
+
if (prevBlock !== nextBlock) {
|
|
357
|
+
// If block type or attrs changed, replace entire block element
|
|
358
|
+
// to ensure block-renderer styling is re-applied cleanly.
|
|
359
|
+
// This mirrors how mark changes replace span elements.
|
|
360
|
+
if (prevBlock && (prevBlock.type !== nextBlock.type || prevBlock.attrs !== nextBlock.attrs)) {
|
|
361
|
+
const newBlockEl = createBlockElement(nextBlock, markRenderers, blockRenderers);
|
|
362
|
+
root.replaceChild(newBlockEl, blockEl);
|
|
363
|
+
// Update our reference so position tracking remains correct
|
|
364
|
+
existingElements.set(nextBlock.id, newBlockEl);
|
|
365
|
+
blockEl = newBlockEl;
|
|
366
|
+
// replaceChild keeps position; advance referenceNode past replaced element
|
|
367
|
+
referenceNode = newBlockEl.nextSibling;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
// Update block-type attribute if changed
|
|
371
|
+
if (blockEl.getAttribute('data-block-type') !== nextBlock.type) {
|
|
372
|
+
blockEl.setAttribute('data-block-type', nextBlock.type);
|
|
373
|
+
}
|
|
374
|
+
// Reconcile spans within this block
|
|
375
|
+
if (prevBlock) {
|
|
376
|
+
const container = getSpanContainer(blockEl, nextBlock.type, blockRenderers);
|
|
377
|
+
reconcileBlockSpans(container, prevBlock, nextBlock, markRenderers);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// No previous block data — full re-render of span content
|
|
381
|
+
const container = getSpanContainer(blockEl, nextBlock.type, blockRenderers);
|
|
382
|
+
while (container.firstChild) {
|
|
383
|
+
container.removeChild(container.firstChild);
|
|
384
|
+
}
|
|
385
|
+
for (const span of nextBlock.spans) {
|
|
386
|
+
container.appendChild(createSpanElement(span, markRenderers));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Ensure correct position
|
|
391
|
+
if (blockEl !== referenceNode) {
|
|
392
|
+
root.insertBefore(blockEl, referenceNode);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
referenceNode = referenceNode.nextSibling;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// New block — create element and insert at correct position
|
|
400
|
+
blockEl = createBlockElement(nextBlock, markRenderers, blockRenderers);
|
|
401
|
+
root.insertBefore(blockEl, referenceNode);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Remove any block elements that are no longer in the document
|
|
405
|
+
const toRemove = [];
|
|
406
|
+
for (const [blockId, el] of existingElements) {
|
|
407
|
+
if (!nextBlockIds.has(blockId)) {
|
|
408
|
+
toRemove.push(el);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const el of toRemove) {
|
|
412
|
+
root.removeChild(el);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-to-cursor utility.
|
|
3
|
+
*
|
|
4
|
+
* After every operation that moves the cursor (typing, Enter, paste, undo),
|
|
5
|
+
* the implementation MUST ensure the cursor is visible within the scrollable
|
|
6
|
+
* viewport (platform-requirements §12.1).
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Scroll the viewport so the current cursor (DOM focus point) is visible.
|
|
12
|
+
*
|
|
13
|
+
* Uses the browser's native `scrollIntoView` on the element nearest the
|
|
14
|
+
* caret. Gracefully no-ops when there is no selection, the focus is outside
|
|
15
|
+
* the editor root, or layout information is unavailable (e.g. jsdom).
|
|
16
|
+
*
|
|
17
|
+
* @param root - The contenteditable root element
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* scrollToCursor(editorRoot);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function scrollToCursor(root: HTMLElement): void;
|
|
25
|
+
//# sourceMappingURL=scroll-to-cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-to-cursor.d.ts","sourceRoot":"","sources":["../src/scroll-to-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAqCtD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll-to-cursor utility.
|
|
3
|
+
*
|
|
4
|
+
* After every operation that moves the cursor (typing, Enter, paste, undo),
|
|
5
|
+
* the implementation MUST ensure the cursor is visible within the scrollable
|
|
6
|
+
* viewport (platform-requirements §12.1).
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Scroll the viewport so the current cursor (DOM focus point) is visible.
|
|
12
|
+
*
|
|
13
|
+
* Uses the browser's native `scrollIntoView` on the element nearest the
|
|
14
|
+
* caret. Gracefully no-ops when there is no selection, the focus is outside
|
|
15
|
+
* the editor root, or layout information is unavailable (e.g. jsdom).
|
|
16
|
+
*
|
|
17
|
+
* @param root - The contenteditable root element
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* scrollToCursor(editorRoot);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function scrollToCursor(root) {
|
|
25
|
+
const sel = root.ownerDocument.defaultView?.getSelection();
|
|
26
|
+
if (!sel || sel.rangeCount === 0)
|
|
27
|
+
return;
|
|
28
|
+
const { focusNode, focusOffset } = sel;
|
|
29
|
+
if (!focusNode)
|
|
30
|
+
return;
|
|
31
|
+
// Ensure the focus is inside the editor root
|
|
32
|
+
if (!root.contains(focusNode))
|
|
33
|
+
return;
|
|
34
|
+
// Create a collapsed range at the focus point
|
|
35
|
+
const range = root.ownerDocument.createRange();
|
|
36
|
+
try {
|
|
37
|
+
range.setStart(focusNode, focusOffset);
|
|
38
|
+
range.collapse(true);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Invalid offset — bail
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// getBoundingClientRect may not exist in non-browser environments (jsdom)
|
|
45
|
+
if (typeof range.getBoundingClientRect !== 'function')
|
|
46
|
+
return;
|
|
47
|
+
const rect = range.getBoundingClientRect();
|
|
48
|
+
// Zero-dimension rect means no layout (jsdom, detached element, etc.)
|
|
49
|
+
if (rect.width === 0 && rect.height === 0)
|
|
50
|
+
return;
|
|
51
|
+
// Find the nearest element ancestor to call scrollIntoView on
|
|
52
|
+
const element = focusNode.nodeType === Node.ELEMENT_NODE
|
|
53
|
+
? focusNode
|
|
54
|
+
: focusNode.parentElement;
|
|
55
|
+
if (!element)
|
|
56
|
+
return;
|
|
57
|
+
element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=scroll-to-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-to-cursor.js","sourceRoot":"","sources":["../src/scroll-to-cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,IAAiB;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,YAAY,EAAE,CAAC;IAC3D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,KAAK,CAAC;QAAE,OAAO;IAEzC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC;IACvC,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,6CAA6C;IAC7C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO;IAEtC,8CAA8C;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC;IAC/C,IAAI,CAAC;QACH,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACvC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,wBAAwB;QACxB,OAAO;IACT,CAAC;IAED,0EAA0E;IAC1E,IAAI,OAAO,KAAK,CAAC,qBAAqB,KAAK,UAAU;QAAE,OAAO;IAE9D,MAAM,IAAI,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAC;IAE3C,sEAAsE;IACtE,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElD,8DAA8D;IAC9D,MAAM,OAAO,GACX,SAAS,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY;QACtC,CAAC,CAAE,SAAqB;QACxB,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,OAAO,CAAC,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional DOM Selection <-> RTIF Selection synchronization.
|
|
3
|
+
*
|
|
4
|
+
* Converts between the browser's Selection API (anchorNode/offset,
|
|
5
|
+
* focusNode/offset) and RTIF's absolute integer offset Selection
|
|
6
|
+
* ({anchor: {offset}, focus: {offset}}).
|
|
7
|
+
*
|
|
8
|
+
* Relies on the DOM structure produced by the renderer:
|
|
9
|
+
* - Root: `[data-rtif-root]` contenteditable div
|
|
10
|
+
* - Blocks: `div[data-rtif-block="{id}"]`
|
|
11
|
+
* - Spans: `span[data-rtif-span]` with Text node or `<br>` children
|
|
12
|
+
*/
|
|
13
|
+
import type { Document, Selection } from '@rtif-sdk/core';
|
|
14
|
+
import type { BlockOffsetEntry, DomPoint } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Build an array of block offset entries for fast DOM <-> RTIF conversion.
|
|
17
|
+
*
|
|
18
|
+
* Each entry records the block's ID, its absolute start offset in the
|
|
19
|
+
* document, and its text length. Block N+1 starts at
|
|
20
|
+
* `block[N].startOffset + block[N].length + 1` (the +1 is the virtual `\n`
|
|
21
|
+
* separator between consecutive blocks).
|
|
22
|
+
*
|
|
23
|
+
* @param doc - The RTIF document
|
|
24
|
+
* @returns Array of offset entries, one per block, in document order
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const doc = { version: 1, blocks: [
|
|
29
|
+
* { id: 'b1', type: 'text', spans: [{ text: 'hello' }] },
|
|
30
|
+
* { id: 'b2', type: 'text', spans: [{ text: 'world' }] },
|
|
31
|
+
* ]};
|
|
32
|
+
* buildBlockOffsetCache(doc);
|
|
33
|
+
* // => [
|
|
34
|
+
* // { blockId: 'b1', startOffset: 0, length: 5 },
|
|
35
|
+
* // { blockId: 'b2', startOffset: 6, length: 5 },
|
|
36
|
+
* // ]
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildBlockOffsetCache(doc: Document): BlockOffsetEntry[];
|
|
40
|
+
/**
|
|
41
|
+
* Convert a DOM position (node + offset) to an absolute RTIF document offset.
|
|
42
|
+
*
|
|
43
|
+
* Walks up from the given node to find the containing block element
|
|
44
|
+
* (`[data-rtif-block]`), then walks the block's span children to compute the
|
|
45
|
+
* local character offset. Adds the block's start offset from the cache.
|
|
46
|
+
*
|
|
47
|
+
* @param root - The editor's root element (`[data-rtif-root]`)
|
|
48
|
+
* @param node - The DOM node containing the position
|
|
49
|
+
* @param domOffset - The offset within the node (character offset for Text nodes,
|
|
50
|
+
* child index for Element nodes)
|
|
51
|
+
* @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
|
|
52
|
+
* @returns The absolute RTIF offset, or `null` if the node is outside the
|
|
53
|
+
* editor or not inside a recognized block
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const textNode = blockEl.querySelector('[data-rtif-span]')!.firstChild!;
|
|
58
|
+
* const rtifOffset = domPointToRtifOffset(root, textNode, 3, cache);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function domPointToRtifOffset(root: HTMLElement, node: Node, domOffset: number, cache: BlockOffsetEntry[]): number | null;
|
|
62
|
+
/**
|
|
63
|
+
* Convert an absolute RTIF document offset to a DOM point (node + offset).
|
|
64
|
+
*
|
|
65
|
+
* Uses `resolve()` from `@rtif-sdk/core` to find the block index and local offset,
|
|
66
|
+
* then walks the block's span elements in the DOM to locate the correct text
|
|
67
|
+
* node and character position.
|
|
68
|
+
*
|
|
69
|
+
* @param root - The editor's root element (`[data-rtif-root]`)
|
|
70
|
+
* @param doc - The current RTIF document (must match the DOM structure)
|
|
71
|
+
* @param offset - The absolute RTIF document offset
|
|
72
|
+
* @returns A DOM point, or `null` if the DOM is out of sync with the document
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const point = rtifOffsetToDomPoint(root, doc, 5);
|
|
77
|
+
* if (point) {
|
|
78
|
+
* console.log(point.node, point.offset);
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function rtifOffsetToDomPoint(root: HTMLElement, doc: Document, offset: number): DomPoint | null;
|
|
83
|
+
/**
|
|
84
|
+
* Read the browser's current DOM selection and convert it to an RTIF Selection.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` if:
|
|
87
|
+
* - There is no selection or no ranges
|
|
88
|
+
* - The selection is outside the editor root
|
|
89
|
+
* - Either anchor or focus cannot be resolved to an RTIF offset
|
|
90
|
+
*
|
|
91
|
+
* @param root - The editor's root element (`[data-rtif-root]`)
|
|
92
|
+
* @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
|
|
93
|
+
* @returns The RTIF Selection, or `null` if the selection cannot be resolved
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const sel = readDomSelection(root, cache);
|
|
98
|
+
* if (sel) {
|
|
99
|
+
* engine.dispatch({ type: 'set_selection', selection: sel });
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function readDomSelection(root: HTMLElement, cache: BlockOffsetEntry[]): Selection | null;
|
|
104
|
+
/**
|
|
105
|
+
* Set the browser's DOM selection to match an RTIF Selection.
|
|
106
|
+
*
|
|
107
|
+
* Converts the RTIF anchor and focus offsets to DOM points, then calls
|
|
108
|
+
* `setBaseAndExtent()` on the window's Selection object.
|
|
109
|
+
*
|
|
110
|
+
* Sets the suppression flag to `true` before modifying the selection, to
|
|
111
|
+
* prevent the `selectionchange` event handler from triggering a feedback
|
|
112
|
+
* loop. The flag is reset via `requestAnimationFrame`.
|
|
113
|
+
*
|
|
114
|
+
* If either offset cannot be resolved to a DOM point (e.g., DOM is out of
|
|
115
|
+
* sync), the function is a no-op.
|
|
116
|
+
*
|
|
117
|
+
* @param root - The editor's root element (`[data-rtif-root]`)
|
|
118
|
+
* @param doc - The current RTIF document
|
|
119
|
+
* @param selection - The RTIF selection to apply to the DOM
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* setDomSelection(root, doc, { anchor: { offset: 0 }, focus: { offset: 5 } });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare function setDomSelection(root: HTMLElement, doc: Document, selection: Selection): void;
|
|
127
|
+
/**
|
|
128
|
+
* Returns `true` if the selection-sync layer is currently suppressing
|
|
129
|
+
* `selectionchange` events to avoid feedback loops.
|
|
130
|
+
*
|
|
131
|
+
* The `selectionchange` handler in the input bridge should check this flag
|
|
132
|
+
* and skip processing when it returns `true`.
|
|
133
|
+
*
|
|
134
|
+
* @returns Whether selectionchange events should be suppressed
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* document.addEventListener('selectionchange', () => {
|
|
139
|
+
* if (isSuppressed()) return;
|
|
140
|
+
* handleUserSelectionChange();
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export declare function isSuppressed(): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Manually reset the suppression flag to `false`.
|
|
147
|
+
*
|
|
148
|
+
* Primarily useful for testing, where `requestAnimationFrame` may not run.
|
|
149
|
+
* Production code should not need to call this.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* afterEach(() => {
|
|
154
|
+
* resetSuppression();
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare function resetSuppression(): void;
|
|
159
|
+
//# sourceMappingURL=selection-sync.d.ts.map
|