@rtif-sdk/web 1.0.0 → 1.1.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 (62) hide show
  1. package/dist/block-drag-handler.d.ts +5 -0
  2. package/dist/block-drag-handler.d.ts.map +1 -1
  3. package/dist/block-drag-handler.js +28 -2
  4. package/dist/block-drag-handler.js.map +1 -1
  5. package/dist/block-renderer.d.ts +12 -6
  6. package/dist/block-renderer.d.ts.map +1 -1
  7. package/dist/block-renderer.js +98 -9
  8. package/dist/block-renderer.js.map +1 -1
  9. package/dist/block-type-dropdown.d.ts +78 -0
  10. package/dist/block-type-dropdown.d.ts.map +1 -0
  11. package/dist/block-type-dropdown.js +276 -0
  12. package/dist/block-type-dropdown.js.map +1 -0
  13. package/dist/color-picker.d.ts +91 -0
  14. package/dist/color-picker.d.ts.map +1 -0
  15. package/dist/color-picker.js +346 -0
  16. package/dist/color-picker.js.map +1 -0
  17. package/dist/content-handlers.d.ts +7 -8
  18. package/dist/content-handlers.d.ts.map +1 -1
  19. package/dist/content-handlers.js +122 -93
  20. package/dist/content-handlers.js.map +1 -1
  21. package/dist/editor.d.ts.map +1 -1
  22. package/dist/editor.js +117 -14
  23. package/dist/editor.js.map +1 -1
  24. package/dist/embed-utils.d.ts +148 -0
  25. package/dist/embed-utils.d.ts.map +1 -0
  26. package/dist/embed-utils.js +197 -0
  27. package/dist/embed-utils.js.map +1 -0
  28. package/dist/font-family-picker.d.ts +105 -0
  29. package/dist/font-family-picker.d.ts.map +1 -0
  30. package/dist/font-family-picker.js +314 -0
  31. package/dist/font-family-picker.js.map +1 -0
  32. package/dist/font-size-picker.d.ts +82 -0
  33. package/dist/font-size-picker.d.ts.map +1 -0
  34. package/dist/font-size-picker.js +290 -0
  35. package/dist/font-size-picker.js.map +1 -0
  36. package/dist/index.d.ts +12 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +21 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/plugins/index.d.ts +2 -1
  41. package/dist/plugins/index.d.ts.map +1 -1
  42. package/dist/plugins/index.js +1 -1
  43. package/dist/plugins/index.js.map +1 -1
  44. package/dist/plugins/link-plugin.d.ts +4 -0
  45. package/dist/plugins/link-plugin.d.ts.map +1 -1
  46. package/dist/plugins/link-plugin.js +17 -0
  47. package/dist/plugins/link-plugin.js.map +1 -1
  48. package/dist/plugins/mark-utils.d.ts +31 -0
  49. package/dist/plugins/mark-utils.d.ts.map +1 -1
  50. package/dist/plugins/mark-utils.js +46 -0
  51. package/dist/plugins/mark-utils.js.map +1 -1
  52. package/dist/renderer.d.ts +2 -2
  53. package/dist/renderer.d.ts.map +1 -1
  54. package/dist/renderer.js +62 -16
  55. package/dist/renderer.js.map +1 -1
  56. package/dist/selection-sync.d.ts +2 -26
  57. package/dist/selection-sync.d.ts.map +1 -1
  58. package/dist/selection-sync.js +49 -13
  59. package/dist/selection-sync.js.map +1 -1
  60. package/dist/types.d.ts +24 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +17 -5
@@ -4,10 +4,11 @@
4
4
  * Provides handlers for:
5
5
  * - Plain text (`text/plain`) — splits on newlines, creates blocks
6
6
  * - RTIF JSON (`application/x-rtif+json`) — preserves marks and block attrs
7
- * - HTML (`text/html`) — stub, falls through to plain text handler
7
+ * - HTML (`text/html`) — deserializes via @rtif-sdk/format-html
8
8
  *
9
9
  * @module
