svedit 0.2.0 → 0.3.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 CHANGED
@@ -321,6 +321,8 @@ Further things to consider:
321
321
 
322
322
  - If you make a sub-tree `contenteditable="false"`, be aware that you can't create a `contenteditable="true"` segment somewhere inside it. Svedit can only work reliably when there's one contenteditable="true" at root (it's set by `<Svedit`>)
323
323
  - `<AnnotatedString>` and `<CustomProperty>` must not be wrapped in `contenteditable="false"` to work properly.
324
+ - Never apply `position: relative` to the direct parent of `<AnnotatedTextProperty>`, it will cause a [weird Safari bug](https://bsky.app/profile/michaelaufreiter.com/post/3lxvdqyxc622s) to destroy the DOM.
325
+ - Never use an `<a>` tag inside a `contenteditable="true"` element, as it will cause unexpected behavior. Make it a `<div>` while editing, and an `<a>` in read-only mode.
324
326
 
325
327
  ## Full API docs?
326
328
 
@@ -1,7 +1,8 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte';
3
+ import { char_slice, get_char_length } from './util.js';
3
4
 
4
- /** @import { AnnotatedStringPropertyProps, AnnotationFragment } from './types.d.ts'; */
5
+ /** @import { AnnotatedStringPropertyProps, Annotation, AnnotationFragment } from './types.d.ts'; */
5
6
 
6
7
  const svedit = getContext('svedit');
7
8
 
@@ -19,7 +20,7 @@
19
20
  /**
20
21
  * Converts text with annotations into renderable fragments for display.
21
22
  * @param {string} text - The plain text content
22
- * @param {Array<[number, number, string, ...any]>} annotations - Array of annotations where each is [start_offset, end_offset, type, options?] (minimum 3 elements)
23
+ * @param {Array<Annotation>} annotations - Array of annotations where each is [start_offset, end_offset, type, options?] (minimum 3 elements)
23
24
  * @returns {Array<string|AnnotationFragment>} Array of fragments - strings for plain text, AnnotationFragment objects for annotated content
24
25
  */
25
26
  function render_annotated_string(text, annotations) {
@@ -27,16 +28,16 @@
27
28
  let last_index = 0;
28
29
 
29
30
  // Sort annotations by start_offset
30
- const sorted_annotations = $state.snapshot(annotations).sort((a, b) => a[0] - b[0]);
31
+ const sorted_annotations = [...annotations].sort((a, b) => a[0] - b[0]);
31
32
 
32
33
  for (let [index, annotation] of sorted_annotations.entries()) {
33
- // Add text before the annotation
34
+ // Add text before the annotation using character-aware slicing
34
35
  if (annotation[0] > last_index) {
35
- fragments.push(text.slice(last_index, annotation[0]));
36
+ fragments.push(char_slice(text, last_index, annotation[0]));
36
37
  }
37
38
 
38
- // Add the annotated string
39
- const annotated_content = text.slice(annotation[0], annotation[1]);
39
+ // Add the annotated string using character-aware slicing
40
+ const annotated_content = char_slice(text, annotation[0], annotation[1]);
40
41
  fragments.push({
41
42
  type: annotation[2],
42
43
  content: annotated_content,
@@ -47,9 +48,9 @@
47
48
  last_index = annotation[1];
48
49
  }
49
50
 
50
- // Add any remaining text after the last annotation
51
- if (last_index < text.length) {
52
- fragments.push(text.slice(last_index));
51
+ // Add any remaining text after the last annotation using character-aware slicing
52
+ if (last_index < get_char_length(text)) {
53
+ fragments.push(char_slice(text, last_index));
53
54
  }
54
55
 
55
56
  return fragments;
@@ -66,35 +67,37 @@
66
67
  }
67
68
  </script>
68
69
 
69
- <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
70
- <div
71
- data-type="text"
72
- data-path={path.join('.')}
73
- style="anchor-name: --{path.join('-')};"
74
- class="text svedit-selectable {css_class}"
75
- class:empty={plain_text.length === 0}
76
- class:focused={is_focused}
77
- placeholder={placeholder}
78
- >
79
- {#each fragments as fragment, index (index)}
80
- {#if typeof fragment === 'string'}<!--
81
- -->{fragment}<!--
82
- -->{:else if fragment.type === 'emphasis'}<!--
83
- --><em>{fragment.content}</em><!--
84
- -->{:else if fragment.type === 'strong'}<!--
85
- --><strong>{fragment.content}</strong><!--
86
- -->{:else if fragment.type === 'link'}<!--
87
- --><a
88
- onclick={handle_link_click}
89
- style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
90
- href={fragment.data.href}
91
- target={fragment.data.target || '_self'}>{fragment.content}</a><!--
92
- -->{:else}<!--
93
- -->{fragment.content}<!--
94
- -->{/if}
95
- {/each}<!--
96
- --><br>
97
- </div>
70
+ {#key plain_text}
71
+ <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
72
+ <div
73
+ data-type="text"
74
+ data-path={path.join('.')}
75
+ style="anchor-name: --{path.join('-')};"
76
+ class="text svedit-selectable {css_class}"
77
+ class:empty={get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)}
78
+ class:focused={is_focused}
79
+ placeholder={placeholder}
80
+ >
81
+ {#each fragments as fragment, index (index)}
82
+ {#if typeof fragment === 'string'}<!--
83
+ -->{fragment}<!--
84
+ -->{:else if fragment.type === 'emphasis'}<!--
85
+ --><em>{fragment.content}</em><!--
86
+ -->{:else if fragment.type === 'strong'}<!--
87
+ --><strong>{fragment.content}</strong><!--
88
+ -->{:else if fragment.type === 'link'}<!--
89
+ --><a
90
+ onclick={handle_link_click}
91
+ style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
92
+ href={fragment.data.href}
93
+ target={fragment.data.target || '_self'}>{fragment.content}</a><!--
94
+ -->{:else}<!--
95
+ -->{fragment.content}<!--
96
+ -->{/if}
97
+ {/each}<!--
98
+ --><br>
99
+ </div>
100
+ {/key}
98
101
 
99
102
  <style>
100
103
  .text {
@@ -1 +1 @@
1
- {"version":3,"file":"AnnotatedStringProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedStringProperty.svelte.js"],"names":[],"mappings":";;;;;AAoFA,+HAAsE"}
1
+ {"version":3,"file":"AnnotatedStringProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedStringProperty.svelte.js"],"names":[],"mappings":";;;;;AAwFA,+HAAsE"}
@@ -1,6 +1,10 @@
1
1
  <script>
2
+ import { getContext } from 'svelte';
3
+
2
4
  /** @import { CustomPropertyProps } from './types.d.ts'; */
3
5
 
6
+ const svedit = getContext('svedit');
7
+
4
8
  /** @type {CustomPropertyProps} */
5
9
  let {
6
10
  path,
@@ -9,19 +13,23 @@
9
13
  } = $props();
10
14
  </script>
11
15
 
12
- <!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
13
- <div
14
- class="property {css_class}"
15
- data-type="property"
16
- data-path={path.join('.')}
17
- style="anchor-name: --{path.join('-')};"
18
- >
19
- <div class="cursor-trap">
20
- <div class="svedit-selectable"><br/></div>
21
- </div>
16
+ {#if svedit.editable}
17
+ <!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
18
+ <div
19
+ class="property {css_class}"
20
+ data-type="property"
21
+ data-path={path.join('.')}
22
+ style="anchor-name: --{path.join('-')};"
23
+ >
24
+ <div class="cursor-trap">
25
+ <div class="svedit-selectable"><br/></div>
26
+ </div>
22
27
 
23
- <div class="property-content" contenteditable="false">{@render children()}</div>
24
- </div>
28
+ <div class="property-content" contenteditable="false">{@render children()}</div>
29
+ </div>
30
+ {:else}
31
+ {@render children()}
32
+ {/if}
25
33
 
26
34
  <style>
27
35
  .property {
@@ -3,6 +3,5 @@ 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<CustomPropertyProps, {}, "">;
7
- import type { CustomPropertyProps } from './types.d.ts';
6
+ declare const CustomProperty: import("svelte").Component<import("./types.d.ts").CustomPropertyProps, {}, "">;
8
7
  //# 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":";;;;;AA2BA,sFAA6D;yCAvBlB,cAAc"}
1
+ {"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AAqCA,6GAA6D"}
@@ -153,7 +153,7 @@ export default class Document {
153
153
  * @returns {Annotation|null}
154
154
  */
155
155
  active_annotation(annotation_type: string): Annotation | null;
156
- get_selected_plain_text(): any;
156
+ get_selected_plain_text(): string;
157
157
  get_selected_nodes(): any;
158
158
  move_node_cursor(direction: any): void;
159
159
  expand_node_selection(direction: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,uCAJ+E,CAAC,SAAlE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAC,IAAI,EAAE,uBAAuB,CAAA;CAAC,CAAC,CAAE,UACnE,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAQjC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CASzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAexB;AAuGD;IAgBE;;;;;;OAMG;IACH,oBANW,cAAc,kBACd,kBAAkB,0BAE1B;QAA4B,SAAS,GAA7B,SAAS;QACK,MAAM,GAApB,GAAG;KACb,EAmBA;IAxCD,oCAAoC;IACpC,WADW,SAAS,GAAG,SAAS,CACX;IACrB,YAAkB;IAClB,iBAAuB;IACvB,WAAiB;IACjB,eAAqB;IACrB,sBAA2B;IAG3B,kBAA6C;IAC7C,kBAAkE;IAGnE,mBAAmD;IAahD,uBAAoB;IAgBtB,mBAMC;IAGF,yBAgBC;IAGA,yBAWC;IAED;;;;OAIG;IACH,oBAHW,GAAG,QAKb;IAED;;;;OAIG;IACH,UAFa,WAAW,CASvB;IAED;;;;OAIG;IACH,mBAFW,WAAW,QAkBrB;IAED,aAWC;IAED,aAaC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAlBW,YAAY,GAAC,MAAM,GACjB,GAAG,CAuDf;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAuBzD;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,MAAM,GAAC,MAAM,CAQzB;IAED;;;;;;OAMG;IACH,mCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAoB3B;IAGD,+BAOC;IAED,0BAOC;IAED,uCAsBC;IAED,4CASC;IAED,sBAmCC;IAED;;;;MAaC;IAKD,8BA4BC;IAED;;;OAGG;IACH,WAFa,kBAAkB,CAI9B;IAID,qNAWC;IAGD,uCAkBC;IAGD;;;OAGG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CASpB;CACF;6CA1pBS,cAAc;mCAAd,cAAc;kCAAd,cAAc;uCAAd,cAAc;oCAAd,cAAc;+BAAd,cAAc;wBAdA,yBAAyB;kCAcvC,cAAc;gCAAd,cAAc;wCAAd,cAAc;4BAAd,cAAc"}
1
+ {"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,uCAJ+E,CAAC,SAAlE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAC,IAAI,EAAE,uBAAuB,CAAA;CAAC,CAAC,CAAE,UACnE,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAQjC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CASzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAexB;AAuGD;IAgBE;;;;;;OAMG;IACH,oBANW,cAAc,kBACd,kBAAkB,0BAE1B;QAA4B,SAAS,GAA7B,SAAS;QACK,MAAM,GAApB,GAAG;KACb,EAmBA;IAxCD,oCAAoC;IACpC,WADW,SAAS,GAAG,SAAS,CACX;IACrB,YAAkB;IAClB,iBAAuB;IACvB,WAAiB;IACjB,eAAqB;IACrB,sBAA2B;IAG3B,kBAA6C;IAC7C,kBAAkE;IAGnE,mBAAmD;IAahD,uBAAoB;IAgBtB,mBAMC;IAGF,yBAkBC;IAGA,yBAWC;IAED;;;;OAIG;IACH,oBAHW,GAAG,QAKb;IAED;;;;OAIG;IACH,UAFa,WAAW,CASvB;IAED;;;;OAIG;IACH,mBAFW,WAAW,QAkBrB;IAED,aAWC;IAED,aAaC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAlBW,YAAY,GAAC,MAAM,GACjB,GAAG,CAuDf;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAuBzD;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,MAAM,GAAC,MAAM,CAQzB;IAED;;;;;;OAMG;IACH,mCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAoB3B;IAGD,kCAOC;IAED,0BAOC;IAED,uCAsBC;IAED,4CASC;IAED,sBAmCC;IAED;;;;MAaC;IAKD,8BA4BC;IAED;;;OAGG;IACH,WAFa,kBAAkB,CAI9B;IAID,qNAWC;IAGD,uCAkBC;IAGD;;;OAGG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CASpB;CACF;6CA5pBS,cAAc;mCAAd,cAAc;kCAAd,cAAc;uCAAd,cAAc;oCAAd,cAAc;+BAAd,cAAc;wBAfA,yBAAyB;kCAevC,cAAc;gCAAd,cAAc;wCAAd,cAAc;4BAAd,cAAc"}
@@ -1,4 +1,5 @@
1
1
  import Transaction from './Transaction.svelte.js';
2
+ import { char_slice } from './util.js';
2
3
 
3
4
  /**
4
5
  * @import {
@@ -239,7 +240,9 @@ export default class Document {
239
240
  return node_id ? this.get(node_id) : null;
240
241
  } else {
241
242
  // we are assuming we are either in a text or property (=custom) selection
242
- const owner_node = this.get(this.selection.path.slice(0, -1));
243
+ const node_id = this.selection?.path?.slice(0, -1);
244
+ if (!node_id) return null;
245
+ const owner_node = this.get(node_id);
243
246
  return owner_node;
244
247
  }
245
248
  }
@@ -489,7 +492,7 @@ export default class Document {
489
492
  const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
490
493
  const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
491
494
  const annotated_string = this.get(this.selection.path);
492
- return annotated_string[0].slice(start, end);
495
+ return char_slice(annotated_string[0], start, end);
493
496
  }
494
497
 
495
498
  get_selected_nodes() {
package/dist/Node.svelte CHANGED
@@ -18,21 +18,25 @@
18
18
  let is_first_node_array_child = $derived(is_inside_node_array && parseInt(String(path.at(-1)), 10) === 0);
19
19
  </script>
20
20
 
21
- <div
22
- class="node"
23
- data-node-id={node.id}
24
- data-path={path.join('.')}
25
- data-type="node"
26
- style="anchor-name: --{path.join('-')};"
27
- >
28
- {#if is_first_node_array_child}
29
- <NodeCursorTrap {path} type="position-zero-cursor-trap" />
30
- {/if}
21
+ {#if svedit.editable}
22
+ <div
23
+ class="node"
24
+ data-node-id={node.id}
25
+ data-path={path.join('.')}
26
+ data-type="node"
27
+ style="anchor-name: --{path.join('-')};"
28
+ >
29
+ {#if is_first_node_array_child}
30
+ <NodeCursorTrap {path} type="position-zero-cursor-trap" />
31
+ {/if}
32
+ {@render children()}
33
+ {#if is_inside_node_array}
34
+ <NodeCursorTrap {path} type="after-node-cursor-trap" />
35
+ {/if}
36
+ </div>
37
+ {:else}
31
38
  {@render children()}
32
- {#if is_inside_node_array}
33
- <NodeCursorTrap {path} type="after-node-cursor-trap" />
34
- {/if}
35
- </div>
39
+ {/if}
36
40
 
37
41
  <style>
38
42
  div {
@@ -1 +1 @@
1
- {"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AAuCA,yFAAmD"}
1
+ {"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AA2CA,yFAAmD"}
@@ -18,7 +18,7 @@
18
18
  </script>
19
19
 
20
20
  <div class={css_class} data-type="node_array" data-path={path.join('.')}>
21
- {#if nodes.length === 0}
21
+ {#if nodes.length === 0 && svedit.editable}
22
22
  <!--
23
23
  Experimental: We'll let .empty-node-array act like a node, so the existing
24
24
  code paths for selection mapping will work as expected.