@dodlhuat/basix 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +266 -6
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/button.scss +48 -0
  5. package/css/calendar.scss +957 -0
  6. package/css/card.scss +65 -65
  7. package/css/chart.scss +270 -157
  8. package/css/chat-bubbles.scss +134 -68
  9. package/css/chips.scss +109 -19
  10. package/css/colors.scss +32 -32
  11. package/css/datepicker.scss +336 -336
  12. package/css/defaults.scss +90 -90
  13. package/css/docs.scss +529 -0
  14. package/css/editor.scss +36 -0
  15. package/css/file-uploader.scss +1 -1
  16. package/css/flyout-menu.scss +361 -361
  17. package/css/form.scss +0 -15
  18. package/css/gallery.scss +65 -6
  19. package/css/grid.scss +41 -40
  20. package/css/group-picker.scss +345 -0
  21. package/css/guitar-chords.css +250 -250
  22. package/css/icons.scss +330 -330
  23. package/css/parameters.scss +3 -3
  24. package/css/placeholder.scss +33 -33
  25. package/css/popover.scss +206 -0
  26. package/css/progress.scss +76 -32
  27. package/css/properties.scss +51 -36
  28. package/css/push-menu.scss +302 -174
  29. package/css/reset.scss +39 -39
  30. package/css/scrollbar.scss +62 -5
  31. package/css/sidebar-nav.scss +92 -0
  32. package/css/spinner.scss +65 -65
  33. package/css/stepper.scss +48 -12
  34. package/css/style.css +3155 -254
  35. package/css/style.css.map +1 -1
  36. package/css/style.min.css +1 -1
  37. package/css/style.scss +51 -45
  38. package/css/table.scss +199 -199
  39. package/css/tabs.scss +154 -123
  40. package/css/timeline.scss +83 -38
  41. package/css/timepicker.scss +100 -5
  42. package/css/toast.scss +81 -81
  43. package/css/virtual-dropdown.scss +35 -29
  44. package/js/calendar.js +532 -0
  45. package/js/calendar.ts +706 -0
  46. package/js/chart.js +573 -257
  47. package/js/chart.ts +692 -0
  48. package/js/code-viewer.js +10 -10
  49. package/js/code-viewer.ts +188 -188
  50. package/js/datepicker.ts +627 -627
  51. package/js/docs-nav.js +204 -0
  52. package/js/dropdown.ts +179 -179
  53. package/js/editor.js +50 -6
  54. package/js/editor.ts +483 -444
  55. package/js/file-uploader.js +1 -0
  56. package/js/file-uploader.ts +1 -0
  57. package/js/flyout-menu.js +14 -14
  58. package/js/flyout-menu.ts +249 -249
  59. package/js/form-builder.js +106 -106
  60. package/js/gallery.js +14 -8
  61. package/js/gallery.ts +245 -236
  62. package/js/group-picker.js +342 -0
  63. package/js/group-picker.ts +447 -0
  64. package/js/guitar-chords.js +268 -268
  65. package/js/lazy-loader.js +121 -121
  66. package/js/modal.ts +166 -166
  67. package/js/popover.js +163 -0
  68. package/js/popover.ts +219 -0
  69. package/js/position.js +108 -0
  70. package/js/position.ts +111 -0
  71. package/js/push-menu.js +113 -0
  72. package/js/push-menu.ts +284 -145
  73. package/js/request.js +50 -50
  74. package/js/scroll.ts +47 -47
  75. package/js/scrollbar.js +13 -0
  76. package/js/scrollbar.ts +324 -307
  77. package/js/select.ts +216 -216
  78. package/js/sidebar-nav.js +41 -0
  79. package/js/sidebar-nav.ts +66 -0
  80. package/js/table.ts +452 -452
  81. package/js/tabs.ts +279 -279
  82. package/js/theme.js +17 -6
  83. package/js/theme.ts +234 -224
  84. package/js/toast.ts +137 -137
  85. package/js/tooltip.js +6 -60
  86. package/js/tooltip.ts +184 -251
  87. package/js/tsconfig.json +18 -18
  88. package/js/utils.ts +83 -83
  89. package/js/virtual-dropdown.js +25 -25
  90. package/js/virtual-dropdown.ts +365 -365
  91. package/package.json +37 -39
  92. package/js/index.js +0 -816
  93. package/js/index.ts +0 -987