10
10
  */
11
+ import { deserialize as deserializeHtml } from '@rtif-sdk/format-html';
11
12
  // ---------------------------------------------------------------------------
12
13
  // Plain text handler
13
14
  // ---------------------------------------------------------------------------
@@ -68,6 +69,105 @@ export function createPlainTextHandler(generateBlockId) {
68
69
  },
69
70
  };
70
71
  }
72
+ // ---------------------------------------------------------------------------
73
+ // Shared helper for block-based paste handlers
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Convert an array of RTIF blocks into operations and dispatch them.
77
+ *
78
+ * Generates a 4-phase operation sequence:
79
+ * 1. Insert text + split blocks
80
+ * 2. Apply span marks
81
+ * 3. Apply block attrs on new blocks
82
+ * 4. Apply block type on new blocks
83
+ *
84
+ * Used by both the RTIF JSON and HTML paste handlers.
85
+ *
86
+ * @param blocks - Source blocks to insert
87
+ * @param context - The content pipeline context
88
+ */
89
+ function insertBlocksAsOps(blocks, context) {
90
+ let insertOffset = context.deleteSelectionAndGetOffset();
91
+ const ops = [];
92
+ // Track the offsets where each new block starts, for set_span_marks later
93
+ const blockInsertions = [];
94
+ // Phase 1: Insert text and create blocks
95
+ for (let bi = 0; bi < blocks.length; bi++) {
96
+ const block = blocks[bi];
97
+ const isFirst = bi === 0;
98
+ let newBlockId = null;
99
+ if (!isFirst) {
100
+ newBlockId = context.generateBlockId();
101
+ ops.push({
102
+ type: 'split_block',
103
+ offset: insertOffset,
104
+ newBlockId,
105
+ });
106
+ insertOffset += 1;
107
+ }
108
+ const blockStartOffset = insertOffset;
109
+ // Insert the full text of all spans in this block
110
+ const fullText = block.spans.map((s) => s.text).join('');
111
+ if (fullText.length > 0) {
112
+ ops.push({
113
+ type: 'insert_text',
114
+ offset: insertOffset,
115
+ text: fullText,
116
+ });
117
+ insertOffset += fullText.length;
118
+ }
119
+ blockInsertions.push({
120
+ blockStartOffset,
121
+ block,
122
+ isFirstBlock: isFirst,
123
+ newBlockId,
124
+ });
125
+ }
126
+ // Phase 2: Apply span marks for each block
127
+ for (const { blockStartOffset, block } of blockInsertions) {
128
+ let spanOffset = blockStartOffset;
129
+ for (const span of block.spans) {
130
+ if (span.marks && Object.keys(span.marks).length > 0) {
131
+ ops.push({
132
+ type: 'set_span_marks',
133
+ offset: spanOffset,
134
+ count: span.text.length,
135
+ marks: span.marks,
136
+ });
137
+ }
138
+ spanOffset += span.text.length;
139
+ }
140
+ }
141
+ // Phase 3: Apply block attrs (only on NEW blocks created by split, not the original)
142
+ for (const { block, isFirstBlock, newBlockId, } of blockInsertions) {
143
+ if (isFirstBlock)
144
+ continue; // Don't set attrs on the block the cursor was in
145
+ if (!block.attrs || Object.keys(block.attrs).length === 0)
146
+ continue;
147
+ if (newBlockId === null)
148
+ continue;
149
+ ops.push({
150
+ type: 'set_block_attrs',
151
+ blockId: newBlockId,
152
+ attrs: block.attrs,
153
+ });
154
+ }
155
+ // Phase 4: Apply block type on NEW blocks to match source document
156
+ for (const { block, isFirstBlock, newBlockId, } of blockInsertions) {
157
+ if (isFirstBlock)
158
+ continue; // Don't change the cursor's original block type
159
+ if (newBlockId === null)
160
+ continue;
161
+ ops.push({
162
+ type: 'set_block_type',
163
+ blockId: newBlockId,
164
+ blockType: block.type,
165
+ });
166
+ }
167
+ if (ops.length > 0) {
168
+ context.dispatch(ops);
169
+ }
170
+ }
71
171
  /**
72
172
  * Create a content handler for RTIF JSON paste.
73
173
  *
@@ -77,16 +177,15 @@ export function createPlainTextHandler(generateBlockId) {
77
177
  *
78
178
  * Priority: -80 (highest built-in, runs before HTML and plain text).
79
179
  *
80
- * @param generateBlockId - Factory for generating unique block IDs
81
180
  * @returns A ContentHandler for RTIF JSON paste
82
181
  *
83
182
  * @example
84
183
  * ```ts
85
- * const handler = createRtifPasteHandler(() => crypto.randomUUID());
184
+ * const handler = createRtifPasteHandler();
86
185
  * pipeline.register(handler);
87
186
  * ```
88
187
  */
