svedit 0.6.0 → 0.6.2

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.
@@ -16,8 +16,18 @@
16
16
  );
17
17
  });
18
18
 
19
+ let plain_text = $derived(svedit.session.get(path).text);
20
+ let is_empty = $derived(
21
+ get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)
22
+ );
23
+
24
+ let is_collapsed = $derived(
25
+ is_focused && svedit.session.selection?.anchor_offset == svedit.session.selection?.focus_offset
26
+ );
27
+
19
28
  // Get selection highlight range if not inside an annotation
20
29
  let selection_highlight_range = $derived.by(() => {
30
+ if (is_collapsed) return null;
21
31
  if (!is_focused) return null;
22
32
  if (svedit.session.active_annotation()) return null;
23
33
  const sel = svedit.session.selection;
@@ -25,6 +35,14 @@
25
35
  return get_selection_range(sel);
26
36
  });
27
37
 
38
+ let fragments = $derived(
39
+ get_fragments(
40
+ svedit.session.get(path).text,
41
+ svedit.session.get(path).annotations,
42
+ selection_highlight_range
43
+ )
44
+ );
45
+
28
46
  /**
29
47
  * Converts text with annotations into renderable fragments for display.
30
48
  * @param {string} text - The plain text content
@@ -78,18 +96,6 @@
78
96
 
79
97
  return fragments;
80
98
  }
81
-
82
- let fragments = $derived(
83
- get_fragments(
84
- svedit.session.get(path).text,
85
- svedit.session.get(path).annotations,
86
- selection_highlight_range
87
- )
88
- );
89
- let plain_text = $derived(svedit.session.get(path).text);
90
- let is_empty = $derived(
91
- get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)
92
- );
93
99
  </script>
94
100
 
95
101
  <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
@@ -1 +1 @@
1
- {"version":3,"file":"AnnotatedTextProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedTextProperty.svelte.js"],"names":[],"mappings":";;;;;AAuHA,2HAAoE"}
1
+ {"version":3,"file":"AnnotatedTextProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedTextProperty.svelte.js"],"names":[],"mappings":";;;;;AA6HA,2HAAoE"}
@@ -4,8 +4,7 @@
4
4
  snake_to_pascal,
5
5
  get_char_length,
6
6
  utf16_to_char_offset,
7
- char_to_utf16_offset,
8
- get_char_at
7
+ char_to_utf16_offset
9
8
  } from './utils.js';
10
9
 
11
10
  /** @import {
@@ -68,7 +67,9 @@
68
67
  const key_mapper = getContext('key_mapper');
69
68
 
70
69
  // Initialize commands and keymap on the session
71
- session.initialize_commands(context);
70
+ $effect(() => {
71
+ session.initialize_commands(context);
72
+ });
72
73
 
73
74
  // Handle focus - push session's keymap onto stack
74
75
  function handle_canvas_focus() {
@@ -949,36 +950,44 @@ ${fallback_html}`;
949
950
  if (!path) return null;
950
951
 
951
952
  // EDGE CASE 1B: Cursor after trailing <br> at end of text
953
+ //
954
+ // AnnotatedTextProperty renders a trailing <br> for non-empty or non-focused text.
955
+ // When the user places their cursor after this <br>, focusNode is the container
956
+ // element (not a text node), and normal processing would return position 0.
957
+ // We detect this and return text_length instead.
952
958
  const text_content = session.get(path).text;
953
959
  const text_length = get_char_length(text_content);
954
- if (text_length > 0) {
955
- const last_char = get_char_at(text_content, text_length - 1);
960
+ const child_nodes = focus_root.childNodes;
961
+
962
+ if (
963
+ focus_node === anchor_node &&
964
+ focus_node === focus_root &&
965
+ focus_root.dataset?.type === 'text' &&
966
+ !focus_root.classList.contains('empty')
967
+ ) {
968
+ // Find the last non-comment child node (comments are inserted by Svelte)
969
+ let last_element_index = child_nodes.length - 1;
970
+ while (last_element_index >= 0 && child_nodes[last_element_index].nodeType === Node.COMMENT_NODE) {
971
+ last_element_index--;
972
+ }
973
+
974
+ // Check if cursor is at or after the trailing <br>
956
975
  if (
957
- focus_node === anchor_node &&
958
- focus_node === focus_root &&
959
- focus_root.dataset?.type === 'text' &&
960
- !focus_root.classList.contains('empty') &&
961
- last_char === '\n'
976
+ last_element_index >= 0 &&
977
+ child_nodes[last_element_index].nodeName === 'BR' &&
978
+ focus_offset_in_node >= last_element_index
962
979
  ) {
963
- const child_nodes = focus_root.childNodes;
964
-
965
- if (
966
- child_nodes.length > 0 &&
967
- child_nodes[child_nodes.length - 1].nodeName === 'BR' &&
968
- focus_offset_in_node >= child_nodes.length - 2
969
- ) {
970
- return {
971
- type: 'text',
972
- path,
973
- anchor_offset: text_length,
974
- focus_offset: text_length
975
- };
976
- }
980
+ return {
981
+ type: 'text',
982
+ path,
983
+ anchor_offset: text_length,
984
+ focus_offset: text_length
985
+ };
977
986
  }
978
987
  }
979
988
 
980
- let anchor_offset = 0;
981
- let focus_offset = 0;
989
+ let start_offset = 0;
990
+ let end_offset = 0;
982
991
  let current_offset = 0;
983
992
 
984
993
  function processNode(node) {
@@ -988,12 +997,12 @@ ${fallback_html}`;
988
997
  if (node === range.startContainer) {
989
998
  // Convert UTF-16 offset to character offset
990
999
  const char_start_offset = utf16_to_char_offset(nodeText, range.startOffset);
991
- anchor_offset = current_offset + char_start_offset;
1000
+ start_offset = current_offset + char_start_offset;
992
1001
  }
993
1002
  if (node === range.endContainer) {
994
1003
  // Convert UTF-16 offset to character offset
995
1004
  const char_end_offset = utf16_to_char_offset(nodeText, range.endOffset);
996
- focus_offset = current_offset + char_end_offset;
1005
+ end_offset = current_offset + char_end_offset;
997
1006
  }
998
1007
  current_offset += nodeCharLength;
999
1008
  } else if (node.nodeType === Node.ELEMENT_NODE) {
@@ -1001,7 +1010,7 @@ ${fallback_html}`;
1001
1010
  processNode(childNode);
1002
1011
  }
1003
1012
  }
1004
- return focus_offset !== 0;
1013
+ return end_offset !== 0;
1005
1014
  }
1006
1015
 
1007
1016
  // Process nodes to find offsets
@@ -1012,21 +1021,17 @@ ${fallback_html}`;
1012
1021
  // Check if it's a backward selection
1013
1022
  // When range is provided, we can't detect backward selection from the range alone
1014
1023
  // since ranges are always normalized (start before end)
1015
- const is_backward = dom_selection
1016
- ? dom_selection.anchorNode === range.endContainer &&
1017
- dom_selection.anchorOffset === range.endOffset
1018
- : false;
1019
-
1020
- // Swap offsets if it's a backward selection
1021
- if (is_backward) {
1022
- [anchor_offset, focus_offset] = [focus_offset, anchor_offset];
1023
- }
1024
+ const is_backward = dom_selection ? __is_dom_selection_backwards() : false;
1025
+
1026
+ // Assign to anchor/focus based on direction
1027
+ const anchor_offset = is_backward ? end_offset : start_offset;
1028
+ const focus_offset = is_backward ? start_offset : end_offset;
1024
1029
 
1025
1030
  return {
1026
1031
  type: 'text',
1027
1032
  path,
1028
- anchor_offset: anchor_offset,
1029
- focus_offset: focus_offset
1033
+ anchor_offset,
1034
+ focus_offset
1030
1035
  };
1031
1036
  }
1032
1037
 
@@ -1163,8 +1168,6 @@ ${fallback_html}`;
1163
1168
  `[data-path="${selection.path.join('.')}"][data-type="text"]`
1164
1169
  );
1165
1170
  const empty_text = session.get(selection.path).text.length === 0;
1166
-
1167
- const range = window.document.createRange();
1168
1171
  const dom_selection = window.getSelection();
1169
1172
  let current_offset = 0;
1170
1173
  /** @type {HTMLElement | Text} */
