@dodlhuat/basix 1.1.1 → 1.2.1

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 (352) hide show
  1. package/README.md +706 -482
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/badge.scss +104 -0
  5. package/css/bottom-sheet.scss +192 -0
  6. package/css/breadcrumb.scss +158 -0
  7. package/css/button.scss +48 -0
  8. package/css/calendar.scss +957 -0
  9. package/css/card.scss +65 -65
  10. package/css/chart.scss +270 -157
  11. package/css/chat-bubbles.scss +134 -68
  12. package/css/chips.scss +109 -19
  13. package/css/colors.scss +32 -32
  14. package/css/context-menu.scss +182 -0
  15. package/css/datepicker.scss +336 -336
  16. package/css/defaults.scss +90 -90
  17. package/css/docs.scss +529 -0
  18. package/css/editor.scss +664 -461
  19. package/css/file-uploader.scss +1 -1
  20. package/css/flyout-menu.scss +361 -361
  21. package/css/form.scss +124 -0
  22. package/css/gallery.scss +65 -6
  23. package/css/grid.scss +41 -40
  24. package/css/group-picker.scss +345 -0
  25. package/css/guitar-chords.css +250 -250
  26. package/css/icons.scss +330 -330
  27. package/css/parameters.scss +3 -3
  28. package/css/placeholder.scss +33 -33
  29. package/css/popover.scss +206 -0
  30. package/css/progress.scss +76 -32
  31. package/css/properties.scss +51 -36
  32. package/css/push-menu.scss +302 -174
  33. package/css/reset.scss +39 -39
  34. package/css/scrollbar.scss +62 -5
  35. package/css/sidebar-nav.scss +92 -0
  36. package/css/spinner.scss +65 -65
  37. package/css/stepper.scss +248 -0
  38. package/css/style.css +4603 -273
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.scss +51 -39
  42. package/css/table.scss +199 -199
  43. package/css/tabs.scss +154 -123
  44. package/css/timeline.scss +83 -38
  45. package/css/timepicker.scss +100 -5
  46. package/css/toast.scss +81 -81
  47. package/css/typography.scss +194 -161
  48. package/css/virtual-dropdown.scss +35 -29
  49. package/js/bottom-sheet.js +173 -0
  50. package/js/bottom-sheet.ts +222 -0
  51. package/js/calendar.js +532 -0
  52. package/js/calendar.ts +706 -0
  53. package/js/carousel.js +26 -13
  54. package/js/chart.js +573 -257
  55. package/js/chart.ts +692 -0
  56. package/js/code-viewer.js +10 -10
  57. package/js/code-viewer.ts +188 -188
  58. package/js/context-menu.js +212 -0
  59. package/js/context-menu.ts +252 -0
  60. package/js/datepicker.ts +627 -627
  61. package/js/docs-nav.js +204 -0
  62. package/js/dropdown.ts +179 -179
  63. package/js/editor.js +96 -38
  64. package/js/editor.ts +483 -425
  65. package/js/file-uploader.js +1 -0
  66. package/js/file-uploader.ts +1 -0
  67. package/js/flyout-menu.js +14 -14
  68. package/js/flyout-menu.ts +249 -249
  69. package/js/form-builder.js +106 -106
  70. package/js/gallery.js +13 -6
  71. package/js/gallery.ts +245 -236
  72. package/js/group-picker.js +342 -0
  73. package/js/group-picker.ts +447 -0
  74. package/js/guitar-chords.js +268 -268
  75. package/js/lazy-loader.js +121 -121
  76. package/js/modal.ts +166 -166
  77. package/js/popover.js +163 -0
  78. package/js/popover.ts +219 -0
  79. package/js/position.js +108 -0
  80. package/js/position.ts +111 -0
  81. package/js/push-menu.js +226 -113
  82. package/js/push-menu.ts +284 -145
  83. package/js/request.js +50 -50
  84. package/js/scroll.ts +47 -47
  85. package/js/scrollbar.js +13 -0
  86. package/js/scrollbar.ts +324 -307
  87. package/js/select.ts +216 -216
  88. package/js/sidebar-nav.js +41 -0
  89. package/js/sidebar-nav.ts +66 -0
  90. package/js/stepper.js +80 -0
  91. package/js/stepper.ts +104 -0
  92. package/js/table.ts +452 -452
  93. package/js/tabs.ts +279 -279
  94. package/js/theme.js +17 -6
  95. package/js/theme.ts +234 -224
  96. package/js/timepicker.js +21 -8
  97. package/js/toast.ts +137 -137
  98. package/js/tooltip.js +6 -60
  99. package/js/tooltip.ts +184 -251
  100. package/js/tsconfig.json +18 -18
  101. package/js/utils.ts +83 -83
  102. package/js/virtual-dropdown.js +25 -25
  103. package/js/virtual-dropdown.ts +365 -365
  104. package/package.json +39 -39
  105. package/fonts/Outfit-VariableFont_wght.woff +0 -0
  106. package/fonts/material-icons.woff2 +0 -0
  107. package/icons/activity-outline.svg +0 -1
  108. package/icons/alert-circle-outline.svg +0 -1
  109. package/icons/alert-triangle-outline.svg +0 -1
  110. package/icons/archive-outline.svg +0 -1
  111. package/icons/arrow-back-outline.svg +0 -1
  112. package/icons/arrow-circle-down-outline.svg +0 -1
  113. package/icons/arrow-circle-left-outline.svg +0 -1
  114. package/icons/arrow-circle-right-outline.svg +0 -1
  115. package/icons/arrow-circle-up-outline.svg +0 -1
  116. package/icons/arrow-down-outline.svg +0 -1
  117. package/icons/arrow-downward-outline.svg +0 -1
  118. package/icons/arrow-forward-outline.svg +0 -1
  119. package/icons/arrow-ios-back-outline.svg +0 -1
  120. package/icons/arrow-ios-downward-outline.svg +0 -1
  121. package/icons/arrow-ios-forward-outline.svg +0 -1
  122. package/icons/arrow-ios-upward-outline.svg +0 -1
  123. package/icons/arrow-left-outline.svg +0 -1
  124. package/icons/arrow-right-outline.svg +0 -1
  125. package/icons/arrow-up-outline.svg +0 -1
  126. package/icons/arrow-upward-outline.svg +0 -1
  127. package/icons/arrowhead-down-outline.svg +0 -1
  128. package/icons/arrowhead-left-outline.svg +0 -1
  129. package/icons/arrowhead-right-outline.svg +0 -1
  130. package/icons/arrowhead-up-outline.svg +0 -1
  131. package/icons/at-outline.svg +0 -1
  132. package/icons/attach-2-outline.svg +0 -1
  133. package/icons/attach-outline.svg +0 -1
  134. package/icons/award-outline.svg +0 -1
  135. package/icons/backspace-outline.svg +0 -1
  136. package/icons/bar-chart-2-outline.svg +0 -1
  137. package/icons/bar-chart-outline.svg +0 -1
  138. package/icons/battery-outline.svg +0 -1
  139. package/icons/behance-outline.svg +0 -1
  140. package/icons/bell-off-outline.svg +0 -1
  141. package/icons/bell-outline.svg +0 -1
  142. package/icons/bluetooth-outline.svg +0 -1
  143. package/icons/book-open-outline.svg +0 -1
  144. package/icons/book-outline.svg +0 -1
  145. package/icons/bookmark-outline.svg +0 -1
  146. package/icons/briefcase-outline.svg +0 -1
  147. package/icons/browser-outline.svg +0 -1
  148. package/icons/brush-outline.svg +0 -1
  149. package/icons/bulb-outline.svg +0 -1
  150. package/icons/calendar-outline.svg +0 -1
  151. package/icons/camera-outline.svg +0 -1
  152. package/icons/car-outline.svg +0 -1
  153. package/icons/cast-outline.svg +0 -1
  154. package/icons/charging-outline.svg +0 -1
  155. package/icons/checkmark-circle-2-outline.svg +0 -1
  156. package/icons/checkmark-circle-outline.svg +0 -1
  157. package/icons/checkmark-outline.svg +0 -1
  158. package/icons/checkmark-square-2-outline.svg +0 -1
  159. package/icons/checkmark-square-outline.svg +0 -1
  160. package/icons/chevron-down-outline.svg +0 -1
  161. package/icons/chevron-left-outline.svg +0 -1
  162. package/icons/chevron-right-outline.svg +0 -1
  163. package/icons/chevron-up-outline.svg +0 -1
  164. package/icons/clipboard-outline.svg +0 -1
  165. package/icons/clock-outline.svg +0 -1
  166. package/icons/close-circle-outline.svg +0 -1
  167. package/icons/close-outline.svg +0 -1
  168. package/icons/close-square-outline.svg +0 -1
  169. package/icons/cloud-download-outline.svg +0 -1
  170. package/icons/cloud-upload-outline.svg +0 -1
  171. package/icons/code-download-outline.svg +0 -1
  172. package/icons/code-outline.svg +0 -1
  173. package/icons/collapse-outline.svg +0 -1
  174. package/icons/color-palette-outline.svg +0 -1
  175. package/icons/color-picker-outline.svg +0 -1
  176. package/icons/compass-outline.svg +0 -1
  177. package/icons/copy-outline.svg +0 -1
  178. package/icons/corner-down-left-outline.svg +0 -1
  179. package/icons/corner-down-right-outline.svg +0 -1
  180. package/icons/corner-left-down-outline.svg +0 -1
  181. package/icons/corner-left-up-outline.svg +0 -1
  182. package/icons/corner-right-down-outline.svg +0 -1
  183. package/icons/corner-right-up-outline.svg +0 -1
  184. package/icons/corner-up-left-outline.svg +0 -1
  185. package/icons/corner-up-right-outline.svg +0 -1
  186. package/icons/credit-card-outline.svg +0 -1
  187. package/icons/crop-outline.svg +0 -1
  188. package/icons/cube-outline.svg +0 -1
  189. package/icons/diagonal-arrow-left-down-outline.svg +0 -1
  190. package/icons/diagonal-arrow-left-up-outline.svg +0 -1
  191. package/icons/diagonal-arrow-right-down-outline.svg +0 -1
  192. package/icons/diagonal-arrow-right-up-outline.svg +0 -1
  193. package/icons/done-all-outline.svg +0 -1
  194. package/icons/download-outline.svg +0 -1
  195. package/icons/droplet-off-outline.svg +0 -1
  196. package/icons/droplet-outline.svg +0 -1
  197. package/icons/edit-2-outline.svg +0 -1
  198. package/icons/edit-outline.svg +0 -1
  199. package/icons/email-outline.svg +0 -1
  200. package/icons/expand-outline.svg +0 -1
  201. package/icons/external-link-outline.svg +0 -1
  202. package/icons/eye-off-2-outline.svg +0 -1
  203. package/icons/eye-off-outline.svg +0 -1
  204. package/icons/eye-outline.svg +0 -1
  205. package/icons/facebook-outline.svg +0 -1
  206. package/icons/file-add-outline.svg +0 -1
  207. package/icons/file-outline.svg +0 -1
  208. package/icons/file-remove-outline.svg +0 -1
  209. package/icons/file-text-outline.svg +0 -1
  210. package/icons/film-outline.svg +0 -1
  211. package/icons/flag-outline.svg +0 -1
  212. package/icons/flash-off-outline.svg +0 -1
  213. package/icons/flash-outline.svg +0 -1
  214. package/icons/flip-2-outline.svg +0 -1
  215. package/icons/flip-outline.svg +0 -1
  216. package/icons/folder-add-outline.svg +0 -1
  217. package/icons/folder-outline.svg +0 -1
  218. package/icons/folder-remove-outline.svg +0 -1
  219. package/icons/funnel-outline.svg +0 -1
  220. package/icons/gift-outline.svg +0 -1
  221. package/icons/github-outline.svg +0 -1
  222. package/icons/globe-2-outline.svg +0 -1
  223. package/icons/globe-outline.svg +0 -1
  224. package/icons/google-outline.svg +0 -1
  225. package/icons/grid-outline.svg +0 -1
  226. package/icons/hard-drive-outline.svg +0 -1
  227. package/icons/hash-outline.svg +0 -1
  228. package/icons/headphones-outline.svg +0 -1
  229. package/icons/heart-outline.svg +0 -1
  230. package/icons/home-outline.svg +0 -1
  231. package/icons/image-outline.svg +0 -1
  232. package/icons/inbox-outline.svg +0 -1
  233. package/icons/info-outline.svg +0 -1
  234. package/icons/keypad-outline.svg +0 -1
  235. package/icons/layers-outline.svg +0 -1
  236. package/icons/layout-outline.svg +0 -1
  237. package/icons/link-2-outline.svg +0 -1
  238. package/icons/link-outline.svg +0 -1
  239. package/icons/linkedin-outline.svg +0 -1
  240. package/icons/list-outline.svg +0 -1
  241. package/icons/loader-outline.svg +0 -1
  242. package/icons/lock-outline.svg +0 -1
  243. package/icons/log-in-outline.svg +0 -1
  244. package/icons/log-out-outline.svg +0 -1
  245. package/icons/map-outline.svg +0 -1
  246. package/icons/maximize-outline.svg +0 -1
  247. package/icons/menu-2-outline.svg +0 -1
  248. package/icons/menu-arrow-outline.svg +0 -1
  249. package/icons/menu-outline.svg +0 -1
  250. package/icons/message-circle-outline.svg +0 -1
  251. package/icons/message-square-outline.svg +0 -1
  252. package/icons/mic-off-outline.svg +0 -1
  253. package/icons/mic-outline.svg +0 -1
  254. package/icons/minimize-outline.svg +0 -1
  255. package/icons/minus-circle-outline.svg +0 -1
  256. package/icons/minus-outline.svg +0 -1
  257. package/icons/minus-square-outline.svg +0 -1
  258. package/icons/monitor-outline.svg +0 -1
  259. package/icons/moon-outline.svg +0 -1
  260. package/icons/more-horizontal-outline.svg +0 -1
  261. package/icons/more-vertical-outline.svg +0 -1
  262. package/icons/move-outline.svg +0 -1
  263. package/icons/music-outline.svg +0 -1
  264. package/icons/navigation-2-outline.svg +0 -1
  265. package/icons/navigation-outline.svg +0 -1
  266. package/icons/npm-outline.svg +0 -1
  267. package/icons/options-2-outline.svg +0 -1
  268. package/icons/options-outline.svg +0 -1
  269. package/icons/pantone-outline.svg +0 -1
  270. package/icons/paper-plane-outline.svg +0 -1
  271. package/icons/pause-circle-outline.svg +0 -1
  272. package/icons/people-outline.svg +0 -1
  273. package/icons/percent-outline.svg +0 -1
  274. package/icons/person-add-outline.svg +0 -1
  275. package/icons/person-delete-outline.svg +0 -1
  276. package/icons/person-done-outline.svg +0 -1
  277. package/icons/person-outline.svg +0 -1
  278. package/icons/person-remove-outline.svg +0 -1
  279. package/icons/phone-call-outline.svg +0 -1
  280. package/icons/phone-missed-outline.svg +0 -1
  281. package/icons/phone-off-outline.svg +0 -1
  282. package/icons/phone-outline.svg +0 -1
  283. package/icons/pie-chart-outline.svg +0 -1
  284. package/icons/pin-outline.svg +0 -1
  285. package/icons/play-circle-outline.svg +0 -1
  286. package/icons/plus-circle-outline.svg +0 -1
  287. package/icons/plus-outline.svg +0 -1
  288. package/icons/plus-square-outline.svg +0 -1
  289. package/icons/power-outline.svg +0 -1
  290. package/icons/pricetags-outline.svg +0 -1
  291. package/icons/printer-outline.svg +0 -1
  292. package/icons/question-mark-circle-outline.svg +0 -1
  293. package/icons/question-mark-outline.svg +0 -1
  294. package/icons/radio-button-off-outline.svg +0 -1
  295. package/icons/radio-button-on-outline.svg +0 -1
  296. package/icons/radio-outline.svg +0 -1
  297. package/icons/recording-outline.svg +0 -1
  298. package/icons/refresh-outline.svg +0 -1
  299. package/icons/repeat-outline.svg +0 -1
  300. package/icons/rewind-left-outline.svg +0 -1
  301. package/icons/rewind-right-outline.svg +0 -1
  302. package/icons/save-outline.svg +0 -1
  303. package/icons/scissors-outline.svg +0 -1
  304. package/icons/search-outline.svg +0 -1
  305. package/icons/settings-2-outline.svg +0 -1
  306. package/icons/settings-outline.svg +0 -1
  307. package/icons/shake-outline.svg +0 -1
  308. package/icons/share-outline.svg +0 -1
  309. package/icons/shield-off-outline.svg +0 -1
  310. package/icons/shield-outline.svg +0 -1
  311. package/icons/shopping-bag-outline.svg +0 -1
  312. package/icons/shopping-cart-outline.svg +0 -1
  313. package/icons/shuffle-2-outline.svg +0 -1
  314. package/icons/shuffle-outline.svg +0 -1
  315. package/icons/skip-back-outline.svg +0 -1
  316. package/icons/skip-forward-outline.svg +0 -1
  317. package/icons/slash-outline.svg +0 -1
  318. package/icons/smartphone-outline.svg +0 -1
  319. package/icons/smiling-face-outline.svg +0 -1
  320. package/icons/speaker-outline.svg +0 -1
  321. package/icons/square-outline.svg +0 -1
  322. package/icons/star-outline.svg +0 -1
  323. package/icons/stop-circle-outline.svg +0 -1
  324. package/icons/sun-outline.svg +0 -1
  325. package/icons/swap-outline.svg +0 -1
  326. package/icons/sync-outline.svg +0 -1
  327. package/icons/text-outline.svg +0 -1
  328. package/icons/thermometer-minus-outline.svg +0 -1
  329. package/icons/thermometer-outline.svg +0 -1
  330. package/icons/thermometer-plus-outline.svg +0 -1
  331. package/icons/toggle-left-outline.svg +0 -1
  332. package/icons/toggle-right-outline.svg +0 -1
  333. package/icons/trash-2-outline.svg +0 -1
  334. package/icons/trash-outline.svg +0 -1
  335. package/icons/trending-down-outline.svg +0 -1
  336. package/icons/trending-up-outline.svg +0 -1
  337. package/icons/tv-outline.svg +0 -1
  338. package/icons/twitter-outline.svg +0 -1
  339. package/icons/umbrella-outline.svg +0 -1
  340. package/icons/undo-outline.svg +0 -1
  341. package/icons/unlock-outline.svg +0 -1
  342. package/icons/upload-outline.svg +0 -1
  343. package/icons/video-off-outline.svg +0 -1
  344. package/icons/video-outline.svg +0 -1
  345. package/icons/volume-down-outline.svg +0 -1
  346. package/icons/volume-mute-outline.svg +0 -1
  347. package/icons/volume-off-outline.svg +0 -1
  348. package/icons/volume-up-outline.svg +0 -1
  349. package/icons/wifi-off-outline.svg +0 -1
  350. package/icons/wifi-outline.svg +0 -1
  351. package/js/index.js +0 -718
  352. package/js/index.ts +0 -873