89
- export function createRtifPasteHandler(generateBlockId) {
188
+ export function createRtifPasteHandler() {
90
189
  return {
91
190
  id: 'rtif:rtif-json',
92
191
  accept: ['application/x-rtif+json'],
@@ -99,86 +198,7 @@ export function createRtifPasteHandler(generateBlockId) {
99
198
  const payload = parseRtifPayload(jsonStr);
100
199
  if (payload === null)
101
200
  return false;
102
- let insertOffset = context.deleteSelectionAndGetOffset();
103
- const ops = [];
104
- // Track the offsets where each new block starts, for set_span_marks later
105
- const blockInsertions = [];
106
- // Phase 1: Insert text and create blocks
107
- for (let bi = 0; bi < payload.blocks.length; bi++) {
108
- const block = payload.blocks[bi];
109
- const isFirst = bi === 0;
110
- let newBlockId = null;
111
- if (!isFirst) {
112
- newBlockId = generateBlockId();
113
- ops.push({
114
- type: 'split_block',
115
- offset: insertOffset,
116
- newBlockId,
117
- });
118
- insertOffset += 1;
119
- }
120
- const blockStartOffset = insertOffset;
121
- // Insert the full text of all spans in this block
122
- const fullText = block.spans.map((s) => s.text).join('');
123
- if (fullText.length > 0) {
124
- ops.push({
125
- type: 'insert_text',
126
- offset: insertOffset,
127
- text: fullText,
128
- });
129
- insertOffset += fullText.length;
130
- }
131
- blockInsertions.push({
132
- blockStartOffset,
133
- block,
134
- isFirstBlock: isFirst,
135
- newBlockId,
136
- });
137
- }
138
- // Phase 2: Apply span marks for each block
139
- for (const { blockStartOffset, block } of blockInsertions) {
140
- let spanOffset = blockStartOffset;
141
- for (const span of block.spans) {
142
- if (span.marks && Object.keys(span.marks).length > 0) {
143
- ops.push({
144
- type: 'set_span_marks',
145
- offset: spanOffset,
146
- count: span.text.length,
147
- marks: span.marks,
148
- });
149
- }
150
- spanOffset += span.text.length;
151
- }
152
- }
153
- // Phase 3: Apply block attrs (only on NEW blocks created by split, not the original)
154
- for (const { block, isFirstBlock, newBlockId, } of blockInsertions) {
155
- if (isFirstBlock)
156
- continue; // Don't set attrs on the block the cursor was in
157
- if (!block.attrs || Object.keys(block.attrs).length === 0)
158
- continue;
159
- if (newBlockId === null)
160
- continue;
161
- ops.push({
162
- type: 'set_block_attrs',
163
- blockId: newBlockId,
164
- attrs: block.attrs,
165
- });
166
- }
167
- // Phase 4: Apply block type on NEW blocks to match source document
168
- for (const { block, isFirstBlock, newBlockId, } of blockInsertions) {
169
- if (isFirstBlock)
170
- continue; // Don't change the cursor's original block type
171
- if (newBlockId === null)
172
- continue;
173
- ops.push({
174
- type: 'set_block_type',
175
- blockId: newBlockId,
176
- blockType: block.type,
177
- });
178
- }
179
- if (ops.length > 0) {
180
- context.dispatch(ops);
181
- }
201
+ insertBlocksAsOps(payload.blocks, context);
182
202
  return true;
183
203
  },
184
204
  };
@@ -230,17 +250,17 @@ function parseRtifPayload(jsonStr) {
230
250
  }
231
251
  }
232
252
  // ---------------------------------------------------------------------------
233
- // HTML paste handler (stub)
253
+ // HTML paste handler
234
254
  // ---------------------------------------------------------------------------
235
255
  /**
236
- * Create a stub content handler for HTML paste.
256
+ * Create a content handler for HTML paste.
237
257
  *
238
- * Currently always returns `false` (falls through to the plain text handler).
239
- * Will be implemented when the `@rtif-sdk/format-html` plugin is available.
258
+ * Accepts `text/html` content and deserializes it via `@rtif-sdk/format-html`,
259
+ * preserving formatting (bold, italic, links, headings, lists, code blocks, etc.).
240
260
  *
241
261
  * Priority: -90 (between RTIF JSON and plain text).
242
262
  *
243
- * @returns A ContentHandler stub for HTML paste
263
+ * @returns A ContentHandler for HTML paste
244
264
  *
245
265
  * @example
246
266
  * ```ts
@@ -253,10 +273,19 @@ export function createHtmlPasteHandler() {
253
273
  id: 'rtif:html',
254
274
  accept: ['text/html'],
255
275
  priority: -90,
256
- async handle(_item, _context) {
257
- // Stub always declines. HTML deserialization will be implemented
258
- // when @rtif-sdk/format-html is available.
259
- return false;
276
+ async handle(item, context) {
277
+ const html = await item.getString();
278
+ if (html === null || html === '')
279
+ return false;
280
+ const doc = deserializeHtml(html);
281
+ // Check if deserialization produced only a single empty block
282
+ if (doc.blocks.length === 1 &&
283
+ doc.blocks[0].spans.length === 1 &&
284
+ doc.blocks[0].spans[0].text === '') {
285
+ return false;
286
+ }
287
+ insertBlocksAsOps(doc.blocks, context);
288
+ return true;
260
289
  },
261
290
  };
262
291
  }
@@ -1 +1 @@
1
- {"version":3,"file":"content-handlers.js","sourceRoot":"","sources":["../src/content-handlers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"content-handlers.js","sourceRoot":"","sources":["../src/content-handlers.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA8C7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CA4oBlE"}
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAkD7D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAguBlE"}
package/dist/editor.js CHANGED
@@ -35,6 +35,9 @@ import { createCursorRectAPI } from './cursor-rect.js';
35
35
  import { createTriggerManager } from './trigger-manager.js';
36
36
  import { findContiguousMarkRange, adjustOffsetAroundAtomicMarks, adjustOffsetAroundAtomicBlocks } from './plugins/mark-utils.js';
37
37
  import { installWebPlugins } from './plugin-kit.js';
38
+ import { createLinkPopover } from './link-popover.js';
39
+ import { LinkCommands } from './plugins/link-plugin.js';
40
+ import { createPerfInstrumentation } from './perf.js';
38
41
  // ---------------------------------------------------------------------------
39
42
  // Factory
40
43
  // ---------------------------------------------------------------------------
@@ -72,6 +75,14 @@ export function createWebEditor(config) {
72
75
  const autoFocus = config.autoFocus ?? false;
73
76
  let destroyed = false;
74
77
  // -------------------------------------------------------------------
78
+ // Performance instrumentation
79
+ // -------------------------------------------------------------------
80
+ const perfEnabled = config.perf != null && config.perf !== false;
81
+ const perfInstrumentation = perfEnabled ? createPerfInstrumentation() : null;
82
+ if (perfInstrumentation && typeof config.perf === 'object') {
83
+ perfInstrumentation.setObserver(config.perf);
84
+ }
85
+ // -------------------------------------------------------------------
75
86
  // Set up the contenteditable root
76
87
  // -------------------------------------------------------------------
77
88
  root.setAttribute('contenteditable', readOnly ? 'false' : 'true');
@@ -110,7 +121,9 @@ export function createWebEditor(config) {
110
121
  // -------------------------------------------------------------------
111
122
  // Initial render
112
123
  // -------------------------------------------------------------------
113
- renderInitial(root, engine.state.doc, markRenderers, blockRenderers);
124
+ // Persistent blockId → DOM element Map for O(1) lookups in reconcile + selection sync
125
+ const blockElementMap = new Map();
126
+ renderInitial(root, engine.state.doc, markRenderers, blockRenderers, blockElementMap);
114
127
  // -------------------------------------------------------------------
115
128
  // Command bus
116
129
  // -------------------------------------------------------------------
@@ -188,7 +201,7 @@ export function createWebEditor(config) {
188
201
  const contentPipeline = createContentPipeline();
189
202
  // Register built-in handlers (priority: RTIF JSON -80, image -85, HTML -90,
190
203
  // URL -95, plain text -100, file drop -105)
191
- contentPipeline.register(createRtifPasteHandler(() => generateId()));
204
+ contentPipeline.register(createRtifPasteHandler());
192
205
  contentPipeline.register(createImageContentHandler());
193
206
  contentPipeline.register(createHtmlPasteHandler());
194
207
  contentPipeline.register(createUrlContentHandler());
@@ -218,6 +231,7 @@ export function createWebEditor(config) {
218
231
  getDoc: () => engine.state.doc,
219
232
  dispatch: (ops) => engine.dispatch(ops),
220
233
  isReadOnly: () => readOnly,
234
+ getBlockElementMap: () => blockElementMap,
221
235
  });
222
236
  // -------------------------------------------------------------------
223
237
  // Paste handler (via content pipeline)
@@ -258,7 +272,7 @@ export function createWebEditor(config) {
258
272
  }
259
273
  else {
260
274
  // Find nearest block for between-blocks indicator
261
- const blockEl = findNearestBlock(root, e.clientY);
275
+ const blockEl = findNearestBlock(root, e.clientY, engine.state.doc, blockElementMap);
262
276
  if (blockEl) {
263
277
  const blockRect = blockEl.getBoundingClientRect();
264
278
  const midY = blockRect.top + blockRect.height / 2;
@@ -310,7 +324,7 @@ export function createWebEditor(config) {
310
324
  // Programmatic selection change (Cmd+A, Cmd+Home/End):
311
325
  // update engine state AND sync to DOM.
312
326
  engine.setSelection(sel);
313
- setDomSelection(root, engine.state.doc, sel);
327
+ setDomSelection(root, engine.state.doc, sel, blockElementMap);
314
328
  },
315
329
  isMac: () => isMac(),
316
330
  });
@@ -349,6 +363,11 @@ export function createWebEditor(config) {
349
363
  return;
350
364
  if (compositionHandler.isComposing())
351
365
  return;
366
+ // Early containment check: skip if selection is outside this editor
367
+ const domSel = document.getSelection();
368
+ if (!domSel || !domSel.focusNode || !root.contains(domSel.focusNode)) {
369
+ return;
370
+ }
352
371
  const cache = buildBlockOffsetCache(engine.state.doc);
353
372
  const sel = readDomSelection(root, cache);
354
373
  if (sel === null)
@@ -358,7 +377,7 @@ export function createWebEditor(config) {
358
377
  if (adjustedSel !== sel) {
359
378
  // Selection was adjusted — update engine AND re-sync DOM
360
379
  engine.setSelection(adjustedSel);
361
- setDomSelection(root, engine.state.doc, adjustedSel);
380
+ setDomSelection(root, engine.state.doc, adjustedSel, blockElementMap);
362
381
  return;
363
382
  }
364
383
  // Adjust offsets that land inside atomic blocks (HR, image, embed)
@@ -378,7 +397,7 @@ export function createWebEditor(config) {
378
397
  focus: { offset: adjFocus },
379
398
  };
380
399
  engine.setSelection(blockAdjustedSel);
381
- setDomSelection(root, engine.state.doc, blockAdjustedSel);
400
+ setDomSelection(root, engine.state.doc, blockAdjustedSel, blockElementMap);
382
401
  return;
383
402
  }
384
403
  // Update engine selection without triggering a dispatch
@@ -423,19 +442,52 @@ export function createWebEditor(config) {
423
442
  // -------------------------------------------------------------------
424
443
  // Engine onChange — reconcile DOM and sync selection
425
444
  // -------------------------------------------------------------------
426
- const unsubscribeOnChange = engine.onChange((state) => {
427
- if (destroyed)
428
- return;
445
+ // Batched reconciliation: when multiple onChange events fire in the same
446
+ // synchronous turn (e.g., from plugin afterApply → engine.dispatch), only
447
+ // the final state is reconciled. The first onChange in a synchronous turn
448
+ // is applied immediately; subsequent ones replace `pendingState` and a
449
+ // microtask flushes the final state.
450
+ let pendingState = null;
451
+ let isReconciling = false;
452
+ const doReconcile = (state) => {
453
+ perfInstrumentation?.markReconcileStart();
429
454
  const composingBlockId = compositionHandler.getComposingBlockId();
430
- reconcile(root, prevDoc, state.doc, composingBlockId, markRenderers, blockRenderers);
455
+ reconcile(root, prevDoc, state.doc, composingBlockId, markRenderers, blockRenderers, blockElementMap);
431
456
  prevDoc = state.doc;
457
+ perfInstrumentation?.markReconcileEnd();
432
458
  // Sync selection to DOM (unless composing)
433
459
  if (!compositionHandler.isComposing()) {
434
- setDomSelection(root, state.doc, state.selection);
460
+ perfInstrumentation?.markSelectionSyncStart();
461
+ setDomSelection(root, state.doc, state.selection, blockElementMap);
435
462
  scrollToCursor(root);
463
+ perfInstrumentation?.markSelectionSyncEnd();
436
464
  }
437
465
  // Update placeholder visibility
438
466
  updatePlaceholder(root, state.doc);
467
+ perfInstrumentation?.collect();
468
+ };
469
+ const unsubscribeOnChange = engine.onChange((state) => {
470
+ if (destroyed)
471
+ return;
472
+ if (isReconciling) {
473
+ // Nested dispatch during reconciliation — defer to microtask
474
+ pendingState = state;
475
+ queueMicrotask(() => {
476
+ if (destroyed || !pendingState)
477
+ return;
478
+ const deferred = pendingState;
479
+ pendingState = null;
480
+ doReconcile(deferred);
481
+ });
482
+ return;
483
+ }
484
+ isReconciling = true;
485
+ try {
486
+ doReconcile(state);
487
+ }
488
+ finally {
489
+ isReconciling = false;
490
+ }
439
491
  });
440
492
  // -------------------------------------------------------------------
441
493
  // Exclusive mark boundary helper
@@ -537,9 +589,13 @@ export function createWebEditor(config) {
537
589
  root.focus();
538
590
  }
539
591
  // -------------------------------------------------------------------
592
+ // Auto-wire link popover
593
+ // -------------------------------------------------------------------
594
+ let linkPopoverHandle = null;
595
+ // -------------------------------------------------------------------
540
596
  // Return WebEditor handle
541
597
  // -------------------------------------------------------------------
542
- return {
598
+ const webEditor = {
543
599
  engine,
544
600
  commandBus,
545
601
  content: contentPipeline,
@@ -547,6 +603,8 @@ export function createWebEditor(config) {
547
603
  blockRenderers,
548
604
  triggers: triggerManager,
549
605
  cursorRect,
606
+ get linkPopover() { return linkPopoverHandle; },
607
+ perf: perfInstrumentation,
550
608
  focus() {
551
609
  if (destroyed)
552
610
  return;
@@ -555,7 +613,7 @@ export function createWebEditor(config) {
555
613
  // reset the contenteditable's selection when focus leaves and returns
556
614
  // (e.g., after interacting with a popover). The engine is the source
557
615
  // of truth, so always sync after focusing.
558
- setDomSelection(root, engine.state.doc, engine.state.selection);
616
+ setDomSelection(root, engine.state.doc, engine.state.selection, blockElementMap);
559
617
  },
560
618
  blur() {
561
619
  if (destroyed)
@@ -571,6 +629,15 @@ export function createWebEditor(config) {
571
629
  if (destroyed)
572
630
  return;
573
631
  destroyed = true;
632
+ // Clean up auto-wired link popover
633
+ if (linkPopoverHandle) {
634
+ linkPopoverHandle.dispose();
635
+ try {
636
+ engine.exec(LinkCommands._UNWIRE_UI);
637
+ }
638
+ catch { /* engine may be destroyed */ }
639
+ linkPopoverHandle = null;
640
+ }
574
641
  // Detach event handlers
