@dodlhuat/basix 1.2.0 → 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.
- package/README.md +56 -1
- package/css/accordion.scss +86 -87
- package/css/alert.scss +137 -137
- package/css/button.scss +48 -0
- package/css/calendar.scss +957 -0
- package/css/card.scss +65 -65
- package/css/chart.scss +270 -157
- package/css/chat-bubbles.scss +134 -68
- package/css/chips.scss +109 -19
- package/css/colors.scss +32 -32
- package/css/datepicker.scss +336 -336
- package/css/defaults.scss +90 -90
- package/css/docs.scss +529 -0
- package/css/editor.scss +36 -0
- package/css/file-uploader.scss +1 -1
- package/css/flyout-menu.scss +361 -361
- package/css/form.scss +0 -15
- package/css/gallery.scss +65 -6
- package/css/grid.scss +41 -40
- package/css/group-picker.scss +345 -0
- package/css/guitar-chords.css +250 -250
- package/css/icons.scss +330 -330
- package/css/parameters.scss +3 -3
- package/css/placeholder.scss +33 -33
- package/css/popover.scss +206 -0
- package/css/progress.scss +76 -32
- package/css/properties.scss +51 -36
- package/css/push-menu.scss +302 -174
- package/css/reset.scss +39 -39
- package/css/scrollbar.scss +62 -5
- package/css/sidebar-nav.scss +92 -0
- package/css/spinner.scss +65 -65
- package/css/stepper.scss +48 -12
- package/css/style.css +3159 -254
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.scss +51 -45
- package/css/table.scss +199 -199
- package/css/tabs.scss +154 -123
- package/css/timeline.scss +83 -38
- package/css/timepicker.scss +100 -5
- package/css/toast.scss +81 -81
- package/css/virtual-dropdown.scss +35 -29
- package/js/calendar.js +532 -0
- package/js/calendar.ts +706 -0
- package/js/chart.js +573 -257
- package/js/chart.ts +692 -0
- package/js/code-viewer.js +10 -10
- package/js/code-viewer.ts +188 -188
- package/js/datepicker.ts +627 -627
- package/js/docs-nav.js +204 -0
- package/js/dropdown.ts +179 -179
- package/js/editor.js +50 -6
- package/js/editor.ts +483 -444
- package/js/file-uploader.js +1 -0
- package/js/file-uploader.ts +1 -0
- package/js/flyout-menu.js +14 -14
- package/js/flyout-menu.ts +249 -249
- package/js/form-builder.js +106 -106
- package/js/gallery.js +14 -8
- package/js/gallery.ts +245 -236
- package/js/group-picker.js +342 -0
- package/js/group-picker.ts +447 -0
- package/js/guitar-chords.js +268 -268
- package/js/lazy-loader.js +121 -121
- package/js/modal.ts +166 -166
- package/js/popover.js +163 -0
- package/js/popover.ts +219 -0
- package/js/position.js +108 -0
- package/js/position.ts +111 -0
- package/js/push-menu.js +113 -0
- package/js/push-menu.ts +284 -145
- package/js/request.js +50 -50
- package/js/scroll.ts +47 -47
- package/js/scrollbar.js +13 -0
- package/js/scrollbar.ts +324 -307
- package/js/select.ts +216 -216
- package/js/sidebar-nav.js +41 -0
- package/js/sidebar-nav.ts +66 -0
- package/js/table.ts +452 -452
- package/js/tabs.ts +279 -279
- package/js/theme.js +17 -6
- package/js/theme.ts +234 -224
- package/js/toast.ts +137 -137
- package/js/tooltip.js +6 -60
- package/js/tooltip.ts +184 -251
- package/js/tsconfig.json +18 -18
- package/js/utils.ts +83 -83
- package/js/virtual-dropdown.js +25 -25
- package/js/virtual-dropdown.ts +365 -365
- package/package.json +39 -39
- package/js/index.js +0 -816
- 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');
|