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,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>
@@ -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,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>
@@ -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));