@@ -1241,24 +1244,14 @@ ${fallback_html}`;
1241
1244
 
1242
1245
  // Set the range if both start and end were found
1243
1246
  if (anchor_node && focus_node) {
1244
- // Always set range in document order (start to end)
1245
- if (is_backward) {
1246
- range.setStart(focus_node, focus_node_offset);
1247
- range.setEnd(anchor_node, anchor_node_offset);
1248
- } else {
1249
- range.setStart(anchor_node, anchor_node_offset);
1250
- range.setEnd(focus_node, focus_node_offset);
1251
- }
1252
-
1253
1247
  dom_selection.removeAllRanges();
1254
- if (is_backward) {
1255
- // For backward selections, collapse to end and extend to start
1256
- range.collapse(false); // collapse to end
1257
- dom_selection.addRange(range);
1258
- dom_selection.extend(focus_node, focus_node_offset);
1259
- } else {
1260
- dom_selection.addRange(range);
1261
- }
1248
+ // NOTE: Only using setBaseAndExtent() will preserve selection direction
1249
+ dom_selection.setBaseAndExtent(
1250
+ anchor_node,
1251
+ anchor_node_offset,
1252
+ focus_node,
1253
+ focus_node_offset
1254
+ );
1262
1255
  el.focus(); // needed?
1263
1256
 
1264
1257
  // Scroll the selection into view
@@ -1323,6 +1316,7 @@ ${fallback_html}`;
1323
1316
  onfocus={handle_canvas_focus}
