svedit 0.4.0 → 0.5.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.
@@ -1,9 +1,19 @@
1
1
  <script>
2
2
  import { setContext } from 'svelte';
3
3
  import { snake_to_pascal, get_char_length, utf16_to_char_offset, char_to_utf16_offset, get_char_at, char_slice } from './util.js';
4
- import { break_text_node, join_text_node, insert_default_node, select_all } from './commands.svelte.js';
4
+ import { break_text_node, insert_default_node, select_all } from './commands.svelte.js';
5
+
6
+ /** @import {
7
+ * SveditProps,
8
+ * DocumentPath,
9
+ * Selection,
10
+ * TextSelection,
11
+ * NodeSelection,
12
+ * PropertySelection,
13
+ * NodeId
14
+ * } from './types.d.ts';
15
+ */
5
16
 
6
- /** @import { SveditProps, DocumentPath, Selection, TextSelection, NodeSelection, PropertySelection, NodeId } from './types.d.ts'; */
7
17
  /** @type {SveditProps} */
8
18
  let {
9
19
  doc,
@@ -21,9 +31,7 @@
21
31
 
22
32
  // Track temporary disabled onkeydown events (e.g. during character composition)
23
33
  let skip_onkeydown = false;
24
-
25
34
  let is_composing = $state(false);
26
-
27
35
  let input_selection = undefined;
28
36
 
29
37
  // Detect Chrome on desktop (not mobile) - only available in browser
@@ -183,73 +191,18 @@
183
191
  event.stopPropagation();
184
192
  }
185
193
 
186
-
187
194
  if (event.inputType === 'deleteContentBackward') {
188
- if (doc.selection?.type === 'property') {
189
- // For property selections, clear the property value only if it's not already falsy
190
- if (doc.get(doc.selection.path)) {
191
- const tr = doc.tr;
192
- tr.set(doc.selection.path, '');
193
- doc.apply(tr);
194
- }
195
- } else {
196
- if (doc.selection?.type === 'text' && doc.selection?.anchor_offset === 0 && doc.selection?.focus_offset === 0) {
197
- const tr = doc.tr;
198
- join_text_node(tr);
199
- doc.apply(tr);
200
- } else {
201
- doc.apply(doc.tr.delete_selection());
202
- }
203
- }
204
-
195
+ doc.apply(doc.tr.delete_selection('backward'));
205
196
  event.preventDefault();
206
197
  event.stopPropagation();
207
198
  }
208
199
 
209
200
  if (event.inputType === 'deleteContentForward') {
210
- // Forward delete: only handle collapsed text selections
211
- if (doc.selection?.type === 'text' && doc.selection?.anchor_offset === doc.selection?.focus_offset) {
212
- const text_content = doc.get(doc.selection.path).text;
213
- const text_length = get_char_length(text_content);
214
-
215
- if (doc.selection.focus_offset < text_length) {
216
- // There's a character after the cursor - select and delete it
217
- const tr = doc.tr;
218
- tr.set_selection({
219
- type: 'text',
220
- path: [...doc.selection.path],
221
- anchor_offset: doc.selection.focus_offset,
222
- focus_offset: doc.selection.focus_offset + 1
223
- });
224
- tr.delete_selection();
225
- doc.apply(tr);
226
- } else {
227
- // At end of text - try to join with next text node
228
- const tr = doc.tr;
229
- const node_index = parseInt(String(doc.selection.path.at(-2)), 10);
230
- const successor_node = doc.get([...doc.selection.path.slice(0, -2), node_index + 1]);
231
-
232
- // Check if next node is a text node
233
- if (successor_node && doc.kind(successor_node) === 'text') {
234
- // Set selection to beginning of next text node
235
- tr.set_selection({
236
- type: 'text',
237
- path: [...doc.selection.path.slice(0, -2), node_index + 1, 'content'],
238
- anchor_offset: 0,
239
- focus_offset: 0
240
- });
241
- // Use join_text_node to merge with previous
242
- join_text_node(tr);
243
- doc.apply(tr);
244
- }
245
- }
246
- }
247
-
201
+ doc.apply(doc.tr.delete_selection('forward'));
248
202
  event.preventDefault();
249
203
  event.stopPropagation();
250
204
  }
