stimeo-ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +163 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +313 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +252 -0
- data/dist/controllers/combobox_controller.js +162 -23
- data/dist/controllers/command_palette_controller.js +194 -17
- data/dist/controllers/context_menu_controller.js +32 -10
- data/dist/controllers/count_up_controller.js +8 -1
- data/dist/controllers/currency_input_controller.js +147 -0
- data/dist/controllers/data_grid_controller.js +246 -0
- data/dist/controllers/date_range_picker_controller.js +441 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +169 -0
- data/dist/controllers/file_dropzone_controller.js +165 -0
- data/dist/controllers/filter_controller.js +86 -0
- data/dist/controllers/flash_controller.js +36 -5
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +67 -19
- data/dist/controllers/lazy_frame_controller.js +54 -11
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +2 -2
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +785 -0
- data/dist/controllers/multi_select_controller.js +755 -0
- data/dist/controllers/navigation_menu_controller.js +511 -0
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +246 -27
- data/dist/controllers/overflow_menu_controller.js +381 -57
- data/dist/controllers/pagination_controller.js +163 -32
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/persist_controller.js +6 -6
- data/dist/controllers/pointer_drag_controller.js +9 -1
- data/dist/controllers/popover_controller.js +2 -2
- data/dist/controllers/radio_group_controller.js +22 -3
- data/dist/controllers/range_slider_controller.js +192 -0
- data/dist/controllers/rating_controller.js +16 -2
- data/dist/controllers/read_more_controller.js +238 -0
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +101 -14
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +40 -6
- data/dist/controllers/scrollspy_controller.js +369 -74
- data/dist/controllers/separator_controller.js +96 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/skeleton_controller.js +1 -1
- data/dist/controllers/slider_controller.js +32 -6
- data/dist/controllers/sortable_controller.js +34 -3
- data/dist/controllers/spinner_controller.js +1 -1
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +9 -4
- data/dist/controllers/sticky_observer_controller.js +109 -20
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +295 -0
- data/dist/controllers/theme_controller.js +42 -13
- data/dist/controllers/time_picker_controller.js +231 -0
- data/dist/controllers/toast_controller.js +40 -14
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +691 -0
- data/dist/index.js +4256 -915
- data/lib/stimeo/ui/version.rb +2 -3
- metadata +28 -2
|
@@ -1,7 +1,53 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/pagination_controller.ts
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// src/utils/focus_candidate.ts
|
|
6
|
+
function inheritsFieldsetDisabled(control) {
|
|
7
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
8
|
+
while (fieldset) {
|
|
9
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
10
|
+
if (!legend?.contains(control)) return true;
|
|
11
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function canTakeFocus(element) {
|
|
16
|
+
if (element.closest("[hidden]")) return false;
|
|
17
|
+
if (!("disabled" in element)) return true;
|
|
18
|
+
if (element.disabled) return false;
|
|
19
|
+
return !inheritsFieldsetDisabled(element);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/utils/tabindex_loan.ts
|
|
23
|
+
var TabindexLoan = class {
|
|
24
|
+
#value;
|
|
25
|
+
#lent = /* @__PURE__ */ new Set();
|
|
26
|
+
/**
|
|
27
|
+
* @param value - the `tabindex` to lend. `"-1"` (the default) is
|
|
28
|
+
* programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
|
|
29
|
+
* which a scroll region with no focusable content of its own needs.
|
|
30
|
+
*/
|
|
31
|
+
constructor(value = "-1") {
|
|
32
|
+
this.#value = value;
|
|
33
|
+
}
|
|
34
|
+
/** Lends `element` the value; no-ops when it already carries a `tabindex`. */
|
|
35
|
+
lend(element) {
|
|
36
|
+
if (element.hasAttribute("tabindex")) return;
|
|
37
|
+
element.setAttribute("tabindex", this.#value);
|
|
38
|
+
this.#lent.add(element);
|
|
39
|
+
}
|
|
40
|
+
/** Takes back every loan whose value is still the one that was lent. */
|
|
41
|
+
returnAll() {
|
|
42
|
+
for (const element of this.#lent) {
|
|
43
|
+
if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
|
|
44
|
+
}
|
|
45
|
+
this.#lent.clear();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/controllers/pagination_controller.ts
|
|
50
|
+
var PaginationController = class _PaginationController extends Controller {
|
|
5
51
|
static targets = ["page", "prev", "next"];
|
|
6
52
|
static values = {
|
|
7
53
|
page: { type: Number, default: 1 },
|
|
@@ -9,79 +55,164 @@ var PaginationController = class extends Controller {
|
|
|
9
55
|
};
|
|
10
56
|
static actions = ["next", "prev", "select"];
|
|
11
57
|
static events = ["change"];
|
|
58
|
+
/** Marks a `disabled` this controller applied at a boundary (ownership flag). */
|
|
59
|
+
static #BOUNDARY_ATTR = "data-stimeo--pagination-boundary-disabled";
|
|
60
|
+
#isConnected = false;
|
|
61
|
+
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
62
|
+
#tabindex = new TabindexLoan();
|
|
12
63
|
/** Normalizes out-of-range initial values and renders the initial state. */
|
|
13
64
|
connect() {
|
|
14
|
-
this
|
|
15
|
-
this
|
|
65
|
+
this.#isConnected = true;
|
|
66
|
+
this.#normalizeAndRender();
|
|
67
|
+
}
|
|
68
|
+
/** Reverts the one attribute the controller adds outside its state hooks. */
|
|
69
|
+
disconnect() {
|
|
70
|
+
this.#isConnected = false;
|
|
71
|
+
this.#tabindex.returnAll();
|
|
72
|
+
}
|
|
73
|
+
/** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
|
|
74
|
+
pageValueChanged() {
|
|
75
|
+
if (!this.#isConnected) return;
|
|
76
|
+
this.#normalizeAndRender();
|
|
77
|
+
}
|
|
78
|
+
/** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
|
|
79
|
+
totalValueChanged() {
|
|
80
|
+
if (!this.#isConnected) return;
|
|
81
|
+
this.#normalizeAndRender();
|
|
82
|
+
}
|
|
83
|
+
/** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
|
|
84
|
+
pageTargetConnected() {
|
|
85
|
+
if (!this.#isConnected) return;
|
|
86
|
+
this.#render();
|
|
87
|
+
}
|
|
88
|
+
/** Syncs a `prev` button appended/replaced at runtime. */
|
|
89
|
+
prevTargetConnected() {
|
|
90
|
+
if (!this.#isConnected) return;
|
|
91
|
+
this.#render();
|
|
92
|
+
}
|
|
93
|
+
/** Syncs a `next` button appended/replaced at runtime. */
|
|
94
|
+
nextTargetConnected() {
|
|
95
|
+
if (!this.#isConnected) return;
|
|
16
96
|
this.#render();
|
|
17
97
|
}
|
|
18
98
|
/** Makes the clicked page button (its `data-page`) current. */
|
|
19
99
|
select(event) {
|
|
20
100
|
const button = event.currentTarget;
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
101
|
+
const raw = button.dataset.page;
|
|
102
|
+
if (raw === void 0 || raw.trim() === "") return;
|
|
103
|
+
const page = Number(raw);
|
|
104
|
+
if (!Number.isInteger(page)) return;
|
|
23
105
|
this.#goto(page);
|
|
24
106
|
}
|
|
25
107
|
/** Steps to the previous page. */
|
|
26
108
|
prev() {
|
|
27
|
-
this.#goto(this
|
|
109
|
+
this.#goto(this.#page - 1);
|
|
28
110
|
}
|
|
29
111
|
/** Steps to the next page. */
|
|
30
112
|
next() {
|
|
31
|
-
this.#goto(this
|
|
113
|
+
this.#goto(this.#page + 1);
|
|
32
114
|
}
|
|
33
115
|
/** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */
|
|
34
116
|
#goto(page) {
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
if (!Number.isFinite(page)) return;
|
|
118
|
+
const previous = this.#page;
|
|
119
|
+
const target = this.#clamp(page);
|
|
120
|
+
if (target === previous) return;
|
|
39
121
|
this.pageValue = target;
|
|
40
122
|
this.#render();
|
|
41
123
|
this.dispatch("change", {
|
|
42
|
-
detail: { page: target, total: this
|
|
124
|
+
detail: { page: target, total: this.#total, previous }
|
|
43
125
|
});
|
|
44
126
|
}
|
|
127
|
+
/** Writes the normalized Values back (only when they differ) and renders. */
|
|
128
|
+
#normalizeAndRender() {
|
|
129
|
+
const total = this.#total;
|
|
130
|
+
if (!Object.is(total, this.totalValue)) this.totalValue = total;
|
|
131
|
+
const page = this.#page;
|
|
132
|
+
if (!Object.is(page, this.pageValue)) this.pageValue = page;
|
|
133
|
+
this.#render();
|
|
134
|
+
}
|
|
45
135
|
/** Syncs `aria-current` on the page buttons and the prev/next `disabled` state. */
|
|
46
136
|
#render() {
|
|
137
|
+
const page = this.#page;
|
|
47
138
|
for (const button of this.pageTargets) {
|
|
48
|
-
if (Number(button.dataset.page) ===
|
|
139
|
+
if (Number(button.dataset.page) === page) {
|
|
49
140
|
button.setAttribute("aria-current", "page");
|
|
50
141
|
} else {
|
|
51
142
|
button.removeAttribute("aria-current");
|
|
52
143
|
}
|
|
53
144
|
}
|
|
54
|
-
this
|
|
55
|
-
this
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
145
|
+
const prev = this.hasPrevTarget ? this.prevTarget : null;
|
|
146
|
+
const next = this.hasNextTarget ? this.nextTarget : null;
|
|
147
|
+
const atStart = page <= 1;
|
|
148
|
+
const atEnd = page >= this.#total;
|
|
149
|
+
const prevStaysDisabled = this.#disabledAfter(prev, atStart);
|
|
150
|
+
const nextStaysDisabled = this.#disabledAfter(next, atEnd);
|
|
151
|
+
this.#release(prev, atStart);
|
|
152
|
+
this.#release(next, atEnd);
|
|
153
|
+
this.#disable(prev, atStart, nextStaysDisabled ? null : next);
|
|
154
|
+
this.#disable(next, atEnd, prevStaysDisabled ? null : prev);
|
|
155
|
+
}
|
|
156
|
+
/** Whether a boundary button will still be `disabled` once this render applies. */
|
|
157
|
+
#disabledAfter(button, atBoundary) {
|
|
158
|
+
if (!button) return true;
|
|
159
|
+
if (atBoundary) return true;
|
|
160
|
+
return button.disabled && !this.#owns(button);
|
|
161
|
+
}
|
|
162
|
+
/** Releases the boundary `disabled` this controller owns, once away from it. */
|
|
163
|
+
#release(button, atBoundary) {
|
|
164
|
+
if (!button || atBoundary || !this.#owns(button)) return;
|
|
165
|
+
button.disabled = false;
|
|
166
|
+
button.removeAttribute(_PaginationController.#BOUNDARY_ATTR);
|
|
59
167
|
}
|
|
60
168
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
169
|
+
* Disables a boundary button, first moving focus off it when it is the active
|
|
170
|
+
* element so disabling never strands focus.
|
|
63
171
|
*/
|
|
64
|
-
#
|
|
65
|
-
if (!button) return;
|
|
66
|
-
if (disabled && button
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
button.
|
|
172
|
+
#disable(button, atBoundary, opposite) {
|
|
173
|
+
if (!button || !atBoundary) return;
|
|
174
|
+
if (button.disabled && !this.#owns(button)) return;
|
|
175
|
+
if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
|
|
176
|
+
button.disabled = true;
|
|
177
|
+
button.setAttribute(_PaginationController.#BOUNDARY_ATTR, "");
|
|
70
178
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Moves focus to `opposite` (already resolved to `null` when it will stay
|
|
181
|
+
* disabled), else to the current page button, else to the landmark itself.
|
|
182
|
+
*/
|
|
183
|
+
#moveFocusAwayFrom(opposite) {
|
|
184
|
+
const page = this.#page;
|
|
74
185
|
const currentPage = this.pageTargets.find(
|
|
75
|
-
(candidate) => Number(candidate.dataset.page) ===
|
|
186
|
+
(candidate) => Number(candidate.dataset.page) === page
|
|
187
|
+
);
|
|
188
|
+
const destination = [opposite, currentPage].find(
|
|
189
|
+
(candidate) => candidate != null && canTakeFocus(candidate)
|
|
76
190
|
);
|
|
77
|
-
const destination = opposite && !opposite.disabled ? opposite : currentPage;
|
|
78
191
|
if (destination) {
|
|
79
192
|
destination.focus();
|
|
80
193
|
return;
|
|
81
194
|
}
|
|
82
|
-
|
|
195
|
+
this.#tabindex.lend(this.element);
|
|
83
196
|
this.element.focus();
|
|
84
197
|
}
|
|
198
|
+
/** Total pages, normalized to a finite integer >= 1. */
|
|
199
|
+
get #total() {
|
|
200
|
+
const total = this.totalValue;
|
|
201
|
+
return Number.isFinite(total) ? Math.max(1, Math.trunc(total)) : 1;
|
|
202
|
+
}
|
|
203
|
+
/** The current page, normalized into `[1, total]`. */
|
|
204
|
+
get #page() {
|
|
205
|
+
return this.#clamp(this.pageValue);
|
|
206
|
+
}
|
|
207
|
+
/** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */
|
|
208
|
+
#clamp(page) {
|
|
209
|
+
if (!Number.isFinite(page)) return 1;
|
|
210
|
+
return Math.min(this.#total, Math.max(1, Math.trunc(page)));
|
|
211
|
+
}
|
|
212
|
+
/** Whether the button's current `disabled` was applied by boundary control. */
|
|
213
|
+
#owns(button) {
|
|
214
|
+
return button.hasAttribute(_PaginationController.#BOUNDARY_ATTR);
|
|
215
|
+
}
|
|
85
216
|
};
|
|
86
217
|
|
|
87
218
|
export { PaginationController };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/password_reveal_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/safe_timeout.ts
|
|
6
|
+
var TimerRegistry = class {
|
|
7
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
8
|
+
ids = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Cancels a single tracked timer.
|
|
11
|
+
*
|
|
12
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
13
|
+
* registry), so callers can clear defensively without guarding.
|
|
14
|
+
*/
|
|
15
|
+
clear(id) {
|
|
16
|
+
if (this.ids.delete(id)) {
|
|
17
|
+
this.cancel(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
22
|
+
* to guarantee no timer outlives the element.
|
|
23
|
+
*/
|
|
24
|
+
clearAll() {
|
|
25
|
+
for (const id of this.ids) {
|
|
26
|
+
this.cancel(id);
|
|
27
|
+
}
|
|
28
|
+
this.ids.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Number of timers currently tracked (pending). */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.ids.size;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
36
|
+
/**
|
|
37
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
38
|
+
*
|
|
39
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
40
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
41
|
+
*/
|
|
42
|
+
set(callback, delay) {
|
|
43
|
+
const id = this.schedule(() => {
|
|
44
|
+
this.ids.delete(id);
|
|
45
|
+
callback();
|
|
46
|
+
}, delay);
|
|
47
|
+
this.ids.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
schedule(callback, delay) {
|
|
51
|
+
return window.setTimeout(callback, delay);
|
|
52
|
+
}
|
|
53
|
+
cancel(id) {
|
|
54
|
+
window.clearTimeout(id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/controllers/password_reveal_controller.ts
|
|
59
|
+
var PasswordRevealController = class extends Controller {
|
|
60
|
+
static targets = ["input", "toggle"];
|
|
61
|
+
static values = {
|
|
62
|
+
autoHide: { type: Number, default: 0 }
|
|
63
|
+
};
|
|
64
|
+
static actions = ["toggle"];
|
|
65
|
+
static events = ["toggle"];
|
|
66
|
+
/** Auto re-mask timer; torn down on disconnect. */
|
|
67
|
+
#timers = new SafeTimeout();
|
|
68
|
+
connect() {
|
|
69
|
+
this.#reflect(this.#isVisible);
|
|
70
|
+
}
|
|
71
|
+
disconnect() {
|
|
72
|
+
this.#timers.clearAll();
|
|
73
|
+
}
|
|
74
|
+
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
75
|
+
toggle() {
|
|
76
|
+
this.#setVisible(!this.#isVisible);
|
|
77
|
+
}
|
|
78
|
+
/** Whether the input is currently revealed (`type="text"`). */
|
|
79
|
+
get #isVisible() {
|
|
80
|
+
return this.hasInputTarget && this.inputTarget.type === "text";
|
|
81
|
+
}
|
|
82
|
+
/** Switches the masked/revealed state, preserving focus and caret. */
|
|
83
|
+
#setVisible(visible) {
|
|
84
|
+
if (!this.hasInputTarget) return;
|
|
85
|
+
const input = this.inputTarget;
|
|
86
|
+
const restoreInputFocus = document.activeElement === input;
|
|
87
|
+
const selectionStart = input.selectionStart;
|
|
88
|
+
const selectionEnd = input.selectionEnd;
|
|
89
|
+
input.type = visible ? "text" : "password";
|
|
90
|
+
if (restoreInputFocus) {
|
|
91
|
+
input.focus();
|
|
92
|
+
if (selectionStart !== null && selectionEnd !== null) {
|
|
93
|
+
try {
|
|
94
|
+
input.setSelectionRange(selectionStart, selectionEnd);
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.#reflect(visible);
|
|
100
|
+
this.dispatch("toggle", { detail: { visible } });
|
|
101
|
+
this.#timers.clearAll();
|
|
102
|
+
if (visible && this.autoHideValue > 0) {
|
|
103
|
+
this.#timers.set(() => this.#setVisible(false), this.autoHideValue);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Reflects the visible state onto `aria-pressed` and `data-state`. */
|
|
107
|
+
#reflect(visible) {
|
|
108
|
+
if (this.hasToggleTarget) {
|
|
109
|
+
this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
110
|
+
}
|
|
111
|
+
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export { PasswordRevealController };
|
|
116
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
117
|
+
//# sourceMappingURL=password_reveal_controller.js.map
|
|
@@ -182,12 +182,12 @@ var PersistController = class extends Controller {
|
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
184
|
* Persistable fields paired with a stable storage key. Uniquely-named fields key
|
|
185
|
-
* by their name
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
185
|
+
* by their name. Repeated same-name fields (e.g. a `tags[]` checkbox group or
|
|
186
|
+
* array text inputs) are disambiguated by DOM-order occurrence — the first keeps
|
|
187
|
+
* its plain `name`, later ones get a NUL-separated index suffix — so each is
|
|
188
|
+
* stored and restored individually instead of the last one clobbering the rest.
|
|
189
|
+
* Radios are the exception: a group intentionally shares one key (one value per
|
|
190
|
+
* name).
|
|
191
191
|
*/
|
|
192
192
|
#fieldEntries() {
|
|
193
193
|
const entries = [];
|
|
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/pointer_drag_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/arrow_step.ts
|
|
6
|
+
function isReservedArrowChord(event, allow = []) {
|
|
7
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
8
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
// src/utils/detach_gate.ts
|
|
6
12
|
var DetachGate = class _DetachGate {
|
|
7
13
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
@@ -186,8 +192,10 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
186
192
|
if (this.disabledValue) return;
|
|
187
193
|
const handle = this.#handleFor(event.target);
|
|
188
194
|
if (!handle) return;
|
|
195
|
+
if (event.defaultPrevented) return;
|
|
196
|
+
if (isReservedArrowChord(event)) return;
|
|
189
197
|
if (event.key === "Escape") {
|
|
190
|
-
if (event.
|
|
198
|
+
if (event.isComposing) return;
|
|
191
199
|
if (this.#pointer?.started) {
|
|
192
200
|
const { pointerType } = this.#pointer;
|
|
193
201
|
this.#teardownSessions();
|
|
@@ -133,7 +133,7 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
133
133
|
/** Starts closed and registers the standing dismissal listeners. */
|
|
134
134
|
connect() {
|
|
135
135
|
this.close();
|
|
136
|
-
document.addEventListener("click", this.#onOutsideClick);
|
|
136
|
+
document.addEventListener("click", this.#onOutsideClick, true);
|
|
137
137
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
@@ -144,7 +144,7 @@ var PopoverController = class _PopoverController extends Controller {
|
|
|
144
144
|
*/
|
|
145
145
|
disconnect() {
|
|
146
146
|
this.#escapeLayer.deactivate();
|
|
147
|
-
document.removeEventListener("click", this.#onOutsideClick);
|
|
147
|
+
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
148
148
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
149
149
|
this.#stopScrollDismiss?.();
|
|
150
150
|
this.#stopScrollDismiss = null;
|
|
@@ -2,6 +2,24 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/radio_group_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowStep(key, element) {
|
|
12
|
+
if (key === "ArrowDown") return 1;
|
|
13
|
+
if (key === "ArrowUp") return -1;
|
|
14
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
|
|
15
|
+
const forward = isRtl(element) ? "ArrowLeft" : "ArrowRight";
|
|
16
|
+
return key === forward ? 1 : -1;
|
|
17
|
+
}
|
|
18
|
+
function isReservedArrowChord(event, allow = []) {
|
|
19
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
20
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
// src/utils/roving_tabindex.ts
|
|
6
24
|
var RovingTabindex = class {
|
|
7
25
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -59,17 +77,18 @@ var RadioGroupController = class extends Controller {
|
|
|
59
77
|
}
|
|
60
78
|
/** Arrow/Home/End/Space navigation with selection-follows-focus. */
|
|
61
79
|
onKeydown(event) {
|
|
80
|
+
if (event.defaultPrevented) return;
|
|
81
|
+
if (isReservedArrowChord(event)) return;
|
|
62
82
|
const current = this.radioTargets.indexOf(event.currentTarget);
|
|
63
83
|
if (current === -1) return;
|
|
64
84
|
let next = null;
|
|
85
|
+
const step = logicalArrowStep(event.key, this.element);
|
|
65
86
|
switch (event.key) {
|
|
66
87
|
case "ArrowDown":
|
|
67
88
|
case "ArrowRight":
|
|
68
|
-
next = rovingMove(current, this.radioTargets.length, 1);
|
|
69
|
-
break;
|
|
70
89
|
case "ArrowUp":
|
|
71
90
|
case "ArrowLeft":
|
|
72
|
-
next = rovingMove(current, this.radioTargets.length,
|
|
91
|
+
next = rovingMove(current, this.radioTargets.length, step);
|
|
73
92
|
break;
|
|
74
93
|
case "Home":
|
|
75
94
|
next = 0;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/range_slider_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowKey(key, element) {
|
|
12
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
|
+
if (!isRtl(element)) return key;
|
|
14
|
+
return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
|
|
15
|
+
}
|
|
16
|
+
function isReservedArrowChord(event, allow = []) {
|
|
17
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
18
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/controllers/range_slider_controller.ts
|
|
22
|
+
var START_PROPERTY = "--stimeo-range-start";
|
|
23
|
+
var END_PROPERTY = "--stimeo-range-end";
|
|
24
|
+
var RangeSliderController = class extends Controller {
|
|
25
|
+
static targets = ["track", "startThumb", "endThumb"];
|
|
26
|
+
static values = {
|
|
27
|
+
min: { type: Number, default: 0 },
|
|
28
|
+
max: { type: Number, default: 100 },
|
|
29
|
+
step: { type: Number, default: 1 },
|
|
30
|
+
start: { type: Number, default: 0 },
|
|
31
|
+
end: { type: Number, default: 100 },
|
|
32
|
+
logicalTrack: { type: Boolean, default: false }
|
|
33
|
+
};
|
|
34
|
+
static actions = ["onKeydown", "onPointerDown"];
|
|
35
|
+
static events = ["change"];
|
|
36
|
+
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
37
|
+
#dragAbort = null;
|
|
38
|
+
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
39
|
+
get #mirrored() {
|
|
40
|
+
return this.logicalTrackValue && isRtl(this.element);
|
|
41
|
+
}
|
|
42
|
+
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
43
|
+
connect() {
|
|
44
|
+
const lo = Math.min(this.startValue, this.endValue);
|
|
45
|
+
const hi = Math.max(this.startValue, this.endValue);
|
|
46
|
+
this.#commit(lo, hi, false);
|
|
47
|
+
}
|
|
48
|
+
/** Cancels any active pointer drag so document listeners never leak. */
|
|
49
|
+
disconnect() {
|
|
50
|
+
this.#dragAbort?.abort();
|
|
51
|
+
this.#dragAbort = null;
|
|
52
|
+
}
|
|
53
|
+
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
54
|
+
onKeydown(event) {
|
|
55
|
+
if (isReservedArrowChord(event)) return;
|
|
56
|
+
const thumb = event.currentTarget;
|
|
57
|
+
const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
|
|
58
|
+
const current = isStart ? this.startValue : this.endValue;
|
|
59
|
+
const lower = isStart ? this.minValue : this.startValue;
|
|
60
|
+
const upper = isStart ? this.endValue : this.maxValue;
|
|
61
|
+
const big = this.stepValue * 10;
|
|
62
|
+
let next = null;
|
|
63
|
+
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
64
|
+
case "ArrowRight":
|
|
65
|
+
case "ArrowUp":
|
|
66
|
+
next = current + this.stepValue;
|
|
67
|
+
break;
|
|
68
|
+
case "ArrowLeft":
|
|
69
|
+
case "ArrowDown":
|
|
70
|
+
next = current - this.stepValue;
|
|
71
|
+
break;
|
|
72
|
+
case "PageUp":
|
|
73
|
+
next = current + big;
|
|
74
|
+
break;
|
|
75
|
+
case "PageDown":
|
|
76
|
+
next = current - big;
|
|
77
|
+
break;
|
|
78
|
+
case "Home":
|
|
79
|
+
next = lower;
|
|
80
|
+
break;
|
|
81
|
+
case "End":
|
|
82
|
+
next = upper;
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
this.#moveThumb(isStart, next);
|
|
89
|
+
}
|
|
90
|
+
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
91
|
+
onPointerDown(event) {
|
|
92
|
+
if (!this.hasTrackTarget) return;
|
|
93
|
+
const mirrored = this.#mirrored;
|
|
94
|
+
const value = this.#valueFromClientX(event.clientX, mirrored);
|
|
95
|
+
if (value === null) return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
const useStart = Math.abs(value - this.startValue) <= Math.abs(value - this.endValue);
|
|
98
|
+
if (useStart) {
|
|
99
|
+
if (this.hasStartThumbTarget) this.startThumbTarget.focus();
|
|
100
|
+
} else if (this.hasEndThumbTarget) {
|
|
101
|
+
this.endThumbTarget.focus();
|
|
102
|
+
}
|
|
103
|
+
this.#moveThumb(useStart, value);
|
|
104
|
+
this.#dragAbort?.abort();
|
|
105
|
+
const abort = new AbortController();
|
|
106
|
+
this.#dragAbort = abort;
|
|
107
|
+
const onMove = (move) => {
|
|
108
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored);
|
|
109
|
+
if (moved !== null) this.#moveThumb(useStart, moved);
|
|
110
|
+
};
|
|
111
|
+
const onUp = () => {
|
|
112
|
+
abort.abort();
|
|
113
|
+
this.#dragAbort = null;
|
|
114
|
+
};
|
|
115
|
+
document.addEventListener("pointermove", onMove, { signal: abort.signal });
|
|
116
|
+
document.addEventListener("pointerup", onUp, { signal: abort.signal });
|
|
117
|
+
document.addEventListener("pointercancel", onUp, { signal: abort.signal });
|
|
118
|
+
}
|
|
119
|
+
/** Maps a pointer X coordinate to a raw value using the track geometry. */
|
|
120
|
+
#valueFromClientX(clientX, mirrored) {
|
|
121
|
+
const rect = this.trackTarget.getBoundingClientRect();
|
|
122
|
+
if (rect.width === 0) return null;
|
|
123
|
+
const offset = (clientX - rect.left) / rect.width;
|
|
124
|
+
const fraction = mirrored ? 1 - offset : offset;
|
|
125
|
+
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
126
|
+
}
|
|
127
|
+
/** Moves one thumb to a new raw value, keeping the pair ordered. */
|
|
128
|
+
#moveThumb(isStart, raw) {
|
|
129
|
+
if (isStart) {
|
|
130
|
+
this.#commit(raw, this.endValue, true);
|
|
131
|
+
} else {
|
|
132
|
+
this.#commit(this.startValue, raw, true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
|
|
137
|
+
* reflects it onto the thumbs' ARIA attributes and the range custom
|
|
138
|
+
* properties. Dispatches `change` only on user-driven updates (`notify`).
|
|
139
|
+
*/
|
|
140
|
+
#commit(start, end, notify) {
|
|
141
|
+
const prevStart = this.startValue;
|
|
142
|
+
const prevEnd = this.endValue;
|
|
143
|
+
let nextStart = this.#snap(start);
|
|
144
|
+
let nextEnd = this.#snap(end);
|
|
145
|
+
if (nextStart > nextEnd) {
|
|
146
|
+
if (isUserMovingStart(start, prevStart, end, prevEnd)) nextStart = nextEnd;
|
|
147
|
+
else nextEnd = nextStart;
|
|
148
|
+
}
|
|
149
|
+
this.startValue = nextStart;
|
|
150
|
+
this.endValue = nextEnd;
|
|
151
|
+
this.#render(nextStart, nextEnd);
|
|
152
|
+
if (notify && (nextStart !== prevStart || nextEnd !== prevEnd)) {
|
|
153
|
+
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Reflects the current pair onto thumb ARIA attributes and CSS properties. */
|
|
157
|
+
#render(start, end) {
|
|
158
|
+
if (this.hasStartThumbTarget) {
|
|
159
|
+
this.startThumbTarget.setAttribute("aria-valuemin", String(this.minValue));
|
|
160
|
+
this.startThumbTarget.setAttribute("aria-valuemax", String(end));
|
|
161
|
+
this.startThumbTarget.setAttribute("aria-valuenow", String(start));
|
|
162
|
+
}
|
|
163
|
+
if (this.hasEndThumbTarget) {
|
|
164
|
+
this.endThumbTarget.setAttribute("aria-valuemin", String(start));
|
|
165
|
+
this.endThumbTarget.setAttribute("aria-valuemax", String(this.maxValue));
|
|
166
|
+
this.endThumbTarget.setAttribute("aria-valuenow", String(end));
|
|
167
|
+
}
|
|
168
|
+
const span = this.maxValue - this.minValue;
|
|
169
|
+
this.element.style.setProperty(
|
|
170
|
+
START_PROPERTY,
|
|
171
|
+
String(span > 0 ? (start - this.minValue) / span : 0)
|
|
172
|
+
);
|
|
173
|
+
this.element.style.setProperty(
|
|
174
|
+
END_PROPERTY,
|
|
175
|
+
String(span > 0 ? (end - this.minValue) / span : 0)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
/** Clamps `raw` to `[min, max]` and snaps it to the nearest step from `min`. */
|
|
179
|
+
#snap(raw) {
|
|
180
|
+
const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));
|
|
181
|
+
if (this.stepValue <= 0) return clamped;
|
|
182
|
+
const stepped = Math.round((clamped - this.minValue) / this.stepValue) * this.stepValue + this.minValue;
|
|
183
|
+
return Math.min(this.maxValue, Math.max(this.minValue, stepped));
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function isUserMovingStart(start, prevStart, end, prevEnd) {
|
|
187
|
+
return start !== prevStart && end === prevEnd;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { RangeSliderController };
|
|
191
|
+
//# sourceMappingURL=range_slider_controller.js.map
|
|
192
|
+
//# sourceMappingURL=range_slider_controller.js.map
|