1324
1317
  onblur={handle_canvas_blur}
1325
1318
  contenteditable={editable ? 'true' : 'false'}
1319
+ tabindex="-1"
1326
1320
  {autocapitalize}
1327
1321
  {spellcheck}
1328
1322
  {...{
@@ -1 +1 @@
1
- {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AAszCA;;eAAqD;iCA9xC1C,cAAc"}
1
+ {"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AA+yCA;;eAAqD;iCAxxC1C,cAAc"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svedit",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -16,8 +16,18 @@
16
16
  );
17
17
  });
18
18
 
19
+ let plain_text = $derived(svedit.session.get(path).text);
20
+ let is_empty = $derived(
21
+ get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)
22
+ );
23
+
24
+ let is_collapsed = $derived(
25
+ is_focused && svedit.session.selection?.anchor_offset == svedit.session.selection?.focus_offset
26
+ );
27
+
19
28
  // Get selection highlight range if not inside an annotation
20
29
  let selection_highlight_range = $derived.by(() => {
30
+ if (is_collapsed) return null;
21
31
  if (!is_focused) return null;
22
32
  if (svedit.session.active_annotation()) return null;
23
33
  const sel = svedit.session.selection;
@@ -25,6 +35,14 @@
25
35
  return get_selection_range(sel);
26
36
  });
27
37
 
