svedit 0.7.1 → 0.8.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 +91 -2
- package/dist/Node.svelte +4 -25
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +17 -3
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +64 -27
- package/dist/NodeGap.svelte.d.ts +6 -6
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +36 -10
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/Svedit.svelte +76 -115
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/node_gap_computation.svelte.js +1 -1
- package/package.json +1 -1
- package/src/lib/Node.svelte +4 -25
- package/src/lib/NodeArrayProperty.svelte +17 -3
- package/src/lib/NodeGap.svelte +64 -27
- package/src/lib/NodeGapMarkers.svelte +36 -10
- package/src/lib/Svedit.svelte +76 -115
- package/src/lib/node_gap_computation.svelte.js +1 -1
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
* --node-caret-blink-duration
|
|
67
67
|
* --node-caret-animation
|
|
68
68
|
* --node-caret-row-inline-position
|
|
69
|
+
* --node-caret-boundary (anchor-name of a parent element; edge
|
|
70
|
+
* gaps clamp to its edges instead of 0px.
|
|
71
|
+
* Prevents overlap when the node array has
|
|
72
|
+
* neighbors. See NodeGap.svelte for details.)
|
|
73
|
+
* --node-caret-boundary-x (per-axis override; clamps left/right only)
|
|
74
|
+
* --node-caret-boundary-y (per-axis override; clamps top/bottom only)
|
|
69
75
|
*
|
|
70
76
|
* Row/column detection uses var(--row, 1) with the * 99999 multiplier
|
|
71
77
|
* trick throughout. Shorthand:
|
|
@@ -262,9 +268,11 @@
|
|
|
262
268
|
|
|
263
269
|
/* Edge first: column = above first node, row = left of first node */
|
|
264
270
|
.gap-edge.first {
|
|
271
|
+
--_b-t: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) top, 0px);
|
|
272
|
+
--_b-l: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) left, 0px);
|
|
265
273
|
top: min(
|
|
266
274
|
calc(anchor(var(--_a) top) + var(--_C) * 99999px),
|
|
267
|
-
calc(max(
|
|
275
|
+
calc(max(var(--_b-t), calc(anchor(var(--_a) top) - var(--_gm))) + var(--_R) * 99999px)
|
|
268
276
|
);
|
|
269
277
|
bottom: min(
|
|
270
278
|
calc(anchor(var(--_a) bottom) + var(--_C) * 99999px),
|
|
@@ -272,7 +280,7 @@
|
|
|
272
280
|
);
|
|
273
281
|
left: min(
|
|
274
282
|
calc(anchor(var(--_a) left) + var(--_R) * 99999px),
|
|
275
|
-
calc(max(
|
|
283
|
+
calc(max(var(--_b-l), calc(anchor(var(--_a) left) - var(--_gm))) + var(--_C) * 99999px)
|
|
276
284
|
);
|
|
277
285
|
right: min(
|
|
278
286
|
calc(anchor(var(--_a) right) + var(--_R) * 99999px),
|
|
@@ -280,23 +288,40 @@
|
|
|
280
288
|
);
|
|
281
289
|
}
|
|
282
290
|
|
|
283
|
-
/* Edge last: column = below last node, row = right of last node
|
|
291
|
+
/* Edge last: column = below last node, row = right of last node.
|
|
292
|
+
top/left also clamp to boundary - gm so that min-height/min-width
|
|
293
|
+
(which win over bottom/right in overconstrained abs-pos) cannot
|
|
294
|
+
push the element past the boundary. */
|
|
284
295
|
.gap-edge.last {
|
|
296
|
+
--_b-b: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) bottom, 0px);
|
|
297
|
+
--_b-r: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) right, 0px);
|
|
298
|
+
--_b-bt: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) bottom, 99999px);
|
|
299
|
+
--_b-rl: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) right, 99999px);
|
|
285
300
|
top: min(
|
|
286
301
|
calc(anchor(var(--_a) top) + var(--_C) * 99999px),
|
|
287
|
-
calc(
|
|
302
|
+
calc(
|
|
303
|
+
min(
|
|
304
|
+
anchor(var(--_a) bottom),
|
|
305
|
+
calc(var(--_b-bt) - var(--_gm))
|
|
306
|
+
) + var(--_R) * 99999px
|
|
307
|
+
)
|
|
288
308
|
);
|
|
289
309
|
bottom: min(
|
|
290
310
|
calc(anchor(var(--_a) bottom) + var(--_C) * 99999px),
|
|
291
|
-
calc(anchor(var(--_a) bottom) - var(--_gm) + var(--_R) * 99999px)
|
|
311
|
+
calc(max(var(--_b-b), calc(anchor(var(--_a) bottom) - var(--_gm))) + var(--_R) * 99999px)
|
|
292
312
|
);
|
|
293
313
|
left: min(
|
|
294
314
|
calc(anchor(var(--_a) left) + var(--_R) * 99999px),
|
|
295
|
-
calc(
|
|
315
|
+
calc(
|
|
316
|
+
min(
|
|
317
|
+
anchor(var(--_a) right),
|
|
318
|
+
calc(var(--_b-rl) - var(--_gm))
|
|
319
|
+
) + var(--_C) * 99999px
|
|
320
|
+
),
|
|
296
321
|
calc(100% - var(--_gm) + var(--_C) * 99999px)
|
|
297
322
|
);
|
|
298
323
|
right: max(
|
|
299
|
-
calc(
|
|
324
|
+
calc(var(--_b-r) + var(--_C) * -99999px),
|
|
300
325
|
calc(anchor(var(--_a) right) + var(--_R) * -99999px),
|
|
301
326
|
calc(anchor(var(--_a) right) - var(--_gm) + var(--_C) * -99999px),
|
|
302
327
|
calc(anchor(var(--_c) right) + var(--_C) * -99999px)
|
|
@@ -314,6 +339,7 @@
|
|
|
314
339
|
left: min(
|
|
315
340
|
calc(anchor(var(--_a) left) + var(--_R) * 99999px),
|
|
316
341
|
calc(anchor(var(--_a) right) + var(--_C) * 99999px),
|
|
342
|
+
calc(var(--_b-rl) - var(--_gm) + var(--_C) * 99999px),
|
|
317
343
|
calc(
|
|
318
344
|
anchor(var(--_a) right)
|
|
319
345
|
+ (max(0px, anchor(var(--_s) left) - anchor(var(--_f) right))) / 2
|
|
@@ -341,7 +367,7 @@
|
|
|
341
367
|
);
|
|
342
368
|
right: max(
|
|
343
369
|
calc(anchor(var(--_a) right) + var(--_R) * -99999px),
|
|
344
|
-
calc(
|
|
370
|
+
calc(var(--_b-r) + var(--_C) * -99999px),
|
|
345
371
|
calc(
|
|
346
372
|
anchor(var(--_a) right)
|
|
347
373
|
- (
|
|
@@ -450,7 +476,7 @@
|
|
|
450
476
|
outline: 0.1px solid green;
|
|
451
477
|
}
|
|
452
478
|
.gap-marker {
|
|
453
|
-
outline:
|
|
479
|
+
outline: 1px solid blue;
|
|
454
480
|
outline-offset: 0.5px;
|
|
455
481
|
} */
|
|
456
|
-
</style>
|
|
482
|
+
</style>
|
package/src/lib/Svedit.svelte
CHANGED
|
@@ -269,6 +269,10 @@
|
|
|
269
269
|
if (!canvas_el?.contains(range.commonAncestorContainer)) return;
|
|
270
270
|
let selection = __get_selection_from_dom();
|
|
271
271
|
if (selection) {
|
|
272
|
+
// Avoid assigning a new object reference when the selection is
|
|
273
|
+
// structurally identical — prevents a redundant $effect cycle
|
|
274
|
+
// (render_selection → scrollIntoView) on every DOM layout change.
|
|
275
|
+
if (JSON.stringify(selection) === JSON.stringify(session.selection)) return;
|
|
272
276
|
session.selection = selection;
|
|
273
277
|
}
|
|
274
278
|
}
|
|
@@ -764,6 +768,23 @@ ${fallback_html}`;
|
|
|
764
768
|
canvas_el?.focus();
|
|
765
769
|
}
|
|
766
770
|
|
|
771
|
+
/**
|
|
772
|
+
* When a DOM selection endpoint lands in a sibling NodeGap (not inside
|
|
773
|
+
* a node), resolves the adjacent node element for the walk-up algorithm.
|
|
774
|
+
* @param {HTMLElement} el
|
|
775
|
+
* @returns {HTMLElement | null}
|
|
776
|
+
*/
|
|
777
|
+
function __resolve_node_from_gap(el) {
|
|
778
|
+
const gap = /** @type {HTMLElement | null} */ (el.closest('[data-gap-array-path]'));
|
|
779
|
+
if (!gap) return null;
|
|
780
|
+
const array_path = gap.dataset.gapArrayPath;
|
|
781
|
+
const offset = parseInt(gap.dataset.gapOffset, 10);
|
|
782
|
+
const node_idx = offset > 0 ? offset - 1 : 0;
|
|
783
|
+
return /** @type {HTMLElement | null} */ (
|
|
784
|
+
canvas_el.querySelector(`[data-path="${array_path}.${node_idx}"][data-type="node"]`)
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
767
788
|
/**
|
|
768
789
|
* Extracts a NodeSelection from the current DOM selection.
|
|
769
790
|
*
|
|
@@ -783,56 +804,30 @@ ${fallback_html}`;
|
|
|
783
804
|
if (focus_node.nodeType !== Node.ELEMENT_NODE) focus_node = focus_node.parentElement;
|
|
784
805
|
if (anchor_node.nodeType !== Node.ELEMENT_NODE) anchor_node = anchor_node.parentElement;
|
|
785
806
|
|
|
786
|
-
// EDGE CASE:
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
796
|
-
const node_path = /** @type {DocumentPath} */ (node.dataset.path.split('.'));
|
|
797
|
-
const node_index = parseInt(String(node_path.at(-1)), 10);
|
|
798
|
-
|
|
799
|
-
return {
|
|
800
|
-
type: 'node',
|
|
801
|
-
path: node_path.slice(0, -1),
|
|
802
|
-
anchor_offset: node_index + 1,
|
|
803
|
-
focus_offset: node_index + 1
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// EDGE CASE: Let's check if we are in a gap-before for node_arrays.
|
|
808
|
-
let gap_before_el = focus_node.closest('[data-type="gap-before"]');
|
|
809
|
-
if (gap_before_el && focus_node === anchor_node) {
|
|
810
|
-
const node_array_el = /** @type {HTMLElement} */ (
|
|
811
|
-
gap_before_el.closest('[data-type="node_array"]')
|
|
812
|
-
);
|
|
813
|
-
const node_array_path = node_array_el.dataset.path.split('.');
|
|
807
|
+
// EDGE CASE: Collapsed selection inside a node gap (gap-after or gap-before).
|
|
808
|
+
// Gaps are siblings of nodes with data-gap-array-path and data-gap-offset.
|
|
809
|
+
const gap_el = /** @type {HTMLElement | null} */ (
|
|
810
|
+
focus_node.closest('[data-gap-array-path]')
|
|
811
|
+
);
|
|
812
|
+
if (gap_el && focus_node === anchor_node) {
|
|
813
|
+
const array_path = gap_el.dataset.gapArrayPath.split('.');
|
|
814
|
+
const gap_offset = parseInt(gap_el.dataset.gapOffset, 10);
|
|
814
815
|
return {
|
|
815
816
|
type: 'node',
|
|
816
|
-
path:
|
|
817
|
-
anchor_offset:
|
|
818
|
-
focus_offset:
|
|
817
|
+
path: array_path,
|
|
818
|
+
anchor_offset: gap_offset,
|
|
819
|
+
focus_offset: gap_offset
|
|
819
820
|
};
|
|
820
821
|
}
|
|
821
822
|
|
|
822
|
-
let focus_root =
|
|
823
|
-
focus_node.closest('[data-path][data-type="node"]')
|
|
824
|
-
);
|
|
823
|
+
let focus_root = __resolve_node_from_gap(focus_node)
|
|
824
|
+
?? /** @type {HTMLElement} */ (focus_node.closest('[data-path][data-type="node"]'));
|
|
825
825
|
if (!focus_root) return null;
|
|
826
826
|
|
|
827
|
-
let anchor_root =
|
|
828
|
-
anchor_node.closest('[data-path][data-type="node"]')
|
|
829
|
-
);
|
|
827
|
+
let anchor_root = __resolve_node_from_gap(anchor_node)
|
|
828
|
+
?? /** @type {HTMLElement} */ (anchor_node.closest('[data-path][data-type="node"]'));
|
|
830
829
|
if (!anchor_root) return null;
|
|
831
830
|
|
|
832
|
-
if (!(focus_root && anchor_root)) {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
831
|
let focus_root_path = focus_root.dataset.path.split('.');
|
|
837
832
|
let anchor_root_path = anchor_root.dataset.path.split('.');
|
|
838
833
|
let focus_node_depth = focus_root_path.length;
|
|
@@ -872,6 +867,10 @@ ${fallback_html}`;
|
|
|
872
867
|
}
|
|
873
868
|
}
|
|
874
869
|
|
|
870
|
+
// If both paths walked up to the root node, there's no common parent node_array
|
|
871
|
+
// (e.g. selecting between nav and page body). The resulting path would be empty.
|
|
872
|
+
if (anchor_root_path.length <= 1) return null;
|
|
873
|
+
|
|
875
874
|
let anchor_offset = parseInt(anchor_root_path.at(-1));
|
|
876
875
|
let focus_offset = parseInt(focus_root_path.at(-1));
|
|
877
876
|
|
|
@@ -1094,106 +1093,68 @@ ${fallback_html}`;
|
|
|
1094
1093
|
}
|
|
1095
1094
|
|
|
1096
1095
|
function __get_node_element(node_array_path, node_offset) {
|
|
1097
|
-
|
|
1098
|
-
`[data-path="${node_array_path}"][data-type="
|
|
1096
|
+
return canvas_el.querySelector(
|
|
1097
|
+
`[data-path="${node_array_path}.${node_offset}"][data-type="node"]`
|
|
1099
1098
|
);
|
|
1100
|
-
if (!node_array_el) return null;
|
|
1101
|
-
|
|
1102
|
-
const node_elements = node_array_el.children;
|
|
1103
|
-
if (node_elements.length === 0) return null;
|
|
1104
|
-
|
|
1105
|
-
return node_elements[node_offset];
|
|
1106
1099
|
}
|
|
1107
1100
|
|
|
1108
1101
|
function __render_node_selection() {
|
|
1109
1102
|
const selection = /** @type {NodeSelection} */ (session.selection);
|
|
1110
1103
|
const node_array_path = selection.path.join('.');
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
// We need to translate the cusor offset to node offsets now
|
|
1115
|
-
let anchor_node_offset, focus_node_offset;
|
|
1104
|
+
const is_collapsed = selection.anchor_offset === selection.focus_offset;
|
|
1105
|
+
const is_backward = !is_collapsed && selection.anchor_offset > selection.focus_offset;
|
|
1116
1106
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
anchor_node_offset = selection.anchor_offset - 1;
|
|
1122
|
-
focus_node_offset = selection.focus_offset;
|
|
1123
|
-
} else {
|
|
1124
|
-
anchor_node_offset = selection.anchor_offset;
|
|
1125
|
-
focus_node_offset = selection.focus_offset - 1;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const anchor_node = __get_node_element(node_array_path, anchor_node_offset);
|
|
1129
|
-
const focus_node = __get_node_element(node_array_path, focus_node_offset);
|
|
1107
|
+
const node_array_el = canvas_el.querySelector(
|
|
1108
|
+
`[data-path="${node_array_path}"][data-type="node_array"]`
|
|
1109
|
+
);
|
|
1110
|
+
if (!node_array_el) return;
|
|
1130
1111
|
|
|
1131
|
-
if (!anchor_node || !focus_node) return;
|
|
1132
1112
|
const dom_selection = window.getSelection();
|
|
1133
1113
|
const range = window.document.createRange();
|
|
1134
1114
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
// IMPORTANT: We need to look for direct children of anchor_node to find the right node gap.
|
|
1138
|
-
const gap_selector = selection.anchor_offset === 0
|
|
1139
|
-
? '.gap-before'
|
|
1140
|
-
: '.gap-after';
|
|
1141
|
-
// In empty arrays the node gap is a sibling of the placeholder, not a child
|
|
1142
|
-
const gap_el =
|
|
1143
|
-
anchor_node.querySelector(`:scope > ${gap_selector}`) ??
|
|
1144
|
-
anchor_node.parentElement?.querySelector(`:scope > ${gap_selector}`);
|
|
1115
|
+
const gap_selector = (offset) =>
|
|
1116
|
+
`[data-gap-array-path="${node_array_path}"][data-gap-offset="${offset}"]`;
|
|
1145
1117
|
|
|
1118
|
+
if (is_collapsed) {
|
|
1119
|
+
const gap_el = node_array_el.querySelector(gap_selector(selection.anchor_offset));
|
|
1146
1120
|
if (!gap_el) return;
|
|
1147
|
-
|
|
1148
|
-
|
|
1121
|
+
// Target .svedit-selectable (has a box), not gap_el which is
|
|
1122
|
+
// display:contents and would cause the browser to normalize
|
|
1123
|
+
// the range into the parent, breaking read-back.
|
|
1124
|
+
const selectable = gap_el.querySelector('.svedit-selectable');
|
|
1125
|
+
if (!selectable) return;
|
|
1126
|
+
range.setStart(selectable, 1);
|
|
1127
|
+
range.setEnd(selectable, 1);
|
|
1149
1128
|
dom_selection.removeAllRanges();
|
|
1150
1129
|
dom_selection.addRange(range);
|
|
1151
1130
|
} else {
|
|
1152
|
-
|
|
1131
|
+
const anchor_gap = node_array_el.querySelector(gap_selector(selection.anchor_offset));
|
|
1132
|
+
const focus_gap = node_array_el.querySelector(gap_selector(selection.focus_offset));
|
|
1133
|
+
if (!anchor_gap || !focus_gap) return;
|
|
1134
|
+
const anchor_sel = anchor_gap.querySelector('.svedit-selectable');
|
|
1135
|
+
const focus_sel = focus_gap.querySelector('.svedit-selectable');
|
|
1136
|
+
if (!anchor_sel || !focus_sel) return;
|
|
1137
|
+
|
|
1153
1138
|
if (is_backward) {
|
|
1154
|
-
// Use the last selectable you find
|
|
1155
|
-
const anchor_node_selectable = [...anchor_node.querySelectorAll('.svedit-selectable')].at(
|
|
1156
|
-
-1
|
|
1157
|
-
);
|
|
1158
|
-
// Use the first selectable you find
|
|
1159
|
-
const focus_node_selectable = [...focus_node.querySelectorAll('.svedit-selectable')].at(0);
|
|
1160
|
-
|
|
1161
|
-
range.setStart(focus_node_selectable, 1);
|
|
1162
|
-
range.setEnd(anchor_node_selectable, 1);
|
|
1163
1139
|
dom_selection.removeAllRanges();
|
|
1164
|
-
dom_selection.
|
|
1165
|
-
// Phew, this was a hard nut to crack. But with that code the direction can be reversed.
|
|
1166
|
-
dom_selection.setBaseAndExtent(anchor_node_selectable, 1, focus_node_selectable, 1);
|
|
1140
|
+
dom_selection.setBaseAndExtent(anchor_sel, 1, focus_sel, 1);
|
|
1167
1141
|
} else {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
0
|
|
1171
|
-
);
|
|
1172
|
-
// Use the last selectable you find
|
|
1173
|
-
const focus_node_selectable = [...focus_node.querySelectorAll('.svedit-selectable')].at(-1);
|
|
1174
|
-
|
|
1175
|
-
range.setStart(anchor_node_selectable, 1);
|
|
1176
|
-
range.setEnd(focus_node_selectable, 1);
|
|
1142
|
+
range.setStart(anchor_sel, 1);
|
|
1143
|
+
range.setEnd(focus_sel, 1);
|
|
1177
1144
|
dom_selection.removeAllRanges();
|
|
1178
1145
|
dom_selection.addRange(range);
|
|
1179
1146
|
}
|
|
1180
1147
|
}
|
|
1181
1148
|
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
// Scroll the selection into view
|
|
1149
|
+
node_array_el.focus();
|
|
1150
|
+
const scroll_node_offset = is_collapsed
|
|
1151
|
+
? Math.max(0, selection.anchor_offset - 1)
|
|
1152
|
+
: (is_backward ? selection.focus_offset : selection.anchor_offset);
|
|
1153
|
+
const scroll_node = __get_node_element(node_array_path, scroll_node_offset);
|
|
1154
|
+
if (scroll_node) {
|
|
1189
1155
|
setTimeout(() => {
|
|
1190
|
-
(
|
|
1191
|
-
block: 'nearest',
|
|
1192
|
-
inline: 'nearest'
|
|
1193
|
-
});
|
|
1156
|
+
scroll_node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1194
1157
|
}, 0);
|
|
1195
|
-
} else {
|
|
1196
|
-
console.log('No container element found!');
|
|
1197
1158
|
}
|
|
1198
1159
|
}
|
|
1199
1160
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*
|
|
27
27
|
* The viewport tracker drives **lazy positioning**: only near-viewport
|
|
28
28
|
* gaps receive CSS anchor positioning (via NodeGap's `positioned`
|
|
29
|
-
* prop). Off-viewport gaps remain as zero-size
|
|
29
|
+
* prop). Off-viewport gaps remain as zero-size absolute elements with
|
|
30
30
|
* no layout cost. Gap markers (NodeGapMarkers) are only computed for
|
|
31
31
|
* visible indices.
|
|
32
32
|
*
|