stimeo-ui 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/controllers/accordion_controller.d.ts +59 -0
- package/dist/controllers/accordion_controller.js +76 -0
- package/dist/controllers/accordion_controller.js.map +1 -0
- package/dist/controllers/announcer_controller.d.ts +78 -0
- package/dist/controllers/announcer_controller.js +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -0
- package/dist/controllers/aspect_ratio_controller.d.ts +39 -0
- package/dist/controllers/aspect_ratio_controller.js +36 -0
- package/dist/controllers/aspect_ratio_controller.js.map +1 -0
- package/dist/controllers/auto_submit_controller.d.ts +74 -0
- package/dist/controllers/auto_submit_controller.js +147 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -0
- package/dist/controllers/avatar_controller.d.ts +50 -0
- package/dist/controllers/avatar_controller.js +66 -0
- package/dist/controllers/avatar_controller.js.map +1 -0
- package/dist/controllers/breadcrumb_controller.d.ts +60 -0
- package/dist/controllers/breadcrumb_controller.js +123 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -0
- package/dist/controllers/bulk_select_controller.d.ts +67 -0
- package/dist/controllers/bulk_select_controller.js +104 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -0
- package/dist/controllers/calendar_controller.d.ts +100 -0
- package/dist/controllers/calendar_controller.js +394 -0
- package/dist/controllers/calendar_controller.js.map +1 -0
- package/dist/controllers/character_counter_controller.d.ts +63 -0
- package/dist/controllers/character_counter_controller.js +179 -0
- package/dist/controllers/character_counter_controller.js.map +1 -0
- package/dist/controllers/checkbox_controller.d.ts +52 -0
- package/dist/controllers/checkbox_controller.js +73 -0
- package/dist/controllers/checkbox_controller.js.map +1 -0
- package/dist/controllers/combobox_controller.d.ts +76 -0
- package/dist/controllers/combobox_controller.js +186 -0
- package/dist/controllers/combobox_controller.js.map +1 -0
- package/dist/controllers/command_palette_controller.d.ts +103 -0
- package/dist/controllers/command_palette_controller.js +381 -0
- package/dist/controllers/command_palette_controller.js.map +1 -0
- package/dist/controllers/conditional_fields_controller.d.ts +56 -0
- package/dist/controllers/conditional_fields_controller.js +112 -0
- package/dist/controllers/conditional_fields_controller.js.map +1 -0
- package/dist/controllers/confirm_controller.d.ts +83 -0
- package/dist/controllers/confirm_controller.js +276 -0
- package/dist/controllers/confirm_controller.js.map +1 -0
- package/dist/controllers/context_menu_controller.d.ts +81 -0
- package/dist/controllers/context_menu_controller.js +112 -0
- package/dist/controllers/context_menu_controller.js.map +1 -0
- package/dist/controllers/countdown_controller.d.ts +93 -0
- package/dist/controllers/countdown_controller.js +202 -0
- package/dist/controllers/countdown_controller.js.map +1 -0
- package/dist/controllers/dialog_controller.d.ts +57 -0
- package/dist/controllers/dialog_controller.js +207 -0
- package/dist/controllers/dialog_controller.js.map +1 -0
- package/dist/controllers/direct_upload_controller.d.ts +77 -0
- package/dist/controllers/direct_upload_controller.js +212 -0
- package/dist/controllers/direct_upload_controller.js.map +1 -0
- package/dist/controllers/dirty_form_controller.d.ts +52 -0
- package/dist/controllers/dirty_form_controller.js +128 -0
- package/dist/controllers/dirty_form_controller.js.map +1 -0
- package/dist/controllers/dropdown_controller.d.ts +49 -0
- package/dist/controllers/dropdown_controller.js +66 -0
- package/dist/controllers/dropdown_controller.js.map +1 -0
- package/dist/controllers/empty_state_controller.d.ts +60 -0
- package/dist/controllers/empty_state_controller.js +67 -0
- package/dist/controllers/empty_state_controller.js.map +1 -0
- package/dist/controllers/flash_controller.d.ts +63 -0
- package/dist/controllers/flash_controller.js +221 -0
- package/dist/controllers/flash_controller.js.map +1 -0
- package/dist/controllers/focus_controller.d.ts +69 -0
- package/dist/controllers/focus_controller.js +216 -0
- package/dist/controllers/focus_controller.js.map +1 -0
- package/dist/controllers/form_field_controller.d.ts +79 -0
- package/dist/controllers/form_field_controller.js +154 -0
- package/dist/controllers/form_field_controller.js.map +1 -0
- package/dist/controllers/form_validation_controller.d.ts +114 -0
- package/dist/controllers/form_validation_controller.js +202 -0
- package/dist/controllers/form_validation_controller.js.map +1 -0
- package/dist/controllers/frame_loading_controller.d.ts +58 -0
- package/dist/controllers/frame_loading_controller.js +177 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -0
- package/dist/controllers/highlight_controller.d.ts +50 -0
- package/dist/controllers/highlight_controller.js +107 -0
- package/dist/controllers/highlight_controller.js.map +1 -0
- package/dist/controllers/hover_card_controller.d.ts +79 -0
- package/dist/controllers/hover_card_controller.js +165 -0
- package/dist/controllers/hover_card_controller.js.map +1 -0
- package/dist/controllers/idle_controller.d.ts +51 -0
- package/dist/controllers/idle_controller.js +141 -0
- package/dist/controllers/idle_controller.js.map +1 -0
- package/dist/controllers/input_mask_controller.d.ts +74 -0
- package/dist/controllers/input_mask_controller.js +166 -0
- package/dist/controllers/input_mask_controller.js.map +1 -0
- package/dist/controllers/lazy_frame_controller.d.ts +53 -0
- package/dist/controllers/lazy_frame_controller.js +68 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -0
- package/dist/controllers/listbox_controller.d.ts +88 -0
- package/dist/controllers/listbox_controller.js +256 -0
- package/dist/controllers/listbox_controller.js.map +1 -0
- package/dist/controllers/local_time_controller.d.ts +60 -0
- package/dist/controllers/local_time_controller.js +81 -0
- package/dist/controllers/local_time_controller.js.map +1 -0
- package/dist/controllers/menu_controller.d.ts +81 -0
- package/dist/controllers/menu_controller.js +134 -0
- package/dist/controllers/menu_controller.js.map +1 -0
- package/dist/controllers/meter_controller.d.ts +93 -0
- package/dist/controllers/meter_controller.js +96 -0
- package/dist/controllers/meter_controller.js.map +1 -0
- package/dist/controllers/nested_form_controller.d.ts +85 -0
- package/dist/controllers/nested_form_controller.js +131 -0
- package/dist/controllers/nested_form_controller.js.map +1 -0
- package/dist/controllers/network_status_controller.d.ts +49 -0
- package/dist/controllers/network_status_controller.js +126 -0
- package/dist/controllers/network_status_controller.js.map +1 -0
- package/dist/controllers/number_input_controller.d.ts +93 -0
- package/dist/controllers/number_input_controller.js +306 -0
- package/dist/controllers/number_input_controller.js.map +1 -0
- package/dist/controllers/otp_controller.d.ts +68 -0
- package/dist/controllers/otp_controller.js +201 -0
- package/dist/controllers/otp_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +62 -0
- package/dist/controllers/overflow_indicator_controller.js +169 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -0
- package/dist/controllers/overflow_menu_controller.d.ts +64 -0
- package/dist/controllers/overflow_menu_controller.js +274 -0
- package/dist/controllers/overflow_menu_controller.js.map +1 -0
- package/dist/controllers/pagination_controller.d.ts +69 -0
- package/dist/controllers/pagination_controller.js +89 -0
- package/dist/controllers/pagination_controller.js.map +1 -0
- package/dist/controllers/password_strength_controller.d.ts +67 -0
- package/dist/controllers/password_strength_controller.js +175 -0
- package/dist/controllers/password_strength_controller.js.map +1 -0
- package/dist/controllers/persist_controller.d.ts +63 -0
- package/dist/controllers/persist_controller.js +259 -0
- package/dist/controllers/persist_controller.js.map +1 -0
- package/dist/controllers/popover_controller.d.ts +63 -0
- package/dist/controllers/popover_controller.js +94 -0
- package/dist/controllers/popover_controller.js.map +1 -0
- package/dist/controllers/portal_controller.d.ts +58 -0
- package/dist/controllers/portal_controller.js +63 -0
- package/dist/controllers/portal_controller.js.map +1 -0
- package/dist/controllers/preview_guard_controller.d.ts +47 -0
- package/dist/controllers/preview_guard_controller.js +69 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -0
- package/dist/controllers/progress_controller.d.ts +85 -0
- package/dist/controllers/progress_controller.js +93 -0
- package/dist/controllers/progress_controller.js.map +1 -0
- package/dist/controllers/radio_group_controller.d.ts +49 -0
- package/dist/controllers/radio_group_controller.js +128 -0
- package/dist/controllers/radio_group_controller.js.map +1 -0
- package/dist/controllers/rating_controller.d.ts +82 -0
- package/dist/controllers/rating_controller.js +179 -0
- package/dist/controllers/rating_controller.js.map +1 -0
- package/dist/controllers/relative_time_controller.d.ts +48 -0
- package/dist/controllers/relative_time_controller.js +129 -0
- package/dist/controllers/relative_time_controller.js.map +1 -0
- package/dist/controllers/reset_before_cache_controller.d.ts +54 -0
- package/dist/controllers/reset_before_cache_controller.js +62 -0
- package/dist/controllers/reset_before_cache_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +81 -0
- package/dist/controllers/resizable_controller.js +163 -0
- package/dist/controllers/resizable_controller.js.map +1 -0
- package/dist/controllers/roving_controller.d.ts +63 -0
- package/dist/controllers/roving_controller.js +116 -0
- package/dist/controllers/roving_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.d.ts +45 -0
- package/dist/controllers/scroll_area_controller.js +183 -0
- package/dist/controllers/scroll_area_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.d.ts +72 -0
- package/dist/controllers/scroll_visibility_controller.js +103 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -0
- package/dist/controllers/scrollspy_controller.d.ts +65 -0
- package/dist/controllers/scrollspy_controller.js +171 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -0
- package/dist/controllers/skeleton_controller.d.ts +50 -0
- package/dist/controllers/skeleton_controller.js +125 -0
- package/dist/controllers/skeleton_controller.js.map +1 -0
- package/dist/controllers/slider_controller.d.ts +76 -0
- package/dist/controllers/slider_controller.js +109 -0
- package/dist/controllers/slider_controller.js.map +1 -0
- package/dist/controllers/spinner_controller.d.ts +62 -0
- package/dist/controllers/spinner_controller.js +164 -0
- package/dist/controllers/spinner_controller.js.map +1 -0
- package/dist/controllers/step_indicator_controller.d.ts +55 -0
- package/dist/controllers/step_indicator_controller.js +55 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +66 -0
- package/dist/controllers/stepper_controller.js +78 -0
- package/dist/controllers/stepper_controller.js.map +1 -0
- package/dist/controllers/stick_to_bottom_controller.d.ts +61 -0
- package/dist/controllers/stick_to_bottom_controller.js +100 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -0
- package/dist/controllers/sticky_observer_controller.d.ts +54 -0
- package/dist/controllers/sticky_observer_controller.js +53 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -0
- package/dist/controllers/submit_once_controller.d.ts +66 -0
- package/dist/controllers/submit_once_controller.js +206 -0
- package/dist/controllers/submit_once_controller.js.map +1 -0
- package/dist/controllers/switch_controller.d.ts +43 -0
- package/dist/controllers/switch_controller.js +50 -0
- package/dist/controllers/switch_controller.js.map +1 -0
- package/dist/controllers/tabs_controller.d.ts +48 -0
- package/dist/controllers/tabs_controller.js +63 -0
- package/dist/controllers/tabs_controller.js.map +1 -0
- package/dist/controllers/textarea_autosize_controller.d.ts +47 -0
- package/dist/controllers/textarea_autosize_controller.js +72 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -0
- package/dist/controllers/theme_controller.d.ts +70 -0
- package/dist/controllers/theme_controller.js +154 -0
- package/dist/controllers/theme_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +123 -0
- package/dist/controllers/toast_controller.js +310 -0
- package/dist/controllers/toast_controller.js.map +1 -0
- package/dist/controllers/toggle_group_controller.d.ts +56 -0
- package/dist/controllers/toggle_group_controller.js +130 -0
- package/dist/controllers/toggle_group_controller.js.map +1 -0
- package/dist/controllers/toolbar_controller.d.ts +59 -0
- package/dist/controllers/toolbar_controller.js +113 -0
- package/dist/controllers/toolbar_controller.js.map +1 -0
- package/dist/controllers/tooltip_controller.d.ts +74 -0
- package/dist/controllers/tooltip_controller.js +165 -0
- package/dist/controllers/tooltip_controller.js.map +1 -0
- package/dist/controllers/transition_controller.d.ts +86 -0
- package/dist/controllers/transition_controller.js +203 -0
- package/dist/controllers/transition_controller.js.map +1 -0
- package/dist/index.d.ts +1931 -0
- package/dist/index.js +12241 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector/cli.d.ts +149 -0
- package/dist/inspector/cli.js +663 -0
- package/dist/inspector/cli.js.map +1 -0
- package/dist/inspector/cli_bin.d.ts +2 -0
- package/dist/inspector/cli_bin.js +665 -0
- package/dist/inspector/cli_bin.js.map +1 -0
- package/dist/inspector/manifest.json +2235 -0
- package/dist/positioning/index.d.ts +226 -0
- package/dist/positioning/index.js +145 -0
- package/dist/positioning/index.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/overflow_menu_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/layout_observer.ts
|
|
6
|
+
var LayoutObserver = class {
|
|
7
|
+
#callback;
|
|
8
|
+
#resizeObserverFactory;
|
|
9
|
+
#resizeObserver = null;
|
|
10
|
+
#observingViewport = false;
|
|
11
|
+
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
12
|
+
#handleViewportResize = () => {
|
|
13
|
+
this.#callback();
|
|
14
|
+
};
|
|
15
|
+
constructor(callback, options = {}) {
|
|
16
|
+
this.#callback = callback;
|
|
17
|
+
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Starts observing an element's size. Repeated calls observe additional
|
|
21
|
+
* elements through the same shared observer. No-ops when no
|
|
22
|
+
* `ResizeObserver` implementation is available.
|
|
23
|
+
*/
|
|
24
|
+
observe(element) {
|
|
25
|
+
if (!this.#resizeObserverFactory) return;
|
|
26
|
+
if (!this.#resizeObserver) {
|
|
27
|
+
this.#resizeObserver = this.#resizeObserverFactory(() => {
|
|
28
|
+
this.#callback();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
this.#resizeObserver.observe(element);
|
|
32
|
+
}
|
|
33
|
+
/** Stops observing a single element while leaving any others in place. */
|
|
34
|
+
unobserve(element) {
|
|
35
|
+
this.#resizeObserver?.unobserve(element);
|
|
36
|
+
}
|
|
37
|
+
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
38
|
+
observeViewport() {
|
|
39
|
+
if (this.#observingViewport) return;
|
|
40
|
+
this.#observingViewport = true;
|
|
41
|
+
window.addEventListener("resize", this.#handleViewportResize);
|
|
42
|
+
}
|
|
43
|
+
/** Stops observing viewport resizes without affecting element observation. */
|
|
44
|
+
unobserveViewport() {
|
|
45
|
+
if (!this.#observingViewport) return;
|
|
46
|
+
this.#observingViewport = false;
|
|
47
|
+
window.removeEventListener("resize", this.#handleViewportResize);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Releases every observation: disconnects the {@link ResizeObserver} and
|
|
51
|
+
* removes the viewport listener. Safe to call multiple times. Call this from a
|
|
52
|
+
* controller's `disconnect()`.
|
|
53
|
+
*/
|
|
54
|
+
disconnect() {
|
|
55
|
+
this.#resizeObserver?.disconnect();
|
|
56
|
+
this.#resizeObserver = null;
|
|
57
|
+
this.unobserveViewport();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/utils/safe_timeout.ts
|
|
62
|
+
var TimerRegistry = class {
|
|
63
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
64
|
+
ids = /* @__PURE__ */ new Set();
|
|
65
|
+
/**
|
|
66
|
+
* Cancels a single tracked timer.
|
|
67
|
+
*
|
|
68
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
69
|
+
* registry), so callers can clear defensively without guarding.
|
|
70
|
+
*/
|
|
71
|
+
clear(id) {
|
|
72
|
+
if (this.ids.delete(id)) {
|
|
73
|
+
this.cancel(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
78
|
+
* to guarantee no timer outlives the element.
|
|
79
|
+
*/
|
|
80
|
+
clearAll() {
|
|
81
|
+
for (const id of this.ids) {
|
|
82
|
+
this.cancel(id);
|
|
83
|
+
}
|
|
84
|
+
this.ids.clear();
|
|
85
|
+
}
|
|
86
|
+
/** Number of timers currently tracked (pending). */
|
|
87
|
+
get size() {
|
|
88
|
+
return this.ids.size;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
92
|
+
/**
|
|
93
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
94
|
+
*
|
|
95
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
96
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
97
|
+
*/
|
|
98
|
+
set(callback, delay) {
|
|
99
|
+
const id = this.schedule(() => {
|
|
100
|
+
this.ids.delete(id);
|
|
101
|
+
callback();
|
|
102
|
+
}, delay);
|
|
103
|
+
this.ids.add(id);
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
schedule(callback, delay) {
|
|
107
|
+
return window.setTimeout(callback, delay);
|
|
108
|
+
}
|
|
109
|
+
cancel(id) {
|
|
110
|
+
window.clearTimeout(id);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/controllers/overflow_menu_controller.ts
|
|
115
|
+
var OverflowMenuController = class extends Controller {
|
|
116
|
+
static targets = ["items", "more"];
|
|
117
|
+
static values = {
|
|
118
|
+
moreLabel: { type: String, default: "More" },
|
|
119
|
+
debounce: { type: Number, default: 100 }
|
|
120
|
+
};
|
|
121
|
+
static actions = ["update"];
|
|
122
|
+
static events = ["change"];
|
|
123
|
+
#layout = new LayoutObserver(() => this.#scheduleUpdate());
|
|
124
|
+
#timers = new SafeTimeout();
|
|
125
|
+
/** The managed items in canonical (authored) order, captured at connect. */
|
|
126
|
+
#items = [];
|
|
127
|
+
#index = /* @__PURE__ */ new WeakMap();
|
|
128
|
+
/**
|
|
129
|
+
* Cached bar-context width per item. An item's natural width is location-independent,
|
|
130
|
+
* so this lets a measure pass read only the items currently in the bar (menu items
|
|
131
|
+
* reuse their last value) instead of pulling everything back to measure — one reflow,
|
|
132
|
+
* not two.
|
|
133
|
+
*/
|
|
134
|
+
#widths = /* @__PURE__ */ new WeakMap();
|
|
135
|
+
/** Last reported overflow count, so `change` fires only on transitions. */
|
|
136
|
+
#lastHidden = null;
|
|
137
|
+
connect() {
|
|
138
|
+
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
139
|
+
const trigger = this.#trigger();
|
|
140
|
+
if (trigger && (trigger.textContent ?? "").trim() === "") {
|
|
141
|
+
trigger.textContent = this.moreLabelValue;
|
|
142
|
+
}
|
|
143
|
+
this.#layout.observe(this.element);
|
|
144
|
+
this.#layout.observeViewport();
|
|
145
|
+
this.update();
|
|
146
|
+
}
|
|
147
|
+
disconnect() {
|
|
148
|
+
this.#layout.disconnect();
|
|
149
|
+
this.#timers.clearAll();
|
|
150
|
+
this.#lastHidden = null;
|
|
151
|
+
}
|
|
152
|
+
/** Re-measures and rebalances items between the bar and the More menu. */
|
|
153
|
+
update() {
|
|
154
|
+
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
155
|
+
this.#syncItems();
|
|
156
|
+
this.moreTarget.hidden = false;
|
|
157
|
+
for (const item of this.#items) {
|
|
158
|
+
if (item.parentElement === this.itemsTarget) this.#widths.set(item, item.offsetWidth);
|
|
159
|
+
}
|
|
160
|
+
const moreWidth = (this.#trigger() ?? this.moreTarget).offsetWidth;
|
|
161
|
+
const gap = this.#gap();
|
|
162
|
+
const widthOf = (el) => this.#widths.get(el) ?? el.offsetWidth;
|
|
163
|
+
const containerWidth = this.element.clientWidth;
|
|
164
|
+
const itemsWidth = this.#items.reduce((sum, el) => sum + widthOf(el), 0);
|
|
165
|
+
let visibleWidth = itemsWidth + Math.max(0, this.#items.length - 1) * gap;
|
|
166
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
167
|
+
if (visibleWidth > containerWidth) {
|
|
168
|
+
const budget = containerWidth - moreWidth - gap;
|
|
169
|
+
const dropOrder = [...this.#items].sort(
|
|
170
|
+
(a, b) => this.#rank(b) - this.#rank(a) || this.#indexOf(b) - this.#indexOf(a)
|
|
171
|
+
);
|
|
172
|
+
for (const item of dropOrder) {
|
|
173
|
+
if (visibleWidth <= budget) break;
|
|
174
|
+
hidden.add(item);
|
|
175
|
+
visibleWidth -= widthOf(item) + gap;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const menu = this.#menuList();
|
|
179
|
+
const active = document.activeElement;
|
|
180
|
+
const refocusTrigger = active instanceof HTMLElement && hidden.has(active);
|
|
181
|
+
for (const item of this.#items) {
|
|
182
|
+
if (hidden.has(item)) this.#toMenu(item, menu);
|
|
183
|
+
else this.#toBar(item);
|
|
184
|
+
}
|
|
185
|
+
if (refocusTrigger) this.#trigger()?.focus();
|
|
186
|
+
const count = hidden.size;
|
|
187
|
+
this.moreTarget.hidden = count === 0;
|
|
188
|
+
if (count > 0) this.element.setAttribute("data-overflowing", "true");
|
|
189
|
+
else this.element.removeAttribute("data-overflowing");
|
|
190
|
+
this.element.setAttribute("data-overflow-count", String(count));
|
|
191
|
+
if (this.#lastHidden !== count) {
|
|
192
|
+
this.#lastHidden = count;
|
|
193
|
+
this.dispatch("change", { detail: { visible: this.#items.length - count, hidden: count } });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** The flex `column-gap` between items in px (0 when none / unsupported). */
|
|
197
|
+
#gap() {
|
|
198
|
+
if (typeof window.getComputedStyle !== "function") return 0;
|
|
199
|
+
const style = window.getComputedStyle(this.itemsTarget);
|
|
200
|
+
const value = Number.parseFloat(style.columnGap || style.gap || "");
|
|
201
|
+
return Number.isNaN(value) ? 0 : value;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Reconciles the managed item set with the DOM each pass: adopts items appended to the
|
|
205
|
+
* bar since last time (so the documented "add items, then call `update`" flow works)
|
|
206
|
+
* and drops any the consumer removed, then renumbers canonical indices. Banked items
|
|
207
|
+
* live in the menu (still within the controller element) and are retained.
|
|
208
|
+
*/
|
|
209
|
+
#syncItems() {
|
|
210
|
+
const known = new Set(this.#items);
|
|
211
|
+
for (const el of this.itemsTarget.children) {
|
|
212
|
+
if (el instanceof HTMLElement && !known.has(el)) this.#items.push(el);
|
|
213
|
+
}
|
|
214
|
+
this.#items = this.#items.filter((el) => this.element.contains(el));
|
|
215
|
+
this.#index = /* @__PURE__ */ new WeakMap();
|
|
216
|
+
this.#items.forEach((el, i) => {
|
|
217
|
+
this.#index.set(el, i);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Debounced re-measure for resize-driven churn. */
|
|
221
|
+
#scheduleUpdate() {
|
|
222
|
+
this.#timers.clearAll();
|
|
223
|
+
this.#timers.set(() => this.update(), this.debounceValue);
|
|
224
|
+
}
|
|
225
|
+
/** Returns the menu list the items are banked into (falls back to the More wrapper). */
|
|
226
|
+
#menuList() {
|
|
227
|
+
return this.moreTarget.querySelector('[data-stimeo--menu-target="menu"]') ?? this.moreTarget;
|
|
228
|
+
}
|
|
229
|
+
#trigger() {
|
|
230
|
+
return this.moreTarget.querySelector('[data-stimeo--menu-target="trigger"]');
|
|
231
|
+
}
|
|
232
|
+
/** Restores an item to the bar, stripping the menu semantics we may have added. */
|
|
233
|
+
#toBar(item) {
|
|
234
|
+
this.itemsTarget.appendChild(item);
|
|
235
|
+
if (item.dataset.overflowMenuized === void 0) return;
|
|
236
|
+
item.removeAttribute("data-stimeo--menu-target");
|
|
237
|
+
this.#restoreAttr(item, "role", item.dataset.overflowOrigRole);
|
|
238
|
+
this.#restoreAttr(item, "tabindex", item.dataset.overflowOrigTabindex);
|
|
239
|
+
delete item.dataset.overflowMenuized;
|
|
240
|
+
delete item.dataset.overflowOrigRole;
|
|
241
|
+
delete item.dataset.overflowOrigTabindex;
|
|
242
|
+
}
|
|
243
|
+
/** Banks an item into the menu with menuitem semantics for Menu to manage. */
|
|
244
|
+
#toMenu(item, menu) {
|
|
245
|
+
if (item.dataset.overflowMenuized === void 0) {
|
|
246
|
+
item.dataset.overflowMenuized = "true";
|
|
247
|
+
item.dataset.overflowOrigRole = item.getAttribute("role") ?? "";
|
|
248
|
+
item.dataset.overflowOrigTabindex = item.getAttribute("tabindex") ?? "";
|
|
249
|
+
item.setAttribute("role", "menuitem");
|
|
250
|
+
item.setAttribute("tabindex", "-1");
|
|
251
|
+
item.setAttribute("data-stimeo--menu-target", "item");
|
|
252
|
+
}
|
|
253
|
+
menu.appendChild(item);
|
|
254
|
+
}
|
|
255
|
+
/** Re-applies a saved attribute value, or removes the attribute when it was absent. */
|
|
256
|
+
#restoreAttr(item, name, original) {
|
|
257
|
+
if (original === void 0 || original === "") item.removeAttribute(name);
|
|
258
|
+
else item.setAttribute(name, original);
|
|
259
|
+
}
|
|
260
|
+
#indexOf(item) {
|
|
261
|
+
return this.#index.get(item) ?? 0;
|
|
262
|
+
}
|
|
263
|
+
/** Retention rank: lower keeps longer; no `data-priority` ranks lowest (drops first). */
|
|
264
|
+
#rank(item) {
|
|
265
|
+
const raw = item.getAttribute("data-priority");
|
|
266
|
+
if (raw === null) return Number.POSITIVE_INFINITY;
|
|
267
|
+
const value = Number(raw);
|
|
268
|
+
return Number.isNaN(value) ? Number.POSITIVE_INFINITY : value;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export { OverflowMenuController };
|
|
273
|
+
//# sourceMappingURL=overflow_menu_controller.js.map
|
|
274
|
+
//# sourceMappingURL=overflow_menu_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/overflow_menu_controller.ts"],"names":[],"mappings":"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","file":"overflow_menu_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","/**\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 { LayoutObserver } from \"../utils/layout_observer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless **overflow / priority menu**: items that no longer fit their container are\n * moved into a \"More\" dropdown (lowest `data-priority` first) and moved back as space\n * returns, watched with a {@link ResizeObserver} (no dedicated APG pattern — a\n * composition; the menu's a11y is delegated to Menu, so it is not re-implemented).\n *\n * Markup contract (identifier: `stimeo--overflow-menu`):\n * <div data-controller=\"stimeo--overflow-menu\" role=\"toolbar\">\n * <div data-stimeo--overflow-menu-target=\"items\">\n * <a data-priority=\"1\">Save</a><a data-priority=\"2\">Duplicate</a><a>Delete</a>\n * </div>\n * <div data-controller=\"stimeo--menu\" data-stimeo--overflow-menu-target=\"more\" hidden>\n * <button data-stimeo--menu-target=\"trigger\">More</button>\n * <ul role=\"menu\" data-stimeo--menu-target=\"menu\" hidden></ul>\n * </div>\n * </div>\n *\n * On connect (and every debounced resize) it measures the items against the container\n * width and, when they overflow, banks the lowest-priority ones into the More menu —\n * giving each `role=\"menuitem\"` / `tabindex=\"-1\"` and the menu's `item` target so Menu\n * drives it — until the rest fit beside the More button. When nothing overflows the\n * More button is `hidden`. The controller element carries `data-overflowing` /\n * `data-overflow-count`, and a `change` event fires on each transition.\n *\n * @remarks\n * Items are *moved* (not cloned). When a focused item retreats into the (collapsed)\n * menu it would otherwise be hidden and lose focus, so focus is redirected to the More\n * trigger. Priority: lower `data-priority` is kept longer; items without one drop first.\n * Behavior only — no styling. The `ResizeObserver` and debounce timer are released on\n * `disconnect()` (Turbo navigation included); state is derived from the DOM each pass\n * (no module-scope state), so `connect()` re-syncs after a Turbo morph. Captures the\n * managed item set at connect; call the `update` action to re-measure after adding\n * items dynamically.\n */\nexport class OverflowMenuController extends Controller<HTMLElement> {\n static override targets = [\"items\", \"more\"];\n static override values = {\n moreLabel: { type: String, default: \"More\" },\n debounce: { type: Number, default: 100 },\n };\n static actions = [\"update\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly itemsTarget: HTMLElement;\n declare readonly moreTarget: HTMLElement;\n declare readonly hasItemsTarget: boolean;\n declare readonly hasMoreTarget: boolean;\n\n declare moreLabelValue: string;\n declare debounceValue: number;\n\n readonly #layout = new LayoutObserver(() => this.#scheduleUpdate());\n readonly #timers = new SafeTimeout();\n /** The managed items in canonical (authored) order, captured at connect. */\n #items: HTMLElement[] = [];\n #index = new WeakMap<HTMLElement, number>();\n /**\n * Cached bar-context width per item. An item's natural width is location-independent,\n * so this lets a measure pass read only the items currently in the bar (menu items\n * reuse their last value) instead of pulling everything back to measure — one reflow,\n * not two.\n */\n #widths = new WeakMap<HTMLElement, number>();\n /** Last reported overflow count, so `change` fires only on transitions. */\n #lastHidden: number | null = null;\n\n override connect(): void {\n if (!this.hasItemsTarget || !this.hasMoreTarget) return;\n\n const trigger = this.#trigger();\n if (trigger && (trigger.textContent ?? \"\").trim() === \"\") {\n trigger.textContent = this.moreLabelValue;\n }\n\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n this.update();\n }\n\n override disconnect(): void {\n this.#layout.disconnect();\n this.#timers.clearAll();\n this.#lastHidden = null;\n }\n\n /** Re-measures and rebalances items between the bar and the More menu. */\n update(): void {\n if (!this.hasItemsTarget || !this.hasMoreTarget) return;\n this.#syncItems();\n\n // Reveal More so its trigger is measurable, then refresh cached widths for the items\n // currently in the bar (menu items keep their location-independent last value).\n this.moreTarget.hidden = false;\n for (const item of this.#items) {\n if (item.parentElement === this.itemsTarget) this.#widths.set(item, item.offsetWidth);\n }\n const moreWidth = (this.#trigger() ?? this.moreTarget).offsetWidth;\n const gap = this.#gap();\n\n const widthOf = (el: HTMLElement): number => this.#widths.get(el) ?? el.offsetWidth;\n const containerWidth = this.element.clientWidth;\n const itemsWidth = this.#items.reduce((sum, el) => sum + widthOf(el), 0);\n let visibleWidth = itemsWidth + Math.max(0, this.#items.length - 1) * gap;\n\n const hidden = new Set<HTMLElement>();\n if (visibleWidth > containerWidth) {\n const budget = containerWidth - moreWidth - gap; // reserve the More button + its gap\n // Drop lowest-retention first: no `data-priority` (rank ∞), then highest number;\n // ties broken right-to-left so leading items survive.\n const dropOrder = [...this.#items].sort(\n (a, b) => this.#rank(b) - this.#rank(a) || this.#indexOf(b) - this.#indexOf(a),\n );\n for (const item of dropOrder) {\n if (visibleWidth <= budget) break;\n hidden.add(item);\n visibleWidth -= widthOf(item) + gap; // dropping an item frees its width and a gap\n }\n }\n\n // Re-home every item in canonical order so the bar and the menu both stay correctly\n // ordered — a banked middle item returns to its original slot, not the end.\n const menu = this.#menuList();\n // If the focused item is one of the ones retreating, redirect focus to the More\n // trigger: in a collapsed menu the banked item becomes hidden and the browser would\n // otherwise drop focus to <body>. Decided before the move (not by polling\n // activeElement afterwards) so it does not race the browser's blur.\n const active = document.activeElement;\n const refocusTrigger = active instanceof HTMLElement && hidden.has(active);\n for (const item of this.#items) {\n if (hidden.has(item)) this.#toMenu(item, menu);\n else this.#toBar(item);\n }\n if (refocusTrigger) this.#trigger()?.focus();\n\n const count = hidden.size;\n this.moreTarget.hidden = count === 0;\n if (count > 0) this.element.setAttribute(\"data-overflowing\", \"true\");\n else this.element.removeAttribute(\"data-overflowing\");\n this.element.setAttribute(\"data-overflow-count\", String(count));\n\n if (this.#lastHidden !== count) {\n this.#lastHidden = count;\n this.dispatch(\"change\", { detail: { visible: this.#items.length - count, hidden: count } });\n }\n }\n\n /** The flex `column-gap` between items in px (0 when none / unsupported). */\n #gap(): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n const style = window.getComputedStyle(this.itemsTarget);\n const value = Number.parseFloat(style.columnGap || style.gap || \"\");\n return Number.isNaN(value) ? 0 : value;\n }\n\n /**\n * Reconciles the managed item set with the DOM each pass: adopts items appended to the\n * bar since last time (so the documented \"add items, then call `update`\" flow works)\n * and drops any the consumer removed, then renumbers canonical indices. Banked items\n * live in the menu (still within the controller element) and are retained.\n */\n #syncItems(): void {\n const known = new Set(this.#items);\n for (const el of this.itemsTarget.children) {\n if (el instanceof HTMLElement && !known.has(el)) this.#items.push(el);\n }\n this.#items = this.#items.filter((el) => this.element.contains(el));\n this.#index = new WeakMap();\n this.#items.forEach((el, i) => {\n this.#index.set(el, i);\n });\n }\n\n /** Debounced re-measure for resize-driven churn. */\n #scheduleUpdate(): void {\n this.#timers.clearAll();\n this.#timers.set(() => this.update(), this.debounceValue);\n }\n\n /** Returns the menu list the items are banked into (falls back to the More wrapper). */\n #menuList(): HTMLElement {\n return (\n this.moreTarget.querySelector<HTMLElement>('[data-stimeo--menu-target=\"menu\"]') ??\n this.moreTarget\n );\n }\n\n #trigger(): HTMLElement | null {\n return this.moreTarget.querySelector<HTMLElement>('[data-stimeo--menu-target=\"trigger\"]');\n }\n\n /** Restores an item to the bar, stripping the menu semantics we may have added. */\n #toBar(item: HTMLElement): void {\n // Always append (in canonical-order calls) so the bar's order is rebuilt correctly.\n this.itemsTarget.appendChild(item);\n if (item.dataset.overflowMenuized === undefined) return;\n item.removeAttribute(\"data-stimeo--menu-target\");\n this.#restoreAttr(item, \"role\", item.dataset.overflowOrigRole);\n this.#restoreAttr(item, \"tabindex\", item.dataset.overflowOrigTabindex);\n delete item.dataset.overflowMenuized;\n delete item.dataset.overflowOrigRole;\n delete item.dataset.overflowOrigTabindex;\n }\n\n /** Banks an item into the menu with menuitem semantics for Menu to manage. */\n #toMenu(item: HTMLElement, menu: HTMLElement): void {\n if (item.dataset.overflowMenuized === undefined) {\n item.dataset.overflowMenuized = \"true\";\n item.dataset.overflowOrigRole = item.getAttribute(\"role\") ?? \"\";\n item.dataset.overflowOrigTabindex = item.getAttribute(\"tabindex\") ?? \"\";\n item.setAttribute(\"role\", \"menuitem\");\n item.setAttribute(\"tabindex\", \"-1\");\n item.setAttribute(\"data-stimeo--menu-target\", \"item\");\n }\n menu.appendChild(item);\n }\n\n /** Re-applies a saved attribute value, or removes the attribute when it was absent. */\n #restoreAttr(item: HTMLElement, name: string, original: string | undefined): void {\n if (original === undefined || original === \"\") item.removeAttribute(name);\n else item.setAttribute(name, original);\n }\n\n #indexOf(item: HTMLElement): number {\n return this.#index.get(item) ?? 0;\n }\n\n /** Retention rank: lower keeps longer; no `data-priority` ranks lowest (drops first). */\n #rank(item: HTMLElement): number {\n const raw = item.getAttribute(\"data-priority\");\n if (raw === null) return Number.POSITIVE_INFINITY;\n const value = Number(raw);\n return Number.isNaN(value) ? Number.POSITIVE_INFINITY : value;\n }\n}\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless, accessible pagination behavior.
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--pagination`):
|
|
7
|
+
* <nav data-controller="stimeo--pagination" aria-label="Pagination"
|
|
8
|
+
* data-stimeo--pagination-page-value="1"
|
|
9
|
+
* data-stimeo--pagination-total-value="5">
|
|
10
|
+
* <button type="button" data-stimeo--pagination-target="prev"
|
|
11
|
+
* data-action="stimeo--pagination#prev">Prev</button>
|
|
12
|
+
* <button type="button" data-page="1" aria-current="page"
|
|
13
|
+
* data-stimeo--pagination-target="page"
|
|
14
|
+
* data-action="stimeo--pagination#select">1</button>
|
|
15
|
+
* <!-- more page buttons -->
|
|
16
|
+
* <button type="button" data-stimeo--pagination-target="next"
|
|
17
|
+
* data-action="stimeo--pagination#next">Next</button>
|
|
18
|
+
* </nav>
|
|
19
|
+
*
|
|
20
|
+
* There is no dedicated APG pattern; this uses a navigation landmark plus
|
|
21
|
+
* `aria-current="page"`. The controller owns current-page state, the
|
|
22
|
+
* `aria-current` sync, boundary disabling of prev/next, and the change event.
|
|
23
|
+
* Generating/eliding the page buttons and fetching data stay with the consumer.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Behavior only — each control is in the natural Tab order (no roving). When a
|
|
27
|
+
* boundary disables the button that currently has focus, focus is moved first so
|
|
28
|
+
* it is never lost to a `disabled` element.
|
|
29
|
+
*
|
|
30
|
+
* Behavior provided:
|
|
31
|
+
* - `select` reads the clicked button's `data-page` and makes it current.
|
|
32
|
+
* - `prev`/`next` step by one, clamped to `[1, total]`.
|
|
33
|
+
* - The current page button gets `aria-current="page"` (removed from the rest);
|
|
34
|
+
* `prev` is `disabled` at page 1 and `next` at `total`.
|
|
35
|
+
* - Every change dispatches `stimeo--pagination:change`.
|
|
36
|
+
*/
|
|
37
|
+
declare class PaginationController extends Controller<HTMLElement> {
|
|
38
|
+
#private;
|
|
39
|
+
static targets: string[];
|
|
40
|
+
static values: {
|
|
41
|
+
page: {
|
|
42
|
+
type: NumberConstructor;
|
|
43
|
+
default: number;
|
|
44
|
+
};
|
|
45
|
+
total: {
|
|
46
|
+
type: NumberConstructor;
|
|
47
|
+
default: number;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
static actions: readonly ["next", "prev", "select"];
|
|
51
|
+
static events: readonly ["change"];
|
|
52
|
+
readonly pageTargets: HTMLElement[];
|
|
53
|
+
readonly prevTarget: HTMLButtonElement;
|
|
54
|
+
readonly nextTarget: HTMLButtonElement;
|
|
55
|
+
readonly hasPrevTarget: boolean;
|
|
56
|
+
readonly hasNextTarget: boolean;
|
|
57
|
+
pageValue: number;
|
|
58
|
+
totalValue: number;
|
|
59
|
+
/** Normalizes out-of-range initial values and renders the initial state. */
|
|
60
|
+
connect(): void;
|
|
61
|
+
/** Makes the clicked page button (its `data-page`) current. */
|
|
62
|
+
select(event: Event): void;
|
|
63
|
+
/** Steps to the previous page. */
|
|
64
|
+
prev(): void;
|
|
65
|
+
/** Steps to the next page. */
|
|
66
|
+
next(): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { PaginationController };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/pagination_controller.ts
|
|
4
|
+
var PaginationController = class extends Controller {
|
|
5
|
+
static targets = ["page", "prev", "next"];
|
|
6
|
+
static values = {
|
|
7
|
+
page: { type: Number, default: 1 },
|
|
8
|
+
total: { type: Number, default: 1 }
|
|
9
|
+
};
|
|
10
|
+
static actions = ["next", "prev", "select"];
|
|
11
|
+
static events = ["change"];
|
|
12
|
+
/** Normalizes out-of-range initial values and renders the initial state. */
|
|
13
|
+
connect() {
|
|
14
|
+
this.totalValue = Math.max(1, Math.trunc(this.totalValue));
|
|
15
|
+
this.pageValue = Math.min(this.totalValue, Math.max(1, Math.trunc(this.pageValue)));
|
|
16
|
+
this.#render();
|
|
17
|
+
}
|
|
18
|
+
/** Makes the clicked page button (its `data-page`) current. */
|
|
19
|
+
select(event) {
|
|
20
|
+
const button = event.currentTarget;
|
|
21
|
+
const page = Number(button.dataset.page);
|
|
22
|
+
if (!Number.isFinite(page)) return;
|
|
23
|
+
this.#goto(page);
|
|
24
|
+
}
|
|
25
|
+
/** Steps to the previous page. */
|
|
26
|
+
prev() {
|
|
27
|
+
this.#goto(this.pageValue - 1);
|
|
28
|
+
}
|
|
29
|
+
/** Steps to the next page. */
|
|
30
|
+
next() {
|
|
31
|
+
this.#goto(this.pageValue + 1);
|
|
32
|
+
}
|
|
33
|
+
/** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */
|
|
34
|
+
#goto(page) {
|
|
35
|
+
const total = Math.max(1, Math.trunc(this.totalValue));
|
|
36
|
+
const target = Math.min(total, Math.max(1, Math.trunc(page)));
|
|
37
|
+
if (target === this.pageValue) return;
|
|
38
|
+
const previous = this.pageValue;
|
|
39
|
+
this.pageValue = target;
|
|
40
|
+
this.#render();
|
|
41
|
+
this.dispatch("change", {
|
|
42
|
+
detail: { page: target, total: this.totalValue, previous }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */
|
|
46
|
+
#render() {
|
|
47
|
+
for (const button of this.pageTargets) {
|
|
48
|
+
if (Number(button.dataset.page) === this.pageValue) {
|
|
49
|
+
button.setAttribute("aria-current", "page");
|
|
50
|
+
} else {
|
|
51
|
+
button.removeAttribute("aria-current");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.#setDisabled(this.hasPrevTarget ? this.prevTarget : null, this.pageValue <= 1);
|
|
55
|
+
this.#setDisabled(
|
|
56
|
+
this.hasNextTarget ? this.nextTarget : null,
|
|
57
|
+
this.pageValue >= this.totalValue
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sets `disabled` on a boundary button, first moving focus off it if it is the
|
|
62
|
+
* active element so disabling never strands focus.
|
|
63
|
+
*/
|
|
64
|
+
#setDisabled(button, disabled) {
|
|
65
|
+
if (!button) return;
|
|
66
|
+
if (disabled && button === document.activeElement) {
|
|
67
|
+
this.#moveFocusAwayFrom(button);
|
|
68
|
+
}
|
|
69
|
+
button.disabled = disabled;
|
|
70
|
+
}
|
|
71
|
+
/** Moves focus to the opposite enabled control, falling back to the current page. */
|
|
72
|
+
#moveFocusAwayFrom(button) {
|
|
73
|
+
const opposite = button === (this.hasPrevTarget ? this.prevTarget : null) ? this.hasNextTarget ? this.nextTarget : null : this.hasPrevTarget ? this.prevTarget : null;
|
|
74
|
+
const currentPage = this.pageTargets.find(
|
|
75
|
+
(candidate) => Number(candidate.dataset.page) === this.pageValue
|
|
76
|
+
);
|
|
77
|
+
const destination = opposite && !opposite.disabled ? opposite : currentPage;
|
|
78
|
+
if (destination) {
|
|
79
|
+
destination.focus();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!this.element.hasAttribute("tabindex")) this.element.setAttribute("tabindex", "-1");
|
|
83
|
+
this.element.focus();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { PaginationController };
|
|
88
|
+
//# sourceMappingURL=pagination_controller.js.map
|
|
89
|
+
//# sourceMappingURL=pagination_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/controllers/pagination_controller.ts"],"names":[],"mappings":"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","file":"pagination_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible pagination behavior.\n *\n * Markup contract (identifier: `stimeo--pagination`):\n * <nav data-controller=\"stimeo--pagination\" aria-label=\"Pagination\"\n * data-stimeo--pagination-page-value=\"1\"\n * data-stimeo--pagination-total-value=\"5\">\n * <button type=\"button\" data-stimeo--pagination-target=\"prev\"\n * data-action=\"stimeo--pagination#prev\">Prev</button>\n * <button type=\"button\" data-page=\"1\" aria-current=\"page\"\n * data-stimeo--pagination-target=\"page\"\n * data-action=\"stimeo--pagination#select\">1</button>\n * <!-- more page buttons -->\n * <button type=\"button\" data-stimeo--pagination-target=\"next\"\n * data-action=\"stimeo--pagination#next\">Next</button>\n * </nav>\n *\n * There is no dedicated APG pattern; this uses a navigation landmark plus\n * `aria-current=\"page\"`. The controller owns current-page state, the\n * `aria-current` sync, boundary disabling of prev/next, and the change event.\n * Generating/eliding the page buttons and fetching data stay with the consumer.\n *\n * @remarks\n * Behavior only — each control is in the natural Tab order (no roving). When a\n * boundary disables the button that currently has focus, focus is moved first so\n * it is never lost to a `disabled` element.\n *\n * Behavior provided:\n * - `select` reads the clicked button's `data-page` and makes it current.\n * - `prev`/`next` step by one, clamped to `[1, total]`.\n * - The current page button gets `aria-current=\"page\"` (removed from the rest);\n * `prev` is `disabled` at page 1 and `next` at `total`.\n * - Every change dispatches `stimeo--pagination:change`.\n */\nexport class PaginationController extends Controller<HTMLElement> {\n static override targets = [\"page\", \"prev\", \"next\"];\n static override values = {\n page: { type: Number, default: 1 },\n total: { type: Number, default: 1 },\n };\n static actions = [\"next\", \"prev\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly pageTargets: HTMLElement[];\n declare readonly prevTarget: HTMLButtonElement;\n declare readonly nextTarget: HTMLButtonElement;\n declare readonly hasPrevTarget: boolean;\n declare readonly hasNextTarget: boolean;\n declare pageValue: number;\n declare totalValue: number;\n\n /** Normalizes out-of-range initial values and renders the initial state. */\n override connect(): void {\n this.totalValue = Math.max(1, Math.trunc(this.totalValue));\n this.pageValue = Math.min(this.totalValue, Math.max(1, Math.trunc(this.pageValue)));\n this.#render();\n }\n\n /** Makes the clicked page button (its `data-page`) current. */\n select(event: Event): void {\n const button = event.currentTarget as HTMLElement;\n const page = Number(button.dataset.page);\n if (!Number.isFinite(page)) return;\n this.#goto(page);\n }\n\n /** Steps to the previous page. */\n prev(): void {\n this.#goto(this.pageValue - 1);\n }\n\n /** Steps to the next page. */\n next(): void {\n this.#goto(this.pageValue + 1);\n }\n\n /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */\n #goto(page: number): void {\n // Normalize total first so a stray total<=0 can't clamp the 1-based page to 0.\n const total = Math.max(1, Math.trunc(this.totalValue));\n const target = Math.min(total, Math.max(1, Math.trunc(page)));\n if (target === this.pageValue) return;\n const previous = this.pageValue;\n this.pageValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { page: target, total: this.totalValue, previous },\n });\n }\n\n /** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */\n #render(): void {\n for (const button of this.pageTargets) {\n if (Number(button.dataset.page) === this.pageValue) {\n button.setAttribute(\"aria-current\", \"page\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n }\n this.#setDisabled(this.hasPrevTarget ? this.prevTarget : null, this.pageValue <= 1);\n this.#setDisabled(\n this.hasNextTarget ? this.nextTarget : null,\n this.pageValue >= this.totalValue,\n );\n }\n\n /**\n * Sets `disabled` on a boundary button, first moving focus off it if it is the\n * active element so disabling never strands focus.\n */\n #setDisabled(button: HTMLButtonElement | null, disabled: boolean): void {\n if (!button) return;\n if (disabled && button === document.activeElement) {\n this.#moveFocusAwayFrom(button);\n }\n button.disabled = disabled;\n }\n\n /** Moves focus to the opposite enabled control, falling back to the current page. */\n #moveFocusAwayFrom(button: HTMLButtonElement): void {\n const opposite =\n button === (this.hasPrevTarget ? this.prevTarget : null)\n ? this.hasNextTarget\n ? this.nextTarget\n : null\n : this.hasPrevTarget\n ? this.prevTarget\n : null;\n const currentPage = this.pageTargets.find(\n (candidate) => Number(candidate.dataset.page) === this.pageValue,\n );\n const destination = opposite && !opposite.disabled ? opposite : currentPage;\n if (destination) {\n destination.focus();\n return;\n }\n // Degenerate config (e.g. a lone boundary button, no page buttons): keep focus\n // inside the pagination landmark instead of letting the browser drop it to\n // <body> when the button disables. The root is made programmatically focusable\n // just-in-time with `tabindex=\"-1\"`, which is not a Tab stop.\n if (!this.element.hasAttribute(\"tabindex\")) this.element.setAttribute(\"tabindex\", \"-1\");\n this.element.focus();\n }\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless password-strength behavior: scores the field with a lightweight
|
|
5
|
+
* zero-dependency heuristic and drives a meter plus an `aria-live` label. No
|
|
6
|
+
* dedicated APG pattern; the meter display follows {@link MeterController}.
|
|
7
|
+
*
|
|
8
|
+
* Markup contract (identifier: `stimeo--password-strength`):
|
|
9
|
+
* <div data-controller="stimeo--password-strength">
|
|
10
|
+
* <input type="password" data-stimeo--password-strength-target="input"
|
|
11
|
+
* data-action="input->stimeo--password-strength#evaluate" aria-describedby="pw">
|
|
12
|
+
* <div data-stimeo--password-strength-target="meter" role="meter"
|
|
13
|
+
* aria-valuemin="0" aria-valuemax="4"></div>
|
|
14
|
+
* <span id="pw" data-stimeo--password-strength-target="label" aria-live="polite"></span>
|
|
15
|
+
* </div>
|
|
16
|
+
*
|
|
17
|
+
* On each input the controller scores the password (length milestones + character
|
|
18
|
+
* variety, capped for trivial repetition), syncs the meter's `aria-valuenow`,
|
|
19
|
+
* reflects a stable band on `data-strength`, the `0–1` fill on
|
|
20
|
+
* `--stimeo-password-strength`, and (when `minScore` is set) `data-below-min`,
|
|
21
|
+
* and writes the level label into the label target.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Behavior only — the meter/bar look is the consumer's, keyed off the data hooks.
|
|
25
|
+
* `data-strength` is one of the fixed {@link STRENGTH_BANDS} (not the localizable
|
|
26
|
+
* `levels` text), so consumers can style by it regardless of locale; the visible
|
|
27
|
+
* label receives the matching `levels` entry. Non-text state (meter ARIA,
|
|
28
|
+
* `data-strength`/`data-below-min`, the custom property, and the `change` event)
|
|
29
|
+
* updates **immediately** on every keystroke so styling and consumers stay
|
|
30
|
+
* responsive, while the label — in an `aria-live="polite"` region — is written on
|
|
31
|
+
* a short debounce so a screen reader is not flooded mid-typing. The score is a
|
|
32
|
+
* pure function of the input value (no module-scope state), so `connect()`
|
|
33
|
+
* re-evaluates idempotently after a Turbo cache restore; the debounce timer is
|
|
34
|
+
* owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo included).
|
|
35
|
+
* The estimator is intentionally not a dictionary/zxcvbn-grade one (kept
|
|
36
|
+
* zero-dep); swap a stronger one in on the consumer side if needed.
|
|
37
|
+
*/
|
|
38
|
+
declare class PasswordStrengthController extends Controller<HTMLElement> {
|
|
39
|
+
#private;
|
|
40
|
+
static targets: string[];
|
|
41
|
+
static values: {
|
|
42
|
+
minScore: {
|
|
43
|
+
type: NumberConstructor;
|
|
44
|
+
default: number;
|
|
45
|
+
};
|
|
46
|
+
levels: {
|
|
47
|
+
type: ArrayConstructor;
|
|
48
|
+
default: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
static actions: readonly ["evaluate"];
|
|
52
|
+
static events: readonly ["change"];
|
|
53
|
+
readonly inputTarget: HTMLInputElement;
|
|
54
|
+
readonly meterTarget: HTMLElement;
|
|
55
|
+
readonly labelTarget: HTMLElement;
|
|
56
|
+
readonly hasInputTarget: boolean;
|
|
57
|
+
readonly hasMeterTarget: boolean;
|
|
58
|
+
readonly hasLabelTarget: boolean;
|
|
59
|
+
minScoreValue: number;
|
|
60
|
+
levelsValue: string[];
|
|
61
|
+
connect(): void;
|
|
62
|
+
disconnect(): void;
|
|
63
|
+
/** Re-evaluates strength from the input. Bound via `data-action` (`input`). */
|
|
64
|
+
evaluate(): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { PasswordStrengthController };
|