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
package/dist/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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AA60CA;;eAAqD;iCAhzC1C,cAAc"}
|
|
@@ -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
|
*
|
package/package.json
CHANGED
package/src/lib/Node.svelte
CHANGED
|
@@ -1,53 +1,32 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
-
import DefaultNodeGap from './NodeGap.svelte';
|
|
4
3
|
|
|
5
4
|
/** @import { NodeProps } from './types.d.ts'; */
|
|
6
5
|
|
|
7
6
|
const svedit = getContext('svedit');
|
|
8
|
-
let NodeGap = $derived(svedit.session.config.system_components?.NodeGap ?? DefaultNodeGap);
|
|
9
7
|
|
|
10
8
|
/** @type {NodeProps} */
|
|
11
9
|
let { path, children, tag = 'div', class: css_class, style = '', ...rest } = $props();
|
|
12
10
|
|
|
13
11
|
let node = $derived(svedit.session.get(path));
|
|
14
12
|
|
|
15
|
-
// NodeArrayProperty sets this context — its presence means we're in a node_array.
|
|
16
|
-
// Using context avoids expensive session.inspect() and session.get() calls
|
|
17
|
-
// that would create O(N) reactive subscriptions to the parent array.
|
|
18
13
|
const node_array_meta = getContext('node_array_meta');
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
let
|
|
22
|
-
let is_last_node_array_child = $derived(
|
|
23
|
-
is_inside_node_array && child_index === node_array_meta.length - 1
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
// Drives lazy anchor positioning for node gaps. Gaps are always in the
|
|
27
|
-
// DOM (stable structure for selection), but only anchor-positioned when
|
|
28
|
-
// near the viewport to avoid O(N) layout cost.
|
|
29
|
-
let is_near_viewport = $derived(
|
|
30
|
-
svedit.is_near_viewport?.(path) ?? true
|
|
31
|
-
);
|
|
14
|
+
let child_index = $derived(node_array_meta ? parseInt(String(path.at(-1)), 10) : -1);
|
|
15
|
+
let is_first = $derived(node_array_meta && child_index === 0);
|
|
16
|
+
let is_last = $derived(node_array_meta && child_index === node_array_meta.length - 1);
|
|
32
17
|
</script>
|
|
33
18
|
|
|
34
19
|
<svelte:element
|
|
35
20
|
id={node.id}
|
|
36
21
|
this={tag}
|
|
37
|
-
class={css_class}
|
|
22
|
+
class="{css_class}{is_first ? ' first' : ''}{is_last ? ' last' : ''}"
|
|
38
23
|
data-node-id={node.id}
|
|
39
24
|
data-path={path.join('.')}
|
|
40
25
|
data-type="node"
|
|
41
26
|
style="anchor-name: --{path.join('-')};{style}"
|
|
42
27
|
{...rest}
|
|
43
28
|
>
|
|
44
|
-
{#if svedit.editable && is_first_node_array_child}
|
|
45
|
-
<NodeGap {path} type="gap-before" positioned={is_near_viewport} />
|
|
46
|
-
{/if}
|
|
47
29
|
{@render children()}
|
|
48
|
-
{#if svedit.editable && is_inside_node_array}
|
|
49
|
-
<NodeGap {path} type="gap-after" last={is_last_node_array_child} positioned={is_near_viewport} />
|
|
50
|
-
{/if}
|
|
51
30
|
</svelte:element>
|
|
52
31
|
|
|
53
32
|
<style>
|
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
.map(/** @param {string} node_id */ (node_id) => svedit.session.get(node_id))
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
// Expose array metadata to child Node.svelte instances via context,
|
|
24
|
-
// avoiding expensive session.inspect() / session.get() calls per node.
|
|
25
23
|
setContext('node_array_meta', {
|
|
26
24
|
get length() { return nodes.length; }
|
|
27
25
|
});
|
|
@@ -52,9 +50,17 @@
|
|
|
52
50
|
<!-- Sibling (not child) of .empty-node-array so its .svedit-selectable
|
|
53
51
|
resolves anchor positioning against the shared containing block,
|
|
54
52
|
not the placeholder which inherits .node positioning styles. -->
|
|
55
|
-
<NodeGap
|
|
53
|
+
<NodeGap array_path={path} offset={0} count={0} empty />
|
|
56
54
|
{/if}
|
|
57
55
|
{#each nodes as node, index (index)}
|
|
56
|
+
{#if svedit.editable}
|
|
57
|
+
<NodeGap
|
|
58
|
+
array_path={path}
|
|
59
|
+
offset={index}
|
|
60
|
+
count={nodes.length}
|
|
61
|
+
positioned={svedit.is_near_viewport?.([...path, index]) ?? true}
|
|
62
|
+
/>
|
|
63
|
+
{/if}
|
|
58
64
|
{@const Component = svedit.session.config.node_components[snake_to_pascal(node.type)]}
|
|
59
65
|
{#if Component}
|
|
60
66
|
<Component path={[...path, index]} />
|
|
@@ -62,6 +68,14 @@
|
|
|
62
68
|
<UnknownNode path={[...path, index]} />
|
|
63
69
|
{/if}
|
|
64
70
|
{/each}
|
|
71
|
+
{#if svedit.editable && nodes.length > 0}
|
|
72
|
+
<NodeGap
|
|
73
|
+
array_path={path}
|
|
74
|
+
offset={nodes.length}
|
|
75
|
+
count={nodes.length}
|
|
76
|
+
positioned={svedit.is_near_viewport?.([...path, nodes.length - 1]) ?? true}
|
|
77
|
+
/>
|
|
78
|
+
{/if}
|
|
65
79
|
{#if svedit.editable && NodeGapMarkers}
|
|
66
80
|
<NodeGapMarkers {path} />
|
|
67
81
|
{/if}
|
package/src/lib/NodeGap.svelte
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Invisible keyboard caret selectable and gap hit area.
|
|
4
4
|
*
|
|
5
|
+
* Rendered as a sibling of Node elements inside NodeArrayProperty,
|
|
6
|
+
* interleaved between nodes in DOM order. This ensures:
|
|
7
|
+
* - Correct DOM order for native drag-to-select across nodes
|
|
8
|
+
* - No containing-block issues from transform/filter/will-change on nodes
|
|
9
|
+
*
|
|
5
10
|
* Always present in the DOM when editable (stable structure for
|
|
6
11
|
* selection anchoring, scrollTo, etc.). Anchor positioning is
|
|
7
12
|
* activated lazily via the `positioned` prop — only gaps near the
|
|
@@ -15,29 +20,37 @@
|
|
|
15
20
|
*
|
|
16
21
|
* Row/column detection uses var(--row, 1) with the * 99999 multiplier
|
|
17
22
|
* trick — no @container style() queries, works in all browsers.
|
|
18
|
-
*
|
|
19
|
-
* Node.svelte does NOT set position: relative, so the selectable's
|
|
20
|
-
* containing block is a higher ancestor that contains all sibling
|
|
21
|
-
* nodes, making cross-sibling anchor references valid.
|
|
22
23
|
*/
|
|
23
|
-
let {
|
|
24
|
+
let { array_path, offset, count, empty = false, positioned = true } = $props();
|
|
25
|
+
|
|
26
|
+
let is_first = $derived(offset === 0);
|
|
27
|
+
let is_last = $derived(offset === count);
|
|
28
|
+
let type = $derived(is_first ? 'gap-before' : 'gap-after');
|
|
24
29
|
|
|
25
30
|
let gap_style = $derived.by(() => {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
31
|
+
const arr = array_path.join('-');
|
|
32
|
+
const prev_idx = offset - 1;
|
|
33
|
+
const pa = is_first ? `--${arr}-0` : `--${arr}-${prev_idx}`;
|
|
34
|
+
const next = `--${arr}-${offset}`;
|
|
35
|
+
const container = `--${arr}`;
|
|
36
|
+
return `--_pa:${pa};--_next:${next};--_container:${container}`;
|
|
30
37
|
});
|
|
31
38
|
|
|
32
|
-
let anchor_name = $derived
|
|
39
|
+
let anchor_name = $derived.by(() => {
|
|
40
|
+
const arr = array_path.join('-');
|
|
41
|
+
if (is_first) return `--g-${arr}-0-gap-before`;
|
|
42
|
+
return `--g-${arr}-${offset - 1}-gap-after`;
|
|
43
|
+
});
|
|
33
44
|
</script>
|
|
34
45
|
|
|
35
46
|
<div
|
|
36
47
|
class="node-gap {type}"
|
|
37
48
|
class:empty
|
|
38
|
-
class:last
|
|
49
|
+
class:last={is_last}
|
|
39
50
|
class:positioned
|
|
40
51
|
data-type={type}
|
|
52
|
+
data-gap-array-path={array_path.join('.')}
|
|
53
|
+
data-gap-offset={offset}
|
|
41
54
|
style={gap_style}
|
|
42
55
|
>
|
|
43
56
|
<div class="svedit-selectable" style="anchor-name:{anchor_name}"><br /></div>
|
|
@@ -46,10 +59,10 @@
|
|
|
46
59
|
|
|
47
60
|
<style>
|
|
48
61
|
.node-gap {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
display: contents;
|
|
63
|
+
/* The native browser caret briefly appears inside .svedit-selectable for one frame
|
|
64
|
+
before the model-driven NodeCaret renders at the correct edge position.
|
|
65
|
+
Suppressing it here avoids a flash of the native caret. */
|
|
53
66
|
caret-color: transparent;
|
|
54
67
|
}
|
|
55
68
|
|
|
@@ -58,9 +71,7 @@
|
|
|
58
71
|
/* ------------------------------------------------------------------ */
|
|
59
72
|
|
|
60
73
|
.node-gap:not(.positioned) .svedit-selectable {
|
|
61
|
-
|
|
62
|
-
contain-intrinsic-size: 0 0;
|
|
63
|
-
position: static;
|
|
74
|
+
position: absolute;
|
|
64
75
|
pointer-events: none;
|
|
65
76
|
width: 0;
|
|
66
77
|
height: 0;
|
|
@@ -73,8 +84,21 @@
|
|
|
73
84
|
|
|
74
85
|
/*
|
|
75
86
|
* Anchor references resolved from CSS variable names passed via
|
|
76
|
-
* inline style (--_pa, --_next, --_container).
|
|
77
|
-
* NodeGapMarkers — keeps JS minimal and anchor() in CSS.
|
|
87
|
+
* inline style (--_pa = parent anchor, --_next, --_container).
|
|
88
|
+
* Same pattern as NodeGapMarkers — keeps JS minimal and anchor() in CSS.
|
|
89
|
+
*
|
|
90
|
+
* --_pa references the anchor-name of the node this gap belongs to,
|
|
91
|
+
* allowing us to position relative to that node's edges.
|
|
92
|
+
*
|
|
93
|
+
* Edge gaps extend outward and clamp to the containing block edge
|
|
94
|
+
* (0px floor). This can overlap neighboring elements when the node
|
|
95
|
+
* array isn't alone in its parent. anchor() only sees the border
|
|
96
|
+
* box, so it can't detect margin, gap, or parent padding around
|
|
97
|
+
* the container. --node-caret-boundary lets consumers set an
|
|
98
|
+
* explicit clamp target (a parent element's anchor-name) so edge
|
|
99
|
+
* gaps stop at that boundary instead. --node-caret-boundary-x and
|
|
100
|
+
* --node-caret-boundary-y override per-axis, falling back to
|
|
101
|
+
* --node-caret-boundary when unset.
|
|
78
102
|
*/
|
|
79
103
|
.node-gap.positioned {
|
|
80
104
|
--_s-t: anchor(var(--_pa) top);
|
|
@@ -82,6 +106,10 @@
|
|
|
82
106
|
--_s-l: anchor(var(--_pa) left);
|
|
83
107
|
--_s-r: anchor(var(--_pa) right);
|
|
84
108
|
}
|
|
109
|
+
.positioned.gap-before:not(.empty) {
|
|
110
|
+
--_b-t: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) top, 0px);
|
|
111
|
+
--_b-l: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) left, 0px);
|
|
112
|
+
}
|
|
85
113
|
.positioned.gap-after:not(.last) {
|
|
86
114
|
--_n-t: anchor(var(--_next) top);
|
|
87
115
|
--_n-l: anchor(var(--_next) left);
|
|
@@ -93,6 +121,10 @@
|
|
|
93
121
|
--_c-b: anchor(var(--_container) bottom);
|
|
94
122
|
--_c-l: anchor(var(--_container) left);
|
|
95
123
|
--_c-r: anchor(var(--_container) right);
|
|
124
|
+
--_b-b: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) bottom, 0px);
|
|
125
|
+
--_b-r: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) right, 0px);
|
|
126
|
+
--_b-bt: anchor(var(--node-caret-boundary-y, var(--node-caret-boundary, --_no-boundary)) bottom, 99999px);
|
|
127
|
+
--_b-rl: anchor(var(--node-caret-boundary-x, var(--node-caret-boundary, --_no-boundary)) right, 99999px);
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
.positioned .svedit-selectable {
|
|
@@ -171,28 +203,33 @@
|
|
|
171
203
|
);
|
|
172
204
|
min-height: calc(var(--_gm) * var(--_C));
|
|
173
205
|
min-width: calc(var(--_gm) * var(--_R));
|
|
206
|
+
/* min-height: max(calc(var(--_gm) * var(--_C)), calc(anchor-size(var(--_pa) height, 100%) * var(--_R)));
|
|
207
|
+
min-width: max(calc(var(--_gm) * var(--_R)), calc(anchor-size(var(--_pa) width, 100%) * var(--_C))); */
|
|
174
208
|
}
|
|
175
209
|
|
|
176
|
-
/* After last node: col extends down, row extends right
|
|
210
|
+
/* After last node: col extends down, row extends right.
|
|
211
|
+
top also clamps to boundary_bottom - eg so that min-height
|
|
212
|
+
(which wins over bottom in overconstrained abs-pos) cannot
|
|
213
|
+
push the element past the boundary. */
|
|
177
214
|
.positioned.gap-after.last .svedit-selectable {
|
|
178
215
|
top: min(
|
|
179
|
-
calc(var(--_s-b) + var(--_R) * 99999px),
|
|
216
|
+
calc(min(var(--_s-b), calc(var(--_b-bt) - var(--_eg))) + var(--_R) * 99999px),
|
|
180
217
|
calc(var(--_s-t) + var(--_C) * 99999px)
|
|
181
218
|
);
|
|
182
219
|
bottom: min(
|
|
183
|
-
calc(var(--_s-b) - var(--_eg) + var(--_R) * 99999px),
|
|
220
|
+
calc(max(var(--_b-b), var(--_s-b) - var(--_eg)) + var(--_R) * 99999px),
|
|
184
221
|
calc(var(--_s-b) + var(--_C) * 99999px)
|
|
185
222
|
);
|
|
186
223
|
left: min(
|
|
187
224
|
calc(var(--_s-l) + var(--_R) * 99999px),
|
|
188
|
-
calc(var(--_s-r) + var(--_C) * 99999px),
|
|
225
|
+
calc(min(var(--_s-r), calc(var(--_b-rl) - var(--_eg))) + var(--_C) * 99999px),
|
|
189
226
|
calc(100% - var(--_eg) + var(--_C) * 99999px)
|
|
190
227
|
);
|
|
191
228
|
right: min(
|
|
192
229
|
calc(var(--_s-r) + var(--_R) * 99999px),
|
|
193
230
|
calc(
|
|
194
231
|
max(
|
|
195
|
-
|
|
232
|
+
var(--_b-r),
|
|
196
233
|
min(
|
|
197
234
|
calc(var(--_c-r) - var(--_eg)),
|
|
198
235
|
calc(var(--_s-r) - var(--_eg))
|
|
@@ -209,7 +246,7 @@
|
|
|
209
246
|
.positioned.gap-before:not(.empty) .svedit-selectable {
|
|
210
247
|
top: min(
|
|
211
248
|
calc(
|
|
212
|
-
max(
|
|
249
|
+
max(var(--_b-t), var(--_s-t) - var(--_eg))
|
|
213
250
|
+ var(--_R) * 99999px
|
|
214
251
|
),
|
|
215
252
|
calc(var(--_s-t) + var(--_C) * 99999px)
|
|
@@ -221,7 +258,7 @@
|
|
|
221
258
|
left: min(
|
|
222
259
|
calc(var(--_s-l) + var(--_R) * 99999px),
|
|
223
260
|
calc(
|
|
224
|
-
max(
|
|
261
|
+
max(var(--_b-l), var(--_s-l) - var(--_eg))
|
|
225
262
|
+ var(--_C) * 99999px
|
|
226
263
|
)
|
|
227
264
|
);
|