svedit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +476 -364
  2. package/dist/AnnotatedTextProperty.svelte +40 -10
  3. package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.d.ts +0 -1
  5. package/dist/Command.svelte.d.ts.map +1 -1
  6. package/dist/Command.svelte.js +48 -6
  7. package/dist/CustomProperty.svelte +20 -9
  8. package/dist/CustomProperty.svelte.d.ts +2 -1
  9. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  10. package/dist/Node.svelte +7 -5
  11. package/dist/Node.svelte.d.ts.map +1 -1
  12. package/dist/NodeArrayProperty.svelte +59 -48
  13. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  14. package/dist/NodeCaret.svelte +0 -7
  15. package/dist/NodeGap.svelte +208 -118
  16. package/dist/NodeGap.svelte.d.ts +0 -2
  17. package/dist/NodeGap.svelte.d.ts.map +1 -1
  18. package/dist/NodeGapMarkers.svelte +251 -84
  19. package/dist/NodeGapMarkers.svelte.d.ts +2 -0
  20. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  21. package/dist/NodeSelectionMarkers.svelte +4 -3
  22. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  23. package/dist/Session.svelte.d.ts +12 -1
  24. package/dist/Session.svelte.d.ts.map +1 -1
  25. package/dist/Session.svelte.js +40 -3
  26. package/dist/Svedit.svelte +496 -221
  27. package/dist/Svedit.svelte.d.ts.map +1 -1
  28. package/dist/Transaction.svelte.d.ts +24 -5
  29. package/dist/Transaction.svelte.d.ts.map +1 -1
  30. package/dist/Transaction.svelte.js +103 -61
  31. package/dist/doc_utils.d.ts +69 -3
  32. package/dist/doc_utils.d.ts.map +1 -1
  33. package/dist/doc_utils.js +276 -10
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +6 -1
  36. package/dist/node_visibility.svelte.d.ts +9 -0
  37. package/dist/node_visibility.svelte.d.ts.map +1 -0
  38. package/dist/node_visibility.svelte.js +718 -0
  39. package/dist/paste_utils.d.ts +46 -0
  40. package/dist/paste_utils.d.ts.map +1 -0
  41. package/dist/paste_utils.js +167 -0
  42. package/dist/styles/svedit-animations.css +8 -0
  43. package/dist/styles/svedit-colors.css +1 -0
  44. package/dist/types.d.ts +46 -3
  45. package/dist/utils.d.ts +52 -0
  46. package/dist/utils.d.ts.map +1 -1
  47. package/dist/utils.js +109 -3
  48. package/package.json +3 -2
  49. package/src/lib/AnnotatedTextProperty.svelte +40 -10
  50. package/src/lib/Command.svelte.js +48 -6
  51. package/src/lib/CustomProperty.svelte +20 -9
  52. package/src/lib/Node.svelte +7 -5
  53. package/src/lib/NodeArrayProperty.svelte +59 -48
  54. package/src/lib/NodeCaret.svelte +0 -7
  55. package/src/lib/NodeGap.svelte +208 -118
  56. package/src/lib/NodeGapMarkers.svelte +251 -84
  57. package/src/lib/NodeSelectionMarkers.svelte +4 -3
  58. package/src/lib/Session.svelte.js +40 -3
  59. package/src/lib/Svedit.svelte +496 -221
  60. package/src/lib/Transaction.svelte.js +103 -61
  61. package/src/lib/doc_utils.js +276 -10
  62. package/src/lib/index.js +6 -1
  63. package/src/lib/node_visibility.svelte.js +718 -0
  64. package/src/lib/paste_utils.js +167 -0
  65. package/src/lib/styles/svedit-animations.css +8 -0
  66. package/src/lib/styles/svedit-colors.css +1 -0
  67. package/src/lib/types.d.ts +46 -3
  68. package/src/lib/utils.js +109 -3
  69. package/dist/node_gap_computation.svelte.d.ts +0 -5
  70. package/dist/node_gap_computation.svelte.d.ts.map +0 -1
  71. package/dist/node_gap_computation.svelte.js +0 -621
  72. package/src/lib/node_gap_computation.svelte.js +0 -621
