@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,432 @@
1
+ /**
2
+ * Clipboard handling — copy, cut, and paste for the RTIF web editor.
3
+ *
4
+ * Handles copy and cut with both plain text and RTIF JSON clipboard formats.
5
+ * Paste can be delegated to the content pipeline (skipPaste mode) or handled
6
+ * directly for plain text fallback.
7
+ *
8
+ * @see docs/spec/platform-requirements.md section 4 (Clipboard)
9
+ */
10
+ import { resolve, blockTextLength } from '@rtif-sdk/core';
11
+ // ---------------------------------------------------------------------------
12
+ // RTIF clipboard MIME type
13
+ // ---------------------------------------------------------------------------
14
+ /** Custom MIME type for RTIF JSON clipboard data. */
15
+ const RTIF_MIME = 'application/x-rtif+json';
16
+ // ---------------------------------------------------------------------------
17
+ // Pure helpers
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Extract plain text from a document range defined by absolute offsets.
21
+ *
22
+ * Walks blocks and spans within `[start, end)`, inserting `'\n'` at
23
+ * virtual block boundaries. This is a pure function with no side effects.
24
+ *
25
+ * @param doc - The RTIF document
26
+ * @param start - Start offset (inclusive)
27
+ * @param end - End offset (exclusive)
28
+ * @returns The plain text string within the range
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const doc = {
33
+ * version: 1,
34
+ * blocks: [
35
+ * { id: 'b1', type: 'text', spans: [{ text: 'hello' }] },
36
+ * { id: 'b2', type: 'text', spans: [{ text: 'world' }] },
37
+ * ],
38
+ * };
39
+ * extractPlainText(doc, 3, 8); // => 'lo\nwo'
40
+ * ```
41
+ */
42
+ export function extractPlainText(doc, start, end) {
43
+ if (start >= end)
44
+ return '';
45
+ const startRes = resolve(doc, start);
46
+ const endRes = resolve(doc, end);
47
+ const parts = [];
48
+ for (let bi = startRes.blockIndex; bi <= endRes.blockIndex; bi++) {
49
+ const block = doc.blocks[bi];
50
+ if (!block)
51
+ continue;
52
+ const blockLen = blockTextLength(block);
53
+ // Determine the local character range within this block
54
+ const localStart = bi === startRes.blockIndex ? startRes.localOffset : 0;
55
+ const localEnd = bi === endRes.blockIndex ? endRes.localOffset : blockLen;
56
+ // Walk spans in this block and extract the relevant slice
57
+ let charsSoFar = 0;
58
+ for (const span of block.spans) {
59
+ const spanStart = charsSoFar;
60
+ const spanEnd = charsSoFar + span.text.length;
61
+ // Skip spans entirely before our range
62
+ if (spanEnd <= localStart) {
63
+ charsSoFar = spanEnd;
64
+ continue;
65
+ }
66
+ // Stop if span starts at or past our range end
67
+ if (spanStart >= localEnd) {
68
+ break;
69
+ }
70
+ // Compute the slice within this span
71
+ const sliceStart = Math.max(localStart - spanStart, 0);
72
+ const sliceEnd = Math.min(localEnd - spanStart, span.text.length);
73
+ parts.push(span.text.slice(sliceStart, sliceEnd));
74
+ charsSoFar = spanEnd;
75
+ }
76
+ // Add a newline between blocks (not after the last one)
77
+ if (bi < endRes.blockIndex) {
78
+ parts.push('\n');
79
+ }
80
+ }
81
+ return parts.join('');
82
+ }
83
+ /**
84
+ * Extract an RTIF block slice from a document range for clipboard serialization.
85
+ *
86
+ * Extracts blocks and spans within the absolute offset range `[start, end)`,
87
+ * preserving span marks and block attributes. The resulting blocks form a
88
+ * self-contained document fragment suitable for clipboard storage.
89
+ *
90
+ * @param doc - The RTIF document
91
+ * @param start - Start offset (inclusive)
92
+ * @param end - End offset (exclusive)
93
+ * @returns Array of extracted blocks with sliced spans
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const doc = {
98
+ * version: 1,
99
+ * blocks: [
100
+ * {
101
+ * id: 'b1', type: 'text',
102
+ * spans: [
103
+ * { text: 'hello ', marks: { bold: true } },
104
+ * { text: 'world' },
105
+ * ],
106
+ * },
107
+ * ],
108
+ * };
109
+ * const blocks = extractRtifSlice(doc, 0, 5);
110
+ * // => [{ id: 'b1', type: 'text', spans: [{ text: 'hello', marks: { bold: true } }] }]
111
+ * ```
112
+ */
113
+ export function extractRtifSlice(doc, start, end) {
114
+ if (start >= end)
115
+ return [];
116
+ const startRes = resolve(doc, start);
117
+ const endRes = resolve(doc, end);
118
+ const result = [];
119
+ for (let bi = startRes.blockIndex; bi <= endRes.blockIndex; bi++) {
120
+ const block = doc.blocks[bi];
121
+ if (!block)
122
+ continue;
123
+ const blockLen = blockTextLength(block);
124
+ // Determine the local character range within this block
125
+ const localStart = bi === startRes.blockIndex ? startRes.localOffset : 0;
126
+ const localEnd = bi === endRes.blockIndex ? endRes.localOffset : blockLen;
127
+ // Extract spans within the local range
128
+ const extractedSpans = extractSpanSlice(block.spans, localStart, localEnd);
129
+ // Build the extracted block
130
+ const extractedBlock = {
131
+ id: block.id,
132
+ type: block.type,
133
+ spans: extractedSpans.length > 0 ? extractedSpans : [{ text: '' }],
134
+ };
135
+ if (block.attrs && Object.keys(block.attrs).length > 0) {
136
+ extractedBlock.attrs = { ...block.attrs };
137
+ }
138
+ result.push(extractedBlock);
139
+ }
140
+ return result;
141
+ }
142
+ /**
143
+ * Extract a slice of spans within a local offset range.
144
+ *
145
+ * @param spans - The source spans
146
+ * @param localStart - Start offset within the block
147
+ * @param localEnd - End offset within the block
148
+ * @returns Array of sliced spans preserving marks
149
+ */
150
+ function extractSpanSlice(spans, localStart, localEnd) {
151
+ const result = [];
152
+ let charsSoFar = 0;
153
+ for (const span of spans) {
154
+ const spanStart = charsSoFar;
155
+ const spanEnd = charsSoFar + span.text.length;
156
+ // Skip spans entirely before our range
157
+ if (spanEnd <= localStart) {
158
+ charsSoFar = spanEnd;
159
+ continue;
160
+ }
161
+ // Stop if span starts at or past our range end
162
+ if (spanStart >= localEnd) {
163
+ break;
164
+ }
165
+ // Compute the slice within this span
166
+ const sliceStart = Math.max(localStart - spanStart, 0);
167
+ const sliceEnd = Math.min(localEnd - spanStart, span.text.length);
168
+ const slicedText = span.text.slice(sliceStart, sliceEnd);
169
+ if (slicedText.length > 0) {
170
+ const slicedSpan = { text: slicedText };
171
+ if (span.marks && Object.keys(span.marks).length > 0) {
172
+ slicedSpan.marks = { ...span.marks };
173
+ }
174
+ result.push(slicedSpan);
175
+ }
176
+ charsSoFar = spanEnd;
177
+ }
178
+ return result;
179
+ }
180
+ /**
181
+ * Generate the RTIF operations needed to delete a selection range.
182
+ *
183
+ * For same-block selections, produces a single `delete_text`. For cross-block
184
+ * selections, composes `delete_text` and `merge_block` operations to remove
185
+ * all content within the range and merge the remaining fragments.
186
+ *
187
+ * The generated operations are designed to be dispatched as a single batch.
188
+ * After each operation, the merge point stays at the same absolute offset
189
+ * (`start`), which makes the sequencing correct.
190
+ *
191
+ * @param doc - The current RTIF document
192
+ * @param selection - The selection to delete
193
+ * @returns An array of operations (may be empty if selection is collapsed)
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * // Single-block deletion
198
+ * const ops = deleteSelectionOps(doc, {
199
+ * anchor: { offset: 2 },
200
+ * focus: { offset: 5 },
201
+ * });
202
+ * // => [{ type: 'delete_text', offset: 2, count: 3 }]
203
+ * ```
204
+ */
205
+ export function deleteSelectionOps(doc, selection) {
206
+ const start = Math.min(selection.anchor.offset, selection.focus.offset);
207
+ const end = Math.max(selection.anchor.offset, selection.focus.offset);
208
+ if (start === end)
209
+ return [];
210
+ const startRes = resolve(doc, start);
211
+ const endRes = resolve(doc, end);
212
+ // Same block: single delete_text
213
+ if (startRes.blockIndex === endRes.blockIndex) {
214
+ return [{ type: 'delete_text', offset: start, count: end - start }];
215
+ }
216
+ // Cross-block deletion:
217
+ //
218
+ // Strategy: after each operation, the "merge point" is always at absolute
219
+ // offset `start`. This works because:
220
+ // - Deleting text at end of first block removes chars after `start`
221
+ // - Merging the next block appends its content at `start`
222
+ // - Deleting that block's content removes chars starting at `start`
223
+ //
224
+ // So every op targets offset `start`.
225
+ const ops = [];
226
+ const firstBlock = doc.blocks[startRes.blockIndex];
227
+ if (!firstBlock)
228
+ return [];
229
+ const firstBlockLen = blockTextLength(firstBlock);
230
+ // 1. Delete from startLocal to end of first block
231
+ const deleteInFirst = firstBlockLen - startRes.localOffset;
232
+ if (deleteInFirst > 0) {
233
+ ops.push({ type: 'delete_text', offset: start, count: deleteInFirst });
234
+ }
235
+ // 2. For each subsequent block through the last: merge, then delete content
236
+ for (let i = startRes.blockIndex + 1; i <= endRes.blockIndex; i++) {
237
+ const block = doc.blocks[i];
238
+ if (!block)
239
+ continue;
240
+ const isLast = i === endRes.blockIndex;
241
+ const bLen = blockTextLength(block);
242
+ // Merge this block into the accumulated first block
243
+ ops.push({ type: 'merge_block', blockId: block.id });
244
+ if (isLast) {
245
+ // Delete from start of this (last) block up to endLocal
246
+ if (endRes.localOffset > 0) {
247
+ ops.push({ type: 'delete_text', offset: start, count: endRes.localOffset });
248
+ }
249
+ }
250
+ else {
251
+ // Delete all content from this intermediate block
252
+ if (bLen > 0) {
253
+ ops.push({ type: 'delete_text', offset: start, count: bLen });
254
+ }
255
+ }
256
+ }
257
+ return ops;
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // ClipboardHandler class
261
+ // ---------------------------------------------------------------------------
262
+ /**
263
+ * Handles copy, cut, and paste events on a contenteditable root element.
264
+ *
265
+ * Copy and cut write both `text/plain` and `application/x-rtif+json` to the
266
+ * clipboard. Paste handling can be delegated to the content pipeline by
267
+ * setting `skipPaste: true` in the deps.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * const handler = new ClipboardHandler(root, deps);
272
+ * handler.attach(); // start listening
273
+ * // ... later ...
274
+ * handler.detach(); // stop listening
275
+ * ```
276
+ */
277
+ export class ClipboardHandler {
278
+ _root;
279
+ _deps;
280
+ _copyHandler = null;
281
+ _cutHandler = null;
282
+ _pasteHandler = null;
283
+ constructor(root, deps) {
284
+ this._root = root;
285
+ this._deps = deps;
286
+ }
287
+ /**
288
+ * Attach copy, cut, and (optionally) paste event listeners to the root element.
289
+ *
290
+ * When `skipPaste` is true in the deps, the paste listener is not attached.
291
+ * The content pipeline handles paste instead.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * handler.attach();
296
+ * ```
297
+ */
298
+ attach() {
299
+ this._copyHandler = (e) => this._onCopy(e);
300
+ this._cutHandler = (e) => this._onCut(e);
301
+ this._root.addEventListener('copy', this._copyHandler);
302
+ this._root.addEventListener('cut', this._cutHandler);
303
+ // Only attach paste if not delegated to content pipeline
304
+ if (!this._deps.skipPaste) {
305
+ this._pasteHandler = (e) => this._onPaste(e);
306
+ this._root.addEventListener('paste', this._pasteHandler);
307
+ }
308
+ }
309
+ /**
310
+ * Remove all clipboard event listeners from the root element.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * handler.detach();
315
+ * ```
316
+ */
317
+ detach() {
318
+ if (this._copyHandler) {
319
+ this._root.removeEventListener('copy', this._copyHandler);
320
+ this._copyHandler = null;
321
+ }
322
+ if (this._cutHandler) {
323
+ this._root.removeEventListener('cut', this._cutHandler);
324
+ this._cutHandler = null;
325
+ }
326
+ if (this._pasteHandler) {
327
+ this._root.removeEventListener('paste', this._pasteHandler);
328
+ this._pasteHandler = null;
329
+ }
330
+ }
331
+ // -----------------------------------------------------------------------
332
+ // Event handlers
333
+ // -----------------------------------------------------------------------
334
+ _onCopy(e) {
335
+ const sel = this._deps.getSelection();
336
+ const start = Math.min(sel.anchor.offset, sel.focus.offset);
337
+ const end = Math.max(sel.anchor.offset, sel.focus.offset);
338
+ // Nothing to copy if selection is collapsed
339
+ if (start === end)
340
+ return;
341
+ const doc = this._deps.getDoc();
342
+ const text = extractPlainText(doc, start, end);
343
+ if (e.clipboardData) {
344
+ e.clipboardData.setData('text/plain', text);
345
+ // Write RTIF JSON for rich paste within RTIF editors
346
+ const blocks = extractRtifSlice(doc, start, end);
347
+ const rtifJson = JSON.stringify({ version: 1, blocks });
348
+ e.clipboardData.setData(RTIF_MIME, rtifJson);
349
+ }
350
+ e.preventDefault();
351
+ }
352
+ _onCut(e) {
353
+ // Read-only: allow browser default (which is nothing, since
354
+ // contenteditable won't mutate), but prevent any mutation attempt.
355
+ if (this._deps.isReadOnly()) {
356
+ e.preventDefault();
357
+ return;
358
+ }
359
+ const sel = this._deps.getSelection();
360
+ const start = Math.min(sel.anchor.offset, sel.focus.offset);
361
+ const end = Math.max(sel.anchor.offset, sel.focus.offset);
362
+ // Nothing to cut if selection is collapsed
363
+ if (start === end) {
364
+ e.preventDefault();
365
+ return;
366
+ }
367
+ // Write to clipboard
368
+ const doc = this._deps.getDoc();
369
+ const text = extractPlainText(doc, start, end);
370
+ if (e.clipboardData) {
371
+ e.clipboardData.setData('text/plain', text);
372
+ // Write RTIF JSON for rich paste within RTIF editors
373
+ const blocks = extractRtifSlice(doc, start, end);
374
+ const rtifJson = JSON.stringify({ version: 1, blocks });
375
+ e.clipboardData.setData(RTIF_MIME, rtifJson);
376
+ }
377
+ e.preventDefault();
378
+ // Delete the selection
379
+ const ops = deleteSelectionOps(doc, sel);
380
+ if (ops.length > 0) {
381
+ this._deps.dispatch(ops);
382
+ }
383
+ }
384
+ _onPaste(e) {
385
+ e.preventDefault();
386
+ if (this._deps.isReadOnly())
387
+ return;
388
+ if (this._deps.isComposing())
389
+ return;
390
+ const pastedText = e.clipboardData?.getData('text/plain') ?? '';
391
+ if (pastedText === '')
392
+ return;
393
+ const doc = this._deps.getDoc();
394
+ const sel = this._deps.getSelection();
395
+ const ops = [];
396
+ // If selection is non-collapsed, delete it first
397
+ const start = Math.min(sel.anchor.offset, sel.focus.offset);
398
+ const end = Math.max(sel.anchor.offset, sel.focus.offset);
399
+ if (start !== end) {
400
+ ops.push(...deleteSelectionOps(doc, sel));
401
+ }
402
+ // Split pasted text on newlines
403
+ const lines = pastedText.split('\n');
404
+ let insertOffset = start;
405
+ for (let i = 0; i < lines.length; i++) {
406
+ const line = lines[i];
407
+ if (i > 0) {
408
+ // Insert a block split before each subsequent line
409
+ ops.push({
410
+ type: 'split_block',
411
+ offset: insertOffset,
412
+ newBlockId: this._deps.generateBlockId(),
413
+ });
414
+ // After split_block, the offset stays the same (now at start of new block),
415
+ // but the absolute offset increases by 1 for the virtual \n
416
+ insertOffset += 1;
417
+ }
418
+ if (line.length > 0) {
419
+ ops.push({
420
+ type: 'insert_text',
421
+ offset: insertOffset,
422
+ text: line,
423
+ });
424
+ insertOffset += line.length;
425
+ }
426
+ }
427
+ if (ops.length > 0) {
428
+ this._deps.dispatch(ops);
429
+ }
430
+ }
431
+ }
432
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Command bus — bridges between UI controls (toolbars, shortcuts) and the
3
+ * engine's command system.
4
+ *
5
+ * The command bus wraps `engine.exec()`, `engine.canExecute()`, and
6
+ * `engine.isActive()` and notifies subscribers (toolbars) whenever command
7
+ * state might have changed.
8
+ *
9
+ * @module
10
+ */
11
+ import type { IEditorEngine } from '@rtif-sdk/engine';
12
+ /**
13
+ * Command bus interface for dispatching editor commands and querying state.
14
+ *
15
+ * UI controls (toolbars, menus) use this interface to:
16
+ * - Execute commands (`execute`)
17
+ * - Query whether a command is available (`canExecute`)
18
+ * - Query whether a command's effect is active (`isActive`)
19
+ * - Subscribe to state changes so the UI updates live (`subscribe`)
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const bus = createCommandBus(engine);
24
+ *
25
+ * // Execute a command
26
+ * bus.execute('toggleMark:bold');
27
+ *
28
+ * // Query state for toolbar rendering
29
+ * const boldActive = bus.isActive('toggleMark:bold');
30
+ * const canUndo = bus.canExecute('undo');
31
+ *
32
+ * // Subscribe for live updates
33
+ * const unsub = bus.subscribe(() => updateToolbar(bus));
34
+ * ```
35
+ */
36
+ export interface CommandBus {
37
+ /**
38
+ * Execute a command by name.
39
+ *
40
+ * @param name - The command name
41
+ * @param payload - Optional payload for the command handler
42
+ * @returns `true` if the command executed successfully, `false` on error
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * bus.execute('toggleMark:bold');
47
+ * ```
48
+ */
49
+ execute(name: string, payload?: unknown): boolean;
50
+ /**
51
+ * Check whether a command can execute in the current state.
52
+ *
53
+ * @param name - The command name
54
+ * @param payload - Optional payload
55
+ * @returns Whether the command can execute
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * if (bus.canExecute('undo')) { ... }
60
+ * ```
61
+ */
62
+ canExecute(name: string, payload?: unknown): boolean;
63
+ /**
64
+ * Check whether a command's effect is currently active.
65
+ *
66
+ * @param name - The command name
67
+ * @param payload - Optional payload
68
+ * @returns Whether the command's effect is active
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const boldActive = bus.isActive('toggleMark:bold');
73
+ * ```
74
+ */
75
+ isActive(name: string, payload?: unknown): boolean;
76
+ /**
77
+ * Subscribe to state changes. The handler is called after every engine
78
+ * state change so toolbars can re-render.
79
+ *
80
+ * @param handler - Callback invoked with the command bus after each change
81
+ * @returns Unsubscribe function
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const unsub = bus.subscribe((b) => updateToolbar(b));
86
+ * // later:
87
+ * unsub();
88
+ * ```
89
+ */
90
+ subscribe(handler: (bus: CommandBus) => void): () => void;
91
+ /**
92
+ * Clean up subscriptions. Called during editor teardown.
93
+ */
94
+ destroy(): void;
95
+ }
96
+ /**
97
+ * Create a command bus backed by the given engine.
98
+ *
99
+ * The bus subscribes to `engine.onChange()` to notify UI subscribers whenever
100
+ * command state might have changed (e.g., undo availability, bold active state).
101
+ *
102
+ * @param engine - The RTIF engine instance
103
+ * @returns A CommandBus instance
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const engine = createEngine(doc);
108
+ * const bus = createCommandBus(engine);
109
+ * bus.execute('toggleMark:bold');
110
+ * ```
111
+ */
112
+ export declare function createCommandBus(engine: IEditorEngine): CommandBus;
113
+ //# sourceMappingURL=command-bus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-bus.d.ts","sourceRoot":"","sources":["../src/command-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMtD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAElD;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAErD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAEnD;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAE1D;;OAEG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CA8ClE"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Command bus — bridges between UI controls (toolbars, shortcuts) and the
3
+ * engine's command system.
4
+ *
5
+ * The command bus wraps `engine.exec()`, `engine.canExecute()`, and
6
+ * `engine.isActive()` and notifies subscribers (toolbars) whenever command
7
+ * state might have changed.
8
+ *
9
+ * @module
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Factory
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Create a command bus backed by the given engine.
16
+ *
17
+ * The bus subscribes to `engine.onChange()` to notify UI subscribers whenever
18
+ * command state might have changed (e.g., undo availability, bold active state).
19
+ *
20
+ * @param engine - The RTIF engine instance
21
+ * @returns A CommandBus instance
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const engine = createEngine(doc);
26
+ * const bus = createCommandBus(engine);
27
+ * bus.execute('toggleMark:bold');
28
+ * ```
29
+ */
30
+ export function createCommandBus(engine) {
31
+ const subscribers = new Set();
32
+ const notifySubscribers = () => {
33
+ for (const handler of subscribers) {
34
+ handler(bus);
35
+ }
36
+ };
37
+ const bus = {
38
+ execute(name, payload) {
39
+ try {
40
+ engine.exec(name, payload);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ },
47
+ canExecute(name, payload) {
48
+ return engine.canExecute(name, payload);
49
+ },
50
+ isActive(name, payload) {
51
+ return engine.isActive(name, payload);
52
+ },
53
+ subscribe(handler) {
54
+ subscribers.add(handler);
55
+ return () => {
56
+ subscribers.delete(handler);
57
+ };
58
+ },
59
+ destroy() {
60
+ unsubChange();
61
+ unsubSelection();
62
+ subscribers.clear();
63
+ },
64
+ };
65
+ // Subscribe to engine changes and notify all bus subscribers
66
+ const unsubChange = engine.onChange(notifySubscribers);
67
+ const unsubSelection = engine.onSelectionChange(notifySubscribers);
68
+ return bus;
69
+ }
70
+ //# sourceMappingURL=command-bus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-bus.js","sourceRoot":"","sources":["../src/command-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAiGH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,MAAM,WAAW,GAAG,IAAI,GAAG,EAA6B,CAAC;IAEzD,MAAM,iBAAiB,GAAG,GAAS,EAAE;QACnC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,GAAG,GAAe;QACtB,OAAO,CAAC,IAAY,EAAE,OAAiB;YACrC,IAAI,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;gBAC3B,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,UAAU,CAAC,IAAY,EAAE,OAAiB;YACxC,OAAO,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;QAED,QAAQ,CAAC,IAAY,EAAE,OAAiB;YACtC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,CAAC;QAED,SAAS,CAAC,OAAkC;YAC1C,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzB,OAAO,GAAG,EAAE;gBACV,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC,CAAC;QACJ,CAAC;QAED,OAAO;YACL,WAAW,EAAE,CAAC;YACd,cAAc,EAAE,CAAC;YACjB,WAAW,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;KACF,CAAC;IAEF,6DAA6D;IAC7D,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IACvD,MAAM,cAAc,GAAG,MAAM,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAEnE,OAAO,GAAG,CAAC;AACb,CAAC"}