575
642
  shortcutHandler.detach();
576
643
  inputBridge.detach();
@@ -601,6 +668,10 @@ export function createWebEditor(config) {
601
668
  // Reset composition handler
602
669
  compositionHandler.reset();
603
670
  resetSuppression();
671
+ // Clear block element map
672
+ blockElementMap.clear();
673
+ // Destroy performance instrumentation
674
+ perfInstrumentation?.destroy();
604
675
  // Remove contenteditable attributes
605
676
  root.removeAttribute('contenteditable');
606
677
  root.removeAttribute('role');
@@ -613,6 +684,12 @@ export function createWebEditor(config) {
613
684
  root.style.whiteSpace = '';
614
685
  },
615
686
  };
687
+ // Auto-wire link popover when link plugin is registered and not disabled/readOnly
688
+ if (config.linkPopover !== false && !readOnly && engine.canExecute(LinkCommands._WIRE_UI)) {
689
+ linkPopoverHandle = createLinkPopover(webEditor, root);
690
+ engine.exec(LinkCommands._WIRE_UI, linkPopoverHandle.show);
691
+ }
692
+ return webEditor;
616
693
  }
617
694
  // ---------------------------------------------------------------------------
618
695
  // Helpers
@@ -689,7 +766,30 @@ function getOffsetFromDropPoint(root, doc, clientX, clientY) {
689
766
  * @param clientY - The Y coordinate to find the nearest block to
690
767
  * @returns The nearest block element, or null if no blocks found
691
768
  */
692
- function findNearestBlock(root, clientY) {
769
+ function findNearestBlock(root, clientY, doc, blockElementMap) {
770
+ // Use persistent Map for O(1) element lookup (avoids querySelectorAll + n getBoundingClientRect)
771
+ if (doc && blockElementMap && blockElementMap.size > 0) {
772
+ let nearest = null;
773
+ let nearestDistance = Infinity;
774
+ for (const block of doc.blocks) {
775
+ const el = blockElementMap.get(block.id);
776
+ if (!el)
777
+ continue;
778
+ const rect = el.getBoundingClientRect();
779
+ const midY = rect.top + rect.height / 2;
780
+ const distance = Math.abs(clientY - midY);
781
+ if (distance < nearestDistance) {
782
+ nearestDistance = distance;
783
+ nearest = el;
784
+ }
785
+ else {
786
+ // Blocks are in document order; once distance starts increasing, we've found the nearest
787
+ break;
788
+ }
789
+ }
790
+ return nearest;
791
+ }
792
+ // Fallback: scan DOM children
693
793
  const blocks = root.querySelectorAll('[data-rtif-block]');
694
794
  if (blocks.length === 0)
695
795
  return null;
@@ -704,6 +804,9 @@ function findNearestBlock(root, clientY) {
704
804
  nearestDistance = distance;
705
805
  nearest = block;
706
806
  }
807
+ else {
808
+ break;
809
+ }
707
810
  }
708
811
  return nearest;
709
812
  }