package/js/docs-nav.js ADDED
@@ -0,0 +1,204 @@
1
+ import { SidebarNav } from './sidebar-nav.js';
2
+ import { Scrollbar } from './scrollbar.js';
3
+
4
+ const NAV = [
5
+ {
6
+ label: 'Foundation',
7
+ items: [
8
+ { label: 'Layout & Grid', href: 'foundation/layout.html' },
9
+ { label: 'Typography', href: 'foundation/typography.html' },
10
+ { label: 'Colors & Tokens', href: 'foundation/colors.html' },
11
+ { label: 'Icons', href: 'foundation/icons.html' },
12
+ ],
13
+ },
14
+ {
15
+ label: 'Forms',
16
+ items: [
17
+ { label: 'Inputs & Forms', href: 'forms/inputs.html' },
18
+ { label: 'Date Picker', href: 'forms/datepicker.html' },
19
+ { label: 'Range Slider', href: 'forms/range-slider.html' },
20
+ { label: 'File Uploader', href: 'forms/file-uploader.html' },
21
+ ],
22
+ },
23
+ {
24
+ label: 'Navigation',
25
+ items: [
26
+ { label: 'Push Menu', href: 'navigation/push-menu.html' },
27
+ { label: 'Flyout Menu', href: 'navigation/flyout-menu.html' },
28
+ { label: 'Sidebar Nav', href: 'navigation/sidebar-nav.html' },
29
+ { label: 'Dropdown', href: 'navigation/dropdown.html' },
30
+ { label: 'Tabs', href: 'navigation/tabs.html' },
31
+ { label: 'Breadcrumbs', href: 'navigation/breadcrumbs.html' },
32
+ ],
33
+ },
34
+ {
35
+ label: 'Overlays',
36
+ items: [
37
+ { label: 'Modal', href: 'overlays/modal.html' },
38
+ { label: 'Popover', href: 'overlays/popover.html' },
39
+ { label: 'Tooltip', href: 'overlays/tooltip.html' },
40
+ { label: 'Bottom Sheet', href: 'overlays/bottom-sheet.html' },
41
+ { label: 'Toast', href: 'overlays/toast.html' },
42
+ ],
43
+ },
44
+ {
45
+ label: 'Components',
46
+ items: [
47
+ { label: 'Buttons & Chips', href: 'components/buttons.html' },
48
+ { label: 'Alerts & Badge', href: 'components/alerts.html' },
49
+ { label: 'Accordion', href: 'components/accordion.html' },
50
+ { label: 'Stepper', href: 'components/stepper.html' },
51
+ { label: 'Timeline', href: 'components/timeline.html' },
52
+ { label: 'Progress & Skeleton', href: 'components/progress.html' },
53
+ { label: 'Data Table', href: 'components/table.html' },
54
+ { label: 'Tree', href: 'components/tree.html' },
55
+ { label: 'Carousel', href: 'components/carousel.html' },
56
+ { label: 'Gallery', href: 'components/gallery.html' },
57
+ { label: 'Scrollbar', href: 'components/scrollbar.html' },
58
+ { label: 'Chat Bubbles', href: 'components/chat-bubbles.html' },
59
+ { label: 'Charts', href: 'components/charts.html' },
60
+ { label: 'Group Picker', href: 'components/group-picker.html' },
61
+ { label: 'Virtual Dropdown', href: 'components/virtual-dropdown.html' },
62
+ { label: 'Timespan Picker', href: 'components/timespan-picker.html' },
63
+ { label: 'Rich Text Editor', href: 'components/editor.html' },
64
+ ],
65
+ },
66
+ {
67
+ label: 'Utilities',
68
+ items: [
69
+ { label: 'Theme', href: 'utilities/theme.html' },
70
+ { label: 'Scroll', href: 'utilities/scroll.html' },
71
+ { label: 'Context Menu', href: 'utilities/context-menu.html' },
72
+ ],
73
+ },
74
+ ];
75
+
76
+ class DocsNav {
77
+ constructor() {
78
+ this.currentPath = window.location.pathname;
79
+ // Compute prefix to reach docs/ root from current page.
80
+ // docs/index.html → prefix = ''
81
+ // docs/overlays/foo.html → prefix = '../'
82
+ const segs = this.currentPath.split('/').filter(Boolean);
83
+ const docsIdx = segs.lastIndexOf('docs');
84
+ const depth = docsIdx >= 0 ? segs.length - docsIdx - 1 : 0;
85
+ this.prefix = depth > 1 ? '../'.repeat(depth - 1) : '';
86
+ this.render();
87
+ this.bindMobile();
88
+ }
89
+
90
+ isActive(href) {
91
+ // href is like 'overlays/popover.html' — match against current path
92
+ return this.currentPath.endsWith('/' + href.replace(/^[^/]+\//, '')) &&
93
+ this.currentPath.includes('/' + href.split('/')[0] + '/');
94
+ }
95
+
96
+ render() {
97
+ // Sidebar
98
+ const sidebar = document.getElementById('docs-sidebar');
99
+ if (!sidebar) return;
100
+
101
+ const logoHref = this.prefix ? this.prefix + '../index.html' : '../index.html';
102
+
103
+ sidebar.innerHTML = `
104
+ <a class="docs-sidebar-logo" href="${logoHref}">
105
+ Basix
106
+ <span class="docs-logo-badge">docs</span>
107
+ </a>
108
+ <div class="docs-sidebar-search">
109
+ <input type="search" placeholder="Search…" id="docs-search" autocomplete="off"/>
110
+ </div>
111
+ <div class="scroll-container sidebar-scroll">
112
+ <div class="viewport">
113
+ <div class="content">
114
+ <nav class="docs-nav" aria-label="Documentation navigation">
115
+ <ul>${NAV.map(section => this.renderSection(section)).join('')}</ul>
116
+ </nav>
117
+ </div>
118
+ </div>
119
+ <div class="scrollbar">
120
+ <div class="track">
121
+ <div class="thumb"></div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ `;
126
+
127
+ new Scrollbar(sidebar.querySelector('.sidebar-scroll'));
128
+
129
+ // Breadcrumb
130
+ this.renderBreadcrumb();
131
+
132
+ // Search
133
+ document.getElementById('docs-search')?.addEventListener('input', e => {
134
+ this.filter(e.target.value);
135
+ });
136
+ }
137
+
138
+ renderSection(section) {
139
+ return `
140
+ <li class="docs-nav-section">
141
+ <div class="docs-nav-section-label">${section.label}</div>
142
+ <ul>
143
+ ${section.items.map(item => this.renderItem(item, section)).join('')}
144
+ </ul>
145
+ </li>`;
146
+ }
147
+
148
+ renderItem(item, section) {
149
+ const active = this.isActive(item.href) ? ' is-active' : '';
150
+ return `
151
+ <li class="docs-nav-item">
152
+ <a href="${this.prefix}${item.href}" class="${active}">${item.label}</a>
153
+ </li>`;
154
+ }
155
+
156
+ renderBreadcrumb() {
157
+ const bc = document.getElementById('docs-breadcrumb');
158
+ if (!bc) return;
159
+
160
+ let activeSection = null;
161
+ let activeItem = null;
162
+ for (const section of NAV) {
163
+ for (const item of section.items) {
164
+ if (this.isActive(item.href)) {
165
+ activeSection = section;
166
+ activeItem = item;
167
+ break;
168
+ }
169
+ }
170
+ if (activeItem) break;
171
+ }
172
+
173
+ if (!activeSection || !activeItem) return;
174
+
175
+ const rootHref = this.prefix ? this.prefix + '../index.html' : '../index.html';
176
+ bc.innerHTML = `
177
+ <a href="${rootHref}">Basix</a>
178
+ <span class="sep">/</span>
179
+ <span>${activeSection.label}</span>
180
+ <span class="sep">/</span>
181
+ <span class="current">${activeItem.label}</span>
182
+ `;
183
+ }
184
+
185
+ filter(query) {
186
+ const q = query.toLowerCase().trim();
187
+ document.querySelectorAll('.docs-nav-item').forEach(item => {
188
+ const label = item.querySelector('a')?.textContent?.toLowerCase() ?? '';
189
+ item.style.display = (!q || label.includes(q)) ? '' : 'none';
190
+ });
191
+ document.querySelectorAll('.docs-nav-section').forEach(section => {
192
+ const visible = [...section.querySelectorAll('.docs-nav-item')]
193
+ .some(i => i.style.display !== 'none');
194
+ section.style.display = (!q || visible) ? '' : 'none';
195
+ });
196
+ }
197
+
198
+ bindMobile() {
199
+ const layout = document.querySelector('.sidebar-layout');
200
+ if (layout) new SidebarNav(layout, { toggleSelector: '#docs-mobile-toggle' });
201
+ }
202
+ }
203
+
204
+ export { DocsNav, NAV };
package/js/dropdown.ts CHANGED
@@ -1,180 +1,180 @@
1
- interface DropdownOptions {
2
- closeOnSelect?: boolean;
3
- allowMultipleOpen?: boolean;
4
- }
5
-
6
- interface DropdownSelectDetail {
7
- text: string;
8
- element: HTMLElement;
9
- }
10
-
11
- class Dropdown {
12
- private container: HTMLElement;
13
- private trigger: HTMLElement;
14
- private menu: HTMLElement;
15
- private options: Required<DropdownOptions>;
16
- private abortController: AbortController;
17
-
18
- constructor(selector: string, options: DropdownOptions = {}) {
19
- const container = document.querySelector<HTMLElement>(selector);
20
-
21
- if (!container) {
22
- console.error(`Dropdown container not found: ${selector}`);
23
- throw new Error(`Dropdown container "${selector}" not found`);
24
- }
25
-
26
- this.container = container;
27
-
28
- const trigger = this.container.querySelector<HTMLElement>('.dropdown-trigger');
29
- const menu = this.container.querySelector<HTMLElement>('.dropdown-menu');
30
-
31
- if (!trigger || !menu) {
32
- throw new Error('Dropdown requires .dropdown-trigger and .dropdown-menu elements');
33
- }
34
-
35
- this.trigger = trigger;
36
- this.menu = menu;
37
-
38
- this.options = {
39
- closeOnSelect: options.closeOnSelect ?? true,
40
- allowMultipleOpen: options.allowMultipleOpen ?? false,
41
- };
42
-
43
- this.abortController = new AbortController();
44
- this.init();
45
- }
46
-
47
- private init(): void {
48
- this.setupItems();
49
- this.attachEventListeners();
50
- }
51
-
52
- private attachEventListeners(): void {
53
- const { signal } = this.abortController;
54
-
55
- // Toggle main dropdown
56
- this.trigger.addEventListener(
57
- 'click',
58
- (e: MouseEvent) => {
59
- e.stopPropagation();
60
- this.toggle();
61
- },
62
- { signal }
63
- );
64
-
65
- // Close when clicking outside
66
- document.addEventListener(
67
- 'click',
68
- (e: MouseEvent) => {
69
- if (!this.container.contains(e.target as Node)) {
70
- this.close();
71
- }
72
- },
73
- { signal }
74
- );
75
-
76
- // Handle item clicks using event delegation
77
- this.menu.addEventListener(
78
- 'click',
79
- (e: MouseEvent) => {
80
- e.stopPropagation();
81
-
82
- const target = e.target as HTMLElement;
83
- const item = target.closest<HTMLElement>('.dropdown-item');
84
-
85
- if (!item) return;
86
-
87
- const li = item.parentElement as HTMLLIElement;
88
- const submenu = li.querySelector<HTMLUListElement>('ul');
89
-
90
- if (submenu) {
91
- this.toggleSubmenu(li);
92
- } else {
93
- this.handleSelection(item);
94
- if (this.options.closeOnSelect) {
95
- this.close();
96
- }
97
- }
98
- },
99
- { signal }
100
- );
101
- }
102
-
103
- private setupItems(): void {
104
- const items = this.menu.querySelectorAll<HTMLElement>('.dropdown-item');
105
-
106
- items.forEach((item) => {
107
- const li = item.parentElement as HTMLLIElement;
108
- if (li.querySelector('ul')) {
109
- item.classList.add('has-children');
110
- }
111
- });
112
- }
113
-
114
- public toggle(): void {
115
- this.container.classList.toggle('active');
116
- }
117
-
118
- public close(): void {
119
- this.container.classList.remove('active');
120
- this.closeAllSubmenus();
121
- }
122
-
123
- public open(): void {
124
- this.container.classList.add('active');
125
- }
126
-
127
- private toggleSubmenu(li: HTMLLIElement): void {
128
- const isOpening = !li.classList.contains('open');
129
-
130
- // Close siblings if not allowing multiple open menus
131
- if (isOpening && !this.options.allowMultipleOpen) {
132
- const parent = li.parentElement;
133
- if (parent) {
134
- const siblings = Array.from(parent.children) as HTMLLIElement[];
135
-
136
- siblings.forEach((sibling) => {
137
- if (sibling !== li && sibling.classList.contains('open')) {
138
- sibling.classList.remove('open');
139
-
140
- // Close deeply nested open items
141
- const deepOpenItems = sibling.querySelectorAll<HTMLLIElement>('.open');
142
- deepOpenItems.forEach((el) => el.classList.remove('open'));
143
- }
144
- });
145
- }
146
- }
147
-
148
- li.classList.toggle('open');
149
- }
150
-
151
- private closeAllSubmenus(): void {
152
- const openItems = this.menu.querySelectorAll<HTMLLIElement>('li.open');
153
- openItems.forEach((item) => item.classList.remove('open'));
154
- }
155
-
156
- private handleSelection(item: HTMLElement): void {
157
- const text = item.textContent?.trim() ?? '';
158
-
159
- // Dispatch custom event with proper typing
160
- const event = new CustomEvent<DropdownSelectDetail>('dropdown-select', {
161
- detail: {
162
- text,
163
- element: item,
164
- },
165
- bubbles: true,
166
- });
167
-
168
- this.container.dispatchEvent(event);
169
- }
170
-
171
- /**
172
- * Cleanup method to remove event listeners
173
- */
174
- public destroy(): void {
175
- this.abortController.abort();
176
- this.close();
177
- }
178
- }
179
-
1
+ interface DropdownOptions {
2
+ closeOnSelect?: boolean;
3
+ allowMultipleOpen?: boolean;
4
+ }
5
+
6
+ interface DropdownSelectDetail {
7
+ text: string;
8
+ element: HTMLElement;
9
+ }
10
+
11
+ class Dropdown {
12
+ private container: HTMLElement;
13
+ private trigger: HTMLElement;
14
+ private menu: HTMLElement;
15
+ private options: Required<DropdownOptions>;
16
+ private abortController: AbortController;
17
+
18
+ constructor(selector: string, options: DropdownOptions = {}) {
19
+ const container = document.querySelector<HTMLElement>(selector);
20
+
21
+ if (!container) {
22
+ console.error(`Dropdown container not found: ${selector}`);
23
+ throw new Error(`Dropdown container "${selector}" not found`);
24
+ }
25
+
26
+ this.container = container;
27
+
28
+ const trigger = this.container.querySelector<HTMLElement>('.dropdown-trigger');
29
+ const menu = this.container.querySelector<HTMLElement>('.dropdown-menu');
30
+
31
+ if (!trigger || !menu) {
32
+ throw new Error('Dropdown requires .dropdown-trigger and .dropdown-menu elements');
33
+ }
34
+
35
+ this.trigger = trigger;
36
+ this.menu = menu;
37
+
38
+ this.options = {
39
+ closeOnSelect: options.closeOnSelect ?? true,
40
+ allowMultipleOpen: options.allowMultipleOpen ?? false,
41
+ };
42
+
43
+ this.abortController = new AbortController();
44
+ this.init();
45
+ }
46
+
47
+ private init(): void {
48
+ this.setupItems();
49
+ this.attachEventListeners();
50
+ }
51
+
52
+ private attachEventListeners(): void {
53
+ const { signal } = this.abortController;
54
+
55
+ // Toggle main dropdown
56
+ this.trigger.addEventListener(
57
+ 'click',
58
+ (e: MouseEvent) => {
59
+ e.stopPropagation();
60
+ this.toggle();
61
+ },
62
+ { signal }
63
+ );
64
+
65
+ // Close when clicking outside
66
+ document.addEventListener(
67
+ 'click',
68
+ (e: MouseEvent) => {
69
+ if (!this.container.contains(e.target as Node)) {
70
+ this.close();
71
+ }
72
+ },
73
+ { signal }
74
+ );
75
+
76
+ // Handle item clicks using event delegation
77
+ this.menu.addEventListener(
78
+ 'click',
79
+ (e: MouseEvent) => {
80
+ e.stopPropagation();
81
+
82
+ const target = e.target as HTMLElement;
83
+ const item = target.closest<HTMLElement>('.dropdown-item');
84
+
85
+ if (!item) return;
86
+
87
+ const li = item.parentElement as HTMLLIElement;
88
+ const submenu = li.querySelector<HTMLUListElement>('ul');
89
+
90
+ if (submenu) {
91
+ this.toggleSubmenu(li);
92
+ } else {
93
+ this.handleSelection(item);
94
+ if (this.options.closeOnSelect) {
95
+ this.close();
96
+ }
97
+ }
98
+ },
99
+ { signal }
100
+ );
101
+ }
102
+
103
+ private setupItems(): void {
104
+ const items = this.menu.querySelectorAll<HTMLElement>('.dropdown-item');
105
+
106
+ items.forEach((item) => {
107
+ const li = item.parentElement as HTMLLIElement;
108
+ if (li.querySelector('ul')) {
109
+ item.classList.add('has-children');
110
+ }
111
+ });
112
+ }
113
+
114
+ public toggle(): void {
115
+ this.container.classList.toggle('active');
116
+ }
117
+
118
+ public close(): void {
119
+ this.container.classList.remove('active');
120
+ this.closeAllSubmenus();
121
+ }
122
+
123
+ public open(): void {
124
+ this.container.classList.add('active');
125
+ }
126
+
127
+ private toggleSubmenu(li: HTMLLIElement): void {
128
+ const isOpening = !li.classList.contains('open');
129
+
130
+ // Close siblings if not allowing multiple open menus
131
+ if (isOpening && !this.options.allowMultipleOpen) {
132
+ const parent = li.parentElement;
133
+ if (parent) {
134
+ const siblings = Array.from(parent.children) as HTMLLIElement[];
135
+
136
+ siblings.forEach((sibling) => {
137
+ if (sibling !== li && sibling.classList.contains('open')) {
138
+ sibling.classList.remove('open');
139
+
140
+ // Close deeply nested open items
141
+ const deepOpenItems = sibling.querySelectorAll<HTMLLIElement>('.open');
142
+ deepOpenItems.forEach((el) => el.classList.remove('open'));
143
+ }
144
+ });
145
+ }
146
+ }
147
+
148
+ li.classList.toggle('open');
149
+ }
150
+
151
+ private closeAllSubmenus(): void {
152
+ const openItems = this.menu.querySelectorAll<HTMLLIElement>('li.open');
153
+ openItems.forEach((item) => item.classList.remove('open'));
154
+ }
155
+
156
+ private handleSelection(item: HTMLElement): void {
157
+ const text = item.textContent?.trim() ?? '';
158
+
159
+ // Dispatch custom event with proper typing
160
+ const event = new CustomEvent<DropdownSelectDetail>('dropdown-select', {
161
+ detail: {
162
+ text,
163
+ element: item,
164
+ },
165
+ bubbles: true,
166
+ });
167
+
168
+ this.container.dispatchEvent(event);
169
+ }
170
+
171
+ /**
172
+ * Cleanup method to remove event listeners
173
+ */
174
+ public destroy(): void {
175
+ this.abortController.abort();
176
+ this.close();
177
+ }
178
+ }
179
+
180
180
  export { Dropdown, DropdownSelectDetail };
package/js/editor.js CHANGED
@@ -238,6 +238,15 @@ class Editor {
238
238
  case 'insertOrderedList':
239
239
  this.insertList('ol');
240
240
  break;
241
+ case 'justifyLeft':
242
+ case 'justifyCenter':
243
+ case 'justifyRight':
244
+ this.setAlignment(command);
245
+ break;
246
+ case 'foreColor':
247
+ if (value)
248
+ this.setForeColor(value);
249
+ break;
241
250
  }
242
251
  }
243
252
  insertText(text) {
@@ -358,6 +367,41 @@ class Editor {
358
367
  }
359
368
  this.onContentChange();
360
369
  }
370
+ setAlignment(cmd) {
371
+ const align = {
372
+ justifyLeft: 'left', justifyCenter: 'center', justifyRight: 'right',
373
+ };
374
+ const sel = window.getSelection();
375
+ if (!sel || sel.rangeCount === 0)
376
+ return;
377
+ const container = sel.getRangeAt(0).commonAncestorContainer;
378
+ let block = container.nodeType === Node.TEXT_NODE
379
+ ? container.parentElement
380
+ : container;
381
+ while (block && block !== this.editable && block.parentElement !== this.editable) {
382
+ block = block.parentElement;
383
+ }
384
+ if (block && block !== this.editable) {
385
+ block.style.textAlign = align[cmd];
386
+ this.onContentChange();
387
+ }
388
+ }
389
+ setForeColor(color) {
390
+ const sel = window.getSelection();
391
+ if (!sel || sel.rangeCount === 0)
392
+ return;
393
+ const range = sel.getRangeAt(0);
394
+ if (range.collapsed)
395
+ return;
396
+ const span = document.createElement('span');
397
+ span.style.color = color;
398
+ span.appendChild(range.extractContents());
399
+ range.insertNode(span);
400
+ range.selectNodeContents(span);
401
+ sel.removeAllRanges();
402
+ sel.addRange(range);
403
+ this.onContentChange();
404
+ }
361
405
  sanitizeHTML(html) {
362
406
  const parser = new DOMParser();
363
407
  const doc = parser.parseFromString(html, 'text/html');
@@ -373,12 +417,12 @@ class Editor {
373
417
  }
374
418
  downloadHTML() {
375
419
  const content = this.sanitizeHTML(this.editable.innerHTML);
376
- const html = `<!doctype html>
377
- <html lang="en">
378
- <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
379
- <body>
380
- ${content}
381
- </body>
420
+ const html = `<!doctype html>
421
+ <html lang="en">
422
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
423
+ <body>
424
+ ${content}
425
+ </body>
382
426
  </html>`;
383
427
  const blob = new Blob([html], { type: 'text/html' });
384
428
  const a = document.createElement('a');