@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.
Files changed (35) hide show
  1. package/README.md +55 -13
  2. package/dist/css/main.css +554 -7
  3. package/package.json +3 -1
  4. package/snippets/buttons.html +51 -0
  5. package/snippets/cards.html +132 -47
  6. package/snippets/comparison.html +26 -22
  7. package/snippets/manifest.json +180 -115
  8. package/snippets/range-group.html +125 -0
  9. package/snippets/splitter.html +44 -38
  10. package/snippets/statistics.html +31 -0
  11. package/src/js/btn-split-auto-absorb.js +327 -0
  12. package/src/js/command-palette.js +472 -0
  13. package/src/js/file-selector.js +1275 -0
  14. package/src/js/internal/logging.js +121 -0
  15. package/src/js/logic-tree-renderer.js +303 -0
  16. package/src/js/modal-dialogs.js +460 -0
  17. package/src/js/overflow.js +371 -0
  18. package/src/js/pa-stat-fit.js +184 -0
  19. package/src/js/range-group.js +663 -0
  20. package/src/js/search-autocomplete-v2.js +907 -0
  21. package/src/js/search-autocomplete.js +434 -0
  22. package/src/js/settings-panel.js +245 -0
  23. package/src/js/split-button.js +141 -0
  24. package/src/js/splitter.js +1323 -0
  25. package/src/js/toast-service.js +302 -0
  26. package/src/js/tooltips-popovers.js +275 -0
  27. package/src/js/virtual-scroll.js +143 -0
  28. package/src/js/virtual-textbox.js +803 -0
  29. package/src/scss/_core.scss +7 -0
  30. package/src/scss/core-components/_buttons.scss +44 -0
  31. package/src/scss/core-components/_cards.scss +95 -6
  32. package/src/scss/core-components/_overflow.scss +50 -0
  33. package/src/scss/core-components/_range-group.scss +474 -0
  34. package/src/scss/core-components/_statistics.scss +163 -0
  35. 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
+ })();