kempo-ui 0.0.43 → 0.0.63

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 (194) hide show
  1. package/.github/copilot-instructions.md +11 -0
  2. package/.github/workflows/publish.yml +66 -0
  3. package/CONTRIBUTING.md +70 -0
  4. package/dist/components/Dialog.js +9 -8
  5. package/dist/components/HtmlEditor.js +91 -0
  6. package/dist/components/Icon.js +2 -2
  7. package/dist/components/Main.js +5 -9
  8. package/dist/components/ShadowComponent.js +1 -1
  9. package/dist/components/SideMenu.js +1 -1
  10. package/dist/components/SidePanel.js +46 -35
  11. package/dist/components/htmlEditorControls/AlignCenter.js +12 -0
  12. package/dist/components/htmlEditorControls/AlignJustify.js +12 -0
  13. package/dist/components/htmlEditorControls/AlignLeft.js +12 -0
  14. package/dist/components/htmlEditorControls/AlignRight.js +12 -0
  15. package/dist/components/htmlEditorControls/Bold.js +12 -0
  16. package/dist/components/htmlEditorControls/BulletList.js +12 -0
  17. package/dist/components/htmlEditorControls/CharacterCount.js +18 -0
  18. package/dist/components/htmlEditorControls/ClearFormatting.js +12 -0
  19. package/dist/components/htmlEditorControls/CodeBlock.js +12 -0
  20. package/dist/components/htmlEditorControls/ControlGroup.js +14 -0
  21. package/dist/components/htmlEditorControls/ControlSpacer.js +6 -0
  22. package/dist/components/htmlEditorControls/CreateLink.js +15 -0
  23. package/dist/components/htmlEditorControls/DropdownControl.js +57 -0
  24. package/dist/components/htmlEditorControls/FormatBlock.js +12 -0
  25. package/dist/components/htmlEditorControls/HtmlEditorControl.js +44 -0
  26. package/dist/components/htmlEditorControls/InlineCode.js +12 -0
  27. package/dist/components/htmlEditorControls/InsertTable.js +15 -0
  28. package/dist/components/htmlEditorControls/Italic.js +12 -0
  29. package/dist/components/htmlEditorControls/Mode.js +9 -0
  30. package/dist/components/htmlEditorControls/NumberList.js +12 -0
  31. package/dist/components/htmlEditorControls/Strikethrough.js +12 -0
  32. package/dist/components/htmlEditorControls/TextBackgroundColor.js +97 -0
  33. package/dist/components/htmlEditorControls/TextColor.js +97 -0
  34. package/dist/components/htmlEditorControls/Underline.js +12 -0
  35. package/dist/components/htmlEditorControls/WordCount.js +18 -0
  36. package/dist/utils/formatCode.js +1 -0
  37. package/docs/components/dropdown.html +2 -2
  38. package/docs/components/html-editor.html +442 -0
  39. package/docs/components/icon.html +2 -7
  40. package/docs/components/photo-viewer.html +14 -14
  41. package/docs/components/side-menu.html +32 -0
  42. package/docs/components/side-panel-dual.html +70 -0
  43. package/docs/components/side-panel-scroll.html +176 -0
  44. package/docs/components/side-panel.html +41 -1
  45. package/docs/icons/arrow_drop_down.svg +1 -0
  46. package/docs/icons/circle.svg +1 -0
  47. package/docs/icons/code.svg +1 -0
  48. package/docs/icons/code_blocks.svg +1 -0
  49. package/docs/icons/copy.svg +1 -0
  50. package/docs/icons/dot.svg +1 -0
  51. package/docs/icons/format_align_center.svg +1 -0
  52. package/docs/icons/format_align_justify.svg +1 -0
  53. package/docs/icons/format_align_left.svg +1 -0
  54. package/docs/icons/format_align_right.svg +1 -0
  55. package/docs/icons/format_bold.svg +1 -0
  56. package/docs/icons/format_clear.svg +1 -0
  57. package/docs/icons/format_color_fill.svg +1 -0
  58. package/docs/icons/format_color_text.svg +1 -0
  59. package/docs/icons/format_h1.svg +1 -0
  60. package/docs/icons/format_h2.svg +1 -0
  61. package/docs/icons/format_h3.svg +1 -0
  62. package/docs/icons/format_h4.svg +1 -0
  63. package/docs/icons/format_h5.svg +1 -0
  64. package/docs/icons/format_h6.svg +1 -0
  65. package/docs/icons/format_italic.svg +1 -0
  66. package/docs/icons/format_list_bulleted.svg +1 -0
  67. package/docs/icons/format_list_numbered.svg +1 -0
  68. package/docs/icons/format_paragraph.svg +1 -0
  69. package/docs/icons/format_underlined.svg +1 -0
  70. package/docs/icons/help.svg +1 -0
  71. package/docs/icons/link.svg +1 -0
  72. package/docs/icons/more_vert.svg +1 -0
  73. package/docs/icons/strikethrough_s.svg +1 -0
  74. package/docs/icons/table.svg +1 -0
  75. package/docs/icons/wysiwyg.svg +1 -0
  76. package/docs/index.html +6 -0
  77. package/docs/nav-1.inc.html +1 -0
  78. package/docs/nav.inc.html +1 -0
  79. package/docs/src/components/Dialog.js +9 -8
  80. package/docs/src/components/HtmlEditor.js +91 -0
  81. package/docs/src/components/Icon.js +2 -2
  82. package/docs/src/components/Main.js +5 -9
  83. package/docs/src/components/ShadowComponent.js +1 -1
  84. package/docs/src/components/SideMenu.js +1 -1
  85. package/docs/src/components/SidePanel.js +46 -35
  86. package/docs/src/components/htmlEditorControls/AlignCenter.js +12 -0
  87. package/docs/src/components/htmlEditorControls/AlignJustify.js +12 -0
  88. package/docs/src/components/htmlEditorControls/AlignLeft.js +12 -0
  89. package/docs/src/components/htmlEditorControls/AlignRight.js +12 -0
  90. package/docs/src/components/htmlEditorControls/Bold.js +12 -0
  91. package/docs/src/components/htmlEditorControls/BulletList.js +12 -0
  92. package/docs/src/components/htmlEditorControls/CharacterCount.js +18 -0
  93. package/docs/src/components/htmlEditorControls/ClearFormatting.js +12 -0
  94. package/docs/src/components/htmlEditorControls/CodeBlock.js +12 -0
  95. package/docs/src/components/htmlEditorControls/ControlGroup.js +14 -0
  96. package/docs/src/components/htmlEditorControls/ControlSpacer.js +6 -0
  97. package/docs/src/components/htmlEditorControls/CreateLink.js +15 -0
  98. package/docs/src/components/htmlEditorControls/DropdownControl.js +57 -0
  99. package/docs/src/components/htmlEditorControls/FormatBlock.js +12 -0
  100. package/docs/src/components/htmlEditorControls/HtmlEditorControl.js +44 -0
  101. package/docs/src/components/htmlEditorControls/InlineCode.js +12 -0
  102. package/docs/src/components/htmlEditorControls/InsertTable.js +15 -0
  103. package/docs/src/components/htmlEditorControls/Italic.js +12 -0
  104. package/docs/src/components/htmlEditorControls/Mode.js +9 -0
  105. package/docs/src/components/htmlEditorControls/NumberList.js +12 -0
  106. package/docs/src/components/htmlEditorControls/Strikethrough.js +12 -0
  107. package/docs/src/components/htmlEditorControls/TextBackgroundColor.js +97 -0
  108. package/docs/src/components/htmlEditorControls/TextColor.js +97 -0
  109. package/docs/src/components/htmlEditorControls/Underline.js +12 -0
  110. package/docs/src/components/htmlEditorControls/WordCount.js +18 -0
  111. package/docs/src/utils/formatCode.js +1 -0
  112. package/icons/arrow_drop_down.svg +1 -0
  113. package/icons/circle.svg +1 -0
  114. package/icons/code.svg +1 -0
  115. package/icons/code_blocks.svg +1 -0
  116. package/icons/copy.svg +1 -0
  117. package/icons/dot.svg +1 -0
  118. package/icons/format_align_center.svg +1 -0
  119. package/icons/format_align_justify.svg +1 -0
  120. package/icons/format_align_left.svg +1 -0
  121. package/icons/format_align_right.svg +1 -0
  122. package/icons/format_bold.svg +1 -0
  123. package/icons/format_clear.svg +1 -0
  124. package/icons/format_color_fill.svg +1 -0
  125. package/icons/format_color_text.svg +1 -0
  126. package/icons/format_h1.svg +1 -0
  127. package/icons/format_h2.svg +1 -0
  128. package/icons/format_h3.svg +1 -0
  129. package/icons/format_h4.svg +1 -0
  130. package/icons/format_h5.svg +1 -0
  131. package/icons/format_h6.svg +1 -0
  132. package/icons/format_italic.svg +1 -0
  133. package/icons/format_list_bulleted.svg +1 -0
  134. package/icons/format_list_numbered.svg +1 -0
  135. package/icons/format_paragraph.svg +1 -0
  136. package/icons/format_underlined.svg +1 -0
  137. package/icons/help.svg +1 -0
  138. package/icons/link.svg +1 -0
  139. package/icons/more_vert.svg +1 -0
  140. package/icons/strikethrough_s.svg +1 -0
  141. package/icons/table.svg +1 -0
  142. package/icons/wysiwyg.svg +1 -0
  143. package/package.json +6 -5
  144. package/scripts/getIcon.js +52 -0
  145. package/src/components/Dialog.js +25 -10
  146. package/src/components/HtmlEditor.js +1548 -0
  147. package/src/components/Icon.js +6 -3
  148. package/src/components/Main.js +22 -23
  149. package/src/components/ShadowComponent.js +0 -1
  150. package/src/components/SideMenu.js +10 -0
  151. package/src/components/SidePanel.js +78 -37
  152. package/src/components/htmlEditorControls/AlignCenter.js +65 -0
  153. package/src/components/htmlEditorControls/AlignJustify.js +65 -0
  154. package/src/components/htmlEditorControls/AlignLeft.js +65 -0
  155. package/src/components/htmlEditorControls/AlignRight.js +65 -0
  156. package/src/components/htmlEditorControls/Bold.js +65 -0
  157. package/src/components/htmlEditorControls/BulletList.js +65 -0
  158. package/src/components/htmlEditorControls/CharacterCount.js +75 -0
  159. package/src/components/htmlEditorControls/ClearFormatting.js +65 -0
  160. package/src/components/htmlEditorControls/CodeBlock.js +72 -0
  161. package/src/components/htmlEditorControls/ControlGroup.js +95 -0
  162. package/src/components/htmlEditorControls/ControlSpacer.js +23 -0
  163. package/src/components/htmlEditorControls/CreateLink.js +121 -0
  164. package/src/components/htmlEditorControls/DropdownControl.js +149 -0
  165. package/src/components/htmlEditorControls/FormatBlock.js +108 -0
  166. package/src/components/htmlEditorControls/HtmlEditorControl.js +126 -0
  167. package/src/components/htmlEditorControls/InlineCode.js +77 -0
  168. package/src/components/htmlEditorControls/InsertTable.js +171 -0
  169. package/src/components/htmlEditorControls/Italic.js +65 -0
  170. package/src/components/htmlEditorControls/Mode.js +66 -0
  171. package/src/components/htmlEditorControls/NumberList.js +65 -0
  172. package/src/components/htmlEditorControls/Strikethrough.js +65 -0
  173. package/src/components/htmlEditorControls/TextBackgroundColor.js +195 -0
  174. package/src/components/htmlEditorControls/TextColor.js +195 -0
  175. package/src/components/htmlEditorControls/Underline.js +65 -0
  176. package/src/components/htmlEditorControls/WordCount.js +76 -0
  177. package/src/utils/formatCode.js +87 -0
  178. package/tests/components/Main.browser-test.js +166 -105
  179. package/tests/components/SidePanel.browser-test.js +62 -8
  180. package/.github/workflows/publish-major.yml +0 -39
  181. package/.github/workflows/publish-minor.yml +0 -39
  182. package/.github/workflows/publish-patch.yml +0 -45
  183. package/docs/icons/arrow-back.svg +0 -1
  184. package/docs/icons/arrow-down-double.svg +0 -1
  185. package/docs/icons/arrow-down.svg +0 -1
  186. package/docs/icons/arrow-forward.svg +0 -1
  187. package/docs/icons/arrow-menu-close.svg +0 -1
  188. package/docs/icons/arrow-menu-open.svg +0 -1
  189. package/docs/icons/arrow-right.svg +0 -1
  190. package/docs/icons/arrow-up-double.svg +0 -1
  191. package/docs/icons/arrow-up.svg +0 -1
  192. package/docs/icons/chevron-left.svg +0 -1
  193. package/docs/icons/chevron-right.svg +0 -1
  194. package/docs/icons/chevron-up.svg +0 -1
