@aquera/nile-elements 0.1.67-beta-1.5 → 0.1.67-beta-1.7

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 (223) hide show
  1. package/demo/index.html +24 -11
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +130 -58
  5. package/dist/nile-avatar/nile-avatar.test.cjs.js +1 -1
  6. package/dist/nile-avatar/nile-avatar.test.cjs.js.map +1 -1
  7. package/dist/nile-avatar/nile-avatar.test.esm.js +1 -1
  8. package/dist/nile-badge/index.cjs.js +1 -1
  9. package/dist/nile-badge/index.esm.js +1 -1
  10. package/dist/nile-badge/nile-badge.cjs.js +1 -1
  11. package/dist/nile-badge/nile-badge.cjs.js.map +1 -1
  12. package/dist/nile-badge/nile-badge.esm.js +1 -1
  13. package/dist/nile-badge/nile-badge.test.cjs.js +1 -1
  14. package/dist/nile-badge/nile-badge.test.cjs.js.map +1 -1
  15. package/dist/nile-badge/nile-badge.test.esm.js +1 -1
  16. package/dist/nile-button/index.cjs.js +1 -1
  17. package/dist/nile-button/index.esm.js +1 -1
  18. package/dist/nile-button/nile-button.cjs.js +1 -1
  19. package/dist/nile-button/nile-button.cjs.js.map +1 -1
  20. package/dist/nile-button/nile-button.esm.js +1 -1
  21. package/dist/nile-button/nile-button.test.cjs.js +1 -1
  22. package/dist/nile-button/nile-button.test.cjs.js.map +1 -1
  23. package/dist/nile-button/nile-button.test.esm.js +1 -1
  24. package/dist/nile-calendar/nile-calendar.test.cjs.js +1 -1
  25. package/dist/nile-calendar/nile-calendar.test.cjs.js.map +1 -1
  26. package/dist/nile-calendar/nile-calendar.test.esm.js +1 -1
  27. package/dist/nile-chip/nile-chip.test.cjs.js +1 -1
  28. package/dist/nile-chip/nile-chip.test.cjs.js.map +1 -1
  29. package/dist/nile-chip/nile-chip.test.esm.js +1 -1
  30. package/dist/nile-dialog/index.cjs.js +1 -1
  31. package/dist/nile-dialog/index.esm.js +1 -1
  32. package/dist/nile-dialog/nile-dialog.cjs.js +1 -1
  33. package/dist/nile-dialog/nile-dialog.cjs.js.map +1 -1
  34. package/dist/nile-dialog/nile-dialog.esm.js +2 -2
  35. package/dist/nile-dialog/nile-dialog.test.cjs.js +1 -1
  36. package/dist/nile-dialog/nile-dialog.test.cjs.js.map +1 -1
  37. package/dist/nile-dialog/nile-dialog.test.esm.js +1 -1
  38. package/dist/nile-drawer/index.cjs.js +1 -1
  39. package/dist/nile-drawer/index.esm.js +1 -1
  40. package/dist/nile-drawer/nile-drawer.cjs.js +1 -1
  41. package/dist/nile-drawer/nile-drawer.cjs.js.map +1 -1
  42. package/dist/nile-drawer/nile-drawer.esm.js +1 -1
  43. package/dist/nile-drawer/nile-drawer.test.cjs.js +1 -1
  44. package/dist/nile-drawer/nile-drawer.test.cjs.js.map +1 -1
  45. package/dist/nile-drawer/nile-drawer.test.esm.js +1 -1
  46. package/dist/nile-icon/icons/svg/format_clear.cjs.js +2 -0
  47. package/dist/nile-icon/icons/svg/format_clear.cjs.js.map +1 -0
  48. package/dist/nile-icon/icons/svg/format_clear.esm.js +1 -0
  49. package/dist/nile-icon/icons/svg/format_list_bulleted.cjs.js +2 -0
  50. package/dist/nile-icon/icons/svg/format_list_bulleted.cjs.js.map +1 -0
  51. package/dist/nile-icon/icons/svg/format_list_bulleted.esm.js +1 -0
  52. package/dist/nile-icon/icons/svg/format_list_numbered.cjs.js +2 -0
  53. package/dist/nile-icon/icons/svg/format_list_numbered.cjs.js.map +1 -0
  54. package/dist/nile-icon/icons/svg/format_list_numbered.esm.js +1 -0
  55. package/dist/nile-icon/icons/svg/index.cjs.js +1 -1
  56. package/dist/nile-icon/icons/svg/index.esm.js +1 -1
  57. package/dist/nile-icon/index.cjs.js +1 -1
  58. package/dist/nile-icon/index.cjs.js.map +1 -1
  59. package/dist/nile-icon/index.esm.js +2 -2
  60. package/dist/nile-icon/nile-icon.test.cjs.js +1 -1
  61. package/dist/nile-icon/nile-icon.test.cjs.js.map +1 -1
  62. package/dist/nile-icon/nile-icon.test.esm.js +1 -1
  63. package/dist/nile-icon-button/index.cjs.js +1 -1
  64. package/dist/nile-icon-button/index.esm.js +1 -1
  65. package/dist/nile-icon-button/nile-icon-button.cjs.js +1 -1
  66. package/dist/nile-icon-button/nile-icon-button.cjs.js.map +1 -1
  67. package/dist/nile-icon-button/nile-icon-button.esm.js +1 -1
  68. package/dist/nile-input/index.cjs.js +1 -1
  69. package/dist/nile-input/index.esm.js +1 -1
  70. package/dist/nile-input/nile-input.cjs.js +1 -1
  71. package/dist/nile-input/nile-input.cjs.js.map +1 -1
  72. package/dist/nile-input/nile-input.esm.js +1 -1
  73. package/dist/nile-input/nile-input.test.cjs.js +1 -1
  74. package/dist/nile-input/nile-input.test.cjs.js.map +1 -1
  75. package/dist/nile-input/nile-input.test.esm.js +1 -1
  76. package/dist/nile-menu-item/index.cjs.js +1 -1
  77. package/dist/nile-menu-item/index.esm.js +1 -1
  78. package/dist/nile-menu-item/nile-menu-item.cjs.js +1 -1
  79. package/dist/nile-menu-item/nile-menu-item.cjs.js.map +1 -1
  80. package/dist/nile-menu-item/nile-menu-item.esm.js +1 -1
  81. package/dist/nile-option/index.cjs.js +1 -1
  82. package/dist/nile-option/index.esm.js +1 -1
  83. package/dist/nile-option/nile-option.cjs.js +1 -1
  84. package/dist/nile-option/nile-option.cjs.js.map +1 -1
  85. package/dist/nile-option/nile-option.esm.js +1 -1
  86. package/dist/nile-rich-text-editor/index.cjs.js +1 -1
  87. package/dist/nile-rich-text-editor/index.esm.js +1 -1
  88. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
  89. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
  90. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
  91. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
  92. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +14 -13
  93. package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
  94. package/dist/nile-rich-text-editor/nile-rte-color.cjs.js +1 -1
  95. package/dist/nile-rich-text-editor/nile-rte-color.cjs.js.map +1 -1
  96. package/dist/nile-rich-text-editor/nile-rte-color.esm.js +54 -1
  97. package/dist/nile-rich-text-editor/nile-rte-link.cjs.js +2 -0
  98. package/dist/nile-rich-text-editor/nile-rte-link.cjs.js.map +1 -0
  99. package/dist/nile-rich-text-editor/nile-rte-link.esm.js +19 -0
  100. package/dist/nile-rich-text-editor/nile-rte-select.cjs.js +1 -1
  101. package/dist/nile-rich-text-editor/nile-rte-select.cjs.js.map +1 -1
  102. package/dist/nile-rich-text-editor/nile-rte-select.esm.js +39 -39
  103. package/dist/nile-rich-text-editor/utils.cjs.js +1 -1
  104. package/dist/nile-rich-text-editor/utils.cjs.js.map +1 -1
  105. package/dist/nile-rich-text-editor/utils.esm.js +1 -1
  106. package/dist/nile-select/index.cjs.js +1 -1
  107. package/dist/nile-select/index.esm.js +1 -1
  108. package/dist/nile-select/nile-select.cjs.js +1 -1
  109. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  110. package/dist/nile-select/nile-select.esm.js +1 -1
  111. package/dist/nile-select/nile-select.test.cjs.js +1 -1
  112. package/dist/nile-select/nile-select.test.cjs.js.map +1 -1
  113. package/dist/nile-select/nile-select.test.esm.js +1 -1
  114. package/dist/nile-tab/index.cjs.js +1 -1
  115. package/dist/nile-tab/index.esm.js +1 -1
  116. package/dist/nile-tab/nile-tab.cjs.js +1 -1
  117. package/dist/nile-tab/nile-tab.cjs.js.map +1 -1
  118. package/dist/nile-tab/nile-tab.esm.js +1 -1
  119. package/dist/nile-tab-group/index.cjs.js +1 -1
  120. package/dist/nile-tab-group/index.esm.js +1 -1
  121. package/dist/nile-tab-group/nile-tab-group.cjs.js +1 -1
  122. package/dist/nile-tab-group/nile-tab-group.cjs.js.map +1 -1
  123. package/dist/nile-tab-group/nile-tab-group.esm.js +1 -1
  124. package/dist/nile-tab-group/nile-tab-group.test.cjs.js +1 -1
  125. package/dist/nile-tab-group/nile-tab-group.test.cjs.js.map +1 -1
  126. package/dist/nile-tab-group/nile-tab-group.test.esm.js +1 -1
  127. package/dist/nile-tag/index.cjs.js +1 -1
  128. package/dist/nile-tag/index.esm.js +1 -1
  129. package/dist/nile-tag/nile-tag.cjs.js +1 -1
  130. package/dist/nile-tag/nile-tag.cjs.js.map +1 -1
  131. package/dist/nile-tag/nile-tag.esm.js +1 -1
  132. package/dist/nile-toast/index.cjs.js +1 -1
  133. package/dist/nile-toast/index.esm.js +1 -1
  134. package/dist/nile-toast/nile-toast.cjs.js +1 -1
  135. package/dist/nile-toast/nile-toast.cjs.js.map +1 -1
  136. package/dist/nile-toast/nile-toast.esm.js +1 -1
  137. package/dist/nile-tree/index.cjs.js +1 -1
  138. package/dist/nile-tree/index.esm.js +1 -1
  139. package/dist/nile-tree/nile-tree.cjs.js +1 -1
  140. package/dist/nile-tree/nile-tree.cjs.js.map +1 -1
  141. package/dist/nile-tree/nile-tree.esm.js +1 -1
  142. package/dist/nile-tree-item/index.cjs.js +1 -1
  143. package/dist/nile-tree-item/index.esm.js +1 -1
  144. package/dist/nile-tree-item/nile-tree-item.cjs.js +1 -1
  145. package/dist/nile-tree-item/nile-tree-item.cjs.js.map +1 -1
  146. package/dist/nile-tree-item/nile-tree-item.esm.js +1 -1
  147. package/dist/nile-virtual-select/index.cjs.js +1 -1
  148. package/dist/nile-virtual-select/index.esm.js +1 -1
  149. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +1 -1
  150. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  151. package/dist/nile-virtual-select/nile-virtual-select.esm.js +1 -1
  152. package/dist/src/nile-icon/icons/svg/format_clear.d.ts +5 -0
  153. package/dist/src/nile-icon/icons/svg/format_clear.js +5 -0
  154. package/dist/src/nile-icon/icons/svg/format_clear.js.map +1 -0
  155. package/dist/src/nile-icon/icons/svg/format_list_bulleted.d.ts +5 -0
  156. package/dist/src/nile-icon/icons/svg/format_list_bulleted.js +5 -0
  157. package/dist/src/nile-icon/icons/svg/format_list_bulleted.js.map +1 -0
  158. package/dist/src/nile-icon/icons/svg/format_list_numbered.d.ts +5 -0
  159. package/dist/src/nile-icon/icons/svg/format_list_numbered.js +5 -0
  160. package/dist/src/nile-icon/icons/svg/format_list_numbered.js.map +1 -0
  161. package/dist/src/nile-icon/icons/svg/index.d.ts +3 -0
  162. package/dist/src/nile-icon/icons/svg/index.js +3 -0
  163. package/dist/src/nile-icon/icons/svg/index.js.map +1 -1
  164. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +14 -13
  165. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
  166. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.d.ts +1 -0
  167. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +38 -90
  168. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
  169. package/dist/src/nile-rich-text-editor/nile-rte-color.d.ts +11 -3
  170. package/dist/src/nile-rich-text-editor/nile-rte-color.js +171 -6
  171. package/dist/src/nile-rich-text-editor/nile-rte-color.js.map +1 -1
  172. package/dist/src/nile-rich-text-editor/nile-rte-link.d.ts +19 -0
  173. package/dist/src/nile-rich-text-editor/nile-rte-link.js +172 -0
  174. package/dist/src/nile-rich-text-editor/nile-rte-link.js.map +1 -0
  175. package/dist/src/nile-rich-text-editor/nile-rte-select.js +62 -57
  176. package/dist/src/nile-rich-text-editor/nile-rte-select.js.map +1 -1
  177. package/dist/src/nile-rich-text-editor/rte-utils/content.d.ts +2 -0
  178. package/dist/src/nile-rich-text-editor/rte-utils/content.js +25 -0
  179. package/dist/src/nile-rich-text-editor/rte-utils/content.js.map +1 -0
  180. package/dist/src/nile-rich-text-editor/rte-utils/css.d.ts +1 -0
  181. package/dist/src/nile-rich-text-editor/rte-utils/css.js +9 -0
  182. package/dist/src/nile-rich-text-editor/rte-utils/css.js.map +1 -0
  183. package/dist/src/nile-rich-text-editor/rte-utils/dom.d.ts +2 -0
  184. package/dist/src/nile-rich-text-editor/rte-utils/dom.js +48 -0
  185. package/dist/src/nile-rich-text-editor/rte-utils/dom.js.map +1 -0
  186. package/dist/src/nile-rich-text-editor/rte-utils/formatting.d.ts +2 -0
  187. package/dist/src/nile-rich-text-editor/rte-utils/formatting.js +69 -0
  188. package/dist/src/nile-rich-text-editor/rte-utils/formatting.js.map +1 -0
  189. package/dist/src/nile-rich-text-editor/rte-utils/keys.d.ts +2 -0
  190. package/dist/src/nile-rich-text-editor/rte-utils/keys.js +38 -0
  191. package/dist/src/nile-rich-text-editor/rte-utils/keys.js.map +1 -0
  192. package/dist/src/nile-rich-text-editor/rte-utils/lists.d.ts +2 -0
  193. package/dist/src/nile-rich-text-editor/rte-utils/lists.js +28 -0
  194. package/dist/src/nile-rich-text-editor/rte-utils/lists.js.map +1 -0
  195. package/dist/src/nile-rich-text-editor/rte-utils/selection.d.ts +17 -0
  196. package/dist/src/nile-rich-text-editor/rte-utils/selection.js +39 -0
  197. package/dist/src/nile-rich-text-editor/rte-utils/selection.js.map +1 -0
  198. package/dist/src/nile-rich-text-editor/rte-utils/toolbar.d.ts +28 -0
  199. package/dist/src/nile-rich-text-editor/rte-utils/toolbar.js +161 -0
  200. package/dist/src/nile-rich-text-editor/rte-utils/toolbar.js.map +1 -0
  201. package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.d.ts +13 -0
  202. package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.js +119 -0
  203. package/dist/src/nile-rich-text-editor/rte-utils/toolbarState.js.map +1 -0
  204. package/dist/src/nile-rich-text-editor/rte-utils/vars.d.ts +1 -0
  205. package/dist/src/nile-rich-text-editor/rte-utils/vars.js +14 -0
  206. package/dist/src/nile-rich-text-editor/rte-utils/vars.js.map +1 -0
  207. package/dist/src/nile-rich-text-editor/sanatize.d.ts +1 -0
  208. package/dist/src/nile-rich-text-editor/sanatize.js +40 -0
  209. package/dist/src/nile-rich-text-editor/sanatize.js.map +1 -0
  210. package/dist/src/nile-rich-text-editor/utils.js.map +1 -1
  211. package/dist/tsconfig.tsbuildinfo +1 -1
  212. package/package.json +1 -1
  213. package/src/nile-icon/icons/svg/format_clear.ts +5 -0
  214. package/src/nile-icon/icons/svg/format_list_bulleted.ts +5 -0
  215. package/src/nile-icon/icons/svg/format_list_numbered.ts +5 -0
  216. package/src/nile-icon/icons/svg/index.ts +3 -0
  217. package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +14 -13
  218. package/src/nile-rich-text-editor/nile-rich-text-editor.ts +56 -90
  219. package/src/nile-rich-text-editor/nile-rte-color.ts +182 -6
  220. package/src/nile-rich-text-editor/nile-rte-link.ts +161 -0
  221. package/src/nile-rich-text-editor/nile-rte-select.ts +178 -173
  222. package/src/nile-rich-text-editor/utils.ts +342 -341
  223. package/vscode-html-custom-data.json +33 -3