251
205
 
252
-
253
206
  // We remember the "input selection", because in some mobile browsers (Samsung Android) the onbeforeinput event
254
207
  // is fired on a different selection than the input event.
255
208
  input_selection = { ...doc.selection };
@@ -383,29 +336,47 @@ ${fallback_html}`;
383
336
  * @param {Object} node - Node object
384
337
  * @returns {string} HTML representation
385
338
  */
386
- function default_node_exporter(node) {
339
+ function default_node_html_exporter(node, doc, html_exporters) {
387
340
  let html = '';
388
- let has_content = false;
341
+ const node_schema = doc.schema[node.type];
389
342
 
390
343
  for (const [prop_name, prop_value] of Object.entries(node)) {
391
344
  if (prop_name === 'id' || prop_name === 'type') continue;
392
-
345
+ const property_definition = node_schema.properties[prop_name];
393
346
  // Check if this is an annotated_text property (object with text property)
394
- if (typeof prop_value === 'object' && prop_value !== null && typeof prop_value.text === 'string') {
347
+ if (property_definition.type === 'annotated_text') {
395
348
  const text_content = prop_value.text;
396
349
  if (text_content.trim()) {
397
- html += `<p>${text_content}</p>\n`;
398
- has_content = true;
350
+ html += `<p>${text_content.trim()}</p>\n`;
351
+ }
352
+ } else if (property_definition.type === 'node_array') {
353
+ for (const child_id of prop_value) {
354
+ const child = doc.get(child_id);
355
+ const child_exporter = html_exporters[child.type] || default_node_html_exporter;
356
+ html += child_exporter(child, doc, html_exporters);
399
357
  }
400
358
  }
401
359
  }
402
360
 
403
- if (!has_content) {
404
- // Generic fallback for unknown node types with no annotated_text content
405
- html += `<div data-node-type="${node.type}">Content from ${node.type}</div>\n`;
361
+ return html;
362
+ }
363
+
364
+ function default_node_plain_text_exporter(node) {
365
+ let plain_text = '';
366
+
367
+ for (const [prop_name, prop_value] of Object.entries(node)) {
368
+ if (prop_name === 'id' || prop_name === 'type') continue;
369
+
370
+ // Check if this is an annotated_text property (object with text property)
371
+ if (typeof prop_value === 'object' && prop_value !== null && typeof prop_value.text === 'string') {
372
+ const text_content = prop_value.text;
373
+ if (text_content.trim()) {
374
+ plain_text += `${text_content.trim()}\n\n`;
375
+ }
376
+ }
406
377
  }
407
378
 
408
- return html;
379
+ return plain_text;
409
380
  }
410
381
 
411
382
  /**
@@ -421,14 +392,22 @@ ${fallback_html}`;
421
392
 
422
393
  if (html_exporters[node.type]) {
423
394
  // Use custom exporter for this node type
424
- html += html_exporters[node.type](node, doc);
395
+ html += html_exporters[node.type](node, doc, html_exporters);
425
396
  } else {
426
397
  // Use default exporter
427
- html += default_node_exporter(node);
398
+ html += default_node_html_exporter(node, doc, html_exporters);
428
399
  }
429
400
  }
401
+ return html;
402
+ }
430
403
 
431
- return html || '<p>Copied content</p>';
404
+ function export_plain_text(nodes) {
405
+ let plain_text = '';
406
+
407
+ for (const node of nodes) {
408
+ plain_text += default_node_plain_text_exporter(node);
409
+ }
410
+ return plain_text.trim();
432
411
  }
433
412
 