@@ -0,0 +1,1548 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import formatCode from '../utils/formatCode.js';
4
+ import debounce from '../utils/debounce.js';
5
+
6
+ export default class HtmlEditor extends ShadowComponent {
7
+ static formAssociated = true;
8
+
9
+ static properties = {
10
+ name: {
11
+ type: String,
12
+ reflect: true
13
+ },
14
+ value: {
15
+ type: String,
16
+ reflect: true
17
+ },
18
+ selection: {
19
+ type: Object,
20
+ state: true
21
+ },
22
+ mode: {
23
+ type: String,
24
+ reflect: true
25
+ },
26
+ hasTopToolbar: {
27
+ type: Boolean,
28
+ state: true
29
+ },
30
+ hasBottomToolbar: {
31
+ type: Boolean,
32
+ state: true
33
+ }
34
+ };
35
+
36
+ constructor() {
37
+ super();
38
+ this.internals = this.attachInternals();
39
+ this.name = '';
40
+ this.value = '';
41
+ this.selection = null;
42
+ this.cursor = null;
43
+ this.mode = 'visual';
44
+ this.hasTopToolbar = false;
45
+ this.hasBottomToolbar = false;
46
+ this.skipSync = false;
47
+ this.savedSelection = null;
48
+
49
+ this.debouncedDispatchChange = debounce(() => {
50
+ this.value = this.editorEl.innerHTML;
51
+ }, 300);
52
+ }
53
+
54
+ /*
55
+ Lifecycle Callbacks
56
+ */
57
+ connectedCallback() {
58
+ super.connectedCallback();
59
+
60
+ if(this.hasAttribute('value')){
61
+ this.value = this.getAttribute('value');
62
+ }
63
+
64
+ this.slotObserver = new MutationObserver(() => {
65
+ this.updateToolbarVisibility();
66
+ });
67
+
68
+ this.slotObserver.observe(this, {
69
+ childList: true,
70
+ subtree: true
71
+ });
72
+
73
+ this.updateToolbarVisibility();
74
+ }
75
+
76
+ disconnectedCallback() {
77
+ super.disconnectedCallback();
78
+ this.slotObserver?.disconnect();
79
+
80
+ if(this.boundUpdateSelection){
81
+ this.shadowRoot.removeEventListener('selectionchange', this.boundUpdateSelection);
82
+
83
+ if(this.editorEl){
84
+ this.editorEl.removeEventListener('mouseup', this.boundUpdateSelection);
85
+ this.editorEl.removeEventListener('keyup', this.boundUpdateSelection);
86
+ this.editorEl.removeEventListener('focus', this.boundUpdateSelection);
87
+ }
88
+ }
89
+ }
90
+
91
+ updateToolbarVisibility() {
92
+ const topSlotContent = Array.from(this.children).filter(child =>
93
+ child.getAttribute('slot') === 'toolbar-top'
94
+ );
95
+ const bottomSlotContent = Array.from(this.children).filter(child =>
96
+ child.getAttribute('slot') === 'toolbar-bottom'
97
+ );
98
+
99
+ this.hasTopToolbar = topSlotContent.length > 0;
100
+ this.hasBottomToolbar = bottomSlotContent.length > 0;
101
+ }
102
+
103
+ updated(changedProperties) {
104
+ super.updated(changedProperties);
105
+
106
+ if(changedProperties.has('value')){
107
+ this.syncContent();
108
+ this.updateFormValue();
109
+ this.dispatchEvent(new CustomEvent('change', {
110
+ detail: { value: this.value },
111
+ bubbles: true
112
+ }));
113
+ }
114
+
115
+ if(changedProperties.has('mode')){
116
+ this.syncContent();
117
+ this.dispatchEvent(new CustomEvent('mode-changed', {
118
+ detail: { mode: this.mode },
119
+ bubbles: true
120
+ }));
121
+ }
122
+ }
123
+
124
+ firstUpdated() {
125
+ this.editorEl = this.shadowRoot.querySelector('.editor');
126
+ this.textareaEl = this.shadowRoot.querySelector('textarea');
127
+
128
+ if(this.value){
129
+ this.syncContent();
130
+ }
131
+
132
+ this.boundUpdateSelection = this.updateSelection.bind(this);
133
+
134
+ this.shadowRoot.addEventListener('selectionchange', this.boundUpdateSelection);
135
+ this.editorEl.addEventListener('mouseup', this.boundUpdateSelection);
136
+ this.editorEl.addEventListener('keyup', this.boundUpdateSelection);
137
+ this.editorEl.addEventListener('focus', this.boundUpdateSelection);
138
+ this.editorEl.addEventListener('keydown', this.handleTableNavigation);
139
+
140
+ this.dispatchEvent(new CustomEvent('ready', {
141
+ detail: { value: this.value },
142
+ bubbles: true
143
+ }));
144
+ }
145
+
146
+ /*
147
+ Form Integration
148
+ */
149
+ updateFormValue() {
150
+ this.internals.setFormValue(this.getValue());
151
+ }
152
+
153
+ formResetCallback() {
154
+ this.value = '';
155
+ }
156
+
157
+ formStateRestoreCallback(state) {
158
+ this.value = state;
159
+ }
160
+
161
+ /*
162
+ Event Handlers
163
+ */
164
+ handleTableNavigation = (e) => {
165
+ if(e.key !== 'Tab' && e.key !== 'Enter') return;
166
+ if(this.mode !== 'visual') return;
167
+
168
+ const sel = this.shadowRoot.getSelection() || window.getSelection();
169
+ if(!sel.rangeCount) return;
170
+
171
+ let node = sel.getRangeAt(0).startContainer;
172
+
173
+ if(node.nodeType === Node.TEXT_NODE){
174
+ node = node.parentElement;
175
+ }
176
+
177
+ const cell = node.closest('td, th');
178
+ if(!cell) return;
179
+
180
+ e.preventDefault();
181
+
182
+ if(e.key === 'Tab'){
183
+ const row = cell.parentElement;
184
+ const cells = Array.from(row.cells);
185
+ const currentIndex = cells.indexOf(cell);
186
+
187
+ if(e.shiftKey){
188
+ // Shift+Tab: Go to previous cell
189
+ let prevCell = cells[currentIndex - 1];
190
+
191
+ if(!prevCell){
192
+ const prevRow = row.previousElementSibling;
193
+ if(prevRow){
194
+ prevCell = prevRow.cells[prevRow.cells.length - 1];
195
+ }
196
+ }
197
+
198
+ if(prevCell){
199
+ const range = document.createRange();
200
+ range.selectNodeContents(prevCell);
201
+ range.collapse(false);
202
+ sel.removeAllRanges();
203
+ sel.addRange(range);
204
+ prevCell.focus();
205
+ }
206
+ } else {
207
+ // Tab: Go to next cell
208
+ let nextCell = cells[currentIndex + 1];
209
+
210
+ if(!nextCell){
211
+ const nextRow = row.nextElementSibling;
212
+ if(nextRow){
213
+ nextCell = nextRow.cells[0];
214
+ }
215
+ }
216
+
217
+ if(nextCell){
218
+ const range = document.createRange();
219
+ range.selectNodeContents(nextCell);
220
+ range.collapse(false);
221
+ sel.removeAllRanges();
222
+ sel.addRange(range);
223
+ nextCell.focus();
224
+ }
225
+ }
226
+ } else if(e.key === 'Enter'){
227
+ const row = cell.parentElement;
228
+ const cells = Array.from(row.cells);
229
+ const currentIndex = cells.indexOf(cell);
230
+
231
+ if(e.shiftKey){
232
+ // Shift+Enter: Go to cell above
233
+ const prevRow = row.previousElementSibling;
234
+ if(prevRow && prevRow.cells[currentIndex]){
235
+ const prevCell = prevRow.cells[currentIndex];
236
+ const range = document.createRange();
237
+ range.selectNodeContents(prevCell);
238
+ range.collapse(false);
239
+ sel.removeAllRanges();
240
+ sel.addRange(range);
241
+ prevCell.focus();
242
+ }
243
+ } else {
244
+ // Enter: Go to cell below or create paragraph after table
245
+ const nextRow = row.nextElementSibling;
246
+ if(nextRow && nextRow.cells[currentIndex]){
247
+ const nextCell = nextRow.cells[currentIndex];
248
+ const range = document.createRange();
249
+ range.selectNodeContents(nextCell);
250
+ range.collapse(false);
251
+ sel.removeAllRanges();
252
+ sel.addRange(range);
253
+ nextCell.focus();
254
+ } else {
255
+ // Last row - create paragraph after table
256
+ const table = cell.closest('table');
257
+ if(table){
258
+ const p = document.createElement('p');
259
+ p.innerHTML = '<br>';
260
+
261
+ if(table.nextSibling){
262
+ table.parentNode.insertBefore(p, table.nextSibling);
263
+ } else {
264
+ table.parentNode.appendChild(p);
265
+ }
266
+
267
+ const range = document.createRange();
268
+ range.setStart(p, 0);
269
+ range.setEnd(p, 0);
270
+ sel.removeAllRanges();
271
+ sel.addRange(range);
272
+ p.focus();
273
+ }
274
+ }
275
+ }
276
+ }
277
+ };
278
+
279
+ updateSelection = () => {
280
+ if(this.mode !== 'visual'){
281
+ this.selection = null;
282
+ return;
283
+ }
284
+
285
+ const sel = this.shadowRoot.getSelection() || window.getSelection();
286
+
287
+ if(!sel.rangeCount){
288
+ this.selection = null;
289
+ return;
290
+ }
291
+
292
+ const range = sel.getRangeAt(0);
293
+
294
+ const isStartInEditor = this.editorEl.contains(range.startContainer) || this.editorEl === range.startContainer;
295
+ const isEndInEditor = this.editorEl.contains(range.endContainer) || this.editorEl === range.endContainer;
296
+
297
+ if(!isStartInEditor || !isEndInEditor){
298
+ this.selection = null;
299
+ return;
300
+ }
301
+
302
+ const selectedText = range.toString();
303
+
304
+ if(range.collapsed){
305
+ this.selection = null;
306
+ this.cursor = {
307
+ container: range.startContainer,
308
+ offset: range.startOffset
309
+ };
310
+ } else {
311
+ this.cursor = null;
312
+ this.selection = {
313
+ startContainer: range.startContainer,
314
+ startOffset: range.startOffset,
315
+ endContainer: range.endContainer,
316
+ endOffset: range.endOffset,
317
+ text: selectedText,
318
+ collapsed: false
319
+ };
320
+ }
321
+ }; handleEditorInput = () => {
322
+ // Don't update this.value on every keystroke - it triggers Lit's reactive system
323
+ // which calls syncContent() and destroys the cursor position.
324
+ // Use debounce to only update after user stops typing for 300ms.
325
+ this.debouncedDispatchChange();
326
+ this.dispatchEvent(new CustomEvent('input', { detail: { value: this.editorEl.innerHTML } }));
327
+ };
328
+
329
+ handleTextareaInput = (e) => {
330
+ if(this.mode === 'code'){
331
+ this.value = e.target.value;
332
+ }
333
+ };
334
+
335
+ handleEditorPaste = (e) => {
336
+ e.preventDefault();
337
+ const text = e.clipboardData.getData('text/plain');
338
+ document.execCommand('insertText', false, text);
339
+ };
340
+
341
+ handleEditorBlur = () => {
342
+ if(this.mode === 'visual'){
343
+ // Save selection before it's cleared
344
+ const selection = this.shadowRoot.getSelection() || window.getSelection();
345
+ if(selection.rangeCount > 0){
346
+ const range = selection.getRangeAt(0);
347
+ if(this.editorEl.contains(range.commonAncestorContainer)){
348
+ this.savedSelection = {
349
+ startContainer: range.startContainer,
350
+ startOffset: range.startOffset,
351
+ endContainer: range.endContainer,
352
+ endOffset: range.endOffset,
353
+ collapsed: range.collapsed
354
+ };
355
+ }
356
+ }
357
+
358
+ const cleanedValue = this.cleanupHtml(this.editorEl.innerHTML);
359
+ if(cleanedValue !== this.value){
360
+ this.value = cleanedValue;
361
+ this.syncContent();
362
+ }
363
+ }
364
+ };
365
+
366
+ /*
367
+ Public Methods - Mode Control
368
+ */
369
+ setMode(mode) {
370
+ if(['visual', 'code'].includes(mode)){
371
+ if(this.mode === 'visual' && mode === 'code'){
372
+ this.value = this.cleanupHtml(this.editorEl.innerHTML);
373
+ }
374
+ this.mode = mode;
375
+ }
376
+ return this;
377
+ }
378
+
379
+ toggleMode() {
380
+ const newMode = this.mode === 'visual' ? 'code' : 'visual';
381
+ this.setMode(newMode);
382
+ return this;
383
+ }
384
+
385
+ /*
386
+ Public Methods - Content Management
387
+ */
388
+ getValue() {
389
+ if(this.mode === 'visual'){
390
+ const content = this.editorEl ? this.editorEl.innerHTML : this.value;
391
+ return content.replace(/\u200B/g, '');
392
+ }
393
+ return this.value;
394
+ }
395
+
396
+ setValue(html) {
397
+ this.value = html;
398
+ this.syncContent();
399
+ return this;
400
+ }
401
+
402
+ clear() {
403
+ this.value = '';
404
+ this.syncContent();
405
+ return this;
406
+ }
407
+
408
+ cleanupHtml(html){
409
+ const tempDiv = document.createElement('div');
410
+ tempDiv.innerHTML = html;
411
+
412
+ const inlineTags = ['code', 'a', 'span', 'strong', 'em', 'b', 'i', 'u', 's'];
413
+ const blockTags = ['pre', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
414
+
415
+ const unwrapNested = (parent, tagName) => {
416
+ const elements = parent.querySelectorAll(tagName);
417
+ elements.forEach(el => {
418
+ let ancestor = el.parentElement;
419
+ while(ancestor && ancestor !== parent){
420
+ if(ancestor.tagName.toLowerCase() === tagName){
421
+ while(el.firstChild){
422
+ el.parentNode.insertBefore(el.firstChild, el);
423
+ }
424
+ el.parentNode.removeChild(el);
425
+ break;
426
+ }
427
+ ancestor = ancestor.parentElement;
428
+ }
429
+ });
430
+ };
431
+
432
+ const removeUselessStyles = (parent) => {
433
+ const allElements = parent.querySelectorAll('*');
434
+ allElements.forEach(el => {
435
+ if(!el.hasAttribute('style')) return;
436
+
437
+ const style = el.style;
438
+ const uselessProps = [
439
+ 'font-size',
440
+ 'font-family',
441
+ 'background-color',
442
+ 'line-height',
443
+ 'font-weight',
444
+ 'font-style'
445
+ ];
446
+
447
+ const uselessValues = [
448
+ 'inherit',
449
+ 'rgba(0, 0, 0, 0)',
450
+ 'transparent',
451
+ 'initial',
452
+ 'unset'
453
+ ];
454
+
455
+ uselessProps.forEach(prop => {
456
+ const value = style.getPropertyValue(prop);
457
+ if(value && uselessValues.some(uv => value.includes(uv))){
458
+ style.removeProperty(prop);
459
+ }
460
+ });
461
+
462
+ if(style.length === 0){
463
+ el.removeAttribute('style');
464
+ }
465
+ });
466
+ };
467
+
468
+ const unwrapEmptySpans = (parent) => {
469
+ const spans = parent.querySelectorAll('span');
470
+ spans.forEach(span => {
471
+ if(!span.className && !span.id && !span.hasAttribute('style')){
472
+ while(span.firstChild){
473
+ span.parentNode.insertBefore(span.firstChild, span);
474
+ }
475
+ span.parentNode.removeChild(span);
476
+ }
477
+ });
478
+ };
479
+
480
+ const normalizeWhitespace = (parent) => {
481
+ const walker = document.createTreeWalker(
482
+ parent,
483
+ NodeFilter.SHOW_TEXT,
484
+ null
485
+ );
486
+
487
+ const textNodes = [];
488
+ while(walker.nextNode()){
489
+ textNodes.push(walker.currentNode);
490
+ }
491
+
492
+ textNodes.forEach(node => {
493
+ node.textContent = node.textContent.replace(/\u00A0/g, ' ');
494
+ });
495
+ };
496
+
497
+ inlineTags.forEach(tag => unwrapNested(tempDiv, tag));
498
+ blockTags.forEach(tag => unwrapNested(tempDiv, tag));
499
+ removeUselessStyles(tempDiv);
500
+ unwrapEmptySpans(tempDiv);
501
+ normalizeWhitespace(tempDiv);
502
+
503
+ return tempDiv.innerHTML;
504
+ }
505
+
506
+ /*
507
+ Public Methods - Text Formatting
508
+ */
509
+ bold() {
510
+ this.execCommand('bold');
511
+ return this;
512
+ }
513
+
514
+ italic() {
515
+ this.execCommand('italic');
516
+ return this;
517
+ }
518
+
519
+ underline() {
520
+ this.execCommand('underline');
521
+ return this;
522
+ }
523
+
524
+ strikethrough() {
525
+ this.execCommand('strikeThrough');
526
+ return this;
527
+ }
528
+
529
+ /*
530
+ Public Methods - Lists
531
+ */
532
+ orderedList() {
533
+ this.execCommand('insertOrderedList');
534
+ return this;
535
+ }
536
+
537
+ unorderedList() {
538
+ this.execCommand('insertUnorderedList');
539
+ return this;
540
+ }
541
+
542
+ /*
543
+ Public Methods - Text Alignment
544
+ */
545
+ alignLeft() {
546
+ this.execCommand('justifyLeft');
547
+ return this;
548
+ }
549
+
550
+ alignCenter() {
551
+ this.execCommand('justifyCenter');
552
+ return this;
553
+ }
554
+
555
+ alignRight() {
556
+ this.execCommand('justifyRight');
557
+ return this;
558
+ }
559
+
560
+ alignJustify() {
561
+ this.execCommand('justifyFull');
562
+ return this;
563
+ }
564
+
565
+ /*
566
+ Public Methods - Text Color
567
+ */
568
+ setTextColor(color) {
569
+ this.execCommand('foreColor', color);
570
+ return this;
571
+ }
572
+
573
+ removeTextColor() {
574
+ this.execCommand('removeFormat');
575
+ return this;
576
+ }
577
+
578
+ setTextBackgroundColor(color) {
579
+ this.execCommand('backColor', color);
580
+ return this;
581
+ }
582
+
583
+ removeTextBackgroundColor() {
584
+ const selection = this.shadowRoot.getSelection();
585
+ if(!selection || selection.rangeCount === 0) return this;
586
+
587
+ const range = selection.getRangeAt(0);
588
+ if(range.collapsed) return this;
589
+
590
+ const fragment = range.extractContents();
591
+ const walker = document.createTreeWalker(
592
+ fragment,
593
+ NodeFilter.SHOW_ELEMENT,
594
+ null
595
+ );
596
+
597
+ const removeBackground = (el) => {
598
+ if(el.style){
599
+ el.style.backgroundColor = '';
600
+ if(!el.style.cssText){
601
+ el.removeAttribute('style');
602
+ }
603
+ }
604
+ };
605
+
606
+ let node;
607
+ while(node = walker.nextNode()){
608
+ removeBackground(node);
609
+ }
610
+
611
+ removeBackground(fragment.firstChild);
612
+ range.insertNode(fragment);
613
+
614
+ this.editorEl.dispatchEvent(new Event('input', {bubbles: true}));
615
+ return this;
616
+ }
617
+
618
+ /*
619
+ Public Methods - Formatting Control
620
+ */
621
+ removeFormat() {
622
+ if(this.mode !== 'visual') return this;
623
+
624
+ // If there's a selection, use the standard removeFormat command
625
+ if(this.selection){
626
+ this.execCommand('removeFormat');
627
+ return this;
628
+ }
629
+
630
+ // No selection - check if cursor is inside a formatting tag
631
+ const sel = this.shadowRoot.getSelection() || window.getSelection();
632
+ if(!sel.rangeCount) return this;
633
+
634
+ let node = sel.getRangeAt(0).startContainer;
635
+ if(node.nodeType === Node.TEXT_NODE){
636
+ node = node.parentElement;
637
+ }
638
+
639
+ // Find the closest formatting element
640
+ const formattingTags = ['B', 'I', 'U', 'S', 'STRONG', 'EM', 'CODE', 'MARK', 'SUB', 'SUP', 'SPAN'];
641
+ const formattingElement = node.closest(formattingTags.map(tag => tag.toLowerCase()).join(','));
642
+
643
+ if(formattingElement && this.editorEl.contains(formattingElement)){
644
+ // Get the text content
645
+ const textContent = formattingElement.textContent;
646
+
647
+ // Create a text node to replace the formatting element
648
+ const textNode = document.createTextNode(textContent);
649
+ formattingElement.replaceWith(textNode);
650
+
651
+ // Place cursor at the end of the text
652
+ const range = document.createRange();
653
+ range.setStart(textNode, textContent.length);
654
+ range.setEnd(textNode, textContent.length);
655
+ sel.removeAllRanges();
656
+ sel.addRange(range);
657
+
658
+ // Update value
659
+ this.value = this.editorEl.innerHTML;
660
+ }
661
+
662
+ return this;
663
+ }
664
+
665
+ formatBlock(tag) {
666
+ this.execCommand('formatBlock', tag);
667
+ return this;
668
+ }
669
+
670
+ insertHTML(html) {
671
+ this.execCommand('insertHTML', html);
672
+ return this;
673
+ }
674
+
675
+ /**
676
+ * Insert or edit a table at the cursor position
677
+ * @param {number} rows - Number of rows (excluding header row if includeHeaders is true)
678
+ * @param {number} columns - Number of columns
679
+ * @param {boolean} includeHeaders - Whether to include a header row
680
+ * @param {Array<Array<string>>|null} cellData - Optional 2D array of cell contents for editing existing tables
681
+ * @returns {HtmlEditor} Returns this for chaining
682
+ */
683
+ insertTable(rows, columns, includeHeaders = false, cellData = null) {
684
+ if(this.mode !== 'visual') return this;
685
+
686
+ const CURSOR_MARKER = '\uFFF0';
687
+ const markerData = this.getValueWithSelectionMarkers();
688
+ let html = markerData.html;
689
+
690
+ // Create table element
691
+ const table = document.createElement('table');
692
+ table.style.cssText = 'border-collapse: collapse; width: 100%;';
693
+
694
+ // Create header if needed
695
+ if(includeHeaders){
696
+ const thead = document.createElement('thead');
697
+ const headerRow = document.createElement('tr');
698
+
699
+ for(let c = 0; c < columns; c++){
700
+ const th = document.createElement('th');
701
+ th.style.cssText = 'border: 1px solid var(--border-color, #ccc); padding: 0.5rem; text-align: left; background-color: var(--background-secondary, #f5f5f5);';
702
+
703
+ if(cellData && cellData[0] && cellData[0][c] !== undefined){
704
+ th.innerHTML = cellData[0][c];
705
+ } else {
706
+ th.innerHTML = `Header ${c + 1}`;
707
+ }
708
+
709
+ headerRow.appendChild(th);
710
+ }
711
+
712
+ thead.appendChild(headerRow);
713
+ table.appendChild(thead);
714
+ }
715
+
716
+ // Create body rows
717
+ const tbody = document.createElement('tbody');
718
+ const startRow = includeHeaders && cellData ? 1 : 0;
719
+ const dataStartRow = includeHeaders ? 1 : 0;
720
+
721
+ for(let r = 0; r < rows; r++){
722
+ const row = document.createElement('tr');
723
+
724
+ for(let c = 0; c < columns; c++){
725
+ const td = document.createElement('td');
726
+ td.style.cssText = 'border: 1px solid var(--border-color, #ccc); padding: 0.5rem;';
727
+
728
+ if(cellData && cellData[r + dataStartRow] && cellData[r + dataStartRow][c] !== undefined){
729
+ td.innerHTML = cellData[r + dataStartRow][c];
730
+ } else {
731
+ td.innerHTML = '\u200B';
732
+ }
733
+
734
+ row.appendChild(td);
735
+ }
736
+
737
+ tbody.appendChild(row);
738
+ }
739
+
740
+ table.appendChild(tbody);
741
+
742
+ // Convert to HTML
743
+ const tempDiv = document.createElement('div');
744
+ tempDiv.appendChild(table);
745
+ const tableHTML = tempDiv.innerHTML;
746
+
747
+ const pAfter = document.createElement('p');
748
+ pAfter.innerHTML = '<br>';
749
+ const fullHTML = tableHTML + pAfter.outerHTML;
750
+
751
+ // Insert or append table
752
+ if(markerData.hasCursor){
753
+ html = html.replace(CURSOR_MARKER, fullHTML + CURSOR_MARKER);
754
+ } else if(markerData.hasSelection){
755
+ // Replace selection with table
756
+ html = html.replace(markerData.selectionStart, '').replace(markerData.selectionEnd, '');
757
+ html = html + fullHTML;
758
+ } else {
759
+ // No cursor or selection - append to end
760
+ html = html + fullHTML;
761
+ }
762
+
763
+ // Restore cursor position after table
764
+ const markers = {
765
+ hasCursor: markerData.hasCursor,
766
+ hasSelection: false,
767
+ cursorMarker: CURSOR_MARKER
768
+ };
769
+
770
+ this.setValueFromSelectionMarkers(html, markers);
771
+
772
+ return this;
773
+ }
774
+
775
+ insertElementAtCursor(element, selectAfter = true) {
776
+ if(this.mode !== 'visual') return this;
777
+
778
+ const CURSOR_MARKER = '\uFFF0';
779
+ const SELECT_START = '\uFFF1';
780
+ const SELECT_END = '\uFFF2';
781
+
782
+ const markerData = this.getValueWithSelectionMarkers();
783
+ let html = markerData.html;
784
+
785
+ if(selectAfter){
786
+ element.textContent = SELECT_START + (element.textContent || '') + SELECT_END;
787
+ }
788
+ const elementHtml = element.outerHTML;
789
+
790
+ if(markerData.hasCursor){
791
+ html = html.replace(CURSOR_MARKER, elementHtml);
792
+ } else {
793
+ // No cursor - append at the very end of content
794
+ // Wrap inline elements in a paragraph
795
+ const inlineElements = ['code', 'a', 'span', 'strong', 'em', 'b', 'i', 'u', 's', 'mark', 'sub', 'sup'];
796
+ const tagName = element.tagName.toLowerCase();
797
+
798
+ if(inlineElements.includes(tagName)){
799
+ html = html + '<p>' + elementHtml + '</p>';
800
+ } else {
801
+ html = html + elementHtml;
802
+ }
803
+ }
804
+
805
+ const markers = {
806
+ hasCursor: false,
807
+ hasSelection: selectAfter,
808
+ selectionStart: SELECT_START,
809
+ selectionEnd: SELECT_END,
810
+ selectedText: element.textContent || ''
811
+ };
812
+
813
+ this.setValueFromSelectionMarkers(html, markers);
814
+
815
+ return this;
816
+ }
817
+
818
+ replaceSelectionWithElement(element, selectAfter = true) {
819
+ if(this.mode !== 'visual') return this;
820
+
821
+ const SELECT_START = '\uFFF1';
822
+ const SELECT_END = '\uFFF2';
823
+
824
+ const markerData = this.getValueWithSelectionMarkers();
825
+
826
+ if(!markerData.hasSelection) return this;
827
+
828
+ let html = markerData.html;
829
+
830
+ if(selectAfter){
831
+ element.textContent = SELECT_START + element.textContent + SELECT_END;
832
+ }
833
+ const elementHtml = element.outerHTML;
834
+
835
+ const selectedPattern = SELECT_START + markerData.selectedText + SELECT_END;
836
+ html = html.replace(selectedPattern, elementHtml);
837
+
838
+ const markers = {
839
+ hasCursor: false,
840
+ hasSelection: selectAfter,
841
+ selectionStart: SELECT_START,
842
+ selectionEnd: SELECT_END,
843
+ selectedText: markerData.selectedText
844
+ };
845
+
846
+ this.setValueFromSelectionMarkers(html, markers);
847
+
848
+ return this;
849
+ }
850
+
851
+ /*
852
+ Public Methods - Selection Management
853
+ */
854
+ getSelection() {
855
+ if(this.mode !== 'visual') return null;
856
+
857
+ this.editorEl.focus();
858
+ const selection = window.getSelection();
859
+
860
+ if(!selection.rangeCount) return null;
861
+
862
+ const range = selection.getRangeAt(0);
863
+ const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
864
+
865
+ if(!isInEditor) return null;
866
+
867
+ return {
868
+ text: selection.toString(),
869
+ html: range.cloneContents().textContent ? new XMLSerializer().serializeToString(range.cloneContents()) : '',
870
+ range: range,
871
+ selection: selection
872
+ };
873
+ }
874
+
875
+ getSelectedText() {
876
+ const sel = this.getSelection();
877
+ return sel ? sel.text : '';
878
+ }
879
+
880
+ getSelectedHTML() {
881
+ const sel = this.getSelection();
882
+ return sel ? sel.html : '';
883
+ }
884
+
885
+ setSelection(startNode, startOffset, endNode, endOffset) {
886
+ if(this.mode !== 'visual') return this;
887
+
888
+ this.editorEl.focus();
889
+ const selection = window.getSelection();
890
+ const range = document.createRange();
891
+
892
+ range.setStart(startNode, startOffset);
893
+ range.setEnd(endNode, endOffset);
894
+
895
+ selection.removeAllRanges();
896
+ selection.addRange(range);
897
+
898
+ return this;
899
+ }
900
+
901
+ selectAll() {
902
+ if(this.mode === 'visual'){
903
+ this.execCommand('selectAll');
904
+ } else {
905
+ this.textareaEl.select();
906
+ }
907
+ return this;
908
+ }
909
+
910
+ replaceSelection(html) {
911
+ if(this.mode !== 'visual') return this;
912
+
913
+ const sel = this.getSelection();
914
+ if(!sel) return this;
915
+
916
+ sel.range.deleteContents();
917
+ const fragment = sel.range.createContextualFragment(html);
918
+ sel.range.insertNode(fragment);
919
+
920
+ this.value = this.editorEl.innerHTML;
921
+ return this;
922
+ }
923
+
924
+ deleteSelection() {
925
+ if(this.mode === 'visual'){
926
+ this.execCommand('delete');
927
+ } else {
928
+ const start = this.textareaEl.selectionStart;
929
+ const end = this.textareaEl.selectionEnd;
930
+ this.textareaEl.value = this.textareaEl.value.substring(0, start) + this.textareaEl.value.substring(end);
931
+ this.value = this.textareaEl.value;
932
+ }
933
+ return this;
934
+ }
935
+
936
+ getTextPosition(root, targetNode){
937
+ const walker = document.createTreeWalker(
938
+ root,
939
+ NodeFilter.SHOW_TEXT,
940
+ null
941
+ );
942
+
943
+ let position = 0;
944
+
945
+ while(walker.nextNode()){
946
+ const node = walker.currentNode;
947
+
948
+ if(targetNode.contains(node)){
949
+ return position;
950
+ }
951
+
952
+ position += node.textContent.length;
953
+ }
954
+
955
+ return position;
956
+ }
957
+
958
+ wrapSelection(before, after, savedSelection = null){
959
+ if(this.mode !== 'visual') return this;
960
+
961
+ let selectedText;
962
+
963
+ if(savedSelection){
964
+ selectedText = savedSelection;
965
+ } else {
966
+ const selection = window.getSelection();
967
+
968
+ if(!selection.rangeCount || selection.isCollapsed){
969
+ return this;
970
+ }
971
+
972
+ const range = selection.getRangeAt(0);
973
+ const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
974
+
975
+ if(!isInEditor){
976
+ return this;
977
+ }
978
+
979
+ selectedText = selection.toString();
980
+ }
981
+
982
+ if(!selectedText) return this;
983
+
984
+ this.editorEl.focus();
985
+
986
+ const openingTagMatch = before.match(/<([a-zA-Z]+)/);
987
+ if(!openingTagMatch) return this;
988
+ const outerTagName = openingTagMatch[1];
989
+
990
+ const tempDiv = document.createElement('div');
991
+ tempDiv.innerHTML = this.getValue();
992
+
993
+ const textContent = tempDiv.textContent;
994
+ const textIndex = textContent.indexOf(selectedText);
995
+
996
+ if(textIndex === -1) return this;
997
+
998
+ const walker = document.createTreeWalker(
999
+ tempDiv,
1000
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
1001
+ null
1002
+ );
1003
+
1004
+ let currentPos = 0;
1005
+ let startNode = null;
1006
+ let startOffset = 0;
1007
+ let endNode = null;
1008
+ let endOffset = 0;
1009
+ const overlappingWrappers = [];
1010
+
1011
+ while(walker.nextNode()){
1012
+ const node = walker.currentNode;
1013
+
1014
+ if(node.nodeType === Node.ELEMENT_NODE){
1015
+ if(node.tagName.toLowerCase() === outerTagName){
1016
+ const nodeTextStart = this.getTextPosition(tempDiv, node);
1017
+ const nodeTextEnd = nodeTextStart + node.textContent.length;
1018
+
1019
+ const selectionEnd = textIndex + selectedText.length;
1020
+ const overlaps = !(nodeTextEnd <= textIndex || nodeTextStart >= selectionEnd);
1021
+
1022
+ if(overlaps){
1023
+ overlappingWrappers.push(node);
1024
+ }
1025
+ }
1026
+ continue;
1027
+ }
1028
+
1029
+ const nodeLength = node.textContent.length;
1030
+ const nodeStart = currentPos;
1031
+ const nodeEnd = currentPos + nodeLength;
1032
+
1033
+ if(!startNode && nodeEnd > textIndex){
1034
+ startNode = node;
1035
+ startOffset = textIndex - nodeStart;
1036
+ }
1037
+
1038
+ if(nodeEnd >= textIndex + selectedText.length){
1039
+ endNode = node;
1040
+ endOffset = (textIndex + selectedText.length) - nodeStart;
1041
+ break;
1042
+ }
1043
+
1044
+ currentPos += nodeLength;
1045
+ }
1046
+
1047
+ if(overlappingWrappers.length > 0){
1048
+ overlappingWrappers.forEach(wrapper => {
1049
+ const parent = wrapper.parentNode;
1050
+ const isPreTag = wrapper.tagName.toLowerCase() === 'pre';
1051
+
1052
+ const allTags = [];
1053
+ const collectTags = (el) => {
1054
+ allTags.push(el);
1055
+ Array.from(el.children).forEach(child => collectTags(child));
1056
+ };
1057
+ collectTags(wrapper);
1058
+
1059
+ if(isPreTag){
1060
+ const p = document.createElement('p');
1061
+ while(wrapper.firstChild){
1062
+ p.appendChild(wrapper.firstChild);
1063
+ }
1064
+ parent.replaceChild(p, wrapper);
1065
+ } else {
1066
+ allTags.reverse().forEach(tag => {
1067
+ if(tag.parentNode){
1068
+ while(tag.firstChild){
1069
+ tag.parentNode.insertBefore(tag.firstChild, tag);
1070
+ }
1071
+ tag.parentNode.removeChild(tag);
1072
+ }
1073
+ });
1074
+ }
1075
+ });
1076
+ } else if(startNode && endNode){
1077
+ const range = document.createRange();
1078
+ range.setStart(startNode, startOffset);
1079
+ range.setEnd(endNode, endOffset);
1080
+
1081
+ const fragment = range.extractContents();
1082
+
1083
+ const wrapperDiv = document.createElement('div');
1084
+ wrapperDiv.innerHTML = before + after;
1085
+ const wrapper = wrapperDiv.firstChild;
1086
+
1087
+ let innermost = wrapper;
1088
+ while(innermost.firstChild && innermost.firstChild.nodeType === Node.ELEMENT_NODE){
1089
+ innermost = innermost.firstChild;
1090
+ }
1091
+
1092
+ innermost.appendChild(fragment);
1093
+ range.insertNode(wrapper);
1094
+
1095
+ if(outerTagName === 'pre'){
1096
+ let parent = wrapper.parentNode;
1097
+ while(parent && parent !== tempDiv){
1098
+ if(parent.tagName && parent.tagName.toLowerCase() === 'p'){
1099
+ const afterContent = [];
1100
+ let sibling = wrapper.nextSibling;
1101
+ while(sibling){
1102
+ const next = sibling.nextSibling;
1103
+ afterContent.push(sibling);
1104
+ sibling = next;
1105
+ }
1106
+
1107
+ parent.parentNode.insertBefore(wrapper, parent.nextSibling);
1108
+
1109
+ if(afterContent.length > 0 && afterContent.some(n => n.textContent.trim())){
1110
+ const newP = document.createElement('p');
1111
+ afterContent.forEach(node => newP.appendChild(node));
1112
+ wrapper.parentNode.insertBefore(newP, wrapper.nextSibling);
1113
+ }
1114
+
1115
+ break;
1116
+ }
1117
+ parent = parent.parentNode;
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ this.setValue(tempDiv.innerHTML);
1123
+
1124
+ this.editorEl.focus();
1125
+
1126
+ setTimeout(() => {
1127
+ const textContent = this.editorEl.textContent;
1128
+ const textIndex = textContent.indexOf(selectedText);
1129
+
1130
+ if(textIndex === -1) return;
1131
+
1132
+ const walker = document.createTreeWalker(
1133
+ this.editorEl,
1134
+ NodeFilter.SHOW_TEXT,
1135
+ null
1136
+ );
1137
+
1138
+ let currentPos = 0;
1139
+ let startNode = null;
1140
+ let startOffset = 0;
1141
+ let endNode = null;
1142
+ let endOffset = 0;
1143
+
1144
+ while(walker.nextNode()){
1145
+ const node = walker.currentNode;
1146
+ const nodeLength = node.textContent.length;
1147
+ const nodeStart = currentPos;
1148
+ const nodeEnd = currentPos + nodeLength;
1149
+
1150
+ if(startNode === null && nodeEnd > textIndex){
1151
+ startNode = node;
1152
+ startOffset = textIndex - nodeStart;
1153
+ }
1154
+
1155
+ if(nodeEnd >= textIndex + selectedText.length){
1156
+ endNode = node;
1157
+ endOffset = (textIndex + selectedText.length) - nodeStart;
1158
+ break;
1159
+ }
1160
+
1161
+ currentPos += nodeLength;
1162
+ }
1163
+
1164
+ if(startNode && endNode){
1165
+ const range = document.createRange();
1166
+ range.setStart(startNode, startOffset);
1167
+ range.setEnd(endNode, endOffset);
1168
+
1169
+ const selection = window.getSelection();
1170
+ selection.removeAllRanges();
1171
+ selection.addRange(range);
1172
+ }
1173
+ }, 0);
1174
+
1175
+ return this;
1176
+ }
1177
+
1178
+ insertAtCursor(html) {
1179
+ this.insertHTML(html);
1180
+ return this;
1181
+ }
1182
+
1183
+ /*
1184
+ Public Methods - Links and Media
1185
+ */
1186
+ createLink(url) {
1187
+ this.execCommand('createLink', url);
1188
+ return this;
1189
+ }
1190
+
1191
+ unlink() {
1192
+ this.execCommand('unlink');
1193
+ return this;
1194
+ }
1195
+
1196
+ insertImage(url) {
1197
+ this.execCommand('insertImage', url);
1198
+ return this;
1199
+ }
1200
+
1201
+ /*
1202
+ Public Methods - History
1203
+ */
1204
+ undo() {
1205
+ this.execCommand('undo');
1206
+ return this;
1207
+ }
1208
+
1209
+ redo() {
1210
+ this.execCommand('redo');
1211
+ return this;
1212
+ }
1213
+
1214
+ /*
1215
+ Utility Methods
1216
+ */
1217
+ execCommand(command, value = null) {
1218
+ if(this.mode !== 'visual') return;
1219
+
1220
+ this.editorEl.focus();
1221
+ document.execCommand(command, false, value);
1222
+ this.value = this.editorEl.innerHTML;
1223
+ }
1224
+
1225
+ captureSelection(){
1226
+ if(this.mode !== 'visual') return null;
1227
+
1228
+ const selection = window.getSelection();
1229
+
1230
+ if(!selection.rangeCount){
1231
+ return null;
1232
+ }
1233
+
1234
+ const range = selection.getRangeAt(0);
1235
+ const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
1236
+
1237
+ if(!isInEditor){
1238
+ return null;
1239
+ }
1240
+
1241
+ const selectedText = range.toString();
1242
+
1243
+ if(range.collapsed){
1244
+ return null;
1245
+ }
1246
+
1247
+ return {
1248
+ startContainer: range.startContainer,
1249
+ startOffset: range.startOffset,
1250
+ endContainer: range.endContainer,
1251
+ endOffset: range.endOffset,
1252
+ text: selectedText,
1253
+ collapsed: range.collapsed
1254
+ };
1255
+ }
1256
+
1257
+ restoreSavedSelection(){
1258
+ if(!this.savedSelection) return false;
1259
+
1260
+ const selection = this.shadowRoot.getSelection() || window.getSelection();
1261
+ const range = document.createRange();
1262
+
1263
+ try {
1264
+ range.setStart(this.savedSelection.startContainer, this.savedSelection.startOffset);
1265
+ range.setEnd(this.savedSelection.endContainer, this.savedSelection.endOffset);
1266
+ selection.removeAllRanges();
1267
+ selection.addRange(range);
1268
+ return true;
1269
+ } catch(e) {
1270
+ this.savedSelection = null;
1271
+ return false;
1272
+ }
1273
+ }
1274
+
1275
+ clearSavedSelection(){
1276
+ this.savedSelection = null;
1277
+ }
1278
+
1279
+ getValueWithSelectionMarkers(){
1280
+ if(this.mode !== 'visual') return { html: this.value, hasCursor: false, hasSelection: false };
1281
+
1282
+ const CURSOR_MARKER = '\uFFF0';
1283
+ const SELECTION_START = '\uFFF1';
1284
+ const SELECTION_END = '\uFFF2';
1285
+
1286
+ const selection = this.shadowRoot.getSelection() || window.getSelection();
1287
+
1288
+ // If no current selection, try to restore saved selection
1289
+ if(!selection.rangeCount){
1290
+ if(!this.restoreSavedSelection()){
1291
+ return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1292
+ }
1293
+ }
1294
+
1295
+ if(!selection.rangeCount){
1296
+ return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1297
+ }
1298
+
1299
+ const range = selection.getRangeAt(0);
1300
+ const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
1301
+
1302
+ if(!isInEditor){
1303
+ return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1304
+ }
1305
+
1306
+ const selectedText = range.toString();
1307
+
1308
+ if(range.collapsed){
1309
+ const cursorMarker = document.createTextNode(CURSOR_MARKER);
1310
+ const tempRange = document.createRange();
1311
+ tempRange.setStart(range.startContainer, range.startOffset);
1312
+ tempRange.insertNode(cursorMarker);
1313
+
1314
+ const htmlWithMarker = this.editorEl.innerHTML;
1315
+ cursorMarker.remove();
1316
+
1317
+ return {
1318
+ html: htmlWithMarker,
1319
+ hasCursor: true,
1320
+ hasSelection: false,
1321
+ cursorMarker: CURSOR_MARKER,
1322
+ selectedText: ''
1323
+ };
1324
+ } else {
1325
+ const startMarker = document.createTextNode(SELECTION_START);
1326
+ const endMarker = document.createTextNode(SELECTION_END);
1327
+
1328
+ const tempRange = document.createRange();
1329
+ tempRange.setStart(range.endContainer, range.endOffset);
1330
+ tempRange.insertNode(endMarker);
1331
+
1332
+ tempRange.setStart(range.startContainer, range.startOffset);
1333
+ tempRange.insertNode(startMarker);
1334
+
1335
+ const htmlWithMarkers = this.editorEl.innerHTML;
1336
+
1337
+ startMarker.remove();
1338
+ endMarker.remove();
1339
+
1340
+ return {
1341
+ html: htmlWithMarkers,
1342
+ hasCursor: false,
1343
+ hasSelection: true,
1344
+ selectionStart: SELECTION_START,
1345
+ selectionEnd: SELECTION_END,
1346
+ selectedText: selectedText
1347
+ };
1348
+ }
1349
+ }
1350
+
1351
+ setValueFromSelectionMarkers(html, markers){
1352
+ if(this.mode !== 'visual') return this;
1353
+
1354
+ const CURSOR_MARKER = markers.cursorMarker || '\uFFF0';
1355
+ const SELECTION_START = markers.selectionStart || '\uFFF1';
1356
+ const SELECTION_END = markers.selectionEnd || '\uFFF2';
1357
+
1358
+ this.editorEl.innerHTML = html;
1359
+
1360
+ if(markers.hasCursor){
1361
+ const walker = document.createTreeWalker(
1362
+ this.editorEl,
1363
+ NodeFilter.SHOW_TEXT,
1364
+ null
1365
+ );
1366
+
1367
+ while(walker.nextNode()){
1368
+ const node = walker.currentNode;
1369
+ const index = node.textContent.indexOf(CURSOR_MARKER);
1370
+
1371
+ if(index !== -1){
1372
+ node.textContent = node.textContent.replace(CURSOR_MARKER, '');
1373
+
1374
+ const range = document.createRange();
1375
+ range.setStart(node, index);
1376
+ range.setEnd(node, index);
1377
+
1378
+ const selection = this.shadowRoot.getSelection() || window.getSelection();
1379
+ selection.removeAllRanges();
1380
+ selection.addRange(range);
1381
+
1382
+ break;
1383
+ }
1384
+ }
1385
+ } else if(markers.hasSelection){
1386
+ const walker = document.createTreeWalker(
1387
+ this.editorEl,
1388
+ NodeFilter.SHOW_TEXT,
1389
+ null
1390
+ );
1391
+
1392
+ let startNode = null;
1393
+ let startOffset = 0;
1394
+ let endNode = null;
1395
+ let endOffset = 0;
1396
+
1397
+ while(walker.nextNode()){
1398
+ const node = walker.currentNode;
1399
+
1400
+ const startIndex = node.textContent.indexOf(SELECTION_START);
1401
+ if(startIndex !== -1){
1402
+ startNode = node;
1403
+ startOffset = startIndex;
1404
+ node.textContent = node.textContent.replace(SELECTION_START, '');
1405
+ }
1406
+
1407
+ const endIndex = node.textContent.indexOf(SELECTION_END);
1408
+ if(endIndex !== -1){
1409
+ endNode = node;
1410
+ endOffset = endIndex;
1411
+ node.textContent = node.textContent.replace(SELECTION_END, '');
1412
+ }
1413
+
1414
+ if(startNode && endNode) break;
1415
+ }
1416
+
1417
+ if(startNode && endNode){
1418
+ const range = document.createRange();
1419
+ range.setStart(startNode, startOffset);
1420
+ range.setEnd(endNode, endOffset);
1421
+
1422
+ const selection = this.shadowRoot.getSelection() || window.getSelection();
1423
+ selection.removeAllRanges();
1424
+ selection.addRange(range);
1425
+ }
1426
+ }
1427
+
1428
+ this.editorEl.focus();
1429
+
1430
+ return this;
1431
+ }
1432
+
1433
+ syncContent() {
1434
+ if(!this.editorEl || !this.textareaEl || this.skipSync) return;
1435
+
1436
+ if(this.mode === 'visual'){
1437
+ if(this.editorEl.innerHTML !== this.value){
1438
+ this.editorEl.innerHTML = this.value;
1439
+ }
1440
+ } else {
1441
+ const formattedValue = formatCode(this.value);
1442
+ if(this.textareaEl.value !== formattedValue){
1443
+ this.textareaEl.value = formattedValue;
1444
+ }
1445
+ }
1446
+ }
1447
+
1448
+ /*
1449
+ Rendering
1450
+ */
1451
+ static styles = css`
1452
+ :host {
1453
+ display: flex;
1454
+ flex-direction: column;
1455
+ gap: 0;
1456
+ height: 300px;
1457
+ }
1458
+
1459
+ .toolbar-top,
1460
+ .toolbar-bottom {
1461
+ display: flex;
1462
+ flex-wrap: wrap;
1463
+ align-items: center;
1464
+ gap: 0;
1465
+ background: var(--bg-secondary);
1466
+ min-height: 40px;
1467
+ }
1468
+
1469
+ .editor-container {
1470
+ position: relative;
1471
+ flex: 1;
1472
+ min-height: 0;
1473
+ overflow: hidden;
1474
+ }
1475
+
1476
+ .editor,
1477
+ textarea {
1478
+ width: 100%;
1479
+ height: 100%;
1480
+ padding: 1rem;
1481
+ border: 1px solid var(--border-color);
1482
+ background: var(--bg-primary);
1483
+ color: var(--text-primary);
1484
+ font-family: inherit;
1485
+ font-size: inherit;
1486
+ line-height: 1.5;
1487
+ overflow: auto;
1488
+ box-sizing: border-box;
1489
+ }
1490
+
1491
+ .editor {
1492
+ outline: none;
1493
+ }
1494
+
1495
+ .editor:focus {
1496
+ border-color: var(--primary-color);
1497
+ }
1498
+
1499
+ textarea {
1500
+ resize: none;
1501
+ font-family: monospace;
1502
+ }
1503
+
1504
+ textarea:focus {
1505
+ outline: none;
1506
+ border-color: var(--primary-color);
1507
+ }
1508
+
1509
+ .editor[hidden],
1510
+ textarea[hidden] {
1511
+ display: none;
1512
+ }
1513
+ `;
1514
+
1515
+ render() {
1516
+ return html`
1517
+ ${this.hasTopToolbar ? html`
1518
+ <div class="toolbar-top bb">
1519
+ <slot name="toolbar-top"></slot>
1520
+ </div>
1521
+ ` : ''}
1522
+
1523
+ <div class="editor-container">
1524
+ <div
1525
+ class="editor"
1526
+ contenteditable="true"
1527
+ ?hidden=${this.mode !== 'visual'}
1528
+ @input=${this.handleEditorInput}
1529
+ @paste=${this.handleEditorPaste}
1530
+ @blur=${this.handleEditorBlur}
1531
+ ></div>
1532
+
1533
+ <textarea
1534
+ ?hidden=${this.mode !== 'code'}
1535
+ @input=${this.handleTextareaInput}
1536
+ ></textarea>
1537
+ </div>
1538
+
1539
+ ${this.hasBottomToolbar ? html`
1540
+ <div class="toolbar-bottom bt">
1541
+ <slot name="toolbar-bottom"></slot>
1542
+ </div>
1543
+ ` : ''}
1544
+ `;
1545
+ }
1546
+ }
1547
+
1548
+ customElements.define('k-html-editor', HtmlEditor);