@cahyo-dimas/freeday 3.13.0 → 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 (42) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/COMPONENTS.md +45 -0
  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-carousel.js +1 -0
  12. package/dist/freeday-cascade.js +1 -0
  13. package/dist/freeday-cfl.js +2 -0
  14. package/dist/freeday-chart.js +1 -0
  15. package/dist/freeday-chip.js +2 -0
  16. package/dist/freeday-datepicker.js +2 -0
  17. package/dist/freeday-datetime.js +1 -0
  18. package/dist/freeday-drawer.js +2 -0
  19. package/dist/freeday-form.js +1 -0
  20. package/dist/freeday-mask.js +2 -0
  21. package/dist/freeday-menu.js +1 -0
  22. package/dist/freeday-number.js +1 -0
  23. package/dist/freeday-rating.js +1 -0
  24. package/dist/freeday-search.js +1 -0
  25. package/dist/freeday-select.js +1 -0
  26. package/dist/freeday-slider.js +1 -0
  27. package/dist/freeday-sortable.js +222 -0
  28. package/dist/freeday-stepper.js +1 -0
  29. package/dist/freeday-table.js +1 -0
  30. package/dist/freeday-tabs.js +1 -0
  31. package/dist/freeday-timepicker.js +1 -0
  32. package/dist/freeday-tree.js +1 -0
  33. package/dist/freeday-unhydrated.js +9 -2
  34. package/dist/freeday-upload.js +1 -0
  35. package/dist/freeday.bundle.css +18 -0
  36. package/dist/freeday.css +18 -0
  37. package/dist/freeday.js +261 -2
  38. package/docs/agent-onboarding.md +2 -0
  39. package/docs/getting-started.md +1 -1
  40. package/docs/integrations.md +1 -0
  41. package/package.json +2 -2
  42. package/src/components/sortable.css +17 -0
package/CHANGELOG.md CHANGED
@@ -9,6 +9,69 @@ benar. Perubahan seperti itu ditulis di bawah `### Changed: BREAKING (types)`
9
9
  lama → tipe baru, dan cara menyempitkannya — bukan di bawah `### Added`, betapapun aditifnya dari
10
10
  sisi kit.
11
11
 