434
413
  /**
@@ -464,18 +443,27 @@ ${fallback_html}`;
464
443
  event.preventDefault();
465
444
  event.stopPropagation();
466
445
 
467
- let plain_text, html;
446
+ let plain_text, annotated_text, html;
468
447
 
469
448
  if (doc.selection?.type === 'text') {
470
449
  plain_text = doc.get_selected_plain_text();
471
- html = `<span style="white-space:pre-wrap;">${plain_text}</span>`;
450
+ annotated_text = doc.get_selected_annotated_text();
451
+ const fallback_html = `<span>${annotated_text.text}</span>`;
452
+
453
+ console.log('Text copy:', {
454
+ annotated_text,
455
+ plain_text,
456
+ html,
457
+ });
458
+
459
+ html = create_svedit_html_format(annotated_text, fallback_html);
472
460
  } else if (doc.selection?.type === 'node') {
473
461
  const selected_nodes = doc.get_selected_nodes();
474
462
  const { nodes, main_nodes } = prepare_copy_payload(selected_nodes);
475
463
 
476
464
  const json_data = { nodes, main_nodes };
477
465
 
478
- console.log('Copy operation:', {
466
+ console.log('Node copy:', {
479
467
  selected_nodes,
480
468
  nodes,
481
469
  total_nodes: Object.keys(nodes).length,
@@ -488,30 +476,30 @@ ${fallback_html}`;
488
476
 
489
477
  // Create HTML with embedded svedit data
490
478
  html = create_svedit_html_format(json_data, fallback_html);
491
-
492
- // Create plain text representation
493
- plain_text = selected_node_objects.map(node => {
494
- if (node.type === 'story') {
495
- return (node.title || 'Untitled Story') + (node.content ? '\n' + node.content : '');
496
- }
497
- return `Content from ${node.type}`;
498
- }).join('\n\n');
479
+ // Generate plain text representation
480
+ plain_text = export_plain_text(selected_node_objects);
481
+ } else if (doc.selection?.type === 'property') {
482
+ const property_definition = doc.inspect(doc.selection.path);
483
+ const value = doc.get(doc.selection.path);
484
+ const json_data = {
485
+ kind: 'property',
486
+ name: property_definition.name,
487
+ type: property_definition.type,
488
+ value,
489
+ };
490
+ console.log('Property copy:', json_data);
491
+ html = create_svedit_html_format(json_data, `<span>${value}</span>`);
492
+ plain_text = String(value);
499
493
  }
500
494
 
501
- // Create a ClipboardItem with multiple formats
502
- const data = {
503
- 'text/plain': new Blob([plain_text || ''], {type: 'text/plain'}),
504
- 'text/html': new Blob([html || ''], {type: 'text/html'}),
505
- };
506
-
507
- const clipboard_item_raw = new ClipboardItem(data);
508
-
509
- // Write to clipboard
510
- navigator.clipboard.write([clipboard_item_raw]).then(() => {
495
+ // Write to clipboard using event.clipboardData
496
+ try {
497
+ event.clipboardData?.setData('text/plain', plain_text || '');
498
+ event.clipboardData?.setData('text/html', html || '');
511
499
  console.log('Data copied to clipboard successfully');
512
- }).catch(err => {
500
+ } catch (err) {
513
501
  console.error('Failed to copy data: ', err);
514
- });
502
+ }
515
503
 
516
504
  if (delete_selection) {
517
505
  doc.apply(doc.tr.delete_selection());
@@ -522,87 +510,163 @@ ${fallback_html}`;
522
510
  oncopy(event, true);
523
511
  }
524
512
 
525
- async function onpaste(event) {
526
- // Only handle paste events if focus is within the canvas
527
- if (!canvas_ref?.contains(document.activeElement)) return;
528
-
529
- event.preventDefault();
530
- const clipboardItems = await navigator.clipboard.read();
531
513
 
532
- let pasted_json;
514
+ /**
515
+ * Attempts to paste JSON data as a node at the current selection.
516
+ *
517
+ * @param {string|object} pasted_json - The JSON data to paste, either as a string or parsed object
518
+ * @param {Selection} [selection] - Optional selection (node cursor) where the payload should be pasted
519
+ * @returns {boolean} True if the paste operation was successful, false otherwise
520
+ */
521
+ function try_node_paste(pasted_json, selection) {
522
+ const { nodes, main_nodes } = pasted_json;
533
523
 
534
- // First try to extract svedit data from HTML format
535
- try {
536
- const html_blob = await clipboardItems[0].getType('text/html');
537
- const html_content = await html_blob.text();
538
- pasted_json = extract_svedit_data_from_html(html_content);
539
- } catch (e) {
540
- console.log('No HTML format available or failed to extract svedit data:', e);
541
- pasted_json = undefined;
524
+ // NOTE: At this point, nodes contains a subgraph from the copy
525
+ // with original ids.
526
+ let tr = doc.tr;
527
+ if (selection) {
528
+ tr.set_selection(selection);
542
529
  }
543
530
 
544
- if (pasted_json) {
545
- const tr = doc.tr;
546
-
547
- // Handle new format: { nodes: {id -> node}, main_nodes: [id1, id2, ...] }
548
- if (pasted_json.nodes && pasted_json.main_nodes) {
549
- const { nodes, main_nodes } = pasted_json;
550
-
551
- // Generate new IDs for all nodes during paste
552
- const id_mapping = {};
553
- for (const node_id in nodes) {
554
- id_mapping[node_id] = doc.generate_id();
531
+ // We can safely assume we're dealing with a node_array property
532
+ const property_definition = doc.inspect(tr.selection.path);
533
+ const first_compatible_text_node_type = property_definition.node_types.find(type => doc.kind({ type }) === 'text');
534
+
535
+ const nodes_to_insert = [];
536
+ let rejected = false;
537
+ for (const node_id of main_nodes) {
538
+ const node = nodes[node_id];
539
+ if (!property_definition.node_types.includes(node.type)) {
540
+ // Incompatible node type detected
541
+ if (doc.kind(node) === 'text' && first_compatible_text_node_type) {
542
+ const new_node_id = tr.build('the_node', {
543
+ the_node: {
544
+ id: 'the_node',
545
+ type: first_compatible_text_node_type,
546
+ content: node.content,
547
+ },
548
+ });
549
+ nodes_to_insert.push(new_node_id);
550
+ } else {
551
+ console.log(`rejected ${node.type}. Only ${property_definition.node_types.join(', ')} allowed.`);
552
+ rejected = true;
553
+ break;
555
554
  }
555
+ } else {
556
+ const new_node_id = tr.build(node_id, nodes);
557
+ nodes_to_insert.push(new_node_id);
558
+ }
559
+ }
556
560
 
557
- // Create nodes with new IDs and updated references
558
- const updated_nodes = {};
559
- for (const [old_id, node] of Object.entries(nodes)) {
560
- const new_node = { ...node };
561
- new_node.id = id_mapping[old_id];
562
-
563
- // Update all property references to use new IDs
564
- for (const [property, value] of Object.entries(new_node)) {
565
- if (property === 'id' || property === 'type') continue;
566
-
567
- const prop_type = doc.schema[node.type]?.properties?.[property]?.type;
568
-
569
- if (prop_type === 'node_array' && Array.isArray(value)) {
570
- new_node[property] = value.map(ref_id => id_mapping[ref_id] || ref_id);
571
- } else if (prop_type === 'node' && typeof value === 'string') {
572
- new_node[property] = id_mapping[value] || value;
573
- } else if (prop_type === 'annotated_text') {
574
- const annotations = value.annotations.map(annotation => {
575
- const {start_offset, end_offset, node_id} = annotation;
576
- return {start_offset, end_offset, node_id: id_mapping[node_id] || node_id};
577
- });
578
- new_node[property] = {text: value.text, annotations};
579
- }
580
- }
581
-
582
- updated_nodes[new_node.id] = new_node;
561
+ if (!rejected) {
562
+ tr.insert_nodes(nodes_to_insert);
563
+ doc.apply(tr);
564
+ return true;
565
+ } else {
566
+ if (tr.selection.path.length >= 2) {
567
+ const next_node_insert_cursor = doc.get_next_node_insert_cursor(tr.selection);
568
+ if (next_node_insert_cursor) {
569
+ try_node_paste(pasted_json, next_node_insert_cursor);
583
570
  }
571
+ }
572
+ }
573
+ return false;
574
+ }
584
575
 
585
- // Create all nodes first
586
- for (const node of Object.values(updated_nodes)) {
587
- tr.create(node);
588
- }
576
+ async function onpaste(event) {
577
+ // Only handle paste events if focus is within the canvas
578
+ if (!canvas_ref?.contains(document.activeElement)) return;
579
+ event.preventDefault();
589
580
 
590
- // Then insert the main nodes at the current selection
591
- const new_main_nodes = main_nodes.map(id => id_mapping[id]);
592
- const main_node_objects = new_main_nodes.map(id => updated_nodes[id]);
593
- tr.insert_nodes(main_node_objects);
581
+ let plain_text, pasted_json, pasted_images = [];
582
+
583
+ // NOTE: For some reason, await navigator.clipboard.read()
584
+ const clipboard_items = event.clipboardData?.items || [];
585
+ for (const item of clipboard_items || []) {
586
+ if (item.type.startsWith("image/")) {
587
+ const blob = item.getAsFile();
588
+ const data_url = URL.createObjectURL(blob);
589
+ pasted_images.push({
590
+ blob,
591
+ data_url,
592
+ type: item.type,
593
+ size: blob.size
594
+ });
594
595
  }
596
+ }
595
597
 
596
- doc.apply(tr);
598
+ if (pasted_images.length > 0) {
599
+ pasted_json = await doc.config.handle_image_paste(doc, pasted_images);
600
+ console.log('pasted_json_after_image_paste', pasted_json);
601
+ // NOTE: If no pasted_json is returned from the custom handler, we assume that content creation has been
602
+ // handled inside handle_image_paste already.
603
+ if (!pasted_json) return;
597
604
  } else {
598
- // Fallback to plain text when no svedit data is found
605
+ // First try to extract svedit data from HTML format
606
+ try {
607
+ const html_content = event.clipboardData?.getData('text/html');
608
+ if (html_content) {
609
+ pasted_json = extract_svedit_data_from_html(html_content);
610
+ }
611
+ } catch (e) {
612
+ console.log('No HTML format available or failed to extract svedit data:', e);
613
+ pasted_json = undefined;
614
+ }
615
+
599
616
  try {
600
- const plain_text_blob = await clipboardItems[0].getType('text/plain');
601
- const plain_text = await plain_text_blob.text();
602
- doc.apply(doc.tr.insert_text(plain_text));
617
+ plain_text = event.clipboardData?.getData('text/plain');
603
618
  } catch (e) {
604
619
  console.error('Failed to paste any content:', e);
605
620
  }
621
+
622
+ // Try to contruct a node payload from plain text when applicable
623
+ if (!pasted_json && typeof plain_text === 'string') {
624
+ const plain_text_fragments = plain_text.split('\n\n').map(fragment => fragment.trim()).filter(Boolean);
625
+ if (plain_text_fragments.length > 1) {
626
+ pasted_json = {
627
+ main_nodes: [],
628
+ nodes: [],
629
+ };
630
+ for (let i = 0; i < plain_text_fragments.length; i++) {
631
+ const fragment = plain_text_fragments[i];
632
+ pasted_json.nodes["fragment_" + i] = {
633
+ id: "fragment_" + i,
634
+ type: 'text',
635
+ content: {
636
+ text: fragment,
637
+ annotations: []
638
+ }
639
+ };
640
+ pasted_json.main_nodes.push("fragment_" + i);
641
+ }
642
+ }
643
+ }
644
+ }
645
+
646
+ if (pasted_json?.main_nodes && doc.selection?.type === 'node') {
647
+ // Paste nodes at a node selection
648
+ try_node_paste(pasted_json);
649
+ } else if (pasted_json?.kind === 'property' && doc.selection?.type === 'property') {
650
+ const property_definition = doc.inspect(doc.selection.path);
651
+ if (property_definition.type === pasted_json.type) {
652
+ doc.apply(doc.tr.set(doc.selection.path, pasted_json.value));
653
+ }
654
+ } else if (doc.selection?.type === 'text' && pasted_json?.text) {
655
+ // Paste text at a text selection
656
+ doc.apply(doc.tr.insert_text(pasted_json.text, pasted_json.annotations, pasted_json.nodes));
657
+ } else if (doc.selection?.type === 'text' && pasted_json?.main_nodes?.length === 1 && doc.kind(pasted_json?.nodes[pasted_json.main_nodes[0]]) === 'text') {
658
+ // Paste a single text node, at a text cursor
659
+ const text_property = pasted_json.nodes[pasted_json.main_nodes[0]].content;
660
+ doc.apply(doc.tr.insert_text(text_property.text, text_property.annotations, pasted_json.nodes));
661
+ } else if (['text', 'property'].includes(doc.selection?.type) && pasted_json?.nodes) {
662
+ // Paste nodes at a text or property selection by finding the next valid insert cursor
663
+ const next_node_insert_cursor = doc.get_next_node_insert_cursor(doc.selection);
664
+ try_node_paste(pasted_json, next_node_insert_cursor);
665
+ } else if (typeof plain_text === 'string') {
666
+ // External paste: Fallback to plain text when no svedit data is found
667
+ doc.apply(doc.tr.insert_text(plain_text.trim()));
668
+ } else {
669
+ console.log('Could not paste.');
606
670
  }
607
671
  }
608
672
 
@@ -700,6 +764,7 @@ ${fallback_html}`;
700
764
  ) {
701
765
  const node = doc.layout_node;
702
766
  const layout_count = doc.config.node_layouts[node.type];
767
+
703
768
  if (layout_count > 1 && node?.layout) {
704
769
  const prev_layout = ((node.layout - 2 + layout_count) % layout_count) + 1;
705
770
  const tr = doc.tr;
@@ -713,16 +778,13 @@ ${fallback_html}`;
713
778
  (e.key === 'ArrowDown' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node )
714
779
  ) {
715
780
  const node = doc.layout_node;
716
-
717
781
  if (doc.selection.type !== 'node') {
718
782
  doc.select_parent();
719
783
  }
720
784
  const old_selection = { ...doc.selection };
721
-
722
785
  const node_array_schema = doc.inspect(doc.selection.path);
723
786
  // If we are not dealing with a node selection in a container, return
724
787
  if (node_array_schema.type !== 'node_array') return;
725
-
726
788
  const current_type_index = node_array_schema.node_types.indexOf(node.type);
727
789
  const next_type_index = (current_type_index + 1) % node_array_schema.node_types.length;
728
790
  const next_type = node_array_schema.node_types[next_type_index];
@@ -736,19 +798,14 @@ ${fallback_html}`;
736
798
  (e.key === 'ArrowUp' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node)
737
799
  ) {
738
800
  const node = doc.layout_node;
739
-
740
801
  if (doc.selection.type !== 'node') {
741
802
  doc.select_parent();
742
803
  }
743
-
744
- // if (selection.type !== 'node') return;
745
804
  const old_selection = { ...doc.selection };
746
805
  const node_array_schema = doc.inspect(doc.selection.path);
747
806
  // If we are not dealing with a node selection in a container, return
748
807
  if (node_array_schema.type !== 'node_array') return;
749
-
750
808
  const current_type_index = node_array_schema.node_types.indexOf(node.type);
751
- // return (current_layout_number - 1 + layout_count) % layout_count;
752
809
  const prev_type_index = (current_type_index - 1 + node_array_schema.node_types.length) % node_array_schema.node_types.length;
753
810
  const prev_type = node_array_schema.node_types[prev_type_index];
754
811
  const tr = doc.tr;
@@ -756,7 +813,6 @@ ${fallback_html}`;
756
813
  tr.set_selection(old_selection);
757
814
  doc.apply(tr);
758
815
  e.preventDefault();
759
-
760
816
  } else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
761
817
  const tr = doc.tr;
762
818
  if (select_all(tr)) {
@@ -812,16 +868,10 @@ ${fallback_html}`;
812
868
  const span_length = Math.abs(selection.focus_offset - selection.anchor_offset);
813
869
 
814
870
  if (is_collapsed) {
815
- // Try to insert default node if there's only one allowed ref_type
871
+ // Always insert default node on ENTER
816
872
  const tr = doc.tr;
817
873
  insert_default_node(tr);
818
874
  doc.apply(tr);
819
- // if (insert_default_node(tr)) {
820
- // doc.apply(tr);
821
- // } else {
822
- // // Fall back to focusing toolbar when multiple types are available
823
- // focus_toolbar();
824
- // }
825
875
  } else if (span_length === 1) {
826
876
  focus_toolbar();
827
877
  }
@@ -1236,7 +1286,7 @@ ${fallback_html}`;
1236
1286
  (is_backward ? focus_node : anchor_node).scrollIntoView({ block: 'nearest', inline: 'nearest' });
1237
1287
  }, 0);
1238
1288
  } else {
1239
- console.log('no container element found!!');
1289
+ console.log('No container element found!');
1240
1290
  }
1241
1291
  }
1242
1292
 
@@ -1245,7 +1295,6 @@ ${fallback_html}`;
1245
1295
  // The element that holds the property
1246
1296
  const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="property"]`);
