@doyosi/laraisy 1.0.2 → 1.0.3

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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +1 -1
  3. package/src/CodeInput.js +48 -48
  4. package/src/DSAlert.js +352 -352
  5. package/src/DSAvatar.js +207 -207
  6. package/src/DSDelete.js +274 -274
  7. package/src/DSForm.js +568 -568
  8. package/src/DSGridOrTable.js +453 -453
  9. package/src/DSLocaleSwitcher.js +239 -239
  10. package/src/DSLogout.js +293 -293
  11. package/src/DSNotifications.js +365 -365
  12. package/src/DSRestore.js +181 -181
  13. package/src/DSSelect.js +1071 -1071
  14. package/src/DSSelectBox.js +563 -563
  15. package/src/DSSimpleSlider.js +517 -517
  16. package/src/DSSvgFetch.js +69 -69
  17. package/src/DSTable/DSTableExport.js +68 -68
  18. package/src/DSTable/DSTableFilter.js +224 -224
  19. package/src/DSTable/DSTablePagination.js +136 -136
  20. package/src/DSTable/DSTableSearch.js +40 -40
  21. package/src/DSTable/DSTableSelection.js +192 -192
  22. package/src/DSTable/DSTableSort.js +58 -58
  23. package/src/DSTable.js +353 -353
  24. package/src/DSTabs.js +488 -488
  25. package/src/DSUpload.js +887 -887
  26. package/dist/CodeInput.d.ts +0 -10
  27. package/dist/DSAlert.d.ts +0 -112
  28. package/dist/DSAvatar.d.ts +0 -45
  29. package/dist/DSDelete.d.ts +0 -61
  30. package/dist/DSForm.d.ts +0 -151
  31. package/dist/DSGridOrTable/DSGOTRenderer.d.ts +0 -60
  32. package/dist/DSGridOrTable/DSGOTViewToggle.d.ts +0 -26
  33. package/dist/DSGridOrTable.d.ts +0 -296
  34. package/dist/DSLocaleSwitcher.d.ts +0 -71
  35. package/dist/DSLogout.d.ts +0 -76
  36. package/dist/DSNotifications.d.ts +0 -54
  37. package/dist/DSRestore.d.ts +0 -56
  38. package/dist/DSSelect.d.ts +0 -221
  39. package/dist/DSSelectBox.d.ts +0 -123
  40. package/dist/DSSimpleSlider.d.ts +0 -136
  41. package/dist/DSSvgFetch.d.ts +0 -17
  42. package/dist/DSTable/DSTableExport.d.ts +0 -11
  43. package/dist/DSTable/DSTableFilter.d.ts +0 -40
  44. package/dist/DSTable/DSTablePagination.d.ts +0 -12
  45. package/dist/DSTable/DSTableSearch.d.ts +0 -8
  46. package/dist/DSTable/DSTableSelection.d.ts +0 -46
  47. package/dist/DSTable/DSTableSort.d.ts +0 -8
  48. package/dist/DSTable.d.ts +0 -116
  49. package/dist/DSTabs.d.ts +0 -156
  50. package/dist/DSUpload.d.ts +0 -220
  51. package/dist/index.d.ts +0 -17
