svedit 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -21
- package/dist/Command.svelte.d.ts +38 -30
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +271 -326
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +16 -13
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +39 -22
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +1 -1
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +12 -6
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +19 -10
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +20 -6
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +81 -129
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +542 -648
- package/dist/Svedit.svelte +144 -138
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +32 -18
- package/dist/TextProperty.svelte.d.ts +3 -5
- package/dist/TextProperty.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +64 -127
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +839 -1001
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +67 -155
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +619 -863
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -17
- package/dist/node_visibility.svelte.d.ts +110 -9
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +473 -544
- package/dist/paste_utils.d.ts +11 -44
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -152
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +125 -150
- package/dist/types.d.ts +277 -309
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +40 -119
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +346 -473
- package/package.json +69 -70
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +16 -13
- package/src/lib/NodeArrayProperty.svelte +39 -22
- package/src/lib/NodeCaret.svelte +1 -1
- package/src/lib/NodeGap.svelte +12 -6
- package/src/lib/NodeGapMarkers.svelte +19 -10
- package/src/lib/NodeSelectionMarkers.svelte +20 -6
- package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
- package/src/lib/Svedit.svelte +144 -138
- package/src/lib/TextProperty.svelte +32 -18
- package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
- package/src/lib/{index.js → index.ts} +47 -0
- package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
- package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
- package/src/lib/{types.d.ts → types.ts} +143 -66
- package/src/lib/{utils.js → utils.ts} +110 -129
package/src/lib/Svedit.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts" generics="S extends DocumentSchema">
|
|
2
2
|
import { flushSync, getContext, setContext } from 'svelte';
|
|
3
3
|
import {
|
|
4
4
|
get_char_length,
|
|
@@ -24,17 +24,21 @@
|
|
|
24
24
|
import './styles/svedit-colors.css';
|
|
25
25
|
import './styles/svedit-animations.css';
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
import type {
|
|
28
|
+
DocumentSchema,
|
|
29
|
+
SveditProps,
|
|
30
|
+
Selection,
|
|
31
|
+
NodeSelection,
|
|
32
|
+
TextSelection,
|
|
33
|
+
PropertySelection,
|
|
34
|
+
DocumentNode,
|
|
35
|
+
DocumentPath,
|
|
36
|
+
DynamicRecord,
|
|
37
|
+
NodeHtmlExporter
|
|
38
|
+
} from './types.js';
|
|
39
|
+
import type { KeyMapper } from './KeyMapper.svelte.js';
|
|
40
|
+
import type Session from './Session.svelte.js';
|
|
36
41
|
|
|
37
|
-
/** @type {SveditProps} */
|
|
38
42
|
let {
|
|
39
43
|
session,
|
|
40
44
|
editable = $bindable(false),
|
|
@@ -42,9 +46,9 @@
|
|
|
42
46
|
class: css_class,
|
|
43
47
|
autocapitalize = 'on',
|
|
44
48
|
spellcheck = 'true'
|
|
45
|
-
} = $props();
|
|
49
|
+
}: SveditProps<S> = $props();
|
|
46
50
|
|
|
47
|
-
let canvas_el;
|
|
51
|
+
let canvas_el: HTMLElement | undefined;
|
|
48
52
|
let root_node = $derived(session.get(path));
|
|
49
53
|
let Overlays = $derived(session.config.system_components?.overlays);
|
|
50
54
|
let NodeSelectionMarkers = $derived(
|
|
@@ -54,7 +58,13 @@
|
|
|
54
58
|
|
|
55
59
|
let is_composing = $state(false);
|
|
56
60
|
let canvas_focused = $state(false);
|
|
57
|
-
let before_composition_selection = null;
|
|
61
|
+
let before_composition_selection: TextSelection | null = null;
|
|
62
|
+
// Selection restoration and selection revealing are separate concerns.
|
|
63
|
+
// DOM changes (for example, toggling an annotation) can require rebuilding
|
|
64
|
+
// the native selection even when the logical selection did not move. Keep
|
|
65
|
+
// the last selection rendered while focused so those rebuilds do not also
|
|
66
|
+
// move the viewport.
|
|
67
|
+
let last_rendered_selection_snapshot: string | null = null;
|
|
58
68
|
// Set by onselectionchange before it commits a DOM-derived selection
|
|
59
69
|
// to the model. render_selection consumes-and-clears it to skip
|
|
60
70
|
// rerender on DOM-driven changes (the DOM is already in place).
|
|
@@ -98,8 +108,7 @@
|
|
|
98
108
|
function check_duplicate_paths() {
|
|
99
109
|
if (!canvas_el) return;
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
const mounted_paths = Object.create(null);
|
|
111
|
+
const mounted_paths: Record<string, true> = Object.create(null);
|
|
103
112
|
|
|
104
113
|
for (const element of canvas_el.querySelectorAll('[data-path]')) {
|
|
105
114
|
if (element.closest('.svedit-canvas') !== canvas_el) continue;
|
|
@@ -117,26 +126,8 @@
|
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
// Test-only hook: expose the svedit context so test specs can read
|
|
121
|
-
// array_indices / edge_map for diagnostics. Gated on `import.meta.env.MODE`
|
|
122
|
-
// — Vite replaces the literal so the comparison is constant-false in
|
|
123
|
-
// production and the branch is dead-code-eliminated. Wrapped in an
|
|
124
|
-
// effect so unmount clears the reference and tests don't leak state
|
|
125
|
-
// across remounts.
|
|
126
|
-
if (import.meta.env?.MODE === 'test') {
|
|
127
|
-
$effect(() => {
|
|
128
|
-
const g = /** @type {any} */ (globalThis);
|
|
129
|
-
g.__svedit_ctx_for_test = context;
|
|
130
|
-
return () => {
|
|
131
|
-
if (g.__svedit_ctx_for_test === context) {
|
|
132
|
-
delete g.__svedit_ctx_for_test;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
129
|
// Get KeyMapper from context (may be undefined if not provided)
|
|
139
|
-
const key_mapper = getContext('key_mapper');
|
|
130
|
+
const key_mapper = getContext<KeyMapper>('key_mapper');
|
|
140
131
|
|
|
141
132
|
// Initialize commands and keymap on the session
|
|
142
133
|
$effect(() => {
|
|
@@ -146,7 +137,7 @@
|
|
|
146
137
|
/**
|
|
147
138
|
* @param {InputEvent} event
|
|
148
139
|
*/
|
|
149
|
-
async function onbeforeinput(event) {
|
|
140
|
+
async function onbeforeinput(event: InputEvent) {
|
|
150
141
|
// console.log(`onbeforeinput: ${event.inputType}, data: "${event.data}", isComposing: ${event.isComposing}`, event);
|
|
151
142
|
|
|
152
143
|
if (event.inputType === 'historyUndo' && is_composing) {
|
|
@@ -277,7 +268,7 @@
|
|
|
277
268
|
* Handles composition end events for input methods like dead keys
|
|
278
269
|
* This occurs when composition is complete (e.g., after typing 'a' following backtick to get 'à')
|
|
279
270
|
*/
|
|
280
|
-
function oncompositionend(event) {
|
|
271
|
+
function oncompositionend(event: CompositionEvent) {
|
|
281
272
|
// console.log('DEBUG: oncompositionend, insert:', event.data, event);
|
|
282
273
|
if (!canvas_el?.contains(document.activeElement)) return;
|
|
283
274
|
if (session.selection?.type === 'text') {
|
|
@@ -350,7 +341,7 @@
|
|
|
350
341
|
* @param {string} fallback_html - HTML for cross-app compatibility
|
|
351
342
|
* @returns {string} HTML with embedded svedit data
|
|
352
343
|
*/
|
|
353
|
-
function create_svedit_html_format(json_data, fallback_html) {
|
|
344
|
+
function create_svedit_html_format(json_data: unknown, fallback_html: string): string {
|
|
354
345
|
// Use encodeURIComponent to handle Unicode, then base64 encode
|
|
355
346
|
const json_string = JSON.stringify(json_data);
|
|
356
347
|
const encoded_data = btoa(encodeURIComponent(json_string));
|
|
@@ -367,7 +358,7 @@ ${fallback_html}`;
|
|
|
367
358
|
* @param {string} html - HTML content from clipboard
|
|
368
359
|
* @returns {Object|null} Parsed svedit data or null if not found
|
|
369
360
|
*/
|
|
370
|
-
function extract_svedit_data_from_html(html) {
|
|
361
|
+
function extract_svedit_data_from_html(html: string): DynamicRecord | null {
|
|
371
362
|
const svedit_regex = /data-svedit="([^"]+)"/;
|
|
372
363
|
const match = html.match(svedit_regex);
|
|
373
364
|
|
|
@@ -391,7 +382,11 @@ ${fallback_html}`;
|
|
|
391
382
|
* @param {Object} node - Node object
|
|
392
383
|
* @returns {string} HTML representation
|
|
393
384
|
*/
|
|
394
|
-
function default_node_html_exporter(
|
|
385
|
+
function default_node_html_exporter(
|
|
386
|
+
node: DocumentNode,
|
|
387
|
+
session: Session<S>,
|
|
388
|
+
html_exporters: Record<string, NodeHtmlExporter<S>>
|
|
389
|
+
): string {
|
|
395
390
|
let html = '';
|
|
396
391
|
const node_schema = session.schema[node.type];
|
|
397
392
|
|
|
@@ -415,7 +410,7 @@ ${fallback_html}`;
|
|
|
415
410
|
return html;
|
|
416
411
|
}
|
|
417
412
|
|
|
418
|
-
function default_node_plain_text_exporter(node) {
|
|
413
|
+
function default_node_plain_text_exporter(node: DocumentNode): string {
|
|
419
414
|
let plain_text = '';
|
|
420
415
|
|
|
421
416
|
for (const [prop_name, prop_value] of Object.entries(node)) {
|
|
@@ -442,7 +437,7 @@ ${fallback_html}`;
|
|
|
442
437
|
* @param {Object[]} nodes - Array of node objects
|
|
443
438
|
* @returns {string} HTML representation
|
|
444
439
|
*/
|
|
445
|
-
function export_html(nodes) {
|
|
440
|
+
function export_html(nodes: DocumentNode[]): string {
|
|
446
441
|
let html = '';
|
|
447
442
|
|
|
448
443
|
for (const node of nodes) {
|
|
@@ -459,7 +454,7 @@ ${fallback_html}`;
|
|
|
459
454
|
return html;
|
|
460
455
|
}
|
|
461
456
|
|
|
462
|
-
function export_plain_text(nodes) {
|
|
457
|
+
function export_plain_text(nodes: DocumentNode[]): string {
|
|
463
458
|
let plain_text = '';
|
|
464
459
|
|
|
465
460
|
for (const node of nodes) {
|
|
@@ -472,7 +467,7 @@ ${fallback_html}`;
|
|
|
472
467
|
* @param {ClipboardEvent} event
|
|
473
468
|
* @param {boolean} delete_selection - used by oncut()
|
|
474
469
|
*/
|
|
475
|
-
function oncopy(event, delete_selection = false) {
|
|
470
|
+
function oncopy(event: ClipboardEvent, delete_selection = false) {
|
|
476
471
|
// Only handle copy events if editable and focus is within the canvas
|
|
477
472
|
if (!editable) return;
|
|
478
473
|
if (!canvas_el?.contains(document.activeElement)) return;
|
|
@@ -539,7 +534,7 @@ ${fallback_html}`;
|
|
|
539
534
|
}
|
|
540
535
|
}
|
|
541
536
|
|
|
542
|
-
function oncut(event) {
|
|
537
|
+
function oncut(event: ClipboardEvent) {
|
|
543
538
|
if (!editable) return;
|
|
544
539
|
oncopy(event, true);
|
|
545
540
|
}
|
|
@@ -565,7 +560,7 @@ ${fallback_html}`;
|
|
|
565
560
|
if (!node_array || !Array.isArray(node_array.nodes)) return null;
|
|
566
561
|
|
|
567
562
|
return {
|
|
568
|
-
type:
|
|
563
|
+
type: 'node' as const,
|
|
569
564
|
path: node_array_path,
|
|
570
565
|
anchor_offset: node_array.nodes.length,
|
|
571
566
|
focus_offset: node_array.nodes.length
|
|
@@ -595,11 +590,10 @@ ${fallback_html}`;
|
|
|
595
590
|
*/
|
|
596
591
|
function get_node_insert_caret_after_text_selection(selection = session.selection) {
|
|
597
592
|
if (selection?.type !== 'text') return null;
|
|
598
|
-
const node_index =
|
|
599
|
-
if (Number.isNaN(node_index)) return null;
|
|
593
|
+
const node_index = selection.path.at(-2) as number;
|
|
600
594
|
|
|
601
595
|
return {
|
|
602
|
-
type:
|
|
596
|
+
type: 'node' as const,
|
|
603
597
|
path: selection.path.slice(0, -2),
|
|
604
598
|
anchor_offset: node_index + 1,
|
|
605
599
|
focus_offset: node_index + 1
|
|
@@ -613,7 +607,7 @@ ${fallback_html}`;
|
|
|
613
607
|
* @param {Selection} [selection] - Optional selection (node caret) where the payload should be pasted
|
|
614
608
|
* @returns {boolean} True if the paste operation was successful, false otherwise
|
|
615
609
|
*/
|
|
616
|
-
function try_node_paste(pasted_json, selection) {
|
|
610
|
+
function try_node_paste(pasted_json: DynamicRecord | null, selection?: Selection): boolean {
|
|
617
611
|
const { nodes, main_nodes, marks = [], annotations = [] } = pasted_json || {};
|
|
618
612
|
if (!nodes || !main_nodes?.length) return false;
|
|
619
613
|
|
|
@@ -653,10 +647,10 @@ ${fallback_html}`;
|
|
|
653
647
|
id: 'the_node',
|
|
654
648
|
type: default_text_node_type,
|
|
655
649
|
[target_text_property_name]: text_content || {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
650
|
+
content: '',
|
|
651
|
+
marks: [],
|
|
652
|
+
annotations: []
|
|
653
|
+
}
|
|
660
654
|
}
|
|
661
655
|
});
|
|
662
656
|
nodes_to_insert.push(new_node_id);
|
|
@@ -685,7 +679,7 @@ ${fallback_html}`;
|
|
|
685
679
|
return false;
|
|
686
680
|
}
|
|
687
681
|
|
|
688
|
-
async function onpaste(event) {
|
|
682
|
+
async function onpaste(event: ClipboardEvent) {
|
|
689
683
|
// Only handle paste events if editable and focus is within the canvas
|
|
690
684
|
if (!editable) return;
|
|
691
685
|
if (!canvas_el?.contains(document.activeElement)) return;
|
|
@@ -861,8 +855,8 @@ ${fallback_html}`;
|
|
|
861
855
|
}
|
|
862
856
|
}
|
|
863
857
|
|
|
864
|
-
function render_selection(dom_driven = false) {
|
|
865
|
-
const selection =
|
|
858
|
+
function render_selection(dom_driven = false, should_scroll_selection_into_view = true) {
|
|
859
|
+
const selection = session.selection as Selection;
|
|
866
860
|
|
|
867
861
|
if (!selection) {
|
|
868
862
|
// No model selection -> just leave things as they are
|
|
@@ -891,11 +885,11 @@ ${fallback_html}`;
|
|
|
891
885
|
}
|
|
892
886
|
|
|
893
887
|
if (selection?.type === 'text') {
|
|
894
|
-
__render_text_selection();
|
|
888
|
+
__render_text_selection(should_scroll_selection_into_view);
|
|
895
889
|
} else if (selection?.type === 'node') {
|
|
896
|
-
__render_node_selection();
|
|
890
|
+
__render_node_selection(should_scroll_selection_into_view);
|
|
897
891
|
} else if (selection?.type === 'property') {
|
|
898
|
-
__render_property_selection();
|
|
892
|
+
__render_property_selection(should_scroll_selection_into_view);
|
|
899
893
|
} else {
|
|
900
894
|
console.warn('unsupported selection', $state.snapshot(selection));
|
|
901
895
|
}
|
|
@@ -938,17 +932,15 @@ ${fallback_html}`;
|
|
|
938
932
|
* @param {HTMLElement} el
|
|
939
933
|
* @returns {HTMLElement | null}
|
|
940
934
|
*/
|
|
941
|
-
function __resolve_node_from_gap(el) {
|
|
942
|
-
const gap =
|
|
935
|
+
function __resolve_node_from_gap(el: Element): HTMLElement | null {
|
|
936
|
+
const gap = el.closest('[data-gap-array-path]') as HTMLElement | null;
|
|
943
937
|
if (!gap) return null;
|
|
944
938
|
const array_path = deserialize_path(gap.dataset.gapArrayPath);
|
|
945
939
|
const offset = parseInt(gap.dataset.gapOffset, 10);
|
|
946
940
|
const node_idx = offset > 0 ? offset - 1 : 0;
|
|
947
|
-
return
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
)
|
|
951
|
-
);
|
|
941
|
+
return canvas_el.querySelector<HTMLElement>(
|
|
942
|
+
`[data-path="${serialize_path([...array_path, node_idx])}"][data-type="node"]`
|
|
943
|
+
) as HTMLElement | null;
|
|
952
944
|
}
|
|
953
945
|
|
|
954
946
|
/**
|
|
@@ -958,12 +950,12 @@ ${fallback_html}`;
|
|
|
958
950
|
* @returns {NodeSelection | null} A NodeSelection object if the DOM selection
|
|
959
951
|
* represents a valid node selection, null otherwise
|
|
960
952
|
*/
|
|
961
|
-
function __get_node_selection_from_dom() {
|
|
953
|
+
function __get_node_selection_from_dom(): NodeSelection | null {
|
|
962
954
|
const dom_selection = window.getSelection();
|
|
963
955
|
if (dom_selection.rangeCount === 0) return null;
|
|
964
956
|
|
|
965
|
-
let focus_node =
|
|
966
|
-
let anchor_node =
|
|
957
|
+
let focus_node = dom_selection.focusNode as HTMLElement;
|
|
958
|
+
let anchor_node = dom_selection.anchorNode as HTMLElement;
|
|
967
959
|
|
|
968
960
|
// If focus_node or anchor_node not an element node (e.g. a text node), we need
|
|
969
961
|
// to use the parent element, so we can perform the closest() query on it.
|
|
@@ -973,12 +965,12 @@ ${fallback_html}`;
|
|
|
973
965
|
// EDGE CASE: Collapsed selection inside an empty node placeholder.
|
|
974
966
|
// Firefox can place the DOM selection on the placeholder node itself,
|
|
975
967
|
// which otherwise looks like selecting node index 0 in an empty array.
|
|
976
|
-
const focus_empty_placeholder =
|
|
977
|
-
|
|
978
|
-
);
|
|
979
|
-
const anchor_empty_placeholder =
|
|
980
|
-
|
|
981
|
-
);
|
|
968
|
+
const focus_empty_placeholder = focus_node.closest(
|
|
969
|
+
'.empty-node-placeholder[data-path][data-type="node"]'
|
|
970
|
+
) as HTMLElement | null;
|
|
971
|
+
const anchor_empty_placeholder = anchor_node.closest(
|
|
972
|
+
'.empty-node-placeholder[data-path][data-type="node"]'
|
|
973
|
+
) as HTMLElement | null;
|
|
982
974
|
if (focus_empty_placeholder && focus_empty_placeholder === anchor_empty_placeholder) {
|
|
983
975
|
const empty_placeholder_path = deserialize_path(focus_empty_placeholder.dataset.path);
|
|
984
976
|
const array_path = empty_placeholder_path.slice(0, -1);
|
|
@@ -995,7 +987,7 @@ ${fallback_html}`;
|
|
|
995
987
|
|
|
996
988
|
// EDGE CASE: Collapsed selection inside a node gap (gap-after or gap-before).
|
|
997
989
|
// Gaps are siblings of nodes with data-gap-array-path and data-gap-offset.
|
|
998
|
-
const gap_el =
|
|
990
|
+
const gap_el = focus_node.closest('[data-gap-array-path]') as HTMLElement | null;
|
|
999
991
|
if (gap_el && focus_node === anchor_node) {
|
|
1000
992
|
const array_path = deserialize_path(gap_el.dataset.gapArrayPath);
|
|
1001
993
|
const gap_offset = parseInt(gap_el.dataset.gapOffset, 10);
|
|
@@ -1009,12 +1001,12 @@ ${fallback_html}`;
|
|
|
1009
1001
|
|
|
1010
1002
|
let focus_root =
|
|
1011
1003
|
__resolve_node_from_gap(focus_node) ??
|
|
1012
|
-
|
|
1004
|
+
(focus_node.closest('[data-path][data-type="node"]') as HTMLElement);
|
|
1013
1005
|
if (!focus_root) return null;
|
|
1014
1006
|
|
|
1015
1007
|
let anchor_root =
|
|
1016
1008
|
__resolve_node_from_gap(anchor_node) ??
|
|
1017
|
-
|
|
1009
|
+
(anchor_node.closest('[data-path][data-type="node"]') as HTMLElement);
|
|
1018
1010
|
if (!anchor_root) return null;
|
|
1019
1011
|
|
|
1020
1012
|
let focus_root_path = deserialize_path(focus_root.dataset.path);
|
|
@@ -1112,17 +1104,17 @@ ${fallback_html}`;
|
|
|
1112
1104
|
* @returns {PropertySelection | null} A PropertySelection object if the DOM selection
|
|
1113
1105
|
* represents a valid property selection, null otherwise
|
|
1114
1106
|
*/
|
|
1115
|
-
function __get_property_selection_from_dom() {
|
|
1107
|
+
function __get_property_selection_from_dom(): PropertySelection | null {
|
|
1116
1108
|
const dom_selection = window.getSelection();
|
|
1117
1109
|
if (dom_selection.rangeCount === 0) return null;
|
|
1118
1110
|
|
|
1119
|
-
let focus_root =
|
|
1120
|
-
|
|
1121
|
-
);
|
|
1111
|
+
let focus_root = dom_selection.focusNode.parentElement?.closest(
|
|
1112
|
+
'[data-path][data-type="property"]'
|
|
1113
|
+
) as HTMLElement;
|
|
1122
1114
|
if (!focus_root) return null;
|
|
1123
|
-
let anchor_root =
|
|
1124
|
-
|
|
1125
|
-
);
|
|
1115
|
+
let anchor_root = dom_selection.anchorNode.parentElement?.closest(
|
|
1116
|
+
'[data-path][data-type="property"]'
|
|
1117
|
+
) as HTMLElement;
|
|
1126
1118
|
if (!anchor_root) return null;
|
|
1127
1119
|
|
|
1128
1120
|
if (focus_root === anchor_root) {
|
|
@@ -1149,10 +1141,13 @@ ${fallback_html}`;
|
|
|
1149
1141
|
* @returns {TextSelection | null} A TextSelection object if the DOM selection
|
|
1150
1142
|
* represents a valid text selection, null otherwise
|
|
1151
1143
|
*/
|
|
1152
|
-
function __get_text_selection_from_dom(
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
let
|
|
1144
|
+
function __get_text_selection_from_dom(
|
|
1145
|
+
range: Range | StaticRange | null = null
|
|
1146
|
+
): TextSelection | null {
|
|
1147
|
+
let dom_selection: globalThis.Selection | null = null;
|
|
1148
|
+
let focus_node: Node | null;
|
|
1149
|
+
let anchor_node: Node | null;
|
|
1150
|
+
let focus_offset_in_node: number; // anchor_offset_in_node;
|
|
1156
1151
|
|
|
1157
1152
|
if (range) {
|
|
1158
1153
|
// When range is provided, use it directly
|
|
@@ -1171,23 +1166,24 @@ ${fallback_html}`;
|
|
|
1171
1166
|
range = dom_selection.getRangeAt(0);
|
|
1172
1167
|
}
|
|
1173
1168
|
|
|
1174
|
-
function get_text_root(node) {
|
|
1169
|
+
function get_text_root(node: Node | null): HTMLElement | null {
|
|
1175
1170
|
if (!node) return null;
|
|
1176
1171
|
|
|
1177
|
-
if (node
|
|
1178
|
-
return
|
|
1179
|
-
node.closest?.('[data-path][data-type="text"]') ?? null
|
|
1180
|
-
);
|
|
1172
|
+
if (node instanceof Element) {
|
|
1173
|
+
return node.closest<HTMLElement>('[data-path][data-type="text"]');
|
|
1181
1174
|
}
|
|
1182
1175
|
|
|
1183
|
-
return
|
|
1184
|
-
node.parentElement?.closest('[data-path][data-type="text"]') ?? null
|
|
1185
|
-
);
|
|
1176
|
+
return node.parentElement?.closest<HTMLElement>('[data-path][data-type="text"]') ?? null;
|
|
1186
1177
|
}
|
|
1187
1178
|
|
|
1188
|
-
let focus_root
|
|
1179
|
+
let focus_root: HTMLElement | null;
|
|
1180
|
+
let anchor_root: HTMLElement | null;
|
|
1189
1181
|
|
|
1190
|
-
if (
|
|
1182
|
+
if (
|
|
1183
|
+
focus_node === anchor_node &&
|
|
1184
|
+
focus_node instanceof HTMLElement &&
|
|
1185
|
+
focus_node.dataset.type === 'text'
|
|
1186
|
+
) {
|
|
1191
1187
|
// EDGE CASE 1: Either text node is empty (only a <br> is present), or caret is after a <br> at the very end of the text node
|
|
1192
1188
|
focus_root = anchor_root = focus_node;
|
|
1193
1189
|
} else {
|
|
@@ -1248,7 +1244,7 @@ ${fallback_html}`;
|
|
|
1248
1244
|
}
|
|
1249
1245
|
}
|
|
1250
1246
|
|
|
1251
|
-
function get_text_offset(container, offset) {
|
|
1247
|
+
function get_text_offset(container: Node, offset: number): number {
|
|
1252
1248
|
const offset_range = window.document.createRange();
|
|
1253
1249
|
offset_range.setStart(focus_root, 0);
|
|
1254
1250
|
offset_range.setEnd(container, offset);
|
|
@@ -1275,7 +1271,7 @@ ${fallback_html}`;
|
|
|
1275
1271
|
};
|
|
1276
1272
|
}
|
|
1277
1273
|
|
|
1278
|
-
function __get_node_element(node_array_path, node_offset) {
|
|
1274
|
+
function __get_node_element(node_array_path: DocumentPath, node_offset: number): Element | null {
|
|
1279
1275
|
if (!canvas_el) return null;
|
|
1280
1276
|
return canvas_el.querySelector(
|
|
1281
1277
|
`[data-path="${serialize_path([...node_array_path, node_offset])}"][data-type="node"]`
|
|
@@ -1289,14 +1285,14 @@ ${fallback_html}`;
|
|
|
1289
1285
|
* node clipped only by an inner scroll container still counts visible.
|
|
1290
1286
|
* @param {Element | null | undefined} el
|
|
1291
1287
|
*/
|
|
1292
|
-
function __intersects_viewport(el) {
|
|
1288
|
+
function __intersects_viewport(el: Element | null | undefined): boolean {
|
|
1293
1289
|
if (!el) return false;
|
|
1294
1290
|
const r = el.getBoundingClientRect();
|
|
1295
1291
|
return r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
|
|
1296
1292
|
}
|
|
1297
1293
|
|
|
1298
|
-
function __render_node_selection() {
|
|
1299
|
-
const selection =
|
|
1294
|
+
function __render_node_selection(should_scroll_selection_into_view = true) {
|
|
1295
|
+
const selection = session.selection as NodeSelection;
|
|
1300
1296
|
const node_array_path = selection.path;
|
|
1301
1297
|
const node_array_path_str = serialize_path(node_array_path);
|
|
1302
1298
|
const is_collapsed = is_selection_collapsed(selection);
|
|
@@ -1310,7 +1306,7 @@ ${fallback_html}`;
|
|
|
1310
1306
|
const dom_selection = window.getSelection();
|
|
1311
1307
|
const range = window.document.createRange();
|
|
1312
1308
|
|
|
1313
|
-
const gap_selector = (offset) =>
|
|
1309
|
+
const gap_selector = (offset: number) =>
|
|
1314
1310
|
`[data-gap-array-path="${node_array_path_str}"][data-gap-offset="${offset}"]`;
|
|
1315
1311
|
|
|
1316
1312
|
if (is_collapsed) {
|
|
@@ -1346,6 +1342,7 @@ ${fallback_html}`;
|
|
|
1346
1342
|
dom_selection.addRange(range);
|
|
1347
1343
|
}
|
|
1348
1344
|
}
|
|
1345
|
+
if (!should_scroll_selection_into_view) return;
|
|
1349
1346
|
|
|
1350
1347
|
// Scroll the cursor into view, but only when it is genuinely
|
|
1351
1348
|
// off-screen. This runs on every node-selection re-render — a
|
|
@@ -1409,10 +1406,10 @@ ${fallback_html}`;
|
|
|
1409
1406
|
}, 0);
|
|
1410
1407
|
}
|
|
1411
1408
|
|
|
1412
|
-
function __render_property_selection() {
|
|
1409
|
+
function __render_property_selection(should_scroll_selection_into_view = true) {
|
|
1413
1410
|
const selection = session.selection;
|
|
1414
1411
|
// The element that holds the property
|
|
1415
|
-
const el = canvas_el.querySelector(
|
|
1412
|
+
const el = canvas_el.querySelector<HTMLElement>(
|
|
1416
1413
|
`[data-path="${serialize_path(selection.path)}"][data-type="property"]`
|
|
1417
1414
|
);
|
|
1418
1415
|
|
|
@@ -1426,36 +1423,33 @@ ${fallback_html}`;
|
|
|
1426
1423
|
dom_selection.removeAllRanges();
|
|
1427
1424
|
dom_selection.addRange(range);
|
|
1428
1425
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1426
|
+
if (should_scroll_selection_into_view) {
|
|
1427
|
+
setTimeout(() => {
|
|
1428
|
+
el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1429
|
+
}, 0);
|
|
1430
|
+
}
|
|
1433
1431
|
}
|
|
1434
1432
|
|
|
1435
|
-
function __render_text_selection() {
|
|
1436
|
-
const selection =
|
|
1433
|
+
function __render_text_selection(should_scroll_selection_into_view = true) {
|
|
1434
|
+
const selection = session.selection as TextSelection;
|
|
1437
1435
|
// The element that holds the annotated string
|
|
1438
|
-
const el = canvas_el.querySelector(
|
|
1436
|
+
const el = canvas_el.querySelector<HTMLElement>(
|
|
1439
1437
|
`[data-path="${serialize_path(selection.path)}"][data-type="text"]`
|
|
1440
1438
|
);
|
|
1441
1439
|
const empty_text = session.get(selection.path).content.length === 0;
|
|
1442
1440
|
const dom_selection = window.getSelection();
|
|
1443
1441
|
let current_offset = 0;
|
|
1444
|
-
|
|
1445
|
-
let
|
|
1446
|
-
|
|
1447
|
-
let
|
|
1448
|
-
/** @type {number} */
|
|
1449
|
-
let anchor_node_offset;
|
|
1450
|
-
/** @type {number} */
|
|
1451
|
-
let focus_node_offset;
|
|
1442
|
+
let anchor_node: HTMLElement | Text;
|
|
1443
|
+
let focus_node: HTMLElement | Text;
|
|
1444
|
+
let anchor_node_offset: number;
|
|
1445
|
+
let focus_node_offset: number;
|
|
1452
1446
|
const is_backward = selection.anchor_offset > selection.focus_offset;
|
|
1453
1447
|
const start_offset = Math.min(selection.anchor_offset, selection.focus_offset);
|
|
1454
1448
|
const end_offset = Math.max(selection.anchor_offset, selection.focus_offset);
|
|
1455
1449
|
|
|
1456
1450
|
// Helper function to process each node
|
|
1457
|
-
function process_node(node) {
|
|
1458
|
-
if (node
|
|
1451
|
+
function process_node(node: Node) {
|
|
1452
|
+
if (node instanceof Text) {
|
|
1459
1453
|
const node_text = node.textContent;
|
|
1460
1454
|
const node_char_length = get_char_length(node_text);
|
|
1461
1455
|
|
|
@@ -1490,7 +1484,7 @@ ${fallback_html}`;
|
|
|
1490
1484
|
}
|
|
1491
1485
|
}
|
|
1492
1486
|
current_offset += node_char_length;
|
|
1493
|
-
} else if (node
|
|
1487
|
+
} else if (node instanceof HTMLElement) {
|
|
1494
1488
|
for (const child_node of node.childNodes) {
|
|
1495
1489
|
if (process_node(child_node)) return true; // Stop iteration if end found
|
|
1496
1490
|
}
|
|
@@ -1526,22 +1520,27 @@ ${fallback_html}`;
|
|
|
1526
1520
|
focus_node_offset
|
|
1527
1521
|
);
|
|
1528
1522
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
selected_element
|
|
1536
|
-
|
|
1537
|
-
|
|
1523
|
+
if (should_scroll_selection_into_view) {
|
|
1524
|
+
// Scroll the rendered selection into view. Capture the target now:
|
|
1525
|
+
// the browser selection is live and may be cleared before this
|
|
1526
|
+
// deferred callback runs, e.g. when app UI focuses an external input.
|
|
1527
|
+
const selected_element = focus_node.parentElement;
|
|
1528
|
+
setTimeout(() => {
|
|
1529
|
+
if (selected_element?.isConnected) {
|
|
1530
|
+
selected_element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1531
|
+
}
|
|
1532
|
+
}, 0);
|
|
1533
|
+
}
|
|
1538
1534
|
}
|
|
1539
1535
|
}
|
|
1540
1536
|
|
|
1541
1537
|
// Utils
|
|
1542
1538
|
// --------------------------
|
|
1543
1539
|
|
|
1544
|
-
function __remove_native_composition_text(
|
|
1540
|
+
function __remove_native_composition_text(
|
|
1541
|
+
selection: TextSelection | null,
|
|
1542
|
+
inserted_text: string
|
|
1543
|
+
) {
|
|
1545
1544
|
if (!selection || selection.type !== 'text' || !inserted_text) return;
|
|
1546
1545
|
|
|
1547
1546
|
const text_el = canvas_el.querySelector(
|
|
@@ -1553,7 +1552,10 @@ ${fallback_html}`;
|
|
|
1553
1552
|
if ((text_el.textContent ?? '') === model_text) return;
|
|
1554
1553
|
|
|
1555
1554
|
let current_offset = 0;
|
|
1556
|
-
function get_dom_text_position(
|
|
1555
|
+
function get_dom_text_position(
|
|
1556
|
+
root: Node,
|
|
1557
|
+
target_offset: number
|
|
1558
|
+
): { node: Node; offset: number } | null {
|
|
1557
1559
|
for (const node of root.childNodes) {
|
|
1558
1560
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
1559
1561
|
const node_text = node.textContent ?? '';
|
|
@@ -1619,7 +1621,11 @@ ${fallback_html}`;
|
|
|
1619
1621
|
const dom_driven = selection_source_is_dom;
|
|
1620
1622
|
selection_source_is_dom = false;
|
|
1621
1623
|
if (!canvas_focused) return;
|
|
1622
|
-
|
|
1624
|
+
const selection_snapshot = JSON.stringify(session.selection);
|
|
1625
|
+
const should_scroll_selection_into_view =
|
|
1626
|
+
!dom_driven && selection_snapshot !== last_rendered_selection_snapshot;
|
|
1627
|
+
render_selection(dom_driven, should_scroll_selection_into_view);
|
|
1628
|
+
last_rendered_selection_snapshot = selection_snapshot;
|
|
1623
1629
|
});
|
|
1624
1630
|
</script>
|
|
1625
1631
|
|