stimeo-ui 0.14.0 → 0.16.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 +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -85,123 +85,215 @@ function monthLabelFormatter(locale) {
|
|
|
85
85
|
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// src/utils/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this
|
|
112
|
-
}
|
|
113
|
-
/** Number of timers currently tracked (pending). */
|
|
114
|
-
get size() {
|
|
115
|
-
return this.ids.size;
|
|
88
|
+
// src/utils/field_mirror.ts
|
|
89
|
+
function writeField(field, value) {
|
|
90
|
+
if (field.value === value) return false;
|
|
91
|
+
field.value = value;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function commitField(target) {
|
|
95
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/utils/locale.ts
|
|
99
|
+
function resolveLocale(element, declared = "") {
|
|
100
|
+
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/utils/microtask_coalescer.ts
|
|
104
|
+
var MicrotaskCoalescer = class {
|
|
105
|
+
#run;
|
|
106
|
+
#queued = false;
|
|
107
|
+
#active = false;
|
|
108
|
+
#generation = 0;
|
|
109
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
110
|
+
constructor(run) {
|
|
111
|
+
this.#run = run;
|
|
116
112
|
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
121
|
-
*
|
|
122
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
123
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
124
|
-
*/
|
|
125
|
-
set(callback, delay) {
|
|
126
|
-
const id = this.schedule(() => {
|
|
127
|
-
this.ids.delete(id);
|
|
128
|
-
callback();
|
|
129
|
-
}, delay);
|
|
130
|
-
this.ids.add(id);
|
|
131
|
-
return id;
|
|
113
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
114
|
+
activate() {
|
|
115
|
+
this.#active = true;
|
|
132
116
|
}
|
|
133
|
-
|
|
134
|
-
|
|
117
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
118
|
+
cancel() {
|
|
119
|
+
this.#active = false;
|
|
120
|
+
this.#queued = false;
|
|
121
|
+
this.#generation += 1;
|
|
135
122
|
}
|
|
136
|
-
|
|
137
|
-
|
|
123
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
124
|
+
schedule() {
|
|
125
|
+
if (!this.#active || this.#queued) return;
|
|
126
|
+
this.#queued = true;
|
|
127
|
+
const generation = this.#generation;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
130
|
+
this.#queued = false;
|
|
131
|
+
this.#run();
|
|
132
|
+
});
|
|
138
133
|
}
|
|
139
134
|
};
|
|
140
135
|
|
|
136
|
+
// src/utils/target_selector.ts
|
|
137
|
+
function targetSelector(identifier, name) {
|
|
138
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
141
|
// src/controllers/calendar_controller.ts
|
|
142
|
-
var OWNED_DISABLED = "
|
|
142
|
+
var OWNED_DISABLED = "owns-disabled";
|
|
143
143
|
var CalendarController = class extends Controller {
|
|
144
|
-
|
|
144
|
+
/** The marker above, in the namespace this controller is registered under. */
|
|
145
|
+
get #ownedDisabled() {
|
|
146
|
+
return `data-${this.identifier}-${OWNED_DISABLED}`;
|
|
147
|
+
}
|
|
148
|
+
static targets = ["label", "grid", "day", "field", "monthField"];
|
|
145
149
|
static values = {
|
|
146
150
|
month: { type: String, default: "" },
|
|
147
151
|
selected: { type: String, default: "" },
|
|
148
152
|
min: { type: String, default: "" },
|
|
149
153
|
max: { type: String, default: "" },
|
|
150
|
-
weekStart: { type: Number, default: 0 }
|
|
154
|
+
weekStart: { type: Number, default: 0 },
|
|
151
155
|
// 0 = Sunday, 1 = Monday, etc.
|
|
156
|
+
locale: { type: String, default: "" }
|
|
152
157
|
};
|
|
153
158
|
static actions = ["next", "onKeydown", "prev", "selectByClick"];
|
|
154
|
-
static events = ["monthchange", "select"];
|
|
159
|
+
static events = ["monthchange", "reconcile", "select"];
|
|
155
160
|
/** The date currently receiving focus in the grid (local time). */
|
|
156
161
|
focusedDate = /* @__PURE__ */ new Date();
|
|
157
162
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* controller never steals focus after the element leaves the DOM (Turbo).
|
|
163
|
+
* Whether `connect()` has run and `disconnect()` has not since. The paints
|
|
164
|
+
* before and during `connect()` describe the grid, so they report nothing.
|
|
161
165
|
*/
|
|
162
|
-
#
|
|
166
|
+
#connected = false;
|
|
163
167
|
/**
|
|
164
|
-
* The
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* not reach a listener that refetches inventory or pushes history.
|
|
168
|
+
* The selection the last paint published — a `YYYY-MM-DD` day, or `""` — which
|
|
169
|
+
* the next move of the selection is measured from. A pick records its own day
|
|
170
|
+
* here before painting it.
|
|
168
171
|
*/
|
|
169
|
-
#
|
|
172
|
+
#published = "";
|
|
173
|
+
/**
|
|
174
|
+
* How many times a pick or a change the page made has moved the selection, so
|
|
175
|
+
* a report still pending for a selection a listener has since replaced can tell.
|
|
176
|
+
*/
|
|
177
|
+
#selectionMoves = 0;
|
|
178
|
+
/**
|
|
179
|
+
* The month the last paint showed (`YYYY-MM`), or `""` before the first paint.
|
|
180
|
+
* Only a connected grid reports a paint that moves it: settling on the initial
|
|
181
|
+
* month — before `connect()` or in it, from the attribute or derived here — is
|
|
182
|
+
* the grid describing itself, not a navigation, so it must not reach a
|
|
183
|
+
* listener that refetches inventory or pushes history.
|
|
184
|
+
*
|
|
185
|
+
* While `month` is empty this is also the month the next paint shows, so the
|
|
186
|
+
* grid stays where it is; the first `connect()` of this instance sets it to the
|
|
187
|
+
* month the grid opens on.
|
|
188
|
+
*/
|
|
189
|
+
#paintedMonth = "";
|
|
190
|
+
/**
|
|
191
|
+
* Whether this instance has connected before. Only its first connection picks
|
|
192
|
+
* the month a grid with no `month` opens on. A later one keeps the month on
|
|
193
|
+
* screen, and so do the paints Stimulus runs before it.
|
|
194
|
+
*/
|
|
195
|
+
#opened = false;
|
|
196
|
+
/**
|
|
197
|
+
* Whether the paint about to run was asked for by this grid's own controls.
|
|
198
|
+
* The `month` and `selected` Values are shared with the page — application
|
|
199
|
+
* code and a Turbo morph write them too — so the field mirrors take their
|
|
200
|
+
* "did the user commit this" answer from the route, not from the Value.
|
|
201
|
+
*/
|
|
202
|
+
#movedByUser = false;
|
|
203
|
+
/**
|
|
204
|
+
* Repaints the month on screen after `min`, `max` or `weekStart` changed.
|
|
205
|
+
*
|
|
206
|
+
* A morph that swaps several of them repaints once, and the delivery Stimulus
|
|
207
|
+
* makes before `connect()` is refused, because the grid is painted on
|
|
208
|
+
* connect. None of these Values decides the month on screen; `min` and `max`
|
|
209
|
+
* decide whether the requested day is published.
|
|
210
|
+
*/
|
|
211
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
212
|
+
this.#repaintKeepingFocus();
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* The cell DOM focus was on, its day and the month on screen, recorded by the
|
|
216
|
+
* first repaint of a batch for the pass that settles focus after it.
|
|
217
|
+
*/
|
|
218
|
+
#focusOrigin = null;
|
|
219
|
+
/**
|
|
220
|
+
* Settles DOM focus once, after the last repaint of a batch. The callbacks of
|
|
221
|
+
* one mutation repaint one by one and each reads every Value as it stands, so
|
|
222
|
+
* a `selected` or `locale` callback can paint a month before that month's own
|
|
223
|
+
* callback has moved the tab stop into it. Outside a connection no pass is
|
|
224
|
+
* scheduled, so a grid that is not connected moves no focus.
|
|
225
|
+
*/
|
|
226
|
+
#settleFocus = new MicrotaskCoalescer(() => {
|
|
227
|
+
this.#restoreFocus();
|
|
228
|
+
});
|
|
170
229
|
connect() {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
230
|
+
this.#repaint.activate();
|
|
231
|
+
this.#settleFocus.activate();
|
|
232
|
+
this.#focusOrigin = null;
|
|
233
|
+
if (!this.monthValue && !this.#opened) {
|
|
234
|
+
this.#paintedMonth = toISOMonthString(parseISODateString(this.#selection) ?? /* @__PURE__ */ new Date());
|
|
176
235
|
}
|
|
236
|
+
this.#opened = true;
|
|
177
237
|
this.#initializeFocusedDate();
|
|
178
|
-
this
|
|
238
|
+
this.#render();
|
|
239
|
+
this.#connected = true;
|
|
179
240
|
}
|
|
180
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Drops a pending repaint and a pending focus move, and stops reporting, so
|
|
243
|
+
* nothing reaches for a grid that is not connected.
|
|
244
|
+
*/
|
|
181
245
|
disconnect() {
|
|
182
|
-
this.#
|
|
246
|
+
this.#connected = false;
|
|
247
|
+
this.#repaint.cancel();
|
|
248
|
+
this.#settleFocus.cancel();
|
|
183
249
|
}
|
|
184
250
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
251
|
+
* Paints the month `month` names, whether application code, a morph or the
|
|
252
|
+
* grid's own navigation wrote it; focus in the grid follows to the tab stop.
|
|
187
253
|
*/
|
|
188
254
|
monthValueChanged() {
|
|
189
255
|
if (!this.monthValue) return;
|
|
190
256
|
this.#syncFocusedDateWithMonth();
|
|
191
|
-
this
|
|
257
|
+
this.#repaintKeepingFocus();
|
|
192
258
|
}
|
|
193
259
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
260
|
+
* Repaints the label when application code changes `locale` at runtime.
|
|
261
|
+
*
|
|
262
|
+
* Stimulus runs this for the initial Value too, before `connect()`. The paint
|
|
263
|
+
* it triggers is idempotent — the month comes from `month` (or, while that is
|
|
264
|
+
* still empty, from `focusedDate`) — and a grid that has not connected
|
|
265
|
+
* reports nothing, so that paint never reports a month.
|
|
266
|
+
*/
|
|
267
|
+
localeValueChanged() {
|
|
268
|
+
this.#repaintKeepingFocus();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Repaints `aria-selected` and the field when the requested day changes, and
|
|
272
|
+
* moves the tab stop to the day the grid now publishes. A request `min` /
|
|
273
|
+
* `max` withhold is not published, so the tab stop stays where it was.
|
|
196
274
|
*/
|
|
197
275
|
selectedValueChanged() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
276
|
+
const selection = parseISODateString(this.#selection);
|
|
277
|
+
if (selection) this.focusedDate = selection;
|
|
278
|
+
this.#repaintKeepingFocus();
|
|
279
|
+
}
|
|
280
|
+
/** Repaints when `min` changes after connect, so the disabled days and the selection follow. */
|
|
281
|
+
minValueChanged() {
|
|
282
|
+
this.#repaint.schedule();
|
|
283
|
+
}
|
|
284
|
+
/** Repaints when `max` changes after connect, so the disabled days and the selection follow. */
|
|
285
|
+
maxValueChanged() {
|
|
286
|
+
this.#repaint.schedule();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Lays the grid out again when `weekStart` changes after connect, so every
|
|
290
|
+
* date moves to the column of its weekday.
|
|
291
|
+
*
|
|
292
|
+
* Only the day cells are painted: column headers are author markup, so the
|
|
293
|
+
* author updates them together with the Value.
|
|
294
|
+
*/
|
|
295
|
+
weekStartValueChanged() {
|
|
296
|
+
this.#repaint.schedule();
|
|
205
297
|
}
|
|
206
298
|
/** Navigates to the previous month. */
|
|
207
299
|
prev(event) {
|
|
@@ -216,7 +308,7 @@ var CalendarController = class extends Controller {
|
|
|
216
308
|
/** Handles day selection when a gridcell is clicked. */
|
|
217
309
|
selectByClick(event) {
|
|
218
310
|
const dayElement = event.target?.closest(
|
|
219
|
-
"
|
|
311
|
+
targetSelector(this.identifier, "day")
|
|
220
312
|
);
|
|
221
313
|
if (!dayElement) return;
|
|
222
314
|
this.selectDayElement(dayElement);
|
|
@@ -226,7 +318,7 @@ var CalendarController = class extends Controller {
|
|
|
226
318
|
if (event.defaultPrevented) return;
|
|
227
319
|
if (isReservedArrowChord(event)) return;
|
|
228
320
|
const dayElement = event.target?.closest(
|
|
229
|
-
"
|
|
321
|
+
targetSelector(this.identifier, "day")
|
|
230
322
|
);
|
|
231
323
|
if (!dayElement) return;
|
|
232
324
|
const date = parseISODateString(dayElement.getAttribute("data-date") ?? "");
|
|
@@ -291,8 +383,8 @@ var CalendarController = class extends Controller {
|
|
|
291
383
|
}
|
|
292
384
|
}
|
|
293
385
|
/**
|
|
294
|
-
* First day of the month the grid paints: the `month` Value when it parses
|
|
295
|
-
* otherwise the focused date's month.
|
|
386
|
+
* First day of the month the grid paints: the `month` Value when it parses —
|
|
387
|
+
* while it is empty, the month on screen — otherwise the focused date's month.
|
|
296
388
|
*
|
|
297
389
|
* A malformed `month` falls back instead of stopping the paint: every cell
|
|
298
390
|
* keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
|
|
@@ -301,12 +393,22 @@ var CalendarController = class extends Controller {
|
|
|
301
393
|
* the paint and the `monthchange` report cannot name different months.
|
|
302
394
|
*/
|
|
303
395
|
#paintedMonthStart() {
|
|
304
|
-
const monthInfo = parseISOMonthString(this
|
|
396
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
305
397
|
return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
|
|
306
398
|
}
|
|
307
399
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
400
|
+
* The month the grid is asked to paint: `month`, or while that is empty the
|
|
401
|
+
* month on screen, which `connect()` opens on the month of the published
|
|
402
|
+
* selection, else on the current one.
|
|
403
|
+
*/
|
|
404
|
+
get #monthRequest() {
|
|
405
|
+
return this.monthValue || this.#paintedMonth;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Renders the grid days and updates the month/year label, mirrors both
|
|
409
|
+
* fields, then reports each field the user's move moved and — once connected
|
|
410
|
+
* — a painted month that differs from the last one painted, and a published
|
|
411
|
+
* selection that a change the page made moved.
|
|
310
412
|
*
|
|
311
413
|
* The report belongs to the paint, not to the `month` Value: the Value is only
|
|
312
414
|
* one of the things that decide the month on screen. While a malformed `month`
|
|
@@ -314,24 +416,36 @@ var CalendarController = class extends Controller {
|
|
|
314
416
|
* move the painted month too, and both repaint from here. Reading the report
|
|
315
417
|
* off the paint keeps the event naming the month the grid shows, in the
|
|
316
418
|
* `YYYY-MM` the detail contract promises, whichever route repainted.
|
|
419
|
+
*
|
|
420
|
+
* @stimeoRenderRoot
|
|
317
421
|
*/
|
|
318
|
-
render() {
|
|
422
|
+
#render() {
|
|
319
423
|
const monthStart = this.#paintedMonthStart();
|
|
320
424
|
const year = monthStart.getFullYear();
|
|
321
425
|
const month = monthStart.getMonth() + 1;
|
|
426
|
+
const selection = this.#selection;
|
|
322
427
|
if (this.hasLabelTarget) {
|
|
323
|
-
const
|
|
324
|
-
this.labelTarget.textContent = monthLabelFormatter(
|
|
428
|
+
const locale = resolveLocale(this.element, this.localeValue);
|
|
429
|
+
this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
|
|
325
430
|
}
|
|
326
431
|
const days = this.#calculateGridDays(year, month);
|
|
327
432
|
const dayElements = this.dayTargets;
|
|
433
|
+
const consumerDisabled = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const el of dayElements) {
|
|
435
|
+
const shown = el.getAttribute("data-date");
|
|
436
|
+
const value = el.getAttribute("aria-disabled");
|
|
437
|
+
if (!shown || value === null || el.hasAttribute(this.#ownedDisabled)) continue;
|
|
438
|
+
consumerDisabled.set(shown, value);
|
|
439
|
+
}
|
|
328
440
|
for (const [index, date] of days.entries()) {
|
|
329
441
|
const el = dayElements[index];
|
|
330
442
|
if (!el) continue;
|
|
331
443
|
const dateStr = toISODateString(date);
|
|
332
444
|
if (el.getAttribute("data-date") !== dateStr) {
|
|
333
445
|
el.removeAttribute("aria-disabled");
|
|
334
|
-
el.removeAttribute(
|
|
446
|
+
el.removeAttribute(this.#ownedDisabled);
|
|
447
|
+
const kept = consumerDisabled.get(dateStr);
|
|
448
|
+
if (kept !== void 0) el.setAttribute("aria-disabled", kept);
|
|
335
449
|
}
|
|
336
450
|
el.setAttribute("data-date", dateStr);
|
|
337
451
|
el.textContent = String(date.getDate());
|
|
@@ -339,17 +453,16 @@ var CalendarController = class extends Controller {
|
|
|
339
453
|
el.setAttribute("data-outside", String(isOutside));
|
|
340
454
|
const todayStr = toISODateString(/* @__PURE__ */ new Date());
|
|
341
455
|
el.setAttribute("data-today", String(dateStr === todayStr));
|
|
342
|
-
|
|
343
|
-
el.setAttribute("aria-selected", String(isSelected));
|
|
456
|
+
el.setAttribute("aria-selected", String(dateStr === selection));
|
|
344
457
|
const isFocused = toISODateString(this.focusedDate) === dateStr;
|
|
345
458
|
el.setAttribute("tabindex", isFocused ? "0" : "-1");
|
|
346
459
|
const isDisabled = this.#isDateOutOfBounds(dateStr);
|
|
347
460
|
if (isDisabled) {
|
|
348
|
-
if (!el.hasAttribute("aria-disabled")) el.setAttribute(
|
|
461
|
+
if (!el.hasAttribute("aria-disabled")) el.setAttribute(this.#ownedDisabled, "");
|
|
349
462
|
el.setAttribute("aria-disabled", "true");
|
|
350
|
-
} else if (el.hasAttribute(
|
|
463
|
+
} else if (el.hasAttribute(this.#ownedDisabled)) {
|
|
351
464
|
el.removeAttribute("aria-disabled");
|
|
352
|
-
el.removeAttribute(
|
|
465
|
+
el.removeAttribute(this.#ownedDisabled);
|
|
353
466
|
}
|
|
354
467
|
}
|
|
355
468
|
if (!dayElements.some((el) => el.getAttribute("tabindex") === "0")) {
|
|
@@ -357,50 +470,151 @@ var CalendarController = class extends Controller {
|
|
|
357
470
|
fallback?.setAttribute("tabindex", "0");
|
|
358
471
|
}
|
|
359
472
|
const painted = toISOMonthString(monthStart);
|
|
360
|
-
const previous = this.#
|
|
361
|
-
this.#
|
|
362
|
-
|
|
473
|
+
const previous = this.#paintedMonth;
|
|
474
|
+
this.#paintedMonth = painted;
|
|
475
|
+
const reconciled = this.#settleSelection(selection);
|
|
476
|
+
const move = this.#selectionMoves;
|
|
477
|
+
for (const [field, value] of this.#mirrorFields(painted, selection, reconciled)) {
|
|
478
|
+
if (field.value === value) commitField(field);
|
|
479
|
+
}
|
|
480
|
+
if (this.#connected && previous !== painted && this.#paintedMonth === painted) {
|
|
363
481
|
this.dispatch("monthchange", { detail: { month: painted } });
|
|
364
482
|
}
|
|
483
|
+
if (reconciled && move === this.#selectionMoves) {
|
|
484
|
+
this.dispatch("reconcile", { detail: { date: selection } });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Records the selection a paint publishes and says whether it moved from the
|
|
489
|
+
* one published before, which a connected grid reports as `reconcile`. A pick
|
|
490
|
+
* records its own day before its paint, so that paint finds no move, and
|
|
491
|
+
* nothing counts before the grid connects. A move counts as a move of the
|
|
492
|
+
* selection.
|
|
493
|
+
*/
|
|
494
|
+
#settleSelection(selection) {
|
|
495
|
+
const published = this.#published;
|
|
496
|
+
this.#published = selection;
|
|
497
|
+
if (!this.#connected || selection === published) return false;
|
|
498
|
+
this.#selectionMoves += 1;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Repaints the grid for a Value callback or a pick. The first repaint of a
|
|
503
|
+
* batch records where DOM focus stood on a day, for the settle pass; focus
|
|
504
|
+
* outside the grid is left alone.
|
|
505
|
+
*/
|
|
506
|
+
#repaintKeepingFocus() {
|
|
507
|
+
if (!this.#focusOrigin) {
|
|
508
|
+
const cell = this.dayTargets.find((day) => day === document.activeElement);
|
|
509
|
+
if (cell) {
|
|
510
|
+
this.#focusOrigin = {
|
|
511
|
+
cell,
|
|
512
|
+
date: cell.getAttribute("data-date"),
|
|
513
|
+
month: this.#paintedMonth
|
|
514
|
+
};
|
|
515
|
+
this.#settleFocus.schedule();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
this.#render();
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Puts DOM focus back on a day it can stay on once a batch of repaints has
|
|
522
|
+
* landed.
|
|
523
|
+
*
|
|
524
|
+
* While the painted month stays, a bound only toggles `aria-disabled` and a
|
|
525
|
+
* disabled day stays focusable, so nothing is called (`focus()` would scroll
|
|
526
|
+
* the day into view); `weekStart` moves every date to another cell, so focus
|
|
527
|
+
* follows its date, or goes to the tab stop once the date has left the grid.
|
|
528
|
+
* When the painted month moves, every cell shows another day, and focus goes
|
|
529
|
+
* to the tab stop of the new month. Focus a listener of the paint's events
|
|
530
|
+
* moved off the recorded cell stays where it went.
|
|
531
|
+
*/
|
|
532
|
+
#restoreFocus() {
|
|
533
|
+
const origin = this.#focusOrigin;
|
|
534
|
+
this.#focusOrigin = null;
|
|
535
|
+
if (!origin || document.activeElement !== origin.cell) return;
|
|
536
|
+
const moved = this.#paintedMonth !== origin.month;
|
|
537
|
+
if (!moved && origin.cell.getAttribute("data-date") === origin.date) return;
|
|
538
|
+
const destination = (moved ? void 0 : this.dayTargets.find((cell) => cell.getAttribute("data-date") === origin.date)) ?? this.dayTargets.find((cell) => cell.getAttribute("tabindex") === "0");
|
|
539
|
+
destination?.focus();
|
|
540
|
+
}
|
|
541
|
+
/** Fills a selected-day field inserted or replaced at runtime with the published selection. */
|
|
542
|
+
fieldTargetConnected(field) {
|
|
543
|
+
writeField(field, this.#selection);
|
|
544
|
+
}
|
|
545
|
+
/** Fills a painted-month field inserted or replaced at runtime. */
|
|
546
|
+
monthFieldTargetConnected(field) {
|
|
547
|
+
writeField(field, toISOMonthString(this.#paintedMonthStart()));
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Mirrors the painted month and the published selection into their optional
|
|
551
|
+
* fields.
|
|
552
|
+
*
|
|
553
|
+
* @param reconciled - Whether a change the page made moved the selection in
|
|
554
|
+
* this paint, which `reconcile` reports rather than the field.
|
|
555
|
+
* @returns Each field this grid's own controls moved, with the value written
|
|
556
|
+
* to it, for the paint to report.
|
|
557
|
+
*/
|
|
558
|
+
#mirrorFields(painted, selection, reconciled) {
|
|
559
|
+
const byUser = this.#movedByUser;
|
|
560
|
+
this.#movedByUser = false;
|
|
561
|
+
const moved = [];
|
|
562
|
+
if (this.hasFieldTarget && writeField(this.fieldTarget, selection) && !reconciled) {
|
|
563
|
+
moved.push([this.fieldTarget, selection]);
|
|
564
|
+
}
|
|
565
|
+
if (this.hasMonthFieldTarget && writeField(this.monthFieldTarget, painted)) {
|
|
566
|
+
moved.push([this.monthFieldTarget, painted]);
|
|
567
|
+
}
|
|
568
|
+
return byUser ? moved : [];
|
|
365
569
|
}
|
|
366
570
|
selectDayElement(dayElement) {
|
|
367
571
|
if (dayElement.getAttribute("aria-disabled") === "true") return;
|
|
368
572
|
const dateStr = dayElement.getAttribute("data-date");
|
|
369
|
-
if (!dateStr) return;
|
|
573
|
+
if (!dateStr || this.#isDateOutOfBounds(dateStr)) return;
|
|
370
574
|
this.selectedValue = dateStr;
|
|
371
575
|
const selected = parseISODateString(dateStr);
|
|
372
576
|
if (selected) this.focusedDate = selected;
|
|
373
|
-
this
|
|
577
|
+
this.#movedByUser = true;
|
|
578
|
+
this.#selectionMoves += 1;
|
|
579
|
+
const move = this.#selectionMoves;
|
|
580
|
+
this.#published = dateStr;
|
|
581
|
+
this.#repaintKeepingFocus();
|
|
582
|
+
if (move !== this.#selectionMoves) return;
|
|
374
583
|
this.dispatch("select", { detail: { date: dateStr } });
|
|
375
584
|
}
|
|
376
585
|
#focusAndNavigateToDate(date) {
|
|
377
586
|
this.focusedDate = date;
|
|
378
587
|
const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
379
|
-
|
|
380
|
-
if (
|
|
588
|
+
this.#movedByUser = true;
|
|
589
|
+
if (targetMonthStr !== this.#monthRequest) {
|
|
381
590
|
this.monthValue = targetMonthStr;
|
|
382
|
-
|
|
383
|
-
this.render();
|
|
384
|
-
}
|
|
385
|
-
const focusTarget = () => {
|
|
386
|
-
const dateStr = toISODateString(date);
|
|
387
|
-
const targetEl = this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr);
|
|
388
|
-
targetEl?.focus();
|
|
389
|
-
};
|
|
390
|
-
if (isMonthTransition) {
|
|
391
|
-
this.#focusTimer.set(focusTarget, 0);
|
|
392
|
-
} else {
|
|
393
|
-
focusTarget();
|
|
591
|
+
return;
|
|
394
592
|
}
|
|
593
|
+
this.#render();
|
|
594
|
+
const dateStr = toISODateString(date);
|
|
595
|
+
this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr)?.focus();
|
|
395
596
|
}
|
|
396
597
|
#isDateOutOfBounds(dateStr) {
|
|
397
598
|
if (this.minValue && dateStr < this.minValue) return true;
|
|
398
599
|
if (this.maxValue && dateStr > this.maxValue) return true;
|
|
399
600
|
return false;
|
|
400
601
|
}
|
|
602
|
+
/** The day `selected` requests: the Value when it names a real day, else `""`. */
|
|
603
|
+
get #requestedDay() {
|
|
604
|
+
return parseISODateString(this.selectedValue) ? this.selectedValue : "";
|
|
605
|
+
}
|
|
606
|
+
/** The selection published for `requested`: the day while `min` / `max` allow it, else `""`. */
|
|
607
|
+
#publishable(requested) {
|
|
608
|
+
return requested && !this.#isDateOutOfBounds(requested) ? requested : "";
|
|
609
|
+
}
|
|
610
|
+
/** The selection the grid publishes now. */
|
|
611
|
+
get #selection() {
|
|
612
|
+
return this.#publishable(this.#requestedDay);
|
|
613
|
+
}
|
|
401
614
|
#shiftMonth(delta) {
|
|
402
|
-
const monthInfo = parseISOMonthString(this
|
|
615
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
403
616
|
if (!monthInfo) return;
|
|
617
|
+
this.#movedByUser = true;
|
|
404
618
|
const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
|
|
405
619
|
this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
|
|
406
620
|
}
|
|
@@ -439,15 +653,18 @@ var CalendarController = class extends Controller {
|
|
|
439
653
|
target.setDate(start.getDate() + 6);
|
|
440
654
|
return target;
|
|
441
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Opens the tab stop on the published selection, else on today when the shown
|
|
658
|
+
* month holds it, else on the 1st of the shown month. A selection the grid
|
|
659
|
+
* does not show falls back to the 1st in the paint.
|
|
660
|
+
*/
|
|
442
661
|
#initializeFocusedDate() {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
662
|
+
const selection = parseISODateString(this.#selection);
|
|
663
|
+
if (selection) {
|
|
664
|
+
this.focusedDate = selection;
|
|
665
|
+
return;
|
|
449
666
|
}
|
|
450
|
-
const monthInfo = parseISOMonthString(this
|
|
667
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
451
668
|
if (monthInfo) {
|
|
452
669
|
const today = /* @__PURE__ */ new Date();
|
|
453
670
|
if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {
|