stimeo-ui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/carousel_controller.d.ts +100 -0
- package/dist/controllers/carousel_controller.js +272 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +76 -0
- package/dist/controllers/color_picker_controller.js +213 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +71 -0
- package/dist/controllers/data_grid_controller.js +168 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +87 -0
- package/dist/controllers/date_range_picker_controller.js +417 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +168 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +41 -18
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +33 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menubar_controller.d.ts +62 -0
- package/dist/controllers/menubar_controller.js +433 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/multi_select_controller.d.ts +115 -0
- package/dist/controllers/multi_select_controller.js +472 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +99 -0
- package/dist/controllers/navigation_menu_controller.js +384 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
- package/dist/controllers/overflow_indicator_controller.js +178 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/range_slider_controller.d.ts +87 -0
- package/dist/controllers/range_slider_controller.js +166 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +194 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.js +15 -2
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.js +8 -4
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +33 -11
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -0
- package/dist/controllers/separator_controller.js +87 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.js +8 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +88 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +3 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +275 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.js +20 -10
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +212 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +1 -9
- package/dist/controllers/toast_controller.js +36 -9
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +54 -0
- package/dist/controllers/tree_view_controller.js +275 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +811 -295
- package/dist/index.js.map +1 -1
- package/dist/inspector/examples.json +2 -2
- package/dist/inspector/manifest.json +15 -3
- package/package.json +1 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/date_range_picker_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/dates.ts
|
|
6
|
+
function toISODateString(date) {
|
|
7
|
+
const year = date.getFullYear();
|
|
8
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
9
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
10
|
+
return `${year}-${month}-${day}`;
|
|
11
|
+
}
|
|
12
|
+
function parseISODateString(dateStr) {
|
|
13
|
+
if (!dateStr) return null;
|
|
14
|
+
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
const [, y, m, d] = match;
|
|
17
|
+
const year = Number(y);
|
|
18
|
+
const month = Number(m);
|
|
19
|
+
const day = Number(d);
|
|
20
|
+
const date = new Date(year, month - 1, day);
|
|
21
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return date;
|
|
25
|
+
}
|
|
26
|
+
function parseISOMonthString(monthStr) {
|
|
27
|
+
if (!monthStr) return null;
|
|
28
|
+
const match = monthStr.match(/^(\d{4})-(\d{2})$/);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
const [, y, m] = match;
|
|
31
|
+
const month = Number(m);
|
|
32
|
+
if (month < 1 || month > 12) return null;
|
|
33
|
+
return { year: Number(y), month };
|
|
34
|
+
}
|
|
35
|
+
function toISOMonthString(date) {
|
|
36
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/utils/safe_timeout.ts
|
|
40
|
+
var TimerRegistry = class {
|
|
41
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
42
|
+
ids = /* @__PURE__ */ new Set();
|
|
43
|
+
/**
|
|
44
|
+
* Cancels a single tracked timer.
|
|
45
|
+
*
|
|
46
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
47
|
+
* registry), so callers can clear defensively without guarding.
|
|
48
|
+
*/
|
|
49
|
+
clear(id) {
|
|
50
|
+
if (this.ids.delete(id)) {
|
|
51
|
+
this.cancel(id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
56
|
+
* to guarantee no timer outlives the element.
|
|
57
|
+
*/
|
|
58
|
+
clearAll() {
|
|
59
|
+
for (const id of this.ids) {
|
|
60
|
+
this.cancel(id);
|
|
61
|
+
}
|
|
62
|
+
this.ids.clear();
|
|
63
|
+
}
|
|
64
|
+
/** Number of timers currently tracked (pending). */
|
|
65
|
+
get size() {
|
|
66
|
+
return this.ids.size;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
70
|
+
/**
|
|
71
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
72
|
+
*
|
|
73
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
74
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
75
|
+
*/
|
|
76
|
+
set(callback, delay) {
|
|
77
|
+
const id = this.schedule(() => {
|
|
78
|
+
this.ids.delete(id);
|
|
79
|
+
callback();
|
|
80
|
+
}, delay);
|
|
81
|
+
this.ids.add(id);
|
|
82
|
+
return id;
|
|
83
|
+
}
|
|
84
|
+
schedule(callback, delay) {
|
|
85
|
+
return window.setTimeout(callback, delay);
|
|
86
|
+
}
|
|
87
|
+
cancel(id) {
|
|
88
|
+
window.clearTimeout(id);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/controllers/date_range_picker_controller.ts
|
|
93
|
+
var GRID_SIZE = 42;
|
|
94
|
+
var DateRangePickerController = class extends Controller {
|
|
95
|
+
static targets = ["grid", "monthLabel", "cell", "status", "startField", "endField"];
|
|
96
|
+
static values = {
|
|
97
|
+
min: { type: String, default: "" },
|
|
98
|
+
max: { type: String, default: "" }
|
|
99
|
+
};
|
|
100
|
+
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
101
|
+
static events = ["change"];
|
|
102
|
+
/** The month currently rendered, as `YYYY-MM`. */
|
|
103
|
+
#viewMonth = "";
|
|
104
|
+
/** The confirmed range endpoints (ISO), or "" when unset. */
|
|
105
|
+
#startDate = "";
|
|
106
|
+
#endDate = "";
|
|
107
|
+
/** The first endpoint of an in-progress selection (ISO), or "" when idle. */
|
|
108
|
+
#pendingStart = "";
|
|
109
|
+
/** The hovered/focused date previewed while selecting (ISO), or "". */
|
|
110
|
+
#previewDate = "";
|
|
111
|
+
/** The roving-focus date in the grid (local time). */
|
|
112
|
+
#focusedDate = /* @__PURE__ */ new Date();
|
|
113
|
+
/** Deferred focus after an async month transition (cancelled on teardown). */
|
|
114
|
+
#focusTimer = new SafeTimeout();
|
|
115
|
+
/** Seeds the range from any pre-filled hidden fields and renders the grid. */
|
|
116
|
+
connect() {
|
|
117
|
+
this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
|
|
118
|
+
this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
|
|
119
|
+
const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
|
|
120
|
+
this.#focusedDate = anchor;
|
|
121
|
+
this.#viewMonth = toISOMonthString(anchor);
|
|
122
|
+
this.#render();
|
|
123
|
+
}
|
|
124
|
+
/** Cancels any pending deferred focus so it never fires on a detached element. */
|
|
125
|
+
disconnect() {
|
|
126
|
+
this.#focusTimer.clearAll();
|
|
127
|
+
}
|
|
128
|
+
/** Navigates to the previous month. */
|
|
129
|
+
prev(event) {
|
|
130
|
+
event?.preventDefault();
|
|
131
|
+
this.#shiftMonth(-1);
|
|
132
|
+
}
|
|
133
|
+
/** Navigates to the next month. */
|
|
134
|
+
next(event) {
|
|
135
|
+
event?.preventDefault();
|
|
136
|
+
this.#shiftMonth(1);
|
|
137
|
+
}
|
|
138
|
+
/** Confirms a range endpoint from a clicked cell. */
|
|
139
|
+
selectDate(event) {
|
|
140
|
+
const cell = this.#cellFrom(event.target);
|
|
141
|
+
if (!cell) return;
|
|
142
|
+
const date = cell.getAttribute("data-date");
|
|
143
|
+
if (!date || cell.getAttribute("aria-disabled") === "true") return;
|
|
144
|
+
this.#choose(date);
|
|
145
|
+
}
|
|
146
|
+
/** Previews the range up to a hovered/focused cell while selecting. */
|
|
147
|
+
previewTo(event) {
|
|
148
|
+
const cell = this.#cellFrom(event.target);
|
|
149
|
+
if (!cell) return;
|
|
150
|
+
const date = cell.getAttribute("data-date");
|
|
151
|
+
if (!date) return;
|
|
152
|
+
if (event.type.startsWith("focus")) {
|
|
153
|
+
const parsed = parseISODateString(date);
|
|
154
|
+
if (parsed) this.#focusedDate = parsed;
|
|
155
|
+
if (!this.#pendingStart && cell.getAttribute("tabindex") !== "0") {
|
|
156
|
+
this.#render();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (this.#pendingStart) {
|
|
160
|
+
this.#previewDate = date;
|
|
161
|
+
this.#render();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */
|
|
165
|
+
applyPreset(event) {
|
|
166
|
+
const button = event.target?.closest("[data-range]");
|
|
167
|
+
const name = button?.getAttribute("data-range");
|
|
168
|
+
if (!name) return;
|
|
169
|
+
const range = computePreset(name);
|
|
170
|
+
if (!range) return;
|
|
171
|
+
const start = this.#clampISO(range.start);
|
|
172
|
+
const end = this.#clampISO(range.end);
|
|
173
|
+
if (!start || !end) return;
|
|
174
|
+
this.#startDate = start;
|
|
175
|
+
this.#endDate = end;
|
|
176
|
+
this.#pendingStart = "";
|
|
177
|
+
this.#previewDate = "";
|
|
178
|
+
const endDate = parseISODateString(end);
|
|
179
|
+
if (endDate) this.#focusedDate = endDate;
|
|
180
|
+
this.#commitFields();
|
|
181
|
+
this.#transitionTo(toISOMonthString(parseISODateString(end) ?? /* @__PURE__ */ new Date()), end);
|
|
182
|
+
this.#announce();
|
|
183
|
+
this.dispatch("change", { detail: { start, end } });
|
|
184
|
+
}
|
|
185
|
+
/** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */
|
|
186
|
+
onKeydown(event) {
|
|
187
|
+
const cell = this.#cellFrom(event.target);
|
|
188
|
+
if (!cell) return;
|
|
189
|
+
const dateStr = cell.getAttribute("data-date");
|
|
190
|
+
const date = dateStr ? parseISODateString(dateStr) : null;
|
|
191
|
+
if (!date) return;
|
|
192
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
if (dateStr && cell.getAttribute("aria-disabled") !== "true") this.#choose(dateStr);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (event.key === "Escape") {
|
|
198
|
+
if (this.#pendingStart && !event.defaultPrevented && !event.isComposing) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
this.#pendingStart = "";
|
|
201
|
+
this.#previewDate = "";
|
|
202
|
+
this.#render();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let next = null;
|
|
207
|
+
switch (event.key) {
|
|
208
|
+
case "ArrowLeft":
|
|
209
|
+
next = addDays(date, -1);
|
|
210
|
+
break;
|
|
211
|
+
case "ArrowRight":
|
|
212
|
+
next = addDays(date, 1);
|
|
213
|
+
break;
|
|
214
|
+
case "ArrowUp":
|
|
215
|
+
next = addDays(date, -7);
|
|
216
|
+
break;
|
|
217
|
+
case "ArrowDown":
|
|
218
|
+
next = addDays(date, 7);
|
|
219
|
+
break;
|
|
220
|
+
case "Home":
|
|
221
|
+
next = addDays(date, -date.getDay());
|
|
222
|
+
break;
|
|
223
|
+
case "End":
|
|
224
|
+
next = addDays(date, 6 - date.getDay());
|
|
225
|
+
break;
|
|
226
|
+
case "PageUp":
|
|
227
|
+
next = shiftMonthClamped(date, -1);
|
|
228
|
+
break;
|
|
229
|
+
case "PageDown":
|
|
230
|
+
next = shiftMonthClamped(date, 1);
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
this.#moveFocusTo(next);
|
|
237
|
+
}
|
|
238
|
+
/** Records a chosen date as either the pending start or the confirmed end. */
|
|
239
|
+
#choose(date) {
|
|
240
|
+
if (!this.#pendingStart) {
|
|
241
|
+
this.#pendingStart = date;
|
|
242
|
+
this.#previewDate = date;
|
|
243
|
+
const parsed = parseISODateString(date);
|
|
244
|
+
if (parsed) this.#focusedDate = parsed;
|
|
245
|
+
this.#render();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const [start, end] = date < this.#pendingStart ? [date, this.#pendingStart] : [this.#pendingStart, date];
|
|
249
|
+
this.#startDate = start;
|
|
250
|
+
this.#endDate = end;
|
|
251
|
+
this.#pendingStart = "";
|
|
252
|
+
this.#previewDate = "";
|
|
253
|
+
this.#commitFields();
|
|
254
|
+
this.#render();
|
|
255
|
+
this.#announce();
|
|
256
|
+
this.dispatch("change", { detail: { start, end } });
|
|
257
|
+
}
|
|
258
|
+
/** Moves roving focus to `date`, transitioning the month when needed. */
|
|
259
|
+
#moveFocusTo(date) {
|
|
260
|
+
this.#focusedDate = date;
|
|
261
|
+
if (this.#pendingStart) this.#previewDate = toISODateString(date);
|
|
262
|
+
this.#transitionTo(toISOMonthString(date), toISODateString(date));
|
|
263
|
+
}
|
|
264
|
+
/** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */
|
|
265
|
+
#transitionTo(month, dateStr) {
|
|
266
|
+
const isTransition = month !== this.#viewMonth;
|
|
267
|
+
this.#viewMonth = month;
|
|
268
|
+
this.#render();
|
|
269
|
+
const focusCell = () => {
|
|
270
|
+
this.cellTargets.find((c) => c.getAttribute("data-date") === dateStr)?.focus();
|
|
271
|
+
};
|
|
272
|
+
if (isTransition) {
|
|
273
|
+
this.#focusTimer.set(focusCell, 0);
|
|
274
|
+
} else {
|
|
275
|
+
focusCell();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Shifts the displayed month by `delta`, keeping focus within it. */
|
|
279
|
+
#shiftMonth(delta) {
|
|
280
|
+
const info = parseISOMonthString(this.#viewMonth);
|
|
281
|
+
if (!info) return;
|
|
282
|
+
const target = new Date(info.year, info.month - 1 + delta, 1);
|
|
283
|
+
this.#viewMonth = toISOMonthString(target);
|
|
284
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
285
|
+
target.setDate(Math.min(this.#focusedDate.getDate(), lastDay));
|
|
286
|
+
this.#focusedDate = target;
|
|
287
|
+
this.#render();
|
|
288
|
+
}
|
|
289
|
+
/** Builds the six-week grid and binds range/roving/disabled state per cell. */
|
|
290
|
+
#render() {
|
|
291
|
+
const info = parseISOMonthString(this.#viewMonth);
|
|
292
|
+
if (!info) return;
|
|
293
|
+
const { year, month } = info;
|
|
294
|
+
if (this.hasMonthLabelTarget) {
|
|
295
|
+
const lang = document.documentElement.lang || "en";
|
|
296
|
+
const formatter = new Intl.DateTimeFormat(lang, { month: "long", year: "numeric" });
|
|
297
|
+
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
298
|
+
}
|
|
299
|
+
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
300
|
+
const days = gridDays(year, month);
|
|
301
|
+
const focusedStr = toISODateString(this.#focusedDate);
|
|
302
|
+
const todayStr = toISODateString(/* @__PURE__ */ new Date());
|
|
303
|
+
for (let i = 0; i < GRID_SIZE; i++) {
|
|
304
|
+
const el = this.cellTargets[i];
|
|
305
|
+
const date = days[i];
|
|
306
|
+
if (!el || !date) continue;
|
|
307
|
+
const iso = toISODateString(date);
|
|
308
|
+
el.setAttribute("data-date", iso);
|
|
309
|
+
el.textContent = String(date.getDate());
|
|
310
|
+
el.setAttribute("data-outside", String(date.getMonth() !== month - 1));
|
|
311
|
+
el.setAttribute("data-today", String(iso === todayStr));
|
|
312
|
+
el.setAttribute("tabindex", iso === focusedStr ? "0" : "-1");
|
|
313
|
+
if (this.#outOfBounds(iso)) el.setAttribute("aria-disabled", "true");
|
|
314
|
+
else el.removeAttribute("aria-disabled");
|
|
315
|
+
const isStart = !!rangeStart && iso === rangeStart;
|
|
316
|
+
const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;
|
|
317
|
+
const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;
|
|
318
|
+
el.toggleAttribute("data-range-start", isStart);
|
|
319
|
+
el.toggleAttribute("data-range-end", isEnd);
|
|
320
|
+
el.toggleAttribute("data-in-range", inside);
|
|
321
|
+
el.setAttribute("aria-selected", String(isStart || isEnd));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */
|
|
325
|
+
#visualRange() {
|
|
326
|
+
if (this.#pendingStart) {
|
|
327
|
+
const other = this.#previewDate || this.#pendingStart;
|
|
328
|
+
return this.#pendingStart <= other ? [this.#pendingStart, other] : [other, this.#pendingStart];
|
|
329
|
+
}
|
|
330
|
+
return [this.#startDate, this.#endDate];
|
|
331
|
+
}
|
|
332
|
+
/** Writes the confirmed range to the hidden fields. */
|
|
333
|
+
#commitFields() {
|
|
334
|
+
if (this.hasStartFieldTarget) this.startFieldTarget.value = this.#startDate;
|
|
335
|
+
if (this.hasEndFieldTarget) this.endFieldTarget.value = this.#endDate;
|
|
336
|
+
}
|
|
337
|
+
/** Announces the confirmed range in the live status region. */
|
|
338
|
+
#announce() {
|
|
339
|
+
if (this.hasStatusTarget && this.#startDate && this.#endDate) {
|
|
340
|
+
this.statusTarget.textContent = `${this.#startDate} \u2013 ${this.#endDate}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/** True when `iso` falls outside the `[min, max]` bounds. */
|
|
344
|
+
#outOfBounds(iso) {
|
|
345
|
+
if (this.minValue && iso < this.minValue) return true;
|
|
346
|
+
if (this.maxValue && iso > this.maxValue) return true;
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
/** Clamps an ISO date string into `[min, max]`, or "" when unparseable. */
|
|
350
|
+
#clampISO(iso) {
|
|
351
|
+
if (!iso) return "";
|
|
352
|
+
if (this.minValue && iso < this.minValue) return this.minValue;
|
|
353
|
+
if (this.maxValue && iso > this.maxValue) return this.maxValue;
|
|
354
|
+
return iso;
|
|
355
|
+
}
|
|
356
|
+
/** Clamps a Date into `[min, max]`, returning null only when unparseable. */
|
|
357
|
+
#clampToBounds(date) {
|
|
358
|
+
const clamped = this.#clampISO(toISODateString(date));
|
|
359
|
+
return parseISODateString(clamped);
|
|
360
|
+
}
|
|
361
|
+
/** Resolves the cell element from an event target, or null. */
|
|
362
|
+
#cellFrom(target) {
|
|
363
|
+
return target?.closest(
|
|
364
|
+
"[data-stimeo--date-range-picker-target='cell']"
|
|
365
|
+
) ?? null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
function addDays(date, n) {
|
|
369
|
+
const next = new Date(date);
|
|
370
|
+
next.setDate(next.getDate() + n);
|
|
371
|
+
return next;
|
|
372
|
+
}
|
|
373
|
+
function shiftMonthClamped(date, delta) {
|
|
374
|
+
const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);
|
|
375
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
376
|
+
target.setDate(Math.min(date.getDate(), lastDay));
|
|
377
|
+
return target;
|
|
378
|
+
}
|
|
379
|
+
function gridDays(year, month) {
|
|
380
|
+
const first = new Date(year, month - 1, 1);
|
|
381
|
+
const start = new Date(first);
|
|
382
|
+
start.setDate(first.getDate() - first.getDay());
|
|
383
|
+
const days = [];
|
|
384
|
+
const current = new Date(start);
|
|
385
|
+
for (let i = 0; i < GRID_SIZE; i++) {
|
|
386
|
+
days.push(new Date(current));
|
|
387
|
+
current.setDate(current.getDate() + 1);
|
|
388
|
+
}
|
|
389
|
+
return days;
|
|
390
|
+
}
|
|
391
|
+
function normalizeISO(value) {
|
|
392
|
+
const date = parseISODateString(value.trim());
|
|
393
|
+
return date ? toISODateString(date) : "";
|
|
394
|
+
}
|
|
395
|
+
function computePreset(name) {
|
|
396
|
+
const today = /* @__PURE__ */ new Date();
|
|
397
|
+
const todayStr = toISODateString(today);
|
|
398
|
+
switch (name) {
|
|
399
|
+
case "today":
|
|
400
|
+
return { start: todayStr, end: todayStr };
|
|
401
|
+
case "last7":
|
|
402
|
+
return { start: toISODateString(addDays(today, -6)), end: todayStr };
|
|
403
|
+
case "last30":
|
|
404
|
+
return { start: toISODateString(addDays(today, -29)), end: todayStr };
|
|
405
|
+
case "thisMonth": {
|
|
406
|
+
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
407
|
+
const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
408
|
+
return { start: toISODateString(start), end: toISODateString(end) };
|
|
409
|
+
}
|
|
410
|
+
default:
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export { DateRangePickerController };
|
|
416
|
+
//# sourceMappingURL=date_range_picker_controller.js.map
|
|
417
|
+
//# sourceMappingURL=date_range_picker_controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/dates.ts","../../src/utils/safe_timeout.ts","../../src/controllers/date_range_picker_controller.ts"],"names":[],"mappings":"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","file":"date_range_picker_controller.js","sourcesContent":["/**\n * Local-time date helpers shared by the calendar-family controllers\n * (`calendar`, `date-range-picker`).\n *\n * All conversions are intentionally **local time** (not UTC): the grid a user\n * sees is built from `Date` components in their own timezone, so round-tripping\n * through ISO strings must use the same frame to avoid off-by-one-day drift near\n * midnight. Strings are bare `YYYY-MM-DD` / `YYYY-MM` with no time or zone.\n */\n\n/** Formats a {@link Date} as a local-time `YYYY-MM-DD` string. */\nexport function toISODateString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parses a `YYYY-MM-DD` string into a local-time {@link Date}, or `null`.\n *\n * Rejects calendar-invalid dates (e.g. `2026-02-31`): the constructed `Date`\n * would silently roll over to another day, so the parsed components are checked\n * back against it and a mismatch returns `null`.\n */\nexport function parseISODateString(dateStr: string): Date | null {\n if (!dateStr) return null;\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m, d] = match;\n const year = Number(y);\n const month = Number(m);\n const day = Number(d);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/**\n * Parses a `YYYY-MM` string into its year/month (1-based) parts, or `null`.\n * Rejects months outside `1..12`.\n */\nexport function parseISOMonthString(monthStr: string): { year: number; month: number } | null {\n if (!monthStr) return null;\n const match = monthStr.match(/^(\\d{4})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m] = match;\n const month = Number(m);\n if (month < 1 || month > 12) return null;\n return { year: Number(y), month };\n}\n\n/** Formats a {@link Date} as the local-time `YYYY-MM` string of its month. */\nexport function toISOMonthString(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\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 {\n parseISODateString,\n parseISOMonthString,\n toISODateString,\n toISOMonthString,\n} from \"../utils/dates\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Number of cells in a six-week month grid (7 × 6). */\nconst GRID_SIZE = 42;\n\n/**\n * Headless, accessible **date range picker** behavior. A derivative of\n * {@link CalendarController}: it reuses the month-grid navigation model and adds\n * two-point range selection, in-progress range preview, and presets.\n *\n * Markup contract (identifier: `stimeo--date-range-picker`):\n * <div data-controller=\"stimeo--date-range-picker\"\n * data-stimeo--date-range-picker-min-value=\"2026-01-01\"\n * data-stimeo--date-range-picker-max-value=\"2026-12-31\">\n * <button data-action=\"stimeo--date-range-picker#prev\">Prev</button>\n * <span data-stimeo--date-range-picker-target=\"monthLabel\" aria-live=\"polite\"></span>\n * <button data-action=\"stimeo--date-range-picker#next\">Next</button>\n * <div role=\"grid\" data-stimeo--date-range-picker-target=\"grid\">\n * <!-- exactly 42 cell targets (7 days × 6 rows) -->\n * <button role=\"gridcell\" tabindex=\"-1\"\n * data-stimeo--date-range-picker-target=\"cell\"\n * data-action=\"click->stimeo--date-range-picker#selectDate\n * mouseenter->stimeo--date-range-picker#previewTo\n * focus->stimeo--date-range-picker#previewTo\n * keydown->stimeo--date-range-picker#onKeydown\"></button>\n * </div>\n * <button data-range=\"last7\" data-action=\"stimeo--date-range-picker#applyPreset\">…</button>\n * <span role=\"status\" aria-live=\"polite\" data-stimeo--date-range-picker-target=\"status\"></span>\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"startField\" />\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"endField\" />\n * </div>\n *\n * @remarks\n * Behavior only — the consumer styles the grid and renders the range using the\n * `data-range-start` / `data-in-range` / `data-range-end` hooks. Assistive tech\n * is told the two confirmed endpoints via `aria-selected`; inner cells use\n * `data-in-range` (visual only) so the announcement stays to the two ends. The\n * confirmed range is also mirrored to the live `status` region.\n *\n * Selection model: the first click/Enter sets a *pending* start and enters\n * \"selecting\" mode (preview follows the pointer/focus); the second confirms the\n * end (auto-swapped if earlier than the start) and dispatches `change`. Escape\n * abandons an in-progress selection, restoring the last confirmed range.\n */\nexport class DateRangePickerController extends Controller<HTMLElement> {\n static override targets = [\"grid\", \"monthLabel\", \"cell\", \"status\", \"startField\", \"endField\"];\n static override values = {\n min: { type: String, default: \"\" },\n max: { type: String, default: \"\" },\n };\n static actions = [\"applyPreset\", \"next\", \"onKeydown\", \"prev\", \"previewTo\", \"selectDate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly gridTarget: HTMLElement;\n declare readonly monthLabelTarget: HTMLElement;\n declare readonly cellTargets: HTMLElement[];\n declare readonly statusTarget: HTMLElement;\n declare readonly startFieldTarget: HTMLInputElement;\n declare readonly endFieldTarget: HTMLInputElement;\n declare readonly hasMonthLabelTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n declare readonly hasStartFieldTarget: boolean;\n declare readonly hasEndFieldTarget: boolean;\n declare minValue: string;\n declare maxValue: string;\n\n /** The month currently rendered, as `YYYY-MM`. */\n #viewMonth = \"\";\n /** The confirmed range endpoints (ISO), or \"\" when unset. */\n #startDate = \"\";\n #endDate = \"\";\n /** The first endpoint of an in-progress selection (ISO), or \"\" when idle. */\n #pendingStart = \"\";\n /** The hovered/focused date previewed while selecting (ISO), or \"\". */\n #previewDate = \"\";\n /** The roving-focus date in the grid (local time). */\n #focusedDate = new Date();\n\n /** Deferred focus after an async month transition (cancelled on teardown). */\n readonly #focusTimer = new SafeTimeout();\n\n /** Seeds the range from any pre-filled hidden fields and renders the grid. */\n override connect(): void {\n this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : \"\";\n this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : \"\";\n\n const anchor =\n parseISODateString(this.#startDate) ?? this.#clampToBounds(new Date()) ?? new Date();\n this.#focusedDate = anchor;\n this.#viewMonth = toISOMonthString(anchor);\n this.#render();\n }\n\n /** Cancels any pending deferred focus so it never fires on a detached element. */\n override disconnect(): void {\n this.#focusTimer.clearAll();\n }\n\n /** Navigates to the previous month. */\n prev(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(-1);\n }\n\n /** Navigates to the next month. */\n next(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(1);\n }\n\n /** Confirms a range endpoint from a clicked cell. */\n selectDate(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date || cell.getAttribute(\"aria-disabled\") === \"true\") return;\n this.#choose(date);\n }\n\n /** Previews the range up to a hovered/focused cell while selecting. */\n previewTo(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date) return;\n // Focus moves the roving tabindex; hover does not.\n if (event.type.startsWith(\"focus\")) {\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n // Re-roll the roving tab stop only when focus landed on a cell that is not\n // already the tab stop — e.g. Tab/click/programmatic focus from outside the\n // grid's own keyboard navigation (which already rendered the new stop). This\n // guard avoids a redundant double-render on the controller's own arrow moves\n // (onKeydown → #render focuses the new stop, which then fires this focus).\n if (!this.#pendingStart && cell.getAttribute(\"tabindex\") !== \"0\") {\n this.#render();\n }\n }\n if (this.#pendingStart) {\n this.#previewDate = date;\n this.#render();\n }\n }\n\n /** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */\n applyPreset(event: Event): void {\n const button = (event.target as HTMLElement | null)?.closest(\"[data-range]\");\n const name = button?.getAttribute(\"data-range\");\n if (!name) return;\n const range = computePreset(name);\n if (!range) return;\n\n const start = this.#clampISO(range.start);\n const end = this.#clampISO(range.end);\n if (!start || !end) return;\n\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n const endDate = parseISODateString(end);\n if (endDate) this.#focusedDate = endDate;\n this.#commitFields();\n this.#transitionTo(toISOMonthString(parseISODateString(end) ?? new Date()), end);\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */\n onKeydown(event: KeyboardEvent): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const dateStr = cell.getAttribute(\"data-date\");\n const date = dateStr ? parseISODateString(dateStr) : null;\n if (!date) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n if (dateStr && cell.getAttribute(\"aria-disabled\") !== \"true\") this.#choose(dateStr);\n return;\n }\n if (event.key === \"Escape\") {\n // Layered-Escape rule 1: leave a press an inner handler already owned;\n // a press during IME composition steers the conversion, not the range.\n if (this.#pendingStart && !event.defaultPrevented && !event.isComposing) {\n event.preventDefault();\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#render();\n }\n return;\n }\n\n let next: Date | null = null;\n switch (event.key) {\n case \"ArrowLeft\":\n next = addDays(date, -1);\n break;\n case \"ArrowRight\":\n next = addDays(date, 1);\n break;\n case \"ArrowUp\":\n next = addDays(date, -7);\n break;\n case \"ArrowDown\":\n next = addDays(date, 7);\n break;\n case \"Home\":\n next = addDays(date, -date.getDay());\n break;\n case \"End\":\n next = addDays(date, 6 - date.getDay());\n break;\n case \"PageUp\":\n next = shiftMonthClamped(date, -1);\n break;\n case \"PageDown\":\n next = shiftMonthClamped(date, 1);\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#moveFocusTo(next);\n }\n\n /** Records a chosen date as either the pending start or the confirmed end. */\n #choose(date: string): void {\n if (!this.#pendingStart) {\n this.#pendingStart = date;\n this.#previewDate = date;\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n this.#render();\n return;\n }\n // Second click confirms; order the two so start ≤ end.\n const [start, end] =\n date < this.#pendingStart ? [date, this.#pendingStart] : [this.#pendingStart, date];\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#commitFields();\n this.#render();\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Moves roving focus to `date`, transitioning the month when needed. */\n #moveFocusTo(date: Date): void {\n this.#focusedDate = date;\n if (this.#pendingStart) this.#previewDate = toISODateString(date);\n this.#transitionTo(toISOMonthString(date), toISODateString(date));\n }\n\n /** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */\n #transitionTo(month: string, dateStr: string): void {\n const isTransition = month !== this.#viewMonth;\n this.#viewMonth = month;\n this.#render();\n const focusCell = (): void => {\n this.cellTargets.find((c) => c.getAttribute(\"data-date\") === dateStr)?.focus();\n };\n if (isTransition) {\n // Let the synchronous render settle before focusing the freshly bound cell.\n this.#focusTimer.set(focusCell, 0);\n } else {\n focusCell();\n }\n }\n\n /** Shifts the displayed month by `delta`, keeping focus within it. */\n #shiftMonth(delta: number): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const target = new Date(info.year, info.month - 1 + delta, 1);\n this.#viewMonth = toISOMonthString(target);\n // Keep the same day-of-month where possible, clamped to the new month.\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(this.#focusedDate.getDate(), lastDay));\n this.#focusedDate = target;\n this.#render();\n }\n\n /** Builds the six-week grid and binds range/roving/disabled state per cell. */\n #render(): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const { year, month } = info;\n\n if (this.hasMonthLabelTarget) {\n const lang = document.documentElement.lang || \"en\";\n const formatter = new Intl.DateTimeFormat(lang, { month: \"long\", year: \"numeric\" });\n this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));\n }\n\n const [rangeStart, rangeEnd] = this.#visualRange();\n const days = gridDays(year, month);\n const focusedStr = toISODateString(this.#focusedDate);\n const todayStr = toISODateString(new Date());\n\n for (let i = 0; i < GRID_SIZE; i++) {\n const el = this.cellTargets[i];\n const date = days[i];\n if (!el || !date) continue;\n const iso = toISODateString(date);\n\n el.setAttribute(\"data-date\", iso);\n el.textContent = String(date.getDate());\n el.setAttribute(\"data-outside\", String(date.getMonth() !== month - 1));\n el.setAttribute(\"data-today\", String(iso === todayStr));\n el.setAttribute(\"tabindex\", iso === focusedStr ? \"0\" : \"-1\");\n\n if (this.#outOfBounds(iso)) el.setAttribute(\"aria-disabled\", \"true\");\n else el.removeAttribute(\"aria-disabled\");\n\n const isStart = !!rangeStart && iso === rangeStart;\n const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;\n const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;\n el.toggleAttribute(\"data-range-start\", isStart);\n el.toggleAttribute(\"data-range-end\", isEnd);\n el.toggleAttribute(\"data-in-range\", inside);\n // AT hears only the two confirmed/pending endpoints as \"selected\".\n el.setAttribute(\"aria-selected\", String(isStart || isEnd));\n }\n }\n\n /** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */\n #visualRange(): [string, string] {\n if (this.#pendingStart) {\n const other = this.#previewDate || this.#pendingStart;\n return this.#pendingStart <= other\n ? [this.#pendingStart, other]\n : [other, this.#pendingStart];\n }\n return [this.#startDate, this.#endDate];\n }\n\n /** Writes the confirmed range to the hidden fields. */\n #commitFields(): void {\n if (this.hasStartFieldTarget) this.startFieldTarget.value = this.#startDate;\n if (this.hasEndFieldTarget) this.endFieldTarget.value = this.#endDate;\n }\n\n /** Announces the confirmed range in the live status region. */\n #announce(): void {\n if (this.hasStatusTarget && this.#startDate && this.#endDate) {\n this.statusTarget.textContent = `${this.#startDate} – ${this.#endDate}`;\n }\n }\n\n /** True when `iso` falls outside the `[min, max]` bounds. */\n #outOfBounds(iso: string): boolean {\n if (this.minValue && iso < this.minValue) return true;\n if (this.maxValue && iso > this.maxValue) return true;\n return false;\n }\n\n /** Clamps an ISO date string into `[min, max]`, or \"\" when unparseable. */\n #clampISO(iso: string): string {\n if (!iso) return \"\";\n if (this.minValue && iso < this.minValue) return this.minValue;\n if (this.maxValue && iso > this.maxValue) return this.maxValue;\n return iso;\n }\n\n /** Clamps a Date into `[min, max]`, returning null only when unparseable. */\n #clampToBounds(date: Date): Date | null {\n const clamped = this.#clampISO(toISODateString(date));\n return parseISODateString(clamped);\n }\n\n /** Resolves the cell element from an event target, or null. */\n #cellFrom(target: EventTarget | null): HTMLElement | null {\n return (\n (target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--date-range-picker-target='cell']\",\n ) ?? null\n );\n }\n}\n\n/** Returns a new Date `n` days from `date`. */\nfunction addDays(date: Date, n: number): Date {\n const next = new Date(date);\n next.setDate(next.getDate() + n);\n return next;\n}\n\n/** Shifts `date` by `delta` months, clamping the day to the target month length. */\nfunction shiftMonthClamped(date: Date, delta: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(date.getDate(), lastDay));\n return target;\n}\n\n/** Builds the 42 local-time dates of a Sunday-started six-week grid for a month. */\nfunction gridDays(year: number, month: number): Date[] {\n const first = new Date(year, month - 1, 1);\n const start = new Date(first);\n start.setDate(first.getDate() - first.getDay());\n const days: Date[] = [];\n const current = new Date(start);\n for (let i = 0; i < GRID_SIZE; i++) {\n days.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n return days;\n}\n\n/** Validates and re-serializes an ISO date string, returning \"\" when invalid. */\nfunction normalizeISO(value: string): string {\n const date = parseISODateString(value.trim());\n return date ? toISODateString(date) : \"\";\n}\n\n/** Computes a preset range relative to today, or null for an unknown name. */\nfunction computePreset(name: string): { start: string; end: string } | null {\n const today = new Date();\n const todayStr = toISODateString(today);\n switch (name) {\n case \"today\":\n return { start: todayStr, end: todayStr };\n case \"last7\":\n return { start: toISODateString(addDays(today, -6)), end: todayStr };\n case \"last30\":\n return { start: toISODateString(addDays(today, -29)), end: todayStr };\n case \"thisMonth\": {\n const start = new Date(today.getFullYear(), today.getMonth(), 1);\n const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);\n return { start: toISODateString(start), end: toISODateString(end) };\n }\n default:\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Headless "dismissible" behavior for banners, notices, and inline alerts.
|
|
5
|
+
*
|
|
6
|
+
* Markup contract (identifier: `stimeo--dismissible`):
|
|
7
|
+
* <div data-controller="stimeo--dismissible"
|
|
8
|
+
* data-stimeo--dismissible-mode-value="remove">
|
|
9
|
+
* <div data-stimeo--dismissible-target="root" role="status">
|
|
10
|
+
* <p>Saved.</p>
|
|
11
|
+
* <button type="button" aria-label="Close"
|
|
12
|
+
* data-action="stimeo--dismissible#dismiss">×</button>
|
|
13
|
+
* </div>
|
|
14
|
+
* </div>
|
|
15
|
+
*
|
|
16
|
+
* A general utility with no dedicated APG pattern. Its accessibility job is to
|
|
17
|
+
* keep focus from being orphaned: if focus is inside the element being removed,
|
|
18
|
+
* it is moved to a safe place first so the close button vanishing never strands
|
|
19
|
+
* the user (WCAG 2.4.3).
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* Behavior only — the consumer owns any exit transition (use `hide` mode, which
|
|
23
|
+
* adds `hidden`, and animate off `data-state`) and any semantics like
|
|
24
|
+
* `role="alert"`. Focus retreats to, in order: the `fallback` target → the next
|
|
25
|
+
* focusable element after the root → the previous one → `document.body` as a
|
|
26
|
+
* last resort (weak focus; prefer providing a `fallback`).
|
|
27
|
+
*/
|
|
28
|
+
declare class DismissibleController extends Controller<HTMLElement> {
|
|
29
|
+
#private;
|
|
30
|
+
static targets: string[];
|
|
31
|
+
static values: {
|
|
32
|
+
mode: {
|
|
33
|
+
type: StringConstructor;
|
|
34
|
+
default: string;
|
|
35
|
+
};
|
|
36
|
+
closeOnEscape: {
|
|
37
|
+
type: BooleanConstructor;
|
|
38
|
+
default: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
static actions: readonly ["dismiss"];
|
|
42
|
+
static events: readonly ["dismiss"];
|
|
43
|
+
readonly rootTarget: HTMLElement;
|
|
44
|
+
readonly fallbackTarget: HTMLElement;
|
|
45
|
+
readonly hasRootTarget: boolean;
|
|
46
|
+
readonly hasFallbackTarget: boolean;
|
|
47
|
+
modeValue: string;
|
|
48
|
+
closeOnEscapeValue: boolean;
|
|
49
|
+
connect(): void;
|
|
50
|
+
disconnect(): void;
|
|
51
|
+
/** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
|
|
52
|
+
closeOnEscapeValueChanged(): void;
|
|
53
|
+
/** Dismisses the element. Bound via `data-action` (click on the close button). */
|
|
54
|
+
dismiss(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { DismissibleController };
|