12
+ ## [3.14.0] - 2026-10-29
13
+
14
+ **Daftar terurut lewat seret dan papan ketik, dan satu atribut untuk berkata "ini bukan urusan
15
+ kit".** Dua butir dari `CATATAN-FREEDAY.md` app ronda (§14 dan §15). Mengaudit §14 menemukan satu
16
+ cacat yang tak dilaporkan: `FdyDrawer` Vue dan React bisa ditutup lewat backdrop walau
17
+ `dismissible=false`.
18
+
19
+ ### Added
20
+ - **`data-fdy-sortable`, enhancer ke-32** (`#103`), `src/freeday-sortable.js` plus
21
+ `.fdy-sortable__handle`. Baris diseret lewat pegangannya, atau dipindah satu langkah dengan ↑ / ↓
22
+ saat pegangannya terfokus. Lima layar di app ronda menyuruh orang mengetik nomor urut di kotak
23
+ angka, dan app itu akhirnya menulis sendiri SortableJS plus jalur papan ketiknya. Selama diseret,
24
+ garis menunjukkan tempat baris akan mendarat dan tak ada elemen yang berpindah sebelum tombol
25
+ dilepas; Escape membatalkan. `fdy-sort-change` (`{key, from, to, order}`) dapat dibatalkan,
26
+ dengan pola yang sama seperti chip di 3.13.0: halaman statis membiarkannya dan kit memindah
27
+ elemennya, sedangkan framework membatalkannya lalu mengurutkan state-nya sendiri, sebab DOM-nya
28
+ milik framework. Sesudah langkah papan ketik, fokus kembali ke pegangan baris yang sama, dicari
29
+ lewat `data-key` supaya tetap ketemu walau render ulang mengganti elemennya. Perpindahan
30
+ diumumkan di live region. `data-fdy-sort-fixed` menandai baris yang tak bisa dilewati, dan
31
+ `data-disabled` mematikan pengurutan selama daftar dicari atau disaring. Yang sengaja tidak
32
+ dibuat: gulir otomatis saat seretan mendekati tepi halaman, dan memindah baris antar-daftar.
33
+ - **`data-fdy-ignore`** (`#102`). Di elemen atau leluhur mana pun, setiap enhancer melewati
34
+ subtree itu, termasuk di bawah `initAll(document)` yang eksplisit, dan peringatan hook dari 3.13.0
35
+ ikut diam. Pesan peringatannya kini menyebut atribut ini sebagai jalan keluar yang sah. Pemicunya
36
+ matriks chip di layar Pola rotasi, yang perilakunya dikelola app sendiri, sehingga setiap klik di
37
+ situ mencetak peringatan untuk sesuatu yang memang disengaja. Nama yang diusulkan catatannya,
38
+ `data-fdy-raw`, tidak dipakai: di dokumen kit "raw" berarti jalur markup + enhancer, kebalikan
39
+ dari maksud atribut ini.
40
+
41
+ ### Fixed
42
+ - **`FdyDrawer` Vue dan React tertutup lewat backdrop walau `dismissible=false`** (`#102`), begitu
43
+ `useFreeday()` dipasang di seluruh halaman. Wrapper itu merender `.fdy-drawer`, yang merupakan hook
44
+ enhancer drawer, sehingga enhancer memasang handler backdrop kedua yang memanggil `close()`
45
+ langsung. Dialognya tertutup, `open` di state app tetap `true`, dan tak ada event `close`. Di luar
46
+ cakupan pemindai, wrapper yang sama memicu peringatan 3.13.0 setiap kali diklik. Keduanya kini
47
+ membawa `data-fdy-ignore`. `FdyChart` juga merender hook, tetapi sejak awal sudah mencap penanda
48
+ siapnya sendiri; ia kini dijaga uji supaya tetap begitu.
49
+
50
+ ### Catatan
51
+ - **Angka 9 menit di §14 bukan ongkos pemindaian.** Catatannya mengukur `useFreeday()` di seluruh
52
+ halaman pada 9 menit 6 detik untuk sepuluh uji e2e, dibanding 19,1 detik untuk cakupan modal saja,
53
+ dan mempersempit cakupannya karena itu. Pada bentuk matriks yang sama (38 baris × 4 shift, dua chip
54
+ per sel, 304 tombol), satu `rehydrate()` di seluruh halaman memakan 1,1 ms dan satu update reaktif
55
+ end-to-end 2,1 ms. Sekitar 55 detik per uji lebih mirip uji yang menunggu sampai timeout. Kalau
56
+ angka itu diambil sebelum 3.13.0, waktu itu kit masih menghapus chip yang diklik, dan itu cukup
57
+ menjelaskannya. Ini dugaan; pengukuran ulangnya ada di sisi app.
58
+
59
+ ### Gerbang
60
+ - `docs.test.mjs`: setiap fungsi init yang mencap penanda hook wajib memeriksa `data-fdy-ignore`
61
+ sebelum mencap, dan setiap wrapper Vue/React yang merender hook enhancer wajib menandainya
62
+ `data-fdy-ignore` atau mencap penanda siapnya sendiri. Snapshot string enhancer mendapat dua
63
+ string sortable.
64
+ - `browser/app-owned.mjs`: `useFreeday()` di seluruh halaman atas matriks itu, dengan ambang longgar
65
+ (25 ms per rehydrate, 50 ms per update) yang menangkap pemindaian yang meledak, bukan runner yang
66
+ lambat. Drawer `dismissible=false` bertahan dari klik backdrop, chart ter-render sekali, subtree
67
+ yang diabaikan tetap tak tersentuh, dan tak ada satu pun peringatan.
68
+ - `browser/sortable.mjs`, dengan mouse dan papan ketik asli: tanda pendaratan di tengah seretan,
69
+ Escape, klik yang bukan seretan, baris tetap, keadaan nonaktif, perubahan yang dibatalkan tak
70
+ menyentuh DOM, dan list Vue ber-key yang mengurutkan state-nya sendiri.
71
+ - `browser/harness.mjs` mendapat `dragFromTo` dan mengekspos `clickAt`.
72
+ - Dua belas mutasi diverifikasi merah. Satu mutasi yang tetap hijau membuang ambang jarak seret dari
73
+ enhancer: tekanan yang tak meninggalkan barisnya sendiri sudah ditolak `allowed()`.
74
+
12
75
  ## [3.13.0] - 2026-10-29
13
76
 
14
77
  **Kit berhenti memutuskan hal yang bukan urusannya, dan markup yang tak pernah dihidrasi kini
package/COMPONENTS.md CHANGED
@@ -52,6 +52,11 @@ live docs.
52
52
  never enhanced logs one `console.warn` naming it, with the element attached. The usual cause is
53
53
  a `useFreeday()` ref on a wrapper that does not contain it. `freeday-unhydrated.js` does this,
54
54
  and it ships in the root import.
55
+ **`data-fdy-ignore` says "not yours".** On an element or any ancestor, every enhancer skips that
56
+ subtree, even under an explicit `initAll(document)`, and the warning above stays quiet. Use it
57
+ where the app drives kit-styled markup itself, such as a chip matrix with its own removal flow.
58
+ The Vue and React `FdyDrawer` carry it already, so a page-wide `useFreeday()` cannot hand the
59
+ drawer enhancer a second backdrop handler.
55
60
 
56
61
  ### Containment — why the kit's containers are `position:relative`
57
62
 
@@ -101,6 +106,7 @@ element is fine. Events are bubbling
101
106
  | `data-fdy-tree` | `freeday-tree.js` | `FreedayTree` | — |
102
107
  | `data-fdy-rating` | `freeday-rating.js` | `FreedayRating` | — (native `change`) |
