@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.
Files changed (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/block-drag-handler.d.ts +189 -0
  4. package/dist/block-drag-handler.d.ts.map +1 -0
  5. package/dist/block-drag-handler.js +745 -0
  6. package/dist/block-drag-handler.js.map +1 -0
  7. package/dist/block-renderer.d.ts +402 -0
  8. package/dist/block-renderer.d.ts.map +1 -0
  9. package/dist/block-renderer.js +424 -0
  10. package/dist/block-renderer.js.map +1 -0
  11. package/dist/clipboard.d.ts +178 -0
  12. package/dist/clipboard.d.ts.map +1 -0
  13. package/dist/clipboard.js +432 -0
  14. package/dist/clipboard.js.map +1 -0
  15. package/dist/command-bus.d.ts +113 -0
  16. package/dist/command-bus.d.ts.map +1 -0
  17. package/dist/command-bus.js +70 -0
  18. package/dist/command-bus.js.map +1 -0
  19. package/dist/composition.d.ts +220 -0
  20. package/dist/composition.d.ts.map +1 -0
  21. package/dist/composition.js +271 -0
  22. package/dist/composition.js.map +1 -0
  23. package/dist/content-extraction.d.ts +69 -0
  24. package/dist/content-extraction.d.ts.map +1 -0
  25. package/dist/content-extraction.js +228 -0
  26. package/dist/content-extraction.js.map +1 -0
  27. package/dist/content-handler-file.d.ts +40 -0
  28. package/dist/content-handler-file.d.ts.map +1 -0
  29. package/dist/content-handler-file.js +91 -0
  30. package/dist/content-handler-file.js.map +1 -0
  31. package/dist/content-handler-image.d.ts +82 -0
  32. package/dist/content-handler-image.d.ts.map +1 -0
  33. package/dist/content-handler-image.js +120 -0
  34. package/dist/content-handler-image.js.map +1 -0
  35. package/dist/content-handler-url.d.ts +129 -0
  36. package/dist/content-handler-url.d.ts.map +1 -0
  37. package/dist/content-handler-url.js +244 -0
  38. package/dist/content-handler-url.js.map +1 -0
  39. package/dist/content-handlers.d.ts +67 -0
  40. package/dist/content-handlers.d.ts.map +1 -0
  41. package/dist/content-handlers.js +263 -0
  42. package/dist/content-handlers.js.map +1 -0
  43. package/dist/content-pipeline.d.ts +383 -0
  44. package/dist/content-pipeline.d.ts.map +1 -0
  45. package/dist/content-pipeline.js +232 -0
  46. package/dist/content-pipeline.js.map +1 -0
  47. package/dist/cursor-nav.d.ts +149 -0
  48. package/dist/cursor-nav.d.ts.map +1 -0
  49. package/dist/cursor-nav.js +230 -0
  50. package/dist/cursor-nav.js.map +1 -0
  51. package/dist/cursor-rect.d.ts +65 -0
  52. package/dist/cursor-rect.d.ts.map +1 -0
  53. package/dist/cursor-rect.js +98 -0
  54. package/dist/cursor-rect.js.map +1 -0
  55. package/dist/drop-indicator.d.ts +108 -0
  56. package/dist/drop-indicator.d.ts.map +1 -0
  57. package/dist/drop-indicator.js +236 -0
  58. package/dist/drop-indicator.js.map +1 -0
  59. package/dist/editor.d.ts +41 -0
  60. package/dist/editor.d.ts.map +1 -0
  61. package/dist/editor.js +710 -0
  62. package/dist/editor.js.map +1 -0
  63. package/dist/floating-toolbar.d.ts +93 -0
  64. package/dist/floating-toolbar.d.ts.map +1 -0
  65. package/dist/floating-toolbar.js +159 -0
  66. package/dist/floating-toolbar.js.map +1 -0
  67. package/dist/index.d.ts +62 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +119 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/input-bridge.d.ts +273 -0
  72. package/dist/input-bridge.d.ts.map +1 -0
  73. package/dist/input-bridge.js +884 -0
  74. package/dist/input-bridge.js.map +1 -0
  75. package/dist/link-popover.d.ts +38 -0
  76. package/dist/link-popover.d.ts.map +1 -0
  77. package/dist/link-popover.js +278 -0
  78. package/dist/link-popover.js.map +1 -0
  79. package/dist/mark-renderer.d.ts +275 -0
  80. package/dist/mark-renderer.d.ts.map +1 -0
  81. package/dist/mark-renderer.js +210 -0
  82. package/dist/mark-renderer.js.map +1 -0
  83. package/dist/perf.d.ts +145 -0
  84. package/dist/perf.d.ts.map +1 -0
  85. package/dist/perf.js +260 -0
  86. package/dist/perf.js.map +1 -0
  87. package/dist/plugin-kit.d.ts +265 -0
  88. package/dist/plugin-kit.d.ts.map +1 -0
  89. package/dist/plugin-kit.js +234 -0
  90. package/dist/plugin-kit.js.map +1 -0
  91. package/dist/plugins/alignment-plugin.d.ts +68 -0
  92. package/dist/plugins/alignment-plugin.d.ts.map +1 -0
  93. package/dist/plugins/alignment-plugin.js +98 -0
  94. package/dist/plugins/alignment-plugin.js.map +1 -0
  95. package/dist/plugins/block-utils.d.ts +113 -0
  96. package/dist/plugins/block-utils.d.ts.map +1 -0
  97. package/dist/plugins/block-utils.js +191 -0
  98. package/dist/plugins/block-utils.js.map +1 -0
  99. package/dist/plugins/blockquote-plugin.d.ts +39 -0
  100. package/dist/plugins/blockquote-plugin.d.ts.map +1 -0
  101. package/dist/plugins/blockquote-plugin.js +88 -0
  102. package/dist/plugins/blockquote-plugin.js.map +1 -0
  103. package/dist/plugins/bold-plugin.d.ts +37 -0
  104. package/dist/plugins/bold-plugin.d.ts.map +1 -0
  105. package/dist/plugins/bold-plugin.js +48 -0
  106. package/dist/plugins/bold-plugin.js.map +1 -0
  107. package/dist/plugins/callout-plugin.d.ts +100 -0
  108. package/dist/plugins/callout-plugin.d.ts.map +1 -0
  109. package/dist/plugins/callout-plugin.js +200 -0
  110. package/dist/plugins/callout-plugin.js.map +1 -0
  111. package/dist/plugins/code-block-plugin.d.ts +62 -0
  112. package/dist/plugins/code-block-plugin.d.ts.map +1 -0
  113. package/dist/plugins/code-block-plugin.js +176 -0
  114. package/dist/plugins/code-block-plugin.js.map +1 -0
  115. package/dist/plugins/code-plugin.d.ts +37 -0
  116. package/dist/plugins/code-plugin.d.ts.map +1 -0
  117. package/dist/plugins/code-plugin.js +48 -0
  118. package/dist/plugins/code-plugin.js.map +1 -0
  119. package/dist/plugins/embed-plugin.d.ts +90 -0
  120. package/dist/plugins/embed-plugin.d.ts.map +1 -0
  121. package/dist/plugins/embed-plugin.js +147 -0
  122. package/dist/plugins/embed-plugin.js.map +1 -0
  123. package/dist/plugins/font-family-plugin.d.ts +58 -0
  124. package/dist/plugins/font-family-plugin.d.ts.map +1 -0
  125. package/dist/plugins/font-family-plugin.js +57 -0
  126. package/dist/plugins/font-family-plugin.js.map +1 -0
  127. package/dist/plugins/font-size-plugin.d.ts +57 -0
  128. package/dist/plugins/font-size-plugin.d.ts.map +1 -0
  129. package/dist/plugins/font-size-plugin.js +56 -0
  130. package/dist/plugins/font-size-plugin.js.map +1 -0
  131. package/dist/plugins/heading-plugin.d.ts +52 -0
  132. package/dist/plugins/heading-plugin.d.ts.map +1 -0
  133. package/dist/plugins/heading-plugin.js +114 -0
  134. package/dist/plugins/heading-plugin.js.map +1 -0
  135. package/dist/plugins/hr-plugin.d.ts +33 -0
  136. package/dist/plugins/hr-plugin.d.ts.map +1 -0
  137. package/dist/plugins/hr-plugin.js +75 -0
  138. package/dist/plugins/hr-plugin.js.map +1 -0
  139. package/dist/plugins/image-plugin.d.ts +115 -0
  140. package/dist/plugins/image-plugin.d.ts.map +1 -0
  141. package/dist/plugins/image-plugin.js +199 -0
  142. package/dist/plugins/image-plugin.js.map +1 -0
  143. package/dist/plugins/indent-plugin.d.ts +62 -0
  144. package/dist/plugins/indent-plugin.d.ts.map +1 -0
  145. package/dist/plugins/indent-plugin.js +128 -0
  146. package/dist/plugins/indent-plugin.js.map +1 -0
  147. package/dist/plugins/index.d.ts +45 -0
  148. package/dist/plugins/index.d.ts.map +1 -0
  149. package/dist/plugins/index.js +42 -0
  150. package/dist/plugins/index.js.map +1 -0
  151. package/dist/plugins/italic-plugin.d.ts +37 -0
  152. package/dist/plugins/italic-plugin.d.ts.map +1 -0
  153. package/dist/plugins/italic-plugin.js +48 -0
  154. package/dist/plugins/italic-plugin.js.map +1 -0
  155. package/dist/plugins/link-plugin.d.ts +129 -0
  156. package/dist/plugins/link-plugin.d.ts.map +1 -0
  157. package/dist/plugins/link-plugin.js +212 -0
  158. package/dist/plugins/link-plugin.js.map +1 -0
  159. package/dist/plugins/list-plugin.d.ts +53 -0
  160. package/dist/plugins/list-plugin.d.ts.map +1 -0
  161. package/dist/plugins/list-plugin.js +309 -0
  162. package/dist/plugins/list-plugin.js.map +1 -0
  163. package/dist/plugins/mark-utils.d.ts +173 -0
  164. package/dist/plugins/mark-utils.d.ts.map +1 -0
  165. package/dist/plugins/mark-utils.js +425 -0
  166. package/dist/plugins/mark-utils.js.map +1 -0
  167. package/dist/plugins/mention-plugin.d.ts +191 -0
  168. package/dist/plugins/mention-plugin.d.ts.map +1 -0
  169. package/dist/plugins/mention-plugin.js +295 -0
  170. package/dist/plugins/mention-plugin.js.map +1 -0
  171. package/dist/plugins/strikethrough-plugin.d.ts +37 -0
  172. package/dist/plugins/strikethrough-plugin.d.ts.map +1 -0
  173. package/dist/plugins/strikethrough-plugin.js +48 -0
  174. package/dist/plugins/strikethrough-plugin.js.map +1 -0
  175. package/dist/plugins/text-color-plugin.d.ts +57 -0
  176. package/dist/plugins/text-color-plugin.d.ts.map +1 -0
  177. package/dist/plugins/text-color-plugin.js +56 -0
  178. package/dist/plugins/text-color-plugin.js.map +1 -0
  179. package/dist/plugins/underline-plugin.d.ts +37 -0
  180. package/dist/plugins/underline-plugin.d.ts.map +1 -0
  181. package/dist/plugins/underline-plugin.js +48 -0
  182. package/dist/plugins/underline-plugin.js.map +1 -0
  183. package/dist/presets.d.ts +95 -0
  184. package/dist/presets.d.ts.map +1 -0
  185. package/dist/presets.js +159 -0
  186. package/dist/presets.js.map +1 -0
  187. package/dist/renderer.d.ts +125 -0
  188. package/dist/renderer.d.ts.map +1 -0
  189. package/dist/renderer.js +415 -0
  190. package/dist/renderer.js.map +1 -0
  191. package/dist/scroll-to-cursor.d.ts +25 -0
  192. package/dist/scroll-to-cursor.d.ts.map +1 -0
  193. package/dist/scroll-to-cursor.js +59 -0
  194. package/dist/scroll-to-cursor.js.map +1 -0
  195. package/dist/selection-sync.d.ts +159 -0
  196. package/dist/selection-sync.d.ts.map +1 -0
  197. package/dist/selection-sync.js +527 -0
  198. package/dist/selection-sync.js.map +1 -0
  199. package/dist/shortcut-handler.d.ts +98 -0
  200. package/dist/shortcut-handler.d.ts.map +1 -0
  201. package/dist/shortcut-handler.js +155 -0
  202. package/dist/shortcut-handler.js.map +1 -0
  203. package/dist/toolbar.d.ts +103 -0
  204. package/dist/toolbar.d.ts.map +1 -0
  205. package/dist/toolbar.js +134 -0
  206. package/dist/toolbar.js.map +1 -0
  207. package/dist/trigger-manager.d.ts +205 -0
  208. package/dist/trigger-manager.d.ts.map +1 -0
  209. package/dist/trigger-manager.js +466 -0
  210. package/dist/trigger-manager.js.map +1 -0
  211. package/dist/types.d.ts +216 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +2 -0
  214. package/dist/types.js.map +1 -0
  215. package/package.json +30 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-sync.d.ts","sourceRoot":"","sources":["../src/selection-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CAmBvE;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,gBAAgB,EAAE,GACxB,MAAM,GAAG,IAAI,CAoCf;AAoOD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,GACb,QAAQ,GAAG,IAAI,CAmBjB;AAuGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,gBAAgB,EAAE,GACxB,SAAS,GAAG,IAAI,CAsBlB;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,SAAS,GACnB,IAAI,CA6BN;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,527 @@
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 { blockTextLength, resolve } from '@rtif-sdk/core';
14
+ /** Module-level flag to suppress selectionchange feedback loops. */
15
+ let suppressSelectionChange = false;
16
+ // ---------------------------------------------------------------------------
17
+ // buildBlockOffsetCache
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Build an array of block offset entries for fast DOM <-> RTIF conversion.
21
+ *
22
+ * Each entry records the block's ID, its absolute start offset in the
23
+ * document, and its text length. Block N+1 starts at
24
+ * `block[N].startOffset + block[N].length + 1` (the +1 is the virtual `\n`
25
+ * separator between consecutive blocks).
26
+ *
27
+ * @param doc - The RTIF document
28
+ * @returns Array of offset entries, one per block, in document order
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const doc = { version: 1, blocks: [
33
+ * { id: 'b1', type: 'text', spans: [{ text: 'hello' }] },
34
+ * { id: 'b2', type: 'text', spans: [{ text: 'world' }] },
35
+ * ]};
36
+ * buildBlockOffsetCache(doc);
37
+ * // => [
38
+ * // { blockId: 'b1', startOffset: 0, length: 5 },
39
+ * // { blockId: 'b2', startOffset: 6, length: 5 },
40
+ * // ]
41
+ * ```
42
+ */
43
+ export function buildBlockOffsetCache(doc) {
44
+ const entries = [];
45
+ let offset = 0;
46
+ for (let i = 0; i < doc.blocks.length; i++) {
47
+ if (i > 0) {
48
+ offset += 1; // virtual \n separator
49
+ }
50
+ const block = doc.blocks[i];
51
+ const length = blockTextLength(block);
52
+ entries.push({
53
+ blockId: block.id,
54
+ startOffset: offset,
55
+ length,
56
+ });
57
+ offset += length;
58
+ }
59
+ return entries;
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // domPointToRtifOffset
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Convert a DOM position (node + offset) to an absolute RTIF document offset.
66
+ *
67
+ * Walks up from the given node to find the containing block element
68
+ * (`[data-rtif-block]`), then walks the block's span children to compute the
69
+ * local character offset. Adds the block's start offset from the cache.
70
+ *
71
+ * @param root - The editor's root element (`[data-rtif-root]`)
72
+ * @param node - The DOM node containing the position
73
+ * @param domOffset - The offset within the node (character offset for Text nodes,
74
+ * child index for Element nodes)
75
+ * @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
76
+ * @returns The absolute RTIF offset, or `null` if the node is outside the
77
+ * editor or not inside a recognized block
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const textNode = blockEl.querySelector('[data-rtif-span]')!.firstChild!;
82
+ * const rtifOffset = domPointToRtifOffset(root, textNode, 3, cache);
83
+ * ```
84
+ */
85
+ export function domPointToRtifOffset(root, node, domOffset, cache) {
86
+ // Special case: node IS the root element.
87
+ // domOffset is a child index referring to block elements.
88
+ if (node === root) {
89
+ return resolveRootOffset(root, domOffset, cache);
90
+ }
91
+ // Verify node is inside root
92
+ if (!root.contains(node)) {
93
+ return null;
94
+ }
95
+ // Find the containing block element
96
+ const blockEl = findContainingBlock(root, node);
97
+ if (blockEl === null) {
98
+ return null;
99
+ }
100
+ const blockId = blockEl.getAttribute('data-rtif-block');
101
+ if (blockId === null) {
102
+ return null;
103
+ }
104
+ // Look up block in cache
105
+ const cacheEntry = cache.find((e) => e.blockId === blockId);
106
+ if (cacheEntry === undefined) {
107
+ return null;
108
+ }
109
+ // Compute local offset within the block
110
+ const localOffset = computeLocalOffset(blockEl, node, domOffset);
111
+ if (localOffset === null) {
112
+ return null;
113
+ }
114
+ return cacheEntry.startOffset + localOffset;
115
+ }
116
+ /**
117
+ * Handle the case where `node` is the root element itself.
118
+ * `domOffset` is a child index referring to block-level children.
119
+ */
120
+ function resolveRootOffset(root, childIndex, cache) {
121
+ if (childIndex === 0) {
122
+ // Before first block
123
+ const first = cache[0];
124
+ return first !== undefined ? first.startOffset : null;
125
+ }
126
+ // After the block at childIndex - 1
127
+ const blockEl = root.children[childIndex - 1];
128
+ if (blockEl === undefined) {
129
+ // childIndex is past the last block — end of document
130
+ const last = cache[cache.length - 1];
131
+ return last !== undefined ? last.startOffset + last.length : null;
132
+ }
133
+ const blockId = blockEl.getAttribute('data-rtif-block');
134
+ if (blockId === null) {
135
+ return null;
136
+ }
137
+ const entry = cache.find((e) => e.blockId === blockId);
138
+ if (entry === undefined) {
139
+ return null;
140
+ }
141
+ // After this block: if there's a next block, return its start
142
+ const entryIndex = cache.indexOf(entry);
143
+ const nextEntry = cache[entryIndex + 1];
144
+ if (nextEntry !== undefined) {
145
+ return nextEntry.startOffset;
146
+ }
147
+ // After the last block — end of document
148
+ return entry.startOffset + entry.length;
149
+ }
150
+ /**
151
+ * Walk up the DOM from `node` to find the nearest ancestor (or self) that
152
+ * is a block element (`[data-rtif-block]`) within the root.
153
+ */
154
+ function findContainingBlock(root, node) {
155
+ let current = node;
156
+ while (current !== null && current !== root) {
157
+ if (current instanceof HTMLElement &&
158
+ current.hasAttribute('data-rtif-block')) {
159
+ return current;
160
+ }
161
+ current = current.parentNode;
162
+ }
163
+ return null;
164
+ }
165
+ /**
166
+ * Compute the local character offset within a block element.
167
+ *
168
+ * Handles three cases:
169
+ * 1. `node` is the block element itself — `domOffset` is a child (span) index.
170
+ * 2. `node` is a span element (`[data-rtif-span]`) — `domOffset` is a child index.
171
+ * 3. `node` is a Text node or BR — walk preceding spans to count characters.
172
+ */
173
+ function computeLocalOffset(blockEl, node, domOffset) {
174
+ // Case 1: node IS the block element
175
+ if (node === blockEl) {
176
+ return computeOffsetFromBlockChildIndex(blockEl, domOffset);
177
+ }
178
+ // Case 2: node is a span element with data-rtif-span
179
+ if (node instanceof HTMLElement &&
180
+ node.hasAttribute('data-rtif-span')) {
181
+ return computeOffsetFromSpanChildIndex(blockEl, node, domOffset);
182
+ }
183
+ // Case 3: node is a child of a span (Text node or BR)
184
+ const parentSpan = findParentSpan(node);
185
+ if (parentSpan === null) {
186
+ return null;
187
+ }
188
+ // Count characters in all preceding spans
189
+ const precedingChars = countPrecedingSpanChars(blockEl, parentSpan);
190
+ // For BR nodes (empty spans), the local offset is 0
191
+ if (node instanceof HTMLBRElement) {
192
+ return precedingChars;
193
+ }
194
+ // For Text nodes, add the domOffset within the text
195
+ if (node.nodeType === Node.TEXT_NODE) {
196
+ return precedingChars + domOffset;
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * When the cursor is on the block element itself, `domOffset` is a child index
202
+ * referring to span children. Sum text lengths of spans before that index.
203
+ */
204
+ function computeOffsetFromBlockChildIndex(blockEl, childIndex) {
205
+ const spans = blockEl.querySelectorAll('[data-rtif-span]');
206
+ let offset = 0;
207
+ for (let i = 0; i < childIndex && i < spans.length; i++) {
208
+ offset += getSpanTextLength(spans[i]);
209
+ }
210
+ return offset;
211
+ }
212
+ /**
213
+ * When the cursor is on a span element, `domOffset` is a child index within
214
+ * that span. Count preceding spans + content up to the child index.
215
+ */
216
+ function computeOffsetFromSpanChildIndex(blockEl, spanEl, childIndex) {
217
+ const precedingChars = countPrecedingSpanChars(blockEl, spanEl);
218
+ // If childIndex is 0, we're at the start of the span
219
+ if (childIndex === 0) {
220
+ return precedingChars;
221
+ }
222
+ // If childIndex >= 1, we're after the first child.
223
+ // For a span with a Text node child, this means end of text.
224
+ // For a span with a BR child, this still means offset 0 in text terms.
225
+ const firstChild = spanEl.firstChild;
226
+ if (firstChild !== null && firstChild.nodeType === Node.TEXT_NODE) {
227
+ return precedingChars + firstChild.textContent.length;
228
+ }
229
+ // BR or no content
230
+ return precedingChars;
231
+ }
232
+ /**
233
+ * Find the nearest parent `<span data-rtif-span>` element for a node.
234
+ */
235
+ function findParentSpan(node) {
236
+ let current = node.parentNode;
237
+ while (current !== null) {
238
+ if (current instanceof HTMLElement &&
239
+ current.hasAttribute('data-rtif-span')) {
240
+ return current;
241
+ }
242
+ current = current.parentNode;
243
+ }
244
+ return null;
245
+ }
246
+ /**
247
+ * Count the total text length of all `[data-rtif-span]` elements in a block
248
+ * that appear before the given span element in document order.
249
+ */
250
+ function countPrecedingSpanChars(blockEl, spanEl) {
251
+ const spans = blockEl.querySelectorAll('[data-rtif-span]');
252
+ let count = 0;
253
+ for (let i = 0; i < spans.length; i++) {
254
+ if (spans[i] === spanEl) {
255
+ break;
256
+ }
257
+ count += getSpanTextLength(spans[i]);
258
+ }
259
+ return count;
260
+ }
261
+ /**
262
+ * Escape a string for safe use inside a CSS attribute selector value.
263
+ * Replaces `\` and `"` with their escaped equivalents.
264
+ * This avoids depending on `CSS.escape()` which is not available in jsdom.
265
+ */
266
+ function escapeAttrValue(value) {
267
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
268
+ }
269
+ /**
270
+ * Get the text length of a span element. A span with only a `<br>` child
271
+ * has length 0.
272
+ */
273
+ function getSpanTextLength(spanEl) {
274
+ const firstChild = spanEl.firstChild;
275
+ if (firstChild === null) {
276
+ return 0;
277
+ }
278
+ if (firstChild.nodeType === Node.TEXT_NODE) {
279
+ return firstChild.textContent?.length ?? 0;
280
+ }
281
+ // BR or other element — length 0
282
+ return 0;
283
+ }
284
+ // ---------------------------------------------------------------------------
285
+ // rtifOffsetToDomPoint
286
+ // ---------------------------------------------------------------------------
287
+ /**
288
+ * Convert an absolute RTIF document offset to a DOM point (node + offset).
289
+ *
290
+ * Uses `resolve()` from `@rtif-sdk/core` to find the block index and local offset,
291
+ * then walks the block's span elements in the DOM to locate the correct text
292
+ * node and character position.
293
+ *
294
+ * @param root - The editor's root element (`[data-rtif-root]`)
295
+ * @param doc - The current RTIF document (must match the DOM structure)
296
+ * @param offset - The absolute RTIF document offset
297
+ * @returns A DOM point, or `null` if the DOM is out of sync with the document
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * const point = rtifOffsetToDomPoint(root, doc, 5);
302
+ * if (point) {
303
+ * console.log(point.node, point.offset);
304
+ * }
305
+ * ```
306
+ */
307
+ export function rtifOffsetToDomPoint(root, doc, offset) {
308
+ const resolved = resolve(doc, offset);
309
+ const block = doc.blocks[resolved.blockIndex];
310
+ if (block === undefined) {
311
+ return null;
312
+ }
313
+ const blockEl = root.querySelector(`[data-rtif-block="${escapeAttrValue(block.id)}"]`);
314
+ if (blockEl === null) {
315
+ return null;
316
+ }
317
+ return resolveLocalOffsetToDomPoint(blockEl, block, resolved.localOffset);
318
+ }
319
+ /**
320
+ * Walk span elements within a block to find the DOM point for a local offset.
321
+ *
322
+ * At span boundaries (localOffset equals the cumulative length at the end of
323
+ * a span), the point resolves to the end of that span's text node — consistent
324
+ * with RTIF's `insert_text` mark inheritance behavior.
325
+ *
326
+ * For empty blocks (single span with empty text / `<br>`), returns the span
327
+ * element itself at offset 0.
328
+ */
329
+ function resolveLocalOffsetToDomPoint(blockEl, block, localOffset) {
330
+ const spanEls = blockEl.querySelectorAll('[data-rtif-span]');
331
+ // Handle empty block: single span with empty text
332
+ if (block.spans.length === 1 &&
333
+ block.spans[0].text === '' &&
334
+ spanEls.length > 0) {
335
+ const spanEl = spanEls[0];
336
+ return { node: spanEl, offset: 0 };
337
+ }
338
+ let remaining = localOffset;
339
+ for (let i = 0; i < spanEls.length; i++) {
340
+ const spanEl = spanEls[i];
341
+ const span = block.spans[i];
342
+ if (span === undefined) {
343
+ return null;
344
+ }
345
+ const spanLen = span.text.length;
346
+ // If remaining fits within (or at end of) this span, this is the target
347
+ if (remaining <= spanLen) {
348
+ // Check if this span is non-editable (e.g., mention chip)
349
+ if (spanEl.getAttribute('contenteditable') === 'false') {
350
+ const container = spanEl.parentElement;
351
+ if (!container)
352
+ return null;
353
+ const childIndex = getChildIndex(container, spanEl);
354
+ if (childIndex < 0)
355
+ return null;
356
+ if (remaining === 0) {
357
+ // Cursor at start of non-editable span → place before it
358
+ return { node: container, offset: childIndex };
359
+ }
360
+ // Cursor at end of or inside non-editable span → place after it
361
+ return { node: container, offset: childIndex + 1 };
362
+ }
363
+ const textNode = findTextNode(spanEl);
364
+ if (textNode === null) {
365
+ // Span has no text node (unexpected for non-empty span)
366
+ return null;
367
+ }
368
+ return { node: textNode, offset: remaining };
369
+ }
370
+ remaining -= spanLen;
371
+ }
372
+ // If we get here, the local offset exceeds the block text length.
373
+ // This shouldn't happen if resolve() works correctly, but handle gracefully.
374
+ return null;
375
+ }
376
+ /**
377
+ * Get the index of a child element within its parent's children.
378
+ * Returns -1 if the child is not found.
379
+ */
380
+ function getChildIndex(parent, child) {
381
+ const children = parent.children;
382
+ for (let i = 0; i < children.length; i++) {
383
+ if (children[i] === child)
384
+ return i;
385
+ }
386
+ return -1;
387
+ }
388
+ /**
389
+ * Find the first Text node child of an element.
390
+ * Returns null if no text node exists (e.g., span only has a `<br>`).
391
+ */
392
+ function findTextNode(el) {
393
+ for (let i = 0; i < el.childNodes.length; i++) {
394
+ const child = el.childNodes[i];
395
+ if (child.nodeType === Node.TEXT_NODE) {
396
+ return child;
397
+ }
398
+ }
399
+ return null;
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // readDomSelection
403
+ // ---------------------------------------------------------------------------
404
+ /**
405
+ * Read the browser's current DOM selection and convert it to an RTIF Selection.
406
+ *
407
+ * Returns `null` if:
408
+ * - There is no selection or no ranges
409
+ * - The selection is outside the editor root
410
+ * - Either anchor or focus cannot be resolved to an RTIF offset
411
+ *
412
+ * @param root - The editor's root element (`[data-rtif-root]`)
413
+ * @param cache - Pre-built block offset cache from `buildBlockOffsetCache`
414
+ * @returns The RTIF Selection, or `null` if the selection cannot be resolved
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * const sel = readDomSelection(root, cache);
419
+ * if (sel) {
420
+ * engine.dispatch({ type: 'set_selection', selection: sel });
421
+ * }
422
+ * ```
423
+ */
424
+ export function readDomSelection(root, cache) {
425
+ const domSel = window.getSelection();
426
+ if (domSel === null || domSel.rangeCount === 0) {
427
+ return null;
428
+ }
429
+ const { anchorNode, anchorOffset, focusNode, focusOffset } = domSel;
430
+ if (anchorNode === null || focusNode === null) {
431
+ return null;
432
+ }
433
+ const anchorRtif = domPointToRtifOffset(root, anchorNode, anchorOffset, cache);
434
+ const focusRtif = domPointToRtifOffset(root, focusNode, focusOffset, cache);
435
+ if (anchorRtif === null || focusRtif === null) {
436
+ return null;
437
+ }
438
+ return {
439
+ anchor: { offset: anchorRtif },
440
+ focus: { offset: focusRtif },
441
+ };
442
+ }
443
+ // ---------------------------------------------------------------------------
444
+ // setDomSelection
445
+ // ---------------------------------------------------------------------------
446
+ /**
447
+ * Set the browser's DOM selection to match an RTIF Selection.
448
+ *
449
+ * Converts the RTIF anchor and focus offsets to DOM points, then calls
450
+ * `setBaseAndExtent()` on the window's Selection object.
451
+ *
452
+ * Sets the suppression flag to `true` before modifying the selection, to
453
+ * prevent the `selectionchange` event handler from triggering a feedback
454
+ * loop. The flag is reset via `requestAnimationFrame`.
455
+ *
456
+ * If either offset cannot be resolved to a DOM point (e.g., DOM is out of
457
+ * sync), the function is a no-op.
458
+ *
459
+ * @param root - The editor's root element (`[data-rtif-root]`)
460
+ * @param doc - The current RTIF document
461
+ * @param selection - The RTIF selection to apply to the DOM
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * setDomSelection(root, doc, { anchor: { offset: 0 }, focus: { offset: 5 } });
466
+ * ```
467
+ */
468
+ export function setDomSelection(root, doc, selection) {
469
+ const anchorPoint = rtifOffsetToDomPoint(root, doc, selection.anchor.offset);
470
+ const focusPoint = rtifOffsetToDomPoint(root, doc, selection.focus.offset);
471
+ if (anchorPoint === null || focusPoint === null) {
472
+ return;
473
+ }
474
+ const domSel = window.getSelection();
475
+ if (domSel === null) {
476
+ return;
477
+ }
478
+ suppressSelectionChange = true;
479
+ domSel.setBaseAndExtent(anchorPoint.node, anchorPoint.offset, focusPoint.node, focusPoint.offset);
480
+ // Reset suppression in the next animation frame to avoid feedback loops.
481
+ // requestAnimationFrame may not exist in test environments (jsdom), so guard.
482
+ if (typeof requestAnimationFrame === 'function') {
483
+ requestAnimationFrame(() => {
484
+ suppressSelectionChange = false;
485
+ });
486
+ }
487
+ }
488
+ // ---------------------------------------------------------------------------
489
+ // Suppression helpers
490
+ // ---------------------------------------------------------------------------
491
+ /**
492
+ * Returns `true` if the selection-sync layer is currently suppressing
493
+ * `selectionchange` events to avoid feedback loops.
494
+ *
495
+ * The `selectionchange` handler in the input bridge should check this flag
496
+ * and skip processing when it returns `true`.
497
+ *
498
+ * @returns Whether selectionchange events should be suppressed
499
+ *
500
+ * @example
501
+ * ```ts
502
+ * document.addEventListener('selectionchange', () => {
503
+ * if (isSuppressed()) return;
504
+ * handleUserSelectionChange();
505
+ * });
506
+ * ```
507
+ */
508
+ export function isSuppressed() {
509
+ return suppressSelectionChange;
510
+ }
511
+ /**
512
+ * Manually reset the suppression flag to `false`.
513
+ *
514
+ * Primarily useful for testing, where `requestAnimationFrame` may not run.
515
+ * Production code should not need to call this.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * afterEach(() => {
520
+ * resetSuppression();
521
+ * });
522
+ * ```
523
+ */
524
+ export function resetSuppression() {
525
+ suppressSelectionChange = false;
526
+ }
527
+ //# sourceMappingURL=selection-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-sync.js","sourceRoot":"","sources":["../src/selection-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Keyboard shortcut handler for the RTIF web editor.
3
+ *
4
+ * Reads shortcut bindings from the engine and intercepts `keydown` events
5
+ * to dispatch the corresponding commands through the {@link CommandBus}.
6
+ * Uses platform-aware modifier key detection (Cmd on Mac, Ctrl on Win/Linux).
7
+ *
8
+ * The shortcut handler fires BEFORE the cursor-nav handler's keydown listener,
9
+ * so plugin-registered shortcuts take priority over built-in navigation.
10
+ *
11
+ * @module
12
+ */
13
+ import type { ShortcutBinding } from '@rtif-sdk/engine';
14
+ import type { CommandBus } from './command-bus.js';
15
+ /**
16
+ * Injected dependencies for the ShortcutHandler.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const deps: ShortcutHandlerDeps = {
21
+ * getShortcuts: () => engine.getShortcuts(),
22
+ * commandBus: bus,
23
+ * isComposing: () => compositionHandler.isComposing(),
24
+ * };
25
+ * ```
26
+ */
27
+ export interface ShortcutHandlerDeps {
28
+ /** Get all registered shortcut bindings from the engine */
29
+ readonly getShortcuts: () => ReadonlyArray<ShortcutBinding>;
30
+ /** The command bus to execute matched commands through */
31
+ readonly commandBus: CommandBus;
32
+ /** Whether an IME composition session is active (shortcuts are suppressed during composition) */
33
+ readonly isComposing: () => boolean;
34
+ }
35
+ /**
36
+ * Intercepts `keydown` events and matches them against engine-registered
37
+ * keyboard shortcuts.
38
+ *
39
+ * When a match is found, the corresponding command is executed via the
40
+ * command bus and the event is prevented/stopped.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const handler = new ShortcutHandler(root, {
45
+ * getShortcuts: () => engine.getShortcuts(),
46
+ * commandBus: bus,
47
+ * isComposing: () => compositionHandler.isComposing(),
48
+ * });
49
+ * handler.attach();
50
+ * ```
51
+ */
52
+ export declare class ShortcutHandler {
53
+ private readonly _root;
54
+ private readonly _deps;
55
+ private _attached;
56
+ private readonly _boundHandler;
57
+ constructor(root: HTMLElement, deps: ShortcutHandlerDeps);
58
+ /**
59
+ * Attach the `keydown` event listener to the root element.
60
+ *
61
+ * Uses `capture: true` so shortcuts fire before other keydown handlers.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * handler.attach();
66
+ * ```
67
+ */
68
+ attach(): void;
69
+ /**
70
+ * Remove the `keydown` event listener.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * handler.detach();
75
+ * ```
76
+ */
77
+ detach(): void;
78
+ private _handleKeyDown;
79
+ }
80
+ /**
81
+ * Check whether a keyboard event matches a shortcut binding.
82
+ *
83
+ * Platform-aware: `mod` matches `metaKey` on Mac, `ctrlKey` on other platforms.
84
+ *
85
+ * @param e - The keyboard event
86
+ * @param binding - The shortcut binding to match against
87
+ * @returns Whether the event matches the binding
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const matches = matchesShortcut(event, {
92
+ * descriptor: { key: 'b', mod: true },
93
+ * command: 'toggleBold',
94
+ * });
95
+ * ```
96
+ */
97
+ export declare function matchesShortcut(e: KeyboardEvent, binding: ShortcutBinding): boolean;
98
+ //# sourceMappingURL=shortcut-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortcut-handler.d.ts","sourceRoot":"","sources":["../src/shortcut-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOnD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,aAAa,CAAC,eAAe,CAAC,CAAC;IAE5D,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAEhC,iGAAiG;IACjG,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;CACrC;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;gBAE/C,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB;IAMxD;;;;;;;;;OASG;IACH,MAAM,IAAI,IAAI;IAMd;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IAMd,OAAO,CAAC,cAAc;CA2BvB;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CA6BnF"}