@seip/blue-bird 1.1.3 → 1.1.5
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/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +10 -0
- package/AGENTS.md +41 -4
- package/README.md +55 -1
- package/core/cli/docker.js +10 -1
- package/core/cli/doctor.js +294 -0
- package/core/cli/init.js +34 -5
- package/core/cli/migrate.js +342 -0
- package/core/cli/nginx.js +138 -0
- package/core/cli/route.js +134 -33
- package/core/cli/swagger.js +2 -0
- package/core/hash.js +201 -201
- package/core/index.d.ts +7 -0
- package/core/queue.js +121 -0
- package/core/validate.js +3 -2
- package/docker/docker-compose.mysql.yml +10 -0
- package/docker/docker-compose.sqlite.yml +69 -69
- package/docker/nginx.conf +23 -0
- package/docker-compose.yml +10 -0
- package/frontend/about.html +156 -156
- package/frontend/index.html +144 -144
- package/frontend/js/bluebird.d.ts +423 -0
- package/frontend/js/bluebird.js +1121 -716
- package/jsconfig.json +15 -0
- package/package.json +7 -2
package/frontend/js/bluebird.js
CHANGED
|
@@ -4,63 +4,63 @@
|
|
|
4
4
|
* @param {object} [options] - Component configuration options
|
|
5
5
|
*/
|
|
6
6
|
function bluebird(component, options) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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);
|
|
10
20
|
}
|
|
11
21
|
|
|
12
|
-
|
|
13
|
-
if (component === 'snackbar') {
|
|
14
|
-
let snackbarEl = document.getElementById('snackbar');
|
|
22
|
+
snackbarEl.className = 'show';
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
if (options && options.type) {
|
|
25
|
+
snackbarEl.classList.add(options.type);
|
|
26
|
+
} else {
|
|
27
|
+
snackbarEl.classList.add('info');
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
snackbarEl.textContent = (options && options.message) || '';
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
snackbarEl.classList.add('show');
|
|
33
|
+
}, 10);
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
35
|
+
const duration = (options && options.duration) || 3000;
|
|
36
|
+
if (snackbarEl.timeoutId) {
|
|
37
|
+
clearTimeout(snackbarEl.timeoutId);
|
|
38
|
+
}
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
snackbarEl.timeoutId = setTimeout(function () {
|
|
41
|
+
snackbarEl.className = '';
|
|
42
|
+
}, duration);
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
// --- MULTI-TOAST SYSTEM ---
|
|
46
|
+
if (component === 'toast') {
|
|
47
|
+
const position = (options && options.position) || 'bottom-right';
|
|
48
|
+
let container = document.querySelector(`.toast-container.${position}`);
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
if (!container) {
|
|
51
|
+
container = document.createElement('div');
|
|
52
|
+
container.className = `toast-container ${position}`;
|
|
53
|
+
document.body.appendChild(container);
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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}`;
|
|
56
|
+
const toastEl = document.createElement('div');
|
|
57
|
+
const typeClass = (options && options.type) ? `toast-${options.type}` : 'toast-info';
|
|
58
|
+
toastEl.className = `toast ${typeClass}`;
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
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
62
|
|
|
63
|
-
|
|
63
|
+
toastEl.innerHTML = `
|
|
64
64
|
<div class="toast-content">
|
|
65
65
|
${title}
|
|
66
66
|
${desc}
|
|
@@ -68,131 +68,137 @@ function bluebird(component, options) {
|
|
|
68
68
|
<button class="toast-close" aria-label="Dismiss">×</button>
|
|
69
69
|
`;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const closeBtn = toastEl.querySelector('.toast-close');
|
|
72
|
+
closeBtn.addEventListener('click', () => dismissToast(toastEl));
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
container.appendChild(toastEl);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
76
|
+
const duration = (options && options.duration) !== undefined ? options.duration : 4000;
|
|
77
|
+
if (duration > 0) {
|
|
78
|
+
setTimeout(() => dismissToast(toastEl), duration);
|
|
80
79
|
}
|
|
80
|
+
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// --- TABS COMPONENT ---
|
|
83
|
+
if (component === 'tab') {
|
|
84
|
+
const targetId = options && options.id;
|
|
85
|
+
if (!targetId) return;
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const targetContent = document.getElementById(targetId);
|
|
88
|
+
if (!targetContent) return;
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
const tabsContainer = targetContent.closest('.tabs');
|
|
91
|
+
if (!tabsContainer) return;
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const allTriggers = tabsContainer.querySelectorAll('.tab-trigger');
|
|
94
|
+
const allContents = tabsContainer.querySelectorAll('.tab-content');
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
allContents.forEach(c => c.classList.remove('active'));
|
|
97
|
+
allTriggers.forEach(t => t.classList.remove('active'));
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
targetContent.classList.add('active');
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
const matchingTrigger = Array.from(allTriggers).find(t =>
|
|
102
|
+
t.getAttribute('data-tab-target') === targetId || t.getAttribute('href') === `#${targetId}`
|
|
103
|
+
);
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
105
|
+
if (matchingTrigger) {
|
|
106
|
+
matchingTrigger.classList.add('active');
|
|
108
107
|
}
|
|
108
|
+
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// --- COMMAND PALETTE MODAL ---
|
|
111
|
+
if (component === 'command') {
|
|
112
|
+
const action = (options && options.action) || 'toggle';
|
|
113
|
+
let backdrop = document.querySelector('.command-backdrop');
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (!backdrop) {
|
|
116
|
+
backdrop = createCommandPaletteModal();
|
|
117
|
+
}
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
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');
|
|
131
130
|
}
|
|
131
|
+
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
138
|
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const popoverEl = document.getElementById(id) || document.querySelector(`[data-popover-id="${id}"]`);
|
|
140
|
+
if (!popoverEl) return;
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
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');
|
|
148
147
|
}
|
|
148
|
+
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// --- STANDALONE DRAWER COMPONENT ---
|
|
151
|
+
if (component === 'drawer') {
|
|
152
|
+
cleanupOrphanedBackdrops();
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
const id = options && options.id;
|
|
155
|
+
const action = (options && options.action) || 'toggle';
|
|
156
|
+
if (!id) return;
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
const drawerEl = document.getElementById(id);
|
|
159
|
+
if (!drawerEl) return;
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
188
170
|
}
|
|
189
171
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
195
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
|
+
}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
/**
|
|
@@ -200,7 +206,7 @@ function bluebird(component, options) {
|
|
|
200
206
|
* @param {object} options - Snackbar configuration options
|
|
201
207
|
*/
|
|
202
208
|
function snackbar(options) {
|
|
203
|
-
|
|
209
|
+
bluebird('snackbar', options);
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
/**
|
|
@@ -208,28 +214,28 @@ function snackbar(options) {
|
|
|
208
214
|
* @param {object} options - Toast configuration options
|
|
209
215
|
*/
|
|
210
216
|
function toast(options) {
|
|
211
|
-
|
|
217
|
+
bluebird('toast', options);
|
|
212
218
|
}
|
|
213
219
|
|
|
214
220
|
function dismissToast(toastEl) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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);
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/**
|
|
227
233
|
* Create default command palette DOM modal
|
|
228
234
|
*/
|
|
229
235
|
function createCommandPaletteModal() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
const backdrop = document.createElement('div');
|
|
237
|
+
backdrop.className = 'command-backdrop';
|
|
238
|
+
backdrop.innerHTML = `
|
|
233
239
|
<div class="command-dialog">
|
|
234
240
|
<div class="command-input-wrapper">
|
|
235
241
|
<span>🔍</span>
|
|
@@ -254,644 +260,1043 @@ function createCommandPaletteModal() {
|
|
|
254
260
|
</div>
|
|
255
261
|
`;
|
|
256
262
|
|
|
257
|
-
|
|
263
|
+
document.body.appendChild(backdrop);
|
|
258
264
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
});
|
|
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';
|
|
273
278
|
});
|
|
279
|
+
});
|
|
274
280
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
});
|
|
282
288
|
|
|
283
|
-
|
|
289
|
+
return backdrop;
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
/**
|
|
287
293
|
* Remove backdrops whose target drawer no longer exists in DOM
|
|
288
294
|
*/
|
|
289
295
|
function cleanupOrphanedBackdrops() {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
});
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
function initMobileDrawer() {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
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);
|
|
337
342
|
}
|
|
343
|
+
}
|
|
338
344
|
}
|
|
339
345
|
|
|
340
346
|
// Global keyboard listener for Ctrl+K / Cmd+K Command Palette shortcut & ESC key
|
|
341
347
|
document.addEventListener('keydown', function (e) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
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' });
|
|
352
357
|
}
|
|
358
|
+
}
|
|
353
359
|
});
|
|
354
360
|
|
|
355
361
|
// Global click listener for Material Ripples, Mobile Navigation Drawer, Tabs, Popovers & Declarative Data Attributes
|
|
356
362
|
document.addEventListener('click', function (e) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 2. Declarative Popover Trigger Click
|
|
368
|
-
const popoverTrigger = e.target.closest('[data-popover-target]');
|
|
369
|
-
if (popoverTrigger) {
|
|
370
|
-
const popoverId = popoverTrigger.getAttribute('data-popover-target');
|
|
371
|
-
bluebird('popover', { id: popoverId, action: 'toggle' });
|
|
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 });
|
|
372
370
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 4. Mobile Navigation Overlay Click
|
|
403
|
-
if (e.target.closest('.bluebird-drawer-overlay')) {
|
|
404
|
-
const drawer = document.querySelector('.bluebird-drawer');
|
|
405
|
-
const overlay = document.querySelector('.bluebird-drawer-overlay');
|
|
406
|
-
if (drawer) drawer.classList.remove('open');
|
|
407
|
-
if (overlay) overlay.classList.remove('open');
|
|
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');
|
|
408
398
|
document.body.style.overflow = '';
|
|
409
|
-
|
|
399
|
+
} else {
|
|
400
|
+
drawer.classList.add('open');
|
|
401
|
+
overlay.classList.add('open');
|
|
402
|
+
document.body.style.overflow = 'hidden';
|
|
403
|
+
}
|
|
410
404
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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' });
|
|
452
457
|
}
|
|
458
|
+
}
|
|
453
459
|
});
|
|
454
460
|
|
|
455
461
|
/**
|
|
456
462
|
* Single Carousel Initialization logic with Touch/Swipe, Drag & Arrow Controls
|
|
457
463
|
*/
|
|
458
464
|
function initSingleCarousel(carousel, opts = {}) {
|
|
459
|
-
|
|
460
|
-
|
|
465
|
+
if (carousel._bb_initialized) return;
|
|
466
|
+
carousel._bb_initialized = true;
|
|
461
467
|
|
|
462
|
-
|
|
463
|
-
|
|
468
|
+
const track = carousel.querySelector('.carousel-track');
|
|
469
|
+
if (!track) return;
|
|
464
470
|
|
|
465
|
-
|
|
466
|
-
|
|
471
|
+
const items = Array.from(track.querySelectorAll('.carousel-item, .carousel-card'));
|
|
472
|
+
if (items.length === 0) return;
|
|
467
473
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
474
|
+
const prevBtn = carousel.querySelector('.carousel-prev');
|
|
475
|
+
const nextBtn = carousel.querySelector('.carousel-next');
|
|
476
|
+
let indicatorsContainer = carousel.querySelector('.carousel-indicators');
|
|
471
477
|
|
|
472
|
-
|
|
478
|
+
let currentSlideIndex = 0;
|
|
473
479
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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);
|
|
485
504
|
}
|
|
505
|
+
}
|
|
486
506
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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;
|
|
498
528
|
}
|
|
499
|
-
|
|
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
|
+
}
|
|
500
543
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
}
|
|
507
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
|
+
}
|
|
508
621
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
523
649
|
});
|
|
650
|
+
}
|
|
651
|
+
setTimeout(() => copyTrigger.classList.remove('copied'), 2000);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
524
656
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
}
|
|
529
667
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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;
|
|
536
678
|
}
|
|
537
679
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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;
|
|
544
696
|
}
|
|
545
697
|
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
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
|
+
}
|
|
549
711
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
+
}
|
|
554
724
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
+
}
|
|
560
749
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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();
|
|
567
762
|
}
|
|
568
|
-
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
569
766
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
}
|
|
575
778
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
580
792
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
});
|
|
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
|
+
}
|
|
596
805
|
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
carousel.addEventListener('mouseleave', () => {
|
|
608
|
-
autoInterval = setInterval(() => {
|
|
609
|
-
const nextIdx = (currentSlideIndex + 1) % items.length;
|
|
610
|
-
scrollToSlide(nextIdx);
|
|
611
|
-
}, intervalTime);
|
|
612
|
-
});
|
|
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;
|
|
613
816
|
}
|
|
614
|
-
}
|
|
615
817
|
|
|
616
|
-
//
|
|
617
|
-
(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
navigator.clipboard.writeText(textToCopy.trim()).then(() => {
|
|
636
|
-
copyTrigger.classList.add('copied');
|
|
637
|
-
if (typeof bluebird === 'function') {
|
|
638
|
-
bluebird('toast', {
|
|
639
|
-
title: 'Copied to clipboard',
|
|
640
|
-
description: textToCopy.length > 50 ? textToCopy.substring(0, 50) + '...' : textToCopy,
|
|
641
|
-
type: 'success',
|
|
642
|
-
duration: 2500
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
setTimeout(() => copyTrigger.classList.remove('copied'), 2000);
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
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;
|
|
650
837
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
e.stopImmediatePropagation();
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
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
|
+
});
|
|
661
844
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
e.preventDefault();
|
|
666
|
-
const targetId = scrollTrigger.getAttribute('data-scroll-to');
|
|
667
|
-
const targetEl = document.querySelector(targetId);
|
|
668
|
-
if (targetEl) {
|
|
669
|
-
targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
670
|
-
}
|
|
671
|
-
return;
|
|
845
|
+
const emptyMsg = targetContainer.querySelector('.no-filter-results');
|
|
846
|
+
if (emptyMsg) {
|
|
847
|
+
emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
|
|
672
848
|
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
673
851
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
: (passToggle.closest('.input-group, .form-input-group, div')?.querySelector('input') || passToggle.previousElementSibling);
|
|
682
|
-
|
|
683
|
-
if (input && (input.type === 'password' || input.type === 'text')) {
|
|
684
|
-
const isPassword = input.type === 'password';
|
|
685
|
-
input.type = isPassword ? 'text' : 'password';
|
|
686
|
-
passToggle.classList.toggle('showing', isPassword);
|
|
687
|
-
passToggle.setAttribute('aria-pressed', isPassword ? 'true' : 'false');
|
|
688
|
-
}
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
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
|
+
});
|
|
691
859
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
+
})();
|
|
705
875
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
});
|
|
718
888
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
// Close dropdown if inside one
|
|
738
|
-
const parentDropdown = selectItem.closest('.dropdown-content, .popover-content');
|
|
739
|
-
if (parentDropdown) {
|
|
740
|
-
parentDropdown.classList.remove('open');
|
|
741
|
-
}
|
|
742
|
-
}
|
|
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
|
+
})();
|
|
743
904
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
}
|
|
760
938
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
+
}
|
|
772
958
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
localStorage.setItem('bluebird-theme', next);
|
|
783
|
-
} catch (err) { }
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
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
|
+
}
|
|
786
967
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
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
|
+
}
|
|
799
977
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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>
|
|
811
1029
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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);
|
|
818
1063
|
});
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
}
|
|
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 : "-";
|
|
844
1087
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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);
|
|
851
1107
|
}
|
|
1108
|
+
td.appendChild(actionsDiv);
|
|
1109
|
+
row.appendChild(td);
|
|
1110
|
+
}
|
|
1111
|
+
tbody.appendChild(row);
|
|
852
1112
|
});
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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);
|
|
866
1175
|
}
|
|
1176
|
+
card.appendChild(actions);
|
|
1177
|
+
}
|
|
1178
|
+
mobileView.appendChild(card);
|
|
867
1179
|
});
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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);
|
|
890
1207
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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());
|
|
896
1215
|
}
|
|
897
|
-
|
|
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
|
+
}
|