@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05
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/README.md +55 -13
- package/dist/css/main.css +554 -7
- package/package.json +3 -1
- package/snippets/buttons.html +51 -0
- package/snippets/cards.html +132 -47
- package/snippets/comparison.html +26 -22
- package/snippets/manifest.json +180 -115
- package/snippets/range-group.html +125 -0
- package/snippets/splitter.html +44 -38
- package/snippets/statistics.html +31 -0
- package/src/js/btn-split-auto-absorb.js +327 -0
- package/src/js/command-palette.js +472 -0
- package/src/js/file-selector.js +1275 -0
- package/src/js/internal/logging.js +121 -0
- package/src/js/logic-tree-renderer.js +303 -0
- package/src/js/modal-dialogs.js +460 -0
- package/src/js/overflow.js +371 -0
- package/src/js/pa-stat-fit.js +184 -0
- package/src/js/range-group.js +663 -0
- package/src/js/search-autocomplete-v2.js +907 -0
- package/src/js/search-autocomplete.js +434 -0
- package/src/js/settings-panel.js +245 -0
- package/src/js/split-button.js +141 -0
- package/src/js/splitter.js +1323 -0
- package/src/js/toast-service.js +302 -0
- package/src/js/tooltips-popovers.js +275 -0
- package/src/js/virtual-scroll.js +143 -0
- package/src/js/virtual-textbox.js +803 -0
- package/src/scss/_core.scss +7 -0
- package/src/scss/core-components/_buttons.scss +44 -0
- package/src/scss/core-components/_cards.scss +95 -6
- package/src/scss/core-components/_overflow.scss +50 -0
- package/src/scss/core-components/_range-group.scss +474 -0
- package/src/scss/core-components/_statistics.scss +163 -0
- package/src/scss/variables/_components.scss +41 -2
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Admin — Range Group
|
|
3
|
+
*
|
|
4
|
+
* Drives the compact multi-range filter (.pa-range-group): a toggle that
|
|
5
|
+
* summarises N numeric range filters inline, expanding into a floating panel
|
|
6
|
+
* of sliders (one .pa-range per dimension).
|
|
7
|
+
*
|
|
8
|
+
* Each .pa-range row is either dual-thumb (min–max range) or single-thumb
|
|
9
|
+
* (a >= / <= threshold), configured per-row via data attributes:
|
|
10
|
+
*
|
|
11
|
+
* data-key unique id for the row (used in event payloads)
|
|
12
|
+
* data-label display label
|
|
13
|
+
* data-min/-max numeric bounds (required)
|
|
14
|
+
* data-step step increment (default 1)
|
|
15
|
+
* data-mode "range" (default) | "single"
|
|
16
|
+
* data-bound single mode only: "gte" (default) | "lte"
|
|
17
|
+
* data-value single mode initial value (default = min for gte, max for lte)
|
|
18
|
+
* data-value-min range mode initial low (default = min)
|
|
19
|
+
* data-value-max range mode initial high (default = max)
|
|
20
|
+
* data-prefix string prepended to formatted numbers (e.g. "$")
|
|
21
|
+
* data-suffix string appended to formatted numbers (e.g. " kg")
|
|
22
|
+
* data-thousands present → group the integer part with thousands separators
|
|
23
|
+
* data-ticks major tick interval (value units) → draws tick marks
|
|
24
|
+
* data-ticks-minor minor tick interval (value units) → finer marks
|
|
25
|
+
* data-tick-labels present → render numeric labels under the major ticks
|
|
26
|
+
* data-snap-ticks present → thumbs settle on the nearest tick, not the step
|
|
27
|
+
*
|
|
28
|
+
* The track is click-to-seek: a pointerdown anywhere on a row (off the thumbs)
|
|
29
|
+
* moves the nearest thumb to that point and continues the drag from there.
|
|
30
|
+
*
|
|
31
|
+
* Positioning is done entirely in 0–100% of the rail via CSS custom
|
|
32
|
+
* properties (--_pos on thumbs, --_fill-start / --_fill-end on the fill).
|
|
33
|
+
* The SCSS reads those through logical inset properties, so RTL mirrors for
|
|
34
|
+
* free — this module inverts the pointer ratio for RTL and is otherwise
|
|
35
|
+
* direction-agnostic.
|
|
36
|
+
*
|
|
37
|
+
* Events (dispatched on the .pa-range-group element, bubbling):
|
|
38
|
+
* pa-range-group:change live on every value change detail = { values }
|
|
39
|
+
* pa-range-group:apply on Apply button detail = { values }
|
|
40
|
+
* pa-range-group:reset on Reset button detail = { values }
|
|
41
|
+
*
|
|
42
|
+
* `values` is a map keyed by data-key. Range rows → { min, max } (null when
|
|
43
|
+
* the bound is at its extent, i.e. "Any"). Single rows → { value, bound }
|
|
44
|
+
* (value null when at extent).
|
|
45
|
+
*
|
|
46
|
+
* Panel anchoring mirrors split-button.js (Floating UI, panel reparented to
|
|
47
|
+
* <body> to escape overflow clipping).
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
(function () {
|
|
51
|
+
'use strict';
|
|
52
|
+
|
|
53
|
+
var EN_DASH = '–';
|
|
54
|
+
|
|
55
|
+
// ---- number formatting -------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function groupThousands(intStr) {
|
|
58
|
+
return intStr.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatValue(row, v) {
|
|
62
|
+
var out = String(Math.round(v * 1e6) / 1e6);
|
|
63
|
+
if (row.thousands) {
|
|
64
|
+
var neg = out.charAt(0) === '-';
|
|
65
|
+
var abs = neg ? out.slice(1) : out;
|
|
66
|
+
var parts = abs.split('.');
|
|
67
|
+
parts[0] = groupThousands(parts[0]);
|
|
68
|
+
out = (neg ? '-' : '') + parts.join('.');
|
|
69
|
+
}
|
|
70
|
+
return row.prefix + out + row.suffix;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Short label for a row's current selection, or '' when it means "Any".
|
|
74
|
+
function selectionText(row) {
|
|
75
|
+
if (row.mode === 'single') {
|
|
76
|
+
if (row.bound === 'lte') {
|
|
77
|
+
return row.value >= row.max ? '' : '≤ ' + formatValue(row, row.value);
|
|
78
|
+
}
|
|
79
|
+
// gte
|
|
80
|
+
return row.value <= row.min ? '' : formatValue(row, row.value) + '+';
|
|
81
|
+
}
|
|
82
|
+
// range
|
|
83
|
+
var full = row.valueMin <= row.min && row.valueMax >= row.max;
|
|
84
|
+
if (full) return '';
|
|
85
|
+
return formatValue(row, row.valueMin) + EN_DASH + formatValue(row, row.valueMax);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- geometry ----------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function pct(row, v) {
|
|
91
|
+
if (row.max === row.min) return 0;
|
|
92
|
+
return ((v - row.min) / (row.max - row.min)) * 100;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function snap(row, v) {
|
|
96
|
+
// With data-snap-ticks, settle on the nearest tick value instead of the
|
|
97
|
+
// (possibly finer) step grid. Tick values are pre-clamped to bounds.
|
|
98
|
+
if (row.snapTicks && row.tickValues && row.tickValues.length) {
|
|
99
|
+
return nearestIn(row.tickValues, v);
|
|
100
|
+
}
|
|
101
|
+
var stepped = row.min + Math.round((v - row.min) / row.step) * row.step;
|
|
102
|
+
// Clean float dust from the division.
|
|
103
|
+
stepped = Math.round(stepped * 1e6) / 1e6;
|
|
104
|
+
return Math.min(row.max, Math.max(row.min, stepped));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nearestIn(arr, v) {
|
|
108
|
+
var best = arr[0], bestD = Math.abs(v - arr[0]);
|
|
109
|
+
for (var i = 1; i < arr.length; i++) {
|
|
110
|
+
var d = Math.abs(v - arr[i]);
|
|
111
|
+
if (d < bestD) { bestD = d; best = arr[i]; }
|
|
112
|
+
}
|
|
113
|
+
return best;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function nearestTickIndex(row, v) {
|
|
117
|
+
var arr = row.tickValues, best = 0, bestD = Math.abs(v - arr[0]);
|
|
118
|
+
for (var i = 1; i < arr.length; i++) {
|
|
119
|
+
var d = Math.abs(v - arr[i]);
|
|
120
|
+
if (d < bestD) { bestD = d; best = i; }
|
|
121
|
+
}
|
|
122
|
+
return best;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function render(row) {
|
|
126
|
+
var fill = row.el.querySelector('[data-range-fill]');
|
|
127
|
+
var minPct, maxPct, fillStart, fillEnd;
|
|
128
|
+
|
|
129
|
+
if (row.mode === 'single') {
|
|
130
|
+
var p = pct(row, row.value);
|
|
131
|
+
row.thumbMax.style.setProperty('--_pos', p + '%');
|
|
132
|
+
if (row.bound === 'lte') {
|
|
133
|
+
fillStart = 0;
|
|
134
|
+
fillEnd = 100 - p;
|
|
135
|
+
} else {
|
|
136
|
+
fillStart = p;
|
|
137
|
+
fillEnd = 0;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
minPct = pct(row, row.valueMin);
|
|
141
|
+
maxPct = pct(row, row.valueMax);
|
|
142
|
+
row.thumbMin.style.setProperty('--_pos', minPct + '%');
|
|
143
|
+
row.thumbMax.style.setProperty('--_pos', maxPct + '%');
|
|
144
|
+
fillStart = minPct;
|
|
145
|
+
fillEnd = 100 - maxPct;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (fill) {
|
|
149
|
+
fill.style.setProperty('--_fill-start', fillStart + '%');
|
|
150
|
+
fill.style.setProperty('--_fill-end', fillEnd + '%');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Aria + per-row value readout.
|
|
154
|
+
var text = selectionText(row);
|
|
155
|
+
var readout = row.el.parentNode.querySelector('[data-range-output]');
|
|
156
|
+
if (readout) {
|
|
157
|
+
readout.textContent = text || 'Any';
|
|
158
|
+
readout.classList.toggle('pa-range-group__row-value--empty', !text);
|
|
159
|
+
}
|
|
160
|
+
updateThumbAria(row);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateThumbAria(row) {
|
|
164
|
+
function set(thumb, val) {
|
|
165
|
+
if (!thumb) return;
|
|
166
|
+
thumb.setAttribute('aria-valuemin', row.min);
|
|
167
|
+
thumb.setAttribute('aria-valuemax', row.max);
|
|
168
|
+
thumb.setAttribute('aria-valuenow', val);
|
|
169
|
+
thumb.setAttribute('aria-valuetext', formatValue(row, val));
|
|
170
|
+
}
|
|
171
|
+
if (row.mode === 'single') {
|
|
172
|
+
set(row.thumbMax, row.value);
|
|
173
|
+
} else {
|
|
174
|
+
set(row.thumbMin, row.valueMin);
|
|
175
|
+
set(row.thumbMax, row.valueMax);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---- value mutation ----------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function setThumb(row, which, v) {
|
|
182
|
+
v = snap(row, v);
|
|
183
|
+
if (row.mode === 'single') {
|
|
184
|
+
row.value = v;
|
|
185
|
+
} else if (which === 'min') {
|
|
186
|
+
row.valueMin = Math.min(v, row.valueMax);
|
|
187
|
+
} else {
|
|
188
|
+
row.valueMax = Math.max(v, row.valueMin);
|
|
189
|
+
}
|
|
190
|
+
render(row);
|
|
191
|
+
row.group.onChange();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Pointer x → value, honouring RTL (rail measured from the inline-start).
|
|
195
|
+
function pointerToValue(row, clientX) {
|
|
196
|
+
var rail = row.el.querySelector('.pa-range__rail');
|
|
197
|
+
var rect = rail.getBoundingClientRect();
|
|
198
|
+
var rtl = getComputedStyle(row.el).direction === 'rtl';
|
|
199
|
+
var ratio = rtl
|
|
200
|
+
? (rect.right - clientX) / rect.width
|
|
201
|
+
: (clientX - rect.left) / rect.width;
|
|
202
|
+
ratio = Math.min(1, Math.max(0, ratio));
|
|
203
|
+
return row.min + ratio * (row.max - row.min);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- row wiring --------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
function numAttr(el, name, fallback) {
|
|
209
|
+
var raw = el.getAttribute(name);
|
|
210
|
+
if (raw === null || raw === '') return fallback;
|
|
211
|
+
var n = parseFloat(raw);
|
|
212
|
+
return isNaN(n) ? fallback : n;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function buildRow(el, group) {
|
|
216
|
+
var min = numAttr(el, 'data-min', 0);
|
|
217
|
+
var max = numAttr(el, 'data-max', 100);
|
|
218
|
+
var mode = el.getAttribute('data-mode') === 'single' ? 'single' : 'range';
|
|
219
|
+
var bound = el.getAttribute('data-bound') === 'lte' ? 'lte' : 'gte';
|
|
220
|
+
|
|
221
|
+
var row = {
|
|
222
|
+
el: el,
|
|
223
|
+
group: group,
|
|
224
|
+
key: el.getAttribute('data-key') || '',
|
|
225
|
+
label: el.getAttribute('data-label') || '',
|
|
226
|
+
min: min,
|
|
227
|
+
max: max,
|
|
228
|
+
step: numAttr(el, 'data-step', 1),
|
|
229
|
+
mode: mode,
|
|
230
|
+
bound: bound,
|
|
231
|
+
prefix: el.getAttribute('data-prefix') || '',
|
|
232
|
+
suffix: el.getAttribute('data-suffix') || '',
|
|
233
|
+
thousands: el.hasAttribute('data-thousands'),
|
|
234
|
+
snapTicks: el.hasAttribute('data-snap-ticks'),
|
|
235
|
+
thumbMin: el.querySelector('[data-range-thumb="min"]'),
|
|
236
|
+
thumbMax: el.querySelector('[data-range-thumb="max"]')
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Compute tick values up front so the initial value snap (below) can
|
|
240
|
+
// honour data-snap-ticks; the DOM marks are drawn later by renderTicks.
|
|
241
|
+
computeTicks(row);
|
|
242
|
+
|
|
243
|
+
if (mode === 'single') {
|
|
244
|
+
row.value = numAttr(el, 'data-value', bound === 'lte' ? max : min);
|
|
245
|
+
row.value = snap(row, row.value);
|
|
246
|
+
el.classList.add('pa-range--single');
|
|
247
|
+
} else {
|
|
248
|
+
row.valueMin = snap(row, numAttr(el, 'data-value-min', min));
|
|
249
|
+
row.valueMax = snap(row, numAttr(el, 'data-value-max', max));
|
|
250
|
+
if (row.valueMin > row.valueMax) row.valueMin = row.valueMax;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
wireThumb(row, row.thumbMin, 'min');
|
|
254
|
+
wireThumb(row, row.thumbMax, 'max');
|
|
255
|
+
wireTrackSeek(row);
|
|
256
|
+
renderTicks(row);
|
|
257
|
+
render(row);
|
|
258
|
+
return row;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Compute a row's tick values from data-ticks / data-ticks-minor. Stores a
|
|
262
|
+
// sorted, de-duped list on row.tickValues (used for snapping + rendering)
|
|
263
|
+
// and a set of the major positions on row.tickMajorAt. No DOM here.
|
|
264
|
+
function computeTicks(row) {
|
|
265
|
+
var majorStep = numAttr(row.el, 'data-ticks', 0);
|
|
266
|
+
if (!(majorStep > 0)) { row.tickValues = null; return; }
|
|
267
|
+
var minorStep = numAttr(row.el, 'data-ticks-minor', 0);
|
|
268
|
+
var EPS = row.step ? row.step * 1e-4 : 1e-6;
|
|
269
|
+
var key = function (v) { return Math.round(v * 1e6); };
|
|
270
|
+
|
|
271
|
+
var majorAt = {}, seen = {}, vals = [];
|
|
272
|
+
var v, cv;
|
|
273
|
+
for (v = row.min; v <= row.max + EPS; v += majorStep) {
|
|
274
|
+
majorAt[key(Math.min(v, row.max))] = true;
|
|
275
|
+
}
|
|
276
|
+
function add(v) {
|
|
277
|
+
cv = Math.min(v, row.max);
|
|
278
|
+
if (!seen[key(cv)]) { seen[key(cv)] = true; vals.push(cv); }
|
|
279
|
+
}
|
|
280
|
+
if (minorStep > 0) {
|
|
281
|
+
for (v = row.min; v <= row.max + EPS; v += minorStep) add(v);
|
|
282
|
+
}
|
|
283
|
+
for (v = row.min; v <= row.max + EPS; v += majorStep) add(v);
|
|
284
|
+
vals.sort(function (a, b) { return a - b; });
|
|
285
|
+
|
|
286
|
+
row.tickValues = vals;
|
|
287
|
+
row.tickMajorAt = majorAt;
|
|
288
|
+
row.tickLabels = row.el.hasAttribute('data-tick-labels');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Draw the tick marks (and optional labels) computed by computeTicks. The
|
|
292
|
+
// container is the rail's FIRST child, so marks paint behind the track/fill
|
|
293
|
+
// and align with thumb travel.
|
|
294
|
+
function renderTicks(row) {
|
|
295
|
+
if (!row.tickValues) return;
|
|
296
|
+
var rail = row.el.querySelector('.pa-range__rail');
|
|
297
|
+
if (!rail) return;
|
|
298
|
+
var key = function (v) { return Math.round(v * 1e6); };
|
|
299
|
+
|
|
300
|
+
var ticks = document.createElement('div');
|
|
301
|
+
ticks.className = 'pa-range__ticks';
|
|
302
|
+
var labels = null;
|
|
303
|
+
if (row.tickLabels) {
|
|
304
|
+
labels = document.createElement('div');
|
|
305
|
+
labels.className = 'pa-range__tick-labels';
|
|
306
|
+
row.el.classList.add('pa-range--ticks-labeled');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
row.tickValues.forEach(function (v) {
|
|
310
|
+
var major = !!row.tickMajorAt[key(v)];
|
|
311
|
+
var m = document.createElement('span');
|
|
312
|
+
m.className = major ? 'pa-range__tick pa-range__tick--major' : 'pa-range__tick';
|
|
313
|
+
m.style.setProperty('--_pos', pct(row, v) + '%');
|
|
314
|
+
ticks.appendChild(m);
|
|
315
|
+
if (major && labels) {
|
|
316
|
+
var l = document.createElement('span');
|
|
317
|
+
l.className = 'pa-range__tick-label';
|
|
318
|
+
l.style.setProperty('--_pos', pct(row, v) + '%');
|
|
319
|
+
l.textContent = formatValue(row, v);
|
|
320
|
+
labels.appendChild(l);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
rail.insertBefore(ticks, rail.firstChild);
|
|
325
|
+
if (labels) rail.appendChild(labels);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Which thumb should a click/drag at value `v` drive? Single → the lone
|
|
329
|
+
// (max) thumb. Range → the nearer thumb, with the bounds biased so a click
|
|
330
|
+
// beyond a thumb always grabs that thumb.
|
|
331
|
+
function nearestWhich(row, v) {
|
|
332
|
+
if (row.mode === 'single') return 'max';
|
|
333
|
+
if (v <= row.valueMin) return 'min';
|
|
334
|
+
if (v >= row.valueMax) return 'max';
|
|
335
|
+
return (v - row.valueMin) <= (row.valueMax - v) ? 'min' : 'max';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Shared drag driver for both thumb-grabs and track-seeks. Moves `which`
|
|
339
|
+
// to the pointer immediately, then follows the pointer via document-level
|
|
340
|
+
// listeners until release (works even if the pointer leaves the thumb).
|
|
341
|
+
function beginDrag(row, which, e) {
|
|
342
|
+
var thumb = which === 'min' ? row.thumbMin : row.thumbMax;
|
|
343
|
+
if (!thumb) return;
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
thumb.focus();
|
|
346
|
+
thumb.classList.add('pa-range__thumb--grabbing');
|
|
347
|
+
row._drag = which;
|
|
348
|
+
setThumb(row, which, pointerToValue(row, e.clientX));
|
|
349
|
+
|
|
350
|
+
function move(ev) {
|
|
351
|
+
if (row._drag == null) return;
|
|
352
|
+
ev.preventDefault();
|
|
353
|
+
setThumb(row, row._drag, pointerToValue(row, ev.clientX));
|
|
354
|
+
}
|
|
355
|
+
function up() {
|
|
356
|
+
if (row._drag == null) return;
|
|
357
|
+
thumb.classList.remove('pa-range__thumb--grabbing');
|
|
358
|
+
row._drag = null;
|
|
359
|
+
document.removeEventListener('pointermove', move);
|
|
360
|
+
document.removeEventListener('pointerup', up);
|
|
361
|
+
document.removeEventListener('pointercancel', up);
|
|
362
|
+
}
|
|
363
|
+
document.addEventListener('pointermove', move);
|
|
364
|
+
document.addEventListener('pointerup', up);
|
|
365
|
+
document.addEventListener('pointercancel', up);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Click anywhere on the track (not on a thumb) seeks the nearest thumb to
|
|
369
|
+
// that point and lets the drag continue from there.
|
|
370
|
+
function wireTrackSeek(row) {
|
|
371
|
+
row.el.addEventListener('pointerdown', function (e) {
|
|
372
|
+
if (e.target.closest('.pa-range__thumb')) return; // thumb owns its own
|
|
373
|
+
var v = snap(row, pointerToValue(row, e.clientX));
|
|
374
|
+
beginDrag(row, nearestWhich(row, v), e);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function wireThumb(row, thumb, which) {
|
|
379
|
+
if (!thumb) return;
|
|
380
|
+
// Single mode uses only the max thumb.
|
|
381
|
+
if (row.mode === 'single' && which === 'min') return;
|
|
382
|
+
|
|
383
|
+
thumb.setAttribute('role', 'slider');
|
|
384
|
+
thumb.setAttribute('tabindex', '0');
|
|
385
|
+
|
|
386
|
+
thumb.addEventListener('pointerdown', function (e) {
|
|
387
|
+
beginDrag(row, which, e);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
thumb.addEventListener('keydown', function (e) {
|
|
391
|
+
var cur = row.mode === 'single'
|
|
392
|
+
? row.value
|
|
393
|
+
: (which === 'min' ? row.valueMin : row.valueMax);
|
|
394
|
+
var next = null;
|
|
395
|
+
|
|
396
|
+
// Snap-to-ticks rows navigate tick-to-tick: a step-sized delta could
|
|
397
|
+
// be smaller than a tick gap and get swallowed by the snap, freezing
|
|
398
|
+
// the handle. Arrows = ±1 tick, Page = ±3 ticks.
|
|
399
|
+
if (row.snapTicks && row.tickValues && row.tickValues.length) {
|
|
400
|
+
var arr = row.tickValues;
|
|
401
|
+
var idx = nearestTickIndex(row, cur);
|
|
402
|
+
var clamp = function (i) { return Math.min(arr.length - 1, Math.max(0, i)); };
|
|
403
|
+
switch (e.key) {
|
|
404
|
+
case 'ArrowRight':
|
|
405
|
+
case 'ArrowUp': next = arr[clamp(idx + 1)]; break;
|
|
406
|
+
case 'ArrowLeft':
|
|
407
|
+
case 'ArrowDown': next = arr[clamp(idx - 1)]; break;
|
|
408
|
+
case 'PageUp': next = arr[clamp(idx + 3)]; break;
|
|
409
|
+
case 'PageDown': next = arr[clamp(idx - 3)]; break;
|
|
410
|
+
case 'Home': next = row.min; break;
|
|
411
|
+
case 'End': next = row.max; break;
|
|
412
|
+
default: return;
|
|
413
|
+
}
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
setThumb(row, which, next);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
var big = row.step * 10;
|
|
420
|
+
switch (e.key) {
|
|
421
|
+
case 'ArrowRight':
|
|
422
|
+
case 'ArrowUp': next = cur + row.step; break;
|
|
423
|
+
case 'ArrowLeft':
|
|
424
|
+
case 'ArrowDown': next = cur - row.step; break;
|
|
425
|
+
case 'PageUp': next = cur + big; break;
|
|
426
|
+
case 'PageDown': next = cur - big; break;
|
|
427
|
+
case 'Home': next = row.min; break;
|
|
428
|
+
case 'End': next = row.max; break;
|
|
429
|
+
default: return;
|
|
430
|
+
}
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
setThumb(row, which, next);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function resetRow(row) {
|
|
437
|
+
if (row.mode === 'single') {
|
|
438
|
+
row.value = row.bound === 'lte' ? row.max : row.min;
|
|
439
|
+
} else {
|
|
440
|
+
row.valueMin = row.min;
|
|
441
|
+
row.valueMax = row.max;
|
|
442
|
+
}
|
|
443
|
+
render(row);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function rowValue(row) {
|
|
447
|
+
if (row.mode === 'single') {
|
|
448
|
+
var atExtent = row.bound === 'lte' ? row.value >= row.max : row.value <= row.min;
|
|
449
|
+
return { value: atExtent ? null : row.value, bound: row.bound };
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
min: row.valueMin <= row.min ? null : row.valueMin,
|
|
453
|
+
max: row.valueMax >= row.max ? null : row.valueMax
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---- group wiring ------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
var FUI = window.FloatingUIDOM || null;
|
|
460
|
+
|
|
461
|
+
function buildGroup(root) {
|
|
462
|
+
if (root._rangeGroup) return;
|
|
463
|
+
|
|
464
|
+
var toggle = root.querySelector('[data-range-group-toggle]');
|
|
465
|
+
var panel = root.querySelector('[data-range-group-panel]');
|
|
466
|
+
var summary = root.querySelector('[data-range-group-summary]');
|
|
467
|
+
if (!toggle || !panel) return;
|
|
468
|
+
|
|
469
|
+
var group = {
|
|
470
|
+
root: root,
|
|
471
|
+
toggle: toggle,
|
|
472
|
+
panel: panel,
|
|
473
|
+
summary: summary,
|
|
474
|
+
rows: [],
|
|
475
|
+
open: false,
|
|
476
|
+
cleanup: null,
|
|
477
|
+
onChange: function () {
|
|
478
|
+
updateSummary(group);
|
|
479
|
+
dispatch(group, 'change');
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
var rowEls = root.querySelectorAll('[data-range]');
|
|
484
|
+
for (var i = 0; i < rowEls.length; i++) {
|
|
485
|
+
group.rows.push(buildRow(rowEls[i], group));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
toggle.addEventListener('click', function (e) {
|
|
489
|
+
e.stopPropagation();
|
|
490
|
+
toggleGroup(group);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
var resetBtn = root.querySelector('[data-range-group-reset]');
|
|
494
|
+
if (resetBtn) {
|
|
495
|
+
resetBtn.addEventListener('click', function (e) {
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
group.rows.forEach(resetRow);
|
|
498
|
+
updateSummary(group);
|
|
499
|
+
dispatch(group, 'change');
|
|
500
|
+
dispatch(group, 'reset');
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
var applyBtn = root.querySelector('[data-range-group-apply]');
|
|
505
|
+
if (applyBtn) {
|
|
506
|
+
applyBtn.addEventListener('click', function (e) {
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
dispatch(group, 'apply');
|
|
509
|
+
closeGroup(group);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
updateSummary(group);
|
|
514
|
+
root._rangeGroup = group;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function collectValues(group) {
|
|
518
|
+
var values = {};
|
|
519
|
+
group.rows.forEach(function (row) {
|
|
520
|
+
if (row.key) values[row.key] = rowValue(row);
|
|
521
|
+
});
|
|
522
|
+
return values;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function dispatch(group, name) {
|
|
526
|
+
group.root.dispatchEvent(new CustomEvent('pa-range-group:' + name, {
|
|
527
|
+
bubbles: true,
|
|
528
|
+
detail: { values: collectValues(group) }
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function span(cls, text) {
|
|
533
|
+
var s = document.createElement('span');
|
|
534
|
+
s.className = cls;
|
|
535
|
+
s.textContent = text;
|
|
536
|
+
return s;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// "LABEL value / LABEL value / …" on a single line.
|
|
540
|
+
function updateSummary(group) {
|
|
541
|
+
if (!group.summary) return;
|
|
542
|
+
group.summary.textContent = '';
|
|
543
|
+
group.rows.forEach(function (row, idx) {
|
|
544
|
+
if (idx > 0) {
|
|
545
|
+
group.summary.appendChild(span('pa-range-group__seg-sep', '/'));
|
|
546
|
+
}
|
|
547
|
+
var text = selectionText(row);
|
|
548
|
+
group.summary.appendChild(span('pa-range-group__seg-label', row.label));
|
|
549
|
+
group.summary.appendChild(document.createTextNode(' '));
|
|
550
|
+
group.summary.appendChild(
|
|
551
|
+
text
|
|
552
|
+
? span('pa-range-group__seg-value', text)
|
|
553
|
+
: span('pa-range-group__seg-value pa-range-group__seg-value--empty', 'Any')
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---- open/close + floating positioning --------------------------------
|
|
559
|
+
|
|
560
|
+
function toggleGroup(group) {
|
|
561
|
+
if (group.open) closeGroup(group); else openGroup(group);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function openGroup(group) {
|
|
565
|
+
closeActive();
|
|
566
|
+
group.open = true;
|
|
567
|
+
group.root.classList.add('pa-range-group--open');
|
|
568
|
+
group.toggle.setAttribute('aria-expanded', 'true');
|
|
569
|
+
|
|
570
|
+
// Reparent to body so the panel escapes any overflow:hidden ancestor.
|
|
571
|
+
group.panel._originalParent = group.root;
|
|
572
|
+
document.body.appendChild(group.panel);
|
|
573
|
+
group.panel.classList.add('pa-range-group__panel--open');
|
|
574
|
+
activeGroup = group;
|
|
575
|
+
|
|
576
|
+
if (FUI) {
|
|
577
|
+
group.cleanup = FUI.autoUpdate(group.toggle, group.panel, function () {
|
|
578
|
+
position(group);
|
|
579
|
+
});
|
|
580
|
+
} else {
|
|
581
|
+
position(group);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function position(group) {
|
|
586
|
+
if (FUI) {
|
|
587
|
+
FUI.computePosition(group.toggle, group.panel, {
|
|
588
|
+
placement: 'bottom-start',
|
|
589
|
+
strategy: 'fixed',
|
|
590
|
+
middleware: [FUI.offset(6), FUI.flip(), FUI.shift({ padding: 8 })]
|
|
591
|
+
}).then(function (pos) {
|
|
592
|
+
Object.assign(group.panel.style, {
|
|
593
|
+
position: 'fixed',
|
|
594
|
+
left: pos.x + 'px',
|
|
595
|
+
top: pos.y + 'px'
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Fallback: anchor directly under the toggle.
|
|
601
|
+
var rect = group.toggle.getBoundingClientRect();
|
|
602
|
+
Object.assign(group.panel.style, {
|
|
603
|
+
position: 'fixed',
|
|
604
|
+
left: rect.left + 'px',
|
|
605
|
+
top: (rect.bottom + 6) + 'px'
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function closeGroup(group) {
|
|
610
|
+
if (!group.open) return;
|
|
611
|
+
group.open = false;
|
|
612
|
+
group.root.classList.remove('pa-range-group--open');
|
|
613
|
+
group.toggle.setAttribute('aria-expanded', 'false');
|
|
614
|
+
|
|
615
|
+
if (group.cleanup) { group.cleanup(); group.cleanup = null; }
|
|
616
|
+
group.panel.classList.remove('pa-range-group__panel--open');
|
|
617
|
+
|
|
618
|
+
if (group.panel._originalParent) {
|
|
619
|
+
group.panel._originalParent.appendChild(group.panel);
|
|
620
|
+
delete group.panel._originalParent;
|
|
621
|
+
}
|
|
622
|
+
group.panel.style.position = '';
|
|
623
|
+
group.panel.style.left = '';
|
|
624
|
+
group.panel.style.top = '';
|
|
625
|
+
|
|
626
|
+
if (activeGroup === group) activeGroup = null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Only one panel open at a time.
|
|
630
|
+
var activeGroup = null;
|
|
631
|
+
function closeActive() {
|
|
632
|
+
if (activeGroup) closeGroup(activeGroup);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
document.addEventListener('click', function (e) {
|
|
636
|
+
if (!activeGroup) return;
|
|
637
|
+
if (activeGroup.panel.contains(e.target)) return;
|
|
638
|
+
if (activeGroup.root.contains(e.target)) return;
|
|
639
|
+
closeGroup(activeGroup);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
document.addEventListener('keydown', function (e) {
|
|
643
|
+
if (e.key === 'Escape' && activeGroup) {
|
|
644
|
+
var g = activeGroup;
|
|
645
|
+
closeGroup(g);
|
|
646
|
+
g.toggle.focus();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// ---- init --------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
function initAll(scope) {
|
|
653
|
+
(scope || document).querySelectorAll('[data-range-group]').forEach(buildGroup);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (document.readyState === 'loading') {
|
|
657
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
658
|
+
} else {
|
|
659
|
+
initAll();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
window.PaRangeGroup = { init: initAll };
|
|
663
|
+
})();
|