38
+ let fragments = $derived(
39
+ get_fragments(
40
+ svedit.session.get(path).text,
41
+ svedit.session.get(path).annotations,
42
+ selection_highlight_range
43
+ )
44
+ );
45
+
28
46
  /**
29
47
  * Converts text with annotations into renderable fragments for display.
30
48
  * @param {string} text - The plain text content
@@ -78,18 +96,6 @@
78
96
 
79
97
  return fragments;
80
98
  }
81
-
82
- let fragments = $derived(
83
- get_fragments(
84
- svedit.session.get(path).text,
85
- svedit.session.get(path).annotations,
86
- selection_highlight_range
87
- )
88
- );
89
- let plain_text = $derived(svedit.session.get(path).text);
90
- let is_empty = $derived(
91
- get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)
92
- );
93
99
  </script>
94
100
 
95
101
  <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
@@ -4,8 +4,7 @@
4
4
  snake_to_pascal,
5
5
  get_char_length,
6
6
  utf16_to_char_offset,
7
- char_to_utf16_offset,
8
- get_char_at
7
+ char_to_utf16_offset
9
8
  } from './utils.js';
10
9
 
11
10
  /** @import {
@@ -68,7 +67,9 @@
68
67
  const key_mapper = getContext('key_mapper');
69
68
 
70
69
  // Initialize commands and keymap on the session
71
- session.initialize_commands(context);
70
+ $effect(() => {
71
+ session.initialize_commands(context);
72
+ });
72
73
 
73
74
  // Handle focus - push session's keymap onto stack
74
75
  function handle_canvas_focus() {
@@ -949,36 +950,44 @@ ${fallback_html}`;
949
950
  if (!path) return null;
950
951
 
951
952
  // EDGE CASE 1B: Cursor after trailing <br> at end of text
953
+ //
954
+ // AnnotatedTextProperty renders a trailing <br> for non-empty or non-focused text.
955
+ // When the user places their cursor after this <br>, focusNode is the container
956
+ // element (not a text node), and normal processing would return position 0.
957
+ // We detect this and return text_length instead.
952
958
  const text_content = session.get(path).text;
953
959
  const text_length = get_char_length(text_content);
954
- if (text_length > 0) {
955
- const last_char = get_char_at(text_content, text_length - 1);
960
+ const child_nodes = focus_root.childNodes;
961
+
962
+ if (
963
+ focus_node === anchor_node &&
964
+ focus_node === focus_root &&
965
+ focus_root.dataset?.type === 'text' &&
966
+ !focus_root.classList.contains('empty')
967
+ ) {
968
+ // Find the last non-comment child node (comments are inserted by Svelte)
969
+ let last_element_index = child_nodes.length - 1;
970
+ while (last_element_index >= 0 && child_nodes[last_element_index].nodeType === Node.COMMENT_NODE) {
971
+ last_element_index--;
972
+ }
973
+
974
+ // Check if cursor is at or after the trailing <br>
956
975
  if (
957
- focus_node === anchor_node &&
958
- focus_node === focus_root &&
959
- focus_root.dataset?.type === 'text' &&
960
- !focus_root.classList.contains('empty') &&
961
- last_char === '\n'
976
+ last_element_index >= 0 &&
977
+ child_nodes[last_element_index].nodeName === 'BR' &&
978
+ focus_offset_in_node >= last_element_index
962
979
  ) {
963
- const child_nodes = focus_root.childNodes;
964
-
965
- if (
966
- child_nodes.length > 0 &&
967
- child_nodes[child_nodes.length - 1].nodeName === 'BR' &&
968
- focus_offset_in_node >= child_nodes.length - 2
969
- ) {
970
- return {
971
- type: 'text',
972
- path,
973
- anchor_offset: text_length,
974
- focus_offset: text_length
975
- };
976
- }
980
+ return {
981
+ type: 'text',
982
+ path,
983
+ anchor_offset: text_length,
984
+ focus_offset: text_length
985
+ };
977
986
  }
978
987
  }
979
988
 
980
- let anchor_offset = 0;
981
- let focus_offset = 0;
989
+ let start_offset = 0;
990
+ let end_offset = 0;
982
991
  let current_offset = 0;
983
992
 
984
993
  function processNode(node) {
@@ -988,12 +997,12 @@ ${fallback_html}`;
988
997
  if (node === range.startContainer) {
989
998
  // Convert UTF-16 offset to character offset
990
999
  const char_start_offset = utf16_to_char_offset(nodeText, range.startOffset);
991
- anchor_offset = current_offset + char_start_offset;
1000
+ start_offset = current_offset + char_start_offset;
992
1001
  }
993
1002
  if (node === range.endContainer) {
994
1003
  // Convert UTF-16 offset to character offset
995
1004
  const char_end_offset = utf16_to_char_offset(nodeText, range.endOffset);
996
- focus_offset = current_offset + char_end_offset;
1005
+ end_offset = current_offset + char_end_offset;
997
1006
  }
998
1007
  current_offset += nodeCharLength;
999
1008
  } else if (node.nodeType === Node.ELEMENT_NODE) {
@@ -1001,7 +1010,7 @@ ${fallback_html}`;
1001
1010
  processNode(childNode);
1002
1011
  }
1003
1012
  }
1004
- return focus_offset !== 0;
1013
+ return end_offset !== 0;
1005
1014
  }
1006
1015
 
1007
1016
  // Process nodes to find offsets
@@ -1012,21 +1021,17 @@ ${fallback_html}`;
1012
1021
  // Check if it's a backward selection
1013
1022
  // When range is provided, we can't detect backward selection from the range alone
1014
1023
  // since ranges are always normalized (start before end)
1015
- const is_backward = dom_selection
1016
- ? dom_selection.anchorNode === range.endContainer &&
1017
- dom_selection.anchorOffset === range.endOffset
1018
- : false;
1019
-
1020
- // Swap offsets if it's a backward selection
1021
- if (is_backward) {
1022
- [anchor_offset, focus_offset] = [focus_offset, anchor_offset];
1023
- }
1024
+ const is_backward = dom_selection ? __is_dom_selection_backwards() : false;
1025
+
1026
+ // Assign to anchor/focus based on direction
1027
+ const anchor_offset = is_backward ? end_offset : start_offset;
1028
+ const focus_offset = is_backward ? start_offset : end_offset;
1024
1029
 
1025
1030
  return {
1026
1031
  type: 'text',
1027
1032
  path,
1028
- anchor_offset: anchor_offset,
1029
- focus_offset: focus_offset
1033
+ anchor_offset,
1034
+ focus_offset
1030
1035
  };
1031
1036
  }
1032
1037
 
@@ -1163,8 +1168,6 @@ ${fallback_html}`;
1163
1168
  `[data-path="${selection.path.join('.')}"][data-type="text"]`
1164
1169
  );
1165
1170
  const empty_text = session.get(selection.path).text.length === 0;
1166
-
1167
- const range = window.document.createRange();
1168
1171
  const dom_selection = window.getSelection();
1169
1172
  let current_offset = 0;
1170
1173
  /** @type {HTMLElement | Text} */
