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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/password_reveal_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/safe_timeout.ts
|
|
6
|
+
var TimerRegistry = class {
|
|
7
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
8
|
+
ids = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Cancels a single tracked timer.
|
|
11
|
+
*
|
|
12
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
13
|
+
* registry), so callers can clear defensively without guarding.
|
|
14
|
+
*/
|
|
15
|
+
clear(id) {
|
|
16
|
+
if (this.ids.delete(id)) {
|
|
17
|
+
this.cancel(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
22
|
+
* to guarantee no timer outlives the element.
|
|
23
|
+
*/
|
|
24
|
+
clearAll() {
|
|
25
|
+
for (const id of this.ids) {
|
|
26
|
+
this.cancel(id);
|
|
27
|
+
}
|
|
28
|
+
this.ids.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Number of timers currently tracked (pending). */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.ids.size;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
36
|
+
/**
|
|
37
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
38
|
+
*
|
|
39
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
40
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
41
|
+
*/
|
|
42
|
+
set(callback, delay) {
|
|
43
|
+
const id = this.schedule(() => {
|
|
44
|
+
this.ids.delete(id);
|
|
45
|
+
callback();
|
|
46
|
+
}, delay);
|
|
47
|
+
this.ids.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
schedule(callback, delay) {
|
|
51
|
+
return window.setTimeout(callback, delay);
|
|
52
|
+
}
|
|
53
|
+
cancel(id) {
|
|
54
|
+
window.clearTimeout(id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/controllers/password_reveal_controller.ts
|
|
59
|
+
var PasswordRevealController = class extends Controller {
|
|
60
|
+
static targets = ["input", "toggle"];
|
|
61
|
+
static values = {
|
|
62
|
+
autoHide: { type: Number, default: 0 }
|
|
63
|
+
};
|
|
64
|
+
static actions = ["toggle"];
|
|
65
|
+
static events = ["toggle"];
|
|
66
|
+
/** Auto re-mask timer; torn down on disconnect. */
|
|
67
|
+
#timers = new SafeTimeout();
|
|
68
|
+
connect() {
|
|
69
|
+
this.#reflect(this.#isVisible);
|
|
70
|
+
}
|
|
71
|
+
disconnect() {
|
|
72
|
+
this.#timers.clearAll();
|
|
73
|
+
}
|
|
74
|
+
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
75
|
+
toggle() {
|
|
76
|
+
this.#setVisible(!this.#isVisible);
|
|
77
|
+
}
|
|
78
|
+
/** Whether the input is currently revealed (`type="text"`). */
|
|
79
|
+
get #isVisible() {
|
|
80
|
+
return this.hasInputTarget && this.inputTarget.type === "text";
|
|
81
|
+
}
|
|
82
|
+
/** Switches the masked/revealed state, preserving focus and caret. */
|
|
83
|
+
#setVisible(visible) {
|
|
84
|
+
if (!this.hasInputTarget) return;
|
|
85
|
+
const input = this.inputTarget;
|
|
86
|
+
const restoreInputFocus = document.activeElement === input;
|
|
87
|
+
const selectionStart = input.selectionStart;
|
|
88
|
+
const selectionEnd = input.selectionEnd;
|
|
89
|
+
input.type = visible ? "text" : "password";
|
|
90
|
+
if (restoreInputFocus) {
|
|
91
|
+
input.focus();
|
|
92
|
+
if (selectionStart !== null && selectionEnd !== null) {
|
|
93
|
+
try {
|
|
94
|
+
input.setSelectionRange(selectionStart, selectionEnd);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.#reflect(visible);
|
|
100
|
+
this.dispatch("toggle", { detail: { visible } });
|
|
101
|
+
this.#timers.clearAll();
|
|
102
|
+
if (visible && this.autoHideValue > 0) {
|
|
103
|
+
this.#timers.set(() => this.#setVisible(false), this.autoHideValue);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Reflects the visible state onto `aria-pressed` and `data-state`. */
|
|
107
|
+
#reflect(visible) {
|
|
108
|
+
if (this.hasToggleTarget) {
|
|
109
|
+
this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
110
|
+
}
|
|
111
|
+
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export { PasswordRevealController };
|
|
116
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
117
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":";;;;;AAwBA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;AC5EO,IAAM,wBAAA,GAAN,cAAuC,UAAA,CAAwB;AAAA,EACpE,OAAgB,OAAA,GAAU,CAAC,OAAA,EAAS,QAAQ,CAAA;AAAA,EAC5C,OAAgB,MAAA,GAAS;AAAA,IACvB,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GACvC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,QAAQ,CAAA;AAAA,EAC1B,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAUzB,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA,EAEjB,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,UAAU,CAAA;AAAA,EAC/B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AAAA,EACxB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,WAAA,CAAY,CAAC,IAAA,CAAK,UAAU,CAAA;AAAA,EACnC;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,WAAA,CAAY,IAAA,KAAS,MAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,YAAY,OAAA,EAAwB;AAClC,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAC1B,IAAA,MAAM,QAAQ,IAAA,CAAK,WAAA;AAInB,IAAA,MAAM,iBAAA,GAAoB,SAAS,aAAA,KAAkB,KAAA;AACrD,IAAA,MAAM,iBAAiB,KAAA,CAAM,cAAA;AAC7B,IAAA,MAAM,eAAe,KAAA,CAAM,YAAA;AAE3B,IAAA,KAAA,CAAM,IAAA,GAAO,UAAU,MAAA,GAAS,UAAA;AAEhC,IAAA,IAAI,iBAAA,EAAmB;AACrB,MAAA,KAAA,CAAM,KAAA,EAAM;AAGZ,MAAA,IAAI,cAAA,KAAmB,IAAA,IAAQ,YAAA,KAAiB,IAAA,EAAM;AACpD,QAAA,IAAI;AACF,UAAA,KAAA,CAAM,iBAAA,CAAkB,gBAAgB,YAAY,CAAA;AAAA,QACtD,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AACrB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,OAAA,IAAW,CAAA;AAE/C,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAI,OAAA,IAAW,IAAA,CAAK,aAAA,GAAgB,CAAA,EAAG;AACrC,MAAA,IAAA,CAAK,OAAA,CAAQ,IAAI,MAAM,IAAA,CAAK,YAAY,KAAK,CAAA,EAAG,KAAK,aAAa,CAAA;AAAA,IACpE;AAAA,EACF;AAAA;AAAA,EAGA,SAAS,OAAA,EAAwB;AAC/B,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,IAAA,CAAK,YAAA,CAAa,YAAA,CAAa,cAAA,EAAgB,OAAA,GAAU,SAAS,OAAO,CAAA;AAAA,IAC3E;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,OAAA,GAAU,YAAY,QAAQ,CAAA;AAAA,EACxE;AACF","file":"password_reveal_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button. An optional `autoHide` re-masks\n * after a delay, and that timer is torn down on disconnect (Turbo).\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n override connect(): void {\n this.#reflect(this.#isVisible);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n\n this.#timers.clearAll();\n if (visible && this.autoHideValue > 0) {\n this.#timers.set(() => this.#setVisible(false), this.autoHideValue);\n }\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible **two-thumb range slider** behavior (APG Slider —
|
|
5
|
+
* Multi-Thumb). A derivative of {@link SliderController}: it manages two thumbs
|
|
6
|
+
* (`start` ≤ `end`) that constrain each other.
|
|
7
|
+
*
|
|
8
|
+
* Markup contract (identifier: `stimeo--range-slider`):
|
|
9
|
+
* <div data-controller="stimeo--range-slider"
|
|
10
|
+
* data-stimeo--range-slider-min-value="0"
|
|
11
|
+
* data-stimeo--range-slider-max-value="100"
|
|
12
|
+
* data-stimeo--range-slider-step-value="1"
|
|
13
|
+
* data-stimeo--range-slider-start-value="20"
|
|
14
|
+
* data-stimeo--range-slider-end-value="80">
|
|
15
|
+
* <div data-stimeo--range-slider-target="track"
|
|
16
|
+
* data-action="pointerdown->stimeo--range-slider#onPointerDown">
|
|
17
|
+
* <div role="slider" tabindex="0" aria-label="Minimum"
|
|
18
|
+
* data-stimeo--range-slider-target="startThumb"
|
|
19
|
+
* data-action="keydown->stimeo--range-slider#onKeydown"></div>
|
|
20
|
+
* <div role="slider" tabindex="0" aria-label="Maximum"
|
|
21
|
+
* data-stimeo--range-slider-target="endThumb"
|
|
22
|
+
* data-action="keydown->stimeo--range-slider#onKeydown"></div>
|
|
23
|
+
* </div>
|
|
24
|
+
* </div>
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Behavior only — the consumer owns all layout (positioning the thumbs and the
|
|
28
|
+
* selected range from the fractions). Only the horizontal orientation is handled
|
|
29
|
+
* in this MVP. Each thumb's movable range is bounded by the *other* thumb's
|
|
30
|
+
* current value, reflected on its `aria-valuemin`/`aria-valuemax` so assistive
|
|
31
|
+
* tech announces the live constraint.
|
|
32
|
+
*
|
|
33
|
+
* Behavior provided (per focused thumb):
|
|
34
|
+
* - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one
|
|
35
|
+
* step; `Home`/`End` jump to that thumb's movable min/max; `PageUp`/`PageDown`
|
|
36
|
+
* move by ten steps. A thumb never crosses the other.
|
|
37
|
+
* - Pointer press/drag on the track moves the nearest thumb.
|
|
38
|
+
*/
|
|
39
|
+
declare class RangeSliderController extends Controller<HTMLElement> {
|
|
40
|
+
#private;
|
|
41
|
+
static targets: string[];
|
|
42
|
+
static values: {
|
|
43
|
+
min: {
|
|
44
|
+
type: NumberConstructor;
|
|
45
|
+
default: number;
|
|
46
|
+
};
|
|
47
|
+
max: {
|
|
48
|
+
type: NumberConstructor;
|
|
49
|
+
default: number;
|
|
50
|
+
};
|
|
51
|
+
step: {
|
|
52
|
+
type: NumberConstructor;
|
|
53
|
+
default: number;
|
|
54
|
+
};
|
|
55
|
+
start: {
|
|
56
|
+
type: NumberConstructor;
|
|
57
|
+
default: number;
|
|
58
|
+
};
|
|
59
|
+
end: {
|
|
60
|
+
type: NumberConstructor;
|
|
61
|
+
default: number;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
static actions: readonly ["onKeydown", "onPointerDown"];
|
|
65
|
+
static events: readonly ["change"];
|
|
66
|
+
readonly trackTarget: HTMLElement;
|
|
67
|
+
readonly startThumbTarget: HTMLElement;
|
|
68
|
+
readonly endThumbTarget: HTMLElement;
|
|
69
|
+
readonly hasTrackTarget: boolean;
|
|
70
|
+
readonly hasStartThumbTarget: boolean;
|
|
71
|
+
readonly hasEndThumbTarget: boolean;
|
|
72
|
+
minValue: number;
|
|
73
|
+
maxValue: number;
|
|
74
|
+
stepValue: number;
|
|
75
|
+
startValue: number;
|
|
76
|
+
endValue: number;
|
|
77
|
+
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
78
|
+
connect(): void;
|
|
79
|
+
/** Cancels any active pointer drag so document listeners never leak. */
|
|
80
|
+
disconnect(): void;
|
|
81
|
+
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
82
|
+
onKeydown(event: KeyboardEvent): void;
|
|
83
|
+
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
84
|
+
onPointerDown(event: PointerEvent): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { RangeSliderController };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/range_slider_controller.ts
|
|
4
|
+
var START_PROPERTY = "--stimeo-range-start";
|
|
5
|
+
var END_PROPERTY = "--stimeo-range-end";
|
|
6
|
+
var RangeSliderController = class extends Controller {
|
|
7
|
+
static targets = ["track", "startThumb", "endThumb"];
|
|
8
|
+
static values = {
|
|
9
|
+
min: { type: Number, default: 0 },
|
|
10
|
+
max: { type: Number, default: 100 },
|
|
11
|
+
step: { type: Number, default: 1 },
|
|
12
|
+
start: { type: Number, default: 0 },
|
|
13
|
+
end: { type: Number, default: 100 }
|
|
14
|
+
};
|
|
15
|
+
static actions = ["onKeydown", "onPointerDown"];
|
|
16
|
+
static events = ["change"];
|
|
17
|
+
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
18
|
+
#dragAbort = null;
|
|
19
|
+
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
20
|
+
connect() {
|
|
21
|
+
const lo = Math.min(this.startValue, this.endValue);
|
|
22
|
+
const hi = Math.max(this.startValue, this.endValue);
|
|
23
|
+
this.#commit(lo, hi, false);
|
|
24
|
+
}
|
|
25
|
+
/** Cancels any active pointer drag so document listeners never leak. */
|
|
26
|
+
disconnect() {
|
|
27
|
+
this.#dragAbort?.abort();
|
|
28
|
+
this.#dragAbort = null;
|
|
29
|
+
}
|
|
30
|
+
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
31
|
+
onKeydown(event) {
|
|
32
|
+
const thumb = event.currentTarget;
|
|
33
|
+
const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
|
|
34
|
+
const current = isStart ? this.startValue : this.endValue;
|
|
35
|
+
const lower = isStart ? this.minValue : this.startValue;
|
|
36
|
+
const upper = isStart ? this.endValue : this.maxValue;
|
|
37
|
+
const big = this.stepValue * 10;
|
|
38
|
+
let next = null;
|
|
39
|
+
switch (event.key) {
|
|
40
|
+
case "ArrowRight":
|
|
41
|
+
case "ArrowUp":
|
|
42
|
+
next = current + this.stepValue;
|
|
43
|
+
break;
|
|
44
|
+
case "ArrowLeft":
|
|
45
|
+
case "ArrowDown":
|
|
46
|
+
next = current - this.stepValue;
|
|
47
|
+
break;
|
|
48
|
+
case "PageUp":
|
|
49
|
+
next = current + big;
|
|
50
|
+
break;
|
|
51
|
+
case "PageDown":
|
|
52
|
+
next = current - big;
|
|
53
|
+
break;
|
|
54
|
+
case "Home":
|
|
55
|
+
next = lower;
|
|
56
|
+
break;
|
|
57
|
+
case "End":
|
|
58
|
+
next = upper;
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
this.#moveThumb(isStart, next);
|
|
65
|
+
}
|
|
66
|
+
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
67
|
+
onPointerDown(event) {
|
|
68
|
+
if (!this.hasTrackTarget) return;
|
|
69
|
+
const value = this.#valueFromClientX(event.clientX);
|
|
70
|
+
if (value === null) return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
const useStart = Math.abs(value - this.startValue) <= Math.abs(value - this.endValue);
|
|
73
|
+
if (useStart) {
|
|
74
|
+
if (this.hasStartThumbTarget) this.startThumbTarget.focus();
|
|
75
|
+
} else if (this.hasEndThumbTarget) {
|
|
76
|
+
this.endThumbTarget.focus();
|
|
77
|
+
}
|
|
78
|
+
this.#moveThumb(useStart, value);
|
|
79
|
+
this.#dragAbort?.abort();
|
|
80
|
+
const abort = new AbortController();
|
|
81
|
+
this.#dragAbort = abort;
|
|
82
|
+
const onMove = (move) => {
|
|
83
|
+
const moved = this.#valueFromClientX(move.clientX);
|
|
84
|
+
if (moved !== null) this.#moveThumb(useStart, moved);
|
|
85
|
+
};
|
|
86
|
+
const onUp = () => {
|
|
87
|
+
abort.abort();
|
|
88
|
+
this.#dragAbort = null;
|
|
89
|
+
};
|
|
90
|
+
document.addEventListener("pointermove", onMove, { signal: abort.signal });
|
|
91
|
+
document.addEventListener("pointerup", onUp, { signal: abort.signal });
|
|
92
|
+
document.addEventListener("pointercancel", onUp, { signal: abort.signal });
|
|
93
|
+
}
|
|
94
|
+
/** Maps a pointer X coordinate to a raw value using the track geometry. */
|
|
95
|
+
#valueFromClientX(clientX) {
|
|
96
|
+
const rect = this.trackTarget.getBoundingClientRect();
|
|
97
|
+
if (rect.width === 0) return null;
|
|
98
|
+
const fraction = (clientX - rect.left) / rect.width;
|
|
99
|
+
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
100
|
+
}
|
|
101
|
+
/** Moves one thumb to a new raw value, keeping the pair ordered. */
|
|
102
|
+
#moveThumb(isStart, raw) {
|
|
103
|
+
if (isStart) {
|
|
104
|
+
this.#commit(raw, this.endValue, true);
|
|
105
|
+
} else {
|
|
106
|
+
this.#commit(this.startValue, raw, true);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
|
|
111
|
+
* reflects it onto the thumbs' ARIA attributes and the range custom
|
|
112
|
+
* properties. Dispatches `change` only on user-driven updates (`notify`).
|
|
113
|
+
*/
|
|
114
|
+
#commit(start, end, notify) {
|
|
115
|
+
const prevStart = this.startValue;
|
|
116
|
+
const prevEnd = this.endValue;
|
|
117
|
+
let nextStart = this.#snap(start);
|
|
118
|
+
let nextEnd = this.#snap(end);
|
|
119
|
+
if (nextStart > nextEnd) {
|
|
120
|
+
if (isUserMovingStart(start, prevStart, end, prevEnd)) nextStart = nextEnd;
|
|
121
|
+
else nextEnd = nextStart;
|
|
122
|
+
}
|
|
123
|
+
this.startValue = nextStart;
|
|
124
|
+
this.endValue = nextEnd;
|
|
125
|
+
this.#render(nextStart, nextEnd);
|
|
126
|
+
if (notify && (nextStart !== prevStart || nextEnd !== prevEnd)) {
|
|
127
|
+
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Reflects the current pair onto thumb ARIA attributes and CSS properties. */
|
|
131
|
+
#render(start, end) {
|
|
132
|
+
if (this.hasStartThumbTarget) {
|
|
133
|
+
this.startThumbTarget.setAttribute("aria-valuemin", String(this.minValue));
|
|
134
|
+
this.startThumbTarget.setAttribute("aria-valuemax", String(end));
|
|
135
|
+
this.startThumbTarget.setAttribute("aria-valuenow", String(start));
|
|
136
|
+
}
|
|
137
|
+
if (this.hasEndThumbTarget) {
|
|
138
|
+
this.endThumbTarget.setAttribute("aria-valuemin", String(start));
|
|
139
|
+
this.endThumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
140
|
+
this.endThumbTarget.setAttribute("aria-valuenow", String(end));
|
|
141
|
+
}
|
|
142
|
+
const span = this.maxValue - this.minValue;
|
|
143
|
+
this.element.style.setProperty(
|
|
144
|
+
START_PROPERTY,
|
|
145
|
+
String(span > 0 ? (start - this.minValue) / span : 0)
|
|
146
|
+
);
|
|
147
|
+
this.element.style.setProperty(
|
|
148
|
+
END_PROPERTY,
|
|
149
|
+
String(span > 0 ? (end - this.minValue) / span : 0)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
/** Clamps `raw` to `[min, max]` and snaps it to the nearest step from `min`. */
|
|
153
|
+
#snap(raw) {
|
|
154
|
+
const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));
|
|
155
|
+
if (this.stepValue <= 0) return clamped;
|
|
156
|
+
const stepped = Math.round((clamped - this.minValue) / this.stepValue) * this.stepValue + this.minValue;
|
|
157
|
+
return Math.min(this.maxValue, Math.max(this.minValue, stepped));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function isUserMovingStart(start, prevStart, end, prevEnd) {
|
|
161
|
+
return start !== prevStart && end === prevEnd;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { RangeSliderController };
|
|
165
|
+
//# sourceMappingURL=range_slider_controller.js.map
|
|
166
|
+
//# sourceMappingURL=range_slider_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/range_slider_controller.ts"],"names":[],"mappings":"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","file":"range_slider_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** CSS custom properties exposing each thumb's position (0..1) to the consumer. */\nconst START_PROPERTY = \"--stimeo-range-start\";\nconst END_PROPERTY = \"--stimeo-range-end\";\n\n/**\n * Headless, accessible **two-thumb range slider** behavior (APG Slider —\n * Multi-Thumb). A derivative of {@link SliderController}: it manages two thumbs\n * (`start` ≤ `end`) that constrain each other.\n *\n * Markup contract (identifier: `stimeo--range-slider`):\n * <div data-controller=\"stimeo--range-slider\"\n * data-stimeo--range-slider-min-value=\"0\"\n * data-stimeo--range-slider-max-value=\"100\"\n * data-stimeo--range-slider-step-value=\"1\"\n * data-stimeo--range-slider-start-value=\"20\"\n * data-stimeo--range-slider-end-value=\"80\">\n * <div data-stimeo--range-slider-target=\"track\"\n * data-action=\"pointerdown->stimeo--range-slider#onPointerDown\">\n * <div role=\"slider\" tabindex=\"0\" aria-label=\"Minimum\"\n * data-stimeo--range-slider-target=\"startThumb\"\n * data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n * <div role=\"slider\" tabindex=\"0\" aria-label=\"Maximum\"\n * data-stimeo--range-slider-target=\"endThumb\"\n * data-action=\"keydown->stimeo--range-slider#onKeydown\"></div>\n * </div>\n * </div>\n *\n * @remarks\n * Behavior only — the consumer owns all layout (positioning the thumbs and the\n * selected range from the fractions). Only the horizontal orientation is handled\n * in this MVP. Each thumb's movable range is bounded by the *other* thumb's\n * current value, reflected on its `aria-valuemin`/`aria-valuemax` so assistive\n * tech announces the live constraint.\n *\n * Behavior provided (per focused thumb):\n * - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one\n * step; `Home`/`End` jump to that thumb's movable min/max; `PageUp`/`PageDown`\n * move by ten steps. A thumb never crosses the other.\n * - Pointer press/drag on the track moves the nearest thumb.\n */\nexport class RangeSliderController extends Controller<HTMLElement> {\n static override targets = [\"track\", \"startThumb\", \"endThumb\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n start: { type: Number, default: 0 },\n end: { type: Number, default: 100 },\n };\n static actions = [\"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly trackTarget: HTMLElement;\n declare readonly startThumbTarget: HTMLElement;\n declare readonly endThumbTarget: HTMLElement;\n declare readonly hasTrackTarget: boolean;\n declare readonly hasStartThumbTarget: boolean;\n declare readonly hasEndThumbTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare startValue: number;\n declare endValue: number;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** Normalizes the initial pair (clamped, snapped, ordered) and renders. */\n override connect(): void {\n // Order the initial pair up front so a reversed start/end is corrected by\n // swapping (preserving both values) rather than collapsing one onto the other.\n const lo = Math.min(this.startValue, this.endValue);\n const hi = Math.max(this.startValue, this.endValue);\n this.#commit(lo, hi, false);\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Keyboard stepping for whichever thumb is focused (the action's element). */\n onKeydown(event: KeyboardEvent): void {\n const thumb = event.currentTarget as HTMLElement;\n const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;\n const current = isStart ? this.startValue : this.endValue;\n // The opposing thumb caps this one's travel so the two never cross.\n const lower = isStart ? this.minValue : this.startValue;\n const upper = isStart ? this.endValue : this.maxValue;\n const big = this.stepValue * 10;\n\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = current + this.stepValue;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = current - this.stepValue;\n break;\n case \"PageUp\":\n next = current + big;\n break;\n case \"PageDown\":\n next = current - big;\n break;\n case \"Home\":\n next = lower;\n break;\n case \"End\":\n next = upper;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#moveThumb(isStart, next);\n }\n\n /** Begins a pointer drag on the track, moving the thumb nearest the press. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasTrackTarget) return;\n const value = this.#valueFromClientX(event.clientX);\n if (value === null) return;\n event.preventDefault();\n\n // Pick the nearer thumb; ties go to the start thumb so a press exactly\n // between them stays deterministic. Only read the target getter behind its\n // has*Target guard — Stimulus throws when an absent target is accessed.\n const useStart = Math.abs(value - this.startValue) <= Math.abs(value - this.endValue);\n if (useStart) {\n if (this.hasStartThumbTarget) this.startThumbTarget.focus();\n } else if (this.hasEndThumbTarget) {\n this.endThumbTarget.focus();\n }\n this.#moveThumb(useStart, value);\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => {\n const moved = this.#valueFromClientX(move.clientX);\n if (moved !== null) this.#moveThumb(useStart, moved);\n };\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n // pointercancel fires when the gesture is interrupted (OS gesture, scroll\n // takeover, device switch); clean up the same way so no listener leaks.\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Maps a pointer X coordinate to a raw value using the track geometry. */\n #valueFromClientX(clientX: number): number | null {\n const rect = this.trackTarget.getBoundingClientRect();\n if (rect.width === 0) return null;\n const fraction = (clientX - rect.left) / rect.width;\n return this.minValue + fraction * (this.maxValue - this.minValue);\n }\n\n /** Moves one thumb to a new raw value, keeping the pair ordered. */\n #moveThumb(isStart: boolean, raw: number): void {\n if (isStart) {\n this.#commit(raw, this.endValue, true);\n } else {\n this.#commit(this.startValue, raw, true);\n }\n }\n\n /**\n * Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and\n * reflects it onto the thumbs' ARIA attributes and the range custom\n * properties. Dispatches `change` only on user-driven updates (`notify`).\n */\n #commit(start: number, end: number, notify: boolean): void {\n const prevStart = this.startValue;\n const prevEnd = this.endValue;\n\n let nextStart = this.#snap(start);\n let nextEnd = this.#snap(end);\n // Keep them ordered: a thumb pushed past its partner stops at the partner.\n if (nextStart > nextEnd) {\n if (isUserMovingStart(start, prevStart, end, prevEnd)) nextStart = nextEnd;\n else nextEnd = nextStart;\n }\n\n this.startValue = nextStart;\n this.endValue = nextEnd;\n this.#render(nextStart, nextEnd);\n\n if (notify && (nextStart !== prevStart || nextEnd !== prevEnd)) {\n this.dispatch(\"change\", { detail: { start: nextStart, end: nextEnd } });\n }\n }\n\n /** Reflects the current pair onto thumb ARIA attributes and CSS properties. */\n #render(start: number, end: number): void {\n if (this.hasStartThumbTarget) {\n this.startThumbTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.startThumbTarget.setAttribute(\"aria-valuemax\", String(end));\n this.startThumbTarget.setAttribute(\"aria-valuenow\", String(start));\n }\n if (this.hasEndThumbTarget) {\n this.endThumbTarget.setAttribute(\"aria-valuemin\", String(start));\n this.endThumbTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n this.endThumbTarget.setAttribute(\"aria-valuenow\", String(end));\n }\n const span = this.maxValue - this.minValue;\n this.element.style.setProperty(\n START_PROPERTY,\n String(span > 0 ? (start - this.minValue) / span : 0),\n );\n this.element.style.setProperty(\n END_PROPERTY,\n String(span > 0 ? (end - this.minValue) / span : 0),\n );\n }\n\n /** Clamps `raw` to `[min, max]` and snaps it to the nearest step from `min`. */\n #snap(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const stepped =\n Math.round((clamped - this.minValue) / this.stepValue) * this.stepValue + this.minValue;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n\n/**\n * Decides which thumb yields when a move would cross the pair. The moving thumb\n * is the one whose requested value differs from its previous value; it stops at\n * the partner rather than dragging the partner along.\n */\nfunction isUserMovingStart(\n start: number,\n prevStart: number,\n end: number,\n prevEnd: number,\n): boolean {\n return start !== prevStart && end === prevEnd;\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless "read more / truncate" behavior for visually clamped text.
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--read-more`):
|
|
7
|
+
* <div data-controller="stimeo--read-more">
|
|
8
|
+
* <p id="bio" data-stimeo--read-more-target="content" data-state="collapsed">…</p>
|
|
9
|
+
* <button data-stimeo--read-more-target="trigger"
|
|
10
|
+
* data-action="stimeo--read-more#toggle"
|
|
11
|
+
* aria-expanded="false" aria-controls="bio" hidden>Read more</button>
|
|
12
|
+
* </div>
|
|
13
|
+
*
|
|
14
|
+
* There is no dedicated APG widget; the toggle borrows the **Disclosure**
|
|
15
|
+
* convention (`aria-expanded`). The visual clamp itself (`-webkit-line-clamp`
|
|
16
|
+
* etc.) is the consumer's CSS, keyed off `data-state`.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Behavior only. The full text always stays in the DOM — the clamp is purely
|
|
20
|
+
* visual, so assistive technology reads everything regardless of state; here
|
|
21
|
+
* `aria-expanded` therefore signals the *visual* expansion, not content hidden
|
|
22
|
+
* from AT. The controller's extra job is **overflow detection**: when the
|
|
23
|
+
* content is not actually clamped (it fits), the toggle is `hidden` so no
|
|
24
|
+
* pointless "read more" is offered. Resizes, content changes, and target
|
|
25
|
+
* replacement re-evaluate it; hiding a focused toggle waits until blur.
|
|
26
|
+
*/
|
|
27
|
+
declare class ReadMoreController extends Controller<HTMLElement> {
|
|
28
|
+
#private;
|
|
29
|
+
static targets: string[];
|
|
30
|
+
static values: {
|
|
31
|
+
collapsed: {
|
|
32
|
+
type: BooleanConstructor;
|
|
33
|
+
default: boolean;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
static actions: readonly ["toggle"];
|
|
37
|
+
readonly contentTarget: HTMLElement;
|
|
38
|
+
readonly triggerTarget: HTMLElement;
|
|
39
|
+
readonly hasContentTarget: boolean;
|
|
40
|
+
readonly hasTriggerTarget: boolean;
|
|
41
|
+
collapsedValue: boolean;
|
|
42
|
+
connect(): void;
|
|
43
|
+
disconnect(): void;
|
|
44
|
+
contentTargetConnected(): void;
|
|
45
|
+
contentTargetDisconnected(): void;
|
|
46
|
+
triggerTargetConnected(): void;
|
|
47
|
+
triggerTargetDisconnected(trigger: HTMLElement): void;
|
|
48
|
+
/** Toggles between the collapsed (clamped) and expanded states. */
|
|
49
|
+
toggle(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ReadMoreController };
|