package/src/DSTabs.js CHANGED
@@ -1,488 +1,488 @@
1
- /**
2
- * DSTabs
3
- *
4
- * A lightweight tab switching component with:
5
- * - Button/link click handlers for tab switching
6
- * - Radio input synchronization
7
- * - Active state management with disabled/enabled buttons
8
- * - CSS class-based content show/hide
9
- * - Data attribute configuration
10
- * - Full event system
11
- *
12
- * @example
13
- * // HTML structure:
14
- * // Buttons: <button data-tab="primary">Primary</button>
15
- * // Radio inputs: <input type="radio" data-tab="primary" checked />
16
- * // Tab content: <div class="tab-content">...</div>
17
- */
18
- export class DSTabs {
19
- static instances = new Map();
20
- static instanceCounter = 0;
21
-
22
- /**
23
- * Default configuration
24
- */
25
- static defaults = {
26
- // Selectors
27
- buttonSelector: 'button[data-tab], a[data-tab]', // Selector for tab buttons/links (excludes inputs)
28
- radioSelector: 'input[type="radio"][data-tab]', // Selector for hidden radio inputs
29
- contentSelector: '.tab-content', // Selector for tab content containers
30
- tabsContainer: '.tabs', // Container that holds radios and content
31
-
32
- // Behavior
33
- activeClass: 'active', // Class to add to active button
34
- disableActive: true, // Disable the active button
35
- showFirst: true, // Auto-show first tab on init
36
-
37
- // Styling
38
- buttonActiveClass: 'btn-active', // Additional class for active button
39
- contentHiddenClass: 'hidden', // Class to hide inactive content
40
-
41
- // Callbacks
42
- onTabChange: null, // Callback when tab changes (tabName, prevTabName)
43
- };
44
-
45
- /**
46
- * @param {string|HTMLElement} containerSelector - Container element or selector
47
- * @param {Object} config - Configuration options
48
- */
49
- constructor(containerSelector, config = {}) {
50
- this.instanceId = `ds-tabs-${++DSTabs.instanceCounter}`;
51
-
52
- // Find the container element
53
- const el = typeof containerSelector === 'string'
54
- ? document.querySelector(containerSelector)
55
- : containerSelector;
56
-
57
- if (!el) {
58
- throw new Error('DSTabs: Container element not found.');
59
- }
60
-
61
- this.container = el;
62
-
63
- // Merge config with data attributes
64
- this.cfg = this._buildConfig(config);
65
-
66
- // State
67
- this._currentTab = null;
68
- this._prevTab = null;
69
-
70
- // Event listeners
71
- this._listeners = {};
72
- this._boundHandlers = {};
73
-
74
- // Initialize
75
- this._init();
76
-
77
- // Register instance
78
- DSTabs.instances.set(this.instanceId, this);
79
- this.container.dataset.dsTabsId = this.instanceId;
80
- }
81
-
82
- /**
83
- * Static factory method
84
- */
85
- static create(containerSelector, config = {}) {
86
- return new DSTabs(containerSelector, config);
87
- }
88
-
89
- /**
90
- * Get instance by element
91
- */
92
- static getInstance(element) {
93
- const el = typeof element === 'string' ? document.querySelector(element) : element;
94
- if (!el) return null;
95
- const container = el.closest('[data-ds-tabs-id]');
96
- if (!container) return null;
97
- return DSTabs.instances.get(container.dataset.dsTabsId);
98
- }
99
-
100
- /**
101
- * Auto-initialize all elements with [data-ds-tabs]
102
- */
103
- static initAll(selector = '[data-ds-tabs]') {
104
- document.querySelectorAll(selector).forEach(el => {
105
- if (!el.closest('[data-ds-tabs-id]')) {
106
- new DSTabs(el);
107
- }
108
- });
109
- }
110
-
111
- // ==================== INITIALIZATION ====================
112
-
113
- _buildConfig(userConfig) {
114
- const dataConfig = this._parseDataAttributes();
115
- return { ...DSTabs.defaults, ...dataConfig, ...userConfig };
116
- }
117
-
118
- _parseDataAttributes() {
119
- const data = this.container.dataset;
120
- const config = {};
121
-
122
- if (data.buttonSelector) config.buttonSelector = data.buttonSelector;
123
- if (data.radioSelector) config.radioSelector = data.radioSelector;
124
- if (data.contentSelector) config.contentSelector = data.contentSelector;
125
- if (data.tabsContainer) config.tabsContainer = data.tabsContainer;
126
- if (data.activeClass) config.activeClass = data.activeClass;
127
- if (data.disableActive !== undefined) config.disableActive = data.disableActive !== 'false';
128
- if (data.showFirst !== undefined) config.showFirst = data.showFirst !== 'false';
129
-
130
- return config;
131
- }
132
-
133
- _init() {
134
- this._cacheElements();
135
- this._bindEvents();
136
- this._initActiveTab();
137
-
138
- this._emit('ready', { activeTab: this._currentTab });
139
- }
140
-
141
- _cacheElements() {
142
- // Find all buttons with data-tab attribute within the container
143
- this.buttons = Array.from(this.container.querySelectorAll(this.cfg.buttonSelector));
144
-
145
- // Find the tabs container
146
- this.tabsContainer = this.container.querySelector(this.cfg.tabsContainer);
147
-
148
- // Find all radio inputs and content pairs within the tabs container
149
- if (this.tabsContainer) {
150
- this.radios = Array.from(this.tabsContainer.querySelectorAll(this.cfg.radioSelector));
151
- this.contents = Array.from(this.tabsContainer.querySelectorAll(this.cfg.contentSelector));
152
- } else {
153
- this.radios = [];
154
- this.contents = [];
155
- }
156
-
157
- // Build a map of tab name -> { button, radio, content }
158
- this._tabMap = new Map();
159
-
160
- this.buttons.forEach(button => {
161
- const tabName = button.dataset.tab;
162
- if (!this._tabMap.has(tabName)) {
163
- this._tabMap.set(tabName, { buttons: [], radio: null, content: null });
164
- }
165
- this._tabMap.get(tabName).buttons.push(button);
166
- });
167
-
168
- // Associate radios with their following content divs
169
- this.radios.forEach((radio, index) => {
170
- const tabName = radio.dataset.tab;
171
- if (this._tabMap.has(tabName)) {
172
- this._tabMap.get(tabName).radio = radio;
173
- // The content div follows the radio input
174
- if (this.contents[index]) {
175
- this._tabMap.get(tabName).content = this.contents[index];
176
- }
177
- }
178
- });
179
- }
180
-
181
- _bindEvents() {
182
- this._boundHandlers.onButtonClick = this._onButtonClick.bind(this);
183
-
184
- this.buttons.forEach(button => {
185
- button.addEventListener('click', this._boundHandlers.onButtonClick);
186
- });
187
- }
188
-
189
- _initActiveTab() {
190
- // Find the initially checked radio or first tab
191
- let activeTabName = null;
192
-
193
- // Check for checked radio
194
- const checkedRadio = this.radios.find(r => r.checked);
195
- if (checkedRadio) {
196
- activeTabName = checkedRadio.dataset.tab;
197
- }
198
-
199
- // Or check for disabled button (indicates active)
200
- if (!activeTabName) {
201
- const disabledButton = this.buttons.find(b => b.disabled);
202
- if (disabledButton) {
203
- activeTabName = disabledButton.dataset.tab;
204
- }
205
- }
206
-
207
- // Or use first tab if showFirst is enabled
208
- if (!activeTabName && this.cfg.showFirst && this._tabMap.size > 0) {
209
- activeTabName = this._tabMap.keys().next().value;
210
- }
211
-
212
- if (activeTabName) {
213
- this._switchTab(activeTabName, false);
214
- }
215
- }
216
-
217
- // ==================== EVENT HANDLERS ====================
218
-
219
- _onButtonClick(e) {
220
- const button = e.currentTarget;
221
- const tabName = button.dataset.tab;
222
-
223
- // Don't switch if already active or button is disabled
224
- if (tabName === this._currentTab || button.disabled) {
225
- return;
226
- }
227
-
228
- this.switchTo(tabName);
229
- }
230
-
231
- // ==================== CORE FUNCTIONALITY ====================
232
-
233
- _switchTab(tabName, emit = true) {
234
- const tabData = this._tabMap.get(tabName);
235
- if (!tabData) {
236
- console.warn(`DSTabs: Tab "${tabName}" not found.`);
237
- return false;
238
- }
239
-
240
- const prevTab = this._currentTab;
241
- this._prevTab = prevTab;
242
- this._currentTab = tabName;
243
-
244
- // Update all buttons
245
- this._tabMap.forEach((data, name) => {
246
- const isActive = name === tabName;
247
-
248
- data.buttons.forEach(button => {
249
- // Handle disabled state
250
- if (this.cfg.disableActive) {
251
- button.disabled = isActive;
252
- }
253
-
254
- // Handle active classes
255
- if (isActive) {
256
- button.classList.add(this.cfg.activeClass);
257
- if (this.cfg.buttonActiveClass) {
258
- button.classList.add(this.cfg.buttonActiveClass);
259
- }
260
- } else {
261
- button.classList.remove(this.cfg.activeClass);
262
- if (this.cfg.buttonActiveClass) {
263
- button.classList.remove(this.cfg.buttonActiveClass);
264
- }
265
- }
266
- });
267
-
268
- // Update radio state
269
- if (data.radio) {
270
- data.radio.checked = isActive;
271
- }
272
-
273
- // Show/hide content
274
- if (data.content) {
275
- if (isActive) {
276
- data.content.classList.remove(this.cfg.contentHiddenClass);
277
- data.content.style.display = '';
278
- } else {
279
- data.content.classList.add(this.cfg.contentHiddenClass);
280
- data.content.style.display = 'none';
281
- }
282
- }
283
- });
284
-
285
- if (emit) {
286
- this._emit('change', { tab: tabName, prevTab });
287
-
288
- if (typeof this.cfg.onTabChange === 'function') {
289
- this.cfg.onTabChange(tabName, prevTab);
290
- }
291
- }
292
-
293
- return true;
294
- }
295
-
296
- // ==================== PUBLIC API ====================
297
-
298
- /**
299
- * Switch to a specific tab
300
- * @param {string} tabName - The tab to switch to
301
- * @returns {boolean} - Whether the switch was successful
302
- */
303
- switchTo(tabName) {
304
- return this._switchTab(tabName);
305
- }
306
-
307
- /**
308
- * Get the current active tab name
309
- * @returns {string|null}
310
- */
311
- getCurrentTab() {
312
- return this._currentTab;
313
- }
314
-
315
- /**
316
- * Get the previous tab name
317
- * @returns {string|null}
318
- */
319
- getPreviousTab() {
320
- return this._prevTab;
321
- }
322
-
323
- /**
324
- * Get all available tab names
325
- * @returns {string[]}
326
- */
327
- getTabNames() {
328
- return Array.from(this._tabMap.keys());
329
- }
330
-
331
- /**
332
- * Check if a tab exists
333
- * @param {string} tabName
334
- * @returns {boolean}
335
- */
336
- hasTab(tabName) {
337
- return this._tabMap.has(tabName);
338
- }
339
-
340
- /**
341
- * Switch to next tab (loops back to first)
342
- * @returns {string|null} - The new active tab name
343
- */
344
- next() {
345
- const tabs = this.getTabNames();
346
- const currentIndex = tabs.indexOf(this._currentTab);
347
- const nextIndex = (currentIndex + 1) % tabs.length;
348
- const nextTab = tabs[nextIndex];
349
- this.switchTo(nextTab);
350
- return nextTab;
351
- }
352
-
353
- /**
354
- * Switch to previous tab (loops to last)
355
- * @returns {string|null} - The new active tab name
356
- */
357
- prev() {
358
- const tabs = this.getTabNames();
359
- const currentIndex = tabs.indexOf(this._currentTab);
360
- const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
361
- const prevTab = tabs[prevIndex];
362
- this.switchTo(prevTab);
363
- return prevTab;
364
- }
365
-
366
- /**
367
- * Enable a specific tab button
368
- * @param {string} tabName
369
- */
370
- enableTab(tabName) {
371
- const tabData = this._tabMap.get(tabName);
372
- if (tabData) {
373
- tabData.buttons.forEach(button => {
374
- if (tabName !== this._currentTab || !this.cfg.disableActive) {
375
- button.disabled = false;
376
- }
377
- });
378
- }
379
- }
380
-
381
- /**
382
- * Disable a specific tab button
383
- * @param {string} tabName
384
- */
385
- disableTab(tabName) {
386
- const tabData = this._tabMap.get(tabName);
387
- if (tabData) {
388
- tabData.buttons.forEach(button => {
389
- button.disabled = true;
390
- });
391
- }
392
- }
393
-
394
- /**
395
- * Subscribe to events
396
- * @param {string} event - Event name ('ready', 'change')
397
- * @param {Function} handler - Event handler
398
- * @returns {DSTabs} - For chaining
399
- */
400
- on(event, handler) {
401
- if (!this._listeners[event]) {
402
- this._listeners[event] = new Set();
403
- }
404
- this._listeners[event].add(handler);
405
- return this;
406
- }
407
-
408
- /**
409
- * Unsubscribe from events
410
- * @param {string} event - Event name
411
- * @param {Function} handler - Event handler (optional, removes all if not provided)
412
- * @returns {DSTabs} - For chaining
413
- */
414
- off(event, handler) {
415
- if (this._listeners[event]) {
416
- if (handler) {
417
- this._listeners[event].delete(handler);
418
- } else {
419
- this._listeners[event].clear();
420
- }
421
- }
422
- return this;
423
- }
424
-
425
- /**
426
- * Emit an event
427
- * @private
428
- */
429
- _emit(event, data = {}) {
430
- if (this._listeners[event]) {
431
- this._listeners[event].forEach(handler => {
432
- try {
433
- handler(data);
434
- } catch (e) {
435
- console.error(`DSTabs: Error in ${event} handler:`, e);
436
- }
437
- });
438
- }
439
-
440
- // Also dispatch a DOM CustomEvent
441
- this.container.dispatchEvent(new CustomEvent(`dstabs:${event}`, {
442
- bubbles: true,
443
- detail: data
444
- }));
445
- }
446
-
447
- /**
448
- * Refresh the tabs (re-cache elements and reinitialize)
449
- */
450
- refresh() {
451
- this._cacheElements();
452
- this._initActiveTab();
453
- this._emit('refresh');
454
- }
455
-
456
- /**
457
- * Destroy the instance and cleanup
458
- */
459
- destroy() {
460
- // Remove event listeners
461
- this.buttons.forEach(button => {
462
- button.removeEventListener('click', this._boundHandlers.onButtonClick);
463
- });
464
-
465
- // Reset button states
466
- this.buttons.forEach(button => {
467
- button.disabled = false;
468
- button.classList.remove(this.cfg.activeClass, this.cfg.buttonActiveClass);
469
- });
470
-
471
- // Show all content
472
- this.contents.forEach(content => {
473
- content.classList.remove(this.cfg.contentHiddenClass);
474
- content.style.display = '';
475
- });
476
-
477
- // Clear state
478
- this._listeners = {};
479
- this._tabMap.clear();
480
- DSTabs.instances.delete(this.instanceId);
481
- delete this.container.dataset.dsTabsId;
482
-
483
- this._emit('destroy');
484
- }
485
- }
486
-
487
- // Export for both ES modules and CommonJS
488
- export default DSTabs;
1
+ /**
2
+ * DSTabs
3
+ *
4
+ * A lightweight tab switching component with:
5
+ * - Button/link click handlers for tab switching
6
+ * - Radio input synchronization
7
+ * - Active state management with disabled/enabled buttons
8
+ * - CSS class-based content show/hide
9
+ * - Data attribute configuration
10
+ * - Full event system
11
+ *
12
+ * @example
13
+ * // HTML structure:
14
+ * // Buttons: <button data-tab="primary">Primary</button>
15
+ * // Radio inputs: <input type="radio" data-tab="primary" checked />
16
+ * // Tab content: <div class="tab-content">...</div>
17
+ */
18
+ export class DSTabs {
19
+ static instances = new Map();
20
+ static instanceCounter = 0;
21
+
22
+ /**
23
+ * Default configuration
24
+ */
25
+ static defaults = {
26
+ // Selectors
27
+ buttonSelector: 'button[data-tab], a[data-tab]', // Selector for tab buttons/links (excludes inputs)
28
+ radioSelector: 'input[type="radio"][data-tab]', // Selector for hidden radio inputs
29
+ contentSelector: '.tab-content', // Selector for tab content containers
30
+ tabsContainer: '.tabs', // Container that holds radios and content
31
+
32
+ // Behavior
33
+ activeClass: 'active', // Class to add to active button
34
+ disableActive: true, // Disable the active button
35
+ showFirst: true, // Auto-show first tab on init
36
+
37
+ // Styling
38
+ buttonActiveClass: 'btn-active', // Additional class for active button
39
+ contentHiddenClass: 'hidden', // Class to hide inactive content
40
+
41
+ // Callbacks
42
+ onTabChange: null, // Callback when tab changes (tabName, prevTabName)
43
+ };
44
+
45
+ /**
46
+ * @param {string|HTMLElement} containerSelector - Container element or selector
47
+ * @param {Object} config - Configuration options
48
+ */
49
+ constructor(containerSelector, config = {}) {
50
+ this.instanceId = `ds-tabs-${++DSTabs.instanceCounter}`;
51
+
52
+ // Find the container element
53
+ const el = typeof containerSelector === 'string'
54
+ ? document.querySelector(containerSelector)
55
+ : containerSelector;
56
+
57
+ if (!el) {
58
+ throw new Error('DSTabs: Container element not found.');
59
+ }
60
+
61
+ this.container = el;
62
+
63
+ // Merge config with data attributes
64
+ this.cfg = this._buildConfig(config);
65
+
66
+ // State
67
+ this._currentTab = null;
68
+ this._prevTab = null;
69
+
70
+ // Event listeners
71
+ this._listeners = {};
72
+ this._boundHandlers = {};
73
+
74
+ // Initialize
75
+ this._init();
76
+
77
+ // Register instance
78
+ DSTabs.instances.set(this.instanceId, this);
79
+ this.container.dataset.dsTabsId = this.instanceId;
80
+ }
81
+
82
+ /**
83
+ * Static factory method
84
+ */
85
+ static create(containerSelector, config = {}) {
86
+ return new DSTabs(containerSelector, config);
87
+ }
88
+
89
+ /**
90
+ * Get instance by element
91
+ */
92
+ static getInstance(element) {
93
+ const el = typeof element === 'string' ? document.querySelector(element) : element;
94
+ if (!el) return null;
95
+ const container = el.closest('[data-ds-tabs-id]');
96
+ if (!container) return null;
97
+ return DSTabs.instances.get(container.dataset.dsTabsId);
98
+ }
99
+
100
+ /**
101
+ * Auto-initialize all elements with [data-ds-tabs]
102
+ */
103
+ static initAll(selector = '[data-ds-tabs]') {
104
+ document.querySelectorAll(selector).forEach(el => {
105
+ if (!el.closest('[data-ds-tabs-id]')) {
106
+ new DSTabs(el);
107
+ }
108
+ });
109
+ }
110
+
111
+ // ==================== INITIALIZATION ====================
112
+
113
+ _buildConfig(userConfig) {
114
+ const dataConfig = this._parseDataAttributes();
115
+ return { ...DSTabs.defaults, ...dataConfig, ...userConfig };
116
+ }
117
+
118
+ _parseDataAttributes() {
119
+ const data = this.container.dataset;
120
+ const config = {};
121
+
122
+ if (data.buttonSelector) config.buttonSelector = data.buttonSelector;
123
+ if (data.radioSelector) config.radioSelector = data.radioSelector;
124
+ if (data.contentSelector) config.contentSelector = data.contentSelector;
125
+ if (data.tabsContainer) config.tabsContainer = data.tabsContainer;
126
+ if (data.activeClass) config.activeClass = data.activeClass;
127
+ if (data.disableActive !== undefined) config.disableActive = data.disableActive !== 'false';
128
+ if (data.showFirst !== undefined) config.showFirst = data.showFirst !== 'false';
129
+
130
+ return config;
131
+ }
132
+
133
+ _init() {
134
+ this._cacheElements();
135
+ this._bindEvents();
136
+ this._initActiveTab();
137
+
138
+ this._emit('ready', { activeTab: this._currentTab });
139
+ }
140
+
141
+ _cacheElements() {
142
+ // Find all buttons with data-tab attribute within the container
143
+ this.buttons = Array.from(this.container.querySelectorAll(this.cfg.buttonSelector));
144
+
145
+ // Find the tabs container
146
+ this.tabsContainer = this.container.querySelector(this.cfg.tabsContainer);
147
+
148
+ // Find all radio inputs and content pairs within the tabs container
149
+ if (this.tabsContainer) {
150
+ this.radios = Array.from(this.tabsContainer.querySelectorAll(this.cfg.radioSelector));
151
+ this.contents = Array.from(this.tabsContainer.querySelectorAll(this.cfg.contentSelector));
152
+ } else {
153
+ this.radios = [];
154
+ this.contents = [];
155
+ }
156
+
157
+ // Build a map of tab name -> { button, radio, content }
158
+ this._tabMap = new Map();
159
+
160
+ this.buttons.forEach(button => {
161
+ const tabName = button.dataset.tab;
162
+ if (!this._tabMap.has(tabName)) {
163
+ this._tabMap.set(tabName, { buttons: [], radio: null, content: null });
164
+ }
165
+ this._tabMap.get(tabName).buttons.push(button);
166
+ });
167
+
168
+ // Associate radios with their following content divs
169
+ this.radios.forEach((radio, index) => {
170
+ const tabName = radio.dataset.tab;
171
+ if (this._tabMap.has(tabName)) {
172
+ this._tabMap.get(tabName).radio = radio;
173
+ // The content div follows the radio input
174
+ if (this.contents[index]) {
175
+ this._tabMap.get(tabName).content = this.contents[index];
176
+ }
177
+ }
178
+ });
179
+ }
180
+
181
+ _bindEvents() {
182
+ this._boundHandlers.onButtonClick = this._onButtonClick.bind(this);
183
+
184
+ this.buttons.forEach(button => {
185
+ button.addEventListener('click', this._boundHandlers.onButtonClick);
186
+ });
187
+ }
188
+
189
+ _initActiveTab() {
190
+ // Find the initially checked radio or first tab
191
+ let activeTabName = null;
192
+
193
+ // Check for checked radio
194
+ const checkedRadio = this.radios.find(r => r.checked);
195
+ if (checkedRadio) {
196
+ activeTabName = checkedRadio.dataset.tab;
197
+ }
198
+
199
+ // Or check for disabled button (indicates active)
200
+ if (!activeTabName) {
201
+ const disabledButton = this.buttons.find(b => b.disabled);
202
+ if (disabledButton) {
203
+ activeTabName = disabledButton.dataset.tab;
204
+ }
205
+ }
206
+
207
+ // Or use first tab if showFirst is enabled
208
+ if (!activeTabName && this.cfg.showFirst && this._tabMap.size > 0) {
209
+ activeTabName = this._tabMap.keys().next().value;
210
+ }
211
+
212
+ if (activeTabName) {
213
+ this._switchTab(activeTabName, false);
214
+ }
215
+ }
216
+
217
+ // ==================== EVENT HANDLERS ====================
218
+
219
+ _onButtonClick(e) {
220
+ const button = e.currentTarget;
221
+ const tabName = button.dataset.tab;
222
+
223
+ // Don't switch if already active or button is disabled
224
+ if (tabName === this._currentTab || button.disabled) {
225
+ return;
226
+ }
227
+
228
+ this.switchTo(tabName);
229
+ }
230
+
231
+ // ==================== CORE FUNCTIONALITY ====================
232
+
233
+ _switchTab(tabName, emit = true) {
234
+ const tabData = this._tabMap.get(tabName);
235
+ if (!tabData) {
236
+ console.warn(`DSTabs: Tab "${tabName}" not found.`);
237
+ return false;
238
+ }
239
+
240
+ const prevTab = this._currentTab;
241
+ this._prevTab = prevTab;
242
+ this._currentTab = tabName;
243
+
244
+ // Update all buttons
245
+ this._tabMap.forEach((data, name) => {
246
+ const isActive = name === tabName;
247
+
248
+ data.buttons.forEach(button => {
249
+ // Handle disabled state
250
+ if (this.cfg.disableActive) {
251
+ button.disabled = isActive;
252
+ }
253
+
254
+ // Handle active classes
255
+ if (isActive) {
256
+ button.classList.add(this.cfg.activeClass);
257
+ if (this.cfg.buttonActiveClass) {
258
+ button.classList.add(this.cfg.buttonActiveClass);
259
+ }
260
+ } else {
261
+ button.classList.remove(this.cfg.activeClass);
262
+ if (this.cfg.buttonActiveClass) {
263
+ button.classList.remove(this.cfg.buttonActiveClass);
264
+ }
265
+ }
266
+ });
267
+
268
+ // Update radio state
269
+ if (data.radio) {
270
+ data.radio.checked = isActive;
271
+ }
272
+
273
+ // Show/hide content
274
+ if (data.content) {
275
+ if (isActive) {
276
+ data.content.classList.remove(this.cfg.contentHiddenClass);
277
+ data.content.style.display = '';
278
+ } else {
279
+ data.content.classList.add(this.cfg.contentHiddenClass);
280
+ data.content.style.display = 'none';
281
+ }
282
+ }
283
+ });
284
+
285
+ if (emit) {
286
+ this._emit('change', { tab: tabName, prevTab });
287
+
288
+ if (typeof this.cfg.onTabChange === 'function') {
289
+ this.cfg.onTabChange(tabName, prevTab);
290
+ }
291
+ }
292
+
293
+ return true;
294
+ }
295
+
296
+ // ==================== PUBLIC API ====================
297
+
298
+ /**
299
+ * Switch to a specific tab
300
+ * @param {string} tabName - The tab to switch to
301
+ * @returns {boolean} - Whether the switch was successful
302
+ */
303
+ switchTo(tabName) {
304
+ return this._switchTab(tabName);
305
+ }
306
+
307
+ /**
308
+ * Get the current active tab name
309
+ * @returns {string|null}
310
+ */
311
+ getCurrentTab() {
312
+ return this._currentTab;
313
+ }
314
+
315
+ /**
316
+ * Get the previous tab name
317
+ * @returns {string|null}
318
+ */
319
+ getPreviousTab() {
320
+ return this._prevTab;
321
+ }
322
+
323
+ /**
324
+ * Get all available tab names
325
+ * @returns {string[]}
326
+ */
327
+ getTabNames() {
328
+ return Array.from(this._tabMap.keys());
329
+ }
330
+
331
+ /**
332
+ * Check if a tab exists
333
+ * @param {string} tabName
334
+ * @returns {boolean}
335
+ */
336
+ hasTab(tabName) {
337
+ return this._tabMap.has(tabName);
338
+ }
339
+
340
+ /**
341
+ * Switch to next tab (loops back to first)
342
+ * @returns {string|null} - The new active tab name
343
+ */
344
+ next() {
345
+ const tabs = this.getTabNames();
346
+ const currentIndex = tabs.indexOf(this._currentTab);
347
+ const nextIndex = (currentIndex + 1) % tabs.length;
348
+ const nextTab = tabs[nextIndex];
349
+ this.switchTo(nextTab);
350
+ return nextTab;
351
+ }
352
+
353
+ /**
354
+ * Switch to previous tab (loops to last)
355
+ * @returns {string|null} - The new active tab name
356
+ */
357
+ prev() {
358
+ const tabs = this.getTabNames();
359
+ const currentIndex = tabs.indexOf(this._currentTab);
360
+ const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
361
+ const prevTab = tabs[prevIndex];
362
+ this.switchTo(prevTab);
363
+ return prevTab;
364
+ }
365
+
366
+ /**
367
+ * Enable a specific tab button
368
+ * @param {string} tabName
369
+ */
370
+ enableTab(tabName) {
371
+ const tabData = this._tabMap.get(tabName);
372
+ if (tabData) {
373
+ tabData.buttons.forEach(button => {
374
+ if (tabName !== this._currentTab || !this.cfg.disableActive) {
375
+ button.disabled = false;
376
+ }
377
+ });
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Disable a specific tab button
383
+ * @param {string} tabName
384
+ */
385
+ disableTab(tabName) {
386
+ const tabData = this._tabMap.get(tabName);
387
+ if (tabData) {
388
+ tabData.buttons.forEach(button => {
389
+ button.disabled = true;
390
+ });
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Subscribe to events
396
+ * @param {string} event - Event name ('ready', 'change')
397
+ * @param {Function} handler - Event handler
398
+ * @returns {DSTabs} - For chaining
399
+ */
400
+ on(event, handler) {
401
+ if (!this._listeners[event]) {
402
+ this._listeners[event] = new Set();
403
+ }
404
+ this._listeners[event].add(handler);
405
+ return this;
406
+ }
407
+
408
+ /**
409
+ * Unsubscribe from events
410
+ * @param {string} event - Event name
411
+ * @param {Function} handler - Event handler (optional, removes all if not provided)
412
+ * @returns {DSTabs} - For chaining
413
+ */
414
+ off(event, handler) {
415
+ if (this._listeners[event]) {
416
+ if (handler) {
417
+ this._listeners[event].delete(handler);
418
+ } else {
419
+ this._listeners[event].clear();
420
+ }
421
+ }
422
+ return this;
423
+ }
424
+
425
+ /**
426
+ * Emit an event
427
+ * @private
428
+ */
429
+ _emit(event, data = {}) {
430
+ if (this._listeners[event]) {
431
+ this._listeners[event].forEach(handler => {
432
+ try {
433
+ handler(data);
434
+ } catch (e) {
435
+ console.error(`DSTabs: Error in ${event} handler:`, e);
436
+ }
437
+ });
438
+ }
439
+
440
+ // Also dispatch a DOM CustomEvent
441
+ this.container.dispatchEvent(new CustomEvent(`dstabs:${event}`, {
442
+ bubbles: true,
443
+ detail: data
444
+ }));
445
+ }
446
+
447
+ /**
448
+ * Refresh the tabs (re-cache elements and reinitialize)
449
+ */
450
+ refresh() {
451
+ this._cacheElements();
452
+ this._initActiveTab();
453
+ this._emit('refresh');
454
+ }
455
+
456
+ /**
457
+ * Destroy the instance and cleanup
458
+ */
459
+ destroy() {
460
+ // Remove event listeners
461
+ this.buttons.forEach(button => {
462
+ button.removeEventListener('click', this._boundHandlers.onButtonClick);
463
+ });
464
+
465
+ // Reset button states
466
+ this.buttons.forEach(button => {
467
+ button.disabled = false;
468
+ button.classList.remove(this.cfg.activeClass, this.cfg.buttonActiveClass);
469
+ });
470
+
471
+ // Show all content
472
+ this.contents.forEach(content => {
473
+ content.classList.remove(this.cfg.contentHiddenClass);
474
+ content.style.display = '';
475
+ });
476
+
477
+ // Clear state
478
+ this._listeners = {};
479
+ this._tabMap.clear();
480
+ DSTabs.instances.delete(this.instanceId);
481
+ delete this.container.dataset.dsTabsId;
482
+
483
+ this._emit('destroy');
484
+ }
485
+ }
486
+
487
+ // Export for both ES modules and CommonJS
488
+ export default DSTabs;