@@ -1241,24 +1244,14 @@ ${fallback_html}`;
1241
1244
 
1242
1245
  // Set the range if both start and end were found
1243
1246
  if (anchor_node && focus_node) {
1244
- // Always set range in document order (start to end)
1245
- if (is_backward) {
1246
- range.setStart(focus_node, focus_node_offset);
1247
- range.setEnd(anchor_node, anchor_node_offset);
1248
- } else {
1249
- range.setStart(anchor_node, anchor_node_offset);
1250
- range.setEnd(focus_node, focus_node_offset);
1251
- }
1252
-
1253
1247
  dom_selection.removeAllRanges();
1254
- if (is_backward) {
1255
- // For backward selections, collapse to end and extend to start
1256
- range.collapse(false); // collapse to end
1257
- dom_selection.addRange(range);
1258
- dom_selection.extend(focus_node, focus_node_offset);
1259
- } else {
1260
- dom_selection.addRange(range);
1261
- }
1248
+ // NOTE: Only using setBaseAndExtent() will preserve selection direction
1249
+ dom_selection.setBaseAndExtent(
1250
+ anchor_node,
1251
+ anchor_node_offset,
1252
+ focus_node,
1253
+ focus_node_offset
1254
+ );
1262
1255
  el.focus(); // needed?
1263
1256
 
1264
1257
  // Scroll the selection into view
@@ -1323,6 +1316,7 @@ ${fallback_html}`;
1323
1316
  onfocus={handle_canvas_focus}
1324
1317
  onblur={handle_canvas_blur}
1325
1318
  contenteditable={editable ? 'true' : 'false'}
1319
+ tabindex="-1"
1326
1320
  {autocapitalize}
1327
1321
  {spellcheck}
1328
1322
  {...{