stimeo-ui 0.1.0.pre.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +72 -0
- data/dist/controllers/accordion_controller.js +76 -0
- data/dist/controllers/announcer_controller.js +184 -0
- data/dist/controllers/aspect_ratio_controller.js +36 -0
- data/dist/controllers/auto_submit_controller.js +147 -0
- data/dist/controllers/avatar_controller.js +66 -0
- data/dist/controllers/breadcrumb_controller.js +123 -0
- data/dist/controllers/bulk_select_controller.js +104 -0
- data/dist/controllers/calendar_controller.js +394 -0
- data/dist/controllers/character_counter_controller.js +179 -0
- data/dist/controllers/checkbox_controller.js +73 -0
- data/dist/controllers/combobox_controller.js +186 -0
- data/dist/controllers/command_palette_controller.js +381 -0
- data/dist/controllers/conditional_fields_controller.js +112 -0
- data/dist/controllers/confirm_controller.js +276 -0
- data/dist/controllers/context_menu_controller.js +112 -0
- data/dist/controllers/countdown_controller.js +202 -0
- data/dist/controllers/dialog_controller.js +207 -0
- data/dist/controllers/direct_upload_controller.js +212 -0
- data/dist/controllers/dirty_form_controller.js +128 -0
- data/dist/controllers/dropdown_controller.js +66 -0
- data/dist/controllers/empty_state_controller.js +67 -0
- data/dist/controllers/flash_controller.js +221 -0
- data/dist/controllers/focus_controller.js +216 -0
- data/dist/controllers/form_field_controller.js +154 -0
- data/dist/controllers/form_validation_controller.js +202 -0
- data/dist/controllers/frame_loading_controller.js +177 -0
- data/dist/controllers/highlight_controller.js +107 -0
- data/dist/controllers/hover_card_controller.js +165 -0
- data/dist/controllers/idle_controller.js +141 -0
- data/dist/controllers/input_mask_controller.js +166 -0
- data/dist/controllers/lazy_frame_controller.js +68 -0
- data/dist/controllers/listbox_controller.js +256 -0
- data/dist/controllers/local_time_controller.js +81 -0
- data/dist/controllers/menu_controller.js +134 -0
- data/dist/controllers/meter_controller.js +96 -0
- data/dist/controllers/nested_form_controller.js +131 -0
- data/dist/controllers/network_status_controller.js +126 -0
- data/dist/controllers/number_input_controller.js +306 -0
- data/dist/controllers/otp_controller.js +201 -0
- data/dist/controllers/overflow_indicator_controller.js +169 -0
- data/dist/controllers/overflow_menu_controller.js +274 -0
- data/dist/controllers/pagination_controller.js +89 -0
- data/dist/controllers/password_strength_controller.js +175 -0
- data/dist/controllers/persist_controller.js +259 -0
- data/dist/controllers/popover_controller.js +94 -0
- data/dist/controllers/portal_controller.js +63 -0
- data/dist/controllers/preview_guard_controller.js +69 -0
- data/dist/controllers/progress_controller.js +93 -0
- data/dist/controllers/radio_group_controller.js +128 -0
- data/dist/controllers/rating_controller.js +179 -0
- data/dist/controllers/relative_time_controller.js +129 -0
- data/dist/controllers/reset_before_cache_controller.js +62 -0
- data/dist/controllers/resizable_controller.js +163 -0
- data/dist/controllers/roving_controller.js +116 -0
- data/dist/controllers/scroll_area_controller.js +183 -0
- data/dist/controllers/scroll_visibility_controller.js +103 -0
- data/dist/controllers/scrollspy_controller.js +171 -0
- data/dist/controllers/skeleton_controller.js +125 -0
- data/dist/controllers/slider_controller.js +109 -0
- data/dist/controllers/spinner_controller.js +164 -0
- data/dist/controllers/step_indicator_controller.js +55 -0
- data/dist/controllers/stepper_controller.js +78 -0
- data/dist/controllers/stick_to_bottom_controller.js +100 -0
- data/dist/controllers/sticky_observer_controller.js +53 -0
- data/dist/controllers/submit_once_controller.js +206 -0
- data/dist/controllers/switch_controller.js +50 -0
- data/dist/controllers/tabs_controller.js +63 -0
- data/dist/controllers/textarea_autosize_controller.js +72 -0
- data/dist/controllers/theme_controller.js +154 -0
- data/dist/controllers/toast_controller.js +310 -0
- data/dist/controllers/toggle_group_controller.js +130 -0
- data/dist/controllers/toolbar_controller.js +113 -0
- data/dist/controllers/tooltip_controller.js +165 -0
- data/dist/controllers/transition_controller.js +203 -0
- data/dist/index.js +12241 -0
- data/dist/positioning/index.js +145 -0
- data/lib/generators/stimeo/install/install_generator.rb +114 -0
- data/lib/generators/stimeo/install/templates/stimeo.js +12 -0
- data/lib/stimeo/ui/version.rb +10 -0
- data/lib/stimeo/ui.rb +19 -0
- data/lib/stimeo-ui.rb +4 -0
- metadata +152 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/radio_group_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/roving_tabindex.ts
|
|
6
|
+
var RovingTabindex = class {
|
|
7
|
+
/** Returns the current ordered item elements; called on every operation. */
|
|
8
|
+
#getItems;
|
|
9
|
+
/**
|
|
10
|
+
* @param getItems - Returns the current ordered item elements. Called on every
|
|
11
|
+
* operation so the live target list is always used.
|
|
12
|
+
*/
|
|
13
|
+
constructor(getItems) {
|
|
14
|
+
this.#getItems = getItems;
|
|
15
|
+
}
|
|
16
|
+
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
17
|
+
get activeIndex() {
|
|
18
|
+
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
22
|
+
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
23
|
+
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
24
|
+
* "nothing is currently tabbable".
|
|
25
|
+
*
|
|
26
|
+
* @param index - Position of the item to make tabbable.
|
|
27
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item.
|
|
28
|
+
*/
|
|
29
|
+
setActive(index, { focus = false } = {}) {
|
|
30
|
+
const items = this.#getItems();
|
|
31
|
+
items.forEach((item, i) => {
|
|
32
|
+
item.tabIndex = i === index ? 0 : -1;
|
|
33
|
+
});
|
|
34
|
+
if (focus) items[index]?.focus();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function rovingMove(current, length, delta, wrap) {
|
|
38
|
+
if (length === 0) return -1;
|
|
39
|
+
const next = current + delta;
|
|
40
|
+
return (next + length) % length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/controllers/radio_group_controller.ts
|
|
44
|
+
var RadioGroupController = class extends Controller {
|
|
45
|
+
static targets = ["radio", "field"];
|
|
46
|
+
static actions = ["onKeydown", "select"];
|
|
47
|
+
static events = ["change"];
|
|
48
|
+
#roving = new RovingTabindex(() => this.radioTargets);
|
|
49
|
+
/** Establishes the roving entry point and reflects the initial selection. */
|
|
50
|
+
connect() {
|
|
51
|
+
const selected = this.#selectedIndex();
|
|
52
|
+
this.#roving.setActive(selected === -1 ? 0 : selected);
|
|
53
|
+
if (selected !== -1) this.#reflectField(this.radioTargets[selected], { silent: true });
|
|
54
|
+
}
|
|
55
|
+
/** Selects the clicked radio. Bound via `data-action` (click). */
|
|
56
|
+
select(event) {
|
|
57
|
+
const index = this.radioTargets.indexOf(event.currentTarget);
|
|
58
|
+
if (index !== -1) this.#selectIndex(index, { focus: false });
|
|
59
|
+
}
|
|
60
|
+
/** Arrow/Home/End/Space navigation with selection-follows-focus. */
|
|
61
|
+
onKeydown(event) {
|
|
62
|
+
const current = this.radioTargets.indexOf(event.currentTarget);
|
|
63
|
+
if (current === -1) return;
|
|
64
|
+
let next = null;
|
|
65
|
+
switch (event.key) {
|
|
66
|
+
case "ArrowDown":
|
|
67
|
+
case "ArrowRight":
|
|
68
|
+
next = rovingMove(current, this.radioTargets.length, 1);
|
|
69
|
+
break;
|
|
70
|
+
case "ArrowUp":
|
|
71
|
+
case "ArrowLeft":
|
|
72
|
+
next = rovingMove(current, this.radioTargets.length, -1);
|
|
73
|
+
break;
|
|
74
|
+
case "Home":
|
|
75
|
+
next = 0;
|
|
76
|
+
break;
|
|
77
|
+
case "End":
|
|
78
|
+
next = this.radioTargets.length - 1;
|
|
79
|
+
break;
|
|
80
|
+
case " ":
|
|
81
|
+
next = current;
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
this.#selectIndex(next, { focus: true });
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks the radio at `index`, clears the rest, updates the roving Tab stop and
|
|
91
|
+
* the hidden field, and dispatches `change`.
|
|
92
|
+
*/
|
|
93
|
+
#selectIndex(index, { focus }) {
|
|
94
|
+
const radio = this.radioTargets[index];
|
|
95
|
+
if (!radio) return;
|
|
96
|
+
this.radioTargets.forEach((item, i) => {
|
|
97
|
+
item.setAttribute("aria-checked", i === index ? "true" : "false");
|
|
98
|
+
});
|
|
99
|
+
this.#roving.setActive(index, { focus });
|
|
100
|
+
this.#reflectField(radio);
|
|
101
|
+
this.dispatch("change", { detail: { value: this.#radioValue(radio), radio } });
|
|
102
|
+
}
|
|
103
|
+
/** Index of the currently checked radio, or `-1` if none. */
|
|
104
|
+
#selectedIndex() {
|
|
105
|
+
return this.radioTargets.findIndex((radio) => radio.getAttribute("aria-checked") === "true");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Mirrors a radio's `data-value` onto the hidden field, when present. Fires a
|
|
109
|
+
* native bubbling `change` on a real value change (matching `listbox`, so
|
|
110
|
+
* `auto-submit` and other native-`change` consumers react) — unless `silent`
|
|
111
|
+
* (the initial connect reflection, which is not a user edit).
|
|
112
|
+
*/
|
|
113
|
+
#reflectField(radio, { silent = false } = {}) {
|
|
114
|
+
if (!this.hasFieldTarget || !radio) return;
|
|
115
|
+
const value = this.#radioValue(radio);
|
|
116
|
+
if (this.fieldTarget.value === value) return;
|
|
117
|
+
this.fieldTarget.value = value;
|
|
118
|
+
if (!silent) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
119
|
+
}
|
|
120
|
+
/** A radio's submitted value (`data-value`, defaulting to empty). */
|
|
121
|
+
#radioValue(radio) {
|
|
122
|
+
return radio.getAttribute("data-value") ?? "";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export { RadioGroupController };
|
|
127
|
+
//# sourceMappingURL=radio_group_controller.js.map
|
|
128
|
+
//# sourceMappingURL=radio_group_controller.js.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/rating_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/roving_tabindex.ts
|
|
6
|
+
var RovingTabindex = class {
|
|
7
|
+
/** Returns the current ordered item elements; called on every operation. */
|
|
8
|
+
#getItems;
|
|
9
|
+
/**
|
|
10
|
+
* @param getItems - Returns the current ordered item elements. Called on every
|
|
11
|
+
* operation so the live target list is always used.
|
|
12
|
+
*/
|
|
13
|
+
constructor(getItems) {
|
|
14
|
+
this.#getItems = getItems;
|
|
15
|
+
}
|
|
16
|
+
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
17
|
+
get activeIndex() {
|
|
18
|
+
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
22
|
+
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
23
|
+
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
24
|
+
* "nothing is currently tabbable".
|
|
25
|
+
*
|
|
26
|
+
* @param index - Position of the item to make tabbable.
|
|
27
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item.
|
|
28
|
+
*/
|
|
29
|
+
setActive(index, { focus = false } = {}) {
|
|
30
|
+
const items = this.#getItems();
|
|
31
|
+
items.forEach((item, i) => {
|
|
32
|
+
item.tabIndex = i === index ? 0 : -1;
|
|
33
|
+
});
|
|
34
|
+
if (focus) items[index]?.focus();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/controllers/rating_controller.ts
|
|
39
|
+
var RatingController = class extends Controller {
|
|
40
|
+
static targets = ["symbol", "field"];
|
|
41
|
+
static values = {
|
|
42
|
+
value: { type: Number, default: 0 },
|
|
43
|
+
max: { type: Number, default: 5 },
|
|
44
|
+
clearable: { type: Boolean, default: true },
|
|
45
|
+
readonly: { type: Boolean, default: false }
|
|
46
|
+
};
|
|
47
|
+
static actions = ["endPreview", "onKeydown", "preview", "select"];
|
|
48
|
+
static events = ["change"];
|
|
49
|
+
#roving = new RovingTabindex(() => this.symbolTargets);
|
|
50
|
+
/** Reflects the initial value, or switches to the non-interactive readonly view. */
|
|
51
|
+
connect() {
|
|
52
|
+
if (this.readonlyValue) {
|
|
53
|
+
this.#applyReadonly();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.#apply(this.#clamp(this.valueValue), { focus: false });
|
|
57
|
+
}
|
|
58
|
+
/** Selects (or clears) the clicked symbol. Bound via `data-action` (click). */
|
|
59
|
+
select(event) {
|
|
60
|
+
if (this.readonlyValue) return;
|
|
61
|
+
const value = this.#clamp(this.#symbolValue(event.currentTarget));
|
|
62
|
+
if (this.clearableValue && value === this.valueValue) {
|
|
63
|
+
this.#render(0, { focus: true });
|
|
64
|
+
} else {
|
|
65
|
+
this.#render(value, { focus: false });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Previews a fill range on hover/focus. Bound via `data-action` (mouseenter/focus). */
|
|
69
|
+
preview(event) {
|
|
70
|
+
if (this.readonlyValue) return;
|
|
71
|
+
this.#setFillRange(this.#clamp(this.#symbolValue(event.currentTarget)));
|
|
72
|
+
}
|
|
73
|
+
/** Restores the fill range to the selected value. Bound via `data-action` (mouseleave/blur). */
|
|
74
|
+
endPreview() {
|
|
75
|
+
if (this.readonlyValue) return;
|
|
76
|
+
this.#setFillRange(this.valueValue);
|
|
77
|
+
}
|
|
78
|
+
/** Arrow/Home/End/Space keyboard control, clamped (no wrap). */
|
|
79
|
+
onKeydown(event) {
|
|
80
|
+
if (this.readonlyValue) return;
|
|
81
|
+
let next = null;
|
|
82
|
+
switch (event.key) {
|
|
83
|
+
case "ArrowRight":
|
|
84
|
+
case "ArrowUp":
|
|
85
|
+
next = this.valueValue + 1;
|
|
86
|
+
break;
|
|
87
|
+
case "ArrowLeft":
|
|
88
|
+
case "ArrowDown":
|
|
89
|
+
next = this.valueValue - 1;
|
|
90
|
+
break;
|
|
91
|
+
case "Home":
|
|
92
|
+
next = this.#minValue;
|
|
93
|
+
break;
|
|
94
|
+
case "End":
|
|
95
|
+
next = this.maxValue;
|
|
96
|
+
break;
|
|
97
|
+
case " ":
|
|
98
|
+
case "Enter":
|
|
99
|
+
next = this.#symbolValue(event.currentTarget);
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
this.#render(this.#clamp(next), { focus: true });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Applies `value` (already clamped) everywhere, then dispatches `change`.
|
|
109
|
+
* Use for user-driven changes; on connect call `#apply` directly so
|
|
110
|
+
* initialization mirrors state without emitting an event.
|
|
111
|
+
*/
|
|
112
|
+
#render(value, { focus }) {
|
|
113
|
+
this.#apply(value, { focus });
|
|
114
|
+
this.dispatch("change", { detail: { value } });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Stores `value`, syncs `aria-checked` and the roving Tab stop, the hidden
|
|
118
|
+
* field, and the fill range — without dispatching `change`. Idempotent and
|
|
119
|
+
* safe on connect (and across Turbo morphing).
|
|
120
|
+
*/
|
|
121
|
+
#apply(value, { focus }) {
|
|
122
|
+
this.valueValue = value;
|
|
123
|
+
this.symbolTargets.forEach((symbol) => {
|
|
124
|
+
symbol.setAttribute(
|
|
125
|
+
"aria-checked",
|
|
126
|
+
value > 0 && this.#symbolValue(symbol) === value ? "true" : "false"
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
this.#roving.setActive(value > 0 ? value - 1 : 0, { focus });
|
|
130
|
+
if (this.hasFieldTarget) this.fieldTarget.value = String(value);
|
|
131
|
+
this.#setFillRange(value);
|
|
132
|
+
}
|
|
133
|
+
/** Marks symbols up to `range` with `data-rating-hover` (the consumer's fill hook). */
|
|
134
|
+
#setFillRange(range) {
|
|
135
|
+
for (const symbol of this.symbolTargets) {
|
|
136
|
+
if (this.#symbolValue(symbol) <= range && range > 0) {
|
|
137
|
+
symbol.setAttribute("data-rating-hover", "");
|
|
138
|
+
} else {
|
|
139
|
+
symbol.removeAttribute("data-rating-hover");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Turns the group into a non-interactive `role="img"` snapshot of the value. */
|
|
144
|
+
#applyReadonly() {
|
|
145
|
+
const value = this.#clamp(this.valueValue);
|
|
146
|
+
this.valueValue = value;
|
|
147
|
+
this.element.setAttribute("role", "img");
|
|
148
|
+
for (const symbol of this.symbolTargets) {
|
|
149
|
+
symbol.removeAttribute("role");
|
|
150
|
+
symbol.setAttribute("aria-hidden", "true");
|
|
151
|
+
symbol.tabIndex = -1;
|
|
152
|
+
}
|
|
153
|
+
if (this.hasFieldTarget) this.fieldTarget.value = String(value);
|
|
154
|
+
this.#setFillRange(value);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Clamps `value` to `[min, max]` (min is 0 when clearable, else 1). The upper
|
|
158
|
+
* bound is also capped at the number of symbols so the roving Tab stop
|
|
159
|
+
* (`value - 1`) always maps to a real symbol — even if the consumer's `max`
|
|
160
|
+
* value and rendered symbol count disagree, the tabbable item is never lost.
|
|
161
|
+
*/
|
|
162
|
+
#clamp(value) {
|
|
163
|
+
const max = Math.min(this.maxValue, this.symbolTargets.length);
|
|
164
|
+
return Math.min(max, Math.max(this.#minValue, value));
|
|
165
|
+
}
|
|
166
|
+
/** Lowest selectable value: 0 when clearable, otherwise 1. */
|
|
167
|
+
get #minValue() {
|
|
168
|
+
return this.clearableValue ? 0 : 1;
|
|
169
|
+
}
|
|
170
|
+
/** A symbol's ordinal value (`data-rating-value`, defaulting to its position). */
|
|
171
|
+
#symbolValue(symbol) {
|
|
172
|
+
const raw = Number(symbol.getAttribute("data-rating-value"));
|
|
173
|
+
return Number.isFinite(raw) && raw > 0 ? raw : this.symbolTargets.indexOf(symbol) + 1;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export { RatingController };
|
|
178
|
+
//# sourceMappingURL=rating_controller.js.map
|
|
179
|
+
//# sourceMappingURL=rating_controller.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/relative_time_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/relative_time_controller.ts
|
|
59
|
+
var YEAR_SCALE = { limit: Number.POSITIVE_INFINITY, unit: "year", ms: 315576e5 };
|
|
60
|
+
var UNITS = [
|
|
61
|
+
{ limit: 60, unit: "second", ms: 1e3 },
|
|
62
|
+
{ limit: 3600, unit: "minute", ms: 6e4 },
|
|
63
|
+
{ limit: 86400, unit: "hour", ms: 36e5 },
|
|
64
|
+
{ limit: 604800, unit: "day", ms: 864e5 },
|
|
65
|
+
{ limit: 2629800, unit: "week", ms: 6048e5 },
|
|
66
|
+
{ limit: 31557600, unit: "month", ms: 26298e5 },
|
|
67
|
+
YEAR_SCALE
|
|
68
|
+
];
|
|
69
|
+
var RelativeTimeController = class extends Controller {
|
|
70
|
+
static values = {
|
|
71
|
+
locale: { type: String, default: "" },
|
|
72
|
+
threshold: { type: Number, default: 0 },
|
|
73
|
+
tickInterval: { type: Number, default: 6e4 }
|
|
74
|
+
};
|
|
75
|
+
#timers = new SafeTimeout();
|
|
76
|
+
/** Epoch ms parsed from `datetime`; `NaN` when absent or invalid. */
|
|
77
|
+
#targetMs = Number.NaN;
|
|
78
|
+
/** The authored absolute text, restored when the threshold fallback kicks in. */
|
|
79
|
+
#absoluteText = "";
|
|
80
|
+
connect() {
|
|
81
|
+
if (this.element.getAttribute("data-state") !== "relative") {
|
|
82
|
+
this.#absoluteText = (this.element.textContent ?? "").trim();
|
|
83
|
+
}
|
|
84
|
+
this.#targetMs = Date.parse(this.element.getAttribute("datetime") ?? "");
|
|
85
|
+
if (Number.isNaN(this.#targetMs)) return;
|
|
86
|
+
this.#schedule();
|
|
87
|
+
}
|
|
88
|
+
disconnect() {
|
|
89
|
+
this.#timers.clearAll();
|
|
90
|
+
}
|
|
91
|
+
/** Renders the current representation and reschedules unless it is now absolute. */
|
|
92
|
+
#schedule() {
|
|
93
|
+
const nextDelay = this.#applyAndComputeDelay();
|
|
94
|
+
if (nextDelay !== null) {
|
|
95
|
+
this.#timers.set(() => this.#schedule(), nextDelay);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Updates the visible text and returns the next poll delay (ms), or `null`
|
|
100
|
+
* once the absolute fallback is shown (which never changes, so stop polling).
|
|
101
|
+
*/
|
|
102
|
+
#applyAndComputeDelay() {
|
|
103
|
+
const deltaMs = this.#targetMs - Date.now();
|
|
104
|
+
const absSeconds = Math.abs(deltaMs) / 1e3;
|
|
105
|
+
if (this.thresholdValue > 0 && absSeconds >= this.thresholdValue && this.#absoluteText) {
|
|
106
|
+
this.element.textContent = this.#absoluteText;
|
|
107
|
+
this.element.setAttribute("data-state", "absolute");
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const scale = UNITS.find((u) => absSeconds < u.limit) ?? YEAR_SCALE;
|
|
111
|
+
const value = Math.round(deltaMs / scale.ms);
|
|
112
|
+
this.element.textContent = this.#formatter.format(value, scale.unit);
|
|
113
|
+
this.element.setAttribute("data-state", "relative");
|
|
114
|
+
const unitFloor = scale.unit === "second" || scale.unit === "minute" ? 6e4 : scale.ms;
|
|
115
|
+
return Math.max(this.tickIntervalValue, Math.min(unitFloor, 864e5));
|
|
116
|
+
}
|
|
117
|
+
/** A `RelativeTimeFormat` for the resolved locale (`numeric: "auto"`). */
|
|
118
|
+
get #formatter() {
|
|
119
|
+
return new Intl.RelativeTimeFormat(this.#locale, { numeric: "auto" });
|
|
120
|
+
}
|
|
121
|
+
/** Locale precedence: the value, then the element's `lang`, then the document's. */
|
|
122
|
+
get #locale() {
|
|
123
|
+
return this.localeValue || this.element.lang || document.documentElement.lang || void 0;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export { RelativeTimeController };
|
|
128
|
+
//# sourceMappingURL=relative_time_controller.js.map
|
|
129
|
+
//# sourceMappingURL=relative_time_controller.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/reset_before_cache_controller.ts
|
|
4
|
+
var ResetBeforeCacheController = class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
scope: { type: String, default: "" },
|
|
7
|
+
dispatchReset: { type: Boolean, default: true }
|
|
8
|
+
};
|
|
9
|
+
static actions = ["reset"];
|
|
10
|
+
static events = ["reset", "request"];
|
|
11
|
+
/** Runs the reset just before Turbo caches the snapshot. */
|
|
12
|
+
#onBeforeCache = () => this.reset();
|
|
13
|
+
connect() {
|
|
14
|
+
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
15
|
+
}
|
|
16
|
+
disconnect() {
|
|
17
|
+
document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resets transient UI within scope to its initial state. Asks controllers to
|
|
21
|
+
* close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
|
|
22
|
+
* and finally emits `reset`. Safe to call any number of times (idempotent).
|
|
23
|
+
*/
|
|
24
|
+
reset() {
|
|
25
|
+
const root = this.#scopeRoot();
|
|
26
|
+
if (this.dispatchResetValue) this.dispatch("request");
|
|
27
|
+
for (const element of root.querySelectorAll("[data-reset-attr]")) {
|
|
28
|
+
for (const name of (element.getAttribute("data-reset-attr") ?? "").split(/\s+/)) {
|
|
29
|
+
if (name) element.removeAttribute(name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const element of root.querySelectorAll("[data-reset-class]")) {
|
|
33
|
+
for (const token of (element.getAttribute("data-reset-class") ?? "").split(/\s+/)) {
|
|
34
|
+
if (token) element.classList.remove(token);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const element of root.querySelectorAll("[data-reset-form]")) {
|
|
38
|
+
if (element instanceof HTMLFormElement) element.reset();
|
|
39
|
+
}
|
|
40
|
+
for (const element of root.querySelectorAll("[data-reset-value]")) {
|
|
41
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
42
|
+
element.value = "";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const element of root.querySelectorAll("[data-reset-hidden]")) {
|
|
46
|
+
element.hidden = true;
|
|
47
|
+
}
|
|
48
|
+
for (const element of root.querySelectorAll("[data-reset-remove]")) {
|
|
49
|
+
element.remove();
|
|
50
|
+
}
|
|
51
|
+
this.dispatch("reset");
|
|
52
|
+
}
|
|
53
|
+
/** The scan root: a `scope` descendant when set, else the controller element. */
|
|
54
|
+
#scopeRoot() {
|
|
55
|
+
if (!this.scopeValue) return this.element;
|
|
56
|
+
return this.element.querySelector(this.scopeValue) ?? this.element;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { ResetBeforeCacheController };
|
|
61
|
+
//# sourceMappingURL=reset_before_cache_controller.js.map
|
|
62
|
+
//# sourceMappingURL=reset_before_cache_controller.js.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/resizable_controller.ts
|
|
4
|
+
var ResizableController = class extends Controller {
|
|
5
|
+
static targets = ["primary", "secondary", "separator"];
|
|
6
|
+
static values = {
|
|
7
|
+
min: { type: Number, default: 0 },
|
|
8
|
+
max: { type: Number, default: 100 },
|
|
9
|
+
step: { type: Number, default: 1 },
|
|
10
|
+
value: { type: Number, default: 50 }
|
|
11
|
+
};
|
|
12
|
+
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
13
|
+
static events = ["change"];
|
|
14
|
+
/** Track previously held value before collapse toggles. */
|
|
15
|
+
#valueBeforeCollapse = 50;
|
|
16
|
+
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
17
|
+
#dragAbort = null;
|
|
18
|
+
connect() {
|
|
19
|
+
this.#clampAndSync();
|
|
20
|
+
}
|
|
21
|
+
/** Cancels any active pointer drag so listeners never leak past disconnect. */
|
|
22
|
+
disconnect() {
|
|
23
|
+
this.#dragAbort?.abort();
|
|
24
|
+
this.#dragAbort = null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Stimulus lifecycle callback when the valueValue changes.
|
|
28
|
+
* Keeps CSS fractions and ARIA status completely aligned.
|
|
29
|
+
*/
|
|
30
|
+
valueValueChanged() {
|
|
31
|
+
this.#clampAndSync();
|
|
32
|
+
}
|
|
33
|
+
/** Starts active pointer drag tracking and locks capture. */
|
|
34
|
+
onPointerDown(event) {
|
|
35
|
+
if (!this.hasSeparatorTarget || event.button !== 0) return;
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
const separator = this.separatorTarget;
|
|
38
|
+
separator.setPointerCapture(event.pointerId);
|
|
39
|
+
separator.focus();
|
|
40
|
+
this.element.setAttribute("data-dragging", "true");
|
|
41
|
+
this.#dragAbort?.abort();
|
|
42
|
+
const abort = new AbortController();
|
|
43
|
+
this.#dragAbort = abort;
|
|
44
|
+
separator.addEventListener("pointermove", this.#onPointerMove, { signal: abort.signal });
|
|
45
|
+
separator.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
|
|
46
|
+
separator.addEventListener("pointercancel", this.#onPointerUp, { signal: abort.signal });
|
|
47
|
+
}
|
|
48
|
+
/** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
|
|
49
|
+
onKeydown(event) {
|
|
50
|
+
if (!this.hasSeparatorTarget) return;
|
|
51
|
+
const orientation = this.separatorTarget.getAttribute("aria-orientation") || "vertical";
|
|
52
|
+
const isVertical = orientation === "vertical";
|
|
53
|
+
let handled = true;
|
|
54
|
+
let nextValue = this.valueValue;
|
|
55
|
+
switch (event.key) {
|
|
56
|
+
case "ArrowLeft":
|
|
57
|
+
if (isVertical) {
|
|
58
|
+
nextValue -= this.stepValue;
|
|
59
|
+
} else {
|
|
60
|
+
handled = false;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "ArrowRight":
|
|
64
|
+
if (isVertical) {
|
|
65
|
+
nextValue += this.stepValue;
|
|
66
|
+
} else {
|
|
67
|
+
handled = false;
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "ArrowUp":
|
|
71
|
+
if (!isVertical) {
|
|
72
|
+
nextValue -= this.stepValue;
|
|
73
|
+
} else {
|
|
74
|
+
handled = false;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case "ArrowDown":
|
|
78
|
+
if (!isVertical) {
|
|
79
|
+
nextValue += this.stepValue;
|
|
80
|
+
} else {
|
|
81
|
+
handled = false;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "Home":
|
|
85
|
+
nextValue = this.minValue;
|
|
86
|
+
break;
|
|
87
|
+
case "End":
|
|
88
|
+
nextValue = this.maxValue;
|
|
89
|
+
break;
|
|
90
|
+
case "Enter":
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
this.toggle();
|
|
93
|
+
return;
|
|
94
|
+
default:
|
|
95
|
+
handled = false;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (handled) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
this.valueValue = Math.max(this.minValue, Math.min(nextValue, this.maxValue));
|
|
101
|
+
this.#clampAndSync();
|
|
102
|
+
this.#dispatchChange();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Double-click or Enter to collapse/restore pane to min/max levels. */
|
|
106
|
+
toggle() {
|
|
107
|
+
const threshold = this.minValue + (this.maxValue - this.minValue) / 2;
|
|
108
|
+
if (this.valueValue > this.minValue) {
|
|
109
|
+
this.#valueBeforeCollapse = this.valueValue;
|
|
110
|
+
this.valueValue = this.minValue;
|
|
111
|
+
} else {
|
|
112
|
+
this.valueValue = this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;
|
|
113
|
+
}
|
|
114
|
+
this.#clampAndSync();
|
|
115
|
+
this.#dispatchChange();
|
|
116
|
+
}
|
|
117
|
+
#onPointerMove = (event) => {
|
|
118
|
+
if (!this.hasSeparatorTarget) return;
|
|
119
|
+
const rect = this.element.getBoundingClientRect();
|
|
120
|
+
const orientation = this.separatorTarget.getAttribute("aria-orientation") || "vertical";
|
|
121
|
+
const isVertical = orientation === "vertical";
|
|
122
|
+
let fraction = 0.5;
|
|
123
|
+
if (isVertical) {
|
|
124
|
+
fraction = (event.clientX - rect.left) / rect.width;
|
|
125
|
+
} else {
|
|
126
|
+
fraction = (event.clientY - rect.top) / rect.height;
|
|
127
|
+
}
|
|
128
|
+
fraction = Math.max(0, Math.min(fraction, 1));
|
|
129
|
+
const percent = Math.round(fraction * 100);
|
|
130
|
+
this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));
|
|
131
|
+
this.#clampAndSync();
|
|
132
|
+
};
|
|
133
|
+
#onPointerUp = (event) => {
|
|
134
|
+
if (!this.hasSeparatorTarget) return;
|
|
135
|
+
const separator = this.separatorTarget;
|
|
136
|
+
separator.releasePointerCapture(event.pointerId);
|
|
137
|
+
this.element.removeAttribute("data-dragging");
|
|
138
|
+
this.#dragAbort?.abort();
|
|
139
|
+
this.#dragAbort = null;
|
|
140
|
+
this.#dispatchChange();
|
|
141
|
+
};
|
|
142
|
+
#clampAndSync() {
|
|
143
|
+
const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));
|
|
144
|
+
if (this.valueValue !== clamped) {
|
|
145
|
+
this.valueValue = clamped;
|
|
146
|
+
}
|
|
147
|
+
const fraction = clamped / 100;
|
|
148
|
+
this.element.style.setProperty("--stimeo--resizable-fraction", String(fraction));
|
|
149
|
+
if (this.hasSeparatorTarget) {
|
|
150
|
+
this.separatorTarget.setAttribute("aria-valuenow", String(clamped));
|
|
151
|
+
this.separatorTarget.setAttribute("aria-valuemin", String(this.minValue));
|
|
152
|
+
this.separatorTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
#dispatchChange() {
|
|
156
|
+
const fraction = this.valueValue / 100;
|
|
157
|
+
this.dispatch("change", { detail: { value: this.valueValue, fraction } });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export { ResizableController };
|
|
162
|
+
//# sourceMappingURL=resizable_controller.js.map
|
|
163
|
+
//# sourceMappingURL=resizable_controller.js.map
|