@kupola/kupola 1.2.0 → 1.3.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -286
  3. package/adapters/axios.d.ts +34 -0
  4. package/adapters/axios.js +122 -0
  5. package/adapters/navios-http.d.ts +110 -0
  6. package/adapters/navios-http.js +151 -0
  7. package/dist/css/accessibility.css +119 -0
  8. package/dist/css/animations.css +224 -0
  9. package/dist/css/brand-themes.css +300 -0
  10. package/dist/css/colors_and_type.css +441 -0
  11. package/dist/css/components-ext.css +4165 -0
  12. package/dist/css/components.css +1483 -0
  13. package/dist/css/responsive.css +697 -0
  14. package/dist/css/scaffold.css +145 -0
  15. package/dist/css/states.css +316 -0
  16. package/dist/css/theme-dark.css +296 -0
  17. package/dist/css/theme-light.css +296 -0
  18. package/dist/css/utilities.css +171 -0
  19. package/dist/kupola.cjs.js +219 -161
  20. package/dist/kupola.cjs.js.map +1 -1
  21. package/dist/kupola.esm.js +6643 -5709
  22. package/dist/kupola.esm.js.map +1 -1
  23. package/dist/kupola.umd.js +219 -161
  24. package/dist/kupola.umd.js.map +1 -1
  25. package/dist/plugins/vite-plugin-kupola.js +120 -0
  26. package/dist/types/kupola.d.ts +421 -323
  27. package/js/calendar.js +334 -25
  28. package/js/carousel.js +182 -48
  29. package/js/collapse.js +148 -34
  30. package/js/color-picker.js +416 -108
  31. package/js/component.js +8 -19
  32. package/js/countdown.js +9 -8
  33. package/js/data-bind.js +73 -16
  34. package/js/datepicker.js +488 -110
  35. package/js/depends.js +710 -0
  36. package/js/dialog.js +4 -2
  37. package/js/drawer.js +172 -8
  38. package/js/dropdown.js +272 -17
  39. package/js/dynamic-tags.js +156 -40
  40. package/js/fileupload.js +9 -8
  41. package/js/form.js +280 -254
  42. package/js/global-events.js +281 -188
  43. package/js/heatmap.js +10 -7
  44. package/js/i18n.js +18 -10
  45. package/js/icons.js +141 -161
  46. package/js/image-preview.js +146 -2
  47. package/js/initializer.js +113 -71
  48. package/js/kupola-core.js +123 -45
  49. package/js/kupola-lifecycle.js +13 -11
  50. package/js/message.js +8 -1
  51. package/js/modal.js +207 -59
  52. package/js/notification.js +8 -1
  53. package/js/numberinput.js +9 -8
  54. package/js/pagination.js +263 -0
  55. package/js/registry.js +29 -12
  56. package/js/select.js +482 -27
  57. package/js/slide-captcha.js +11 -2
  58. package/js/slider.js +442 -25
  59. package/js/statcard.js +9 -7
  60. package/js/table.js +1210 -0
  61. package/js/tag.js +268 -14
  62. package/js/theme.js +14 -43
  63. package/js/timepicker.js +335 -66
  64. package/js/tooltip.js +317 -86
  65. package/js/utils.js +6 -2
  66. package/js/validation.js +6 -2
  67. package/js/virtual-list.js +11 -7
  68. package/js/web-components.js +288 -0
  69. package/package.json +77 -67
  70. package/plugins/vite-plugin-kupola.js +120 -0
  71. package/types/kupola.d.ts +421 -323
  72. package/CHANGELOG.md +0 -130
  73. package/INTEGRATION.md +0 -440
  74. package/PROJECT_SUMMARY.md +0 -312
  75. package/SKILL.md +0 -572
  76. package/dist/utils/utils/Kupola.cs +0 -77
  77. package/dist/utils/utils/Kupola.java +0 -104
  78. package/dist/utils/utils/kupola.go +0 -120
  79. package/dist/utils/utils/kupola.js +0 -63
  80. package/dist/utils/utils/kupola.py +0 -1392
  81. package/dist/utils/utils/kupola.rb +0 -69
  82. package/js/composition-api.js +0 -458
  83. package/js/error-handler.js +0 -181
  84. package/js/http.js +0 -419
  85. package/js/kupola-devtools.js +0 -598
  86. package/js/performance.js +0 -250
  87. package/js/router.js +0 -396
  88. package/js/security.js +0 -189
  89. package/js/test-utils.js +0 -251
  90. package/templates/base.html +0 -30
  91. package/templates/base_dashboard.html +0 -99
  92. package/utils/Kupola.cs +0 -77
  93. package/utils/Kupola.java +0 -104
  94. package/utils/kupola.go +0 -120
  95. package/utils/kupola.js +0 -63
  96. package/utils/kupola.py +0 -1392
  97. package/utils/kupola.rb +0 -69
