@ktfth/stickjs 3.0.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/CHANGELOG.md +169 -0
- package/README.md +449 -0
- package/bin/registry.json +20 -0
- package/bin/stickjs.js +158 -0
- package/llms.txt +244 -0
- package/package.json +38 -0
- package/stick-ui/components/accordion.html +25 -0
- package/stick-ui/components/autocomplete.html +82 -0
- package/stick-ui/components/command-palette.html +28 -0
- package/stick-ui/components/copy-button.html +12 -0
- package/stick-ui/components/data-table.html +191 -0
- package/stick-ui/components/dialog.html +23 -0
- package/stick-ui/components/dropdown.html +16 -0
- package/stick-ui/components/notification.html +11 -0
- package/stick-ui/components/skeleton.html +11 -0
- package/stick-ui/components/stepper.html +102 -0
- package/stick-ui/components/tabs.html +26 -0
- package/stick-ui/components/toast.html +10 -0
- package/stick-ui/components/toggle-group.html +16 -0
- package/stick-ui/components/toggle.html +9 -0
- package/stick-ui/components/tooltip.html +12 -0
- package/stick-ui/plugins/autocomplete.js +422 -0
- package/stick-ui/plugins/command-palette.js +289 -0
- package/stick-ui/plugins/data-table.js +426 -0
- package/stick-ui/plugins/dropdown.js +70 -0
- package/stick-ui/plugins/stepper.js +155 -0
- package/stick-ui/plugins/toast.js +51 -0
- package/stick-ui/plugins/tooltip.js +67 -0
- package/stick-ui/stick-ui.css +825 -0
- package/stick.d.ts +105 -0
- package/stick.js +655 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Stick UI — Data Table plugin
|
|
3
|
+
* Provides sort, filter, and pagination for HTML tables.
|
|
4
|
+
*
|
|
5
|
+
* Handlers:
|
|
6
|
+
* table-sort click:table-sort:COL_INDEX — cycle asc/desc/none on column
|
|
7
|
+
* table-filter input:table-filter:.stk-table — substring search across all cells
|
|
8
|
+
* table-paginate click:table-paginate:next — next/prev/first/last/N
|
|
9
|
+
* table-page-size change:table-page-size:.stk-table — change rows per page
|
|
10
|
+
*
|
|
11
|
+
* Initialise: stkDataTable.init(tableEl, { pageSize: 10 })
|
|
12
|
+
*/
|
|
13
|
+
(function (root) {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/* ── State store (WeakMap — no globals on the element) ───── */
|
|
17
|
+
var STATE = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function getState(table) {
|
|
20
|
+
return STATE.get(table);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensureState(table, opts) {
|
|
24
|
+
if (STATE.has(table)) return STATE.get(table);
|
|
25
|
+
var pageSize = Number(table.getAttribute('data-stk-page-size')) ||
|
|
26
|
+
(opts && opts.pageSize) || 10;
|
|
27
|
+
var state = {
|
|
28
|
+
currentPage: 1,
|
|
29
|
+
pageSize: pageSize,
|
|
30
|
+
sortCol: -1,
|
|
31
|
+
sortDir: 'none', // 'none' | 'ascending' | 'descending'
|
|
32
|
+
filterValue: '',
|
|
33
|
+
allRows: [], // original <tr> references (from <tbody>)
|
|
34
|
+
filteredRows: []
|
|
35
|
+
};
|
|
36
|
+
STATE.set(table, state);
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ── Helpers ─────────────────────────────────────────────── */
|
|
41
|
+
|
|
42
|
+
function getTable(el, param) {
|
|
43
|
+
// param may be a CSS selector for the table; fall back to closest table
|
|
44
|
+
if (param) {
|
|
45
|
+
var t = document.querySelector(param);
|
|
46
|
+
if (t) {
|
|
47
|
+
if (t.tagName === 'TABLE') return t;
|
|
48
|
+
var inner = t.querySelector('table');
|
|
49
|
+
if (inner) return inner;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return el.closest('table') || el.closest('.stk-table-wrapper');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getTableFromSelector(sel) {
|
|
56
|
+
if (!sel) return null;
|
|
57
|
+
var el = document.querySelector(sel);
|
|
58
|
+
if (!el) return null;
|
|
59
|
+
if (el.tagName === 'TABLE') return el;
|
|
60
|
+
return el.querySelector('table') || el;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function collectRows(table) {
|
|
64
|
+
var tbody = table.querySelector('tbody');
|
|
65
|
+
if (!tbody) return [];
|
|
66
|
+
return Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cellText(row, colIdx) {
|
|
70
|
+
var cells = row.querySelectorAll('td');
|
|
71
|
+
if (colIdx < 0 || colIdx >= cells.length) return '';
|
|
72
|
+
return (cells[colIdx].getAttribute('data-sort-value') || cells[colIdx].textContent || '').trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isNumeric(val) {
|
|
76
|
+
return val !== '' && !isNaN(Number(val));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── Sort ────────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
function applySort(table) {
|
|
82
|
+
var state = getState(table);
|
|
83
|
+
if (!state) return;
|
|
84
|
+
|
|
85
|
+
// Reset aria-sort on all headers
|
|
86
|
+
var headers = table.querySelectorAll('th[data-stk-sortable]');
|
|
87
|
+
for (var i = 0; i < headers.length; i++) {
|
|
88
|
+
headers[i].removeAttribute('aria-sort');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (state.sortCol < 0 || state.sortDir === 'none') {
|
|
92
|
+
// Restore original order
|
|
93
|
+
state.filteredRows = filterRows(state.allRows, state.filterValue);
|
|
94
|
+
applyPagination(table);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Mark active header
|
|
99
|
+
var activeHeader = table.querySelectorAll('th')[state.sortCol];
|
|
100
|
+
if (activeHeader) {
|
|
101
|
+
activeHeader.setAttribute('aria-sort', state.sortDir);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Decide numeric vs string
|
|
105
|
+
var col = state.sortCol;
|
|
106
|
+
var rows = state.filteredRows.length ? state.filteredRows : filterRows(state.allRows, state.filterValue);
|
|
107
|
+
var allNumeric = rows.every(function (r) {
|
|
108
|
+
var v = cellText(r, col);
|
|
109
|
+
return v === '' || isNumeric(v);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
var sorted = rows.slice().sort(function (a, b) {
|
|
113
|
+
var va = cellText(a, col);
|
|
114
|
+
var vb = cellText(b, col);
|
|
115
|
+
var cmp;
|
|
116
|
+
if (allNumeric) {
|
|
117
|
+
cmp = (Number(va) || 0) - (Number(vb) || 0);
|
|
118
|
+
} else {
|
|
119
|
+
cmp = va.localeCompare(vb, undefined, { sensitivity: 'base' });
|
|
120
|
+
}
|
|
121
|
+
return state.sortDir === 'descending' ? -cmp : cmp;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
state.filteredRows = sorted;
|
|
125
|
+
applyPagination(table);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── Filter ──────────────────────────────────────────────── */
|
|
129
|
+
|
|
130
|
+
function filterRows(allRows, filterValue) {
|
|
131
|
+
if (!filterValue) return allRows.slice();
|
|
132
|
+
var needle = filterValue.toLowerCase();
|
|
133
|
+
return allRows.filter(function (row) {
|
|
134
|
+
return (row.textContent || '').toLowerCase().indexOf(needle) !== -1;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function applyFilter(table) {
|
|
139
|
+
var state = getState(table);
|
|
140
|
+
if (!state) return;
|
|
141
|
+
state.filteredRows = filterRows(state.allRows, state.filterValue);
|
|
142
|
+
state.currentPage = 1;
|
|
143
|
+
// Re-apply sort on filtered set
|
|
144
|
+
if (state.sortCol >= 0 && state.sortDir !== 'none') {
|
|
145
|
+
applySort(table);
|
|
146
|
+
} else {
|
|
147
|
+
applyPagination(table);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── Pagination ──────────────────────────────────────────── */
|
|
152
|
+
|
|
153
|
+
function totalPages(state) {
|
|
154
|
+
return Math.max(1, Math.ceil(state.filteredRows.length / state.pageSize));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function applyPagination(table) {
|
|
158
|
+
var state = getState(table);
|
|
159
|
+
if (!state) return;
|
|
160
|
+
|
|
161
|
+
var total = totalPages(state);
|
|
162
|
+
if (state.currentPage > total) state.currentPage = total;
|
|
163
|
+
if (state.currentPage < 1) state.currentPage = 1;
|
|
164
|
+
|
|
165
|
+
var start = (state.currentPage - 1) * state.pageSize;
|
|
166
|
+
var end = start + state.pageSize;
|
|
167
|
+
|
|
168
|
+
// Hide all rows first
|
|
169
|
+
state.allRows.forEach(function (row) {
|
|
170
|
+
row.hidden = true;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Show only current page of filtered rows
|
|
174
|
+
state.filteredRows.forEach(function (row, idx) {
|
|
175
|
+
row.hidden = (idx < start || idx >= end);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
updatePaginationUI(table, state);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function updatePaginationUI(table, state) {
|
|
182
|
+
// Find wrapper
|
|
183
|
+
var wrapper = table.closest('.stk-table-wrapper') || table.parentElement;
|
|
184
|
+
if (!wrapper) return;
|
|
185
|
+
|
|
186
|
+
var total = totalPages(state);
|
|
187
|
+
var start = (state.currentPage - 1) * state.pageSize + 1;
|
|
188
|
+
var end = Math.min(state.currentPage * state.pageSize, state.filteredRows.length);
|
|
189
|
+
var count = state.filteredRows.length;
|
|
190
|
+
|
|
191
|
+
// Update page info text
|
|
192
|
+
var info = wrapper.querySelector('.stk-table-page-info');
|
|
193
|
+
if (info) {
|
|
194
|
+
if (count === 0) {
|
|
195
|
+
info.textContent = 'No results';
|
|
196
|
+
} else {
|
|
197
|
+
info.textContent = 'Showing ' + start + '\u2013' + end + ' of ' + count;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Enable/disable prev/next buttons
|
|
202
|
+
var prevBtns = wrapper.querySelectorAll('[data-stk-page="prev"]');
|
|
203
|
+
var nextBtns = wrapper.querySelectorAll('[data-stk-page="next"]');
|
|
204
|
+
var firstBtns = wrapper.querySelectorAll('[data-stk-page="first"]');
|
|
205
|
+
var lastBtns = wrapper.querySelectorAll('[data-stk-page="last"]');
|
|
206
|
+
|
|
207
|
+
for (var i = 0; i < prevBtns.length; i++) {
|
|
208
|
+
prevBtns[i].disabled = (state.currentPage <= 1);
|
|
209
|
+
}
|
|
210
|
+
for (i = 0; i < firstBtns.length; i++) {
|
|
211
|
+
firstBtns[i].disabled = (state.currentPage <= 1);
|
|
212
|
+
}
|
|
213
|
+
for (i = 0; i < nextBtns.length; i++) {
|
|
214
|
+
nextBtns[i].disabled = (state.currentPage >= total);
|
|
215
|
+
}
|
|
216
|
+
for (i = 0; i < lastBtns.length; i++) {
|
|
217
|
+
lastBtns[i].disabled = (state.currentPage >= total);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Update page number indicator if present
|
|
221
|
+
var pageNum = wrapper.querySelector('.stk-table-page-num');
|
|
222
|
+
if (pageNum) {
|
|
223
|
+
pageNum.textContent = 'Page ' + state.currentPage + ' of ' + total;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ── Auto-create pagination controls ─────────────────────── */
|
|
228
|
+
|
|
229
|
+
function createPaginationControls(table) {
|
|
230
|
+
var wrapper = table.closest('.stk-table-wrapper') || table.parentElement;
|
|
231
|
+
if (!wrapper) return;
|
|
232
|
+
// Only auto-create if data-stk-paginate exists and no pagination bar yet
|
|
233
|
+
if (!table.hasAttribute('data-stk-paginate') &&
|
|
234
|
+
!wrapper.hasAttribute('data-stk-paginate')) return;
|
|
235
|
+
if (wrapper.querySelector('.stk-table-pagination')) return;
|
|
236
|
+
|
|
237
|
+
var bar = document.createElement('div');
|
|
238
|
+
bar.className = 'stk-table-pagination';
|
|
239
|
+
|
|
240
|
+
var info = document.createElement('span');
|
|
241
|
+
info.className = 'stk-table-page-info';
|
|
242
|
+
info.setAttribute('aria-live', 'polite');
|
|
243
|
+
info.setAttribute('role', 'status');
|
|
244
|
+
bar.appendChild(info);
|
|
245
|
+
|
|
246
|
+
var nav = document.createElement('span');
|
|
247
|
+
nav.className = 'stk-table-page-nav';
|
|
248
|
+
|
|
249
|
+
var prevBtn = document.createElement('button');
|
|
250
|
+
prevBtn.className = 'stk-table-page-btn';
|
|
251
|
+
prevBtn.setAttribute('data-stk-page', 'prev');
|
|
252
|
+
prevBtn.textContent = '\u2039 Prev';
|
|
253
|
+
prevBtn.type = 'button';
|
|
254
|
+
|
|
255
|
+
var pageNum = document.createElement('span');
|
|
256
|
+
pageNum.className = 'stk-table-page-num';
|
|
257
|
+
|
|
258
|
+
var nextBtn = document.createElement('button');
|
|
259
|
+
nextBtn.className = 'stk-table-page-btn';
|
|
260
|
+
nextBtn.setAttribute('data-stk-page', 'next');
|
|
261
|
+
nextBtn.textContent = 'Next \u203A';
|
|
262
|
+
nextBtn.type = 'button';
|
|
263
|
+
|
|
264
|
+
nav.appendChild(prevBtn);
|
|
265
|
+
nav.appendChild(pageNum);
|
|
266
|
+
nav.appendChild(nextBtn);
|
|
267
|
+
bar.appendChild(nav);
|
|
268
|
+
|
|
269
|
+
// Insert after table
|
|
270
|
+
if (table.nextSibling) {
|
|
271
|
+
wrapper.insertBefore(bar, table.nextSibling);
|
|
272
|
+
} else {
|
|
273
|
+
wrapper.appendChild(bar);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Wire up click handlers on the auto-created buttons
|
|
277
|
+
prevBtn.addEventListener('click', function () {
|
|
278
|
+
navigatePage(table, 'prev');
|
|
279
|
+
});
|
|
280
|
+
nextBtn.addEventListener('click', function () {
|
|
281
|
+
navigatePage(table, 'next');
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function navigatePage(table, param) {
|
|
286
|
+
var state = getState(table);
|
|
287
|
+
if (!state) return;
|
|
288
|
+
var total = totalPages(state);
|
|
289
|
+
switch (param) {
|
|
290
|
+
case 'next': state.currentPage = Math.min(state.currentPage + 1, total); break;
|
|
291
|
+
case 'prev': state.currentPage = Math.max(state.currentPage - 1, 1); break;
|
|
292
|
+
case 'first': state.currentPage = 1; break;
|
|
293
|
+
case 'last': state.currentPage = total; break;
|
|
294
|
+
default:
|
|
295
|
+
var n = parseInt(param, 10);
|
|
296
|
+
if (!isNaN(n) && n >= 1 && n <= total) state.currentPage = n;
|
|
297
|
+
}
|
|
298
|
+
applyPagination(table);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ── Plugin API ──────────────────────────────────────────── */
|
|
302
|
+
|
|
303
|
+
var stkDataTable = {
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Register Stick handlers.
|
|
307
|
+
*/
|
|
308
|
+
install: function (stick) {
|
|
309
|
+
|
|
310
|
+
/* table-sort — click:table-sort:COL_INDEX */
|
|
311
|
+
stick.add('table-sort', function (el, param) {
|
|
312
|
+
var colIdx = parseInt(param, 10);
|
|
313
|
+
if (isNaN(colIdx)) return;
|
|
314
|
+
var table = el.closest('table');
|
|
315
|
+
if (!table) return;
|
|
316
|
+
|
|
317
|
+
var state = getState(table);
|
|
318
|
+
if (!state) {
|
|
319
|
+
stkDataTable.init(table);
|
|
320
|
+
state = getState(table);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Cycle: none → ascending → descending → none
|
|
324
|
+
if (state.sortCol !== colIdx) {
|
|
325
|
+
state.sortCol = colIdx;
|
|
326
|
+
state.sortDir = 'ascending';
|
|
327
|
+
} else {
|
|
328
|
+
switch (state.sortDir) {
|
|
329
|
+
case 'none': state.sortDir = 'ascending'; break;
|
|
330
|
+
case 'ascending': state.sortDir = 'descending'; break;
|
|
331
|
+
case 'descending': state.sortDir = 'none'; state.sortCol = -1; break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
state.currentPage = 1;
|
|
336
|
+
// Re-filter then sort
|
|
337
|
+
state.filteredRows = filterRows(state.allRows, state.filterValue);
|
|
338
|
+
applySort(table);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/* table-filter — input:table-filter:.stk-table */
|
|
342
|
+
stick.add('table-filter', function (el, param) {
|
|
343
|
+
var table = getTableFromSelector(param) || el.closest('table');
|
|
344
|
+
if (!table) return;
|
|
345
|
+
|
|
346
|
+
var state = getState(table);
|
|
347
|
+
if (!state) {
|
|
348
|
+
stkDataTable.init(table);
|
|
349
|
+
state = getState(table);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
state.filterValue = (el.value || '').trim();
|
|
353
|
+
applyFilter(table);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/* table-paginate — click:table-paginate:next */
|
|
357
|
+
stick.add('table-paginate', function (el, param) {
|
|
358
|
+
var wrapper = el.closest('.stk-table-wrapper');
|
|
359
|
+
var table = wrapper ? wrapper.querySelector('table') : el.closest('table');
|
|
360
|
+
if (!table) return;
|
|
361
|
+
|
|
362
|
+
var state = getState(table);
|
|
363
|
+
if (!state) {
|
|
364
|
+
stkDataTable.init(table);
|
|
365
|
+
state = getState(table);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
navigatePage(table, param);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/* table-page-size — change:table-page-size:.stk-table */
|
|
372
|
+
stick.add('table-page-size', function (el, param) {
|
|
373
|
+
var table = getTableFromSelector(param) || el.closest('table');
|
|
374
|
+
if (!table) return;
|
|
375
|
+
|
|
376
|
+
var state = getState(table);
|
|
377
|
+
if (!state) {
|
|
378
|
+
stkDataTable.init(table);
|
|
379
|
+
state = getState(table);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
var newSize = parseInt(el.value, 10);
|
|
383
|
+
if (isNaN(newSize) || newSize < 1) return;
|
|
384
|
+
state.pageSize = newSize;
|
|
385
|
+
state.currentPage = 1;
|
|
386
|
+
applyPagination(table);
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Initialise a table for data-table features.
|
|
392
|
+
* @param {HTMLTableElement} table
|
|
393
|
+
* @param {Object} [opts]
|
|
394
|
+
* @param {number} [opts.pageSize=10]
|
|
395
|
+
*/
|
|
396
|
+
init: function (table, opts) {
|
|
397
|
+
if (!table) return;
|
|
398
|
+
// Resolve table if a wrapper was passed
|
|
399
|
+
if (table.tagName !== 'TABLE') {
|
|
400
|
+
var inner = table.querySelector('table');
|
|
401
|
+
if (inner) table = inner;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
var state = ensureState(table, opts);
|
|
405
|
+
state.allRows = collectRows(table);
|
|
406
|
+
state.filteredRows = state.allRows.slice();
|
|
407
|
+
|
|
408
|
+
// Apply page size from options (override default if provided)
|
|
409
|
+
if (opts && opts.pageSize) {
|
|
410
|
+
state.pageSize = opts.pageSize;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Auto-create pagination bar if attribute present
|
|
414
|
+
createPaginationControls(table);
|
|
415
|
+
|
|
416
|
+
// Initial render
|
|
417
|
+
applyPagination(table);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/* ── UMD export ──────────────────────────────────────────── */
|
|
422
|
+
if (root.Stick) root.Stick.use(stkDataTable);
|
|
423
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = stkDataTable;
|
|
424
|
+
root.stkDataTable = stkDataTable;
|
|
425
|
+
|
|
426
|
+
}(typeof window !== 'undefined' ? window : this));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Stick UI — Dropdown plugin
|
|
3
|
+
* Usage: data-stick="click:dropdown" data-stick-target="#menu"
|
|
4
|
+
* Features: click-outside close, Escape key, group exclusivity
|
|
5
|
+
*/
|
|
6
|
+
(function (root) {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var stkDropdown = {
|
|
10
|
+
install: function(stick) {
|
|
11
|
+
var openMenus = [];
|
|
12
|
+
|
|
13
|
+
function closeAll() {
|
|
14
|
+
openMenus.forEach(function(entry) {
|
|
15
|
+
entry.menu.hidden = true;
|
|
16
|
+
entry.trigger.setAttribute('aria-expanded', 'false');
|
|
17
|
+
});
|
|
18
|
+
openMenus = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
document.addEventListener('click', function(e) {
|
|
22
|
+
var remaining = [];
|
|
23
|
+
openMenus.forEach(function(entry) {
|
|
24
|
+
if (!entry.trigger.contains(e.target) && !entry.menu.contains(e.target)) {
|
|
25
|
+
entry.menu.hidden = true;
|
|
26
|
+
entry.trigger.setAttribute('aria-expanded', 'false');
|
|
27
|
+
} else {
|
|
28
|
+
remaining.push(entry);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
openMenus = remaining;
|
|
32
|
+
}, true);
|
|
33
|
+
|
|
34
|
+
document.addEventListener('keydown', function(e) {
|
|
35
|
+
if (e.key === 'Escape' && openMenus.length) {
|
|
36
|
+
var last = openMenus[openMenus.length - 1];
|
|
37
|
+
closeAll();
|
|
38
|
+
last.trigger.focus();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
stick.add('dropdown', function(el, p, e, target) {
|
|
43
|
+
var isOpen = !target.hidden;
|
|
44
|
+
|
|
45
|
+
// Close others in same group
|
|
46
|
+
var group = target.dataset.stickGroup;
|
|
47
|
+
if (group) {
|
|
48
|
+
document.querySelectorAll('[data-stick-group="' + group + '"]').forEach(function(m) {
|
|
49
|
+
if (m !== target) m.hidden = true;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
target.hidden = isOpen;
|
|
54
|
+
el.setAttribute('aria-expanded', String(!isOpen));
|
|
55
|
+
|
|
56
|
+
if (!isOpen) {
|
|
57
|
+
openMenus.push({ trigger: el, menu: target });
|
|
58
|
+
var first = target.querySelector('[role="menuitem"]');
|
|
59
|
+
if (first) first.focus();
|
|
60
|
+
} else {
|
|
61
|
+
openMenus = openMenus.filter(function(entry) { return entry.menu !== target; });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (root.Stick) root.Stick.use(stkDropdown);
|
|
68
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = stkDropdown;
|
|
69
|
+
root.stkDropdown = stkDropdown;
|
|
70
|
+
}(typeof window !== 'undefined' ? window : this));
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Stick UI — Stepper / Wizard plugin
|
|
3
|
+
* Usage:
|
|
4
|
+
* data-stick="click:step-next" data-stick-target="#my-stepper"
|
|
5
|
+
* data-stick="click:step-prev" data-stick-target="#my-stepper"
|
|
6
|
+
* data-stick="click:step-goto:2" data-stick-target="#my-stepper"
|
|
7
|
+
*
|
|
8
|
+
* Markup:
|
|
9
|
+
* <div id="my-stepper" class="stk-stepper" data-stk-stepper role="group" aria-label="...">
|
|
10
|
+
* <div class="stk-stepper-header">
|
|
11
|
+
* <div class="stk-stepper-indicator" data-stk-step-indicator aria-current="step">1</div>
|
|
12
|
+
* <div class="stk-stepper-connector"></div>
|
|
13
|
+
* <div class="stk-stepper-indicator" data-stk-step-indicator>2</div>
|
|
14
|
+
* ...
|
|
15
|
+
* </div>
|
|
16
|
+
* <div class="stk-stepper-body">
|
|
17
|
+
* <div class="stk-stepper-step" data-stk-step>Step 1 content</div>
|
|
18
|
+
* <div class="stk-stepper-step" data-stk-step hidden>Step 2 content</div>
|
|
19
|
+
* ...
|
|
20
|
+
* </div>
|
|
21
|
+
* </div>
|
|
22
|
+
*/
|
|
23
|
+
(function (root) {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
var state = new WeakMap();
|
|
27
|
+
|
|
28
|
+
function getState(container) {
|
|
29
|
+
if (!state.has(container)) {
|
|
30
|
+
var steps = container.querySelectorAll('[data-stk-step]');
|
|
31
|
+
state.set(container, { currentStep: 0, totalSteps: steps.length });
|
|
32
|
+
}
|
|
33
|
+
return state.get(container);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getSteps(container) {
|
|
37
|
+
return container.querySelectorAll('[data-stk-step]');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getIndicators(container) {
|
|
41
|
+
return container.querySelectorAll('[data-stk-step-indicator]');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getConnectors(container) {
|
|
45
|
+
return container.querySelectorAll('.stk-stepper-connector');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate required fields in the given step element.
|
|
50
|
+
* Returns true if all required fields have values, false otherwise.
|
|
51
|
+
*/
|
|
52
|
+
function validateStep(stepEl) {
|
|
53
|
+
var required = stepEl.querySelectorAll('[required]');
|
|
54
|
+
var valid = true;
|
|
55
|
+
for (var i = 0; i < required.length; i++) {
|
|
56
|
+
var input = required[i];
|
|
57
|
+
if (!input.value || !input.value.trim()) {
|
|
58
|
+
valid = false;
|
|
59
|
+
input.classList.add('stk-stepper-invalid');
|
|
60
|
+
// Remove the class after the animation plays
|
|
61
|
+
(function (el) {
|
|
62
|
+
setTimeout(function () { el.classList.remove('stk-stepper-invalid'); }, 800);
|
|
63
|
+
})(input);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return valid;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Navigate to a specific step index.
|
|
71
|
+
* @param {Element} container - The stepper container
|
|
72
|
+
* @param {number} index - 0-based target step
|
|
73
|
+
* @param {boolean} skipValidation - skip validation (used for going backward)
|
|
74
|
+
*/
|
|
75
|
+
function goTo(container, index, skipValidation) {
|
|
76
|
+
var s = getState(container);
|
|
77
|
+
var steps = getSteps(container);
|
|
78
|
+
var indicators = getIndicators(container);
|
|
79
|
+
var connectors = getConnectors(container);
|
|
80
|
+
|
|
81
|
+
if (index < 0 || index >= s.totalSteps) return;
|
|
82
|
+
|
|
83
|
+
// Validate current step when moving forward
|
|
84
|
+
if (!skipValidation && index > s.currentStep) {
|
|
85
|
+
if (!validateStep(steps[s.currentStep])) return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Hide all steps, show target
|
|
89
|
+
for (var i = 0; i < steps.length; i++) {
|
|
90
|
+
steps[i].hidden = (i !== index);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update indicators
|
|
94
|
+
for (var j = 0; j < indicators.length; j++) {
|
|
95
|
+
indicators[j].removeAttribute('aria-current');
|
|
96
|
+
if (j < index) {
|
|
97
|
+
indicators[j].setAttribute('data-stk-step-complete', '');
|
|
98
|
+
} else {
|
|
99
|
+
indicators[j].removeAttribute('data-stk-step-complete');
|
|
100
|
+
}
|
|
101
|
+
if (j === index) {
|
|
102
|
+
indicators[j].setAttribute('aria-current', 'step');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update connectors — connector at index i sits between indicator i and i+1
|
|
107
|
+
for (var k = 0; k < connectors.length; k++) {
|
|
108
|
+
if (k < index) {
|
|
109
|
+
connectors[k].setAttribute('data-complete', '');
|
|
110
|
+
} else {
|
|
111
|
+
connectors[k].removeAttribute('data-complete');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
s.currentStep = index;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var stkStepper = {
|
|
119
|
+
install: function (stick) {
|
|
120
|
+
|
|
121
|
+
stick.add('step-next', function (el, param, e, target) {
|
|
122
|
+
var container = target && target.hasAttribute('data-stk-stepper') ? target : el.closest('[data-stk-stepper]');
|
|
123
|
+
if (!container) return;
|
|
124
|
+
var s = getState(container);
|
|
125
|
+
if (s.currentStep >= s.totalSteps - 1) {
|
|
126
|
+
// Already on the last step — emit completion event
|
|
127
|
+
container.dispatchEvent(new CustomEvent('stepper-complete', { bubbles: true }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
goTo(container, s.currentStep + 1, false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
stick.add('step-prev', function (el, param, e, target) {
|
|
134
|
+
var container = target && target.hasAttribute('data-stk-stepper') ? target : el.closest('[data-stk-stepper]');
|
|
135
|
+
if (!container) return;
|
|
136
|
+
var s = getState(container);
|
|
137
|
+
goTo(container, s.currentStep - 1, true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
stick.add('step-goto', function (el, param, e, target) {
|
|
141
|
+
var container = target && target.hasAttribute('data-stk-stepper') ? target : el.closest('[data-stk-stepper]');
|
|
142
|
+
if (!container) return;
|
|
143
|
+
var index = parseInt(param, 10);
|
|
144
|
+
if (isNaN(index)) return;
|
|
145
|
+
var s = getState(container);
|
|
146
|
+
var skipValidation = index <= s.currentStep;
|
|
147
|
+
goTo(container, index, skipValidation);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (root.Stick) root.Stick.use(stkStepper);
|
|
153
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = stkStepper;
|
|
154
|
+
root.stkStepper = stkStepper;
|
|
155
|
+
}(typeof window !== 'undefined' ? window : this));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Stick UI — Toast plugin
|
|
3
|
+
* Usage: data-stick="click:toast:Message here"
|
|
4
|
+
* data-stick="click:toast:success:Saved!"
|
|
5
|
+
* Container: auto-created, or add <div id="stk-toast-zone"></div>
|
|
6
|
+
*/
|
|
7
|
+
(function (root) {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
function getContainer() {
|
|
11
|
+
var c = document.getElementById('stk-toast-zone');
|
|
12
|
+
if (!c) {
|
|
13
|
+
c = document.createElement('div');
|
|
14
|
+
c.id = 'stk-toast-zone';
|
|
15
|
+
c.className = 'stk-toast-zone';
|
|
16
|
+
document.body.appendChild(c);
|
|
17
|
+
}
|
|
18
|
+
return c;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dismiss(t) {
|
|
22
|
+
t.classList.add('stk-toast-exit');
|
|
23
|
+
t.addEventListener('animationend', function() { t.remove(); }, { once: true });
|
|
24
|
+
setTimeout(function() { if (t.parentElement) t.remove(); }, 500);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var stkToast = {
|
|
28
|
+
install: function(stick) {
|
|
29
|
+
stick.add('toast', function(el, param) {
|
|
30
|
+
var variant = '', message = param;
|
|
31
|
+
var match = param.match(/^(success|error|warning|info):(.+)/);
|
|
32
|
+
if (match) { variant = match[1]; message = match[2]; }
|
|
33
|
+
|
|
34
|
+
var container = getContainer();
|
|
35
|
+
var toast = document.createElement('div');
|
|
36
|
+
toast.className = 'stk-toast' + (variant ? ' stk-toast-' + variant : '');
|
|
37
|
+
toast.setAttribute('role', 'status');
|
|
38
|
+
toast.setAttribute('aria-live', 'polite');
|
|
39
|
+
toast.innerHTML = '<span class="stk-toast-message">' + message + '</span>' +
|
|
40
|
+
'<button class="stk-toast-dismiss" aria-label="Dismiss">×</button>';
|
|
41
|
+
toast.querySelector('.stk-toast-dismiss').addEventListener('click', function() { dismiss(toast); });
|
|
42
|
+
container.appendChild(toast);
|
|
43
|
+
setTimeout(function() { dismiss(toast); }, 3000);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (root.Stick) root.Stick.use(stkToast);
|
|
49
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = stkToast;
|
|
50
|
+
root.stkToast = stkToast;
|
|
51
|
+
}(typeof window !== 'undefined' ? window : this));
|