@seip/blue-bird-css 0.1.0

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.
@@ -0,0 +1,1321 @@
1
+ /**
2
+ * Blue Bird CSS Framework JS Helper
3
+ * @param {string|object} component - Component name ('snackbar', 'drawer', 'carousel', 'toast', 'tab', 'command', 'popover') or options
4
+ * @param {object} [options] - Component configuration options
5
+ */
6
+ function bluebird(component, options) {
7
+ if (typeof component === 'object') {
8
+ options = component;
9
+ component = 'snackbar';
10
+ }
11
+
12
+ // --- SNACKBAR COMPONENT ---
13
+ if (component === 'snackbar') {
14
+ let snackbarEl = document.getElementById('snackbar');
15
+
16
+ if (!snackbarEl) {
17
+ snackbarEl = document.createElement('div');
18
+ snackbarEl.id = 'snackbar';
19
+ document.body.appendChild(snackbarEl);
20
+ }
21
+
22
+ snackbarEl.className = 'show';
23
+
24
+ if (options && options.type) {
25
+ snackbarEl.classList.add(options.type);
26
+ } else {
27
+ snackbarEl.classList.add('info');
28
+ }
29
+
30
+ snackbarEl.textContent = (options && options.message) || '';
31
+ setTimeout(() => {
32
+ snackbarEl.classList.add('show');
33
+ }, 10);
34
+
35
+ const duration = (options && options.duration) || 3000;
36
+ if (snackbarEl.timeoutId) {
37
+ clearTimeout(snackbarEl.timeoutId);
38
+ }
39
+
40
+ snackbarEl.timeoutId = setTimeout(function () {
41
+ snackbarEl.className = '';
42
+ }, duration);
43
+ }
44
+
45
+ // --- MULTI-TOAST SYSTEM ---
46
+ if (component === 'toast') {
47
+ const position = (options && options.position) || 'bottom-right';
48
+ let container = document.querySelector(`.toast-container.${position}`);
49
+
50
+ if (!container) {
51
+ container = document.createElement('div');
52
+ container.className = `toast-container ${position}`;
53
+ document.body.appendChild(container);
54
+ }
55
+
56
+ const toastEl = document.createElement('div');
57
+ const typeClass = (options && options.type) ? `toast-${options.type}` : 'toast-info';
58
+ toastEl.className = `toast ${typeClass}`;
59
+
60
+ const title = (options && options.title) ? `<div class="toast-title">${options.title}</div>` : '';
61
+ const desc = (options && options.description) ? `<div class="toast-description">${options.description}</div>` : '';
62
+
63
+ toastEl.innerHTML = `
64
+ <div class="toast-content">
65
+ ${title}
66
+ ${desc}
67
+ </div>
68
+ <button class="toast-close" aria-label="Dismiss">&times;</button>
69
+ `;
70
+
71
+ const closeBtn = toastEl.querySelector('.toast-close');
72
+ closeBtn.addEventListener('click', () => dismissToast(toastEl));
73
+
74
+ container.appendChild(toastEl);
75
+
76
+ const duration = (options && options.duration) !== undefined ? options.duration : 4000;
77
+ if (duration > 0) {
78
+ setTimeout(() => dismissToast(toastEl), duration);
79
+ }
80
+ }
81
+
82
+ // --- TABS COMPONENT ---
83
+ if (component === 'tab') {
84
+ const targetId = options && options.id;
85
+ if (!targetId) return;
86
+
87
+ const targetContent = document.getElementById(targetId);
88
+ if (!targetContent) return;
89
+
90
+ const tabsContainer = targetContent.closest('.tabs');
91
+ if (!tabsContainer) return;
92
+
93
+ const allTriggers = tabsContainer.querySelectorAll('.tab-trigger');
94
+ const allContents = tabsContainer.querySelectorAll('.tab-content');
95
+
96
+ allContents.forEach(c => c.classList.remove('active'));
97
+ allTriggers.forEach(t => t.classList.remove('active'));
98
+
99
+ targetContent.classList.add('active');
100
+
101
+ const matchingTrigger = Array.from(allTriggers).find(t =>
102
+ t.getAttribute('data-tab-target') === targetId || t.getAttribute('href') === `#${targetId}`
103
+ );
104
+
105
+ if (matchingTrigger) {
106
+ matchingTrigger.classList.add('active');
107
+ }
108
+ }
109
+
110
+ // --- COMMAND PALETTE MODAL ---
111
+ if (component === 'command') {
112
+ const action = (options && options.action) || 'toggle';
113
+ let backdrop = document.querySelector('.command-backdrop');
114
+
115
+ if (!backdrop) {
116
+ backdrop = createCommandPaletteModal();
117
+ }
118
+
119
+ const isOpen = backdrop.classList.contains('open');
120
+
121
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
122
+ backdrop.classList.add('open');
123
+ const input = backdrop.querySelector('.command-input');
124
+ if (input) {
125
+ input.value = '';
126
+ setTimeout(() => input.focus(), 50);
127
+ }
128
+ } else if (action === 'close' || (action === 'toggle' && isOpen)) {
129
+ backdrop.classList.remove('open');
130
+ }
131
+ }
132
+
133
+ // --- POPOVER COMPONENT ---
134
+ if (component === 'popover') {
135
+ const id = options && options.id;
136
+ const action = (options && options.action) || 'toggle';
137
+ if (!id) return;
138
+
139
+ const popoverEl = document.getElementById(id) || document.querySelector(`[data-popover-id="${id}"]`);
140
+ if (!popoverEl) return;
141
+
142
+ const isOpen = popoverEl.classList.contains('open');
143
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
144
+ popoverEl.classList.add('open');
145
+ } else {
146
+ popoverEl.classList.remove('open');
147
+ }
148
+ }
149
+
150
+ // --- STANDALONE DRAWER COMPONENT ---
151
+ if (component === 'drawer') {
152
+ cleanupOrphanedBackdrops();
153
+
154
+ const id = options && options.id;
155
+ const action = (options && options.action) || 'toggle';
156
+ if (!id) return;
157
+
158
+ const drawerEl = document.getElementById(id);
159
+ if (!drawerEl) return;
160
+
161
+ let overlay = document.querySelector(`.drawer-backdrop[data-for="${id}"]`);
162
+ if (!overlay) {
163
+ overlay = document.createElement('div');
164
+ overlay.className = 'drawer-backdrop';
165
+ overlay.setAttribute('data-for', id);
166
+ document.body.appendChild(overlay);
167
+ overlay.addEventListener('click', () => {
168
+ bluebird('drawer', { id, action: 'close' });
169
+ });
170
+ }
171
+
172
+ const isOpen = drawerEl.classList.contains('open') || drawerEl.classList.contains('active');
173
+
174
+ if (action === 'open' || (action === 'toggle' && !isOpen)) {
175
+ drawerEl.classList.add('open');
176
+ overlay.classList.add('open');
177
+ document.body.style.overflow = 'hidden';
178
+ } else if (action === 'close' || (action === 'toggle' && isOpen)) {
179
+ drawerEl.classList.remove('open');
180
+ overlay.classList.remove('open');
181
+ document.body.style.overflow = '';
182
+ setTimeout(() => {
183
+ if (overlay && overlay.parentNode && !drawerEl.classList.contains('open')) {
184
+ overlay.remove();
185
+ }
186
+ }, 300);
187
+ }
188
+ }
189
+
190
+ // --- CAROUSEL COMPONENT ---
191
+ if (component === 'carousel') {
192
+ const selector = (options && options.selector) || '.carousel';
193
+ const carousels = document.querySelectorAll(selector);
194
+ carousels.forEach(carousel => initSingleCarousel(carousel, options));
195
+ }
196
+
197
+ // --- RESPONSIVE DATATABLE COMPONENT ---
198
+ if (component === 'datatable' || component === 'table') {
199
+ const containerId = (options && (options.container || options.id)) || 'datatable';
200
+ return new ResponsiveDataTable(containerId, options);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Global alias for snackbar
206
+ * @param {object} options - Snackbar configuration options
207
+ */
208
+ function snackbar(options) {
209
+ bluebird('snackbar', options);
210
+ }
211
+
212
+ /**
213
+ * Global alias for toast notification
214
+ * @param {object} options - Toast configuration options
215
+ */
216
+ function toast(options) {
217
+ bluebird('toast', options);
218
+ }
219
+
220
+ function dismissToast(toastEl) {
221
+ if (!toastEl || toastEl.isDismissing) return;
222
+ toastEl.isDismissing = true;
223
+ toastEl.style.opacity = '0';
224
+ toastEl.style.transform = 'translateY(-10px) scale(0.95)';
225
+ setTimeout(() => {
226
+ if (toastEl.parentNode) {
227
+ toastEl.remove();
228
+ }
229
+ }, 200);
230
+ }
231
+
232
+ /**
233
+ * Create default command palette DOM modal
234
+ */
235
+ function createCommandPaletteModal() {
236
+ const backdrop = document.createElement('div');
237
+ backdrop.className = 'command-backdrop';
238
+ backdrop.innerHTML = `
239
+ <div class="command-dialog">
240
+ <div class="command-input-wrapper">
241
+ <span>🔍</span>
242
+ <input type="text" class="command-input" placeholder="Type a command or search documentation..." />
243
+ <kbd>ESC</kbd>
244
+ </div>
245
+ <div class="command-list">
246
+ <div class="command-group">
247
+ <div class="command-group-title">Navigation</div>
248
+ <div class="command-item" data-navigate="#/"><span>Documentation Home</span><kbd>↵</kbd></div>
249
+ <div class="command-item" data-navigate="#/buttons"><span>Buttons & Badges</span><kbd>↵</kbd></div>
250
+ <div class="command-item" data-navigate="#/forms"><span>Forms & Inputs</span><kbd>↵</kbd></div>
251
+ </div>
252
+ <div class="command-group">
253
+ <div class="command-group-title">Components</div>
254
+ <div class="command-item" data-navigate="#/carousel"><span>Touch Carousel</span><kbd>↵</kbd></div>
255
+ <div class="command-item" data-navigate="#/aside-drawer"><span>Aside & Drawers</span><kbd>↵</kbd></div>
256
+ <div class="command-item" data-navigate="#/animations"><span>CSS Animations</span><kbd>↵</kbd></div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ `;
261
+
262
+ document.body.appendChild(backdrop);
263
+
264
+ backdrop.addEventListener('click', (e) => {
265
+ if (e.target === backdrop) {
266
+ bluebird('command', { action: 'close' });
267
+ }
268
+ });
269
+
270
+ const input = backdrop.querySelector('.command-input');
271
+ input.addEventListener('input', (e) => {
272
+ const query = e.target.value.toLowerCase().trim();
273
+ const items = backdrop.querySelectorAll('.command-item');
274
+ items.forEach(item => {
275
+ const text = item.textContent.toLowerCase();
276
+ item.style.display = text.includes(query) ? 'flex' : 'none';
277
+ });
278
+ });
279
+
280
+ backdrop.addEventListener('click', (e) => {
281
+ const item = e.target.closest('.command-item');
282
+ if (item && item.getAttribute('data-navigate')) {
283
+ window.location.hash = item.getAttribute('data-navigate');
284
+ bluebird('command', { action: 'close' });
285
+ }
286
+ });
287
+
288
+ return backdrop;
289
+ }
290
+
291
+ /**
292
+ * Remove backdrops whose target drawer no longer exists in DOM
293
+ */
294
+ function cleanupOrphanedBackdrops() {
295
+ document.querySelectorAll('.drawer-backdrop[data-for]').forEach(backdrop => {
296
+ const targetId = backdrop.getAttribute('data-for');
297
+ if (!document.getElementById(targetId)) {
298
+ backdrop.remove();
299
+ }
300
+ });
301
+ }
302
+
303
+ function initMobileDrawer() {
304
+ const mainEl = document.querySelector('main');
305
+ const aside = mainEl ? mainEl.querySelector(':scope > aside') : null;
306
+ if (!aside) return;
307
+
308
+ let overlay = document.querySelector('.bluebird-drawer-overlay');
309
+ if (!overlay) {
310
+ overlay = document.createElement('div');
311
+ overlay.className = 'bluebird-drawer-overlay';
312
+ document.body.appendChild(overlay);
313
+ }
314
+
315
+ let drawer = document.querySelector('.bluebird-drawer');
316
+ if (!drawer) {
317
+ drawer = document.createElement('div');
318
+ drawer.className = 'bluebird-drawer';
319
+ document.body.appendChild(drawer);
320
+ }
321
+
322
+ drawer.innerHTML = aside.innerHTML;
323
+
324
+ let toggle = document.querySelector('.bluebird-drawer-toggle');
325
+ if (!toggle) {
326
+ toggle = document.createElement('button');
327
+ toggle.className = 'bluebird-drawer-toggle';
328
+ toggle.innerHTML = '☰';
329
+ toggle.setAttribute('aria-label', 'Toggle navigation menu');
330
+
331
+ const header = document.querySelector('header');
332
+ if (header) {
333
+ const nav = header.querySelector('nav');
334
+ if (nav) {
335
+ nav.insertBefore(toggle, nav.firstChild);
336
+ } else {
337
+ header.prepend(toggle);
338
+ }
339
+ } else {
340
+ document.body.prepend(toggle);
341
+ }
342
+ }
343
+ }
344
+
345
+ // Global keyboard listener for Ctrl+K / Cmd+K Command Palette shortcut & ESC key
346
+ document.addEventListener('keydown', function (e) {
347
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
348
+ e.preventDefault();
349
+ bluebird('command', { action: 'toggle' });
350
+ }
351
+
352
+ if (e.key === 'Escape') {
353
+ const commandBackdrop = document.querySelector('.command-backdrop.open');
354
+ if (commandBackdrop) {
355
+ bluebird('command', { action: 'close' });
356
+ }
357
+ }
358
+ });
359
+
360
+ // Global click listener for Material Ripples, Mobile Navigation Drawer, Tabs, Popovers & Declarative Data Attributes
361
+ document.addEventListener('click', function (e) {
362
+ // 1. Declarative Tab Trigger Click
363
+ const tabTrigger = e.target.closest('[data-tab-target], .tab-trigger');
364
+ if (tabTrigger) {
365
+ const targetId = tabTrigger.getAttribute('data-tab-target') || (tabTrigger.getAttribute('href') || '').replace('#', '');
366
+ if (targetId) {
367
+ e.preventDefault();
368
+ bluebird('tab', { id: targetId });
369
+ }
370
+ }
371
+
372
+ // 2. Declarative Popover Trigger Click
373
+ const popoverTrigger = e.target.closest('[data-popover-target]');
374
+ if (popoverTrigger) {
375
+ const popoverId = popoverTrigger.getAttribute('data-popover-target');
376
+ bluebird('popover', { id: popoverId, action: 'toggle' });
377
+ }
378
+
379
+ // Close open popovers when clicking outside
380
+ if (!e.target.closest('.popover') && !e.target.closest('[data-popover-target]')) {
381
+ document.querySelectorAll('.popover.open').forEach(p => p.classList.remove('open'));
382
+ }
383
+
384
+ // 3. Mobile Navigation Toggle Button Click
385
+ const mobileToggle = e.target.closest('.bluebird-drawer-toggle');
386
+ if (mobileToggle) {
387
+ e.preventDefault();
388
+ e.stopPropagation();
389
+ initMobileDrawer();
390
+ const drawer = document.querySelector('.bluebird-drawer');
391
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
392
+ if (drawer && overlay) {
393
+ const isOpen = drawer.classList.contains('open');
394
+ if (isOpen) {
395
+ drawer.classList.remove('open');
396
+ overlay.classList.remove('open');
397
+ document.body.style.overflow = '';
398
+ } else {
399
+ drawer.classList.add('open');
400
+ overlay.classList.add('open');
401
+ document.body.style.overflow = 'hidden';
402
+ }
403
+ }
404
+ return;
405
+ }
406
+
407
+ // 4. Mobile Navigation Overlay Click
408
+ if (e.target.closest('.bluebird-drawer-overlay')) {
409
+ const drawer = document.querySelector('.bluebird-drawer');
410
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
411
+ if (drawer) drawer.classList.remove('open');
412
+ if (overlay) overlay.classList.remove('open');
413
+ document.body.style.overflow = '';
414
+ return;
415
+ }
416
+
417
+ // 5. Mobile Navigation Drawer Link Click
418
+ if (e.target.closest('.bluebird-drawer a')) {
419
+ const drawer = document.querySelector('.bluebird-drawer');
420
+ const overlay = document.querySelector('.bluebird-drawer-overlay');
421
+ if (drawer) drawer.classList.remove('open');
422
+ if (overlay) overlay.classList.remove('open');
423
+ document.body.style.overflow = '';
424
+ }
425
+
426
+ // Material Ripple Effect
427
+ const btn = e.target.closest("button, a[role='button']");
428
+ if (btn && !btn.classList.contains('fab') && !btn.classList.contains('carousel-nav') && !btn.classList.contains('bluebird-drawer-toggle')) {
429
+ const rect = btn.getBoundingClientRect();
430
+ const size = Math.max(rect.width, rect.height);
431
+ const x = e.clientX - rect.left - size / 2;
432
+ const y = e.clientY - rect.top - size / 2;
433
+
434
+ const ripple = document.createElement('span');
435
+ ripple.className = 'ripple';
436
+ ripple.style.width = ripple.style.height = size + 'px';
437
+ ripple.style.left = x + 'px';
438
+ ripple.style.top = y + 'px';
439
+
440
+ btn.appendChild(ripple);
441
+ ripple.addEventListener('animationend', () => ripple.remove());
442
+ }
443
+
444
+ // Declarative Standalone Drawer Triggers
445
+ const drawerTrigger = e.target.closest('[data-drawer-target]');
446
+ if (drawerTrigger) {
447
+ const id = drawerTrigger.getAttribute('data-drawer-target');
448
+ bluebird('drawer', { id, action: 'toggle' });
449
+ }
450
+
451
+ const drawerClose = e.target.closest('[data-drawer-close]');
452
+ if (drawerClose) {
453
+ const drawerEl = drawerClose.closest('.drawer');
454
+ if (drawerEl && drawerEl.id) {
455
+ bluebird('drawer', { id: drawerEl.id, action: 'close' });
456
+ }
457
+ }
458
+ });
459
+
460
+ /**
461
+ * Single Carousel Initialization logic with Touch/Swipe, Drag & Arrow Controls
462
+ */
463
+ function initSingleCarousel(carousel, opts = {}) {
464
+ if (carousel._bb_initialized) return;
465
+ carousel._bb_initialized = true;
466
+
467
+ const track = carousel.querySelector('.carousel-track');
468
+ if (!track) return;
469
+
470
+ const items = Array.from(track.querySelectorAll('.carousel-item, .carousel-card'));
471
+ if (items.length === 0) return;
472
+
473
+ const prevBtn = carousel.querySelector('.carousel-prev');
474
+ const nextBtn = carousel.querySelector('.carousel-next');
475
+ let indicatorsContainer = carousel.querySelector('.carousel-indicators');
476
+
477
+ let currentSlideIndex = 0;
478
+
479
+ if (indicatorsContainer && indicatorsContainer.children.length === 0) {
480
+ items.forEach((_, idx) => {
481
+ const dot = document.createElement('button');
482
+ dot.className = `carousel-dot ${idx === 0 ? 'active' : ''}`;
483
+ dot.setAttribute('aria-label', `Go to slide ${idx + 1}`);
484
+ dot.addEventListener('click', (e) => {
485
+ e.preventDefault();
486
+ scrollToSlide(idx);
487
+ });
488
+ indicatorsContainer.appendChild(dot);
489
+ });
490
+ }
491
+
492
+ function scrollToSlide(index) {
493
+ if (index < 0) index = 0;
494
+ if (index >= items.length) index = items.length - 1;
495
+ currentSlideIndex = index;
496
+ const targetItem = items[index];
497
+ if (targetItem) {
498
+ track.scrollTo({
499
+ left: targetItem.offsetLeft - track.offsetLeft,
500
+ behavior: 'smooth'
501
+ });
502
+ updateIndicators(index);
503
+ }
504
+ }
505
+
506
+ function updateIndicators(activeIndex) {
507
+ if (!indicatorsContainer) return;
508
+ const dots = Array.from(indicatorsContainer.children);
509
+ dots.forEach((dot, idx) => {
510
+ dot.classList.toggle('active', idx === activeIndex);
511
+ });
512
+ }
513
+
514
+ let scrollTimeout;
515
+ track.addEventListener('scroll', () => {
516
+ clearTimeout(scrollTimeout);
517
+ scrollTimeout = setTimeout(() => {
518
+ const trackLeft = track.scrollLeft;
519
+ let closestIndex = 0;
520
+ let minDistance = Infinity;
521
+
522
+ items.forEach((item, idx) => {
523
+ const distance = Math.abs(item.offsetLeft - track.offsetLeft - trackLeft);
524
+ if (distance < minDistance) {
525
+ minDistance = distance;
526
+ closestIndex = idx;
527
+ }
528
+ });
529
+
530
+ currentSlideIndex = closestIndex;
531
+ updateIndicators(closestIndex);
532
+ }, 40);
533
+ });
534
+
535
+ if (prevBtn) {
536
+ prevBtn.addEventListener('click', (e) => {
537
+ e.preventDefault();
538
+ e.stopPropagation();
539
+ scrollToSlide(currentSlideIndex - 1);
540
+ });
541
+ }
542
+
543
+ if (nextBtn) {
544
+ nextBtn.addEventListener('click', (e) => {
545
+ e.preventDefault();
546
+ e.stopPropagation();
547
+ scrollToSlide(currentSlideIndex + 1);
548
+ });
549
+ }
550
+
551
+ // Mobile Touch Swipe & Desktop Mouse Drag
552
+ let startX = 0;
553
+ let isDragging = false;
554
+
555
+ track.addEventListener('touchstart', (e) => {
556
+ startX = e.touches[0].clientX;
557
+ isDragging = true;
558
+ }, { passive: true });
559
+
560
+ track.addEventListener('touchend', (e) => {
561
+ if (!isDragging) return;
562
+ isDragging = false;
563
+ const endX = e.changedTouches[0].clientX;
564
+ const diffX = startX - endX;
565
+
566
+ if (Math.abs(diffX) > 35) {
567
+ if (diffX > 0) {
568
+ scrollToSlide(currentSlideIndex + 1);
569
+ } else {
570
+ scrollToSlide(currentSlideIndex - 1);
571
+ }
572
+ }
573
+ });
574
+
575
+ track.addEventListener('mousedown', (e) => {
576
+ startX = e.clientX;
577
+ isDragging = true;
578
+ track.style.cursor = 'grabbing';
579
+ });
580
+
581
+ track.addEventListener('mouseleave', () => {
582
+ isDragging = false;
583
+ track.style.cursor = 'grab';
584
+ });
585
+
586
+ track.addEventListener('mouseup', (e) => {
587
+ if (!isDragging) return;
588
+ isDragging = false;
589
+ track.style.cursor = 'grab';
590
+ const endX = e.clientX;
591
+ const diffX = startX - endX;
592
+
593
+ if (Math.abs(diffX) > 35) {
594
+ if (diffX > 0) {
595
+ scrollToSlide(currentSlideIndex + 1);
596
+ } else {
597
+ scrollToSlide(currentSlideIndex - 1);
598
+ }
599
+ }
600
+ });
601
+
602
+ const isAutoplay = (opts && opts.autoplay) || carousel.getAttribute('data-autoplay') === 'true';
603
+ const intervalTime = parseInt((opts && opts.interval) || carousel.getAttribute('data-interval') || 3500, 10);
604
+
605
+ if (isAutoplay) {
606
+ let autoInterval = setInterval(() => {
607
+ const nextIdx = (currentSlideIndex + 1) % items.length;
608
+ scrollToSlide(nextIdx);
609
+ }, intervalTime);
610
+
611
+ carousel.addEventListener('mouseenter', () => clearInterval(autoInterval));
612
+ carousel.addEventListener('mouseleave', () => {
613
+ autoInterval = setInterval(() => {
614
+ const nextIdx = (currentSlideIndex + 1) % items.length;
615
+ scrollToSlide(nextIdx);
616
+ }, intervalTime);
617
+ });
618
+ }
619
+ }
620
+
621
+ // --- DECLARATIVE DATA ATTRIBUTES & GLOBAL EVENT DELEGATION ---
622
+ (function setupDeclarativeListeners() {
623
+ // Click Delegations
624
+ document.addEventListener('click', (e) => {
625
+ // 1. Data-Copy
626
+ const copyTrigger = e.target.closest('[data-copy]');
627
+ if (copyTrigger) {
628
+ e.preventDefault();
629
+ const targetAttr = copyTrigger.getAttribute('data-copy');
630
+ let textToCopy = targetAttr;
631
+
632
+ if (targetAttr && (targetAttr.startsWith('#') || targetAttr.startsWith('.'))) {
633
+ const targetEl = document.querySelector(targetAttr);
634
+ if (targetEl) {
635
+ textToCopy = targetEl.value !== undefined ? targetEl.value : (targetEl.innerText || targetEl.textContent);
636
+ }
637
+ }
638
+
639
+ if (textToCopy) {
640
+ navigator.clipboard.writeText(textToCopy.trim()).then(() => {
641
+ copyTrigger.classList.add('copied');
642
+ if (typeof bluebird === 'function') {
643
+ bluebird('toast', {
644
+ title: 'Copied to clipboard',
645
+ description: textToCopy.length > 50 ? textToCopy.substring(0, 50) + '...' : textToCopy,
646
+ type: 'success',
647
+ duration: 2500
648
+ });
649
+ }
650
+ setTimeout(() => copyTrigger.classList.remove('copied'), 2000);
651
+ });
652
+ }
653
+ return;
654
+ }
655
+
656
+ // 2. Data-Confirm (Prompt verification before action)
657
+ const confirmTrigger = e.target.closest('[data-confirm]');
658
+ if (confirmTrigger) {
659
+ const msg = confirmTrigger.getAttribute('data-confirm') || 'Are you sure?';
660
+ if (!window.confirm(msg)) {
661
+ e.preventDefault();
662
+ e.stopImmediatePropagation();
663
+ return;
664
+ }
665
+ }
666
+
667
+ // 3. Data-Scroll-To (Smooth scroll with header compensation)
668
+ const scrollTrigger = e.target.closest('[data-scroll-to]');
669
+ if (scrollTrigger) {
670
+ e.preventDefault();
671
+ const targetId = scrollTrigger.getAttribute('data-scroll-to');
672
+ const targetEl = document.querySelector(targetId);
673
+ if (targetEl) {
674
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
675
+ }
676
+ return;
677
+ }
678
+
679
+ // 4. Data-Password-Toggle (Toggle password unmask)
680
+ const passToggle = e.target.closest('[data-password-toggle]');
681
+ if (passToggle) {
682
+ e.preventDefault();
683
+ const targetSelector = passToggle.getAttribute('data-password-toggle');
684
+ const input = targetSelector
685
+ ? document.querySelector(targetSelector)
686
+ : (passToggle.closest('.input-group, .form-input-group, div')?.querySelector('input') || passToggle.previousElementSibling);
687
+
688
+ if (input && (input.type === 'password' || input.type === 'text')) {
689
+ const isPassword = input.type === 'password';
690
+ input.type = isPassword ? 'text' : 'password';
691
+ passToggle.classList.toggle('showing', isPassword);
692
+ passToggle.setAttribute('aria-pressed', isPassword ? 'true' : 'false');
693
+ }
694
+ return;
695
+ }
696
+
697
+ // 5. Data-Step-Up / Data-Step-Down (Number Steppers)
698
+ const stepUp = e.target.closest('[data-step-up]');
699
+ if (stepUp) {
700
+ e.preventDefault();
701
+ const targetInput = document.querySelector(stepUp.getAttribute('data-step-up')) ||
702
+ stepUp.closest('.stepper')?.querySelector('input[type="number"]');
703
+ if (targetInput && typeof targetInput.stepUp === 'function') {
704
+ targetInput.stepUp();
705
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
706
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
707
+ }
708
+ return;
709
+ }
710
+
711
+ const stepDown = e.target.closest('[data-step-down]');
712
+ if (stepDown) {
713
+ e.preventDefault();
714
+ const targetInput = document.querySelector(stepDown.getAttribute('data-step-down')) ||
715
+ stepDown.closest('.stepper')?.querySelector('input[type="number"]');
716
+ if (targetInput && typeof targetInput.stepDown === 'function') {
717
+ targetInput.stepDown();
718
+ targetInput.dispatchEvent(new Event('input', { bubbles: true }));
719
+ targetInput.dispatchEvent(new Event('change', { bubbles: true }));
720
+ }
721
+ return;
722
+ }
723
+
724
+ // 6. Data-Select-Value (ComboBox / Select2 item selection)
725
+ const selectItem = e.target.closest('[data-select-value]');
726
+ if (selectItem) {
727
+ const val = selectItem.getAttribute('data-select-value');
728
+ const targetSelector = selectItem.getAttribute('data-select-target') ||
729
+ selectItem.closest('[data-select-container]')?.getAttribute('data-select-target');
730
+ if (targetSelector) {
731
+ const targetEl = document.querySelector(targetSelector);
732
+ if (targetEl) {
733
+ if (targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT') {
734
+ targetEl.value = val;
735
+ targetEl.dispatchEvent(new Event('input', { bubbles: true }));
736
+ targetEl.dispatchEvent(new Event('change', { bubbles: true }));
737
+ } else {
738
+ targetEl.textContent = selectItem.textContent.trim();
739
+ }
740
+ }
741
+ }
742
+ // Close dropdown if inside one
743
+ const parentDropdown = selectItem.closest('.dropdown-content, .popover-content');
744
+ if (parentDropdown) {
745
+ parentDropdown.classList.remove('open');
746
+ }
747
+ }
748
+
749
+ // 7. Data-Toggle / Modal Trigger
750
+ const modalTrigger = e.target.closest('[data-toggle="modal"], [data-modal-target], [data-dialog-target]');
751
+ if (modalTrigger) {
752
+ e.preventDefault();
753
+ const targetSelector = modalTrigger.getAttribute('data-modal-target') ||
754
+ modalTrigger.getAttribute('data-dialog-target') ||
755
+ modalTrigger.getAttribute('data-target') ||
756
+ modalTrigger.getAttribute('href');
757
+ if (targetSelector) {
758
+ const dialog = document.querySelector(targetSelector);
759
+ if (dialog && typeof dialog.showModal === 'function') {
760
+ dialog.showModal();
761
+ }
762
+ }
763
+ return;
764
+ }
765
+
766
+ // 8. Data-Dismiss / Modal Close
767
+ const dismissTrigger = e.target.closest('[data-dismiss="modal"], [data-close-dialog], [data-close-modal]');
768
+ if (dismissTrigger) {
769
+ e.preventDefault();
770
+ const dialog = dismissTrigger.closest('dialog') ||
771
+ document.querySelector(dismissTrigger.getAttribute('data-target') || '');
772
+ if (dialog && typeof dialog.close === 'function') {
773
+ dialog.close();
774
+ }
775
+ return;
776
+ }
777
+
778
+ // 9. Data-Toggle Theme
779
+ const themeTrigger = e.target.closest('[data-toggle="theme"]');
780
+ if (themeTrigger) {
781
+ e.preventDefault();
782
+ const html = document.documentElement;
783
+ const current = html.getAttribute('data-theme') || 'light';
784
+ const next = current === 'dark' ? 'light' : 'dark';
785
+ html.setAttribute('data-theme', next);
786
+ try {
787
+ localStorage.setItem('bluebird-theme', next);
788
+ } catch (err) { }
789
+ return;
790
+ }
791
+
792
+ // 10. Data-Toast Trigger
793
+ const toastTrigger = e.target.closest('[data-toast]');
794
+ if (toastTrigger) {
795
+ e.preventDefault();
796
+ const desc = toastTrigger.getAttribute('data-toast') || '';
797
+ const title = toastTrigger.getAttribute('data-toast-title') || '';
798
+ const type = toastTrigger.getAttribute('data-toast-type') || 'info';
799
+ if (typeof bluebird === 'function') {
800
+ bluebird('toast', { title, description: desc, type });
801
+ }
802
+ return;
803
+ }
804
+
805
+ // 11. Data-Snackbar Trigger
806
+ const snackbarTrigger = e.target.closest('[data-snackbar]');
807
+ if (snackbarTrigger) {
808
+ e.preventDefault();
809
+ const message = snackbarTrigger.getAttribute('data-snackbar') || '';
810
+ const type = snackbarTrigger.getAttribute('data-snackbar-type') || 'info';
811
+ if (typeof bluebird === 'function') {
812
+ bluebird('snackbar', { message, type });
813
+ }
814
+ return;
815
+ }
816
+
817
+ // 12. Click outside dropdown / popover auto-close
818
+ if (!e.target.closest('.dropdown') && !e.target.closest('.popover')) {
819
+ document.querySelectorAll('.dropdown-content.open, .popover-content.open').forEach(el => {
820
+ el.classList.remove('open');
821
+ });
822
+ }
823
+ });
824
+
825
+ // Live Input Event Delegations (Filter Target & Auto-Resize Textarea)
826
+ document.addEventListener('input', (e) => {
827
+ // A. Real-time List/Table/ComboBox Filtering (data-filter-target="#lista")
828
+ const filterInput = e.target.closest('[data-filter-target]');
829
+ if (filterInput) {
830
+ const targetSelector = filterInput.getAttribute('data-filter-target');
831
+ const targetContainer = document.querySelector(targetSelector);
832
+ if (targetContainer) {
833
+ const query = filterInput.value.toLowerCase().trim();
834
+ const items = targetContainer.querySelectorAll('[data-filter-item], li, tr, .card, .dropdown-item, .item');
835
+ let visibleCount = 0;
836
+
837
+ items.forEach(item => {
838
+ const text = item.textContent.toLowerCase();
839
+ const matches = text.includes(query);
840
+ item.style.display = matches ? '' : 'none';
841
+ if (matches) visibleCount++;
842
+ });
843
+
844
+ const emptyMsg = targetContainer.querySelector('.no-filter-results');
845
+ if (emptyMsg) {
846
+ emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
847
+ }
848
+ }
849
+ }
850
+
851
+ // B. Auto-Resize Textarea (data-auto-resize)
852
+ if (e.target.matches('textarea[data-auto-resize]')) {
853
+ const textarea = e.target;
854
+ textarea.style.height = 'auto';
855
+ textarea.style.height = (textarea.scrollHeight + 2) + 'px';
856
+ }
857
+ });
858
+
859
+ // Global Ctrl+K / Cmd+K listener
860
+ document.addEventListener('keydown', (e) => {
861
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
862
+ e.preventDefault();
863
+ if (typeof bluebird === 'function') {
864
+ bluebird('command', { action: 'toggle' });
865
+ }
866
+ } else if (e.key === 'Escape') {
867
+ const openCommand = document.querySelector('.command-backdrop.open');
868
+ if (openCommand && typeof bluebird === 'function') {
869
+ bluebird('command', { action: 'close' });
870
+ }
871
+ }
872
+ });
873
+ })();
874
+
875
+ // Auto Setup Helper Elements on DOMReady
876
+ (function () {
877
+ function init() {
878
+ cleanupOrphanedBackdrops();
879
+ initMobileDrawer();
880
+ document.querySelectorAll('.carousel').forEach(c => initSingleCarousel(c));
881
+
882
+ // Auto resize textareas on init
883
+ document.querySelectorAll('textarea[data-auto-resize]').forEach(t => {
884
+ t.style.height = 'auto';
885
+ t.style.height = (t.scrollHeight + 2) + 'px';
886
+ });
887
+
888
+ // Restore saved theme if available
889
+ try {
890
+ const savedTheme = localStorage.getItem('bluebird-theme');
891
+ if (savedTheme) {
892
+ document.documentElement.setAttribute('data-theme', savedTheme);
893
+ }
894
+ } catch (e) { }
895
+ }
896
+
897
+ if (document.readyState === 'loading') {
898
+ document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
899
+ } else {
900
+ setTimeout(init, 100);
901
+ }
902
+ })();
903
+
904
+ /**
905
+ * Modern fetch wrapper with automatic CSRF token support and JSON error handling.
906
+ * @param {string} [url="/"] - Target endpoint URL.
907
+ * @param {string} [method="GET"] - HTTP method.
908
+ * @param {object|boolean} [body=false] - Request JSON body payload.
909
+ * @param {FormData|boolean} [bodyForm=false] - Request FormData body payload.
910
+ * @param {object} [headers={}] - Custom headers.
911
+ * @returns {Promise<any>} Parsed JSON response.
912
+ */
913
+ async function Http(
914
+ url = "/",
915
+ method = "GET",
916
+ body = false,
917
+ bodyForm = false,
918
+ headers = {},
919
+ ) {
920
+ const csrfEl = document.getElementById("csrf");
921
+ const csrfToken = csrfEl ? csrfEl.value : null;
922
+
923
+ const mergedHeaders = { ...headers };
924
+ if (csrfToken) {
925
+ mergedHeaders["X-CSRF-Token"] = csrfToken;
926
+ }
927
+
928
+ const options = { method: method, headers: mergedHeaders, credentials: "include" };
929
+
930
+ if (body) {
931
+ const payload = csrfToken ? { ...body, csrf: csrfToken } : body;
932
+ options["body"] = JSON.stringify(payload);
933
+ if (!mergedHeaders["Content-Type"]) {
934
+ mergedHeaders["Content-Type"] = "application/json";
935
+ }
936
+ }
937
+
938
+ if (bodyForm) {
939
+ if (csrfToken && bodyForm instanceof FormData) {
940
+ bodyForm.append("csrf", csrfToken);
941
+ }
942
+ options["body"] = bodyForm;
943
+ }
944
+
945
+ const response = await fetch(url, options);
946
+ if (!response.ok) {
947
+ let errorData;
948
+ try {
949
+ errorData = await response.json();
950
+ } catch {
951
+ errorData = { message: `HTTP Error ${response.status}: ${response.statusText}` };
952
+ }
953
+ throw new Error(errorData.message || errorData.msg || errorData.mensaje || "Fetch request failed");
954
+ }
955
+ return await response.json();
956
+ }
957
+
958
+ /**
959
+ * Extracts a query parameter from the current URL search string.
960
+ * @param {string} name - Query parameter key name.
961
+ * @returns {string|null} Parameter value or null.
962
+ */
963
+ function getUrlParameter(name) {
964
+ return new URLSearchParams(window.location.search).get(name);
965
+ }
966
+
967
+ /**
968
+ * Checks if current document language starts with the specified code.
969
+ * @param {string} [l="es"] - Language prefix.
970
+ * @returns {boolean}
971
+ */
972
+ function lang(l = "es") {
973
+ const docLang = document.documentElement.lang || "es";
974
+ return docLang === l || docLang.startsWith(l);
975
+ }
976
+
977
+ /**
978
+ * Responsive Data Table component with mobile card switching, live search, and pagination.
979
+ */
980
+ class ResponsiveDataTable {
981
+ constructor(containerId, options = {}) {
982
+ this.container = typeof containerId === "string" ? document.getElementById(containerId) : containerId;
983
+ if (!this.container) return;
984
+ this.defaults = {
985
+ data: [],
986
+ columns: [],
987
+ rowsPerPage: 10,
988
+ search: true,
989
+ pagination: true,
990
+ headerTitles: {},
991
+ summaryFields: ["id"],
992
+ edit: false,
993
+ delete: false,
994
+ breakpoint: 768,
995
+ };
996
+ this.options = { ...this.defaults, ...options };
997
+ this.currentPage = 1;
998
+ this.filteredData = [...this.options.data];
999
+ this.isMobile = window.innerWidth < this.options.breakpoint;
1000
+ this.init();
1001
+ window.addEventListener("resize", () => this.handleResize());
1002
+ }
1003
+
1004
+ init() {
1005
+ this.renderContainer();
1006
+ this.updateTable();
1007
+ if (this.options.search) this.setupSearch();
1008
+ }
1009
+
1010
+ handleResize() {
1011
+ const wasMobile = this.isMobile;
1012
+ this.isMobile = window.innerWidth < this.options.breakpoint;
1013
+ if (wasMobile !== this.isMobile) this.updateTable();
1014
+ }
1015
+
1016
+ renderContainer() {
1017
+ this.container.innerHTML = `
1018
+ <section class="w-full">
1019
+ ${this.options.search
1020
+ ? `<div class="mb-4 flex items-center justify-between"><input type="search" class="datatable-search-input outline" placeholder="${lang() ? "Buscar..." : "Search..."}" aria-label="Search"/></div>`
1021
+ : ""
1022
+ }
1023
+
1024
+ <div class="overflow-x-auto">
1025
+ <table class="datatable-table hidden"></table>
1026
+ <div class="datatable-mobile"></div>
1027
+ </div>
1028
+
1029
+ ${this.options.pagination ? `<nav class="datatable-pagination mt-4 flex items-center justify-center gap-1" aria-label="Pagination"></nav>` : ""}
1030
+ </section>`;
1031
+ }
1032
+
1033
+ renderTable() {
1034
+ const table = this.container.querySelector(".datatable-table");
1035
+ const mobileView = this.container.querySelector(".datatable-mobile");
1036
+ if (!table || !mobileView) return;
1037
+ if (this.isMobile) {
1038
+ table.classList.add("hidden");
1039
+ mobileView.classList.remove("hidden");
1040
+ this.renderMobileView();
1041
+ } else {
1042
+ table.classList.remove("hidden");
1043
+ mobileView.classList.add("hidden");
1044
+ this.renderDesktopTable();
1045
+ }
1046
+ }
1047
+
1048
+ renderDesktopTable() {
1049
+ const table = this.container.querySelector(".datatable-table");
1050
+ table.innerHTML = `
1051
+ <thead>
1052
+ <tr class="datatable-header"></tr>
1053
+ </thead>
1054
+ <tbody class="datatable-body"></tbody>`;
1055
+ const headerRow = table.querySelector("thead tr");
1056
+ this.options.columns.forEach((column) => {
1057
+ const th = document.createElement("th");
1058
+ th.scope = "col";
1059
+ th.textContent =
1060
+ this.options.headerTitles[column.key] || column.title || column.key;
1061
+ headerRow.appendChild(th);
1062
+ });
1063
+ if (this.options.edit || this.options.delete) {
1064
+ const th = document.createElement("th");
1065
+ th.scope = "col";
1066
+ th.textContent = lang() ? "Acciones" : "Actions";
1067
+ headerRow.appendChild(th);
1068
+ }
1069
+ const startIndex = (this.currentPage - 1) * this.options.rowsPerPage;
1070
+ const endIndex = startIndex + this.options.rowsPerPage;
1071
+ const paginatedData = this.filteredData.slice(startIndex, endIndex);
1072
+ const tbody = table.querySelector("tbody");
1073
+ paginatedData.forEach((item) => {
1074
+ const row = document.createElement("tr");
1075
+ this.options.columns.forEach((column) => {
1076
+ const td = document.createElement("td");
1077
+ const value = item[column.key];
1078
+ if (
1079
+ value &&
1080
+ typeof value === "string" &&
1081
+ /<[a-z][\s\S]*>/i.test(value)
1082
+ ) {
1083
+ td.innerHTML = value;
1084
+ } else {
1085
+ td.textContent = value !== undefined && value !== null ? value : "-";
1086
+ }
1087
+ row.appendChild(td);
1088
+ });
1089
+ if (this.options.edit || this.options.delete) {
1090
+ const td = document.createElement("td");
1091
+ const actionsDiv = document.createElement("div");
1092
+ actionsDiv.className = "flex items-center gap-2";
1093
+ if (this.options.edit) {
1094
+ const btn = document.createElement("button");
1095
+ btn.className = "outline";
1096
+ btn.textContent = lang() ? "Editar" : "Edit";
1097
+ btn.onclick = (e) => this.handleAction("edit", e, item);
1098
+ actionsDiv.appendChild(btn);
1099
+ }
1100
+ if (this.options.delete) {
1101
+ const btn = document.createElement("button");
1102
+ btn.className = "destructive";
1103
+ btn.textContent = lang() ? "Eliminar" : "Delete";
1104
+ btn.onclick = (e) => this.handleAction("delete", e, item);
1105
+ actionsDiv.appendChild(btn);
1106
+ }
1107
+ td.appendChild(actionsDiv);
1108
+ row.appendChild(td);
1109
+ }
1110
+ tbody.appendChild(row);
1111
+ });
1112
+ }
1113
+
1114
+ renderMobileView() {
1115
+ const mobileView = this.container.querySelector(".datatable-mobile");
1116
+ mobileView.innerHTML = "";
1117
+ const startIndex = (this.currentPage - 1) * this.options.rowsPerPage;
1118
+ const endIndex = startIndex + this.options.rowsPerPage;
1119
+ const paginatedData = this.filteredData.slice(startIndex, endIndex);
1120
+ paginatedData.forEach((item) => {
1121
+ const card = document.createElement("article");
1122
+ card.className = "card mb-4";
1123
+ const summary = document.createElement("h3");
1124
+ summary.className =
1125
+ "flex items-center justify-between font-bold mb-2 pb-2";
1126
+ this.options.summaryFields.forEach((fieldKey) => {
1127
+ const value = item[fieldKey];
1128
+ summary.innerHTML += `<span>${value !== undefined && value !== null ? value : "-"}</span>`;
1129
+ });
1130
+ card.appendChild(summary);
1131
+ const details = document.createElement("dl");
1132
+ details.className =
1133
+ "grid cols-1 gap-2 mb-2 pb-2";
1134
+ this.options.columns.forEach((column) => {
1135
+ if (this.options.summaryFields.includes(column.key)) return;
1136
+ const dt = document.createElement("dt");
1137
+ dt.className =
1138
+ "font-bold text-muted";
1139
+ dt.textContent =
1140
+ this.options.headerTitles[column.key] || column.title || column.key;
1141
+ const dd = document.createElement("dd");
1142
+ dd.className =
1143
+ "text-left";
1144
+ const cellValue = item[column.key];
1145
+ if (
1146
+ cellValue &&
1147
+ typeof cellValue === "string" &&
1148
+ /<[a-z][\s\S]*>/i.test(cellValue)
1149
+ ) {
1150
+ dd.innerHTML = cellValue;
1151
+ } else {
1152
+ dd.textContent = cellValue !== undefined && cellValue !== null ? cellValue : "-";
1153
+ }
1154
+ details.appendChild(dt);
1155
+ details.appendChild(dd);
1156
+ });
1157
+ card.appendChild(details);
1158
+ if (this.options.edit || this.options.delete) {
1159
+ const actions = document.createElement("div");
1160
+ actions.className = "flex items-center gap-2 justify-end";
1161
+ if (this.options.edit) {
1162
+ const btn = document.createElement("button");
1163
+ btn.className = "outline";
1164
+ btn.textContent = lang() ? "Editar" : "Edit";
1165
+ btn.onclick = (e) => this.handleAction("edit", e, item);
1166
+ actions.appendChild(btn);
1167
+ }
1168
+ if (this.options.delete) {
1169
+ const btn = document.createElement("button");
1170
+ btn.className = "destructive";
1171
+ btn.textContent = lang() ? "Eliminar" : "Delete";
1172
+ btn.onclick = (e) => this.handleAction("delete", e, item);
1173
+ actions.appendChild(btn);
1174
+ }
1175
+ card.appendChild(actions);
1176
+ }
1177
+ mobileView.appendChild(card);
1178
+ });
1179
+ }
1180
+
1181
+ renderPagination() {
1182
+ const pagination = this.container.querySelector(".datatable-pagination");
1183
+ if (!pagination || !this.options.pagination) return;
1184
+ pagination.innerHTML = "";
1185
+ const pageCount = Math.ceil(
1186
+ this.filteredData.length / this.options.rowsPerPage,
1187
+ );
1188
+ if (pageCount <= 1) return;
1189
+
1190
+ const baseClass = "px-3 py-2 outline";
1191
+ const activeClass = "px-3 py-2";
1192
+
1193
+ const prevButton = document.createElement("button");
1194
+ prevButton.textContent = "«";
1195
+ prevButton.className = baseClass;
1196
+ prevButton.disabled = this.currentPage === 1;
1197
+ prevButton.onclick = () => this.changePage(this.currentPage - 1);
1198
+ pagination.appendChild(prevButton);
1199
+
1200
+ const maxVisible = 5;
1201
+ let start = Math.max(1, this.currentPage - Math.floor(maxVisible / 2));
1202
+ let end = start + maxVisible - 1;
1203
+ if (end > pageCount) {
1204
+ end = pageCount;
1205
+ start = Math.max(1, end - maxVisible + 1);
1206
+ }
1207
+ if (start > 1) {
1208
+ const firstButton = document.createElement("button");
1209
+ firstButton.className = baseClass;
1210
+ firstButton.textContent = "1";
1211
+ firstButton.onclick = () => this.changePage(1);
1212
+ pagination.appendChild(firstButton);
1213
+ if (start > 2) pagination.appendChild(this.createEllipsis());
1214
+ }
1215
+ for (let i = start; i <= end; i++) {
1216
+ const button = document.createElement("button");
1217
+ button.className =
1218
+ i === this.currentPage ? activeClass : baseClass;
1219
+ button.textContent = i;
1220
+ button.onclick = () => this.changePage(i);
1221
+ pagination.appendChild(button);
1222
+ }
1223
+ if (end < pageCount) {
1224
+ if (end < pageCount - 1) pagination.appendChild(this.createEllipsis());
1225
+ const lastButton = document.createElement("button");
1226
+ lastButton.className = baseClass;
1227
+ lastButton.textContent = pageCount;
1228
+ lastButton.onclick = () => this.changePage(pageCount);
1229
+ pagination.appendChild(lastButton);
1230
+ }
1231
+ const nextButton = document.createElement("button");
1232
+ nextButton.className = baseClass;
1233
+ nextButton.textContent = "»";
1234
+ nextButton.disabled = this.currentPage === pageCount;
1235
+ nextButton.onclick = () => this.changePage(this.currentPage + 1);
1236
+ pagination.appendChild(nextButton);
1237
+ }
1238
+
1239
+ createEllipsis() {
1240
+ const span = document.createElement("span");
1241
+ span.className = "px-2 text-muted";
1242
+ span.textContent = "...";
1243
+ return span;
1244
+ }
1245
+
1246
+ changePage(page) {
1247
+ this.currentPage = page;
1248
+ this.updateTable();
1249
+ }
1250
+
1251
+ handleAction(type, event, item) {
1252
+ if (!this.options[type]) return;
1253
+ const callback =
1254
+ typeof this.options[type] === "function"
1255
+ ? this.options[type]
1256
+ : window[this.options[type]];
1257
+ if (typeof callback === "function") callback(event, item.id || item);
1258
+ }
1259
+
1260
+ setupSearch() {
1261
+ const searchInput = this.container.querySelector(".datatable-search-input");
1262
+ if (!searchInput) return;
1263
+ searchInput.addEventListener("input", (e) => {
1264
+ const term = e.target.value.toLowerCase().trim();
1265
+ this.filteredData = this.options.data.filter((item) =>
1266
+ this.options.columns.some((column) =>
1267
+ String(item[column.key] || "")
1268
+ .toLowerCase()
1269
+ .includes(term),
1270
+ ),
1271
+ );
1272
+ this.currentPage = 1;
1273
+ this.updateTable();
1274
+ });
1275
+ }
1276
+
1277
+ updateTable() {
1278
+ this.renderTable();
1279
+ if (this.options.pagination) this.renderPagination();
1280
+ }
1281
+
1282
+ updateData(newData) {
1283
+ this.options.data = newData;
1284
+ this.filteredData = [...newData];
1285
+ this.currentPage = 1;
1286
+ this.updateTable();
1287
+ }
1288
+
1289
+ updateColumns(newColumns) {
1290
+ this.options.columns = newColumns;
1291
+ this.updateTable();
1292
+ }
1293
+ }
1294
+
1295
+ if (typeof window !== "undefined") {
1296
+ window.ResponsiveDataTable = ResponsiveDataTable;
1297
+ window.Http = Http;
1298
+ window.getUrlParameter = getUrlParameter;
1299
+ window.snackbar = snackbar;
1300
+ window.toast = toast;
1301
+ window.bluebird = bluebird;
1302
+ window.dismissToast = dismissToast;
1303
+ window.initMobileDrawer = initMobileDrawer;
1304
+ window.initSingleCarousel = initSingleCarousel;
1305
+ window.lang = lang;
1306
+ }
1307
+
1308
+
1309
+ export {
1310
+ bluebird,
1311
+ toast,
1312
+ snackbar,
1313
+ dismissToast,
1314
+ initMobileDrawer,
1315
+ initSingleCarousel,
1316
+ ResponsiveDataTable,
1317
+ Http,
1318
+ getUrlParameter,
1319
+ lang
1320
+ };
1321
+ export default bluebird;