@dodlhuat/basix 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +1 -1
  2. package/css/accordion.scss +31 -22
  3. package/css/alert.scss +79 -27
  4. package/css/button.scss +151 -102
  5. package/css/card.scss +11 -12
  6. package/css/carousel.scss +123 -87
  7. package/css/chart.scss +9 -11
  8. package/css/chat-bubbles.scss +2 -2
  9. package/css/checkbox.scss +72 -55
  10. package/css/chips.scss +52 -52
  11. package/css/code-viewer.scss +73 -98
  12. package/css/datepicker.scss +20 -0
  13. package/css/dropdown.scss +151 -137
  14. package/css/editor.scss +9 -6
  15. package/css/file-uploader.scss +187 -195
  16. package/css/flyout-menu.scss +20 -13
  17. package/css/form.scss +168 -115
  18. package/css/gallery.scss +62 -63
  19. package/css/grid.scss +0 -1
  20. package/css/modal.scss +117 -72
  21. package/css/placeholder.scss +17 -12
  22. package/css/properties.scss +6 -0
  23. package/css/push-menu.scss +70 -23
  24. package/css/radiobutton.scss +86 -64
  25. package/css/range-slider.scss +136 -0
  26. package/css/scrollbar.scss +69 -69
  27. package/css/spinner.scss +41 -66
  28. package/css/style.css +4351 -3735
  29. package/css/style.css.map +1 -1
  30. package/css/style.scss +2 -1
  31. package/css/switch.scss +43 -42
  32. package/css/table.scss +61 -40
  33. package/css/tabs.scss +12 -7
  34. package/css/timeline.scss +72 -69
  35. package/css/timepicker.scss +151 -72
  36. package/css/toast.scss +49 -48
  37. package/css/tooltip.scss +112 -122
  38. package/css/tree.scss +135 -192
  39. package/css/typography.scss +70 -9
  40. package/css/virtual-dropdown.scss +201 -142
  41. package/js/carousel.js +45 -18
  42. package/js/carousel.ts +217 -173
  43. package/js/datepicker.js +505 -497
  44. package/js/datepicker.ts +9 -0
  45. package/js/editor.js +398 -415
  46. package/js/file-uploader.js +142 -128
  47. package/js/file-uploader.ts +364 -350
  48. package/js/gallery.js +22 -15
  49. package/js/gallery.ts +17 -12
  50. package/js/index.js +718 -720
  51. package/js/index.ts +7 -8
  52. package/js/push-menu.js +113 -101
  53. package/js/push-menu.ts +17 -2
  54. package/js/range-slider.js +26 -0
  55. package/js/range-slider.ts +33 -0
  56. package/js/timepicker.js +144 -98
  57. package/js/timepicker.ts +194 -131
  58. package/js/tree.js +56 -28
  59. package/js/tree.ts +239 -218
  60. package/package.json +1 -1
  61. package/css/accordion.css +0 -109
  62. package/css/accordion.css.map +0 -1
  63. package/css/alert.css +0 -57
  64. package/css/alert.css.map +0 -1
  65. package/css/button.css +0 -69
  66. package/css/button.css.map +0 -1
  67. package/css/card.css +0 -144
  68. package/css/card.css.map +0 -1
  69. package/css/carousel.css +0 -118
  70. package/css/carousel.css.map +0 -1
  71. package/css/chart.css +0 -159
  72. package/css/chart.css.map +0 -1
  73. package/css/chat-bubbles.css +0 -97
  74. package/css/chat-bubbles.css.map +0 -1
  75. package/css/checkbox.css +0 -77
  76. package/css/checkbox.css.map +0 -1
  77. package/css/chips.css +0 -72
  78. package/css/chips.css.map +0 -1
  79. package/css/code-viewer.css +0 -97
  80. package/css/code-viewer.css.map +0 -1
  81. package/css/colors.css +0 -63
  82. package/css/colors.css.map +0 -1
  83. package/css/datepicker.css +0 -264
  84. package/css/datepicker.css.map +0 -1
  85. package/css/defaults.css +0 -118
  86. package/css/defaults.css.map +0 -1
  87. package/css/dropdown.css +0 -146
  88. package/css/dropdown.css.map +0 -1
  89. package/css/editor.css +0 -413
  90. package/css/file-uploader.css +0 -194
  91. package/css/file-uploader.css.map +0 -1
  92. package/css/flyout-menu.css +0 -345
  93. package/css/flyout-menu.css.map +0 -1
  94. package/css/form-builder.css +0 -9
  95. package/css/form-builder.css.map +0 -1
  96. package/css/form-builder.scss +0 -11
  97. package/css/form.css +0 -130
  98. package/css/form.css.map +0 -1
  99. package/css/gallery.css +0 -91
  100. package/css/gallery.css.map +0 -1
  101. package/css/grid.css +0 -44
  102. package/css/grid.css.map +0 -1
  103. package/css/icons.css +0 -327
  104. package/css/icons.css.map +0 -1
  105. package/css/modal.css +0 -97
  106. package/css/modal.css.map +0 -1
  107. package/css/parameters.css +0 -1
  108. package/css/parameters.css.map +0 -1
  109. package/css/placeholder.css +0 -50
  110. package/css/placeholder.css.map +0 -1
  111. package/css/progress.css +0 -51
  112. package/css/progress.css.map +0 -1
  113. package/css/properties.css +0 -31
  114. package/css/properties.css.map +0 -1
  115. package/css/push-menu.css +0 -145
  116. package/css/push-menu.css.map +0 -1
  117. package/css/radiobutton.css +0 -91
  118. package/css/radiobutton.css.map +0 -1
  119. package/css/reset.css +0 -46
  120. package/css/reset.css.map +0 -1
  121. package/css/scrollbar.css +0 -91
  122. package/css/scrollbar.css.map +0 -1
  123. package/css/spinner.css +0 -118
  124. package/css/spinner.css.map +0 -1
  125. package/css/switch.css +0 -66
  126. package/css/switch.css.map +0 -1
  127. package/css/table.css +0 -201
  128. package/css/table.css.map +0 -1
  129. package/css/tabs.css +0 -135
  130. package/css/tabs.css.map +0 -1
  131. package/css/timeline.css +0 -69
  132. package/css/timeline.css.map +0 -1
  133. package/css/toast.css +0 -98
  134. package/css/toast.css.map +0 -1
  135. package/css/tooltip.css +0 -151
  136. package/css/tooltip.css.map +0 -1
  137. package/css/tree.css +0 -199
  138. package/css/tree.css.map +0 -1
  139. package/css/typography.css +0 -137
  140. package/css/typography.css.map +0 -1
  141. package/css/virtual-dropdown.css +0 -149
  142. package/css/virtual-dropdown.css.map +0 -1