103
108
  | `data-fdy-slider` | `freeday-slider.js` | `FreedaySlider` | — (native `input`) |
109
+ | `data-fdy-sortable` | `freeday-sortable.js` | `FreedaySortable` | `fdy-sort-change` |
104
110
  | `data-fdy-number` | `freeday-number.js` | `FreedayNumber` | — (native `input`, `change`) |
105
111
  | `data-fdy-search` | `freeday-search.js` | `FreedaySearch` | `fdy-search` (+ native `input` on clear) |
106
112
  | `data-fdy-copy="value"` (delegated, no hydration) | `freeday-copy.js` | `FreedayCopy` | `fdy-copy` |
@@ -2367,6 +2373,45 @@ row.addEventListener('fdy-chip-remove', async (e) => {
2367
2373
  ```
2368
2374
  `--tone-1`…`--tone-8` for non-semantic category colours.
2369
2375
 
2376
+ ## Sortable list — `data-fdy-sortable`
2377
+ Reorder a list by dragging a handle, or by focusing the handle and pressing ↑ / ↓. Needs
2378
+ `freeday-sortable.js`. The kit draws the handle and the drag states; the list keeps whatever look it
2379
+ already had (`.fdy-list`, a table body, plain rows).
2380
+
2381
+ - Container: `.fdy-sortable` + `data-fdy-sortable` on a `<ul>`/`<ol>`, a `<tbody>` or a `<div>`.
2382
+ Its element children are the items.
2383
+ - Item: `data-key` (the id the event reports, **required** when a framework renders the list),
2384
+ `data-label` (the name read out, the item's text otherwise), and `data-fdy-sort-fixed` for an item
2385
+ that never moves and that nothing can pass, such as a system row pinned to one end.
2386
+ - Handle: `<button class="fdy-sortable__handle" type="button">` inside the item, icon inside. With no
2387
+ `aria-label` of your own it gets *Move {label}. Drag, or press the up and down arrow keys.*
2388
+ - `data-disabled` on the container switches reordering off, and the handles follow through
2389
+ `aria-disabled`. Set it while the list is searched or filtered, where a row's visible position is
2390
+ not its stored one.
2391
+ - States, set by the enhancer: `is-sorting` on the container, `is-dragging` on the item, and
2392
+ `is-drop-before` / `is-drop-after` on the item it will land against. Nothing moves until the
2393
+ pointer is released, and Escape cancels.
2394
+ - Event: `fdy-sort-change` on the container, `detail {key, from, to, order}`, **cancelable**. Left
2395
+ alone, the kit moves the element. Under Vue, React or Blazor, cancel it and reorder your state,
2396
+ because the DOM is the framework's. Either way the kit announces *{label} moved to position {n} of
2397
+ {total}.* in a polite live region, and after a keyboard step it puts focus back on the moved
2398
+ item's handle, found by `data-key` so a re-render that replaced the element still gets it.
2399
+ - Strings: `data-fdy-text-handle`, `data-fdy-text-moved` on the container.
2400
+ - Not in the kit: scrolling the page while a drag nears its edge, and moving items between lists.
2401
+ Keep a sortable list short enough to see whole, or paginate it.
2402
+
2403
+ ```vue
2404
+ <ul ref="list" class="fdy-list fdy-sortable" data-fdy-sortable
2405
+ :data-disabled="filtered ? '' : undefined"
2406
+ @fdy-sort-change.prevent="(e) => saveOrder(e.detail.order)">
2407
+ <li v-for="r in rows" :key="r.id" class="fdy-list__row" :data-key="r.id" :data-label="r.name">
2408
+ <button class="fdy-sortable__handle" type="button"><IconGrip class="fdy-icon" aria-hidden="true" /></button>
2409
+ {{ r.name }}
2410
+ </li>
2411
+ </ul>
2412
+ <!-- useFreeday(list) in <script setup> -->
2413
+ ```
2414
+
2370
2415
  ## Timeline — `.fdy-timeline`
2371
2416
  Vertical event feed, pure CSS. `<ol class="fdy-timeline">` of `.fdy-timeline__item` (`--muted`
2372
2417
  `--success` `--warning` `--danger`) with `__marker` `__time` `__title` `__text`. Status is carried
package/README.id.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **Lebih banyak _free day_ buat dev. UI kit-nya sudah siap pakai.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.13.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.13.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.0)
9
9
 
10
10
  UI KIT yang token-driven & framework-agnostic. Satu sumber kebenaran untuk warna, tipografi,
11
11
  spasi, dan komponen. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > **More free days for devs. The UI kit is ready to use.**
6
6
 
7
7
  [![Live docs](https://img.shields.io/badge/docs-live-2050d8?style=flat-square)](https://cahyo-dimas.github.io/freeday-ui-kit/)
8
- [![Release](https://img.shields.io/badge/release-v3.13.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.13.0)
8
+ [![Release](https://img.shields.io/badge/release-v3.14.0-0078d4?style=flat-square)](https://github.com/cahyo-dimas/freeday-ui-kit/tree/v3.14.0)
9
9
 
10
10
  A token-driven, framework-agnostic UI kit. One source of truth for color, typography,
11
11
  spacing, and components. Blueprint: `docs/superpowers/specs/2026-07-21-freeday-ui-kit-design.md`.
@@ -7,6 +7,9 @@ import { useEffect, useId, useRef } from 'react';
7
7
  // through app state, backdrop-click via `event.target === dialogEl`) applied to a drawer that anchors
8
8
  // left (default) or right. Native <dialog> supplies the focus trap, focus restore, top-layer stacking
9
9
  // and inert background; `dismissible` (default true) gates Esc + backdrop dismissal.
10
+ // `data-fdy-ignore` on the root: this component owns the behaviour, so a page-wide useFreeday() must
11
+ // not hand `.fdy-drawer` to the enhancer as well. Its backdrop handler calls close() past
12
+ // `dismissible` and leaves `open` true on a dialog that is already closed.
10
13
 
11
14
  export interface FdyDrawerProps {
12
15
  open: boolean;
@@ -37,6 +40,7 @@ export function FdyDrawer(props: FdyDrawerProps): JSX.Element {
37
40
  <dialog
38
41
  ref={dialogRef}
39
42
  className={drawerClass}
43
+ data-fdy-ignore=""
40
44
  aria-labelledby={titleId}
41
45
  onCancel={(e): void => {
42
46
  e.preventDefault();
@@ -23,6 +23,8 @@ export interface FdyMaskDetail { value: string; raw: string }
23
23
  export interface FdyFormInvalidDetail { invalid: HTMLElement[] }
24
24
  export interface FdyChipChangeDetail { value: string; pressed: boolean; selected: string[] }
25
25
  export interface FdyChipRemoveDetail { value: string }
26
+ /** `fdy-sort-change`, cancelable: `order` is every item's `data-key` in the new order. */
27
+ export interface FdySortChangeDetail { key: string; from: number; to: number; order: string[] }
26
28
  export interface FdyCflSelectDetail { row?: unknown; rows?: unknown[] }
27
29
  export interface FdyTableChangeDetail { page: number; sort: string | null; query: string }
28
30
 
@@ -7,6 +7,9 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
7
7
  // dialogEl`, applied to a drawer that anchors left (default) or right. Native <dialog> supplies the
8
8
  // focus trap, focus restore, top-layer stacking and inert background; `dismissible` (default true)
9
9
  // gates Esc + backdrop dismissal.
10
+ // `data-fdy-ignore` on the root: this component owns the behaviour, so a page-wide useFreeday() must
11
+ // not hand `.fdy-drawer` to the enhancer as well. Its backdrop handler calls close() past
12
+ // `dismissible` and leaves `open` true on a dialog that is already closed.
10
13
 
11
14
  // dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
12
15
  // `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
@@ -53,6 +56,7 @@ function onClick(e: MouseEvent): void {
53
56
  <dialog
54
57
  ref="dialogEl"
55
58
  :class="drawerClass"
59
+ data-fdy-ignore
56
60
  :aria-labelledby="titleId"
57
61
  @cancel.prevent="onCancel"
58
62
  @click="onClick"
@@ -93,5 +93,7 @@ export interface FdyMaskDetail { value: string; raw: string }
93
93
  export interface FdyFormInvalidDetail { invalid: HTMLElement[] }
94
94
  export interface FdyChipChangeDetail { value: string; pressed: boolean; selected: string[] }
95
95
  export interface FdyChipRemoveDetail { value: string }
96
+ /** `fdy-sort-change`, cancelable: `order` is every item's `data-key` in the new order. */
97
+ export interface FdySortChangeDetail { key: string; from: number; to: number; order: string[] }
96
98
  export interface FdyCflSelectDetail { row?: unknown; rows?: unknown[] }
97
99
  export interface FdyTableChangeDetail { page: number; sort: string | null; query: string }
@@ -46,6 +46,7 @@
46
46
 
47
47
  function initShell(app) {
48
48
  if (app.dataset.fdyAppShellReady === '1') return;
49
+ if (app.closest('[data-fdy-ignore]')) return;
49
50
  app.dataset.fdyAppShellReady = '1';
50
51
 
51
52
  var toggle = app.querySelector('.fdy-app__navtoggle');
@@ -18,6 +18,7 @@
18
18
 
19
19
  function initAutocomplete(root) {
20
20
  if (root.dataset.fdyAcReady === '1') return;
21
+ if (root.closest('[data-fdy-ignore]')) return;
21
22
  root.dataset.fdyAcReady = '1';
22
23
 
23
24
  var input = root.querySelector('input[role="combobox"]') || root.querySelector('input');
@@ -46,6 +46,7 @@
46
46
 
47
47
  function initCarousel(root) {
48
48
  if (root.dataset.fdyCarouselReady === '1') return;
49
+ if (root.closest('[data-fdy-ignore]')) return;
49
50
  root.dataset.fdyCarouselReady = '1';
50
51
 
51
52
  var viewport = root.querySelector('.fdy-carousel__viewport');
@@ -84,6 +84,7 @@
84
84
 
85
85
  function initCascade(wrap) {
86
86
  if (wrap.dataset.fdyCascadeReady === '1') return;
87
+ if (wrap.closest('[data-fdy-ignore]')) return;
87
88
  wrap.dataset.fdyCascadeReady = '1';
88
89
  wrap.classList.add('fdy-cascade');
89
90
 
@@ -207,6 +207,7 @@
207
207
 
208
208
  function bindGroup(group) {
209
209
  if (group.dataset.fdyCflBound === '1') return;
210
+ if (group.closest('[data-fdy-ignore]')) return;
210
211
  var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
211
212
  if (!dialog) return;
212
213
  initDialog(dialog);
@@ -218,6 +219,7 @@
218
219
 
219
220
  function bindOpener(opener) {
220
221
  if (opener.dataset.fdyCflBound === '1') return;
222
+ if (opener.closest('[data-fdy-ignore]')) return;
221
223
  var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
222
224
  if (!dialog) return;
223
225
  initDialog(dialog);
@@ -489,6 +489,7 @@
489
489
  // Auto-init renders once; explicit update() re-renders after data changes.
490
490
  function initChart(el) {
491
491
  if (el.dataset.fdyChartReady === '1') return;
492
+ if (el.closest('[data-fdy-ignore]')) return;
492
493
  el.dataset.fdyChartReady = '1';
493
494
  renderChart(el);
494
495
  }
@@ -18,6 +18,7 @@
18
18
 
19
19
  function initRemove(btn) {
20
20
  if (btn.dataset.fdyChipRmReady === '1') return;
21
+ if (btn.closest('[data-fdy-ignore]')) return;
21
22
  btn.dataset.fdyChipRmReady = '1';
22
23
  btn.addEventListener('click', function () {
23
24
  var chip = btn.closest('.fdy-chip');
@@ -41,6 +42,7 @@
41
42
 
42
43
  function initGroup(group) {
43
44
  if (group.dataset.fdyChipsReady === '1') return;
45
+ if (group.closest('[data-fdy-ignore]')) return;
44
46
  group.dataset.fdyChipsReady = '1';
45
47
  var single = group.hasAttribute('data-single');
46
48
  if (!group.getAttribute('role')) group.setAttribute('role', 'group');
@@ -118,6 +118,7 @@
118
118
 
119
119
  function initPicker(wrap) {
120
120
  if (wrap.dataset.fdyDpReady === '1') return wrap._fdyDp || null;
121
+ if (wrap.closest('[data-fdy-ignore]')) return null;
121
122
  wrap.dataset.fdyDpReady = '1';
122
123
  wrap.classList.add('fdy-datepicker');
123
124
 
@@ -647,6 +648,7 @@
647
648
 
648
649
  function initRange(root) {
649
650
  if (root.dataset.fdyDrReady === '1') return;
651
+ if (root.closest('[data-fdy-ignore]')) return;
650
652
  root.dataset.fdyDrReady = '1';
651
653
  var fromEl = root.querySelector('[data-fdy-datepicker][data-role="from"]');
652
654
  var toEl = root.querySelector('[data-fdy-datepicker][data-role="to"]');
@@ -17,6 +17,7 @@
17
17
 
18
18
  function initDatetime(wrap) {
19
19
  if (wrap.dataset.fdyDtReady === '1') return;
20
+ if (wrap.closest('[data-fdy-ignore]')) return;
20
21
  wrap.dataset.fdyDtReady = '1';
21
22
  wrap.classList.add('fdy-datetimepicker');
22
23
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  function initDrawer(dialog) {
10
10
  if (dialog.dataset.fdyDrawerReady === '1') return;
11
+ if (dialog.closest('[data-fdy-ignore]')) return;
11
12
  dialog.dataset.fdyDrawerReady = '1';
12
13
  // Click on the backdrop (target is the dialog box itself) closes it.
13
14
  dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
@@ -18,6 +19,7 @@
18
19
 
19
20
  function bindTrigger(btn) {
20
21
  if (btn.dataset.fdyDrawerBound === '1') return;
22
+ if (btn.closest('[data-fdy-ignore]')) return;
21
23
  var dialog = document.getElementById(btn.getAttribute('data-fdy-drawer'));
22
24
  if (!dialog) return;
23
25
  initDrawer(dialog);
@@ -186,6 +186,7 @@
186
186
 
187
187
  function initForm(form) {
188
188
  if (form.dataset.fdyFormReady === '1') return;
189
+ if (form.closest('[data-fdy-ignore]')) return;
189
190
  form.dataset.fdyFormReady = '1';
190
191
  form.setAttribute('novalidate', '');
191
192
  var eager = form.hasAttribute('data-fdy-eager');
@@ -79,6 +79,7 @@
79
79
 
80
80
  function initMask(el) {
81
81
  if (el.dataset.fdyMaskReady === '1') return;
82
+ if (el.closest('[data-fdy-ignore]')) return;
82
83
  el.dataset.fdyMaskReady = '1';
83
84
  var pattern = el.getAttribute('data-fdy-mask') || '';
84
85
  if (!pattern) return;
@@ -101,6 +102,7 @@
101
102
 
102
103
  function initReveal(input) {
103
104
  if (input.dataset.fdyPwReady === '1') return;
105
+ if (input.closest('[data-fdy-ignore]')) return;
104
106
  input.dataset.fdyPwReady = '1';
105
107
 
106
108
  var group = document.createElement('span');
@@ -19,6 +19,7 @@
19
19
 
20
20
  function initMenu(wrap) {
21
21
  if (wrap.dataset.fdyMenuReady === '1') return;
22
+ if (wrap.closest('[data-fdy-ignore]')) return;
22
23
  wrap.dataset.fdyMenuReady = '1';
23
24
  var trigger = wrap.querySelector('[aria-haspopup="menu"]');
24
25
  var menu = wrap.querySelector('.fdy-menu, [role="menu"]');
@@ -27,6 +27,7 @@
27
27
 
28
28
  function initNumber(root) {
29
29
  if (root.dataset.fdyNumberReady === '1') return;
30
+ if (root.closest('[data-fdy-ignore]')) return;
30
31
 
31
32
  var input = root.querySelector('input[type="number"]');
32
33
  var buttons = Array.prototype.slice.call(root.querySelectorAll('[data-fdy-number-step]'));
@@ -8,6 +8,7 @@
8
8
 
9
9
  function initRating(root) {
10
10
  if (root.dataset.fdyRatingReady === '1') return;
11
+ if (root.closest('[data-fdy-ignore]')) return;
11
12
  root.dataset.fdyRatingReady = '1';
12
13
 
13
14
  var stars = Array.prototype.slice.call(root.querySelectorAll('.fdy-rating__star'));
@@ -24,6 +24,7 @@
24
24
 
25
25
  function initSearch(root) {
26
26
  if (root.dataset.fdySearchReady === '1') return;
27
+ if (root.closest('[data-fdy-ignore]')) return;
27
28
  var input = root.querySelector('input');
28
29
  if (!input) return;
29
30
  root.dataset.fdySearchReady = '1';
@@ -32,6 +32,7 @@
32
32
 
33
33
  function initCombo(root) {
34
34
  if (root.dataset.fdyComboReady === '1') return; // idempotent
35
+ if (root.closest('[data-fdy-ignore]')) return;
35
36
  var button = root.querySelector('.fdy-combo__button');
36
37
  var listbox = root.querySelector('.fdy-combo__listbox');
37
38
  var valueEl = root.querySelector('.fdy-combo__value');
@@ -12,6 +12,7 @@
12
12
 
13
13
  function initSlider(input) {
14
14
  if (input.dataset.fdySliderReady === '1') return;
15
+ if (input.closest('[data-fdy-ignore]')) return;
15
16
  input.dataset.fdySliderReady = '1';
16
17
  var out = input.id ? document.querySelector('output[for="' + input.id + '"]') : null;
17
18
  if (!out) return;
@@ -0,0 +1,222 @@
1
+ /* Freeday, sortable list enhancer (optional, zero-dependency).
2
+ * Reorder the children of a list by dragging a handle, or by focusing it and pressing ↑ / ↓. The
3
+ * keyboard path is not an extra: drag-only reordering locks out every keyboard and screen-reader
4
+ * user from a settings screen, and that path is the part each app used to write for itself
5
+ * (CATATAN-FREEDAY §15).
6
+ *
7
+ * Markup contract:
8
+ * <ul class="fdy-sortable" data-fdy-sortable> (or a <tbody>, or a <div>)
9
+ * <li data-key="ketua" data-label="Ketua">
10
+ * <button class="fdy-sortable__handle" type="button">…icon…</button> Ketua
11
+ * </li>
12
+ * <li data-key="sistem" data-fdy-sort-fixed>…</li> (never moves, and nothing passes it)
13
+ * </ul>
14
+ * The items are the container's element children. `data-key` names each in the event; `data-label`
15
+ * is what the handle's default aria-label and the announcement call it (the item text otherwise).
16
+ * `data-disabled` on the container switches reordering off, e.g. while the list is filtered, and
17
+ * the handles follow it through aria-disabled.
18
+ *
19
+ * Emits a bubbling, CANCELABLE "fdy-sort-change" on the container, detail {key, from, to, order},
20
+ * `order` being every item's key in the new order. Not cancelled, the kit moves the element, which
21
+ * is the whole job on a static page. A framework that renders the list from state cancels it and
22
+ * reorders the state instead, because the DOM is the framework's and an element moved behind its
23
+ * back leaves the two disagreeing about where each row is. Either way the kit announces the move
24
+ * and, after a keyboard step, puts focus back on the moved item's handle (looked up by data-key,
25
+ * since a framework re-render may have replaced the element).
26
+ *
27
+ * The drag shows where the item will land (is-drop-before / is-drop-after on the item it lands
28
+ * against) and moves nothing until the pointer is released; Escape cancels it. Exposes
29
+ * window.FreedaySortable.
30
+ */
31
+ (function () {
32
+ 'use strict';
33
+
34
+ var TEXT = {
35
+ handle: 'Move {label}. Drag, or press the up and down arrow keys.',
36
+ moved: '{label} moved to position {position} of {total}.'
37
+ };
38
+ /* Kebab-cased for the lookup, see the note in freeday-upload.js. */
39
+ function textAttr(root, key) {
40
+ if (!root || !root.getAttribute) return null;
41
+ var kebab = root.getAttribute('data-fdy-text-' + key.replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); }));
42
+ return kebab != null && kebab !== '' ? kebab : root.getAttribute('data-fdy-text-' + key);
43
+ }
44
+ function textOf(root, key, vars) {
45
+ var custom = textAttr(root, key);
46
+ var s = custom != null && custom !== '' ? custom : TEXT[key];
47
+ for (var k in vars) if (Object.prototype.hasOwnProperty.call(vars, k)) s = s.split('{' + k + '}').join(vars[k]);
48
+ return s;
49
+ }
50
+
51
+ var HANDLE = '.fdy-sortable__handle';
52
+
53
+ var live = null;
54
+ function announce(message) {
55
+ if (live === null) {
56
+ live = document.createElement('div');
57
+ live.className = 'fdy-visually-hidden';
58
+ live.setAttribute('aria-live', 'polite');
59
+ document.body.appendChild(live);
60
+ }
61
+ /* The same sentence twice in a row is no change, and a live region announces changes. */
62
+ live.textContent = live.textContent === message ? message + ' ' : message;
63
+ }
64
+
65
+ function items(root) {
66
+ return Array.prototype.slice.call(root.children);
67
+ }
68
+ function keyOf(item, index) {
69
+ var key = item.getAttribute('data-key');
70
+ return key !== null ? key : String(index);
71
+ }
72
+ function labelOf(item) {
73
+ var label = item.getAttribute('data-label');
74
+ return label !== null ? label : (item.textContent || '').replace(/\s+/g, ' ').trim();
75
+ }
76
+ function disabled(root) {
77
+ return root.hasAttribute('data-disabled');
78
+ }
79
+ function itemOf(root, node) {
80
+ for (var n = node; n && n !== root; n = n.parentElement) {
81
+ if (n.parentElement === root) return n;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /* A move is allowed only if it shifts no fixed item, which is the same as saying the item does
87
+ not start fixed and does not cross one on its way. */
88
+ function allowed(list, from, to) {
89
+ if (from === to || to < 0 || to >= list.length) return false;
90
+ var lo = Math.min(from, to), hi = Math.max(from, to);
91
+ for (var i = lo; i <= hi; i++) if (list[i].hasAttribute('data-fdy-sort-fixed')) return false;
92
+ return true;
93
+ }
94
+
95
+ function syncHandles(root) {
96
+ var off = disabled(root) ? 'true' : 'false';
97
+ items(root).forEach(function (item) {
98
+ var handle = item.querySelector(HANDLE);
99
+ if (!handle) return;
100
+ if (handle.getAttribute('aria-disabled') !== off) handle.setAttribute('aria-disabled', off);
101
+ if (!handle.hasAttribute('aria-label')) handle.setAttribute('aria-label', textOf(root, 'handle', { label: labelOf(item) }));
102
+ });
103
+ }
104
+
105
+ function commit(root, from, to, refocus) {
106
+ var list = items(root);
107
+ var item = list[from];
108
+ var key = keyOf(item, from);
109
+ var keys = list.map(keyOf);
110
+ keys.splice(to, 0, keys.splice(from, 1)[0]);
111
+ var ev = new CustomEvent('fdy-sort-change', {
112
+ bubbles: true,
113
+ cancelable: true,
114
+ detail: { key: key, from: from, to: to, order: keys }
115
+ });
116
+ root.dispatchEvent(ev);
117
+ if (!ev.defaultPrevented) root.insertBefore(item, to > from ? list[to].nextSibling : list[to]);
118
+ announce(textOf(root, 'moved', { label: labelOf(item), position: String(to + 1), total: String(list.length) }));
119
+ if (!refocus) return;
120
+ /* After the app's own render, which a framework runs after this handler returns. Moving a focused
121
+ element drops its focus, and a keyed re-render may have replaced the element outright. */
122
+ setTimeout(function () {
123
+ var now = items(root);
124
+ var moved = item.parentElement === root ? item : now.filter(function (el, i) { return keyOf(el, i) === key; })[0];
125
+ var handle = moved ? moved.querySelector(HANDLE) : null;
126
+ if (handle && document.activeElement !== handle) handle.focus();
127
+ }, 0);
128
+ }
129
+
130
+ function initSortable(root) {
131
+ if (root.dataset.fdySortableReady === '1') return;
132
+ if (root.closest('[data-fdy-ignore]')) return;
133
+ root.dataset.fdySortableReady = '1';
134
+ syncHandles(root);
135
+ /* Rows a framework adds later get their aria-label, and data-disabled reaches every handle. */
136
+ new MutationObserver(function () { syncHandles(root); })
137
+ .observe(root, { childList: true, attributes: true, attributeFilter: ['data-disabled'] });
138
+
139
+ root.addEventListener('keydown', function (e) {
140
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
141
+ if (!handle || !root.contains(handle) || disabled(root)) return;
142
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
143
+ e.preventDefault();
144
+ var list = items(root);
145
+ var from = list.indexOf(itemOf(root, handle));
146
+ var to = from + (e.key === 'ArrowUp' ? -1 : 1);
147
+ if (from !== -1 && allowed(list, from, to)) commit(root, from, to, true);
148
+ });
149
+
150
+ var drag = null;
151
+
152
+ function clearMarks() {
153
+ items(root).forEach(function (el) { el.classList.remove('is-drop-before', 'is-drop-after'); });
154
+ }
155
+ function end(apply) {
156
+ if (drag === null) return;
157
+ var d = drag;
158
+ drag = null;
159
+ document.removeEventListener('keydown', onEscape, true);
160
+ clearMarks();
161
+ d.item.classList.remove('is-dragging');
162
+ root.classList.remove('is-sorting');
163
+ if (apply && d.started && d.to !== null) commit(root, d.from, d.to, false);
164
+ }
165
+ function onEscape(e) {
166
+ if (e.key === 'Escape') { e.preventDefault(); end(false); }
167
+ }
168
+
169
+ root.addEventListener('pointerdown', function (e) {
170
+ var handle = e.target.closest ? e.target.closest(HANDLE) : null;
171
+ if (!handle || !root.contains(handle) || disabled(root) || e.button !== 0) return;
172
+ var item = itemOf(root, handle);
173
+ if (!item) return;
174
+ e.preventDefault();
175
+ handle.focus();
176
+ handle.setPointerCapture(e.pointerId);
177
+ drag = { item: item, from: items(root).indexOf(item), started: false, to: null };
178
+ document.addEventListener('keydown', onEscape, true);
179
+ });
180
+
181
+ root.addEventListener('pointermove', function (e) {
182
+ if (drag === null) return;
183
+ /* No travel threshold: a press that never leaves its own row resolves to to === from, which
184
+ allowed() refuses, so a click on the handle cannot reorder anything. */
185
+ if (!drag.started) {
186
+ drag.started = true;
187
+ drag.item.classList.add('is-dragging');
188
+ root.classList.add('is-sorting');
189
+ }
190
+ clearMarks();
191
+ drag.to = null;
192
+ var list = items(root);
193
+ var over = itemOf(root, document.elementFromPoint(e.clientX, e.clientY));
194
+ if (!over) return;
195
+ var rect = over.getBoundingClientRect();
196
+ var after = e.clientY > rect.top + rect.height / 2;
197
+ var slot = list.indexOf(over) + (after ? 1 : 0);
198
+ var to = slot > drag.from ? slot - 1 : slot;
199
+ if (!allowed(list, drag.from, to)) return;
200
+ drag.to = to;
201
+ over.classList.add(after ? 'is-drop-after' : 'is-drop-before');
202
+ });
203
+
204
+ root.addEventListener('pointerup', function () { end(true); });
205
+ root.addEventListener('pointercancel', function () { end(false); });
206
+ }
207
+
208
+ function initAll(context) {
209
+ var root = context || document;
210
+ /* root included: querySelectorAll never matches its own root, and a framework ref often sits ON the widget. */
211
+ if (root.matches && root.matches('[data-fdy-sortable]')) initSortable(root);
212
+ Array.prototype.forEach.call(root.querySelectorAll('[data-fdy-sortable]'), initSortable);
213
+ }
214
+
215
+ if (document.readyState === 'loading') {
216
+ document.addEventListener('DOMContentLoaded', function () { initAll(); });
217
+ } else {
218
+ initAll();
219
+ }
220
+
221
+ window.FreedaySortable = { init: initSortable, initAll: initAll };
222
+ })();
@@ -49,6 +49,7 @@
49
49
 
50
50
  function initStepper(root) {
51
51
  if (root.dataset.fdyStepperReady === '1') return;
52
+ if (root.closest('[data-fdy-ignore]')) return;
52
53
  root.dataset.fdyStepperReady = '1';
53
54
 
54
55
  var steps = Array.prototype.slice.call(root.querySelectorAll('.fdy-step'));