stimeo-ui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/carousel_controller.d.ts +100 -0
- package/dist/controllers/carousel_controller.js +272 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +76 -0
- package/dist/controllers/color_picker_controller.js +213 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +71 -0
- package/dist/controllers/data_grid_controller.js +168 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +87 -0
- package/dist/controllers/date_range_picker_controller.js +417 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +168 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +41 -18
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +33 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menubar_controller.d.ts +62 -0
- package/dist/controllers/menubar_controller.js +433 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/multi_select_controller.d.ts +115 -0
- package/dist/controllers/multi_select_controller.js +472 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +99 -0
- package/dist/controllers/navigation_menu_controller.js +384 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
- package/dist/controllers/overflow_indicator_controller.js +178 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/range_slider_controller.d.ts +87 -0
- package/dist/controllers/range_slider_controller.js +166 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +194 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.js +15 -2
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.js +8 -4
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +33 -11
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -0
- package/dist/controllers/separator_controller.js +87 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.js +8 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +88 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +3 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +275 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.js +20 -10
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +212 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +1 -9
- package/dist/controllers/toast_controller.js +36 -9
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +54 -0
- package/dist/controllers/tree_view_controller.js +275 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +811 -295
- package/dist/index.js.map +1 -1
- package/dist/inspector/examples.json +2 -2
- package/dist/inspector/manifest.json +15 -3
- package/package.json +1 -1
|
@@ -18,6 +18,11 @@ var IntersectionWatcher = class {
|
|
|
18
18
|
* (Re)creates the observer and observes `targets`. Returns `false` — leaving
|
|
19
19
|
* the watcher inert — without `IntersectionObserver` support (very old
|
|
20
20
|
* browsers; the caller's no-JS fallback stays in charge) or with no targets.
|
|
21
|
+
*
|
|
22
|
+
* @throws Whatever the platform throws for an invalid `rootMargin`/`threshold`
|
|
23
|
+
* or a failing `observe()`. The exception is passed through unchanged, but
|
|
24
|
+
* the watcher rolls back first: every target observed so far is released and
|
|
25
|
+
* `active` stays `false`, so a caller that retries starts from a clean slate.
|
|
21
26
|
*/
|
|
22
27
|
start(targets, options = {}) {
|
|
23
28
|
this.stop();
|
|
@@ -25,25 +30,42 @@ var IntersectionWatcher = class {
|
|
|
25
30
|
const list = Array.isArray(targets) ? targets : [targets];
|
|
26
31
|
if (list.length === 0) return false;
|
|
27
32
|
const root = "root" in options ? options.root ?? null : options.rootSelector ? document.querySelector(options.rootSelector) : null;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let observer = null;
|
|
34
|
+
try {
|
|
35
|
+
observer = new IntersectionObserver(
|
|
36
|
+
(entries) => {
|
|
37
|
+
if (this.#active && this.#observer === observer) this.#onEntries(entries);
|
|
38
|
+
},
|
|
39
|
+
{ root, rootMargin: options.rootMargin, threshold: options.threshold }
|
|
40
|
+
);
|
|
41
|
+
for (const target of list) observer.observe(target);
|
|
42
|
+
this.#observer = observer;
|
|
43
|
+
this.#active = true;
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
observer?.disconnect();
|
|
47
|
+
this.#observer = null;
|
|
48
|
+
this.#active = false;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
37
51
|
}
|
|
38
52
|
/**
|
|
39
53
|
* Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
|
|
40
54
|
* only reports *changes*, but `observe()` always reports the present state,
|
|
41
55
|
* so unobserve→observe turns "still intersecting" into a fresh callback.
|
|
56
|
+
*
|
|
57
|
+
* @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
|
|
58
|
+
* first, so it never stays live with a half-rearmed target.
|
|
42
59
|
*/
|
|
43
60
|
rearm(target) {
|
|
44
61
|
if (!this.#observer) return;
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
try {
|
|
63
|
+
this.#observer.unobserve(target);
|
|
64
|
+
this.#observer.observe(target);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.stop();
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
47
69
|
}
|
|
48
70
|
/** Severs the observer; late queued callbacks become no-ops via the guard. */
|
|
49
71
|
stop() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/intersection_watcher.ts","../../src/controllers/scrollspy_controller.ts"],"names":[],"mappings":";;;;;AA4BO,IAAM,sBAAN,MAA0B;AAAA,EACtB,UAAA;AAAA,EACT,SAAA,GAAyC,IAAA;AAAA,EACzC,OAAA,GAAU,KAAA;AAAA,EAEV,YAAY,SAAA,EAA2D;AACrE,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAA,CAAM,OAAA,EAAuC,OAAA,GAAoC,EAAC,EAAY;AAC5F,IAAA,IAAA,CAAK,IAAA,EAAK;AACV,IAAA,IAAI,OAAO,oBAAA,KAAyB,WAAA,EAAa,OAAO,KAAA;AACxD,IAAA,MAAM,OAAO,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,GAAK,OAAA,GAAiC,CAAC,OAAkB,CAAA;AAC3F,IAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,KAAA;AAE9B,IAAA,MAAM,IAAA,GACJ,MAAA,IAAU,OAAA,GACL,OAAA,CAAQ,IAAA,IAAQ,IAAA,GACjB,OAAA,CAAQ,YAAA,GACN,QAAA,CAAS,aAAA,CAAc,OAAA,CAAQ,YAAY,CAAA,GAC3C,IAAA;AAER,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,IAAA,CAAK,YAAY,IAAI,oBAAA;AAAA,MACnB,CAAC,OAAA,KAAY;AACX,QAAA,IAAI,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA;AAAA,MAC3C,CAAA;AAAA,MACA,EAAE,IAAA,EAAM,UAAA,EAAY,QAAQ,UAAA,EAAY,SAAA,EAAW,QAAQ,SAAA;AAAU,KACvE;AACA,IAAA,KAAA,MAAW,MAAA,IAAU,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,QAAQ,MAAM,CAAA;AACxD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAA,EAAuB;AAC3B,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACrB,IAAA,IAAA,CAAK,SAAA,CAAU,UAAU,MAAM,CAAA;AAC/B,IAAA,IAAA,CAAK,SAAA,CAAU,QAAQ,MAAM,CAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AACF,CAAA;;;AC7DO,IAAM,mBAAA,GAAN,cAAkC,UAAA,CAAwB;AAAA,EAC/D,OAAgB,OAAA,GAAU,CAAC,MAAM,CAAA;AAAA,EACjC,OAAgB,MAAA,GAAS;AAAA,IACvB,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACnC,UAAA,EAAY,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACxC,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA;AAAG,GAC5C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,UAAU,CAAA;AAAA,EAC5B,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAShB,QAAA,GAAW,IAAI,mBAAA,CAAoB,CAAC,YAAY,IAAA,CAAK,eAAA,CAAgB,OAAO,CAAC,CAAA;AAAA,EACtF,YAAA,GAAe,KAAA;AAAA;AAAA,EAGf,mBAAA,uBAA0B,GAAA,EAAsD;AAAA;AAAA,EAGhF,gBAAA,GAAmB,EAAA;AAAA,EAEV,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AACpB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AACpB,IAAA,IAAA,CAAK,SAAS,IAAA,EAAK;AACnB,IAAA,IAAA,CAAK,oBAAoB,KAAA,EAAM;AAC/B,IAAA,IAAA,CAAK,gBAAA,GAAmB,EAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAA,GAA2B;AACzB,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA,EAEA,sBAAA,GAA+B;AAC7B,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA,EAEA,wBAAA,GAAiC;AAC/B,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,KAAA,EAAoB;AAC3B,IAAA,MAAM,OAAO,KAAA,CAAM,aAAA;AACnB,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,YAAA,CAAa,IAAI,CAAA;AACjC,IAAA,IAAI,CAAC,EAAA,EAAI;AAET,IAAA,KAAA,CAAM,cAAA,EAAe;AAErB,IAAA,MAAM,aAAA,GAAgB,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA;AAChD,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,WAAA,GAAc,KAAK,eAAA,EAAgB;AACzC,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,MAAM,aAAA,GAAgB,YAAY,qBAAA,EAAsB;AACxD,MAAA,MAAM,UAAA,GAAa,cAAc,qBAAA,EAAsB;AACvD,MAAA,MAAM,iBACJ,WAAA,CAAY,SAAA,IAAa,WAAW,GAAA,GAAM,aAAA,CAAc,OAAO,IAAA,CAAK,WAAA;AAEtE,MAAA,WAAA,CAAY,QAAA,CAAS;AAAA,QACnB,GAAA,EAAK,cAAA;AAAA,QACL,QAAA,EAAU;AAAA,OACX,CAAA;AAAA,IACH,CAAA,MAAO;AACL,MAAA,MAAM,UAAA,GAAa,cAAc,qBAAA,EAAsB;AACvD,MAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,OAAA,GAAU,UAAA,CAAW,MAAM,IAAA,CAAK,WAAA;AAE9D,MAAA,MAAA,CAAO,QAAA,CAAS;AAAA,QACd,GAAA,EAAK,cAAA;AAAA,QACL,QAAA,EAAU;AAAA,OACX,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,eAAA,GAAsC;AACpC,IAAA,IAAI,CAAC,IAAA,CAAK,iBAAA,EAAmB,OAAO,IAAA;AACpC,IAAA,OAAO,QAAA,CAAS,aAAA,CAAc,IAAA,CAAK,iBAAiB,CAAA;AAAA,EACtD;AAAA,EAEA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,SAAS,IAAA,EAAK;AACnB,IAAA,IAAA,CAAK,oBAAoB,KAAA,EAAM;AAC/B,IAAA,IAAA,CAAK,gBAAA,GAAmB,EAAA;AAExB,IAAA,IAAI,IAAA,CAAK,WAAA,CAAY,MAAA,KAAW,CAAA,EAAG;AAGnC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,eAAA,IAAmB,CAAA,CAAA,EAAI,KAAK,WAAW,CAAA,eAAA,CAAA;AAG3D,IAAA,MAAM,WAAsB,EAAC;AAC7B,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,WAAA,EAAa;AACnC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,YAAA,CAAa,IAAI,CAAA;AACjC,MAAA,IAAI,CAAC,EAAA,EAAI;AACT,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA;AAC1C,MAAA,IAAI,OAAA,EAAS,QAAA,CAAS,IAAA,CAAK,OAAO,CAAA;AAAA,IACpC;AAEA,IAAA,IAAA,CAAK,QAAA,CAAS,MAAM,QAAA,EAAU;AAAA,MAC5B,IAAA,EAAM,KAAK,eAAA,EAAgB;AAAA,MAC3B,UAAA,EAAY,MAAA;AAAA,MACZ,WAAW,CAAC,CAAA,EAAG,KAAK,GAAA,EAAK,GAAA,EAAK,KAAK,CAAC;AAAA;AAAA,KACrC,CAAA;AAAA,EACH;AAAA,EAES,eAAA,GAAkB,CAAC,OAAA,KAA+C;AAIzE,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AAExB,IAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,MAAA,MAAM,EAAA,GAAK,MAAM,MAAA,CAAO,EAAA;AACxB,MAAA,IAAI,CAAC,EAAA,EAAI;AAET,MAAA,IAAA,CAAK,mBAAA,CAAoB,IAAI,EAAA,EAAI;AAAA,QAC/B,gBAAgB,KAAA,CAAM,cAAA;AAAA,QACtB,GAAA,EAAK,MAAM,kBAAA,CAAmB;AAAA,OAC/B,CAAA;AAAA,IACH;AAEA,IAAA,IAAA,CAAK,sBAAA,EAAuB;AAAA,EAC9B,CAAA;AAAA,EAEA,sBAAA,GAA+B;AAM7B,IAAA,MAAM,MAAA,GAAS,KAAK,eAAA,EAAgB;AACpC,IAAA,MAAM,eAAe,MAAA,GAAS,MAAA,CAAO,uBAAsB,CAAE,GAAA,GAAM,KAAK,IAAA,CAAK,WAAA;AAE7E,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,aAAa,MAAA,CAAO,SAAA;AAGxB,IAAA,KAAA,MAAW,CAAC,EAAA,EAAI,KAAK,KAAK,IAAA,CAAK,mBAAA,CAAoB,SAAQ,EAAG;AAC5D,MAAA,IAAI,MAAM,cAAA,EAAgB;AAExB,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,MAAM,WAAW,CAAA;AACjD,QAAA,IAAI,WAAW,UAAA,EAAY;AACzB,UAAA,UAAA,GAAa,QAAA;AACb,UAAA,MAAA,GAAS,EAAA;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAKA,IAAA,IAAI,CAAC,MAAA,IAAU,IAAA,CAAK,mBAAA,CAAoB,OAAO,CAAA,EAAG;AAChD,MAAA,IAAI,iBAAA,GAAoB,EAAA;AACxB,MAAA,IAAI,qBAAqB,MAAA,CAAO,SAAA;AAEhC,MAAA,KAAA,MAAW,CAAC,EAAA,EAAI,KAAK,KAAK,IAAA,CAAK,mBAAA,CAAoB,SAAQ,EAAG;AAC5D,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,MAAM,WAAW,CAAA;AACjD,QAAA,IAAI,WAAW,kBAAA,EAAoB;AACjC,UAAA,kBAAA,GAAqB,QAAA;AACrB,UAAA,iBAAA,GAAoB,EAAA;AAAA,QACtB;AAAA,MACF;AACA,MAAA,MAAA,GAAS,iBAAA;AAAA,IACX;AAEA,IAAA,IAAI,MAAA,IAAU,MAAA,KAAW,IAAA,CAAK,gBAAA,EAAkB;AAC9C,MAAA,IAAA,CAAK,gBAAA,GAAmB,MAAA;AACxB,MAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,iBAAA,GAA0B;AACxB,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,CAAC,CAAA,KAAM,IAAA,CAAK,YAAA,CAAa,CAAC,CAAA,KAAM,IAAA,CAAK,gBAAgB,CAAA;AAE9F,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,WAAA,EAAa;AACnC,MAAA,MAAM,WAAW,IAAA,KAAS,UAAA;AAC1B,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,IAAA,CAAK,YAAA,CAAa,gBAAgB,UAAU,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,gBAAgB,cAAc,CAAA;AAAA,MACrC;AAAA,IACF;AAEA,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,EAAA,EAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,EAAM,UAAA,EAAW,EAAG,CAAA;AAAA,IACrF;AAAA,EACF;AAAA,EAEA,aAAa,IAAA,EAAkC;AAC7C,IAAA,MAAM,OAAO,IAAA,CAAK,YAAA,CAAa,MAAM,CAAA,IAAK,IAAA,CAAK,aAAa,WAAW,CAAA;AACvE,IAAA,IAAI,CAAC,IAAA,EAAM,UAAA,CAAW,GAAG,GAAG,OAAO,IAAA;AACnC,IAAA,OAAO,IAAA,CAAK,UAAU,CAAC,CAAA;AAAA,EACzB;AACF","file":"scrollspy_controller.js","sourcesContent":["/**\n * Shared `IntersectionObserver` plumbing for Stimeo's scroll-triggered\n * controllers (`intersection`, `scrollspy`, `sticky-observer`, `lazy-frame`).\n *\n * It centralizes the `IntersectionObserver` support guard, root resolution from\n * a selector, observer creation/teardown, the **active guard** (the browser may\n * flush a final queued callback batch right after `disconnect()`, and a\n * detached controller must not mutate possibly-cached DOM), and the\n * unobserve→observe **re-arm** that re-delivers the current state even when the\n * target never leaves the viewport.\n *\n * Like {@link RovingTabindex} and `FocusTrap`, this is a policy-free internal\n * util: what an intersection *means* (a spied link, a stuck header, a lazy\n * load) stays in each controller. The public `stimeo--intersection` controller\n * is its thin declarative face.\n */\nexport interface IntersectionWatchOptions {\n /**\n * The observation root. Pass an element (or `null` for the viewport) when\n * the caller already resolved it; omit to resolve from `rootSelector`.\n */\n root?: Element | null;\n /** Selector for the observation root; empty/omitted = viewport. */\n rootSelector?: string;\n rootMargin?: string;\n threshold?: number | number[];\n}\n\nexport class IntersectionWatcher {\n readonly #onEntries: (entries: IntersectionObserverEntry[]) => void;\n #observer: IntersectionObserver | null = null;\n #active = false;\n\n constructor(onEntries: (entries: IntersectionObserverEntry[]) => void) {\n this.#onEntries = onEntries;\n }\n\n /** Whether an observer is live (started, `IntersectionObserver` supported). */\n get active(): boolean {\n return this.#active;\n }\n\n /**\n * (Re)creates the observer and observes `targets`. Returns `false` — leaving\n * the watcher inert — without `IntersectionObserver` support (very old\n * browsers; the caller's no-JS fallback stays in charge) or with no targets.\n */\n start(targets: Element | readonly Element[], options: IntersectionWatchOptions = {}): boolean {\n this.stop();\n if (typeof IntersectionObserver === \"undefined\") return false;\n const list = Array.isArray(targets) ? (targets as readonly Element[]) : [targets as Element];\n if (list.length === 0) return false;\n\n const root =\n \"root\" in options\n ? (options.root ?? null)\n : options.rootSelector\n ? document.querySelector(options.rootSelector)\n : null;\n\n this.#active = true;\n this.#observer = new IntersectionObserver(\n (entries) => {\n if (this.#active) this.#onEntries(entries);\n },\n { root, rootMargin: options.rootMargin, threshold: options.threshold },\n );\n for (const target of list) this.#observer.observe(target);\n return true;\n }\n\n /**\n * Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`\n * only reports *changes*, but `observe()` always reports the present state,\n * so unobserve→observe turns \"still intersecting\" into a fresh callback.\n */\n rearm(target: Element): void {\n if (!this.#observer) return;\n this.#observer.unobserve(target);\n this.#observer.observe(target);\n }\n\n /** Severs the observer; late queued callbacks become no-ops via the guard. */\n stop(): void {\n this.#active = false;\n this.#observer?.disconnect();\n this.#observer = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { IntersectionWatcher } from \"../utils/intersection_watcher\";\n\n/**\n * Headless, accessible Scrollspy / Navigation catalog synchronizer.\n *\n * Markup contract (identifier: `stimeo--scrollspy`):\n * <nav data-controller=\"stimeo--scrollspy\"\n * data-stimeo--scrollspy-offset-value=\"80\"\n * aria-label=\"Table of contents\">\n * <a href=\"#intro\" data-stimeo--scrollspy-target=\"link\">Intro</a>\n * <a href=\"#usage\" data-stimeo--scrollspy-target=\"link\">Usage</a>\n * </nav>\n * <section id=\"intro\">…</section>\n * <section id=\"usage\">…</section>\n *\n * Implements clean active location synchronization driven by `aria-current`:\n * - Leverages native `IntersectionObserver` to track viewport visibility.\n * - Robust evaluation algorithm: among all intersecting sections, the one closest\n * to the top offset trigger line (`boundingClientRect.top`) is prioritized as active.\n * - Toggles `aria-current=\"location\"` dynamically on active/inactive links.\n * - Gracefully handles teardown `disconnect()` by severing observer references to prevent leaks.\n *\n * @remarks\n * Behavior only. The controller manages state on catalog links via `aria-current`,\n * respects custom offsets, and dispatches `stimeo--scrollspy:change` events.\n */\nexport class ScrollspyController extends Controller<HTMLElement> {\n static override targets = [\"link\"];\n static override values = {\n offset: { type: Number, default: 0 },\n rootMargin: { type: String, default: \"\" },\n rootSelector: { type: String, default: \"\" },\n };\n static actions = [\"scrollTo\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly linkTargets: HTMLElement[];\n\n declare offsetValue: number;\n declare rootMarginValue: string;\n declare rootSelectorValue: string;\n\n /** Shared IO plumbing (support guard, active guard, teardown). */\n readonly #watcher = new IntersectionWatcher((entries) => this.#onIntersection(entries));\n #isConnected = false;\n\n /** Track active/intersecting status of each section element by ID. */\n #intersectionStates = new Map<string, { isIntersecting: boolean; top: number }>();\n\n /** Current active section ID, used to avoid duplicate event dispatching. */\n #activeSectionId = \"\";\n\n override connect(): void {\n this.#isConnected = true;\n this.#initializeObserver();\n }\n\n override disconnect(): void {\n this.#isConnected = false;\n this.#watcher.stop();\n this.#intersectionStates.clear();\n this.#activeSectionId = \"\";\n }\n\n /**\n * Re-initializes the observer if the offset or rootMargin values change dynamically.\n */\n offsetValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n rootMarginValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n rootSelectorValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n /**\n * Smoothly scrolls to the target element mapped by the link anchor.\n * Prevents full window scroll jumps when tracking nested scrollable containers.\n */\n scrollTo(event: Event): void {\n const link = event.currentTarget as HTMLElement;\n const id = this.#getAnchorId(link);\n if (!id) return;\n\n event.preventDefault();\n\n const targetElement = document.getElementById(id);\n if (!targetElement) return;\n\n const rootElement = this.#getRootElement();\n if (rootElement) {\n const containerRect = rootElement.getBoundingClientRect();\n const targetRect = targetElement.getBoundingClientRect();\n const scrollPosition =\n rootElement.scrollTop + (targetRect.top - containerRect.top) - this.offsetValue;\n\n rootElement.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\",\n });\n } else {\n const targetRect = targetElement.getBoundingClientRect();\n const scrollPosition = window.scrollY + targetRect.top - this.offsetValue;\n\n window.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\",\n });\n }\n }\n\n #getRootElement(): HTMLElement | null {\n if (!this.rootSelectorValue) return null;\n return document.querySelector(this.rootSelectorValue);\n }\n\n #initializeObserver(): void {\n this.#watcher.stop();\n this.#intersectionStates.clear();\n this.#activeSectionId = \"\";\n\n if (this.linkTargets.length === 0) return;\n\n // Build the dynamic rootMargin string based on offset value\n const margin = this.rootMarginValue || `-${this.offsetValue}px 0px -80% 0px`;\n\n // Observe each target section mapped by the href anchors.\n const sections: Element[] = [];\n for (const link of this.linkTargets) {\n const id = this.#getAnchorId(link);\n if (!id) continue;\n const section = document.getElementById(id);\n if (section) sections.push(section);\n }\n\n this.#watcher.start(sections, {\n root: this.#getRootElement(),\n rootMargin: margin,\n threshold: [0, 0.2, 0.4, 0.6, 0.8, 1], // Multiple thresholds handle large sections safely\n });\n }\n\n readonly #onIntersection = (entries: IntersectionObserverEntry[]): void => {\n // Ignore any callback delivered after teardown: the browser may flush a\n // final queued batch right after `disconnect()`, and a detached controller\n // must not mutate `aria-current` on (possibly cached) links.\n if (!this.#isConnected) return;\n\n for (const entry of entries) {\n const id = entry.target.id;\n if (!id) continue;\n\n this.#intersectionStates.set(id, {\n isIntersecting: entry.isIntersecting,\n top: entry.boundingClientRect.top,\n });\n }\n\n this.#evaluateActiveSection();\n };\n\n #evaluateActiveSection(): void {\n // The trigger line is `offset` px below the top of the scroll root. When a\n // nested `rootSelector` container is used it is not at the viewport top, so\n // the line must be measured from the container's current top — comparing the\n // viewport-based `boundingClientRect.top` against a bare `offset` would\n // otherwise pick the section nearest the viewport top, not the container's.\n const rootEl = this.#getRootElement();\n const triggerLine = (rootEl ? rootEl.getBoundingClientRect().top : 0) + this.offsetValue;\n\n let bestId = \"\";\n let closestTop = Number.MAX_VALUE;\n\n // Evaluate among all intersecting elements\n for (const [id, state] of this.#intersectionStates.entries()) {\n if (state.isIntersecting) {\n // Evaluate the one closest to the offset trigger line\n const distance = Math.abs(state.top - triggerLine);\n if (distance < closestTop) {\n closestTop = distance;\n bestId = id;\n }\n }\n }\n\n // Fallback: when no section currently intersects (e.g. scrolled past the\n // bottom, or between sections), pick the tracked section whose top is closest\n // to the trigger line so something stays highlighted.\n if (!bestId && this.#intersectionStates.size > 0) {\n let absoluteClosestId = \"\";\n let absoluteClosestTop = Number.MAX_VALUE;\n\n for (const [id, state] of this.#intersectionStates.entries()) {\n const distance = Math.abs(state.top - triggerLine);\n if (distance < absoluteClosestTop) {\n absoluteClosestTop = distance;\n absoluteClosestId = id;\n }\n }\n bestId = absoluteClosestId;\n }\n\n if (bestId && bestId !== this.#activeSectionId) {\n this.#activeSectionId = bestId;\n this.#syncActiveStates();\n }\n }\n\n #syncActiveStates(): void {\n const activeLink = this.linkTargets.find((l) => this.#getAnchorId(l) === this.#activeSectionId);\n\n for (const link of this.linkTargets) {\n const isActive = link === activeLink;\n if (isActive) {\n link.setAttribute(\"aria-current\", \"location\");\n } else {\n link.removeAttribute(\"aria-current\");\n }\n }\n\n if (activeLink) {\n this.dispatch(\"change\", { detail: { id: this.#activeSectionId, link: activeLink } });\n }\n }\n\n #getAnchorId(link: HTMLElement): string | null {\n const href = link.getAttribute(\"href\") || link.getAttribute(\"data-href\");\n if (!href?.startsWith(\"#\")) return null;\n return href.substring(1);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/intersection_watcher.ts","../../src/controllers/scrollspy_controller.ts"],"names":[],"mappings":"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","file":"scrollspy_controller.js","sourcesContent":["/**\n * Shared `IntersectionObserver` plumbing for Stimeo's scroll-triggered\n * controllers (`intersection`, `scrollspy`, `sticky-observer`, `lazy-frame`).\n *\n * It centralizes the `IntersectionObserver` support guard, root resolution from\n * a selector, observer creation/teardown, the **active guard** (the browser may\n * flush a final queued callback batch right after `disconnect()`, and a\n * detached controller must not mutate possibly-cached DOM), and the\n * unobserve→observe **re-arm** that re-delivers the current state even when the\n * target never leaves the viewport.\n *\n * Like {@link RovingTabindex} and `FocusTrap`, this is a policy-free internal\n * util: what an intersection *means* (a spied link, a stuck header, a lazy\n * load) stays in each controller. The public `stimeo--intersection` controller\n * is its thin declarative face.\n */\n/**\n * Whether `entry`'s target sits entirely before the root's **start (top)** edge —\n * the \"scrolled past the top\" half of a non-intersecting entry, as opposed to\n * \"not reached yet\" below the root.\n *\n * A target with no layout box (`display: none`, a `hidden` ancestor, a collapsed\n * `<details>`) is reported with an **empty rect**, whose `bottom` of `0` would\n * otherwise satisfy `bottom <= rootTop` for a viewport root and read as \"passed\"\n * even though the target was never scrolled anywhere. An empty rect carries no\n * position at all, so it is deliberately never \"before the edge\"; what a caller\n * publishes for that case is its own policy (both consumers treat it as the\n * neutral \"not passed\"/\"not stuck\", and the real rect that arrives once the\n * target is laid out re-establishes the true state).\n */\nexport function isBeforeRootStart(entry: IntersectionObserverEntry): boolean {\n const rect = entry.boundingClientRect;\n if (rect.width === 0 && rect.height === 0) return false;\n // rootBounds is null for a cross-origin/removed root; fall back to the\n // viewport origin.\n const rootTop = entry.rootBounds?.top ?? 0;\n return rect.bottom <= rootTop;\n}\n\nexport interface IntersectionWatchOptions {\n /**\n * The observation root. Pass an element (or `null` for the viewport) when\n * the caller already resolved it; omit to resolve from `rootSelector`.\n */\n root?: Element | null;\n /** Selector for the observation root; empty/omitted = viewport. */\n rootSelector?: string;\n rootMargin?: string;\n threshold?: number | number[];\n}\n\nexport class IntersectionWatcher {\n readonly #onEntries: (entries: IntersectionObserverEntry[]) => void;\n #observer: IntersectionObserver | null = null;\n #active = false;\n\n constructor(onEntries: (entries: IntersectionObserverEntry[]) => void) {\n this.#onEntries = onEntries;\n }\n\n /** Whether an observer is live (started, `IntersectionObserver` supported). */\n get active(): boolean {\n return this.#active;\n }\n\n /**\n * (Re)creates the observer and observes `targets`. Returns `false` — leaving\n * the watcher inert — without `IntersectionObserver` support (very old\n * browsers; the caller's no-JS fallback stays in charge) or with no targets.\n *\n * @throws Whatever the platform throws for an invalid `rootMargin`/`threshold`\n * or a failing `observe()`. The exception is passed through unchanged, but\n * the watcher rolls back first: every target observed so far is released and\n * `active` stays `false`, so a caller that retries starts from a clean slate.\n */\n start(targets: Element | readonly Element[], options: IntersectionWatchOptions = {}): boolean {\n this.stop();\n if (typeof IntersectionObserver === \"undefined\") return false;\n const list = Array.isArray(targets) ? (targets as readonly Element[]) : [targets as Element];\n if (list.length === 0) return false;\n\n const root =\n \"root\" in options\n ? (options.root ?? null)\n : options.rootSelector\n ? document.querySelector(options.rootSelector)\n : null;\n\n let observer: IntersectionObserver | null = null;\n try {\n observer = new IntersectionObserver(\n (entries) => {\n // Identity matters across an immediate restart: the old observer can\n // flush a queued batch after the new observer has made `active` true.\n if (this.#active && this.#observer === observer) this.#onEntries(entries);\n },\n { root, rootMargin: options.rootMargin, threshold: options.threshold },\n );\n for (const target of list) observer.observe(target);\n this.#observer = observer;\n this.#active = true;\n return true;\n } catch (error) {\n // A constructor or partial observe failure must not leave earlier targets\n // observed or report an active watcher. Preserve the platform exception.\n observer?.disconnect();\n this.#observer = null;\n this.#active = false;\n throw error;\n }\n }\n\n /**\n * Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`\n * only reports *changes*, but `observe()` always reports the present state,\n * so unobserve→observe turns \"still intersecting\" into a fresh callback.\n *\n * @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped\n * first, so it never stays live with a half-rearmed target.\n */\n rearm(target: Element): void {\n if (!this.#observer) return;\n try {\n this.#observer.unobserve(target);\n this.#observer.observe(target);\n } catch (error) {\n this.stop();\n throw error;\n }\n }\n\n /** Severs the observer; late queued callbacks become no-ops via the guard. */\n stop(): void {\n this.#active = false;\n this.#observer?.disconnect();\n this.#observer = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { IntersectionWatcher } from \"../utils/intersection_watcher\";\n\n/**\n * Headless, accessible Scrollspy / Navigation catalog synchronizer.\n *\n * Markup contract (identifier: `stimeo--scrollspy`):\n * <nav data-controller=\"stimeo--scrollspy\"\n * data-stimeo--scrollspy-offset-value=\"80\"\n * aria-label=\"Table of contents\">\n * <a href=\"#intro\" data-stimeo--scrollspy-target=\"link\">Intro</a>\n * <a href=\"#usage\" data-stimeo--scrollspy-target=\"link\">Usage</a>\n * </nav>\n * <section id=\"intro\">…</section>\n * <section id=\"usage\">…</section>\n *\n * Implements clean active location synchronization driven by `aria-current`:\n * - Leverages native `IntersectionObserver` to track viewport visibility.\n * - Robust evaluation algorithm: among all intersecting sections, the one closest\n * to the top offset trigger line (`boundingClientRect.top`) is prioritized as active.\n * - Toggles `aria-current=\"location\"` dynamically on active/inactive links.\n * - Gracefully handles teardown `disconnect()` by severing observer references to prevent leaks.\n *\n * @remarks\n * Behavior only. The controller manages state on catalog links via `aria-current`,\n * respects custom offsets, and dispatches `stimeo--scrollspy:change` events.\n */\nexport class ScrollspyController extends Controller<HTMLElement> {\n static override targets = [\"link\"];\n static override values = {\n offset: { type: Number, default: 0 },\n rootMargin: { type: String, default: \"\" },\n rootSelector: { type: String, default: \"\" },\n };\n static actions = [\"scrollTo\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly linkTargets: HTMLElement[];\n\n declare offsetValue: number;\n declare rootMarginValue: string;\n declare rootSelectorValue: string;\n\n /** Shared IO plumbing (support guard, active guard, teardown). */\n readonly #watcher = new IntersectionWatcher((entries) => this.#onIntersection(entries));\n #isConnected = false;\n\n /** Track active/intersecting status of each section element by ID. */\n #intersectionStates = new Map<string, { isIntersecting: boolean; top: number }>();\n\n /** Current active section ID, used to avoid duplicate event dispatching. */\n #activeSectionId = \"\";\n\n override connect(): void {\n this.#isConnected = true;\n this.#initializeObserver();\n }\n\n override disconnect(): void {\n this.#isConnected = false;\n this.#watcher.stop();\n this.#intersectionStates.clear();\n this.#activeSectionId = \"\";\n }\n\n /**\n * Re-initializes the observer if the offset or rootMargin values change dynamically.\n */\n offsetValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n rootMarginValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n rootSelectorValueChanged(): void {\n if (!this.#isConnected) return;\n this.#initializeObserver();\n }\n\n /**\n * Smoothly scrolls to the target element mapped by the link anchor.\n * Prevents full window scroll jumps when tracking nested scrollable containers.\n */\n scrollTo(event: Event): void {\n const link = event.currentTarget as HTMLElement;\n const id = this.#getAnchorId(link);\n if (!id) return;\n\n event.preventDefault();\n\n const targetElement = document.getElementById(id);\n if (!targetElement) return;\n\n const rootElement = this.#getRootElement();\n if (rootElement) {\n const containerRect = rootElement.getBoundingClientRect();\n const targetRect = targetElement.getBoundingClientRect();\n const scrollPosition =\n rootElement.scrollTop + (targetRect.top - containerRect.top) - this.offsetValue;\n\n rootElement.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\",\n });\n } else {\n const targetRect = targetElement.getBoundingClientRect();\n const scrollPosition = window.scrollY + targetRect.top - this.offsetValue;\n\n window.scrollTo({\n top: scrollPosition,\n behavior: \"smooth\",\n });\n }\n }\n\n #getRootElement(): HTMLElement | null {\n if (!this.rootSelectorValue) return null;\n return document.querySelector(this.rootSelectorValue);\n }\n\n #initializeObserver(): void {\n this.#watcher.stop();\n this.#intersectionStates.clear();\n this.#activeSectionId = \"\";\n\n if (this.linkTargets.length === 0) return;\n\n // Build the dynamic rootMargin string based on offset value\n const margin = this.rootMarginValue || `-${this.offsetValue}px 0px -80% 0px`;\n\n // Observe each target section mapped by the href anchors.\n const sections: Element[] = [];\n for (const link of this.linkTargets) {\n const id = this.#getAnchorId(link);\n if (!id) continue;\n const section = document.getElementById(id);\n if (section) sections.push(section);\n }\n\n this.#watcher.start(sections, {\n root: this.#getRootElement(),\n rootMargin: margin,\n threshold: [0, 0.2, 0.4, 0.6, 0.8, 1], // Multiple thresholds handle large sections safely\n });\n }\n\n readonly #onIntersection = (entries: IntersectionObserverEntry[]): void => {\n // Ignore any callback delivered after teardown: the browser may flush a\n // final queued batch right after `disconnect()`, and a detached controller\n // must not mutate `aria-current` on (possibly cached) links.\n if (!this.#isConnected) return;\n\n for (const entry of entries) {\n const id = entry.target.id;\n if (!id) continue;\n\n this.#intersectionStates.set(id, {\n isIntersecting: entry.isIntersecting,\n top: entry.boundingClientRect.top,\n });\n }\n\n this.#evaluateActiveSection();\n };\n\n #evaluateActiveSection(): void {\n // The trigger line is `offset` px below the top of the scroll root. When a\n // nested `rootSelector` container is used it is not at the viewport top, so\n // the line must be measured from the container's current top — comparing the\n // viewport-based `boundingClientRect.top` against a bare `offset` would\n // otherwise pick the section nearest the viewport top, not the container's.\n const rootEl = this.#getRootElement();\n const triggerLine = (rootEl ? rootEl.getBoundingClientRect().top : 0) + this.offsetValue;\n\n let bestId = \"\";\n let closestTop = Number.MAX_VALUE;\n\n // Evaluate among all intersecting elements\n for (const [id, state] of this.#intersectionStates.entries()) {\n if (state.isIntersecting) {\n // Evaluate the one closest to the offset trigger line\n const distance = Math.abs(state.top - triggerLine);\n if (distance < closestTop) {\n closestTop = distance;\n bestId = id;\n }\n }\n }\n\n // Fallback: when no section currently intersects (e.g. scrolled past the\n // bottom, or between sections), pick the tracked section whose top is closest\n // to the trigger line so something stays highlighted.\n if (!bestId && this.#intersectionStates.size > 0) {\n let absoluteClosestId = \"\";\n let absoluteClosestTop = Number.MAX_VALUE;\n\n for (const [id, state] of this.#intersectionStates.entries()) {\n const distance = Math.abs(state.top - triggerLine);\n if (distance < absoluteClosestTop) {\n absoluteClosestTop = distance;\n absoluteClosestId = id;\n }\n }\n bestId = absoluteClosestId;\n }\n\n if (bestId && bestId !== this.#activeSectionId) {\n this.#activeSectionId = bestId;\n this.#syncActiveStates();\n }\n }\n\n #syncActiveStates(): void {\n const activeLink = this.linkTargets.find((l) => this.#getAnchorId(l) === this.#activeSectionId);\n\n for (const link of this.linkTargets) {\n const isActive = link === activeLink;\n if (isActive) {\n link.setAttribute(\"aria-current\", \"location\");\n } else {\n link.removeAttribute(\"aria-current\");\n }\n }\n\n if (activeLink) {\n this.dispatch(\"change\", { detail: { id: this.#activeSectionId, link: activeLink } });\n }\n }\n\n #getAnchorId(link: HTMLElement): string | null {\n const href = link.getAttribute(\"href\") || link.getAttribute(\"data-href\");\n if (!href?.startsWith(\"#\")) return null;\n return href.substring(1);\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless **Separator** behavior (APG Separator pattern).
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--separator`):
|
|
7
|
+
* <!-- decorative -->
|
|
8
|
+
* <div data-controller="stimeo--separator" role="separator"
|
|
9
|
+
* data-stimeo--separator-orientation-value="horizontal"></div>
|
|
10
|
+
*
|
|
11
|
+
* <!-- focusable, value-bearing -->
|
|
12
|
+
* <div data-controller="stimeo--separator" role="separator" tabindex="0"
|
|
13
|
+
* aria-label="Resize sidebar" aria-orientation="vertical"
|
|
14
|
+
* aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"
|
|
15
|
+
* data-stimeo--separator-focusable-value="true"
|
|
16
|
+
* data-action="keydown->stimeo--separator#onKeydown"></div>
|
|
17
|
+
*
|
|
18
|
+
* Most separators are static (`role="separator"` + `aria-orientation`); this
|
|
19
|
+
* controller adds those semantics and, for the optional **focusable** variant,
|
|
20
|
+
* keeps `aria-valuenow` in sync and emits arrow-key value changes. The actual
|
|
21
|
+
* pane resize/drag is out of scope — that belongs to {@link ResizableController}.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Behavior only — line drawing is the consumer's CSS. Increment direction
|
|
25
|
+
* follows the slider convention: ArrowUp/ArrowRight increase, ArrowDown/ArrowLeft
|
|
26
|
+
* decrease, scoped to the relevant axis for the orientation.
|
|
27
|
+
*/
|
|
28
|
+
declare class SeparatorController extends Controller<HTMLElement> {
|
|
29
|
+
#private;
|
|
30
|
+
static values: {
|
|
31
|
+
orientation: {
|
|
32
|
+
type: StringConstructor;
|
|
33
|
+
default: string;
|
|
34
|
+
};
|
|
35
|
+
focusable: {
|
|
36
|
+
type: BooleanConstructor;
|
|
37
|
+
default: boolean;
|
|
38
|
+
};
|
|
39
|
+
step: {
|
|
40
|
+
type: NumberConstructor;
|
|
41
|
+
default: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
static actions: readonly ["onKeydown"];
|
|
45
|
+
static events: readonly ["change"];
|
|
46
|
+
orientationValue: string;
|
|
47
|
+
focusableValue: boolean;
|
|
48
|
+
stepValue: number;
|
|
49
|
+
connect(): void;
|
|
50
|
+
/** Adjusts the value on arrow / Home / End keys (focusable variant only). */
|
|
51
|
+
onKeydown(event: KeyboardEvent): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { SeparatorController };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/separator_controller.ts
|
|
4
|
+
var SeparatorController = class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
orientation: { type: String, default: "horizontal" },
|
|
7
|
+
focusable: { type: Boolean, default: false },
|
|
8
|
+
step: { type: Number, default: 1 }
|
|
9
|
+
};
|
|
10
|
+
static actions = ["onKeydown"];
|
|
11
|
+
static events = ["change"];
|
|
12
|
+
connect() {
|
|
13
|
+
if (!this.element.hasAttribute("role")) {
|
|
14
|
+
this.element.setAttribute("role", "separator");
|
|
15
|
+
}
|
|
16
|
+
if (!this.element.hasAttribute("aria-orientation")) {
|
|
17
|
+
this.element.setAttribute("aria-orientation", this.orientationValue);
|
|
18
|
+
}
|
|
19
|
+
if (this.focusableValue) {
|
|
20
|
+
if (!this.element.hasAttribute("tabindex")) {
|
|
21
|
+
this.element.setAttribute("tabindex", "0");
|
|
22
|
+
}
|
|
23
|
+
this.#setDefault("aria-valuemin", "0");
|
|
24
|
+
this.#setDefault("aria-valuemax", "100");
|
|
25
|
+
this.#setDefault("aria-valuenow", String(this.#clamp(this.#value)));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Adjusts the value on arrow / Home / End keys (focusable variant only). */
|
|
29
|
+
onKeydown(event) {
|
|
30
|
+
if (!this.focusableValue) return;
|
|
31
|
+
const horizontal = this.element.getAttribute("aria-orientation") !== "vertical";
|
|
32
|
+
let next = null;
|
|
33
|
+
switch (event.key) {
|
|
34
|
+
case "ArrowUp":
|
|
35
|
+
if (horizontal) next = this.#value + this.stepValue;
|
|
36
|
+
break;
|
|
37
|
+
case "ArrowDown":
|
|
38
|
+
if (horizontal) next = this.#value - this.stepValue;
|
|
39
|
+
break;
|
|
40
|
+
case "ArrowRight":
|
|
41
|
+
if (!horizontal) next = this.#value + this.stepValue;
|
|
42
|
+
break;
|
|
43
|
+
case "ArrowLeft":
|
|
44
|
+
if (!horizontal) next = this.#value - this.stepValue;
|
|
45
|
+
break;
|
|
46
|
+
case "Home":
|
|
47
|
+
next = this.#min;
|
|
48
|
+
break;
|
|
49
|
+
case "End":
|
|
50
|
+
next = this.#max;
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (next === null) return;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
const clamped = this.#clamp(next);
|
|
58
|
+
if (clamped === this.#value) return;
|
|
59
|
+
this.element.setAttribute("aria-valuenow", String(clamped));
|
|
60
|
+
this.dispatch("change", { detail: { value: clamped } });
|
|
61
|
+
}
|
|
62
|
+
get #value() {
|
|
63
|
+
return this.#numericAttr("aria-valuenow", 0);
|
|
64
|
+
}
|
|
65
|
+
get #min() {
|
|
66
|
+
return this.#numericAttr("aria-valuemin", 0);
|
|
67
|
+
}
|
|
68
|
+
get #max() {
|
|
69
|
+
return this.#numericAttr("aria-valuemax", 100);
|
|
70
|
+
}
|
|
71
|
+
#clamp(value) {
|
|
72
|
+
return Math.min(this.#max, Math.max(this.#min, value));
|
|
73
|
+
}
|
|
74
|
+
#numericAttr(name, fallback) {
|
|
75
|
+
const parsed = Number.parseFloat(this.element.getAttribute(name) ?? "");
|
|
76
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
77
|
+
}
|
|
78
|
+
#setDefault(name, value) {
|
|
79
|
+
if (!this.element.hasAttribute(name)) {
|
|
80
|
+
this.element.setAttribute(name, value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export { SeparatorController };
|
|
86
|
+
//# sourceMappingURL=separator_controller.js.map
|
|
87
|
+
//# sourceMappingURL=separator_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/separator_controller.ts"],"names":[],"mappings":"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","file":"separator_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **Separator** behavior (APG Separator pattern).\n *\n * Markup contract (identifier: `stimeo--separator`):\n * <!-- decorative -->\n * <div data-controller=\"stimeo--separator\" role=\"separator\"\n * data-stimeo--separator-orientation-value=\"horizontal\"></div>\n *\n * <!-- focusable, value-bearing -->\n * <div data-controller=\"stimeo--separator\" role=\"separator\" tabindex=\"0\"\n * aria-label=\"Resize sidebar\" aria-orientation=\"vertical\"\n * aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"50\"\n * data-stimeo--separator-focusable-value=\"true\"\n * data-action=\"keydown->stimeo--separator#onKeydown\"></div>\n *\n * Most separators are static (`role=\"separator\"` + `aria-orientation`); this\n * controller adds those semantics and, for the optional **focusable** variant,\n * keeps `aria-valuenow` in sync and emits arrow-key value changes. The actual\n * pane resize/drag is out of scope — that belongs to {@link ResizableController}.\n *\n * @remarks\n * Behavior only — line drawing is the consumer's CSS. Increment direction\n * follows the slider convention: ArrowUp/ArrowRight increase, ArrowDown/ArrowLeft\n * decrease, scoped to the relevant axis for the orientation.\n */\nexport class SeparatorController extends Controller<HTMLElement> {\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n focusable: { type: Boolean, default: false },\n step: { type: Number, default: 1 },\n };\n static actions = [\"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare orientationValue: string;\n declare focusableValue: boolean;\n declare stepValue: number;\n\n override connect(): void {\n if (!this.element.hasAttribute(\"role\")) {\n this.element.setAttribute(\"role\", \"separator\");\n }\n if (!this.element.hasAttribute(\"aria-orientation\")) {\n this.element.setAttribute(\"aria-orientation\", this.orientationValue);\n }\n\n if (this.focusableValue) {\n if (!this.element.hasAttribute(\"tabindex\")) {\n this.element.setAttribute(\"tabindex\", \"0\");\n }\n // A value-bearing separator needs a bounded range; default it if the\n // consumer left any bound off so arrow keys have something to clamp to.\n this.#setDefault(\"aria-valuemin\", \"0\");\n this.#setDefault(\"aria-valuemax\", \"100\");\n this.#setDefault(\"aria-valuenow\", String(this.#clamp(this.#value)));\n }\n }\n\n /** Adjusts the value on arrow / Home / End keys (focusable variant only). */\n onKeydown(event: KeyboardEvent): void {\n if (!this.focusableValue) return;\n\n // `aria-orientation` is the source of truth (connect seeds it from the\n // orientation value when the consumer left it off), so the focusable variant\n // that sets the attribute directly drives the axis correctly.\n const horizontal = this.element.getAttribute(\"aria-orientation\") !== \"vertical\";\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n if (horizontal) next = this.#value + this.stepValue;\n break;\n case \"ArrowDown\":\n if (horizontal) next = this.#value - this.stepValue;\n break;\n case \"ArrowRight\":\n if (!horizontal) next = this.#value + this.stepValue;\n break;\n case \"ArrowLeft\":\n if (!horizontal) next = this.#value - this.stepValue;\n break;\n case \"Home\":\n next = this.#min;\n break;\n case \"End\":\n next = this.#max;\n break;\n default:\n return;\n }\n if (next === null) return;\n\n event.preventDefault();\n const clamped = this.#clamp(next);\n if (clamped === this.#value) return;\n\n this.element.setAttribute(\"aria-valuenow\", String(clamped));\n this.dispatch(\"change\", { detail: { value: clamped } });\n }\n\n get #value(): number {\n return this.#numericAttr(\"aria-valuenow\", 0);\n }\n\n get #min(): number {\n return this.#numericAttr(\"aria-valuemin\", 0);\n }\n\n get #max(): number {\n return this.#numericAttr(\"aria-valuemax\", 100);\n }\n\n #clamp(value: number): number {\n return Math.min(this.#max, Math.max(this.#min, value));\n }\n\n #numericAttr(name: string, fallback: number): number {\n const parsed = Number.parseFloat(this.element.getAttribute(name) ?? \"\");\n return Number.isNaN(parsed) ? fallback : parsed;\n }\n\n #setDefault(name: string, value: string): void {\n if (!this.element.hasAttribute(name)) {\n this.element.setAttribute(name, value);\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible **responsive collapsible sidebar** behavior.
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--sidebar`):
|
|
7
|
+
* <div data-controller="stimeo--sidebar"
|
|
8
|
+
* data-action="turbo:before-cache@document->stimeo--sidebar#beforeCache"
|
|
9
|
+
* data-stimeo--sidebar-breakpoint-value="768"
|
|
10
|
+
* data-stimeo--sidebar-key-value="main-nav">
|
|
11
|
+
* <header>
|
|
12
|
+
* <button data-stimeo--sidebar-target="trigger"
|
|
13
|
+
* data-action="click->stimeo--sidebar#toggle"
|
|
14
|
+
* aria-expanded="true" aria-controls="app-sidebar">Menu</button>
|
|
15
|
+
* </header>
|
|
16
|
+
* <div data-stimeo--sidebar-target="backdrop"
|
|
17
|
+
* data-action="click->stimeo--sidebar#close" hidden></div>
|
|
18
|
+
* <aside id="app-sidebar" data-stimeo--sidebar-target="panel"
|
|
19
|
+
* aria-label="Main" data-mode="inline" data-state="expanded">
|
|
20
|
+
* <nav aria-label="…">…</nav>
|
|
21
|
+
* </aside>
|
|
22
|
+
* </div>
|
|
23
|
+
* <main>…</main> <!-- a body-level sibling so it can be made inert in overlay -->
|
|
24
|
+
*
|
|
25
|
+
* No dedicated APG pattern: the base is **Disclosure** (the trigger's
|
|
26
|
+
* `aria-expanded` controls the panel's expanded state) and, *below* the
|
|
27
|
+
* `breakpoint`, it borrows the **Dialog (Modal)** focus behavior via the shared
|
|
28
|
+
* {@link FocusTrap} (the same trap used by dialog / alert-dialog / drawer).
|
|
29
|
+
*
|
|
30
|
+
* Above the breakpoint it is an **inline**, non-modal element that toggles
|
|
31
|
+
* `expanded`↔`collapsed` (a rail), persisting that preference in `localStorage`.
|
|
32
|
+
* Below it, it becomes an **overlay** off-canvas panel: opening activates the
|
|
33
|
+
* trap (focus move, `Tab` cycle, `Escape`, body scroll lock, background `inert`,
|
|
34
|
+
* focus restore); closing defers `hidden` and the trap teardown until the exit
|
|
35
|
+
* transition ends (synchronously when there is none).
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Behavior only — rail width, slide, and backdrop are the consumer's CSS, keyed
|
|
39
|
+
* off `data-mode` (`inline`/`overlay`) and `data-state`. `aria-expanded` is an
|
|
40
|
+
* abstract "is the panel expanded" flag, independent of the visual difference
|
|
41
|
+
* between an inline collapsed rail (still in the DOM) and an overlay closed panel
|
|
42
|
+
* (`hidden`/off-canvas). The `role="dialog"` semantics are intentionally **not**
|
|
43
|
+
* applied — the sidebar stays an `<aside>`/`<nav>` landmark and only borrows the
|
|
44
|
+
* modal *behavior* (cf. drawer's note on `<aside role="dialog">` conflicts).
|
|
45
|
+
* The collapsed preference persists across Turbo navigations and full reloads;
|
|
46
|
+
* the transient overlay-open state never persists, so "back/forward" never
|
|
47
|
+
* restores a stuck-open menu.
|
|
48
|
+
*/
|
|
49
|
+
declare class SidebarController extends Controller<HTMLElement> {
|
|
50
|
+
#private;
|
|
51
|
+
static targets: string[];
|
|
52
|
+
static values: {
|
|
53
|
+
breakpoint: {
|
|
54
|
+
type: NumberConstructor;
|
|
55
|
+
default: number;
|
|
56
|
+
};
|
|
57
|
+
key: {
|
|
58
|
+
type: StringConstructor;
|
|
59
|
+
default: string;
|
|
60
|
+
};
|
|
61
|
+
collapsed: {
|
|
62
|
+
type: BooleanConstructor;
|
|
63
|
+
default: boolean;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
static actions: readonly ["beforeCache", "close", "open", "toggle"];
|
|
67
|
+
readonly triggerTarget: HTMLElement;
|
|
68
|
+
readonly panelTarget: HTMLElement;
|
|
69
|
+
readonly backdropTarget: HTMLElement;
|
|
70
|
+
readonly hasTriggerTarget: boolean;
|
|
71
|
+
readonly hasPanelTarget: boolean;
|
|
72
|
+
readonly hasBackdropTarget: boolean;
|
|
73
|
+
breakpointValue: number;
|
|
74
|
+
keyValue: string;
|
|
75
|
+
collapsedValue: boolean;
|
|
76
|
+
connect(): void;
|
|
77
|
+
disconnect(): void;
|
|
78
|
+
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
79
|
+
panelTargetConnected(panel: HTMLElement): void;
|
|
80
|
+
/** Closes and releases overlay side effects when the actively trapped panel disappears. */
|
|
81
|
+
panelTargetDisconnected(panel: HTMLElement): void;
|
|
82
|
+
/**
|
|
83
|
+
* Rebinds responsive observation when `breakpoint` changes at runtime.
|
|
84
|
+
*
|
|
85
|
+
* Stimulus calls value callbacks before `connect()`, so the connected guard
|
|
86
|
+
* prevents an eager subscription. Equivalent normalized queries retain the
|
|
87
|
+
* existing listener instead of allocating duplicate `MediaQueryList` objects.
|
|
88
|
+
*/
|
|
89
|
+
breakpointValueChanged(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Sanitizes overlay markup before Turbo snapshots the page.
|
|
92
|
+
*
|
|
93
|
+
* Inline state is durable and remains untouched. Overlay state is transient:
|
|
94
|
+
* pending transitions are cancelled, controller-owned open attributes are
|
|
95
|
+
* closed immediately, and modal side effects are released without moving
|
|
96
|
+
* focus during navigation.
|
|
97
|
+
*/
|
|
98
|
+
beforeCache(): void;
|
|
99
|
+
/** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
|
|
100
|
+
toggle(): void;
|
|
101
|
+
/** Shows the panel (inline: expand; overlay: open). */
|
|
102
|
+
open(): void;
|
|
103
|
+
/** Hides the panel (inline: collapse; overlay: close). */
|
|
104
|
+
close(): void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { SidebarController };
|