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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/date_range_picker_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/utils/dates.ts
|
|
22
|
+
function toISODateString(date) {
|
|
23
|
+
const year = date.getFullYear();
|
|
24
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
26
|
+
return `${year}-${month}-${day}`;
|
|
27
|
+
}
|
|
28
|
+
function parseISODateString(dateStr) {
|
|
29
|
+
if (!dateStr) return null;
|
|
30
|
+
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
const [, y, m, d] = match;
|
|
33
|
+
const year = Number(y);
|
|
34
|
+
const month = Number(m);
|
|
35
|
+
const day = Number(d);
|
|
36
|
+
const date = new Date(year, month - 1, day);
|
|
37
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return date;
|
|
41
|
+
}
|
|
42
|
+
function parseISOMonthString(monthStr) {
|
|
43
|
+
if (!monthStr) return null;
|
|
44
|
+
const match = monthStr.match(/^(\d{4})-(\d{2})$/);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
const [, y, m] = match;
|
|
47
|
+
const month = Number(m);
|
|
48
|
+
if (month < 1 || month > 12) return null;
|
|
49
|
+
return { year: Number(y), month };
|
|
50
|
+
}
|
|
51
|
+
function toISOMonthString(date) {
|
|
52
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/utils/safe_timeout.ts
|
|
56
|
+
var TimerRegistry = class {
|
|
57
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
58
|
+
ids = /* @__PURE__ */ new Set();
|
|
59
|
+
/**
|
|
60
|
+
* Cancels a single tracked timer.
|
|
61
|
+
*
|
|
62
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
63
|
+
* registry), so callers can clear defensively without guarding.
|
|
64
|
+
*/
|
|
65
|
+
clear(id) {
|
|
66
|
+
if (this.ids.delete(id)) {
|
|
67
|
+
this.cancel(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
72
|
+
* to guarantee no timer outlives the element.
|
|
73
|
+
*/
|
|
74
|
+
clearAll() {
|
|
75
|
+
for (const id of this.ids) {
|
|
76
|
+
this.cancel(id);
|
|
77
|
+
}
|
|
78
|
+
this.ids.clear();
|
|
79
|
+
}
|
|
80
|
+
/** Number of timers currently tracked (pending). */
|
|
81
|
+
get size() {
|
|
82
|
+
return this.ids.size;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
86
|
+
/**
|
|
87
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
88
|
+
*
|
|
89
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
90
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
91
|
+
*/
|
|
92
|
+
set(callback, delay) {
|
|
93
|
+
const id = this.schedule(() => {
|
|
94
|
+
this.ids.delete(id);
|
|
95
|
+
callback();
|
|
96
|
+
}, delay);
|
|
97
|
+
this.ids.add(id);
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
schedule(callback, delay) {
|
|
101
|
+
return window.setTimeout(callback, delay);
|
|
102
|
+
}
|
|
103
|
+
cancel(id) {
|
|
104
|
+
window.clearTimeout(id);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/controllers/date_range_picker_controller.ts
|
|
109
|
+
var GRID_SIZE = 42;
|
|
110
|
+
var DateRangePickerController = class extends Controller {
|
|
111
|
+
static targets = ["grid", "monthLabel", "cell", "status", "startField", "endField"];
|
|
112
|
+
static values = {
|
|
113
|
+
min: { type: String, default: "" },
|
|
114
|
+
max: { type: String, default: "" }
|
|
115
|
+
};
|
|
116
|
+
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
117
|
+
static events = ["change"];
|
|
118
|
+
/** The month currently rendered, as `YYYY-MM`. */
|
|
119
|
+
#viewMonth = "";
|
|
120
|
+
/** The confirmed range endpoints (ISO), or "" when unset. */
|
|
121
|
+
#startDate = "";
|
|
122
|
+
#endDate = "";
|
|
123
|
+
/** The first endpoint of an in-progress selection (ISO), or "" when idle. */
|
|
124
|
+
#pendingStart = "";
|
|
125
|
+
/** The hovered/focused date previewed while selecting (ISO), or "". */
|
|
126
|
+
#previewDate = "";
|
|
127
|
+
/** The roving-focus date in the grid (local time). */
|
|
128
|
+
#focusedDate = /* @__PURE__ */ new Date();
|
|
129
|
+
/** Deferred focus after an async month transition (cancelled on teardown). */
|
|
130
|
+
#focusTimer = new SafeTimeout();
|
|
131
|
+
/** Seeds the range from any pre-filled hidden fields and renders the grid. */
|
|
132
|
+
connect() {
|
|
133
|
+
this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
|
|
134
|
+
this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
|
|
135
|
+
const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
|
|
136
|
+
this.#focusedDate = anchor;
|
|
137
|
+
this.#viewMonth = toISOMonthString(anchor);
|
|
138
|
+
if (this.hasGridTarget) this.gridTarget.setAttribute("aria-multiselectable", "true");
|
|
139
|
+
this.#render();
|
|
140
|
+
}
|
|
141
|
+
/** Cancels any pending deferred focus so it never fires on a detached element. */
|
|
142
|
+
disconnect() {
|
|
143
|
+
this.#focusTimer.clearAll();
|
|
144
|
+
}
|
|
145
|
+
/** Navigates to the previous month. */
|
|
146
|
+
prev(event) {
|
|
147
|
+
event?.preventDefault();
|
|
148
|
+
this.#shiftMonth(-1);
|
|
149
|
+
}
|
|
150
|
+
/** Navigates to the next month. */
|
|
151
|
+
next(event) {
|
|
152
|
+
event?.preventDefault();
|
|
153
|
+
this.#shiftMonth(1);
|
|
154
|
+
}
|
|
155
|
+
/** Confirms a range endpoint from a clicked cell. */
|
|
156
|
+
selectDate(event) {
|
|
157
|
+
const cell = this.#cellFrom(event.target);
|
|
158
|
+
if (!cell) return;
|
|
159
|
+
const date = cell.getAttribute("data-date");
|
|
160
|
+
if (!date || cell.getAttribute("aria-disabled") === "true") return;
|
|
161
|
+
this.#choose(date);
|
|
162
|
+
}
|
|
163
|
+
/** Previews the range up to a hovered/focused cell while selecting. */
|
|
164
|
+
previewTo(event) {
|
|
165
|
+
const cell = this.#cellFrom(event.target);
|
|
166
|
+
if (!cell) return;
|
|
167
|
+
const date = cell.getAttribute("data-date");
|
|
168
|
+
if (!date) return;
|
|
169
|
+
if (event.type.startsWith("focus")) {
|
|
170
|
+
const parsed = parseISODateString(date);
|
|
171
|
+
if (parsed) this.#focusedDate = parsed;
|
|
172
|
+
if (!this.#pendingStart && cell.getAttribute("tabindex") !== "0") {
|
|
173
|
+
this.#render();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (this.#pendingStart) {
|
|
177
|
+
this.#previewDate = date;
|
|
178
|
+
this.#render();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */
|
|
182
|
+
applyPreset(event) {
|
|
183
|
+
const button = event.target?.closest("[data-range]");
|
|
184
|
+
const name = button?.getAttribute("data-range");
|
|
185
|
+
if (!name) return;
|
|
186
|
+
const range = computePreset(name);
|
|
187
|
+
if (!range) return;
|
|
188
|
+
const start = this.#clampISO(range.start);
|
|
189
|
+
const end = this.#clampISO(range.end);
|
|
190
|
+
if (!start || !end) return;
|
|
191
|
+
this.#startDate = start;
|
|
192
|
+
this.#endDate = end;
|
|
193
|
+
this.#pendingStart = "";
|
|
194
|
+
this.#previewDate = "";
|
|
195
|
+
const endDate = parseISODateString(end);
|
|
196
|
+
if (endDate) this.#focusedDate = endDate;
|
|
197
|
+
this.#commitFields();
|
|
198
|
+
this.#transitionTo(toISOMonthString(parseISODateString(end) ?? /* @__PURE__ */ new Date()), end);
|
|
199
|
+
this.#announce();
|
|
200
|
+
this.dispatch("change", { detail: { start, end } });
|
|
201
|
+
}
|
|
202
|
+
/** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */
|
|
203
|
+
onKeydown(event) {
|
|
204
|
+
if (event.defaultPrevented) return;
|
|
205
|
+
if (isReservedArrowChord(event)) return;
|
|
206
|
+
const cell = this.#cellFrom(event.target);
|
|
207
|
+
if (!cell) return;
|
|
208
|
+
const dateStr = cell.getAttribute("data-date");
|
|
209
|
+
const date = dateStr ? parseISODateString(dateStr) : null;
|
|
210
|
+
if (!date) return;
|
|
211
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
if (dateStr && cell.getAttribute("aria-disabled") !== "true") this.#choose(dateStr);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (event.key === "Escape") {
|
|
217
|
+
if (this.#pendingStart && !event.isComposing) {
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
this.#pendingStart = "";
|
|
220
|
+
this.#previewDate = "";
|
|
221
|
+
this.#render();
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let next = null;
|
|
226
|
+
switch (logicalArrowKey(event.key, this.element)) {
|
|
227
|
+
case "ArrowLeft":
|
|
228
|
+
next = addDays(date, -1);
|
|
229
|
+
break;
|
|
230
|
+
case "ArrowRight":
|
|
231
|
+
next = addDays(date, 1);
|
|
232
|
+
break;
|
|
233
|
+
case "ArrowUp":
|
|
234
|
+
next = addDays(date, -7);
|
|
235
|
+
break;
|
|
236
|
+
case "ArrowDown":
|
|
237
|
+
next = addDays(date, 7);
|
|
238
|
+
break;
|
|
239
|
+
case "Home":
|
|
240
|
+
next = addDays(date, -date.getDay());
|
|
241
|
+
break;
|
|
242
|
+
case "End":
|
|
243
|
+
next = addDays(date, 6 - date.getDay());
|
|
244
|
+
break;
|
|
245
|
+
case "PageUp":
|
|
246
|
+
next = shiftMonthClamped(date, -1);
|
|
247
|
+
break;
|
|
248
|
+
case "PageDown":
|
|
249
|
+
next = shiftMonthClamped(date, 1);
|
|
250
|
+
break;
|
|
251
|
+
default:
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
this.#moveFocusTo(next);
|
|
256
|
+
}
|
|
257
|
+
/** Records a chosen date as either the pending start or the confirmed end. */
|
|
258
|
+
#choose(date) {
|
|
259
|
+
if (!this.#pendingStart) {
|
|
260
|
+
this.#pendingStart = date;
|
|
261
|
+
this.#previewDate = date;
|
|
262
|
+
const parsed = parseISODateString(date);
|
|
263
|
+
if (parsed) this.#focusedDate = parsed;
|
|
264
|
+
this.#render();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const [start, end] = date < this.#pendingStart ? [date, this.#pendingStart] : [this.#pendingStart, date];
|
|
268
|
+
this.#startDate = start;
|
|
269
|
+
this.#endDate = end;
|
|
270
|
+
this.#pendingStart = "";
|
|
271
|
+
this.#previewDate = "";
|
|
272
|
+
this.#commitFields();
|
|
273
|
+
this.#render();
|
|
274
|
+
this.#announce();
|
|
275
|
+
this.dispatch("change", { detail: { start, end } });
|
|
276
|
+
}
|
|
277
|
+
/** Moves roving focus to `date`, transitioning the month when needed. */
|
|
278
|
+
#moveFocusTo(date) {
|
|
279
|
+
this.#focusedDate = date;
|
|
280
|
+
if (this.#pendingStart) this.#previewDate = toISODateString(date);
|
|
281
|
+
this.#transitionTo(toISOMonthString(date), toISODateString(date));
|
|
282
|
+
}
|
|
283
|
+
/** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */
|
|
284
|
+
#transitionTo(month, dateStr) {
|
|
285
|
+
const isTransition = month !== this.#viewMonth;
|
|
286
|
+
this.#viewMonth = month;
|
|
287
|
+
this.#render();
|
|
288
|
+
const focusCell = () => {
|
|
289
|
+
this.cellTargets.find((c) => c.getAttribute("data-date") === dateStr)?.focus();
|
|
290
|
+
};
|
|
291
|
+
if (isTransition) {
|
|
292
|
+
this.#focusTimer.set(focusCell, 0);
|
|
293
|
+
} else {
|
|
294
|
+
focusCell();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Shifts the displayed month by `delta`, keeping focus within it. */
|
|
298
|
+
#shiftMonth(delta) {
|
|
299
|
+
const info = parseISOMonthString(this.#viewMonth);
|
|
300
|
+
if (!info) return;
|
|
301
|
+
const target = new Date(info.year, info.month - 1 + delta, 1);
|
|
302
|
+
this.#viewMonth = toISOMonthString(target);
|
|
303
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
304
|
+
target.setDate(Math.min(this.#focusedDate.getDate(), lastDay));
|
|
305
|
+
this.#focusedDate = target;
|
|
306
|
+
this.#render();
|
|
307
|
+
}
|
|
308
|
+
/** Builds the six-week grid and binds range/roving/disabled state per cell. */
|
|
309
|
+
#render() {
|
|
310
|
+
const info = parseISOMonthString(this.#viewMonth);
|
|
311
|
+
if (!info) return;
|
|
312
|
+
const { year, month } = info;
|
|
313
|
+
if (this.hasMonthLabelTarget) {
|
|
314
|
+
const lang = document.documentElement.lang || "en";
|
|
315
|
+
const formatter = new Intl.DateTimeFormat(lang, { month: "long", year: "numeric" });
|
|
316
|
+
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
317
|
+
}
|
|
318
|
+
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
319
|
+
const days = gridDays(year, month);
|
|
320
|
+
const focusedStr = toISODateString(this.#focusedDate);
|
|
321
|
+
const todayStr = toISODateString(/* @__PURE__ */ new Date());
|
|
322
|
+
for (let i = 0; i < GRID_SIZE; i++) {
|
|
323
|
+
const el = this.cellTargets[i];
|
|
324
|
+
const date = days[i];
|
|
325
|
+
if (!el || !date) continue;
|
|
326
|
+
const iso = toISODateString(date);
|
|
327
|
+
el.setAttribute("data-date", iso);
|
|
328
|
+
el.textContent = String(date.getDate());
|
|
329
|
+
el.setAttribute("data-outside", String(date.getMonth() !== month - 1));
|
|
330
|
+
el.setAttribute("data-today", String(iso === todayStr));
|
|
331
|
+
el.setAttribute("tabindex", iso === focusedStr ? "0" : "-1");
|
|
332
|
+
if (this.#outOfBounds(iso)) el.setAttribute("aria-disabled", "true");
|
|
333
|
+
else el.removeAttribute("aria-disabled");
|
|
334
|
+
const isStart = !!rangeStart && iso === rangeStart;
|
|
335
|
+
const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;
|
|
336
|
+
const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;
|
|
337
|
+
el.toggleAttribute("data-range-start", isStart);
|
|
338
|
+
el.toggleAttribute("data-range-end", isEnd);
|
|
339
|
+
el.toggleAttribute("data-in-range", inside);
|
|
340
|
+
el.setAttribute(
|
|
341
|
+
"aria-selected",
|
|
342
|
+
String(
|
|
343
|
+
!!this.#startDate && iso === this.#startDate || !!this.#endDate && iso === this.#endDate
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */
|
|
349
|
+
#visualRange() {
|
|
350
|
+
if (this.#pendingStart) {
|
|
351
|
+
const other = this.#previewDate || this.#pendingStart;
|
|
352
|
+
return this.#pendingStart <= other ? [this.#pendingStart, other] : [other, this.#pendingStart];
|
|
353
|
+
}
|
|
354
|
+
return [this.#startDate, this.#endDate];
|
|
355
|
+
}
|
|
356
|
+
/** Writes the confirmed range to the hidden fields. */
|
|
357
|
+
#commitFields() {
|
|
358
|
+
if (this.hasStartFieldTarget) this.startFieldTarget.value = this.#startDate;
|
|
359
|
+
if (this.hasEndFieldTarget) this.endFieldTarget.value = this.#endDate;
|
|
360
|
+
}
|
|
361
|
+
/** Announces the confirmed range in the live status region. */
|
|
362
|
+
#announce() {
|
|
363
|
+
if (this.hasStatusTarget && this.#startDate && this.#endDate) {
|
|
364
|
+
this.statusTarget.textContent = `${this.#startDate} \u2013 ${this.#endDate}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** True when `iso` falls outside the `[min, max]` bounds. */
|
|
368
|
+
#outOfBounds(iso) {
|
|
369
|
+
if (this.minValue && iso < this.minValue) return true;
|
|
370
|
+
if (this.maxValue && iso > this.maxValue) return true;
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
/** Clamps an ISO date string into `[min, max]`, or "" when unparseable. */
|
|
374
|
+
#clampISO(iso) {
|
|
375
|
+
if (!iso) return "";
|
|
376
|
+
if (this.minValue && iso < this.minValue) return this.minValue;
|
|
377
|
+
if (this.maxValue && iso > this.maxValue) return this.maxValue;
|
|
378
|
+
return iso;
|
|
379
|
+
}
|
|
380
|
+
/** Clamps a Date into `[min, max]`, returning null only when unparseable. */
|
|
381
|
+
#clampToBounds(date) {
|
|
382
|
+
const clamped = this.#clampISO(toISODateString(date));
|
|
383
|
+
return parseISODateString(clamped);
|
|
384
|
+
}
|
|
385
|
+
/** Resolves the cell element from an event target, or null. */
|
|
386
|
+
#cellFrom(target) {
|
|
387
|
+
return target?.closest(
|
|
388
|
+
"[data-stimeo--date-range-picker-target='cell']"
|
|
389
|
+
) ?? null;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
function addDays(date, n) {
|
|
393
|
+
const next = new Date(date);
|
|
394
|
+
next.setDate(next.getDate() + n);
|
|
395
|
+
return next;
|
|
396
|
+
}
|
|
397
|
+
function shiftMonthClamped(date, delta) {
|
|
398
|
+
const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);
|
|
399
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
400
|
+
target.setDate(Math.min(date.getDate(), lastDay));
|
|
401
|
+
return target;
|
|
402
|
+
}
|
|
403
|
+
function gridDays(year, month) {
|
|
404
|
+
const first = new Date(year, month - 1, 1);
|
|
405
|
+
const start = new Date(first);
|
|
406
|
+
start.setDate(first.getDate() - first.getDay());
|
|
407
|
+
const days = [];
|
|
408
|
+
const current = new Date(start);
|
|
409
|
+
for (let i = 0; i < GRID_SIZE; i++) {
|
|
410
|
+
days.push(new Date(current));
|
|
411
|
+
current.setDate(current.getDate() + 1);
|
|
412
|
+
}
|
|
413
|
+
return days;
|
|
414
|
+
}
|
|
415
|
+
function normalizeISO(value) {
|
|
416
|
+
const date = parseISODateString(value.trim());
|
|
417
|
+
return date ? toISODateString(date) : "";
|
|
418
|
+
}
|
|
419
|
+
function computePreset(name) {
|
|
420
|
+
const today = /* @__PURE__ */ new Date();
|
|
421
|
+
const todayStr = toISODateString(today);
|
|
422
|
+
switch (name) {
|
|
423
|
+
case "today":
|
|
424
|
+
return { start: todayStr, end: todayStr };
|
|
425
|
+
case "last7":
|
|
426
|
+
return { start: toISODateString(addDays(today, -6)), end: todayStr };
|
|
427
|
+
case "last30":
|
|
428
|
+
return { start: toISODateString(addDays(today, -29)), end: todayStr };
|
|
429
|
+
case "thisMonth": {
|
|
430
|
+
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
431
|
+
const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
432
|
+
return { start: toISODateString(start), end: toISODateString(end) };
|
|
433
|
+
}
|
|
434
|
+
default:
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export { DateRangePickerController };
|
|
440
|
+
//# sourceMappingURL=date_range_picker_controller.js.map
|
|
441
|
+
//# sourceMappingURL=date_range_picker_controller.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/dismissible_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/focus_trap.ts
|
|
6
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
7
|
+
|
|
8
|
+
// src/controllers/dismissible_controller.ts
|
|
9
|
+
var DismissibleController = class extends Controller {
|
|
10
|
+
static targets = ["root", "fallback"];
|
|
11
|
+
static values = {
|
|
12
|
+
mode: { type: String, default: "remove" },
|
|
13
|
+
closeOnEscape: { type: Boolean, default: false }
|
|
14
|
+
};
|
|
15
|
+
static actions = ["dismiss"];
|
|
16
|
+
static events = ["dismiss"];
|
|
17
|
+
connect() {
|
|
18
|
+
const root = this.#root;
|
|
19
|
+
if (!root.hasAttribute("data-state")) {
|
|
20
|
+
root.setAttribute("data-state", "open");
|
|
21
|
+
}
|
|
22
|
+
this.#syncEscapeListener();
|
|
23
|
+
}
|
|
24
|
+
disconnect() {
|
|
25
|
+
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
26
|
+
}
|
|
27
|
+
/** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
|
|
28
|
+
closeOnEscapeValueChanged() {
|
|
29
|
+
this.#syncEscapeListener();
|
|
30
|
+
}
|
|
31
|
+
/** Dismisses the element. Bound via `data-action` (click on the close button). */
|
|
32
|
+
dismiss() {
|
|
33
|
+
this.#performDismiss();
|
|
34
|
+
}
|
|
35
|
+
/** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */
|
|
36
|
+
#onKeydown = (event) => {
|
|
37
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
38
|
+
if (!this.closeOnEscapeValue) return;
|
|
39
|
+
const active = document.activeElement;
|
|
40
|
+
if (!active || !this.element.contains(active)) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
this.#performDismiss();
|
|
43
|
+
};
|
|
44
|
+
/** Adds or removes the one stable listener reference without duplicating it. */
|
|
45
|
+
#syncEscapeListener() {
|
|
46
|
+
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
47
|
+
if (this.closeOnEscapeValue) {
|
|
48
|
+
this.element.addEventListener("keydown", this.#onKeydown);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The element to dismiss: the explicit `root` target, or the host element. */
|
|
52
|
+
get #root() {
|
|
53
|
+
return this.hasRootTarget ? this.rootTarget : this.element;
|
|
54
|
+
}
|
|
55
|
+
#performDismiss() {
|
|
56
|
+
const root = this.#root;
|
|
57
|
+
const mode = this.modeValue === "hide" ? "hide" : "remove";
|
|
58
|
+
this.#retreatFocus(root);
|
|
59
|
+
root.setAttribute("data-state", "closing");
|
|
60
|
+
this.dispatch("dismiss", { detail: { mode } });
|
|
61
|
+
if (mode === "hide") {
|
|
62
|
+
root.hidden = true;
|
|
63
|
+
} else {
|
|
64
|
+
root.remove();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Moves focus out of `root` *before* it is removed, but only when focus is
|
|
69
|
+
* actually inside it — otherwise the user's place elsewhere is left undisturbed.
|
|
70
|
+
*/
|
|
71
|
+
#retreatFocus(root) {
|
|
72
|
+
const active = document.activeElement;
|
|
73
|
+
if (!(active instanceof HTMLElement) || !root.contains(active)) return;
|
|
74
|
+
for (const candidate of this.#focusFallbacks(root)) {
|
|
75
|
+
candidate.focus();
|
|
76
|
+
if (document.activeElement === candidate) return;
|
|
77
|
+
}
|
|
78
|
+
document.body.focus();
|
|
79
|
+
}
|
|
80
|
+
/** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
|
|
81
|
+
*#focusFallbacks(root) {
|
|
82
|
+
const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
|
|
83
|
+
if (explicitFallback && !root.contains(explicitFallback) && this.#isFocusAvailable(explicitFallback)) {
|
|
84
|
+
yield explicitFallback;
|
|
85
|
+
}
|
|
86
|
+
const candidates = document.querySelectorAll(FOCUSABLE);
|
|
87
|
+
for (const element of candidates) {
|
|
88
|
+
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (this.#isFocusAvailable(element)) yield element;
|
|
92
|
+
}
|
|
93
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
94
|
+
const element = candidates.item(index);
|
|
95
|
+
if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (this.#isFocusAvailable(element)) yield element;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Whether focus is allowed by the element and its semantic ancestors. */
|
|
102
|
+
#isFocusAvailable(element) {
|
|
103
|
+
if (!element.matches(FOCUSABLE) && !element.hasAttribute("tabindex") && !element.isContentEditable) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (element.matches(":disabled")) return false;
|
|
107
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
108
|
+
for (let current = element; current; current = current.parentElement) {
|
|
109
|
+
if (current.hidden || current.inert) return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export { DismissibleController };
|
|
116
|
+
//# sourceMappingURL=dismissible_controller.js.map
|
|
117
|
+
//# sourceMappingURL=dismissible_controller.js.map
|