@cahyo-dimas/freeday 3.12.1 → 3.14.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +133 -9
  2. package/COMPONENTS.md +76 -1
  3. package/README.id.md +1 -1
  4. package/README.md +1 -1
  5. package/adapters/react/components/FdyDrawer.tsx +4 -0
  6. package/adapters/react/index.d.ts +2 -0
  7. package/adapters/vue/components/FdyDrawer.vue +4 -0
  8. package/adapters/vue/index.d.ts +2 -0
  9. package/dist/freeday-app-shell.js +1 -0
  10. package/dist/freeday-autocomplete.js +1 -0
  11. package/dist/freeday-breakpoint.js +12 -2
  12. package/dist/freeday-carousel.js +1 -0
  13. package/dist/freeday-cascade.js +1 -0
  14. package/dist/freeday-cfl.js +5 -1
  15. package/dist/freeday-chart.js +1 -0
  16. package/dist/freeday-chip.js +12 -4
  17. package/dist/freeday-datepicker.js +2 -0
  18. package/dist/freeday-datetime.js +1 -0
  19. package/dist/freeday-drawer.js +2 -0
  20. package/dist/freeday-form.js +1 -0
  21. package/dist/freeday-mask.js +2 -0
  22. package/dist/freeday-menu.js +1 -0
  23. package/dist/freeday-number.js +1 -0
  24. package/dist/freeday-rating.js +1 -0
  25. package/dist/freeday-search.js +1 -0
  26. package/dist/freeday-select.js +1 -0
  27. package/dist/freeday-slider.js +1 -0
  28. package/dist/freeday-sortable.js +222 -0
  29. package/dist/freeday-stepper.js +1 -0
  30. package/dist/freeday-table.js +1 -0
  31. package/dist/freeday-tabs.js +1 -0
  32. package/dist/freeday-timepicker.js +1 -0
  33. package/dist/freeday-tree.js +1 -0
  34. package/dist/freeday-unhydrated.js +83 -0
  35. package/dist/freeday-upload.js +6 -3
  36. package/dist/freeday.bundle.css +20 -1
  37. package/dist/freeday.css +20 -1
  38. package/dist/freeday.js +366 -10
  39. package/docs/agent-onboarding.md +4 -1
  40. package/docs/getting-started.md +1 -1
  41. package/docs/integrations.md +3 -2
  42. package/package.json +2 -2
  43. package/src/components/input.css +2 -1
  44. package/src/components/sortable.css +17 -0
package/dist/freeday.js CHANGED
@@ -48,6 +48,7 @@
48
48
 
49
49
  function initShell(app) {
50
50
  if (app.dataset.fdyAppShellReady === '1') return;
51
+ if (app.closest('[data-fdy-ignore]')) return;
51
52
  app.dataset.fdyAppShellReady = '1';
52
53
 
53
54
  var toggle = app.querySelector('.fdy-app__navtoggle');
@@ -246,6 +247,7 @@
246
247
 
247
248
  function initAutocomplete(root) {
248
249
  if (root.dataset.fdyAcReady === '1') return;
250
+ if (root.closest('[data-fdy-ignore]')) return;
249
251
  root.dataset.fdyAcReady = '1';
250
252
 
251
253
  var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
@@ -409,10 +411,19 @@
409
411
  if (bp === last) return;
410
412
  last = bp;
411
413
  root.setAttribute('data-breakpoint', bp);
412
- Array.prototype.forEach.call(document.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
414
+ fillLabels(document);
415
+ root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
416
+ }
417
+
418
+ /* Also run over late-mounted markup by the stack helpers, which find it through initAll: a label
419
+ rendered after load otherwise stayed empty until the next breakpoint change. */
420
+ function fillLabels(context) {
421
+ var ctx = context || document;
422
+ var bp = last || current();
423
+ if (ctx.matches && ctx.matches('[data-fdy-breakpoint-label]')) ctx.textContent = bp;
424
+ Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-breakpoint-label]'), function (el) {
413
425
  el.textContent = bp;
414
426
  });
415
- root.dispatchEvent(new CustomEvent('fdy-breakpoint-change', { bubbles: true, detail: { breakpoint: bp } }));
416
427
  }
417
428
 
418
429
  window.addEventListener('resize', update, { passive: true });
@@ -423,6 +434,7 @@
423
434
  }
424
435
 