package/js/dialog.js CHANGED
@@ -126,6 +126,8 @@ class Dialog {
126
126
  }
127
127
  }
128
128
 
129
- if (typeof module !== 'undefined' && module.exports) {
130
- module.exports = Dialog;
129
+ export { Dialog };
130
+
131
+ if (typeof window !== 'undefined') {
132
+ window.Dialog = Dialog;
131
133
  }
package/js/drawer.js CHANGED
@@ -1,8 +1,21 @@
1
1
  class Drawer {
2
- constructor(element) {
2
+ constructor(element, options = {}) {
3
3
  this.element = element;
4
4
  this.mask = element.querySelector('.ds-drawer-mask');
5
5
  this.drawerEl = element.querySelector('.ds-drawer');
6
+
7
+ // Options
8
+ this.placement = options.placement || element.getAttribute('data-drawer-placement') || 'right'; // right | left | top | bottom
9
+ this.width = options.width || element.getAttribute('data-drawer-width') || '400px';
10
+ this.height = options.height || element.getAttribute('data-drawer-height') || '400px';
11
+ this.escClose = options.escClose !== false;
12
+ this.maskClosable = options.maskClosable !== false;
13
+ this.showMask = options.showMask !== false;
14
+ this.onOpen = options.onOpen || null;
15
+ this.onClose = options.onClose || null;
16
+ this.onBeforeClose = options.onBeforeClose || null;
17
+
18
+ this._keydownHandler = null;
6
19
  this._bindEvents();
7
20
  }
8
21
 
@@ -12,16 +25,25 @@ class Drawer {
12
25
  const confirmBtn = this.mask?.querySelector('.ds-drawer__footer .ds-btn--brand');
13
26
 
14
27
  this.closeDrawer = () => {
28
+ if (this.onBeforeClose) {
29
+ const result = this.onBeforeClose();
30
+ if (result === false) return;
31
+ }
32
+
15
33
  if (this.mask) {
16
34
  this.mask.classList.remove('is-visible');
17
35
  }
18
36
  if (this.drawerEl) {
19
37
  this.drawerEl.classList.remove('is-visible');
20
38
  }
39
+ document.body.style.overflow = '';
40
+
41
+ if (this.onClose) this.onClose();
42
+ this.element.dispatchEvent(new CustomEvent('kupola:drawer-close', { bubbles: true }));
21
43
  };
22
44
 
23
45
  this.handleMaskClick = (e) => {
24
- if (e.target === this.mask) {
46
+ if (this.maskClosable && e.target === this.mask) {
25
47
  this.closeDrawer();
26
48
  }
27
49
  };
@@ -42,6 +64,16 @@ class Drawer {
42
64
  confirmBtn.addEventListener('click', this.closeDrawer);
43
65
  }
44
66
 
67
+ // ESC key
68
+ if (this.escClose) {
69
+ this._keydownHandler = (e) => {
70
+ if (e.key === 'Escape' && this.drawerEl?.classList.contains('is-visible')) {
71
+ this.closeDrawer();
72
+ }
73
+ };
74
+ document.addEventListener('keydown', this._keydownHandler);
75
+ }
76
+
45
77
  this._listeners = [
46
78
  { el: this.mask, event: 'click', handler: this.handleMaskClick },
47
79
  { el: closeBtn, event: 'click', handler: this.closeDrawer },
@@ -50,10 +82,35 @@ class Drawer {
50
82
  ].filter(item => item.el);
51
83
  }
52
84
 
85
+ _applyPlacement() {
86
+ if (!this.drawerEl) return;
87
+
88
+ // Set direction class
89
+ this.drawerEl.classList.remove('ds-drawer--right', 'ds-drawer--left', 'ds-drawer--top', 'ds-drawer--bottom');
90
+ this.drawerEl.classList.add(`ds-drawer--${this.placement}`);
91
+
92
+ // Set size
93
+ if (this.placement === 'left' || this.placement === 'right') {
94
+ this.drawerEl.style.width = this.width;
95
+ } else {
96
+ this.drawerEl.style.height = this.height;
97
+ }
98
+
99
+ // Hide mask if configured
100
+ if (!this.showMask && this.mask) {
101
+ this.mask.style.background = 'transparent';
102
+ this.mask.style.pointerEvents = 'none';
103
+ this.drawerEl.style.boxShadow = '0 0 24px rgba(0,0,0,0.15)';
104
+ }
105
+ }
106
+
53
107
  destroy() {
54
108
  this._listeners?.forEach(({ el, event, handler }) => {
55
109
  el.removeEventListener(event, handler);
56
110
  });
111
+ if (this._keydownHandler) {
112
+ document.removeEventListener('keydown', this._keydownHandler);
113
+ }
57
114
  this._listeners = null;
58
115
  this.mask = null;
59
116
  this.drawerEl = null;
@@ -61,23 +118,116 @@ class Drawer {
61
118
  }
62
119
 
63
120
  open() {
121
+ this._applyPlacement();
122
+
64
123
  if (this.mask) {
65
124
  this.mask.classList.add('is-visible');
66
125
  }
67
126
  if (this.drawerEl) {
68
127
  this.drawerEl.classList.add('is-visible');
69
128
  }
129
+ document.body.style.overflow = 'hidden';
130
+
131
+ if (this.onOpen) this.onOpen();
132
+ this.element.dispatchEvent(new CustomEvent('kupola:drawer-open', { bubbles: true }));
70
133
  }
71
134
 
72
135
  close() {
73
136
  this.closeDrawer();
74
137
  }
138
+
139
+ isOpen() {
140
+ return this.drawerEl?.classList.contains('is-visible') || false;
141
+ }
75
142
  }
76
143
 
77
- function initDrawer(element) {
144
+ function createDrawer(options = {}) {
145
+ const {
146
+ title = '',
147
+ content = '',
148
+ html = false,
149
+ placement = 'right',
150
+ width = '400px',
151
+ height = '400px',
152
+ closable = true,
153
+ showMask = true,
154
+ footer = null,
155
+ onClose,
156
+ onOpen
157
+ } = options;
158
+
159
+ const container = document.createElement('div');
160
+ container.className = 'ds-drawer-container';
161
+
162
+ let footerHTML = '';
163
+ if (footer !== null) {
164
+ if (typeof footer === 'string') {
165
+ footerHTML = `<div class="ds-drawer__footer">${footer}</div>`;
166
+ } else {
167
+ footerHTML = `<div class="ds-drawer__footer">
168
+ <button class="ds-btn ds-btn--ghost" data-drawer-cancel>Cancel</button>
169
+ <button class="ds-btn ds-btn--brand" data-drawer-confirm>OK</button>
170
+ </div>`;
171
+ }
172
+ }
173
+
174
+ container.innerHTML = `
175
+ <div class="ds-drawer-mask">
176
+ <div class="ds-drawer ds-drawer--${placement}" style="${placement === 'left' || placement === 'right' ? 'width:' + width : 'height:' + height}">
177
+ <div class="ds-drawer__header">
178
+ <span class="ds-drawer__title"></span>
179
+ ${closable ? `<button class="ds-drawer__close" aria-label="Close">
180
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <path d="M18 6L6 18M6 6l12 12"/>
182
+ </svg>
183
+ </button>` : ''}
184
+ </div>
185
+ <div class="ds-drawer__body"></div>
186
+ ${footerHTML}
187
+ </div>
188
+ </div>
189
+ `;
190
+
191
+ document.body.appendChild(container);
192
+
193
+ const drawer = new Drawer(container, { placement, width, height, showMask, onClose, onOpen });
194
+
195
+ const titleEl = container.querySelector('.ds-drawer__title');
196
+ if (titleEl) titleEl.textContent = title;
197
+ const bodyEl = container.querySelector('.ds-drawer__body');
198
+ if (bodyEl) {
199
+ if (html) bodyEl.innerHTML = content;
200
+ else bodyEl.textContent = content;
201
+ }
202
+
203
+ const confirmBtn = container.querySelector('[data-drawer-confirm]');
204
+ const cancelBtn = container.querySelector('[data-drawer-cancel]');
205
+
206
+ if (cancelBtn) cancelBtn.addEventListener('click', () => drawer.close());
207
+ if (confirmBtn) {
208
+ confirmBtn.addEventListener('click', () => {
209
+ if (options.onConfirm) options.onConfirm();
210
+ drawer.close();
211
+ });
212
+ }
213
+
214
+ const origClose = drawer.closeDrawer;
215
+ drawer.closeDrawer = () => {
216
+ origClose();
217
+ setTimeout(() => {
218
+ drawer.destroy();
219
+ container.remove();
220
+ }, 300);
221
+ };
222
+
223
+ drawer.open();
224
+ return drawer;
225
+ }
226
+
227
+ function initDrawer(element, options) {
78
228
  if (element.__kupolaInitialized) return;
79
229
 
80
- const drawer = new Drawer(element);
230
+ const drawer = new Drawer(element, options);
81
231
  element.__kupolaInstance = drawer;
82
232
  element.__kupolaInitialized = true;
83
233
  }
@@ -100,7 +250,11 @@ function initDrawers() {
100
250
 
101
251
  if (!drawer) return;
102
252
 
103
- initDrawer(drawer);
253
+ initDrawer(drawer, {
254
+ placement: trigger.getAttribute('data-drawer-placement') || 'right',
255
+ width: trigger.getAttribute('data-drawer-width'),
256
+ height: trigger.getAttribute('data-drawer-height')
257
+ });
104
258
  drawer.__kupolaInstance?.open();
105
259
  });
106
260
  });
@@ -113,6 +267,16 @@ function initDrawers() {
113
267
  });
114
268
  }
115
269
 
116
- if (window.kupolaInitializer) {
117
- window.kupolaInitializer.register('drawer', initDrawer, cleanupDrawer);
118
- }
270
+ export { Drawer, initDrawer, initDrawers, cleanupDrawer, createDrawer };
271
+
272
+ if (typeof window !== 'undefined') {
273
+ window.Drawer = Drawer;
274
+ window.initDrawer = initDrawer;
275
+ window.initDrawers = initDrawers;
276
+ window.cleanupDrawer = cleanupDrawer;
277
+ window.createDrawer = createDrawer;
278
+
279
+ if (window.kupolaInitializer) {
280
+ window.kupolaInitializer.register('drawer', initDrawer, cleanupDrawer);
281
+ }
282
+ }
package/js/dropdown.js CHANGED
@@ -1,41 +1,155 @@
1
1
  class Dropdown {
2
- constructor(element) {
2
+ constructor(element, options = {}) {
3
3
  this.element = element;
4
4
  this.trigger = element.querySelector('.ds-dropdown__trigger');
5
5
  this.menu = element.querySelector('.ds-dropdown__menu');
6
6
  this.triggerText = this.trigger ? this.trigger.querySelector('span') : null;
7
7
  this.scope = `dropdown-${Math.random().toString(36).substr(2, 9)}`;
8
8
 
9
+ // Options
10
+ this.triggerMode = options.trigger || element.getAttribute('data-dropdown-trigger') || 'click'; // click | hover
11
+ this.hoverDelay = options.hoverDelay || parseInt(element.getAttribute('data-dropdown-hover-delay')) || 150;
12
+ this.disabled = options.disabled || element.hasAttribute('data-dropdown-disabled');
13
+ this.keyboardNav = options.keyboardNav !== false;
14
+ this.autoPosition = options.autoPosition !== false;
15
+ this.onSelect = options.onSelect || null;
16
+ this.onShow = options.onShow || null;
17
+ this.onHide = options.onHide || null;
18
+
19
+ this.isOpen = false;
20
+ this.focusIndex = -1;
21
+ this._hoverTimer = null;
22
+ this._hoverLeaveTimer = null;
23
+
9
24
  this._triggerClickHandler = null;
10
25
  this._documentClickHandler = null;
11
26
  this._documentClickListener = null;
12
27
  this._itemClickHandler = null;
28
+ this._keydownHandler = null;
29
+ this._mouseenterHandler = null;
30
+ this._mouseleaveHandler = null;
31
+ this._triggerMouseenterHandler = null;
32
+ this._triggerMouseleaveHandler = null;
13
33
  }
14
34
 
15
35
  init() {
16
36
  if (!this.trigger || !this.menu) return;
17
37
  if (this.element.__kupolaInitialized) return;
18
38
 
39
+ // Item click handler
19
40
  this._itemClickHandler = (e) => {
20
41
  const item = e.currentTarget;
21
- if (this.triggerText) {
42
+ if (item.classList.contains('is-disabled') || item.classList.contains('ds-dropdown__divider')) return;
43
+
44
+ if (this.triggerText && !item.hasAttribute('data-no-update-trigger')) {
22
45
  this.triggerText.textContent = item.textContent.trim();
23
46
  }
47
+
48
+ if (this.onSelect) {
49
+ this.onSelect({ item, value: item.getAttribute('data-value'), text: item.textContent.trim() });
50
+ }
51
+
24
52
  this.hideMenu();
53
+ this.trigger.focus();
25
54
  };
26
55
 
27
- this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
28
- item.addEventListener('click', this._itemClickHandler);
29
- item._dropdownItemClickHandler = this._itemClickHandler;
30
- });
56
+ this._bindMenuItems();
31
57
 
58
+ // Trigger click
32
59
  this._triggerClickHandler = (e) => {
33
60
  e.stopPropagation();
61
+ if (this.disabled) return;
34
62
  this.toggleMenu();
35
63
  };
36
64
 
37
- this.trigger.addEventListener('click', this._triggerClickHandler);
65
+ // Hover trigger
66
+ this._triggerMouseenterHandler = () => {
67
+ if (this.disabled || this.triggerMode !== 'hover') return;
68
+ clearTimeout(this._hoverLeaveTimer);
69
+ this._hoverTimer = setTimeout(() => this.showMenu(), this.hoverDelay);
70
+ };
71
+
72
+ this._triggerMouseleaveHandler = () => {
73
+ if (this.triggerMode !== 'hover') return;
74
+ clearTimeout(this._hoverTimer);
75
+ this._hoverLeaveTimer = setTimeout(() => this.hideMenu(), this.hoverDelay);
76
+ };
77
+
78
+ this._mouseenterHandler = () => {
79
+ if (this.disabled || this.triggerMode !== 'hover') return;
80
+ clearTimeout(this._hoverLeaveTimer);
81
+ };
82
+
83
+ this._mouseleaveHandler = () => {
84
+ if (this.triggerMode !== 'hover') return;
85
+ this._hoverLeaveTimer = setTimeout(() => this.hideMenu(), this.hoverDelay);
86
+ };
87
+
88
+ // Keyboard navigation
89
+ this._keydownHandler = (e) => {
90
+ if (!this.isOpen || this.disabled) return;
91
+
92
+ const items = this._getNavigableItems();
93
+ if (!items.length) return;
94
+
95
+ switch (e.key) {
96
+ case 'ArrowDown':
97
+ e.preventDefault();
98
+ this.focusIndex = Math.min(this.focusIndex + 1, items.length - 1);
99
+ this._focusItem(items);
100
+ break;
101
+ case 'ArrowUp':
102
+ e.preventDefault();
103
+ this.focusIndex = Math.max(this.focusIndex - 1, 0);
104
+ this._focusItem(items);
105
+ break;
106
+ case 'Enter':
107
+ case ' ':
108
+ e.preventDefault();
109
+ if (this.focusIndex >= 0 && items[this.focusIndex]) {
110
+ items[this.focusIndex].click();
111
+ }
112
+ break;
113
+ case 'Escape':
114
+ e.preventDefault();
115
+ this.hideMenu();
116
+ this.trigger.focus();
117
+ break;
118
+ case 'Home':
119
+ e.preventDefault();
120
+ this.focusIndex = 0;
121
+ this._focusItem(items);
122
+ break;
123
+ case 'End':
124
+ e.preventDefault();
125
+ this.focusIndex = items.length - 1;
126
+ this._focusItem(items);
127
+ break;
128
+ }
129
+ };
130
+
131
+ // Bind trigger events
132
+ if (this.triggerMode === 'hover') {
133
+ this.trigger.addEventListener('mouseenter', this._triggerMouseenterHandler);
134
+ this.trigger.addEventListener('mouseleave', this._triggerMouseleaveHandler);
135
+ this.menu.addEventListener('mouseenter', this._mouseenterHandler);
136
+ this.menu.addEventListener('mouseleave', this._mouseleaveHandler);
137
+ } else {
138
+ this.trigger.addEventListener('click', this._triggerClickHandler);
139
+ }
38
140
 
141
+ // Keyboard on trigger
142
+ this.trigger.addEventListener('keydown', (e) => {
143
+ if (this.disabled) return;
144
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
145
+ e.preventDefault();
146
+ this.showMenu();
147
+ }
148
+ });
149
+
150
+ document.addEventListener('keydown', this._keydownHandler);
151
+
152
+ // Document click to close
39
153
  this._documentClickHandler = (e) => {
40
154
  if (!this.element.contains(e.target)) {
41
155
  this.hideMenu();
@@ -52,22 +166,144 @@ class Dropdown {
52
166
  this.element.__kupolaInitialized = true;
53
167
  }
54
168
 
55
- toggleMenu() {
56
- const isOpen = this.menu.style.display === 'block';
57
- this.menu.style.display = isOpen ? 'none' : 'block';
58
- this.element.classList.toggle('is-open', !isOpen);
169
+ _bindMenuItems() {
170
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
171
+ item.addEventListener('click', this._itemClickHandler);
172
+ item._dropdownItemClickHandler = this._itemClickHandler;
173
+ });
174
+ }
175
+
176
+ _getNavigableItems() {
177
+ return Array.from(this.menu.querySelectorAll('.ds-dropdown__item'))
178
+ .filter(item => !item.classList.contains('is-disabled') && !item.classList.contains('ds-dropdown__divider'));
179
+ }
180
+
181
+ _focusItem(items) {
182
+ items.forEach(item => item.classList.remove('is-focused'));
183
+ if (items[this.focusIndex]) {
184
+ items[this.focusIndex].classList.add('is-focused');
185
+ items[this.focusIndex].scrollIntoView({ block: 'nearest' });
186
+ }
187
+ }
188
+
189
+ _calculatePosition() {
190
+ if (!this.autoPosition) return;
191
+
192
+ const triggerRect = this.element.getBoundingClientRect();
193
+ const menuRect = this.menu.getBoundingClientRect();
194
+ const viewportHeight = window.innerHeight;
195
+ const viewportWidth = window.innerWidth;
196
+
197
+ // Reset
198
+ this.menu.classList.remove('ds-dropdown--top', 'ds-dropdown--right', 'ds-dropdown--dropup');
199
+
200
+ // Vertical: prefer below, flip to above if needed
201
+ const spaceBelow = viewportHeight - triggerRect.bottom;
202
+ const spaceAbove = triggerRect.top;
203
+
204
+ if (spaceBelow < menuRect.height && spaceAbove > spaceBelow) {
205
+ this.menu.classList.add('ds-dropdown--dropup');
206
+ this.menu.style.top = 'auto';
207
+ this.menu.style.bottom = '100%';
208
+ this.menu.style.marginBottom = '4px';
209
+ } else {
210
+ this.menu.style.top = '100%';
211
+ this.menu.style.bottom = 'auto';
212
+ this.menu.style.marginBottom = '0';
213
+ }
214
+
215
+ // Horizontal: prevent overflow right
216
+ if (triggerRect.left + menuRect.width > viewportWidth) {
217
+ this.menu.style.left = 'auto';
218
+ this.menu.style.right = '0';
219
+ } else {
220
+ this.menu.style.left = '0';
221
+ this.menu.style.right = 'auto';
222
+ }
223
+ }
224
+
225
+ showMenu() {
226
+ if (this.disabled || this.isOpen) return;
227
+ this.isOpen = true;
228
+ this.focusIndex = -1;
229
+ this.menu.style.display = 'block';
230
+ this.element.classList.add('is-open');
231
+ this._calculatePosition();
232
+ if (this.onShow) this.onShow();
233
+ this.element.dispatchEvent(new CustomEvent('kupola:dropdown-show', { bubbles: true }));
59
234
  }
60
235
 
61
236
  hideMenu() {
237
+ if (!this.isOpen) return;
238
+ this.isOpen = false;
62
239
  this.menu.style.display = 'none';
63
240
  this.element.classList.remove('is-open');
241
+
242
+ // Clear focus
243
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => item.classList.remove('is-focused'));
244
+ if (this.onHide) this.onHide();
245
+ this.element.dispatchEvent(new CustomEvent('kupola:dropdown-hide', { bubbles: true }));
246
+ }
247
+
248
+ toggleMenu() {
249
+ if (this.isOpen) {
250
+ this.hideMenu();
251
+ } else {
252
+ this.showMenu();
253
+ }
254
+ }
255
+
256
+ enable() {
257
+ this.disabled = false;
258
+ this.element.removeAttribute('data-dropdown-disabled');
259
+ }
260
+
261
+ disable() {
262
+ this.disabled = true;
263
+ this.element.setAttribute('data-dropdown-disabled', '');
264
+ this.hideMenu();
265
+ }
266
+
267
+ // Update menu items dynamically
268
+ setItems(items) {
269
+ // Remove old item listeners
270
+ this.menu.querySelectorAll('.ds-dropdown__item').forEach(item => {
271
+ if (item._dropdownItemClickHandler) {
272
+ item.removeEventListener('click', item._dropdownItemClickHandler);
273
+ }
274
+ });
275
+
276
+ // Rebuild menu content
277
+ this.menu.innerHTML = '';
278
+ items.forEach(item => {
279
+ if (item.type === 'divider') {
280
+ const divider = document.createElement('div');
281
+ divider.className = 'ds-dropdown__divider';
282
+ this.menu.appendChild(divider);
283
+ } else {
284
+ const el = document.createElement('div');
285
+ el.className = 'ds-dropdown__item' + (item.disabled ? ' is-disabled' : '') + (item.active ? ' is-selected' : '');
286
+ el.textContent = item.text || item.label || '';
287
+ if (item.value !== undefined) el.setAttribute('data-value', item.value);
288
+ if (item.icon) el.innerHTML = item.icon + el.innerHTML;
289
+ if (item.disabled) el.classList.add('is-disabled');
290
+ this.menu.appendChild(el);
291
+ }
292
+ });
293
+
294
+ this._bindMenuItems();
64
295
  }
65
296
 
66
297
  destroy() {
67
298
  if (!this.element.__kupolaInitialized) return;
68
299
 
69
- if (this.trigger && this._triggerClickHandler) {
70
- this.trigger.removeEventListener('click', this._triggerClickHandler);
300
+ clearTimeout(this._hoverTimer);
301
+ clearTimeout(this._hoverLeaveTimer);
302
+
303
+ if (this.trigger) {
304
+ if (this._triggerClickHandler) this.trigger.removeEventListener('click', this._triggerClickHandler);
305
+ if (this._triggerMouseenterHandler) this.trigger.removeEventListener('mouseenter', this._triggerMouseenterHandler);
306
+ if (this._triggerMouseleaveHandler) this.trigger.removeEventListener('mouseleave', this._triggerMouseleaveHandler);
71
307
  }
72
308
 
73
309
  if (this.menu) {
@@ -76,8 +312,12 @@ class Dropdown {
76
312
  item.removeEventListener('click', item._dropdownItemClickHandler);
77
313
  }
78
314
  });
315
+ if (this._mouseenterHandler) this.menu.removeEventListener('mouseenter', this._mouseenterHandler);
316
+ if (this._mouseleaveHandler) this.menu.removeEventListener('mouseleave', this._mouseleaveHandler);
79
317
  }
80
318
 
319
+ if (this._keydownHandler) document.removeEventListener('keydown', this._keydownHandler);
320
+
81
321
  if (this._documentClickListener && this._documentClickListener.unsubscribe) {
82
322
  this._documentClickListener.unsubscribe();
83
323
  } else if (this._documentClickHandler) {
@@ -88,12 +328,17 @@ class Dropdown {
88
328
  this._documentClickListener = null;
89
329
  this._triggerClickHandler = null;
90
330
  this._itemClickHandler = null;
331
+ this._keydownHandler = null;
332
+ this._mouseenterHandler = null;
333
+ this._mouseleaveHandler = null;
334
+ this._triggerMouseenterHandler = null;
335
+ this._triggerMouseleaveHandler = null;
91
336
  this.element.__kupolaInitialized = false;
92
337
  }
93
338
  }
94
339
 
95
- function initDropdown(element) {
96
- const dropdown = new Dropdown(element);
340
+ function initDropdown(element, options) {
341
+ const dropdown = new Dropdown(element, options);
97
342
  dropdown.init();
98
343
  element._kupolaDropdown = dropdown;
99
344
  }
@@ -117,6 +362,16 @@ function cleanupAllDropdowns() {
117
362
  });
118
363
  }
119
364
 
120
- if (window.kupolaInitializer) {
121
- window.kupolaInitializer.register('dropdown', initDropdown, cleanupDropdown);
365
+ export { Dropdown, initDropdown, initDropdowns, cleanupDropdown, cleanupAllDropdowns };
366
+
367
+ if (typeof window !== 'undefined') {
368
+ window.Dropdown = Dropdown;
369
+ window.initDropdown = initDropdown;
370
+ window.initDropdowns = initDropdowns;
371
+ window.cleanupDropdown = cleanupDropdown;
372
+ window.cleanupAllDropdowns = cleanupAllDropdowns;
373
+
374
+ if (window.kupolaInitializer) {
375
+ window.kupolaInitializer.register('dropdown', initDropdown, cleanupDropdown);
376
+ }
122
377
  }