@seip/blue-bird 1.1.2 → 1.1.4

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