@@ -1,6 +1,12 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte';
3
- import { char_slice, get_char_length, snake_to_pascal, get_selection_range } from './utils.js';
3
+ import {
4
+ char_slice,
5
+ get_char_length,
6
+ paths_equal,
7
+ serialize_path,
8
+ get_selection_range
9
+ } from './utils.js';
4
10
 
5
11
  /** @import { AnnotatedTextPropertyProps, Annotation, Fragment, SelectionRange } from './types.d.ts'; */
6
12
 
@@ -9,10 +15,11 @@
9
15
  /** @type {AnnotatedTextPropertyProps} */
10
16
  let { path, class: css_class, placeholder = '', tag = 'div', style = '', ...rest } = $props();
11
17
 
18
+ let path_str = $derived(serialize_path(path));
19
+
12
20
  let is_focused = $derived.by(() => {
13
21
  return (
14
- svedit.session.selection?.type === 'text' &&
15
- path.join('.') === svedit.session.selection?.path.join('.')
22
+ svedit.session.selection?.type === 'text' && paths_equal(path, svedit.session.selection.path)
16
23
  );
17
24
  });
18
25
 
@@ -111,12 +118,11 @@
111
118
  <svelte:element
112
119
  this={tag}
113
120
  data-type="text"
114
- data-path={path.join('.')}
115
- style="anchor-name: --{path.join('-')};{style}"
121
+ data-path={path_str}
122
+ style="anchor-name: --{path_str};{style}"
116
123
  class="text svedit-selectable {css_class}"
117
124
  class:empty={is_empty}
118
125
  class:focused={is_focused}
119
- class:editable={svedit.editable}
120
126
  {placeholder}
121
127
  {...rest}
122
128
  >
@@ -125,8 +131,7 @@
125
131
  class="selection-highlight"
126
132
  style="anchor-name: --selection-highlight;">{fragment.content}</span
127
133
  >{:else if fragment.type === 'annotation'}
128
- {@const AnnotationComponent =
129
- svedit.session.config.node_components[snake_to_pascal(fragment.node.type)]}
134
+ {@const AnnotationComponent = svedit.session.config.node_components[fragment.node.type]}
130
135
  <AnnotationComponent
131
136
  path={[...path, 'annotations', fragment.annotation_index, 'node_id']}
132
137
  content={fragment.content}
@@ -137,11 +142,13 @@
137
142
  </svelte:element>
138
143
 
139
144
  <style>
