@doyosi/laraisy 1.0.1 → 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.
- package/LICENSE +1 -1
- package/package.json +1 -1
- package/src/CodeInput.js +48 -48
- package/src/DSAlert.js +352 -352
- package/src/DSAvatar.js +207 -207
- package/src/DSDelete.js +274 -274
- package/src/DSForm.js +568 -568
- package/src/DSGridOrTable.js +453 -453
- package/src/DSLocaleSwitcher.js +239 -239
- package/src/DSLogout.js +293 -293
- package/src/DSNotifications.js +365 -365
- package/src/DSRestore.js +181 -181
- package/src/DSSelect.js +1071 -1071
- package/src/DSSelectBox.js +563 -563
- package/src/DSSimpleSlider.js +517 -517
- package/src/DSSvgFetch.js +69 -69
- package/src/DSTable/DSTableExport.js +68 -68
- package/src/DSTable/DSTableFilter.js +224 -224
- package/src/DSTable/DSTablePagination.js +136 -136
- package/src/DSTable/DSTableSearch.js +40 -40
- package/src/DSTable/DSTableSelection.js +192 -192
- package/src/DSTable/DSTableSort.js +58 -58
- package/src/DSTable.js +353 -353
- package/src/DSTabs.js +488 -488
- package/src/DSUpload.js +887 -887
- package/dist/CodeInput.d.ts +0 -10
- package/dist/DSAlert.d.ts +0 -112
- package/dist/DSAvatar.d.ts +0 -45
- package/dist/DSDelete.d.ts +0 -61
- package/dist/DSForm.d.ts +0 -151
- package/dist/DSGridOrTable/DSGOTRenderer.d.ts +0 -60
- package/dist/DSGridOrTable/DSGOTViewToggle.d.ts +0 -26
- package/dist/DSGridOrTable.d.ts +0 -296
- package/dist/DSLocaleSwitcher.d.ts +0 -71
- package/dist/DSLogout.d.ts +0 -76
- package/dist/DSNotifications.d.ts +0 -54
- package/dist/DSRestore.d.ts +0 -56
- package/dist/DSSelect.d.ts +0 -221
- package/dist/DSSelectBox.d.ts +0 -123
- package/dist/DSSimpleSlider.d.ts +0 -136
- package/dist/DSSvgFetch.d.ts +0 -17
- package/dist/DSTable/DSTableExport.d.ts +0 -11
- package/dist/DSTable/DSTableFilter.d.ts +0 -40
- package/dist/DSTable/DSTablePagination.d.ts +0 -12
- package/dist/DSTable/DSTableSearch.d.ts +0 -8
- package/dist/DSTable/DSTableSelection.d.ts +0 -46
- package/dist/DSTable/DSTableSort.d.ts +0 -8
- package/dist/DSTable.d.ts +0 -116
- package/dist/DSTabs.d.ts +0 -156
- package/dist/DSUpload.d.ts +0 -220
- 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;
|