425
436
  window.FreedayBreakpoint = {
437
+ initAll: fillLabels,
426
438
  get: function () { return last || current(); },
427
439
  current: current,
428
440
  // true if the current breakpoint is `name` or wider (e.g. is('md') → md, lg, or xl)
@@ -650,6 +662,7 @@
650
662
 
651
663
  function initCarousel(root) {
652
664
  if (root.dataset.fdyCarouselReady === '1') return;
665
+ if (root.closest('[data-fdy-ignore]')) return;
653
666
  root.dataset.fdyCarouselReady = '1';
654
667
 
655
668
  var viewport = root.querySelector('.fdy-carousel__viewport');
@@ -830,6 +843,7 @@
830
843
 
831
844
  function initCascade(wrap) {
832
845
  if (wrap.dataset.fdyCascadeReady === '1') return;
846
+ if (wrap.closest('[data-fdy-ignore]')) return;
833
847
  wrap.dataset.fdyCascadeReady = '1';
834
848
  wrap.classList.add('fdy-cascade');
835
849
 
@@ -1295,6 +1309,7 @@
1295
1309
 
1296
1310
  function bindGroup(group) {
1297
1311
  if (group.dataset.fdyCflBound === '1') return;
1312
+ if (group.closest('[data-fdy-ignore]')) return;
1298
1313
  var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
1299
1314
  if (!dialog) return;
1300
1315
  initDialog(dialog);
@@ -1306,6 +1321,7 @@
1306
1321
 
1307
1322
  function bindOpener(opener) {
1308
1323
  if (opener.dataset.fdyCflBound === '1') return;
1324
+ if (opener.closest('[data-fdy-ignore]')) return;
1309
1325
  var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
1310
1326
  if (!dialog) return;
1311
1327
  initDialog(dialog);
@@ -1328,7 +1344,9 @@
1328
1344
  initTriggers();
1329
1345
  }
1330
1346
 
1331
- window.FreedayCfl = { init: initTriggers };
1347
+ /* initAll is what every stack helper looks for (useFreeday in Vue and React, FreedayBlazor.initAll):
1348
+ without it they skipped this enhancer, and raw CFL markup rendered by a framework never opened. */
1349
+ window.FreedayCfl = { init: initTriggers, initAll: initTriggers };
1332
1350
  })();
1333
1351
 
1334
1352
  /* Freeday, chart renderer (optional, zero-dependency, no external chart lib).
@@ -1822,6 +1840,7 @@
1822
1840
  // Auto-init renders once; explicit update() re-renders after data changes.
1823
1841
  function initChart(el) {
1824
1842
  if (el.dataset.fdyChartReady === '1') return;
1843
+ if (el.closest('[data-fdy-ignore]')) return;
1825
1844
  el.dataset.fdyChartReady = '1';
1826
1845
  renderChart(el);
1827
1846
  }
@@ -1845,8 +1864,10 @@
1845
1864
  /* Freeday, chip enhancer (optional, zero-dependency).
1846
1865
  * Two behaviours, both progressive enhancement:
1847
1866
  *
1848
- * 1. Removable chips, any .fdy-chip__remove button removes its chip and emits a
1849
- * bubbling "fdy-chip-remove" (detail {value}).
1867
+ * 1. Removable chips, any .fdy-chip__remove button emits a bubbling, CANCELABLE
1868
+ * "fdy-chip-remove" (detail {value}) and then removes its chip, unless a listener called
1869
+ * preventDefault(). Cancel it whenever the app owns the removal: a confirm step, or a chip
1870
+ * rendered from framework state, which should leave the DOM when the state does.
1850
1871
  * 2. Choice / filter groups, a [data-fdy-chips] container of interactive chips
1851
1872
  * (.fdy-chip--choice / .fdy-chip--filter, each a <button>) toggles aria-pressed
1852
1873
  * on click. Add data-single for one-at-a-time (radio-like) selection; filter
@@ -1860,6 +1881,7 @@
1860
1881
 
1861
1882
  function initRemove(btn) {
1862
1883
  if (btn.dataset.fdyChipRmReady === '1') return;
1884
+ if (btn.closest('[data-fdy-ignore]')) return;
1863
1885
  btn.dataset.fdyChipRmReady = '1';
1864
1886
  btn.addEventListener('click', function () {
1865
1887
  var chip = btn.closest('.fdy-chip');
@@ -1872,13 +1894,18 @@
1872
1894
  if (rm) rm.remove();
1873
1895
  value = (clone.textContent || '').trim();
1874
1896
  }
1875
- chip.dispatchEvent(new CustomEvent('fdy-chip-remove', { bubbles: true, detail: { value: value } }));
1876
- chip.remove();
1897
+ /* Cancelable, because the kit cannot know whether this removal needs asking first. Removing
1898
+ unconditionally made a chip vanish while the app's own confirm dialog was still open, and
1899
+ stay gone after Cancel, with the data intact and the screen wrong (CATATAN-FREEDAY §9). */
1900
+ var ev = new CustomEvent('fdy-chip-remove', { bubbles: true, cancelable: true, detail: { value: value } });
1901
+ chip.dispatchEvent(ev);
1902
+ if (!ev.defaultPrevented) chip.remove();
1877
1903
  });
1878
1904
  }
1879
1905
 