1247
1297
  const cursor_trap_selectable = el.querySelector('.svedit-selectable');
1248
-
1249
1298
  const range = window.document.createRange();
1250
1299
  const dom_selection = window.getSelection();
1251
1300
 
@@ -1254,6 +1303,14 @@ ${fallback_html}`;
1254
1303
  range.collapse(true); // Collapse to start position
1255
1304
  dom_selection.removeAllRanges();
1256
1305
  dom_selection.addRange(range);
1306
+
1307
+ // Scroll the selection into view
1308
+ setTimeout(() => {
1309
+ const selectedElement = dom_selection.focusNode.parentElement;
1310
+ if (selectedElement) {
1311
+ selectedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
1312
+ }
1313
+ }, 0);
1257
1314
  }
1258
1315
 
1259
1316
  function __render_text_selection() {
@@ -1313,7 +1370,6 @@ ${fallback_html}`;
1313
1370
  return true; // Stop iteration
1314
1371
  }
1315
1372
  }
1316
-
1317
1373
  current_offset += node_char_length;
1318
1374
  } else if (node.nodeType === Node.ELEMENT_NODE) {
1319
1375
  for (const child_node of node.childNodes) {
@@ -1336,16 +1392,6 @@ ${fallback_html}`;
1336
1392
  for (const child_node of el.childNodes) {
1337
1393
  if (process_node(child_node)) break;
1338
1394
  }
1339
-
1340
- // EDGE CASE: cursor at the end of an annotation
1341
- // TODO: It seems this edge case is handled elsewhere. We keep around the checker for a
1342
- // while and delete this branch, if we don't see any problems.
1343
- if (anchor_node && !focus_node && current_offset === end_offset) {
1344
- alert('EDGE CASE: cursor at the end of an annotation.');
1345
- // console.log('EDGE CASE: cursor at the end of an annotation');
1346
- // focus_node = /** @type {HTMLElement | Text} */ (anchor_node.nextSibling || anchor_node);
1347
- // focus_node_offset = focus_node === anchor_node ? anchor_node.length : 0;
1348
- }
1349
1395
  }
1350
1396
 
1351
1397
  // Set the range if both start and end were found
@@ -1431,6 +1477,7 @@ ${fallback_html}`;
1431
1477
  class="svedit-canvas {css_class}"
1432
1478
  class:hide-selection={doc.selection?.type === 'node'}
1433
1479
  class:node-cursor={doc.selection?.type === 'node' && doc.selection.anchor_offset === doc.selection.focus_offset}
1480
+ class:property-selection={doc.selection?.type === 'property'}
1434
1481
  bind:this={canvas_ref}
1435
1482
  {oninput}
1436
1483
  {onbeforeinput}
@@ -1475,7 +1522,7 @@ ${fallback_html}`;
1475
1522
  }
1476
1523
 
1477
1524
  /* When the cursor is in a cursor-trap we never want to see the caret */
1478
- .svedit-canvas.node-cursor {
1525
+ .svedit-canvas.node-cursor, .svedit-canvas.property-selection {
1479
1526
  caret-color: transparent;
1480
1527
  }
1481
1528
 
@@ -1 +1 @@
1
- {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAo6CA;;eAAqD;iCAx5CgE,cAAc"}
1
+ {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAk9CA;;eAAqD;iCA97CzC,cAAc"}