@@ -1,189 +1,189 @@
1
1
  // src/nile-rich-text-editor/utils.ts
2
2
  export function closestBlock(node: Node | null, root: HTMLElement): HTMLElement | null {
3
- while (node && node !== root) {
4
- if (node instanceof HTMLElement) {
5
- const display = getComputedStyle(node).display;
6
- if (node.tagName.match(/^(P|DIV|H1|H2|H3|H4|H5|H6|LI)$/) || display === 'block' || display === 'list-item') {
7
- return node;
3
+ while (node && node !== root) {
4
+ if (node instanceof HTMLElement) {
5
+ const display = getComputedStyle(node).display;
6
+ if (node.tagName.match(/^(P|DIV|H1|H2|H3|H4|H5|H6|LI)$/) || display === 'block' || display === 'list-item') {
7
+ return node;
8
+ }
8
9
  }
10
+ node = node?.parentNode || null;
9
11
  }
10
- node = node?.parentNode || null;
12
+ return root;
11
13
  }
12
- return root;
13
- }
14
-
15
- export function nearestElement(n: Node | null): HTMLElement | null {
16
- while (n && !(n instanceof HTMLElement)) n = n.parentNode as Node | null;
17
- return n as HTMLElement | null;
18
- }
19
-
20
- export function rgbToHex(rgb: string): string {
21
- const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
22
- if (!m) return '#000000';
23
- const r = Number(m[1]).toString(16).padStart(2,'0');
24
- const g = Number(m[2]).toString(16).padStart(2,'0');
25
- const b = Number(m[3]).toString(16).padStart(2,'0');
26
- return `#${r}${g}${b}`;
27
- }
28
-
29
- export function unwrap(node: HTMLElement) {
30
- const p = node.parentNode; if (!p) return;
31
- while (node.firstChild) p.insertBefore(node.firstChild, node);
32
- p.removeChild(node);
33
- }
34
-
35
- export function surroundInline(range: Range, tag: string, attrs?: Record<string,string>) {
36
- const wrap = document.createElement(tag);
37
- if (attrs) Object.entries(attrs).forEach(([k,v]) => wrap.setAttribute(k, v));
38
- try { range.surroundContents(wrap); }
39
- catch {
40
- const frag = range.extractContents();
41
- wrap.appendChild(frag);
42
- range.insertNode(wrap);
14
+
15
+ export function nearestElement(n: Node | null): HTMLElement | null {
16
+ while (n && !(n instanceof HTMLElement)) n = n.parentNode as Node | null;
17
+ return n as HTMLElement | null;
43
18
  }
44
- }
45
-
46
- export function toggleInlineTag(root: HTMLElement, tag: 'strong'|'em'|'u'|'span', attrs?: Record<string,string>) {
47
- const sel = document.getSelection();
48
- if (!sel || sel.rangeCount === 0) return;
49
- const range = sel.getRangeAt(0);
50
-
51
- if (range.collapsed) {
52
- const elm = document.createElement(tag);
53
- if (attrs) Object.entries(attrs).forEach(([k,v]) => elm.setAttribute(k,v));
54
- elm.appendChild(document.createTextNode('\u200b'));
55
- range.insertNode(elm);
56
- const r = document.createRange();
57
- r.setStart(elm.firstChild!, 1);
58
- r.collapse(true);
59
- sel.removeAllRanges(); sel.addRange(r);
60
- return;
19
+
20
+ export function rgbToHex(rgb: string): string {
21
+ const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
22
+ if (!m) return '#000000';
23
+ const r = Number(m[1]).toString(16).padStart(2,'0');
24
+ const g = Number(m[2]).toString(16).padStart(2,'0');
25
+ const b = Number(m[3]).toString(16).padStart(2,'0');
26
+ return `#${r}${g}${b}`;
61
27
  }
62
-
63
- let n: Node | null = range.startContainer;
64
- let target: HTMLElement | null = null;
65
- while (n && n !== root) {
66
- if (n instanceof HTMLElement && n.tagName.toLowerCase() === tag) { target = n; break; }
67
- n = n.parentNode;
28
+
29
+ export function unwrap(node: HTMLElement) {
30
+ const p = node.parentNode; if (!p) return;
31
+ while (node.firstChild) p.insertBefore(node.firstChild, node);
32
+ p.removeChild(node);
68
33
  }
69
- if (target) unwrap(target); else surroundInline(range, tag, attrs);
70
- }
71
-
72
- export function setBlockTag(root: HTMLElement, tag: 'p'|'h1'|'h2'|'h3') {
73
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
74
- const range = sel.getRangeAt(0);
75
- const block = closestBlock(range.startContainer, root); if (!block) return;
76
- if (block.tagName.toLowerCase() === tag) return;
77
- const nb = document.createElement(tag);
78
- while (block.firstChild) nb.appendChild(block.firstChild);
79
- block.replaceWith(nb);
80
- const r = document.createRange(); r.selectNodeContents(nb); r.collapse(true);
81
- sel.removeAllRanges(); sel.addRange(r);
82
- }
83
-
84
- export function setAlignment(root: HTMLElement, align: 'left'|'center'|'right'|'justify') {
85
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
86
- const range = sel.getRangeAt(0);
87
- const block = closestBlock(range.startContainer, root); if (!block) return;
88
- block.style.textAlign = align === 'justify' ? 'justify' : align;
89
- }
90
-
91
- export function setFontFamily(root: HTMLElement, family: string) {
92
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
93
- const range = sel.getRangeAt(0);
94
- if (range.collapsed) {
95
- const span = document.createElement('span');
96
- span.style.fontFamily = family;
97
- span.appendChild(document.createTextNode('\u200b'));
98
- range.insertNode(span);
99
- const r = document.createRange(); r.setStart(span.firstChild!, 1); r.collapse(true);
34
+
35
+ export function surroundInline(range: Range, tag: string, attrs?: Record<string,string>) {
36
+ const wrap = document.createElement(tag);
37
+ if (attrs) Object.entries(attrs).forEach(([k,v]) => wrap.setAttribute(k, v));
38
+ try { range.surroundContents(wrap); }
39
+ catch {
40
+ const frag = range.extractContents();
41
+ wrap.appendChild(frag);
42
+ range.insertNode(wrap);
43
+ }
44
+ }
45
+
46
+ export function toggleInlineTag(root: HTMLElement, tag: 'strong'|'em'|'u'|'span', attrs?: Record<string,string>) {
47
+ const sel = document.getSelection();
48
+ if (!sel || sel.rangeCount === 0) return;
49
+ const range = sel.getRangeAt(0);
50
+
51
+ if (range.collapsed) {
52
+ const elm = document.createElement(tag);
53
+ if (attrs) Object.entries(attrs).forEach(([k,v]) => elm.setAttribute(k,v));
54
+ elm.appendChild(document.createTextNode('\u200b'));
55
+ range.insertNode(elm);
56
+ const r = document.createRange();
57
+ r.setStart(elm.firstChild!, 1);
58
+ r.collapse(true);
59
+ sel.removeAllRanges(); sel.addRange(r);
60
+ return;
61
+ }
62
+
63
+ let n: Node | null = range.startContainer;
64
+ let target: HTMLElement | null = null;
65
+ while (n && n !== root) {
66
+ if (n instanceof HTMLElement && n.tagName.toLowerCase() === tag) { target = n; break; }
67
+ n = n.parentNode;
68
+ }
69
+ if (target) unwrap(target); else surroundInline(range, tag, attrs);
70
+ }
71
+
72
+ export function setBlockTag(root: HTMLElement, tag: 'p'|'h1'|'h2'|'h3') {
73
+ const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
74
+ const range = sel.getRangeAt(0);
75
+ const block = closestBlock(range.startContainer, root); if (!block) return;
76
+ if (block.tagName.toLowerCase() === tag) return;
77
+ const nb = document.createElement(tag);
78
+ while (block.firstChild) nb.appendChild(block.firstChild);
79
+ block.replaceWith(nb);
80
+ const r = document.createRange(); r.selectNodeContents(nb); r.collapse(true);
100
81
  sel.removeAllRanges(); sel.addRange(r);
101
- return;
102
82
  }
103
- surroundInline(range, 'span', { style: `font-family:${family}` });
104
- }
105
-
83
+
84
+ export function setAlignment(root: HTMLElement, align: 'left'|'center'|'right'|'justify') {
85
+ const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
86
+ const range = sel.getRangeAt(0);
87
+ const block = closestBlock(range.startContainer, root); if (!block) return;
88
+ block.style.textAlign = align === 'justify' ? 'justify' : align;
89
+ }
90
+
91
+ export function setFontFamily(root: HTMLElement, family: string) {
92
+ const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
93
+ const range = sel.getRangeAt(0);
94
+ if (range.collapsed) {
95
+ const span = document.createElement('span');
96
+ span.style.fontFamily = family;
97
+ span.appendChild(document.createTextNode('\u200b'));
98
+ range.insertNode(span);
99
+ const r = document.createRange(); r.setStart(span.firstChild!, 1); r.collapse(true);
100
+ sel.removeAllRanges(); sel.addRange(r);
101
+ return;
102
+ }
103
+ surroundInline(range, 'span', { style: `font-family:${family}` });
104
+ }
106
105
 
106
+
107
107
  function enclosingStyledSpan(
108
- editor: HTMLElement,
109
- node: Node | null,
110
- dataAttr: 'data-rte-color' | 'data-rte-bg'
108
+ editor: HTMLElement,
109
+ node: Node | null,
110
+ dataAttr: 'data-rte-color' | 'data-rte-bg'
111
111
  ): HTMLSpanElement | null {
112
- while (node && node !== editor) {
113
- if (node instanceof HTMLSpanElement && node.hasAttribute(dataAttr)) {
114
- return node;
112
+ while (node && node !== editor) {
113
+ if (node instanceof HTMLSpanElement && node.hasAttribute(dataAttr)) {
114
+ return node;
115
+ }
116
+ node = node.parentNode;
115
117
  }
116
- node = node.parentNode;
117
- }
118
- return null;
118
+ return null;
119
119
  }
120
120
 
121
-
121
+
122
122
  function applyInlineStyle(
123
- editor: HTMLElement,
124
- cssProp: 'color' | 'backgroundColor',
125
- value: string,
126
- dataAttr: 'data-rte-color' | 'data-rte-bg'
123
+ editor: HTMLElement,
124
+ cssProp: 'color' | 'backgroundColor',
125
+ value: string,
126
+ dataAttr: 'data-rte-color' | 'data-rte-bg'
127
127
  ) {
128
- const sel = window.getSelection();
129
- if (!sel || sel.rangeCount === 0) return;
130
- const r0 = sel.getRangeAt(0);
131
- if (!editor.contains(r0.commonAncestorContainer)) return;
132
-
133
- const range = r0.cloneRange();
134
-
135
-
136
- if (range.collapsed) {
137
- const enclosing = enclosingStyledSpan(editor, range.startContainer, dataAttr);
138
- if (enclosing) {
139
- (enclosing.style as any)[cssProp] = value;
140
- mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
141
- return;
142
- }
128
+ const sel = window.getSelection();
129
+ if (!sel || sel.rangeCount === 0) return;
130
+ const r0 = sel.getRangeAt(0);
131
+ if (!editor.contains(r0.commonAncestorContainer)) return;
143
132
 
144
- const s = document.createElement('span');
145
- s.setAttribute(dataAttr, '1');
146
- (s.style as any)[cssProp] = value;
147
- s.appendChild(document.createTextNode('\u200B'));
148
- range.insertNode(s);
133
+ const range = r0.cloneRange();
149
134
 
135
+
136
+ if (range.collapsed) {
137
+ const enclosing = enclosingStyledSpan(editor, range.startContainer, dataAttr);
138
+ if (enclosing) {
139
+ (enclosing.style as any)[cssProp] = value;
140
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
141
+ return;
142
+ }
150
143
 
151
- const caret = document.createRange();
152
- caret.setStart(s.firstChild!, 1);
153
- caret.collapse(true);
154
- sel.removeAllRanges(); sel.addRange(caret);
144
+ const s = document.createElement('span');
145
+ s.setAttribute(dataAttr, '1');
146
+ (s.style as any)[cssProp] = value;
147
+ s.appendChild(document.createTextNode('\u200B'));
148
+ range.insertNode(s);
155
149
 
156
- mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
157
- return;
158
- }
150
+
151
+ const caret = document.createRange();
152
+ caret.setStart(s.firstChild!, 1);
153
+ caret.collapse(true);
154
+ sel.removeAllRanges(); sel.addRange(caret);
159
155
 
156
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
157
+ return;
158
+ }
160
159
 
161
- const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
162
- const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
163
- if (leftEdge && leftEdge === rightEdge) {
164
160
 
165
- if (rangeCoversWholeNode(range, leftEdge)) {
166
- (leftEdge.style as any)[cssProp] = value;
167
- } else {
161
+ const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
162
+ const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
163
+ if (leftEdge && leftEdge === rightEdge) {
168
164
 
169
- const mid = splitAndRecolorWithinSpan(
170
- leftEdge,
171
- range,
172
- dataAttr,
173
- cssProp,
174
- value
175
- );
176
-
177
-
178
- const sel = window.getSelection();
179
- const r = document.createRange();
180
- r.selectNodeContents(mid);
181
- sel?.removeAllRanges();
182
- sel?.addRange(r);
165
+ if (rangeCoversWholeNode(range, leftEdge)) {
166
+ (leftEdge.style as any)[cssProp] = value;
167
+ } else {
168
+
169
+ const mid = splitAndRecolorWithinSpan(
170
+ leftEdge,
171
+ range,
172
+ dataAttr,
173
+ cssProp,
174
+ value
175
+ );
176
+
177
+
178
+ const sel = window.getSelection();
179
+ const r = document.createRange();
180
+ r.selectNodeContents(mid);
181
+ sel?.removeAllRanges();
182
+ sel?.addRange(r);
183
+ }
184
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
185
+ return;
183
186
  }
184
- mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
185
- return;
186
- }
187
187
 
188
188
 
189
189
  const commonEl = (() => {
@@ -192,227 +192,228 @@ if (leftEdge && leftEdge === rightEdge) {
192
192
  return n as HTMLElement | null;
193
193
  })();
194
194
 
195
- const walker = document.createTreeWalker(
196
- commonEl || editor,
197
- NodeFilter.SHOW_TEXT,
198
- {
199
- acceptNode: (n) => {
200
- if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
201
- const nodeRange = document.createRange();
202
- nodeRange.selectNodeContents(n);
203
- const intersects =
204
- range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0 &&
205
- range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0;
206
- return intersects ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
195
+ const walker = document.createTreeWalker(
196
+ commonEl || editor,
197
+ NodeFilter.SHOW_TEXT,
198
+ {
199
+ acceptNode: (n) => {
200
+ if (!n.nodeValue || !n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
201
+ const nodeRange = document.createRange();
202
+ nodeRange.selectNodeContents(n);
203
+ const intersects =
204
+ range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0 &&
205
+ range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0;
206
+ return intersects ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
207
+ }
208
+ }
209
+ );
210
+
211
+ const toProcess: Text[] = [];
212
+ let t: Node | null;
213
+ while ((t = walker.nextNode())) toProcess.push(t as Text);
214
+
215
+ toProcess.forEach((text) => {
216
+
217
+ let start = 0, end = text.length;
218
+ if (text === range.startContainer) start = range.startOffset;
219
+ if (text === range.endContainer) end = range.endOffset;
220
+ if (start > 0) text = text.splitText(start);
221
+ if (end < text.length) text.splitText(end);
222
+
223
+ // If this slice already sits in a styled span → update it, don’t nest.
224
+ const existing = enclosingStyledSpan(editor, text, dataAttr);
225
+ if (existing) {
226
+ (existing.style as any)[cssProp] = value;
227
+ return;
207
228
  }
208
- }
209
- );
210
-
211
- const toProcess: Text[] = [];
212
- let t: Node | null;
213
- while ((t = walker.nextNode())) toProcess.push(t as Text);
214
-
215
- toProcess.forEach((text) => {
216
-
217
- let start = 0, end = text.length;
218
- if (text === range.startContainer) start = range.startOffset;
219
- if (text === range.endContainer) end = range.endOffset;
220
- if (start > 0) text = text.splitText(start);
221
- if (end < text.length) text.splitText(end);
222
-
223
- // If this slice already sits in a styled span → update it, don’t nest.
224
- const existing = enclosingStyledSpan(editor, text, dataAttr);
225
- if (existing) {
226
- (existing.style as any)[cssProp] = value;
227
- return;
228
- }
229
229
 
230
- // Create a new, single-purpose span
231
- const span = document.createElement('span');
232
- span.setAttribute(dataAttr, '1');
233
- (span.style as any)[cssProp] = value;
234
- const parent = text.parentElement!;
235
- parent.replaceChild(span, text);
236
- span.appendChild(text);
237
- });
230
+ // Create a new, single-purpose span
231
+ const span = document.createElement('span');
232
+ span.setAttribute(dataAttr, '1');
233
+ (span.style as any)[cssProp] = value;
234
+ const parent = text.parentElement!;
235
+ parent.replaceChild(span, text);
236
+ span.appendChild(text);
237
+ });
238
238
 
239
- mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
239
+ mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
240
240
 
241
- // restore selection
242
- sel.removeAllRanges(); sel.addRange(range);
241
+ // restore selection
242
+ sel.removeAllRanges(); sel.addRange(range);
243
243
  }
244
244
 
245
- // Is the range covering the entire node's contents?
245
+ // Is the range covering the entire node's contents?
246
246
  function rangeCoversWholeNode(range: Range, node: Node): boolean {
247
- const all = document.createRange();
248
- all.selectNodeContents(node);
249
- return (
250
- range.compareBoundaryPoints(Range.START_TO_START, all) <= 0 &&
251
- range.compareBoundaryPoints(Range.END_TO_END, all) >= 0
252
- );
247
+ const all = document.createRange();
248
+ all.selectNodeContents(node);
249
+ return (
250
+ range.compareBoundaryPoints(Range.START_TO_START, all) <= 0 &&
251
+ range.compareBoundaryPoints(Range.END_TO_END, all) >= 0
252
+ );
253
253
  }
254
254
 
255
255
  function hasRangeContent(r: Range): boolean {
256
- if (r.collapsed) return false;
257
- const text = r.cloneContents().textContent || '';
258
- return text.length > 0;
256
+ if (r.collapsed) return false;
257
+ const text = r.cloneContents().textContent || '';
258
+ return text.length > 0;
259
259
  }
260
260
 
261
261
  // Split one styled span into [left][middle][right]; recolor only middle
262
262
  function splitAndRecolorWithinSpan(
263
- span: HTMLSpanElement,
264
- range: Range,
265
- dataAttr: 'data-rte-color' | 'data-rte-bg',
266
- cssProp: 'color' | 'backgroundColor',
267
- newValue: string
263
+ span: HTMLSpanElement,
264
+ range: Range,
265
+ dataAttr: 'data-rte-color' | 'data-rte-bg',
266
+ cssProp: 'color' | 'backgroundColor',
267
+ newValue: string
268
268
  ): HTMLSpanElement {
269
- const oldValue = (span.style as any)[cssProp];
270
-
271
- const left = document.createRange();
272
- left.setStart(span, 0);
273
- left.setEnd(range.startContainer, range.startOffset);
274
-
275
- const right = document.createRange();
276
- right.setStart(range.endContainer, range.endOffset);
277
- right.setEnd(span, span.childNodes.length);
278
-
279
- // Build replacement fragment
280
- const frag = document.createDocumentFragment();
281
-
282
- // helper to make a styled clone shell
283
- const makeShell = (val: string) => {
284
- const s = document.createElement('span');
285
- s.setAttribute(dataAttr, '1');
286
- (s.style as any)[cssProp] = val;
287
- return s;
288
- };
289
-
290
- if (hasRangeContent(left)) {
291
- const sLeft = makeShell(oldValue);
292
- sLeft.appendChild(left.cloneContents());
293
- frag.appendChild(sLeft);
294
- }
269
+ const oldValue = (span.style as any)[cssProp];
270
+
271
+ const left = document.createRange();
272
+ left.setStart(span, 0);
273
+ left.setEnd(range.startContainer, range.startOffset);
274
+
275
+ const right = document.createRange();
276
+ right.setStart(range.endContainer, range.endOffset);
277
+ right.setEnd(span, span.childNodes.length);
278
+
279
+ // Build replacement fragment
280
+ const frag = document.createDocumentFragment();
281
+
282
+ // helper to make a styled clone shell
283
+ const makeShell = (val: string) => {
284
+ const s = document.createElement('span');
285
+ s.setAttribute(dataAttr, '1');
286
+ (s.style as any)[cssProp] = val;
287
+ return s;
288
+ };
289
+
290
+ if (hasRangeContent(left)) {
291
+ const sLeft = makeShell(oldValue);
292
+ sLeft.appendChild(left.cloneContents());
293
+ frag.appendChild(sLeft);
294
+ }
295
295
 
296
- const mid = makeShell(newValue);
297
- mid.appendChild(range.cloneContents());
298
- frag.appendChild(mid);
296
+ const mid = makeShell(newValue);
297
+ mid.appendChild(range.cloneContents());
298
+ frag.appendChild(mid);
299
299
 
300
- if (hasRangeContent(right)) {
301
- const sRight = makeShell(oldValue);
302
- sRight.appendChild(right.cloneContents());
303
- frag.appendChild(sRight);
304
- }
300
+ if (hasRangeContent(right)) {
301
+ const sRight = makeShell(oldValue);
302
+ sRight.appendChild(right.cloneContents());
303
+ frag.appendChild(sRight);
304
+ }
305
305
 
306
- // Replace original span
307
- span.replaceWith(frag);
308
- return mid; // return the middle span so caller can restore selection
306
+ // Replace original span
307
+ span.replaceWith(frag);
308
+ return mid; // return the middle span so caller can restore selection
309
309
  }
310
310
 
311
311
 
312
312
  function mergeAdjacentStyledSpans(
313
- root: HTMLElement,
314
- dataAttr: 'data-rte-color' | 'data-rte-bg',
315
- cssProp: 'color' | 'backgroundColor'
313
+ root: HTMLElement,
314
+ dataAttr: 'data-rte-color' | 'data-rte-bg',
315
+ cssProp: 'color' | 'backgroundColor'
316
316
  ) {
317
- const spans = Array.from(root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
317
+ const spans = Array.from(root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
318
318
 
319
- const valOf = (el: HTMLElement) => (el.style as any)[cssProp];
319
+ const valOf = (el: HTMLElement) => (el.style as any)[cssProp];
320
320
 
321
- spans.forEach((s) => {
321
+ spans.forEach((s) => {
322
+
323
+ const nested = Array.from(s.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
324
+ nested.forEach((child) => {
325
+ if (valOf(child) === valOf(s)) {
326
+ while (child.firstChild) s.insertBefore(child.firstChild, child);
327
+ child.remove();
328
+ }
329
+ });
330
+
331
+
332
+ const prev = s.previousSibling;
333
+ if (prev instanceof HTMLSpanElement &&
334
+ prev.hasAttribute(dataAttr) &&
335
+ valOf(prev) === valOf(s)) {
336
+ while (s.firstChild) prev.appendChild(s.firstChild);
337
+ s.remove();
338
+ return; // s is gone, next checks not needed
339
+ }
322
340
 
323
- const nested = Array.from(s.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`));
324
- nested.forEach((child) => {
325
- if (valOf(child) === valOf(s)) {
326
- while (child.firstChild) s.insertBefore(child.firstChild, child);
327
- child.remove();
341
+ // 3) Merge with next sibling if identical
342
+ const next = s.nextSibling;
343
+ if (next instanceof HTMLSpanElement &&
344
+ next.hasAttribute(dataAttr) &&
345
+ valOf(next) === valOf(s)) {
346
+ while (next.firstChild) s.appendChild(next.firstChild);
347
+ next.remove();
328
348
  }
329
349
  });
330
-
331
-
332
- const prev = s.previousSibling;
333
- if (prev instanceof HTMLSpanElement &&
334
- prev.hasAttribute(dataAttr) &&
335
- valOf(prev) === valOf(s)) {
336
- while (s.firstChild) prev.appendChild(s.firstChild);
337
- s.remove();
338
- return; // s is gone, next checks not needed
339
- }
340
-
341
- // 3) Merge with next sibling if identical
342
- const next = s.nextSibling;
343
- if (next instanceof HTMLSpanElement &&
344
- next.hasAttribute(dataAttr) &&
345
- valOf(next) === valOf(s)) {
346
- while (next.firstChild) s.appendChild(next.firstChild);
347
- next.remove();
348
- }
349
- });
350
350
  }
351
351
 
352
352
 
353
353
  export function setForeColor(editor: HTMLElement, hex: string) {
354
- applyInlineStyle(editor, 'color', hex, 'data-rte-color');
354
+ applyInlineStyle(editor, 'color', hex, 'data-rte-color');
355
355
  }
356
356
  export function setBackColor(editor: HTMLElement, hex: string) {
357
- applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
357
+ applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
358
358
  }
359
359
 
360
360
 
361
- export function toggleList(root: HTMLElement, kind: 'ul'|'ol') {
362
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
363
- const range = sel.getRangeAt(0);
364
- const block = closestBlock(range.startContainer, root); if (!block) return;
365
-
366
- let li = block.closest('li');
367
- if (li) {
368
- const list = li.closest('ul,ol') as HTMLElement | null;
369
- if (!list) return;
370
- if (list.tagName.toLowerCase() === kind) {
371
- const parent = list.parentElement!;
372
- const frag = document.createDocumentFragment();
373
- for (const child of Array.from(list.children)) {
374
- if (child.tagName.toLowerCase() === 'li') {
375
- const p = document.createElement('p');
376
- while (child.firstChild) p.appendChild(child.firstChild);
377
- frag.appendChild(p);
361
+ export function toggleList(root: HTMLElement, kind: 'ul'|'ol') {
362
+ const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
363
+ const range = sel.getRangeAt(0);
364
+ const block = closestBlock(range.startContainer, root); if (!block) return;
365
+
366
+ let li = block.closest('li');
367
+ if (li) {
368
+ const list = li.closest('ul,ol') as HTMLElement | null;
369
+ if (!list) return;
370
+ if (list.tagName.toLowerCase() === kind) {
371
+ const parent = list.parentElement!;
372
+ const frag = document.createDocumentFragment();
373
+ for (const child of Array.from(list.children)) {
374
+ if (child.tagName.toLowerCase() === 'li') {
375
+ const p = document.createElement('p');
376
+ while (child.firstChild) p.appendChild(child.firstChild);
377
+ frag.appendChild(p);
378
+ }
378
379
  }
380
+ parent.replaceChild(frag, list);
381
+ } else {
382
+ const newList = document.createElement(kind);
383
+ while (list.firstChild) newList.appendChild(list.firstChild);
384
+ list.replaceWith(newList);
379
385
  }
380
- parent.replaceChild(frag, list);
381
- } else {
382
- const newList = document.createElement(kind);
383
- while (list.firstChild) newList.appendChild(list.firstChild);
384
- list.replaceWith(newList);
386
+ return;
385
387
  }
386
- return;
388
+
389
+ const list = document.createElement(kind);
390
+ const item = document.createElement('li'); list.appendChild(item);
391
+ while (block.firstChild) item.appendChild(block.firstChild);
392
+ block.replaceWith(list);
387
393
  }
388
-
389
- const list = document.createElement(kind);
390
- const item = document.createElement('li'); list.appendChild(item);
391
- while (block.firstChild) item.appendChild(block.firstChild);
392
- block.replaceWith(list);
393
- }
394
-
395
- export function insertOrEditLink(root: HTMLElement, href?: string) {
396
- const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
397
- const range = sel.getRangeAt(0);
398
-
399
- let n: Node | null = range.startContainer;
400
- let a: HTMLAnchorElement | null = null;
401
- while (n && n !== root) { if (n instanceof HTMLAnchorElement) { a = n; break; } n = n.parentNode; }
402
-
403
- const url = href ?? (typeof window !== 'undefined' ? window.prompt('Enter URL', a?.href || 'https://') || '' : '');
404
- if (!url) return;
405
-
406
- if (a) { a.href = url; return; }
407
-
408
- if (range.collapsed) {
409
- const link = document.createElement('a'); link.href = url; link.textContent = url; range.insertNode(link);
410
- const r = document.createRange(); r.setStartAfter(link); r.collapse(true);
411
- sel.removeAllRanges(); sel.addRange(r);
412
- return;
394
+
395
+ export function insertOrEditLink(root: HTMLElement, href?: string) {
396
+ const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) return;
397
+ const range = sel.getRangeAt(0);
398
+
399
+ let n: Node | null = range.startContainer;
400
+ let a: HTMLAnchorElement | null = null;
401
+ while (n && n !== root) { if (n instanceof HTMLAnchorElement) { a = n; break; } n = n.parentNode; }
402
+
403
+ const url = href ?? (typeof window !== 'undefined' ? window.prompt('Enter URL', a?.href || 'https://') || '' : '');
404
+ if (!url) return;
405
+
406
+ if (a) { a.href = url; return; }
407
+
408
+ if (range.collapsed) {
409
+ const link = document.createElement('a'); link.href = url; link.textContent = url; range.insertNode(link);
410
+ const r = document.createRange(); r.setStartAfter(link); r.collapse(true);
411
+ sel.removeAllRanges(); sel.addRange(r);
412
+ return;
413
+ }
414
+
415
+ const link = document.createElement('a'); link.href = url;
416
+ try { range.surroundContents(link); }
417
+ catch { const frag = range.extractContents(); link.appendChild(frag); range.insertNode(link); }
413
418
  }
414
-
415
- const link = document.createElement('a'); link.href = url;
416
- try { range.surroundContents(link); }
417
- catch { const frag = range.extractContents(); link.appendChild(frag); range.insertNode(link); }
418
- }
419
+