1880
1906
  function initGroup(group) {
1881
1907
  if (group.dataset.fdyChipsReady === '1') return;
1908
+ if (group.closest('[data-fdy-ignore]')) return;
1882
1909
  group.dataset.fdyChipsReady = '1';
1883
1910
  var single = group.hasAttribute('data-single');
1884
1911
  if (!group.getAttribute('role')) group.setAttribute('role', 'group');
@@ -2210,6 +2237,7 @@
2210
2237
 
2211
2238
  function initPicker(wrap) {
2212
2239
  if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
2240
+ if (wrap.closest('[data-fdy-ignore]')) return null;
2213
2241
  wrap.dataset.fdyDpReady = '1';
2214
2242
  wrap.classList.add('fdy-datepicker');
2215
2243
 
@@ -2739,6 +2767,7 @@
2739
2767
 
2740
2768
  function initRange(root) {
2741
2769
  if (root.dataset.fdyDrReady === '1') return;
2770
+ if (root.closest('[data-fdy-ignore]')) return;
2742
2771
  root.dataset.fdyDrReady = '1';
2743
2772
  var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
2744
2773
  var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
@@ -2828,6 +2857,7 @@
2828
2857
 
2829
2858
  function initDatetime(wrap) {
2830
2859
  if (wrap.dataset.fdyDtReady === '1') return;
2860
+ if (wrap.closest('[data-fdy-ignore]')) return;
2831
2861
  wrap.dataset.fdyDtReady = '1';
2832
2862
  wrap.classList.add('fdy-datetimepicker');
2833
2863
 
@@ -2906,6 +2936,7 @@
2906
2936
 
2907
2937
  function initDrawer(dialog) {
2908
2938
  if (dialog.dataset.fdyDrawerReady === '1') return;
2939
+ if (dialog.closest('[data-fdy-ignore]')) return;
2909
2940
  dialog.dataset.fdyDrawerReady = '1';
2910
2941
  // Click on the backdrop (target is the dialog box itself) closes it.
2911
2942
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
@@ -2916,6 +2947,7 @@
2916
2947
 
2917
2948
  function bindTrigger(btn) {
2918
2949
  if (btn.dataset.fdyDrawerBound === '1') return;
2950
+ if (btn.closest('[data-fdy-ignore]')) return;
2919
2951
  var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
2920
2952
  if (!dialog) return;
2921
2953
  initDrawer(dialog);
@@ -3133,6 +3165,7 @@
3133
3165
 
3134
3166
  function initForm(form) {
3135
3167
  if (form.dataset.fdyFormReady === '1') return;
3168
+ if (form.closest('[data-fdy-ignore]')) return;
3136
3169
  form.dataset.fdyFormReady = '1';
3137
3170
  form.setAttribute('novalidate', '');
3138
3171
  var eager = form.hasAttribute('data-fdy-eager');
@@ -3317,6 +3350,7 @@
3317
3350
 
3318
3351
  function initMask(el) {
3319
3352
  if (el.dataset.fdyMaskReady === '1') return;
3353
+ if (el.closest('[data-fdy-ignore]')) return;
3320
3354
  el.dataset.fdyMaskReady = '1';
3321
3355
  var pattern = el.getAttribute('data-fdy-mask') || '';
3322
3356
  if (!pattern) return;
@@ -3339,6 +3373,7 @@
3339
3373
 
3340
3374
  function initReveal(input) {
3341
3375
  if (input.dataset.fdyPwReady === '1') return;
3376
+ if (input.closest('[data-fdy-ignore]')) return;
3342
3377
  input.dataset.fdyPwReady = '1';
3343
3378
 
3344
3379
  var group = document.createElement('span');
@@ -3402,6 +3437,7 @@
3402
3437
 
3403
3438
  function initMenu(wrap) {
3404
3439
  if (wrap.dataset.fdyMenuReady === '1') return;
3440
+ if (wrap.closest('[data-fdy-ignore]')) return;
3405
3441
  wrap.dataset.fdyMenuReady = '1';
3406
3442
  var trigger = wrap.querySelector('[aria-haspopup="menu"]');
3407
3443
  var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
@@ -3521,6 +3557,7 @@
3521
3557
 
3522
3558
  function initNumber(root) {
3523
3559
  if (root.dataset.fdyNumberReady === '1') return;
3560
+ if (root.closest('[data-fdy-ignore]')) return;
3524
3561
 
3525
3562
  var input = root.querySelector('input[type="number"]');
3526
3563
  var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
@@ -3699,6 +3736,7 @@
3699
3736
 
3700
3737
  function initRating(root) {
3701
3738
  if (root.dataset.fdyRatingReady === '1') return;
3739
+ if (root.closest('[data-fdy-ignore]')) return;
3702
3740
  root.dataset.fdyRatingReady = '1';
3703
3741
 
3704
3742
  var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));
@@ -3769,6 +3807,7 @@
3769
3807
 
3770
3808
  function initSearch(root) {
3771
3809
  if (root.dataset.fdySearchReady === '1') return;
3810
+ if (root.closest('[data-fdy-ignore]')) return;
3772
3811
  var input = root.querySelector('input');
3773
3812
  if (!input) return;
3774
3813
  root.dataset.fdySearchReady = '1';
@@ -3855,6 +3894,7 @@
3855
3894
 
3856
3895
  function initCombo(root) {
3857
3896
  if (root.dataset.fdyComboReady === '1') return; // idempotent
3897
+ if (root.closest('[data-fdy-ignore]')) return;
3858
3898
  var button = root.querySelector('.fdy-combo__button');
3859
3899
  var listbox = root.querySelector('.fdy-combo__listbox');
3860
3900
  var valueEl = root.querySelector('.fdy-combo__value');
@@ -4067,6 +4107,7 @@
4067
4107
 
4068
4108
  function initSlider(input) {
4069
4109
  if (input.dataset.fdySliderReady === '1') return;
4110
+ if (input.closest('[data-fdy-ignore]')) return;
4070
4111
  input.dataset.fdySliderReady = '1';
4071
4112
  var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
4072
4113
  if (!out) return;
@@ -4091,6 +4132,229 @@
4091
4132
  window.FreedaySlider = { init: initSlider, initAll: initAll };
4092
4133
  })();
4093
4134
 
4135
+ /* Freeday, sortable list enhancer (optional, zero-dependency).
4136
+ * Reorder the children of a list by dragging a handle, or by focusing it and pressing ↑ / ↓. The
4137
+ * keyboard path is not an extra: drag-only reordering locks out every keyboard and screen-reader
4138
+ * user from a settings screen, and that path is the part each app used to write for itself
4139
+ * (CATATAN-FREEDAY §15).
4140
+ *
4141
+ * Markup contract:
4142
+ * <ul class="fdy-sortable" data-fdy-sortable> (or a <tbody>, or a <div>)
4143
+ * <li data-key="ketua" data-label="Ketua">
4144
+ * <button class="fdy-sortable__handle" type="button">…icon…</button> Ketua
4145
+ * </li>
4146
+ * <li data-key="sistem" data-fdy-sort-fixed>…</li> (never moves, and nothing passes it)
4147
+ * </ul>
4148
+ * The items are the container's element children. `data-key` names each in the event; `data-label`
4149
+ * is what the handle's default aria-label and the announcement call it (the item text otherwise).
4150
+ * `data-disabled` on the container switches reordering off, e.g. while the list is filtered, and
4151
+ * the handles follow it through aria-disabled.
4152
+ *
4153
+ * Emits a bubbling, CANCELABLE "fdy-sort-change" on the container, detail {key, from, to, order},
4154
+ * `order` being every item's key in the new order. Not cancelled, the kit moves the element, which
4155
+ * is the whole job on a static page. A framework that renders the list from state cancels it and
4156
+ * reorders the state instead, because the DOM is the framework's and an element moved behind its
4157
+ * back leaves the two disagreeing about where each row is. Either way the kit announces the move
4158
+ * and, after a keyboard step, puts focus back on the moved item's handle (looked up by data-key,
4159
+ * since a framework re-render may have replaced the element).
4160
+ *
4161
+ * The drag shows where the item will land (is-drop-before / is-drop-after on the item it lands
4162
+ * against) and moves nothing until the pointer is released; Escape cancels it. Exposes
4163
+ * window.FreedaySortable.
4164
+ */
4165
+ (function () {
4166
+ 'use strict';
4167
+
4168
+ var TEXT = {
4169
+ handle: 'Move {label}. Drag, or press the up and down arrow keys.',
4170
+ moved: '{label} moved to position {position} of {total}.'
4171
+ };
4172
+ /* Kebab-cased for the lookup, see the note in freeday-upload.js. */
4173
+ function textAttr(root, key) {
4174
+ if (!root || !root.getAttribute) return null;
4175
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
4176
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
4177
+ }
4178
+ function textOf(root, key, vars) {
4179
+ var custom = textAttr(root, key);
4180
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
4181
+ for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
4182
+ return s;
4183
+ }
4184
+
4185
+ var HANDLE = '.fdy-sortable__handle';
4186
+
4187
+ var live = null;
4188
+ function announce(message) {
4189
+ if (live === null) {
4190
+ live = document.createElement('div');
4191
+ live.className = 'fdy-visually-hidden';
4192
+ live.setAttribute('aria-live', 'polite');
4193
+ document.body.appendChild(live);
4194
+ }
4195
+ /* The same sentence twice in a row is no change, and a live region announces changes. */
4196
+ live.textContent = live.textContent === message ? message + ' ' : message;
4197
+ }
4198
+
4199
+ function items(root) {
4200
+ return Array.prototype.slice.call(root.children);
4201
+ }
4202
+ function keyOf(item, index) {
4203
+ var key = item.getAttribute('data-key');
4204
+ return key !== null ? key : String(index);
4205
+ }
4206
+ function labelOf(item) {
4207
+ var label = item.getAttribute('data-label');
4208
+ return label !== null ? label : (item.textContent || '').replace(/\s+/g, ' ').trim();
4209
+ }
4210
+ function disabled(root) {
4211
+ return root.hasAttribute('data-disabled');
4212
+ }
4213
+ function itemOf(root, node) {
4214
+ for (var n = node; n && n !== root; n = n.parentElement) {
4215
+ if (n.parentElement === root) return n;
4216
+ }
4217
+ return null;
4218
+ }
4219
+
4220
+ /* A move is allowed only if it shifts no fixed item, which is the same as saying the item does
4221
+ not start fixed and does not cross one on its way. */
4222
+ function allowed(list, from, to) {
4223
+ if (from === to || to < 0 || to >= list.length) return false;
4224
+ var lo = Math.min(from, to), hi = Math.max(from, to);
4225
+ for (var i = lo; i <= hi; i++) if (list[i].hasAttribute('data-fdy-sort-fixed')) return false;
4226
+ return true;
4227
+ }
4228
+
4229
+ function syncHandles(root) {
4230
+ var off = disabled(root) ? 'true' : 'false';
4231
+ items(root).forEach(function (item) {
4232
+ var handle = item.querySelector(HANDLE);
4233
+ if (!handle) return;
4234
+ if (handle.getAttribute('aria-disabled') !== off) handle.setAttribute('aria-disabled', off);
4235
+ if (!handle.hasAttribute('aria-label')) handle.setAttribute('aria-label', textOf(root, 'handle', { label: labelOf(item) }));
4236
+ });
4237
+ }
4238
+
4239
+ function commit(root, from, to, refocus) {
4240
+ var list = items(root);
4241
+ var item = list[from];
4242
+ var key = keyOf(item, from);
4243
+ var keys = list.map(keyOf);
4244
+ keys.splice(to, 0, keys.splice(from, 1)[0]);
4245
+ var ev = new CustomEvent('fdy-sort-change', {
4246
+ bubbles: true,
4247
+ cancelable: true,
4248
+ detail: { key: key, from: from, to: to, order: keys }
4249
+ });
4250
+ root.dispatchEvent(ev);
4251
+ if (!ev.defaultPrevented) root.insertBefore(item, to > from ? list[to].nextSibling : list[to]);
4252
+ announce(textOf(root, 'moved', { label: labelOf(item), position: String(to + 1), total: String(list.length) }));
4253
+ if (!refocus) return;
4254
+ /* After the app's own render, which a framework runs after this handler returns. Moving a focused
4255
+ element drops its focus, and a keyed re-render may have replaced the element outright. */
4256
+ setTimeout(function () {
4257
+ var now = items(root);
4258
+ var moved = item.parentElement === root ? item : now.filter(function (el, i) { return keyOf(el, i) === key; })[0];
4259
+ var handle = moved ? moved.querySelector(HANDLE) : null;
4260
+ if (handle && document.activeElement !== handle) handle.focus();
4261
+ }, 0);
4262
+ }
4263
+
4264
+ function initSortable(root) {
4265
+ if (root.dataset.fdySortableReady === '1') return;
4266
+ if (root.closest('[data-fdy-ignore]')) return;
4267
+ root.dataset.fdySortableReady = '1';
4268
+ syncHandles(root);
4269
+ /* Rows a framework adds later get their aria-label, and data-disabled reaches every handle. */
4270
+ new MutationObserver(function () { syncHandles(root); })
4271
+ .observe(root, { childList: true, attributes: true, attributeFilter: ['data-disabled'] });
4272
+
4273
+ root.addEventListener('keydown', function (e) {
4274
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
4275
+ if (!handle || !root.contains(handle) || disabled(root)) return;
4276
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
4277
+ e.preventDefault();
4278
+ var list = items(root);
4279
+ var from = list.indexOf(itemOf(root, handle));
4280
+ var to = from + (e.key === 'ArrowUp' ? -1 : 1);
4281
+ if (from !== -1 && allowed(list, from, to)) commit(root, from, to, true);
4282
+ });
4283
+
4284
+ var drag = null;
4285
+
4286
+ function clearMarks() {
4287
+ items(root).forEach(function (el) { el.classList.remove('is-drop-before', 'is-drop-after'); });
4288
+ }
4289
+ function end(apply) {
4290
+ if (drag === null) return;
4291
+ var d = drag;
4292
+ drag = null;
4293
+ document.removeEventListener('keydown', onEscape, true);
4294
+ clearMarks();
4295
+ d.item.classList.remove('is-dragging');
4296
+ root.classList.remove('is-sorting');
4297
+ if (apply && d.started && d.to !== null) commit(root, d.from, d.to, false);
4298
+ }
4299
+ function onEscape(e) {
4300
+ if (e.key === 'Escape') { e.preventDefault(); end(false); }
4301
+ }
4302
+
4303
+ root.addEventListener('pointerdown', function (e) {
4304
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
4305
+ if (!handle || !root.contains(handle) || disabled(root) || e.button !== 0) return;
4306
+ var item = itemOf(root, handle);
4307
+ if (!item) return;
4308
+ e.preventDefault();
4309
+ handle.focus();
4310
+ handle.setPointerCapture(e.pointerId);
4311
+ drag = { item: item, from: items(root).indexOf(item), started: false, to: null };
4312
+ document.addEventListener('keydown', onEscape, true);
4313
+ });
4314
+
4315
+ root.addEventListener('pointermove', function (e) {
4316
+ if (drag === null) return;
4317
+ /* No travel threshold: a press that never leaves its own row resolves to to === from, which
4318
+ allowed() refuses, so a click on the handle cannot reorder anything. */
4319
+ if (!drag.started) {
4320
+ drag.started = true;
4321
+ drag.item.classList.add('is-dragging');
4322
+ root.classList.add('is-sorting');
4323
+ }
4324
+ clearMarks();
4325
+ drag.to = null;
4326
+ var list = items(root);
4327
+ var over = itemOf(root, document.elementFromPoint(e.clientX, e.clientY));
4328
+ if (!over) return;
4329
+ var rect = over.getBoundingClientRect();
4330
+ var after = e.clientY > rect.top + rect.height / 2;
4331
+ var slot = list.indexOf(over) + (after ? 1 : 0);
4332
+ var to = slot > drag.from ? slot - 1 : slot;
4333
+ if (!allowed(list, drag.from, to)) return;
4334
+ drag.to = to;
4335
+ over.classList.add(after ? 'is-drop-after' : 'is-drop-before');
4336
+ });
4337
+
4338
+ root.addEventListener('pointerup', function () { end(true); });
4339
+ root.addEventListener('pointercancel', function () { end(false); });
4340
+ }
4341
+
4342
+ function initAll(context) {
4343
+ var root = context || document;
4344
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
4345
+ if (root.matches && root.matches('[data-fdy-sortable]')) initSortable(root);
4346
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-sortable]'), initSortable);
4347
+ }
4348
+
4349
+ if (document.readyState === 'loading') {
4350
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
4351
+ } else {
4352
+ initAll();
4353
+ }
4354
+
4355
+ window.FreedaySortable = { init: initSortable, initAll: initAll };
4356
+ })();
4357
+
4094
4358
  /* Freeday, stepper/wizard enhancer (optional, zero-dependency).
4095
4359
  * Drives a step header + panels: prev/next navigation, completed/active state, and
4096
4360
  * clicking a reached step to revisit it (linear, can't skip ahead). Auto-inits
@@ -4142,6 +4406,7 @@
4142
4406
 
4143
4407
  function initStepper(root) {
4144
4408
  if (root.dataset.fdyStepperReady === '1') return;
4409
+ if (root.closest('[data-fdy-ignore]')) return;
4145
4410
  root.dataset.fdyStepperReady = '1';
4146
4411
 
4147
4412
  var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));
@@ -4341,6 +4606,7 @@
4341
4606
 
4342
4607
  function initTable(root) {
4343
4608
  if (root.dataset.fdyTableReady === '1') return;
4609
+ if (root.closest('[data-fdy-ignore]')) return;
4344
4610
  var table = root.querySelector('table.fdy-table');
4345
4611
  var tbody = table && table.querySelector('tbody');
4346
4612
  if (!table || !tbody) return;
@@ -4884,6 +5150,7 @@
4884
5150
 
4885
5151
  function initTabs(root) {
4886
5152
  if (root.dataset.fdyTabsReady === '1') return;
5153
+ if (root.closest('[data-fdy-ignore]')) return;
4887
5154
  var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
4888
5155
  if (tabs.length === 0) return;
4889
5156
  root.dataset.fdyTabsReady = '1';
@@ -4999,6 +5266,7 @@
4999
5266
 
5000
5267
  function initTime(wrap) {
5001
5268
  if (wrap.dataset.fdyTpReady === '1') return;
5269
+ if (wrap.closest('[data-fdy-ignore]')) return;
5002
5270
  wrap.dataset.fdyTpReady = '1';
5003
5271
  wrap.classList.add('fdy-timepicker');
5004
5272
 
@@ -5435,6 +5703,7 @@
5435
5703
 
5436
5704
  function initTree(tree) {
5437
5705
  if (tree.dataset.fdyTreeReady === '1') return;
5706
+ if (tree.closest('[data-fdy-ignore]')) return;
5438
5707
  tree.dataset.fdyTreeReady = '1';
5439
5708
  Array.prototype.forEach.call(tree.querySelectorAll('input[type="checkbox"]'), function (box) {
5440
5709
  // A branch checkbox lives inside <summary>; keep its click from toggling the branch.
@@ -5465,6 +5734,90 @@
5465
5734
  window.FreedayTree = { init: initTree, initAll: initAll };
5466
5735
  })();
5467
5736
 
5737
+ /* Freeday, un-hydrated hook warning (optional, zero-dependency, no markup of its own).
5738
+ * Every enhancer marks the element it took over (`data-fdy-*-ready`). Markup that no initAll()
5739
+ * ever reached carries the hook and not the mark, and it fails in the worst way available: it
5740
+ * renders, looks finished, and does nothing, with no error anywhere. A `data-fdy-validate` form
5741
+ * outside the subtree handed to useFreeday() neither paints a message nor fires fdy-form-valid,
5742
+ * so its save button reads as dead (CATATAN-FREEDAY §10). The enhancer knows the attribute is
5743
+ * there; the person who wrote it does not.
5744
+ *
5745
+ * So on the first click, focus or submit inside such an element this logs ONE console.warn per
5746
+ * element, naming the hook, with the element itself as the second argument so devtools links it.
5747
+ * Interaction, not load, is the trigger on purpose: a framework mounts and hydrates in between,
5748
+ * and only once a person is touching the widget is "not enhanced yet" the same as "never".
5749
+ *
5750
+ * HOOKS pairs each hook selector with the ready flag its enhancer stamps on THAT element and the
5751
+ * namespace that owns it. A hook whose enhancer is not loaded is skipped: that is a different
5752
+ * mistake, and this message would misname it. test/docs.test.mjs fails if an enhancer gains a
5753
+ * ready flag this table does not list.
5754
+ *
5755
+ * `data-fdy-ignore` on an element or any ancestor is the sanctioned way out: every enhancer skips
5756
+ * that subtree, and this stays quiet about it. It is for markup whose behaviour something else
5757
+ * owns: the kit's own Vue/React FdyDrawer, and kit classes an app uses for their look
5758
+ * alone (CATATAN-FREEDAY §14: a chip matrix the app drives itself).
5759
+ */
5760
+ (function () {
5761
+ 'use strict';
5762
+
5763
+ var HOOKS = [
5764
+ ['[data-fdy-app]', 'fdyAppShellReady', 'FreedayAppShell'],
5765
+ ['[data-fdy-autocomplete]', 'fdyAcReady', 'FreedayAutocomplete'],
5766
+ ['[data-fdy-carousel]', 'fdyCarouselReady', 'FreedayCarousel'],
5767
+ ['[data-fdy-cascade]', 'fdyCascadeReady', 'FreedayCascade'],
5768
+ ['[data-fdy-cfl]', 'fdyCflBound', 'FreedayCfl'],
5769
+ ['[data-fdy-cfl-open]', 'fdyCflBound', 'FreedayCfl'],
5770
+ ['[data-fdy-chart]', 'fdyChartReady', 'FreedayChart'],
5771
+ ['[data-fdy-chips]', 'fdyChipsReady', 'FreedayChip'],
5772
+ ['.fdy-chip__remove', 'fdyChipRmReady', 'FreedayChip'],
5773
+ ['[data-fdy-datepicker]', 'fdyDpReady', 'FreedayDatepicker'],
5774
+ ['[data-fdy-daterange]', 'fdyDrReady', 'FreedayDatepicker'],
5775
+ ['[data-fdy-datetimepicker]', 'fdyDtReady', 'FreedayDatetime'],
5776
+ ['[data-fdy-drawer]', 'fdyDrawerBound', 'FreedayDrawer'],
5777
+ ['.fdy-drawer', 'fdyDrawerReady', 'FreedayDrawer'],
5778
+ ['[data-fdy-validate]', 'fdyFormReady', 'FreedayForm'],
5779
+ ['[data-fdy-mask]', 'fdyMaskReady', 'FreedayMask'],
5780
+ ['[data-fdy-password]', 'fdyPwReady', 'FreedayMask'],
5781
+ ['[data-fdy-menu]', 'fdyMenuReady', 'FreedayMenu'],
5782
+ ['[data-fdy-number]', 'fdyNumberReady', 'FreedayNumber'],
5783
+ ['[data-fdy-rating]', 'fdyRatingReady', 'FreedayRating'],
5784
+ ['[data-fdy-search]', 'fdySearchReady', 'FreedaySearch'],
5785
+ ['[data-fdy-combo]', 'fdyComboReady', 'FreedayCombo'],
5786
+ ['[data-fdy-slider]', 'fdySliderReady', 'FreedaySlider'],
5787
+ ['[data-fdy-sortable]', 'fdySortableReady', 'FreedaySortable'],
5788
+ ['[data-fdy-stepper]', 'fdyStepperReady', 'FreedayStepper'],
5789
+ ['[data-fdy-table]', 'fdyTableReady', 'FreedayTable'],
5790
+ ['[data-fdy-tabs]', 'fdyTabsReady', 'FreedayTabs'],
5791
+ ['[data-fdy-timepicker]', 'fdyTpReady', 'FreedayTimepicker'],
5792
+ ['[data-fdy-tree]', 'fdyTreeReady', 'FreedayTree'],
5793
+ ['[data-fdy-dropzone]', 'fdyUploadReady', 'FreedayUpload']
5794
+ ];
5795
+
5796
+ var warned = typeof WeakSet === 'function' ? new WeakSet() : null;
5797
+
5798
+ function check(e) {
5799
+ if (!warned || !e.target || !e.target.closest) return;
5800
+ for (var i = 0; i < HOOKS.length; i++) {
5801
+ var hook = HOOKS[i];
5802
+ if (!window[hook[2]]) continue;
5803
+ var el = e.target.closest(hook[0]);
5804
+ if (!el || el.dataset[hook[1]] === '1' || warned.has(el) || el.closest('[data-fdy-ignore]')) continue;
5805
+ warned.add(el);
5806
+ console.warn('Freeday: this ' + hook[0] + ' was never enhanced, so it does nothing. Either it is '
5807
+ + 'outside every initAll() scope (Vue/React: inside the element given to useFreeday(); Blazor: '
5808
+ + 'FreedayBlazor.initAll after render; anything else: ' + hook[2] + '.initAll(el) once it is in '
5809
+ + 'the DOM), or its markup lacks a part the enhancer needs (see COMPONENTS.md). If the app '
5810
+ + 'drives this element itself on purpose, mark it or an ancestor data-fdy-ignore.', el);
5811
+ }
5812
+ }
5813
+
5814
+ ['click', 'focusin', 'submit'].forEach(function (type) {
5815
+ document.addEventListener(type, check, true);
5816
+ });
5817
+
5818
+ window.FreedayUnhydrated = { hooks: HOOKS };
5819
+ })();
5820
+
5468
5821
  /* Freeday, file-upload enhancer (optional, zero-dependency).
5469
5822
  * A drop target that also opens the file dialog on click / Enter / Space, validates each file
5470
5823
  * (accept + max size), and renders a per-file row with explicit state. Actual upload is the
@@ -5485,7 +5838,8 @@
5485
5838
  * Emits bubbling CustomEvents. BOTH on the dropzone element, which is the one target a consumer
5486
5839
  * needs to listen on:
5487
5840
  * "fdy-upload-add" {file, rejected, reason, row}
5488
- * "fdy-upload-remove" {file}
5841
+ * "fdy-upload-remove" {file}, cancelable: preventDefault() keeps the row (e.g. to confirm
5842
+ * removing a file the server already holds, then call row.el.remove())
5489
5843
  * `row` is the state machine over the rendered .fdy-file:
5490
5844
  * .ready() (rest, where a dropped file starts) / .uploading() / .setProgress(pct) /
5491
5845
  * .waiting(label) (sent, awaiting the server) / .done() / .fail(msg) / .el
@@ -5578,8 +5932,9 @@
5578
5932
  Firing on both would look safer, but it makes the pair asymmetric, one `add` and, for
5579
5933
  anyone delegating on a common ancestor, two `remove`s, which is its own silent bug. It
5580
5934
  also fixes the listless case: a row that was never attached bubbles to nothing at all. */
5581
- zone.dispatchEvent(new CustomEvent('fdy-upload-remove', { bubbles: true, detail: { file: file } }));
5582
- el.remove();
5935
+ var ev = new CustomEvent('fdy-upload-remove', { bubbles: true, cancelable: true, detail: { file: file } });
5936
+ zone.dispatchEvent(ev);
5937
+ if (!ev.defaultPrevented) el.remove();
5583
5938
  });
5584
5939
  el.appendChild(icon);
5585
5940
  el.appendChild(meta);
@@ -5693,6 +6048,7 @@
5693
6048
 
5694
6049
  function initDropzone(zone) {
5695
6050
  if (zone.dataset.fdyUploadReady === '1') return;
6051
+ if (zone.closest('[data-fdy-ignore]')) return;
5696
6052
  zone.dataset.fdyUploadReady = '1';
5697
6053
 
5698
6054
  var input = zone.querySelector('input[type="file"]');
@@ -118,6 +118,8 @@ Recent additions most likely to replace something an app hand-rolled (all detail
118
118
 
119
119
  | Reach for | Instead of |
120
120
  |---|---|
121
+ | `data-fdy-sortable` + `.fdy-sortable__handle` | SortableJS plus a hand-written arrow-key path, or a number box for an `order` column |
122
+ | `data-fdy-ignore` | narrowing `useFreeday()` to dodge kit markup the app drives itself, which leaves every form outside the scope dead |
121
123
  | `.fdy-btn--stretch` | a hand-rolled overlay to make a card or list row clickable |
122
124
  | `[data-fdy-number]` | a bare `<input type="number">` with the browser's own spin buttons |
123
125
  | `clearable` on `FdyCfl` / `FdyDatepicker` | a value that can be set but never unset |
@@ -229,4 +231,5 @@ Order of work that avoids a half-migrated mess:
229
231
  - Keyboard: Tab reaches every control, focus is always visible, Esc closes overlays.
230
232
  - Form errors carry `aria-invalid` + a linked message; icon-only buttons have `aria-label`.
231
233
  - The interactive components on the screen have their enhancer loaded, and SPA-rendered DOM is
232
- re-hydrated.
234
+ re-hydrated. Click each one once with devtools open: a `Freeday: this [data-fdy-…] was never
235
+ enhanced` warning means it sits outside every `initAll()` scope.
@@ -198,7 +198,7 @@ live docs also have a copy button per component.
198
198
  ```bash
199
199
  npm i @cahyo-dimas/freeday
200
200
  ```
201
- Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.12.1"` (public npm package). `dist/` is
201
+ Lands in `package.json` as `"@cahyo-dimas/freeday": "^3.14.0"` (public npm package). `dist/` is
202
202
  committed and published → no build step; `npm ci` runs without auth.
203
203
 
204
204
  ### 2. Import the CSS + enhancers **once** in your entry (`src/main.ts`)
@@ -40,8 +40,9 @@ Every integration comes down to one of these:
40
40
  | `freeday-mask` | `fdy-mask` `{value,raw}` | `FreedayMask` |
41
41
  | `freeday-form` | `fdy-form-invalid` `{invalid}` / `fdy-form-valid` | `FreedayForm` |
42
42
  | `freeday-table` | `fdy-table-change` · `fdy-row-select` | `FreedayTable` |
43
- | `freeday-chip` | `fdy-chip-change` `{value,pressed,selected}` · `fdy-chip-remove` `{value}` | `FreedayChip` |
44
- | `freeday-upload` | `fdy-upload-add` / `fdy-upload-remove` | `FreedayUpload` |
43
+ | `freeday-chip` | `fdy-chip-change` `{value,pressed,selected}` · `fdy-chip-remove` `{value}`, cancelable | `FreedayChip` |
44
+ | `freeday-upload` | `fdy-upload-add` / `fdy-upload-remove` (cancelable) | `FreedayUpload` |
45
+ | `freeday-sortable` | `fdy-sort-change` `{key,from,to,order}`, cancelable | `FreedaySortable` |
45
46
  | `freeday-stepper` | `fdy-step-change` | `FreedayStepper` |
46
47
  | `freeday-carousel` | `fdy-carousel-change` | `FreedayCarousel` |
47
48
  | `freeday-breakpoint` | `fdy-breakpoint-change` | `FreedayBreakpoint` |