140
- .text {
145
+ /* Editable text base layout; :where() allows easy override without specificity conflicts. */
146
+ :where(.text) {
141
147
  white-space: pre-wrap;
142
148
  overflow-wrap: anywhere;
143
149
  box-sizing: content-box;
144
150
  }
151
+
145
152
  /* We switch from ::before to ::after when the element is focused. So the the caret is always before the placeholder. */
146
153
  [placeholder].empty:not(.focused)::before,
147
154
  [placeholder].empty.focused::after {
@@ -150,13 +157,36 @@
150
157
  color: color-mix(in oklch, currentcolor 50%, transparent);
151
158
  }
152
159
 
160
+ /* A virtual caret: to fix the caret vertical alignment issue in Chrome and Firefox for empty focused contenteditable with placeholders */
161
+ /* Browser BUG: iOS Safari only considers the caret color set on the top contenteditable element, not on nested elements (e.g. the second selector doesn't work in iOS Safari) */
162
+ :global(.svedit.editable .svedit-canvas:has([placeholder].empty.focused)),
163
+ :global(.svedit.editable) [placeholder].empty.focused {
164
+ caret-color: transparent !important;
165
+ }
166
+ :global(.svedit.editable) [placeholder].empty.focused::before {
167
+ content: '';
168
+ /* we limit width & height to avoid layout shifts in case the text has a lower natural height */
169
+ width: 0px;
170
+ height: 1cap;
171
+ display: inline-block;
172
+ /* we use box-shadow to draw the caret shape, matching the native caret */
173
+ box-shadow:
174
+ 0 -0.4cap 0 0.65px var(--svedit-caret-color, AccentColor),
175
+ 0 0 0 0.65px var(--svedit-caret-color, AccentColor),
176
+ 0 0.4cap 0 0.65px var(--svedit-caret-color, AccentColor);
177
+ animation: var(
178
+ --node-caret-animation,
179
+ node-caret-blink var(--node-caret-blink-duration, 1.1s) ease-in-out infinite
180
+ );
181
+ }
182
+
153
183
  /* Hide flickering: in Chrome, the caret jumps from end of placeholder string to start of text property when we focus */
154
184
  .text:not(.focused) {
155
185
  caret-color: transparent;
156
186
  }
157
187
 
158
188
  /* Disable text-transform when editable and focused so users see original text */
159
- .text.editable.focused {
189
+ :global(.svedit.editable) .text.focused {
160
190
  text-transform: none !important;
161
191
  }
162
192
 
@@ -1 +1 @@
1
- {"version":3,"file":"AnnotatedTextProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedTextProperty.svelte.js"],"names":[],"mappings":";;;;;AAkIA,2HAAoE"}
1
+ {"version":3,"file":"AnnotatedTextProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedTextProperty.svelte.js"],"names":[],"mappings":";;;;;AAwIA,2HAAoE"}
@@ -68,7 +68,6 @@ export class RedoCommand extends Command {
68
68
  * Useful for navigating up the document hierarchy.
69
69
  */
70
70
  export class SelectParentCommand extends Command {
71
- is_enabled(): any;
72
71
  execute(): void;
73
72
  }
74
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Command.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Command.svelte.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAOC;;;;OAIG;IACH,qBAFW,GAAG,EAIb;IAbD;;;OAGG;IACH,kBAAwC;IAQvC,aAAsB;IAGvB;;;;;OAKG;IACH,cAFa,OAAO,CAInB;IAED;;;;;;OAMG;IACH,WAFa,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhC;CACD;AAED;;GAEG;AACH;IACC,kBAEC;IAED,gBAEC;CACD;AAED;;GAEG;AACH;IACC,kBAEC;IAED,gBAEC;CACD;AAED;;;GAGG;AACH;IACC,kBAEC;IAED,gBAEC;CACD;AAED;;;GAGG;AACH;IACC,0CAGC;IADA,eAA0B;IAG3B,YAAoC;IAEpC,iBAEC;IAED,kBAWC;IAED,gBAEC;CACD;AAED;;;;GAIG;AACH;IACC,kBAUC;IAED,gBAEC;CACD;AAED;;;;GAIG;AACH;IAKC,gBAKC;CACD;AAED;;;GAGG;AACH;IACC,kBAEC;IAED,gBAwGC;CACD;AAED;;;GAGG;AACH;IAUC,gBAIC;CACD"}
1
+ {"version":3,"file":"Command.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Command.svelte.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;IAOC;;;;OAIG;IACH,qBAFW,GAAG,EAIb;IAbD;;;OAGG;IACH,kBAAwC;IAQvC,aAAsB;IAGvB;;;;;OAKG;IACH,cAFa,OAAO,CAInB;IAED;;;;;;OAMG;IACH,WAFa,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhC;CACD;AAED;;GAEG;AACH;IACC,kBAEC;IAED,gBAEC;CACD;AAED;;GAEG;AACH;IACC,kBAEC;IAED,gBAEC;CACD;AAED;;;GAGG;AACH;IASC,gBAEC;CACD;AAED;;;GAGG;AACH;IACC,0CAGC;IADA,eAA0B;IAG3B,YAAoC;IAEpC,iBAEC;IAED,kBAqBC;IAED,gBAEC;CACD;AAED;;;;GAIG;AACH;IACC,kBAUC;IAED,gBAqBC;CACD;AAED;;;;GAIG;AACH;IAaC,gBAKC;CACD;AAED;;;GAGG;AACH;IACC,kBAEC;IAED,gBAwGC;CACD;AAED;;;GAGG;AACH;IAUC,gBAIC;CACD"}
@@ -1,5 +1,6 @@
1
1
  import { insert_default_node, break_text_node } from './transforms.svelte.js';
2
- import { is_selection_collapsed, is_mobile_browser, get_char_length } from './utils.js';
2
+ import { can_switch_annotation_type } from './doc_utils.js';
3
+ import { is_selection_collapsed, is_mobile_browser, get_char_length, char_slice } from './utils.js';
3
4
 
4
5
  /**
5
6
  * Base class for commands that can be executed in response to user actions
@@ -93,7 +94,11 @@ export class RedoCommand extends Command {
93
94
  */
94
95
  export class SelectParentCommand extends Command {
95
96
  is_enabled() {
96
- return this.context.editable && this.context.session.selection;
97
+ return (
98
+ this.context.editable &&
99
+ this.context.session.selection &&
100
+ this.context.session.selection.path.length > 3
101
+ );
97
102
  }
98
103
 
99
104
  execute() {
@@ -119,14 +124,24 @@ export class ToggleAnnotationCommand extends Command {
119
124
 
120
125
  is_enabled() {
121
126
  const { session, editable } = this.context;
127
+ const active_annotation = session.active_annotation();
122
128
  const has_annotation = session.active_annotation(this.node_type);
129
+ const can_switch_annotation =
130
+ active_annotation &&
131
+ !has_annotation &&
132
+ session.available_annotation_types.includes(this.node_type) &&
133
+ can_switch_annotation_type(
134
+ session.schema,
135
+ session.get(active_annotation.node_id),
136
+ this.node_type
137
+ );
123
138
  const no_annotation_and_caret_not_collapsed =
124
- !session.active_annotation() && !is_selection_collapsed(session.selection);
139
+ !active_annotation && !is_selection_collapsed(session.selection);
125
140
 
126
141
  return (
127
142
  editable &&
128
143
  session.selection?.type === 'text' &&
129
- (has_annotation || no_annotation_and_caret_not_collapsed)
144
+ (has_annotation || can_switch_annotation || no_annotation_and_caret_not_collapsed)
130
145
  );
131
146
  }
132
147
 
@@ -154,7 +169,26 @@ export class AddNewLineCommand extends Command {
154
169
  }
155
170
 
156
171
  execute() {
157
- this.context.session.apply(this.context.session.tr.insert_text('\n'));
172
+ const session = this.context.session;
173
+ const selection = session.selection;
174
+ if (!selection || selection.type !== 'text') return;
175
+ if (!session.inspect(selection.path).allow_newlines) return;
176
+
177
+ const tr = session.tr;
178
+ if (selection.anchor_offset !== selection.focus_offset) {
179
+ tr.delete_selection();
180
+ }
181
+
182
+ const collapsed_offset = tr.selection.anchor_offset;
183
+ const content = tr.get(tr.selection.path);
184
+ const text_before_caret = char_slice(content.text, 0, collapsed_offset);
185
+ const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
186
+ const current_line_prefix = text_before_caret.slice(line_start_index);
187
+ const indentation_match = current_line_prefix.match(/^[\t ]*/);
188
+ const indentation = indentation_match ? indentation_match[0] : '';
189
+
190
+ tr.insert_text(`\n${indentation}`);
191
+ session.apply(tr);
158
192
  }
159
193
  }
160
194
 
@@ -165,7 +199,15 @@ export class AddNewLineCommand extends Command {
165
199
  */
166
200
  export class BreakTextNodeCommand extends Command {
167
201
  is_enabled() {
168
- return this.context.editable && this.context.session.selection?.type === 'text';
202
+ const session = this.context.session;
203
+ const selection = session.selection;
204
+ if (!this.context.editable || selection?.type !== 'text') return false;
205
+
206
+ const owner_node = session.get(selection.path.slice(0, -1));
207
+ const owner_node_schema = owner_node ? session.schema[owner_node.type] : null;
208
+ if (!owner_node_schema || owner_node_schema.kind !== 'text') return false;
209
+
210
+ return session.inspect(selection.path.slice(0, -2))?.type === 'node_array';
169
211
  }
170
212
 
171
213
  execute() {
@@ -1,26 +1,25 @@
1
1
  <script>
2
- import { getContext } from 'svelte';
2
+ import { serialize_path } from './utils.js';
3
3
 
4
4
  /** @import { CustomPropertyProps } from './types.d.ts'; */
5
- const svedit = getContext('svedit');
6
5
 
7
6
  /** @type {CustomPropertyProps} */
8
7
  let { path, tag = 'div', class: css_class, children, style, ...rest } = $props();
8
+ let path_str = $derived(serialize_path(path));
9
+
9
10
  </script>
10
11
 
11
12
  <svelte:element
12
13
  this={tag}
13
14
  class={css_class}
14
15
  data-type="property"
15
- data-path={path.join('.')}
16
- style="anchor-name: --{path.join('-')};{style ? ` ${style}` : ''}"
16
+ data-path={path_str}
17
+ style="anchor-name: --{path_str};{style ? ` ${style}` : ''}"
17
18
  {...rest}
18
19
  >
19
- {#if svedit.editable}
20
- <div class="property-selectable">
21
- <div class="svedit-selectable"><br /></div>
22
- </div>
23
- {/if}
20
+ <div class="property-selectable">
21
+ <div class="svedit-selectable"><br /></div>
22
+ </div>
24
23
 
25
24
  {@render children()}
26
25
  </svelte:element>
@@ -37,5 +36,17 @@
37
36
  bottom: 0;
38
37
  z-index: 1;
39
38
  outline: none;
39
+ /* Position the hidden selectable element at the bottom so the
40
+ browser's native scroll-to-caret ensures the full property
41
+ is visible, not just the top edge. */
42
+ display: none;
43
+ align-items: flex-end;
44
+ justify-content: center;
45
+ }
46
+ :global(.svedit.editable) .property-selectable {
47
+ display: flex;
48
+ }
49
+ .svedit-selectable {
50
+ caret-color: transparent;
40
51
  }
41
52
  </style>
@@ -3,5 +3,6 @@ type CustomProperty = {
3
3
  $on?(type: string, callback: (e: any) => void): () => void;
4
4
  $set?(props: Partial<CustomPropertyProps>): void;
5
5
  };
6
- declare const CustomProperty: import("svelte").Component<import("./types.d.ts").CustomPropertyProps, {}, "">;
6
+ declare const CustomProperty: import("svelte").Component<CustomPropertyProps, {}, "">;
7
+ import type { CustomPropertyProps } from './types.d.ts';
7
8
  //# sourceMappingURL=CustomProperty.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AA6BA,6GAA6D"}
1
+ {"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AA4BA,sFAA6D;yCApBnB,cAAc"}
package/dist/Node.svelte CHANGED
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte';
3
+ import { serialize_path } from './utils.js';
3
4
 
4
5
  /** @import { NodeProps } from './types.d.ts'; */
5
6
 
@@ -9,6 +10,7 @@
9
10
  let { path, children, tag = 'div', class: css_class, style = '', ...rest } = $props();
10
11
 
11
12
  let node = $derived(svedit.session.get(path));
13
+ let path_str = $derived(serialize_path(path));
12
14
 
13
15
  const node_array_meta = getContext('node_array_meta');
14
16
  let child_index = $derived(node_array_meta ? parseInt(String(path.at(-1)), 10) : -1);
@@ -17,13 +19,13 @@
17
19
  </script>
18
20
 
19
21
  <svelte:element
20
- id={node.id}
21
22
  this={tag}
22
- class="{css_class}{is_first ? ' first' : ''}{is_last ? ' last' : ''}"
23
+ id={node.id}
24
+ class="node-{node.type} {css_class}{is_first ? ' first' : ''}{is_last ? ' last' : ''}"
23
25
  data-node-id={node.id}
24
- data-path={path.join('.')}
26
+ data-path={path_str}
25
27
  data-type="node"
26
- style="anchor-name: --{path.join('-')};{style}"
28
+ style="anchor-name: --{path_str};{style}"
27
29
  {...rest}
28
30
  >
29
31
  {@render children()}
@@ -35,4 +37,4 @@
35
37
  /* For developers who need to position their node with `position: absolute` or `position: relative`, they need to wrap their node in a div */
36
38
  position: static !important;
37
39
  }
38
- </style>
40
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AA8BA,yFAAmD"}
1
+ {"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AAkCA,yFAAmD"}
@@ -1,82 +1,93 @@
1
1
  <script>
2
2
  import { getContext, setContext } from 'svelte';
3
3
  import UnknownNode from './UnknownNode.svelte';
4
- import { snake_to_pascal } from './utils.js';
4
+ import { serialize_path } from './utils.js';
5
5
  import DefaultNodeGap from './NodeGap.svelte';
6
6
  import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
7
7
 
8
8
  /** @import { NodeArrayPropertyProps } from './types.d.ts'; */
9
9
 
10
10
  const svedit = getContext('svedit');
11
- let NodeGap = $derived(svedit.session.config.system_components?.NodeGap ?? DefaultNodeGap);
12
- let NodeGapMarkers = $derived(svedit.session.config.system_components?.NodeGapMarkers ?? DefaultNodeGapMarkers);
11
+ let NodeGap = $derived(svedit.session.config.system_components?.node_gap ?? DefaultNodeGap);
12
+ let NodeGapMarkers = $derived(
13
+ svedit.session.config.system_components?.node_gap_markers ?? DefaultNodeGapMarkers
14
+ );
13
15
 
14
16
  /** @type {NodeArrayPropertyProps} */
15
17
  let { path, tag = 'div', class: css_class, style = '', ...rest } = $props();
16
18
 
17
- let nodes = $derived(
18
- svedit.session
19
- .get(path)
20
- .map(/** @param {string} node_id */ (node_id) => svedit.session.get(node_id))
19
+ // Pre-joined once per path change; reused by data-path and by every
20
+ // NodeGap's should_position_gap call to avoid N+1 joins per render.
21
+ let path_str = $derived(serialize_path(path));
22
+
23
+ let node_ids = $derived(svedit.session.get(path));
24
+ let nodes = $derived(node_ids.map((id) => svedit.session.get(id)));
25
+
26
+ // Mirrors the AnnotatedTextProperty pattern: a `focused` class follows
27
+ // the model selection. Used by the CSS rule below to hide the empty-array
28
+ // NodeGap while the caret is in the placeholder, so its <br> isn't a
29
+ // second arrow-key stop (issue #260).
30
+ let is_focused = $derived(
31
+ svedit.session.selection?.type === 'node' &&
32
+ serialize_path(svedit.session.selection.path) === path_str
21
33
  );
22
34
 
23
35
  setContext('node_array_meta', {
24
- get length() { return nodes.length; }
36
+ get length() {
37
+ return node_ids.length;
38
+ }
25
39
  });
26
40
  </script>
41
+
27
42
  <!-- we use the anchor of node_array in Overlays.svelte to position the last insertion point in a horizontal layout based on the right edge of the container -->
28
- <svelte:element
29
- this={tag}
30
- class={css_class}
31
- data-type="node_array"
32
- data-path={path.join('.')}
33
- style="anchor-name: --{path.join('-')};{style ? ` ${style}` : ''}" {...rest}
43
+ <svelte:element
44
+ this={tag}
45
+ class={css_class}
46
+ class:focused={is_focused}
47
+ data-type="node_array"
48
+ data-path={path_str}
49
+ style="anchor-name: --{path_str};{style ? ` ${style}` : ''}"
50
+ {...rest}
34
51
  >
35
- {#if nodes.length === 0 && svedit.editable}
36
- <!--
37
- Experimental: We'll let .empty-node-array act like a node, so the existing
38
- code paths for selection mapping will work as expected.
39
-
40
- TODO: Need to figure out a way to make .empty-node-array customizable.
41
- -->
52
+ {#if node_ids.length === 0 && svedit.editable}
42
53
  <div
43
- class="node empty-node-array"
44
- data-path={[...path, 0].join('.')}
54
+ class="empty-node-placeholder"
55
+ data-path={serialize_path([...path, 0])}
45
56
  data-type="node"
46
- style="anchor-name: --{[...path, 0].join(
47
- '-'
48
- )}; min-height: 40px; min-width: 24px;"
49
- ></div>
50
- <!-- Sibling (not child) of .empty-node-array so its .svedit-selectable
51
- resolves anchor positioning against the shared containing block,
52
- not the placeholder which inherits .node positioning styles. -->
53
- <NodeGap array_path={path} offset={0} count={0} empty />
57
+ style="anchor-name: --{serialize_path([...path, 0])};"
58
+ >
59
+ <NodeGap array_path={path} offset={0} count={0} empty />
60
+ </div>
54
61
  {/if}
55
62
  {#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}
64
- {@const Component = svedit.session.config.node_components[snake_to_pascal(node.type)]}
63
+ <NodeGap array_path={path} offset={index} count={nodes.length} />
64
+ {@const Component = svedit.session.config.node_components[node.type]}
65
65
  {#if Component}
66
66
  <Component path={[...path, index]} />
67
67
  {:else}
68
68
  <UnknownNode path={[...path, index]} />
69
69
  {/if}
70
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
- />
71
+ {#if node_ids.length > 0}
72
+ <NodeGap array_path={path} offset={node_ids.length} count={node_ids.length} />
78
73
  {/if}
79
74
  {#if svedit.editable && NodeGapMarkers}
80
- <NodeGapMarkers {path} />
75
+ <NodeGapMarkers {path} count={node_ids.length} />
81
76
  {/if}
82
77
  </svelte:element>
78
+
79
+ <style>
80
+ /* position: relative makes this the containing block for the gap's
81
+ .svedit-selectable, which fills it via inset:0. You may override to
82
+ position: absolute (e.g. so an empty array doesn't occupy flow space)
83
+ — still a containing block; then also set position: relative on the
84
+ parent node-array container so this placeholder's inset:0 resolves
85
+ against it. */
86
+ :where(.empty-node-placeholder) {
87
+ position: relative;
88
+ inset: 0;
89
+ min-height: 40px;
90
+ min-width: 24px;
91
+ cursor: pointer;
92
+ }
93
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"NodeArrayProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeArrayProperty.svelte.js"],"names":[],"mappings":";;;;;AAkEA,mHAAgE"}
1
+ {"version":3,"file":"NodeArrayProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/NodeArrayProperty.svelte.js"],"names":[],"mappings":";;;;;AA8EA,mHAAgE"}
@@ -11,13 +11,6 @@
11
11
  <div class="caret" role="none"></div>
12
12
 
13
13
  <style>
14
- @keyframes node-caret-blink {
15
- 0%, 60% { opacity: 1; }
16
- 68% { opacity: 0; }
17
- 88% { opacity: 0; }
18
- 100% { opacity: 1; }
19
- }
20
-
21
14
  .caret {
22
15
  --_R: var(--row, 1);
23
16
  --_C: calc(1 - var(--row, 1));