package/js/editor.js CHANGED
@@ -1,421 +1,404 @@
1
- class Editor {
2
- constructor() {
3
- this.undoStack = [];
4
- this.redoStack = [];
5
-
6
- const editable = document.getElementById('editable');
7
- const code = document.getElementById('code');
8
- const preview = document.getElementById('preview');
9
- const sidePanel = document.getElementById('sidePanel');
10
- const wordCount = document.getElementById('wordCount');
11
-
12
- if (!editable || !code || !preview || !sidePanel) {
13
- throw new Error('Editor: Required elements not found');
14
- }
15
-
16
- this.editable = editable;
17
- this.code = code;
18
- this.preview = preview;
19
- this.sidePanel = sidePanel;
20
- this.wordCount = wordCount;
21
-
22
- this.bindToolbar();
23
- this.bindActions();
24
- this.bindKeyboard();
25
- this.bindEditable();
26
- this.bindTabs();
27
- this.syncViews();
28
- this.saveState();
29
-
30
- // Start with side panel hidden
31
- this.sidePanel.classList.add('hidden');
32
- }
33
-
34
- bindToolbar() {
35
- document.querySelectorAll('[data-cmd]').forEach(btn => {
36
- btn.addEventListener('click', () => {
37
- const cmd = btn.dataset.cmd;
38
- const val = btn.dataset.value ?? null;
39
- this.exec(cmd, val);
40
- this.editable.focus();
41
- });
42
- });
43
- }
44
-
45
- bindActions() {
46
- document.getElementById('linkBtn')?.addEventListener('click', () => {
47
- const url = prompt('Enter URL:', 'https://');
48
- if (url) this.exec('createLink', url);
49
- });
50
-
51
- const imageFile = document.getElementById('imageFile');
52
- document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
53
- imageFile?.addEventListener('change', () => {
54
- const file = imageFile.files?.[0];
55
- if (!file) return;
56
- const reader = new FileReader();
57
- reader.onload = () => {
58
- if (typeof reader.result === 'string') {
59
- this.insertImage(reader.result);
60
- }
61
- };
62
- reader.readAsDataURL(file);
63
- imageFile.value = '';
64
- });
65
-
66
- document.getElementById('cleanBtn')?.addEventListener('click', () => {
67
- const sel = window.getSelection();
68
- if (!sel || sel.rangeCount === 0) return;
69
- const range = sel.getRangeAt(0);
70
- const text = range.toString();
71
- range.deleteContents();
72
- range.insertNode(document.createTextNode(text));
73
- this.onContentChange();
74
- });
75
-
76
- document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
77
- document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
78
-
79
- document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
80
- this.sidePanel.classList.toggle('hidden');
81
- this.syncViews();
82
- });
83
-
84
- // Code action buttons — matched by position within .code-actions
85
- const codeActions = document.querySelectorAll('.code-actions button');
86
- codeActions[0]?.addEventListener('click', () => {
87
- this.editable.innerHTML = this.sanitizeHTML(this.code.value);
88
- this.onContentChange();
89
- });
90
- codeActions[1]?.addEventListener('click', () => {
91
- this.code.value = this.sanitizeHTML(this.code.value);
92
- this.editable.innerHTML = this.code.value;
93
- this.onContentChange();
94
- });
95
- codeActions[2]?.addEventListener('click', () => {
96
- this.code.value = this.code.value
97
- .replace(/\n/g, '')
98
- .replace(/>\s+</g, '><')
99
- .trim();
100
- });
101
-
102
- const saveBtn = document.getElementById('saveBtn');
103
- saveBtn?.addEventListener('click', () => this.downloadHTML());
104
-
105
- document.getElementById('clearBtn')?.addEventListener('click', () => {
106
- if (confirm('Clear all content?')) {
107
- this.editable.innerHTML = '';
108
- this.onContentChange();
109
- }
110
- });
111
- }
112
-
113
- bindKeyboard() {
114
- const saveBtn = document.getElementById('saveBtn');
115
-
116
- window.addEventListener('keydown', (e) => {
117
- const mod = e.ctrlKey || e.metaKey;
118
- if (!mod) return;
119
-
120
- const key = e.key.toLowerCase();
121
-
122
- if (key === 'b') { e.preventDefault(); this.exec('bold'); }
123
- else if (key === 'i') { e.preventDefault(); this.exec('italic'); }
124
- else if (key === 'u') { e.preventDefault(); this.exec('underline'); }
125
- else if (key === 'k') {
126
- e.preventDefault();
127
- const url = prompt('Enter URL:', 'https://');
128
- if (url) this.exec('createLink', url);
129
- }
130
- else if (key === 's') { e.preventDefault(); saveBtn?.click(); }
131
- else if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); }
132
- else if (key === 'y' || (key === 'z' && e.shiftKey)) { e.preventDefault(); this.redo(); }
133
- });
134
- }
135
-
136
- bindEditable() {
137
- this.editable.addEventListener('input', () => this.onContentChange());
138
-
139
- this.editable.addEventListener('paste', (e) => {
140
- e.preventDefault();
141
- const text = e.clipboardData?.getData('text/plain') ?? '';
142
- this.insertText(text);
143
- });
144
-
145
- this.editable.addEventListener('keyup', () => this.refreshActiveState());
146
- this.editable.addEventListener('mouseup', () => this.refreshActiveState());
147
- }
148
-
149
- bindTabs() {
150
- document.querySelectorAll('.side-tab[data-tab]').forEach(tab => {
151
- tab.addEventListener('click', () => {
152
- const targetId = tab.dataset.tab;
153
-
154
- document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
155
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
156
-
157
- tab.classList.add('active');
158
- document.getElementById(targetId)?.classList.add('active');
159
- });
160
- });
161
- }
162
-
163
- onContentChange() {
164
- this.saveState();
165
- this.syncViews();
166
- }
167
-
168
- syncViews() {
169
- this.code.value = this.editable.innerHTML.trim();
170
- this.preview.innerHTML = this.editable.innerHTML;
171
- this.updateWordCount();
172
- }
173
-
174
- updateWordCount() {
175
- if (!this.wordCount) return;
176
- const text = this.editable.innerText || '';
177
- const words = text.trim().split(/\s+/).filter(w => w.length > 0);
178
- const count = words.length;
179
- this.wordCount.textContent = `${count} word${count !== 1 ? 's' : ''}`;
180
- }
181
-
182
- saveState() {
183
- this.undoStack.push(this.editable.innerHTML);
184
- if (this.undoStack.length > 100) this.undoStack.shift();
185
- this.redoStack = [];
186
- }
187
-
188
- undo() {
189
- if (this.undoStack.length <= 1) return;
190
- this.redoStack.push(this.undoStack.pop());
191
- this.editable.innerHTML = this.undoStack[this.undoStack.length - 1];
192
- this.syncViews();
193
- }
194
-
195
- redo() {
196
- if (this.redoStack.length === 0) return;
197
- const state = this.redoStack.pop();
198
- this.undoStack.push(state);
199
- this.editable.innerHTML = state;
200
- this.syncViews();
201
- }
202
-
203
- exec(command, value = null) {
204
- switch (command) {
205
- case 'bold': this.toggleInlineStyle('strong'); break;
206
- case 'italic': this.toggleInlineStyle('em'); break;
207
- case 'underline': this.toggleInlineStyle('u'); break;
208
- case 'strikeThrough': this.toggleInlineStyle('s'); break;
209
- case 'createLink': if (value) this.createLink(value); break;
210
- case 'formatBlock': if (value) this.formatBlock(value); break;
211
- case 'insertUnorderedList': this.insertList('ul'); break;
212
- case 'insertOrderedList': this.insertList('ol'); break;
213
- }
214
- }
215
-
216
- insertText(text) {
217
- const sel = window.getSelection();
218
- if (!sel || sel.rangeCount === 0) return;
219
-
220
- const range = sel.getRangeAt(0);
221
- range.deleteContents();
222
- range.insertNode(document.createTextNode(text));
223
- range.collapse(false);
224
- sel.removeAllRanges();
225
- sel.addRange(range);
226
-
227
- this.onContentChange();
228
- }
229
-
230
- insertImage(dataUrl) {
231
- const sel = window.getSelection();
232
- if (!sel || sel.rangeCount === 0) return;
233
-
234
- const range = sel.getRangeAt(0);
235
- const img = document.createElement('img');
236
- img.src = dataUrl;
237
- img.style.maxWidth = '100%';
238
- range.deleteContents();
239
- range.insertNode(img);
240
-
241
- range.setStartAfter(img);
242
- range.collapse(true);
243
- sel.removeAllRanges();
244
- sel.addRange(range);
245
-
246
- this.onContentChange();
247
- }
248
-
249
- toggleInlineStyle(tagName) {
250
- const sel = window.getSelection();
251
- if (!sel || sel.rangeCount === 0) return;
252
-
253
- const range = sel.getRangeAt(0);
254
- const container = range.commonAncestorContainer;
255
- let current = container.nodeType === Node.TEXT_NODE
256
- ? container.parentElement
257
- : container;
258
-
259
- let wrapper = null;
260
- while (current && current !== this.editable) {
261
- if (current.tagName === tagName.toUpperCase()) {
262
- wrapper = current;
263
- break;
264
- }
265
- current = current.parentElement;
266
- }
267
-
268
- if (wrapper) {
269
- const parent = wrapper.parentNode;
270
- while (wrapper.firstChild) {
271
- parent?.insertBefore(wrapper.firstChild, wrapper);
272
- }
273
- parent?.removeChild(wrapper);
274
- } else {
275
- const contents = range.extractContents();
276
- const el = document.createElement(tagName);
277
- el.appendChild(contents);
278
- range.insertNode(el);
279
- range.selectNodeContents(el);
280
- sel.removeAllRanges();
281
- sel.addRange(range);
282
- }
283
-
284
- this.onContentChange();
285
- }
286
-
287
- createLink(url) {
288
- const sel = window.getSelection();
289
- if (!sel || sel.rangeCount === 0) return;
290
-
291
- const range = sel.getRangeAt(0);
292
- const contents = range.extractContents();
293
- const link = document.createElement('a');
294
- link.href = url;
295
- link.appendChild(contents);
296
- range.insertNode(link);
297
-
298
- this.onContentChange();
299
- }
300
-
301
- formatBlock(tag) {
302
- const sel = window.getSelection();
303
- if (!sel || sel.rangeCount === 0) return;
304
-
305
- const range = sel.getRangeAt(0);
306
- const container = range.commonAncestorContainer;
307
- let blockElement = container.nodeType === Node.TEXT_NODE
308
- ? container.parentElement
309
- : container;
310
-
311
- while (blockElement && blockElement !== this.editable && blockElement.parentElement !== this.editable) {
312
- blockElement = blockElement.parentElement;
313
- }
314
-
315
- if (blockElement && blockElement !== this.editable) {
316
- const newBlock = document.createElement(tag);
317
- newBlock.innerHTML = blockElement.innerHTML;
318
- blockElement.parentNode?.replaceChild(newBlock, blockElement);
319
- this.onContentChange();
320
- }
321
- }
322
-
323
- insertList(listTag) {
324
- const sel = window.getSelection();
325
- if (!sel || sel.rangeCount === 0) return;
326
-
327
- const range = sel.getRangeAt(0);
328
- const text = range.toString();
329
-
330
- const list = document.createElement(listTag);
331
- const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
332
-
333
- for (const line of lines) {
334
- const li = document.createElement('li');
335
- li.textContent = line.trim() || '\u200B';
336
- list.appendChild(li);
337
- }
338
-
339
- range.deleteContents();
340
- range.insertNode(list);
341
-
342
- const lastLi = list.lastElementChild;
343
- if (lastLi) {
344
- range.setStart(lastLi, lastLi.childNodes.length);
345
- range.collapse(true);
346
- sel.removeAllRanges();
347
- sel.addRange(range);
348
- }
349
-
350
- this.onContentChange();
351
- }
352
-
353
- sanitizeHTML(html) {
354
- const parser = new DOMParser();
355
- const doc = parser.parseFromString(html, 'text/html');
356
-
357
- doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
358
-
359
- doc.querySelectorAll('*').forEach(el => {
360
- for (const attr of Array.from(el.attributes)) {
361
- if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
362
- el.removeAttribute(attr.name);
363
- }
364
- }
365
- });
366
-
367
- return doc.body.innerHTML;
368
- }
369
-
370
- downloadHTML() {
371
- const content = this.sanitizeHTML(this.editable.innerHTML);
1
+ class Editor {
2
+ constructor() {
3
+ this.undoStack = [];
4
+ this.redoStack = [];
5
+ const editable = document.getElementById('editable');
6
+ const code = document.getElementById('code');
7
+ const preview = document.getElementById('preview');
8
+ const sidePanel = document.getElementById('sidePanel');
9
+ const wordCount = document.getElementById('wordCount');
10
+ if (!editable || !code || !preview || !sidePanel) {
11
+ throw new Error('Editor: Required elements not found');
12
+ }
13
+ this.editable = editable;
14
+ this.code = code;
15
+ this.preview = preview;
16
+ this.sidePanel = sidePanel;
17
+ this.wordCount = wordCount;
18
+ this.bindToolbar();
19
+ this.bindActions();
20
+ this.bindKeyboard();
21
+ this.bindEditable();
22
+ this.bindTabs();
23
+ this.syncViews();
24
+ this.saveState();
25
+ // Start with side panel hidden
26
+ this.sidePanel.classList.add('hidden');
27
+ }
28
+ bindToolbar() {
29
+ document.querySelectorAll('[data-cmd]').forEach(btn => {
30
+ btn.addEventListener('click', () => {
31
+ const cmd = btn.dataset.cmd;
32
+ const val = btn.dataset.value ?? null;
33
+ this.exec(cmd, val);
34
+ this.editable.focus();
35
+ });
36
+ });
37
+ }
38
+ bindActions() {
39
+ document.getElementById('linkBtn')?.addEventListener('click', () => {
40
+ const url = prompt('Enter URL:', 'https://');
41
+ if (url)
42
+ this.exec('createLink', url);
43
+ });
44
+ const imageFile = document.getElementById('imageFile');
45
+ document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
46
+ imageFile?.addEventListener('change', () => {
47
+ const file = imageFile.files?.[0];
48
+ if (!file)
49
+ return;
50
+ const reader = new FileReader();
51
+ reader.onload = () => {
52
+ if (typeof reader.result === 'string') {
53
+ this.insertImage(reader.result);
54
+ }
55
+ };
56
+ reader.readAsDataURL(file);
57
+ imageFile.value = '';
58
+ });
59
+ document.getElementById('cleanBtn')?.addEventListener('click', () => {
60
+ const sel = window.getSelection();
61
+ if (!sel || sel.rangeCount === 0)
62
+ return;
63
+ const range = sel.getRangeAt(0);
64
+ const text = range.toString();
65
+ range.deleteContents();
66
+ range.insertNode(document.createTextNode(text));
67
+ this.onContentChange();
68
+ });
69
+ document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
70
+ document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
71
+ document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
72
+ this.sidePanel.classList.toggle('hidden');
73
+ this.syncViews();
74
+ });
75
+ // Code action buttons — matched by position within .code-actions
76
+ const codeActions = document.querySelectorAll('.code-actions button');
77
+ codeActions[0]?.addEventListener('click', () => {
78
+ this.editable.innerHTML = this.sanitizeHTML(this.code.value);
79
+ this.onContentChange();
80
+ });
81
+ codeActions[1]?.addEventListener('click', () => {
82
+ this.code.value = this.sanitizeHTML(this.code.value);
83
+ this.editable.innerHTML = this.code.value;
84
+ this.onContentChange();
85
+ });
86
+ codeActions[2]?.addEventListener('click', () => {
87
+ this.code.value = this.code.value
88
+ .replace(/\n/g, '')
89
+ .replace(/>\s+</g, '><')
90
+ .trim();
91
+ });
92
+ const saveBtn = document.getElementById('saveBtn');
93
+ saveBtn?.addEventListener('click', () => this.downloadHTML());
94
+ document.getElementById('clearBtn')?.addEventListener('click', () => {
95
+ if (confirm('Clear all content?')) {
96
+ this.editable.innerHTML = '';
97
+ this.onContentChange();
98
+ }
99
+ });
100
+ }
101
+ bindKeyboard() {
102
+ const saveBtn = document.getElementById('saveBtn');
103
+ window.addEventListener('keydown', (e) => {
104
+ const mod = e.ctrlKey || e.metaKey;
105
+ if (!mod)
106
+ return;
107
+ const key = e.key.toLowerCase();
108
+ if (key === 'b') {
109
+ e.preventDefault();
110
+ this.exec('bold');
111
+ }
112
+ else if (key === 'i') {
113
+ e.preventDefault();
114
+ this.exec('italic');
115
+ }
116
+ else if (key === 'u') {
117
+ e.preventDefault();
118
+ this.exec('underline');
119
+ }
120
+ else if (key === 'k') {
121
+ e.preventDefault();
122
+ const url = prompt('Enter URL:', 'https://');
123
+ if (url)
124
+ this.exec('createLink', url);
125
+ }
126
+ else if (key === 's') {
127
+ e.preventDefault();
128
+ saveBtn?.click();
129
+ }
130
+ else if (key === 'z' && !e.shiftKey) {
131
+ e.preventDefault();
132
+ this.undo();
133
+ }
134
+ else if (key === 'y' || (key === 'z' && e.shiftKey)) {
135
+ e.preventDefault();
136
+ this.redo();
137
+ }
138
+ });
139
+ }
140
+ bindEditable() {
141
+ this.editable.addEventListener('input', () => this.onContentChange());
142
+ this.editable.addEventListener('paste', (e) => {
143
+ e.preventDefault();
144
+ const text = e.clipboardData?.getData('text/plain') ?? '';
145
+ this.insertText(text);
146
+ });
147
+ this.editable.addEventListener('keyup', () => this.refreshActiveState());
148
+ this.editable.addEventListener('mouseup', () => this.refreshActiveState());
149
+ }
150
+ bindTabs() {
151
+ document.querySelectorAll('.side-tab[data-tab]').forEach(tab => {
152
+ tab.addEventListener('click', () => {
153
+ const targetId = tab.dataset.tab;
154
+ document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
155
+ document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
156
+ tab.classList.add('active');
157
+ document.getElementById(targetId)?.classList.add('active');
158
+ });
159
+ });
160
+ }
161
+ onContentChange() {
162
+ this.saveState();
163
+ this.syncViews();
164
+ }
165
+ syncViews() {
166
+ this.code.value = this.editable.innerHTML.trim();
167
+ this.preview.innerHTML = this.editable.innerHTML;
168
+ this.updateWordCount();
169
+ }
170
+ updateWordCount() {
171
+ if (!this.wordCount)
172
+ return;
173
+ const text = this.editable.innerText || '';
174
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0);
175
+ const count = words.length;
176
+ this.wordCount.textContent = `${count} word${count !== 1 ? 's' : ''}`;
177
+ }
178
+ saveState() {
179
+ this.undoStack.push(this.editable.innerHTML);
180
+ if (this.undoStack.length > 100)
181
+ this.undoStack.shift();
182
+ this.redoStack = [];
183
+ }
184
+ undo() {
185
+ if (this.undoStack.length <= 1)
186
+ return;
187
+ this.redoStack.push(this.undoStack.pop());
188
+ this.editable.innerHTML = this.undoStack[this.undoStack.length - 1];
189
+ this.syncViews();
190
+ }
191
+ redo() {
192
+ if (this.redoStack.length === 0)
193
+ return;
194
+ const state = this.redoStack.pop();
195
+ this.undoStack.push(state);
196
+ this.editable.innerHTML = state;
197
+ this.syncViews();
198
+ }
199
+ exec(command, value = null) {
200
+ switch (command) {
201
+ case 'bold':
202
+ this.toggleInlineStyle('strong');
203
+ break;
204
+ case 'italic':
205
+ this.toggleInlineStyle('em');
206
+ break;
207
+ case 'underline':
208
+ this.toggleInlineStyle('u');
209
+ break;
210
+ case 'strikeThrough':
211
+ this.toggleInlineStyle('s');
212
+ break;
213
+ case 'createLink':
214
+ if (value)
215
+ this.createLink(value);
216
+ break;
217
+ case 'formatBlock':
218
+ if (value)
219
+ this.formatBlock(value);
220
+ break;
221
+ case 'insertUnorderedList':
222
+ this.insertList('ul');
223
+ break;
224
+ case 'insertOrderedList':
225
+ this.insertList('ol');
226
+ break;
227
+ }
228
+ }
229
+ insertText(text) {
230
+ const sel = window.getSelection();
231
+ if (!sel || sel.rangeCount === 0)
232
+ return;
233
+ const range = sel.getRangeAt(0);
234
+ range.deleteContents();
235
+ range.insertNode(document.createTextNode(text));
236
+ range.collapse(false);
237
+ sel.removeAllRanges();
238
+ sel.addRange(range);
239
+ this.onContentChange();
240
+ }
241
+ insertImage(dataUrl) {
242
+ const sel = window.getSelection();
243
+ if (!sel || sel.rangeCount === 0)
244
+ return;
245
+ const range = sel.getRangeAt(0);
246
+ const img = document.createElement('img');
247
+ img.src = dataUrl;
248
+ img.style.maxWidth = '100%';
249
+ range.deleteContents();
250
+ range.insertNode(img);
251
+ range.setStartAfter(img);
252
+ range.collapse(true);
253
+ sel.removeAllRanges();
254
+ sel.addRange(range);
255
+ this.onContentChange();
256
+ }
257
+ toggleInlineStyle(tagName) {
258
+ const sel = window.getSelection();
259
+ if (!sel || sel.rangeCount === 0)
260
+ return;
261
+ const range = sel.getRangeAt(0);
262
+ const container = range.commonAncestorContainer;
263
+ let current = container.nodeType === Node.TEXT_NODE
264
+ ? container.parentElement
265
+ : container;
266
+ let wrapper = null;
267
+ while (current && current !== this.editable) {
268
+ if (current.tagName === tagName.toUpperCase()) {
269
+ wrapper = current;
270
+ break;
271
+ }
272
+ current = current.parentElement;
273
+ }
274
+ if (wrapper) {
275
+ const parent = wrapper.parentNode;
276
+ while (wrapper.firstChild) {
277
+ parent?.insertBefore(wrapper.firstChild, wrapper);
278
+ }
279
+ parent?.removeChild(wrapper);
280
+ }
281
+ else {
282
+ const contents = range.extractContents();
283
+ const el = document.createElement(tagName);
284
+ el.appendChild(contents);
285
+ range.insertNode(el);
286
+ range.selectNodeContents(el);
287
+ sel.removeAllRanges();
288
+ sel.addRange(range);
289
+ }
290
+ this.onContentChange();
291
+ }
292
+ createLink(url) {
293
+ const sel = window.getSelection();
294
+ if (!sel || sel.rangeCount === 0)
295
+ return;
296
+ const range = sel.getRangeAt(0);
297
+ const contents = range.extractContents();
298
+ const link = document.createElement('a');
299
+ link.href = url;
300
+ link.appendChild(contents);
301
+ range.insertNode(link);
302
+ this.onContentChange();
303
+ }
304
+ formatBlock(tag) {
305
+ const sel = window.getSelection();
306
+ if (!sel || sel.rangeCount === 0)
307
+ return;
308
+ const range = sel.getRangeAt(0);
309
+ const container = range.commonAncestorContainer;
310
+ let blockElement = container.nodeType === Node.TEXT_NODE
311
+ ? container.parentElement
312
+ : container;
313
+ while (blockElement && blockElement !== this.editable && blockElement.parentElement !== this.editable) {
314
+ blockElement = blockElement.parentElement;
315
+ }
316
+ if (blockElement && blockElement !== this.editable) {
317
+ const newBlock = document.createElement(tag);
318
+ newBlock.innerHTML = blockElement.innerHTML;
319
+ blockElement.parentNode?.replaceChild(newBlock, blockElement);
320
+ this.onContentChange();
321
+ }
322
+ }
323
+ insertList(listTag) {
324
+ const sel = window.getSelection();
325
+ if (!sel || sel.rangeCount === 0)
326
+ return;
327
+ const range = sel.getRangeAt(0);
328
+ const text = range.toString();
329
+ const list = document.createElement(listTag);
330
+ const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
331
+ for (const line of lines) {
332
+ const li = document.createElement('li');
333
+ li.textContent = line.trim() || '\u200B';
334
+ list.appendChild(li);
335
+ }
336
+ range.deleteContents();
337
+ range.insertNode(list);
338
+ const lastLi = list.lastElementChild;
339
+ if (lastLi) {
340
+ range.setStart(lastLi, lastLi.childNodes.length);
341
+ range.collapse(true);
342
+ sel.removeAllRanges();
343
+ sel.addRange(range);
344
+ }
345
+ this.onContentChange();
346
+ }
347
+ sanitizeHTML(html) {
348
+ const parser = new DOMParser();
349
+ const doc = parser.parseFromString(html, 'text/html');
350
+ doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
351
+ doc.querySelectorAll('*').forEach(el => {
352
+ for (const attr of Array.from(el.attributes)) {
353
+ if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
354
+ el.removeAttribute(attr.name);
355
+ }
356
+ }
357
+ });
358
+ return doc.body.innerHTML;
359
+ }
360
+ downloadHTML() {
361
+ const content = this.sanitizeHTML(this.editable.innerHTML);
372
362
  const html = `<!doctype html>
373
363
  <html lang="en">
374
364
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
375
365
  <body>
376
366
  ${content}
377
367
  </body>
378
- </html>`;
379
- const blob = new Blob([html], { type: 'text/html' });
380
- const a = document.createElement('a');
381
- a.href = URL.createObjectURL(blob);
382
- a.download = 'document.html';
383
- a.click();
384
- URL.revokeObjectURL(a.href);
385
- }
386
-
387
- refreshActiveState() {
388
- const sel = window.getSelection();
389
- if (!sel || sel.rangeCount === 0) return;
390
-
391
- const range = sel.getRangeAt(0);
392
- const container = range.commonAncestorContainer;
393
- const element = container.nodeType === Node.TEXT_NODE
394
- ? container.parentElement
395
- : container;
396
-
397
- document.querySelectorAll('[data-cmd]').forEach(btn => {
398
- const cmd = btn.dataset.cmd;
399
- let active = false;
400
-
401
- let current = element;
402
- while (current && current !== this.editable) {
403
- const tag = current.tagName?.toLowerCase();
404
- if (
405
- (cmd === 'bold' && (tag === 'strong' || tag === 'b')) ||
406
- (cmd === 'italic' && (tag === 'em' || tag === 'i')) ||
407
- (cmd === 'underline' && tag === 'u') ||
408
- (cmd === 'strikeThrough' && tag === 's')
409
- ) {
410
- active = true;
411
- break;
412
- }
413
- current = current.parentElement;
414
- }
415
-
416
- btn.classList.toggle('active', active);
417
- });
418
- }
419
- }
420
-
421
- export { Editor };
368
+ </html>`;
369
+ const blob = new Blob([html], { type: 'text/html' });
370
+ const a = document.createElement('a');
371
+ a.href = URL.createObjectURL(blob);
372
+ a.download = 'document.html';
373
+ a.click();
374
+ URL.revokeObjectURL(a.href);
375
+ }
376
+ refreshActiveState() {
377
+ const sel = window.getSelection();
378
+ if (!sel || sel.rangeCount === 0)
379
+ return;
380
+ const range = sel.getRangeAt(0);
381
+ const container = range.commonAncestorContainer;
382
+ const element = container.nodeType === Node.TEXT_NODE
383
+ ? container.parentElement
384
+ : container;
385
+ document.querySelectorAll('[data-cmd]').forEach(btn => {
386
+ const cmd = btn.dataset.cmd;
387
+ let active = false;
388
+ let current = element;
389
+ while (current && current !== this.editable) {
390
+ const tag = current.tagName?.toLowerCase();
391
+ if ((cmd === 'bold' && (tag === 'strong' || tag === 'b')) ||
392
+ (cmd === 'italic' && (tag === 'em' || tag === 'i')) ||
393
+ (cmd === 'underline' && tag === 'u') ||
394
+ (cmd === 'strikeThrough' && tag === 's')) {
395
+ active = true;
396
+ break;
397
+ }
398
+ current = current.parentElement;
399
+ }
400
+ btn.classList.toggle('active', active);
401
+ });
402
+ }
403
+ }
404
+ export { Editor };