@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.
- package/LICENSE +21 -0
- package/README.md +930 -0
- package/dist/bluebird.css +10196 -0
- package/dist/bluebird.d.ts +78 -0
- package/dist/bluebird.esm.js +1321 -0
- package/dist/bluebird.html-data.json +4465 -0
- package/dist/bluebird.js +1306 -0
- package/dist/bluebird.min.css +2 -0
- package/dist/bluebird.min.js +2 -0
- package/package.json +85 -0
package/dist/bluebird.js
ADDED
|
@@ -0,0 +1,1306 @@
|
|
|
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">×</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
|
+
}
|