package/js/editor.ts CHANGED
@@ -1,426 +1,484 @@
1
- class Editor {
2
- private readonly editable: HTMLElement;
3
- private readonly code: HTMLTextAreaElement;
4
- private readonly preview: HTMLElement;
5
- private readonly sidePanel: HTMLElement;
6
- private readonly wordCount: HTMLElement | null;
7
- private undoStack: string[] = [];
8
- private redoStack: string[] = [];
9
-
10
- constructor() {
11
- const editable = document.getElementById('editable');
12
- const code = document.getElementById('code') as HTMLTextAreaElement;
13
- const preview = document.getElementById('preview');
14
- const sidePanel = document.getElementById('sidePanel');
15
- const wordCount = document.getElementById('wordCount');
16
-
17
- if (!editable || !code || !preview || !sidePanel) {
18
- throw new Error('Editor: Required elements not found');
19
- }
20
-
21
- this.editable = editable;
22
- this.code = code;
23
- this.preview = preview;
24
- this.sidePanel = sidePanel;
25
- this.wordCount = wordCount;
26
-
27
- this.bindToolbar();
28
- this.bindActions();
29
- this.bindKeyboard();
30
- this.bindEditable();
31
- this.bindTabs();
32
- this.syncViews();
33
- this.saveState();
34
-
35
- // Start with side panel hidden
36
- this.sidePanel.classList.add('hidden');
37
- }
38
-
39
- private bindToolbar(): void {
40
- document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
41
- btn.addEventListener('click', () => {
42
- const cmd = btn.dataset.cmd!;
43
- const val = btn.dataset.value ?? null;
44
- this.exec(cmd, val);
45
- this.editable.focus();
46
- });
47
- });
48
- }
49
-
50
- private bindActions(): void {
51
- document.getElementById('linkBtn')?.addEventListener('click', () => {
52
- const url = prompt('Enter URL:', 'https://');
53
- if (url) this.exec('createLink', url);
54
- });
55
-
56
- const imageFile = document.getElementById('imageFile') as HTMLInputElement;
57
- document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
58
- imageFile?.addEventListener('change', () => {
59
- const file = imageFile.files?.[0];
60
- if (!file) return;
61
- const reader = new FileReader();
62
- reader.onload = () => {
63
- if (typeof reader.result === 'string') {
64
- this.insertImage(reader.result);
65
- }
66
- };
67
- reader.readAsDataURL(file);
68
- imageFile.value = '';
69
- });
70
-
71
- document.getElementById('cleanBtn')?.addEventListener('click', () => {
72
- const sel = window.getSelection();
73
- if (!sel || sel.rangeCount === 0) return;
74
- const range = sel.getRangeAt(0);
75
- const text = range.toString();
76
- range.deleteContents();
77
- range.insertNode(document.createTextNode(text));
78
- this.onContentChange();
79
- });
80
-
81
- document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
82
- document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
83
-
84
- document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
85
- this.sidePanel.classList.toggle('hidden');
86
- this.syncViews();
87
- });
88
-
89
- // Code action buttons matched by position within .code-actions
90
- const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
91
- codeActions[0]?.addEventListener('click', () => {
92
- this.editable.innerHTML = this.sanitizeHTML(this.code.value);
93
- this.onContentChange();
94
- });
95
- codeActions[1]?.addEventListener('click', () => {
96
- this.code.value = this.sanitizeHTML(this.code.value);
97
- this.editable.innerHTML = this.code.value;
98
- this.onContentChange();
99
- });
100
- codeActions[2]?.addEventListener('click', () => {
101
- this.code.value = this.code.value
102
- .replace(/\n/g, '')
103
- .replace(/>\s+</g, '><')
104
- .trim();
105
- });
106
-
107
- const saveBtn = document.getElementById('saveBtn');
108
- saveBtn?.addEventListener('click', () => this.downloadHTML());
109
-
110
- document.getElementById('clearBtn')?.addEventListener('click', () => {
111
- if (confirm('Clear all content?')) {
112
- this.editable.innerHTML = '';
113
- this.onContentChange();
114
- }
115
- });
116
- }
117
-
118
- private bindKeyboard(): void {
119
- const saveBtn = document.getElementById('saveBtn');
120
-
121
- window.addEventListener('keydown', (e: KeyboardEvent) => {
122
- const mod = e.ctrlKey || e.metaKey;
123
- if (!mod) return;
124
-
125
- const key = e.key.toLowerCase();
126
-
127
- if (key === 'b') { e.preventDefault(); this.exec('bold'); }
128
- else if (key === 'i') { e.preventDefault(); this.exec('italic'); }
129
- else if (key === 'u') { e.preventDefault(); this.exec('underline'); }
130
- else if (key === 'k') {
131
- e.preventDefault();
132
- const url = prompt('Enter URL:', 'https://');
133
- if (url) this.exec('createLink', url);
134
- }
135
- else if (key === 's') { e.preventDefault(); saveBtn?.click(); }
136
- else if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); }
137
- else if (key === 'y' || (key === 'z' && e.shiftKey)) { e.preventDefault(); this.redo(); }
138
- });
139
- }
140
-
141
- private bindEditable(): void {
142
- this.editable.addEventListener('input', () => this.onContentChange());
143
-
144
- this.editable.addEventListener('paste', (e: ClipboardEvent) => {
145
- e.preventDefault();
146
- const text = e.clipboardData?.getData('text/plain') ?? '';
147
- this.insertText(text);
148
- });
149
-
150
- this.editable.addEventListener('keyup', () => this.refreshActiveState());
151
- this.editable.addEventListener('mouseup', () => this.refreshActiveState());
152
- }
153
-
154
- private bindTabs(): void {
155
- document.querySelectorAll<HTMLElement>('.side-tab[data-tab]').forEach(tab => {
156
- tab.addEventListener('click', () => {
157
- const targetId = tab.dataset.tab!;
158
-
159
- document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
160
- document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
161
-
162
- tab.classList.add('active');
163
- document.getElementById(targetId)?.classList.add('active');
164
- });
165
- });
166
- }
167
-
168
- private onContentChange(): void {
169
- this.saveState();
170
- this.syncViews();
171
- }
172
-
173
- private syncViews(): void {
174
- this.code.value = this.editable.innerHTML.trim();
175
- this.preview.innerHTML = this.editable.innerHTML;
176
- this.updateWordCount();
177
- }
178
-
179
- private updateWordCount(): void {
180
- if (!this.wordCount) return;
181
- const text = this.editable.innerText || '';
182
- const words = text.trim().split(/\s+/).filter(w => w.length > 0);
183
- const count = words.length;
184
- this.wordCount.textContent = `${count} word${count !== 1 ? 's' : ''}`;
185
- }
186
-
187
- private saveState(): void {
188
- this.undoStack.push(this.editable.innerHTML);
189
- if (this.undoStack.length > 100) this.undoStack.shift();
190
- this.redoStack = [];
191
- }
192
-
193
- private undo(): void {
194
- if (this.undoStack.length <= 1) return;
195
- this.redoStack.push(this.undoStack.pop()!);
196
- this.editable.innerHTML = this.undoStack[this.undoStack.length - 1];
197
- this.syncViews();
198
- }
199
-
200
- private redo(): void {
201
- if (this.redoStack.length === 0) return;
202
- const state = this.redoStack.pop()!;
203
- this.undoStack.push(state);
204
- this.editable.innerHTML = state;
205
- this.syncViews();
206
- }
207
-
208
- private exec(command: string, value: string | null = null): void {
209
- switch (command) {
210
- case 'bold': this.toggleInlineStyle('strong'); break;
211
- case 'italic': this.toggleInlineStyle('em'); break;
212
- case 'underline': this.toggleInlineStyle('u'); break;
213
- case 'strikeThrough': this.toggleInlineStyle('s'); break;
214
- case 'createLink': if (value) this.createLink(value); break;
215
- case 'formatBlock': if (value) this.formatBlock(value); break;
216
- case 'insertUnorderedList': this.insertList('ul'); break;
217
- case 'insertOrderedList': this.insertList('ol'); break;
218
- }
219
- }
220
-
221
- private insertText(text: string): void {
222
- const sel = window.getSelection();
223
- if (!sel || sel.rangeCount === 0) return;
224
-
225
- const range = sel.getRangeAt(0);
226
- range.deleteContents();
227
- range.insertNode(document.createTextNode(text));
228
- range.collapse(false);
229
- sel.removeAllRanges();
230
- sel.addRange(range);
231
-
232
- this.onContentChange();
233
- }
234
-
235
- private insertImage(dataUrl: string): void {
236
- const sel = window.getSelection();
237
- if (!sel || sel.rangeCount === 0) return;
238
-
239
- const range = sel.getRangeAt(0);
240
- const img = document.createElement('img');
241
- img.src = dataUrl;
242
- img.style.maxWidth = '100%';
243
- range.deleteContents();
244
- range.insertNode(img);
245
-
246
- range.setStartAfter(img);
247
- range.collapse(true);
248
- sel.removeAllRanges();
249
- sel.addRange(range);
250
-
251
- this.onContentChange();
252
- }
253
-
254
- private toggleInlineStyle(tagName: string): void {
255
- const sel = window.getSelection();
256
- if (!sel || sel.rangeCount === 0) return;
257
-
258
- const range = sel.getRangeAt(0);
259
- const container = range.commonAncestorContainer;
260
- let current: HTMLElement | null = container.nodeType === Node.TEXT_NODE
261
- ? container.parentElement
262
- : container as HTMLElement;
263
-
264
- let wrapper: HTMLElement | null = null;
265
- while (current && current !== this.editable) {
266
- if (current.tagName === tagName.toUpperCase()) {
267
- wrapper = current;
268
- break;
269
- }
270
- current = current.parentElement;
271
- }
272
-
273
- if (wrapper) {
274
- const parent = wrapper.parentNode;
275
- while (wrapper.firstChild) {
276
- parent?.insertBefore(wrapper.firstChild, wrapper);
277
- }
278
- parent?.removeChild(wrapper);
279
- } else {
280
- const contents = range.extractContents();
281
- const el = document.createElement(tagName);
282
- el.appendChild(contents);
283
- range.insertNode(el);
284
- range.selectNodeContents(el);
285
- sel.removeAllRanges();
286
- sel.addRange(range);
287
- }
288
-
289
- this.onContentChange();
290
- }
291
-
292
- private createLink(url: string): void {
293
- const sel = window.getSelection();
294
- if (!sel || sel.rangeCount === 0) return;
295
-
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
-
303
- this.onContentChange();
304
- }
305
-
306
- private formatBlock(tag: string): void {
307
- const sel = window.getSelection();
308
- if (!sel || sel.rangeCount === 0) return;
309
-
310
- const range = sel.getRangeAt(0);
311
- const container = range.commonAncestorContainer;
312
- let blockElement: HTMLElement | null = container.nodeType === Node.TEXT_NODE
313
- ? container.parentElement
314
- : container as HTMLElement;
315
-
316
- while (blockElement && blockElement !== this.editable && blockElement.parentElement !== this.editable) {
317
- blockElement = blockElement.parentElement;
318
- }
319
-
320
- if (blockElement && blockElement !== this.editable) {
321
- const newBlock = document.createElement(tag);
322
- newBlock.innerHTML = blockElement.innerHTML;
323
- blockElement.parentNode?.replaceChild(newBlock, blockElement);
324
- this.onContentChange();
325
- }
326
- }
327
-
328
- private insertList(listTag: string): void {
329
- const sel = window.getSelection();
330
- if (!sel || sel.rangeCount === 0) return;
331
-
332
- const range = sel.getRangeAt(0);
333
- const text = range.toString();
334
-
335
- const list = document.createElement(listTag);
336
- const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
337
-
338
- for (const line of lines) {
339
- const li = document.createElement('li');
340
- li.textContent = line.trim() || '\u200B';
341
- list.appendChild(li);
342
- }
343
-
344
- range.deleteContents();
345
- range.insertNode(list);
346
-
347
- const lastLi = list.lastElementChild;
348
- if (lastLi) {
349
- range.setStart(lastLi, lastLi.childNodes.length);
350
- range.collapse(true);
351
- sel.removeAllRanges();
352
- sel.addRange(range);
353
- }
354
-
355
- this.onContentChange();
356
- }
357
-
358
- private sanitizeHTML(html: string): string {
359
- const parser = new DOMParser();
360
- const doc = parser.parseFromString(html, 'text/html');
361
-
362
- doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
363
-
364
- doc.querySelectorAll('*').forEach(el => {
365
- for (const attr of Array.from(el.attributes)) {
366
- if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
367
- el.removeAttribute(attr.name);
368
- }
369
- }
370
- });
371
-
372
- return doc.body.innerHTML;
373
- }
374
-
375
- private downloadHTML(): void {
376
- const content = this.sanitizeHTML(this.editable.innerHTML);
377
- const html = `<!doctype html>
378
- <html lang="en">
379
- <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
380
- <body>
381
- ${content}
382
- </body>
383
- </html>`;
384
- const blob = new Blob([html], { type: 'text/html' });
385
- const a = document.createElement('a');
386
- a.href = URL.createObjectURL(blob);
387
- a.download = 'document.html';
388
- a.click();
389
- URL.revokeObjectURL(a.href);
390
- }
391
-
392
- private refreshActiveState(): void {
393
- const sel = window.getSelection();
394
- if (!sel || sel.rangeCount === 0) return;
395
-
396
- const range = sel.getRangeAt(0);
397
- const container = range.commonAncestorContainer;
398
- const element = container.nodeType === Node.TEXT_NODE
399
- ? container.parentElement
400
- : container as HTMLElement;
401
-
402
- document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
403
- const cmd = btn.dataset.cmd;
404
- let active = false;
405
-
406
- let current: HTMLElement | null = element;
407
- while (current && current !== this.editable) {
408
- const tag = current.tagName?.toLowerCase();
409
- if (
410
- (cmd === 'bold' && (tag === 'strong' || tag === 'b')) ||
411
- (cmd === 'italic' && (tag === 'em' || tag === 'i')) ||
412
- (cmd === 'underline' && tag === 'u') ||
413
- (cmd === 'strikeThrough' && tag === 's')
414
- ) {
415
- active = true;
416
- break;
417
- }
418
- current = current.parentElement;
419
- }
420
-
421
- btn.classList.toggle('active', active);
422
- });
423
- }
424
- }
425
-
1
+ interface EditorOptions {
2
+ /** Hides the entire side panel (code/preview) permanently. Safe to use
3
+ * without #code, #preview, or #sidePanel in the DOM. */
4
+ simple?: boolean;
5
+ }
6
+
7
+ class Editor {
8
+ private readonly editable: HTMLElement;
9
+ private readonly code: HTMLTextAreaElement | null;
10
+ private readonly preview: HTMLElement | null;
11
+ private readonly sidePanel: HTMLElement | null;
12
+ private readonly wordCount: HTMLElement | null;
13
+ private undoStack: string[] = [];
14
+ private redoStack: string[] = [];
15
+
16
+ constructor(options: EditorOptions = {}) {
17
+ const editable = document.getElementById('editable');
18
+
19
+ if (!editable) {
20
+ throw new Error('Editor: #editable element not found');
21
+ }
22
+
23
+ this.editable = editable;
24
+ this.wordCount = document.getElementById('wordCount');
25
+
26
+ if (options.simple) {
27
+ this.code = null;
28
+ this.preview = null;
29
+ this.sidePanel = document.getElementById('sidePanel');
30
+ this.sidePanel?.classList.add('hidden');
31
+ } else {
32
+ const code = document.getElementById('code') as HTMLTextAreaElement;
33
+ const preview = document.getElementById('preview');
34
+ const sidePanel = document.getElementById('sidePanel');
35
+
36
+ if (!code || !preview || !sidePanel) {
37
+ throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
38
+ }
39
+
40
+ this.code = code;
41
+ this.preview = preview;
42
+ this.sidePanel = sidePanel;
43
+ this.sidePanel.classList.add('hidden');
44
+ }
45
+
46
+ this.bindToolbar();
47
+ this.bindActions();
48
+ this.bindKeyboard();
49
+ this.bindEditable();
50
+ this.bindTabs();
51
+ this.syncViews();
52
+ this.saveState();
53
+ }
54
+
55
+ private bindToolbar(): void {
56
+ document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
57
+ btn.addEventListener('click', () => {
58
+ const cmd = btn.dataset.cmd!;
59
+ const val = btn.dataset.value ?? null;
60
+ this.exec(cmd, val);
61
+ this.editable.focus();
62
+ });
63
+ });
64
+ }
65
+
66
+ private bindActions(): void {
67
+ document.getElementById('linkBtn')?.addEventListener('click', () => {
68
+ const url = prompt('Enter URL:', 'https://');
69
+ if (url) this.exec('createLink', url);
70
+ });
71
+
72
+ const imageFile = document.getElementById('imageFile') as HTMLInputElement;
73
+ document.getElementById('imageBtn')?.addEventListener('click', () => imageFile.click());
74
+ imageFile?.addEventListener('change', () => {
75
+ const file = imageFile.files?.[0];
76
+ if (!file) return;
77
+ const reader = new FileReader();
78
+ reader.onload = () => {
79
+ if (typeof reader.result === 'string') {
80
+ this.insertImage(reader.result);
81
+ }
82
+ };
83
+ reader.readAsDataURL(file);
84
+ imageFile.value = '';
85
+ });
86
+
87
+ document.getElementById('cleanBtn')?.addEventListener('click', () => {
88
+ const sel = window.getSelection();
89
+ if (!sel || sel.rangeCount === 0) return;
90
+ const range = sel.getRangeAt(0);
91
+ const text = range.toString();
92
+ range.deleteContents();
93
+ range.insertNode(document.createTextNode(text));
94
+ this.onContentChange();
95
+ });
96
+
97
+ document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
98
+ document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
99
+
100
+ document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
101
+ this.sidePanel?.classList.toggle('hidden');
102
+ this.syncViews();
103
+ });
104
+
105
+ // Code action buttons — matched by position within .code-actions
106
+ if (this.code) {
107
+ const code = this.code;
108
+ const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
109
+ codeActions[0]?.addEventListener('click', () => {
110
+ this.editable.innerHTML = this.sanitizeHTML(code.value);
111
+ this.onContentChange();
112
+ });
113
+ codeActions[1]?.addEventListener('click', () => {
114
+ code.value = this.sanitizeHTML(code.value);
115
+ this.editable.innerHTML = code.value;
116
+ this.onContentChange();
117
+ });
118
+ codeActions[2]?.addEventListener('click', () => {
119
+ code.value = code.value
120
+ .replace(/\n/g, '')
121
+ .replace(/>\s+</g, '><')
122
+ .trim();
123
+ });
124
+ }
125
+
126
+ const saveBtn = document.getElementById('saveBtn');
127
+ saveBtn?.addEventListener('click', () => this.downloadHTML());
128
+
129
+ document.getElementById('clearBtn')?.addEventListener('click', () => {
130
+ if (confirm('Clear all content?')) {
131
+ this.editable.innerHTML = '';
132
+ this.onContentChange();
133
+ }
134
+ });
135
+ }
136
+
137
+ private bindKeyboard(): void {
138
+ const saveBtn = document.getElementById('saveBtn');
139
+
140
+ window.addEventListener('keydown', (e: KeyboardEvent) => {
141
+ const mod = e.ctrlKey || e.metaKey;
142
+ if (!mod) return;
143
+
144
+ const key = e.key.toLowerCase();
145
+
146
+ if (key === 'b') { e.preventDefault(); this.exec('bold'); }
147
+ else if (key === 'i') { e.preventDefault(); this.exec('italic'); }
148
+ else if (key === 'u') { e.preventDefault(); this.exec('underline'); }
149
+ else if (key === 'k') {
150
+ e.preventDefault();
151
+ const url = prompt('Enter URL:', 'https://');
152
+ if (url) this.exec('createLink', url);
153
+ }
154
+ else if (key === 's') { e.preventDefault(); saveBtn?.click(); }
155
+ else if (key === 'z' && !e.shiftKey) { e.preventDefault(); this.undo(); }
156
+ else if (key === 'y' || (key === 'z' && e.shiftKey)) { e.preventDefault(); this.redo(); }
157
+ });
158
+ }
159
+
160
+ private bindEditable(): void {
161
+ this.editable.addEventListener('input', () => this.onContentChange());
162
+
163
+ this.editable.addEventListener('paste', (e: ClipboardEvent) => {
164
+ e.preventDefault();
165
+ const text = e.clipboardData?.getData('text/plain') ?? '';
166
+ this.insertText(text);
167
+ });
168
+
169
+ this.editable.addEventListener('keyup', () => this.refreshActiveState());
170
+ this.editable.addEventListener('mouseup', () => this.refreshActiveState());
171
+ }
172
+
173
+ private bindTabs(): void {
174
+ document.querySelectorAll<HTMLElement>('.side-tab[data-tab]').forEach(tab => {
175
+ tab.addEventListener('click', () => {
176
+ const targetId = tab.dataset.tab!;
177
+
178
+ document.querySelectorAll('.side-tab').forEach(t => t.classList.remove('active'));
179
+ document.querySelectorAll('.side-panel').forEach(p => p.classList.remove('active'));
180
+
181
+ tab.classList.add('active');
182
+ document.getElementById(targetId)?.classList.add('active');
183
+ });
184
+ });
185
+ }
186
+
187
+ private onContentChange(): void {
188
+ this.saveState();
189
+ this.syncViews();
190
+ }
191
+
192
+ private syncViews(): void {
193
+ if (this.code) this.code.value = this.editable.innerHTML.trim();
194
+ if (this.preview) this.preview.innerHTML = this.editable.innerHTML;
195
+ this.updateWordCount();
196
+ }
197
+
198
+ private updateWordCount(): void {
199
+ if (!this.wordCount) return;
200
+ const text = this.editable.innerText || '';
201
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0);
202
+ const count = words.length;
203
+ this.wordCount.textContent = `${count} word${count !== 1 ? 's' : ''}`;
204
+ }
205
+
206
+ private saveState(): void {
207
+ this.undoStack.push(this.editable.innerHTML);
208
+ if (this.undoStack.length > 100) this.undoStack.shift();
209
+ this.redoStack = [];
210
+ }
211
+
212
+ private undo(): void {
213
+ if (this.undoStack.length <= 1) return;
214
+ this.redoStack.push(this.undoStack.pop()!);
215
+ this.editable.innerHTML = this.undoStack[this.undoStack.length - 1];
216
+ this.syncViews();
217
+ }
218
+
219
+ private redo(): void {
220
+ if (this.redoStack.length === 0) return;
221
+ const state = this.redoStack.pop()!;
222
+ this.undoStack.push(state);
223
+ this.editable.innerHTML = state;
224
+ this.syncViews();
225
+ }
226
+
227
+ private exec(command: string, value: string | null = null): void {
228
+ switch (command) {
229
+ case 'bold': this.toggleInlineStyle('strong'); break;
230
+ case 'italic': this.toggleInlineStyle('em'); break;
231
+ case 'underline': this.toggleInlineStyle('u'); break;
232
+ case 'strikeThrough': this.toggleInlineStyle('s'); break;
233
+ case 'createLink': if (value) this.createLink(value); break;
234
+ case 'formatBlock': if (value) this.formatBlock(value); break;
235
+ case 'insertUnorderedList': this.insertList('ul'); break;
236
+ case 'insertOrderedList': this.insertList('ol'); break;
237
+ case 'justifyLeft':
238
+ case 'justifyCenter':
239
+ case 'justifyRight': this.setAlignment(command); break;
240
+ case 'foreColor': if (value) this.setForeColor(value); break;
241
+ }
242
+ }
243
+
244
+ private insertText(text: string): void {
245
+ const sel = window.getSelection();
246
+ if (!sel || sel.rangeCount === 0) return;
247
+
248
+ const range = sel.getRangeAt(0);
249
+ range.deleteContents();
250
+ range.insertNode(document.createTextNode(text));
251
+ range.collapse(false);
252
+ sel.removeAllRanges();
253
+ sel.addRange(range);
254
+
255
+ this.onContentChange();
256
+ }
257
+
258
+ private insertImage(dataUrl: string): void {
259
+ const sel = window.getSelection();
260
+ if (!sel || sel.rangeCount === 0) return;
261
+
262
+ const range = sel.getRangeAt(0);
263
+ const img = document.createElement('img');
264
+ img.src = dataUrl;
265
+ img.style.maxWidth = '100%';
266
+ range.deleteContents();
267
+ range.insertNode(img);
268
+
269
+ range.setStartAfter(img);
270
+ range.collapse(true);
271
+ sel.removeAllRanges();
272
+ sel.addRange(range);
273
+
274
+ this.onContentChange();
275
+ }
276
+
277
+ private toggleInlineStyle(tagName: string): void {
278
+ const sel = window.getSelection();
279
+ if (!sel || sel.rangeCount === 0) return;
280
+
281
+ const range = sel.getRangeAt(0);
282
+ const container = range.commonAncestorContainer;
283
+ let current: HTMLElement | null = container.nodeType === Node.TEXT_NODE
284
+ ? container.parentElement
285
+ : container as HTMLElement;
286
+
287
+ let wrapper: HTMLElement | null = null;
288
+ while (current && current !== this.editable) {
289
+ if (current.tagName === tagName.toUpperCase()) {
290
+ wrapper = current;
291
+ break;
292
+ }
293
+ current = current.parentElement;
294
+ }
295
+
296
+ if (wrapper) {
297
+ const parent = wrapper.parentNode;
298
+ while (wrapper.firstChild) {
299
+ parent?.insertBefore(wrapper.firstChild, wrapper);
300
+ }
301
+ parent?.removeChild(wrapper);
302
+ } else {
303
+ const contents = range.extractContents();
304
+ const el = document.createElement(tagName);
305
+ el.appendChild(contents);
306
+ range.insertNode(el);
307
+ range.selectNodeContents(el);
308
+ sel.removeAllRanges();
309
+ sel.addRange(range);
310
+ }
311
+
312
+ this.onContentChange();
313
+ }
314
+
315
+ private createLink(url: string): void {
316
+ const sel = window.getSelection();
317
+ if (!sel || sel.rangeCount === 0) return;
318
+
319
+ const range = sel.getRangeAt(0);
320
+ const contents = range.extractContents();
321
+ const link = document.createElement('a');
322
+ link.href = url;
323
+ link.appendChild(contents);
324
+ range.insertNode(link);
325
+
326
+ this.onContentChange();
327
+ }
328
+
329
+ private formatBlock(tag: string): void {
330
+ const sel = window.getSelection();
331
+ if (!sel || sel.rangeCount === 0) return;
332
+
333
+ const range = sel.getRangeAt(0);
334
+ const container = range.commonAncestorContainer;
335
+ let blockElement: HTMLElement | null = container.nodeType === Node.TEXT_NODE
336
+ ? container.parentElement
337
+ : container as HTMLElement;
338
+
339
+ while (blockElement && blockElement !== this.editable && blockElement.parentElement !== this.editable) {
340
+ blockElement = blockElement.parentElement;
341
+ }
342
+
343
+ if (blockElement && blockElement !== this.editable) {
344
+ const newBlock = document.createElement(tag);
345
+ newBlock.innerHTML = blockElement.innerHTML;
346
+ blockElement.parentNode?.replaceChild(newBlock, blockElement);
347
+ this.onContentChange();
348
+ }
349
+ }
350
+
351
+ private insertList(listTag: string): void {
352
+ const sel = window.getSelection();
353
+ if (!sel || sel.rangeCount === 0) return;
354
+
355
+ const range = sel.getRangeAt(0);
356
+ const text = range.toString();
357
+
358
+ const list = document.createElement(listTag);
359
+ const lines = text ? text.split('\n').filter(l => l.trim()) : [''];
360
+
361
+ for (const line of lines) {
362
+ const li = document.createElement('li');
363
+ li.textContent = line.trim() || '\u200B';
364
+ list.appendChild(li);
365
+ }
366
+
367
+ range.deleteContents();
368
+ range.insertNode(list);
369
+
370
+ const lastLi = list.lastElementChild;
371
+ if (lastLi) {
372
+ range.setStart(lastLi, lastLi.childNodes.length);
373
+ range.collapse(true);
374
+ sel.removeAllRanges();
375
+ sel.addRange(range);
376
+ }
377
+
378
+ this.onContentChange();
379
+ }
380
+
381
+ private setAlignment(cmd: string): void {
382
+ const align: Record<string, string> = {
383
+ justifyLeft: 'left', justifyCenter: 'center', justifyRight: 'right',
384
+ };
385
+ const sel = window.getSelection();
386
+ if (!sel || sel.rangeCount === 0) return;
387
+ const container = sel.getRangeAt(0).commonAncestorContainer;
388
+ let block: HTMLElement | null = container.nodeType === Node.TEXT_NODE
389
+ ? container.parentElement
390
+ : container as HTMLElement;
391
+ while (block && block !== this.editable && block.parentElement !== this.editable) {
392
+ block = block.parentElement;
393
+ }
394
+ if (block && block !== this.editable) {
395
+ block.style.textAlign = align[cmd];
396
+ this.onContentChange();
397
+ }
398
+ }
399
+
400
+ private setForeColor(color: string): void {
401
+ const sel = window.getSelection();
402
+ if (!sel || sel.rangeCount === 0) return;
403
+ const range = sel.getRangeAt(0);
404
+ if (range.collapsed) return;
405
+ const span = document.createElement('span');
406
+ span.style.color = color;
407
+ span.appendChild(range.extractContents());
408
+ range.insertNode(span);
409
+ range.selectNodeContents(span);
410
+ sel.removeAllRanges();
411
+ sel.addRange(range);
412
+ this.onContentChange();
413
+ }
414
+
415
+ private sanitizeHTML(html: string): string {
416
+ const parser = new DOMParser();
417
+ const doc = parser.parseFromString(html, 'text/html');
418
+
419
+ doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
420
+
421
+ doc.querySelectorAll('*').forEach(el => {
422
+ for (const attr of Array.from(el.attributes)) {
423
+ if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
424
+ el.removeAttribute(attr.name);
425
+ }
426
+ }
427
+ });
428
+
429
+ return doc.body.innerHTML;
430
+ }
431
+
432
+ private downloadHTML(): void {
433
+ const content = this.sanitizeHTML(this.editable.innerHTML);
434
+ const html = `<!doctype html>
435
+ <html lang="en">
436
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
437
+ <body>
438
+ ${content}
439
+ </body>
440
+ </html>`;
441
+ const blob = new Blob([html], { type: 'text/html' });
442
+ const a = document.createElement('a');
443
+ a.href = URL.createObjectURL(blob);
444
+ a.download = 'document.html';
445
+ a.click();
446
+ URL.revokeObjectURL(a.href);
447
+ }
448
+
449
+ private refreshActiveState(): void {
450
+ const sel = window.getSelection();
451
+ if (!sel || sel.rangeCount === 0) return;
452
+
453
+ const range = sel.getRangeAt(0);
454
+ const container = range.commonAncestorContainer;
455
+ const element = container.nodeType === Node.TEXT_NODE
456
+ ? container.parentElement
457
+ : container as HTMLElement;
458
+
459
+ document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(btn => {
460
+ const cmd = btn.dataset.cmd;
461
+ let active = false;
462
+
463
+ let current: HTMLElement | null = element;
464
+ while (current && current !== this.editable) {
465
+ const tag = current.tagName?.toLowerCase();
466
+ if (
467
+ (cmd === 'bold' && (tag === 'strong' || tag === 'b')) ||
468
+ (cmd === 'italic' && (tag === 'em' || tag === 'i')) ||
469
+ (cmd === 'underline' && tag === 'u') ||
470
+ (cmd === 'strikeThrough' && tag === 's')
471
+ ) {
472
+ active = true;
473
+ break;
474
+ }
475
+ current = current.parentElement;
476
+ }
477
+
478
+ btn.classList.toggle('active', active);
479
+ });
480
+
481
+ }
482
+ }
483
+
426
484
  export { Editor };