stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function logicalArrowKey(key, element) {
|
|
12
30
|
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
31
|
if (!isRtl(element)) return key;
|
|
@@ -18,7 +36,6 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
36
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
// src/utils/intl_format.ts
|
|
22
39
|
var formatters = /* @__PURE__ */ new WeakMap();
|
|
23
40
|
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
24
41
|
const cache = cacheFor(factory);
|
|
@@ -47,7 +64,6 @@ function construct(factory, locale, options, fallbackLocale) {
|
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
// src/utils/dates.ts
|
|
51
67
|
function toISODateString(date) {
|
|
52
68
|
const year = date.getFullYear();
|
|
53
69
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
@@ -85,7 +101,6 @@ function monthLabelFormatter(locale) {
|
|
|
85
101
|
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
// src/utils/field_mirror.ts
|
|
89
104
|
function writeField(field, value) {
|
|
90
105
|
if (field.value === value) return false;
|
|
91
106
|
field.value = value;
|
|
@@ -95,32 +110,26 @@ function commitField(target) {
|
|
|
95
110
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
// src/utils/locale.ts
|
|
99
113
|
function resolveLocale(element, declared = "") {
|
|
100
114
|
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
// src/utils/microtask_coalescer.ts
|
|
104
117
|
var MicrotaskCoalescer = class {
|
|
105
118
|
#run;
|
|
106
119
|
#queued = false;
|
|
107
120
|
#active = false;
|
|
108
121
|
#generation = 0;
|
|
109
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
110
122
|
constructor(run) {
|
|
111
123
|
this.#run = run;
|
|
112
124
|
}
|
|
113
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
114
125
|
activate() {
|
|
115
126
|
this.#active = true;
|
|
116
127
|
}
|
|
117
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
118
128
|
cancel() {
|
|
119
129
|
this.#active = false;
|
|
120
130
|
this.#queued = false;
|
|
121
131
|
this.#generation += 1;
|
|
122
132
|
}
|
|
123
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
124
133
|
schedule() {
|
|
125
134
|
if (!this.#active || this.#queued) return;
|
|
126
135
|
this.#queued = true;
|
|
@@ -133,15 +142,92 @@ var MicrotaskCoalescer = class {
|
|
|
133
142
|
}
|
|
134
143
|
};
|
|
135
144
|
|
|
136
|
-
|
|
145
|
+
var ListenerSet = class {
|
|
146
|
+
#abort = new AbortController();
|
|
147
|
+
add(target, type, handler, options) {
|
|
148
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
149
|
+
}
|
|
150
|
+
dispose() {
|
|
151
|
+
this.#abort.abort();
|
|
152
|
+
this.#abort = new AbortController();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
var MorphRenderWatcher = class {
|
|
157
|
+
#listeners = new ListenerSet();
|
|
158
|
+
#pass;
|
|
159
|
+
#element = null;
|
|
160
|
+
#onMorph = () => this.#pass.schedule();
|
|
161
|
+
constructor(run) {
|
|
162
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
163
|
+
}
|
|
164
|
+
observe(element) {
|
|
165
|
+
if (this.#element === element) return;
|
|
166
|
+
this.disconnect();
|
|
167
|
+
this.#element = element;
|
|
168
|
+
this.#pass.activate();
|
|
169
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
170
|
+
}
|
|
171
|
+
schedule() {
|
|
172
|
+
this.#pass.schedule();
|
|
173
|
+
}
|
|
174
|
+
disconnect() {
|
|
175
|
+
this.#listeners.dispose();
|
|
176
|
+
this.#pass.cancel();
|
|
177
|
+
this.#element = null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function matchesNumberBounds(value, bounds) {
|
|
182
|
+
if (!Number.isFinite(value)) {
|
|
183
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
184
|
+
if (direction === null) return false;
|
|
185
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
186
|
+
}
|
|
187
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
188
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
189
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
190
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
191
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readNumber(raw, fallback, bounds) {
|
|
196
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
var NumberValueReader = class {
|
|
200
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
201
|
+
read(owner, name, raw, fallback, bounds) {
|
|
202
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
203
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
204
|
+
this.#lastRejected.delete(name);
|
|
205
|
+
return resolved;
|
|
206
|
+
}
|
|
207
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
208
|
+
const literal = owner.element.getAttribute(attribute);
|
|
209
|
+
if (literal === null) {
|
|
210
|
+
this.#lastRejected.delete(name);
|
|
211
|
+
return resolved;
|
|
212
|
+
}
|
|
213
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
214
|
+
this.#lastRejected.set(name, literal);
|
|
215
|
+
console.warn(
|
|
216
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return resolved;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
137
223
|
function targetSelector(identifier, name) {
|
|
138
224
|
return `[data-${identifier}-target~="${name}"]`;
|
|
139
225
|
}
|
|
140
226
|
|
|
141
|
-
// src/controllers/calendar_controller.ts
|
|
142
227
|
var OWNED_DISABLED = "owns-disabled";
|
|
143
|
-
var CalendarController = class extends Controller {
|
|
144
|
-
|
|
228
|
+
var CalendarController = class _CalendarController extends Controller {
|
|
229
|
+
#numbers = new NumberValueReader();
|
|
230
|
+
#fieldWrites = /* @__PURE__ */ new WeakMap();
|
|
145
231
|
get #ownedDisabled() {
|
|
146
232
|
return `data-${this.identifier}-${OWNED_DISABLED}`;
|
|
147
233
|
}
|
|
@@ -152,82 +238,31 @@ var CalendarController = class extends Controller {
|
|
|
152
238
|
min: { type: String, default: "" },
|
|
153
239
|
max: { type: String, default: "" },
|
|
154
240
|
weekStart: { type: Number, default: 0 },
|
|
155
|
-
// 0 = Sunday, 1 = Monday, etc.
|
|
156
241
|
locale: { type: String, default: "" }
|
|
157
242
|
};
|
|
158
|
-
static
|
|
243
|
+
static valueConstraints = {
|
|
244
|
+
weekStart: { finite: true, integer: true, min: 0, max: 6 }
|
|
245
|
+
};
|
|
246
|
+
static actions = ["next", "onKeydown", "prev", "select"];
|
|
159
247
|
static events = ["monthchange", "reconcile", "select"];
|
|
160
|
-
/** The date currently receiving focus in the grid (local time). */
|
|
161
248
|
focusedDate = /* @__PURE__ */ new Date();
|
|
162
|
-
/**
|
|
163
|
-
* Whether `connect()` has run and `disconnect()` has not since. The paints
|
|
164
|
-
* before and during `connect()` describe the grid, so they report nothing.
|
|
165
|
-
*/
|
|
166
249
|
#connected = false;
|
|
167
|
-
/**
|
|
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.
|
|
171
|
-
*/
|
|
172
250
|
#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
251
|
#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
252
|
#paintedMonth = "";
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
*/
|
|
253
|
+
#monthMoves = 0;
|
|
254
|
+
#paintedDates = /* @__PURE__ */ new WeakMap();
|
|
195
255
|
#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
256
|
#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(() => {
|
|
257
|
+
#repaint = new MorphRenderWatcher(() => {
|
|
212
258
|
this.#repaintKeepingFocus();
|
|
213
259
|
});
|
|
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
260
|
#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
261
|
#settleFocus = new MicrotaskCoalescer(() => {
|
|
227
262
|
this.#restoreFocus();
|
|
228
263
|
});
|
|
229
264
|
connect() {
|
|
230
|
-
this.#repaint.
|
|
265
|
+
this.#repaint.observe(this.element);
|
|
231
266
|
this.#settleFocus.activate();
|
|
232
267
|
this.#focusOrigin = null;
|
|
233
268
|
if (!this.monthValue && !this.#opened) {
|
|
@@ -238,82 +273,49 @@ var CalendarController = class extends Controller {
|
|
|
238
273
|
this.#render();
|
|
239
274
|
this.#connected = true;
|
|
240
275
|
}
|
|
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
|
-
*/
|
|
245
276
|
disconnect() {
|
|
246
277
|
this.#connected = false;
|
|
247
|
-
this.#repaint.
|
|
278
|
+
this.#repaint.disconnect();
|
|
248
279
|
this.#settleFocus.cancel();
|
|
249
280
|
}
|
|
250
|
-
/**
|
|
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.
|
|
253
|
-
*/
|
|
254
281
|
monthValueChanged() {
|
|
255
282
|
if (!this.monthValue) return;
|
|
256
283
|
this.#syncFocusedDateWithMonth();
|
|
257
284
|
this.#repaintKeepingFocus();
|
|
258
285
|
}
|
|
259
|
-
/**
|
|
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
286
|
localeValueChanged() {
|
|
268
287
|
this.#repaintKeepingFocus();
|
|
269
288
|
}
|
|
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.
|
|
274
|
-
*/
|
|
275
289
|
selectedValueChanged() {
|
|
276
290
|
const selection = parseISODateString(this.#selection);
|
|
277
291
|
if (selection) this.focusedDate = selection;
|
|
278
292
|
this.#repaintKeepingFocus();
|
|
279
293
|
}
|
|
280
|
-
/** Repaints when `min` changes after connect, so the disabled days and the selection follow. */
|
|
281
294
|
minValueChanged() {
|
|
282
295
|
this.#repaint.schedule();
|
|
283
296
|
}
|
|
284
|
-
/** Repaints when `max` changes after connect, so the disabled days and the selection follow. */
|
|
285
297
|
maxValueChanged() {
|
|
286
298
|
this.#repaint.schedule();
|
|
287
299
|
}
|
|
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
300
|
weekStartValueChanged() {
|
|
296
301
|
this.#repaint.schedule();
|
|
297
302
|
}
|
|
298
|
-
/** Navigates to the previous month. */
|
|
299
303
|
prev(event) {
|
|
300
304
|
if (event) event.preventDefault();
|
|
301
305
|
this.#shiftMonth(-1);
|
|
302
306
|
}
|
|
303
|
-
/** Navigates to the next month. */
|
|
304
307
|
next(event) {
|
|
305
308
|
if (event) event.preventDefault();
|
|
306
309
|
this.#shiftMonth(1);
|
|
307
310
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
313
|
-
if (!
|
|
314
|
-
this
|
|
311
|
+
select(source) {
|
|
312
|
+
const { origin, reason } = actionSource(source);
|
|
313
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
314
|
+
return;
|
|
315
|
+
const day = origin?.closest(targetSelector(this.identifier, "day"));
|
|
316
|
+
if (!day || !this.dayTargets.includes(day)) return;
|
|
317
|
+
this.#selectDay(day, reason);
|
|
315
318
|
}
|
|
316
|
-
/** Handles grid cell keyboard navigation and triggers selection. */
|
|
317
319
|
onKeydown(event) {
|
|
318
320
|
if (event.defaultPrevented) return;
|
|
319
321
|
if (isReservedArrowChord(event)) return;
|
|
@@ -371,7 +373,7 @@ var CalendarController = class extends Controller {
|
|
|
371
373
|
case "Enter":
|
|
372
374
|
case " ":
|
|
373
375
|
event.preventDefault();
|
|
374
|
-
this
|
|
376
|
+
this.#selectDay(dayElement, stateReasonFor(event));
|
|
375
377
|
return;
|
|
376
378
|
default:
|
|
377
379
|
handled = false;
|
|
@@ -382,43 +384,13 @@ var CalendarController = class extends Controller {
|
|
|
382
384
|
this.#focusAndNavigateToDate(nextDate);
|
|
383
385
|
}
|
|
384
386
|
}
|
|
385
|
-
/**
|
|
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.
|
|
388
|
-
*
|
|
389
|
-
* A malformed `month` falls back instead of stopping the paint: every cell
|
|
390
|
-
* keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
|
|
391
|
-
* still leaves the grid reachable by Tab and the author can see what they
|
|
392
|
-
* typed. Every consumer of "which month is on screen" reads it from here, so
|
|
393
|
-
* the paint and the `monthchange` report cannot name different months.
|
|
394
|
-
*/
|
|
395
387
|
#paintedMonthStart() {
|
|
396
388
|
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
397
389
|
return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
|
|
398
390
|
}
|
|
399
|
-
/**
|
|
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
391
|
get #monthRequest() {
|
|
405
392
|
return this.monthValue || this.#paintedMonth;
|
|
406
393
|
}
|
|
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.
|
|
412
|
-
*
|
|
413
|
-
* The report belongs to the paint, not to the `month` Value: the Value is only
|
|
414
|
-
* one of the things that decide the month on screen. While a malformed `month`
|
|
415
|
-
* falls back, changing `selected` and selecting a neighbouring month's cell
|
|
416
|
-
* move the painted month too, and both repaint from here. Reading the report
|
|
417
|
-
* off the paint keeps the event naming the month the grid shows, in the
|
|
418
|
-
* `YYYY-MM` the detail contract promises, whichever route repainted.
|
|
419
|
-
*
|
|
420
|
-
* @stimeoRenderRoot
|
|
421
|
-
*/
|
|
422
394
|
#render() {
|
|
423
395
|
const monthStart = this.#paintedMonthStart();
|
|
424
396
|
const year = monthStart.getFullYear();
|
|
@@ -447,6 +419,7 @@ var CalendarController = class extends Controller {
|
|
|
447
419
|
const kept = consumerDisabled.get(dateStr);
|
|
448
420
|
if (kept !== void 0) el.setAttribute("aria-disabled", kept);
|
|
449
421
|
}
|
|
422
|
+
this.#paintedDates.set(el, dateStr);
|
|
450
423
|
el.setAttribute("data-date", dateStr);
|
|
451
424
|
el.textContent = String(date.getDate());
|
|
452
425
|
const isOutside = date.getFullYear() !== year || date.getMonth() !== month - 1;
|
|
@@ -472,25 +445,20 @@ var CalendarController = class extends Controller {
|
|
|
472
445
|
const painted = toISOMonthString(monthStart);
|
|
473
446
|
const previous = this.#paintedMonth;
|
|
474
447
|
this.#paintedMonth = painted;
|
|
448
|
+
if (previous !== painted) this.#monthMoves += 1;
|
|
449
|
+
const monthMove = this.#monthMoves;
|
|
475
450
|
const reconciled = this.#settleSelection(selection);
|
|
476
451
|
const move = this.#selectionMoves;
|
|
477
|
-
for (const [field, value] of this.#mirrorFields(painted, selection, reconciled)) {
|
|
478
|
-
if (field.value === value) commitField(field);
|
|
452
|
+
for (const [field, value, write] of this.#mirrorFields(painted, selection, reconciled)) {
|
|
453
|
+
if (field.value === value && this.#fieldWrites.get(field) === write) commitField(field);
|
|
479
454
|
}
|
|
480
|
-
if (this.#connected && previous !== painted &&
|
|
455
|
+
if (this.#connected && previous !== painted && monthMove === this.#monthMoves) {
|
|
481
456
|
this.dispatch("monthchange", { detail: { month: painted } });
|
|
482
457
|
}
|
|
483
458
|
if (reconciled && move === this.#selectionMoves) {
|
|
484
459
|
this.dispatch("reconcile", { detail: { date: selection } });
|
|
485
460
|
}
|
|
486
461
|
}
|
|
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
462
|
#settleSelection(selection) {
|
|
495
463
|
const published = this.#published;
|
|
496
464
|
this.#published = selection;
|
|
@@ -498,18 +466,13 @@ var CalendarController = class extends Controller {
|
|
|
498
466
|
this.#selectionMoves += 1;
|
|
499
467
|
return true;
|
|
500
468
|
}
|
|
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
469
|
#repaintKeepingFocus() {
|
|
507
470
|
if (!this.#focusOrigin) {
|
|
508
471
|
const cell = this.dayTargets.find((day) => day === document.activeElement);
|
|
509
472
|
if (cell) {
|
|
510
473
|
this.#focusOrigin = {
|
|
511
474
|
cell,
|
|
512
|
-
date: cell.getAttribute("data-date"),
|
|
475
|
+
date: cell.getAttribute("data-date") ?? this.#paintedDates.get(cell) ?? null,
|
|
513
476
|
month: this.#paintedMonth
|
|
514
477
|
};
|
|
515
478
|
this.#settleFocus.schedule();
|
|
@@ -517,18 +480,6 @@ var CalendarController = class extends Controller {
|
|
|
517
480
|
}
|
|
518
481
|
this.#render();
|
|
519
482
|
}
|
|
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
483
|
#restoreFocus() {
|
|
533
484
|
const origin = this.#focusOrigin;
|
|
534
485
|
this.#focusOrigin = null;
|
|
@@ -538,36 +489,46 @@ var CalendarController = class extends Controller {
|
|
|
538
489
|
const destination = (moved ? void 0 : this.dayTargets.find((cell) => cell.getAttribute("data-date") === origin.date)) ?? this.dayTargets.find((cell) => cell.getAttribute("tabindex") === "0");
|
|
539
490
|
destination?.focus();
|
|
540
491
|
}
|
|
541
|
-
|
|
492
|
+
labelTargetConnected() {
|
|
493
|
+
this.#repaint.schedule();
|
|
494
|
+
}
|
|
495
|
+
labelTargetDisconnected() {
|
|
496
|
+
this.#repaint.schedule();
|
|
497
|
+
}
|
|
542
498
|
fieldTargetConnected(field) {
|
|
543
|
-
writeField(field, this.#selection);
|
|
499
|
+
this.#writeField(field, this.#selection);
|
|
500
|
+
}
|
|
501
|
+
fieldTargetDisconnected() {
|
|
502
|
+
this.#repaint.schedule();
|
|
544
503
|
}
|
|
545
|
-
/** Fills a painted-month field inserted or replaced at runtime. */
|
|
546
504
|
monthFieldTargetConnected(field) {
|
|
547
|
-
writeField(field, toISOMonthString(this.#paintedMonthStart()));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*/
|
|
505
|
+
this.#writeField(field, toISOMonthString(this.#paintedMonthStart()));
|
|
506
|
+
}
|
|
507
|
+
monthFieldTargetDisconnected() {
|
|
508
|
+
this.#repaint.schedule();
|
|
509
|
+
}
|
|
510
|
+
#writeField(field, value) {
|
|
511
|
+
if (!writeField(field, value)) return false;
|
|
512
|
+
this.#fieldWrites.set(field, (this.#fieldWrites.get(field) ?? 0) + 1);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
558
515
|
#mirrorFields(painted, selection, reconciled) {
|
|
559
516
|
const byUser = this.#movedByUser;
|
|
560
517
|
this.#movedByUser = false;
|
|
561
518
|
const moved = [];
|
|
562
|
-
if (this.hasFieldTarget && writeField(this.fieldTarget, selection) && !reconciled) {
|
|
563
|
-
moved.push([this.fieldTarget, selection]);
|
|
519
|
+
if (this.hasFieldTarget && this.#writeField(this.fieldTarget, selection) && !reconciled) {
|
|
520
|
+
moved.push([this.fieldTarget, selection, this.#fieldWrites.get(this.fieldTarget) ?? 0]);
|
|
564
521
|
}
|
|
565
|
-
if (this.hasMonthFieldTarget && writeField(this.monthFieldTarget, painted)) {
|
|
566
|
-
moved.push([
|
|
522
|
+
if (this.hasMonthFieldTarget && this.#writeField(this.monthFieldTarget, painted)) {
|
|
523
|
+
moved.push([
|
|
524
|
+
this.monthFieldTarget,
|
|
525
|
+
painted,
|
|
526
|
+
this.#fieldWrites.get(this.monthFieldTarget) ?? 0
|
|
527
|
+
]);
|
|
567
528
|
}
|
|
568
529
|
return byUser ? moved : [];
|
|
569
530
|
}
|
|
570
|
-
|
|
531
|
+
#selectDay(dayElement, reason) {
|
|
571
532
|
if (dayElement.getAttribute("aria-disabled") === "true") return;
|
|
572
533
|
const dateStr = dayElement.getAttribute("data-date");
|
|
573
534
|
if (!dateStr || this.#isDateOutOfBounds(dateStr)) return;
|
|
@@ -575,12 +536,13 @@ var CalendarController = class extends Controller {
|
|
|
575
536
|
const selected = parseISODateString(dateStr);
|
|
576
537
|
if (selected) this.focusedDate = selected;
|
|
577
538
|
this.#movedByUser = true;
|
|
578
|
-
this.#
|
|
539
|
+
const changed = dateStr !== this.#published;
|
|
540
|
+
if (changed) this.#selectionMoves += 1;
|
|
579
541
|
const move = this.#selectionMoves;
|
|
580
542
|
this.#published = dateStr;
|
|
581
543
|
this.#repaintKeepingFocus();
|
|
582
|
-
if (move !== this.#selectionMoves) return;
|
|
583
|
-
this.dispatch("select", { detail: { date: dateStr } });
|
|
544
|
+
if (!changed || move !== this.#selectionMoves) return;
|
|
545
|
+
this.dispatch("select", { detail: { date: dateStr, reason } });
|
|
584
546
|
}
|
|
585
547
|
#focusAndNavigateToDate(date) {
|
|
586
548
|
this.focusedDate = date;
|
|
@@ -588,7 +550,7 @@ var CalendarController = class extends Controller {
|
|
|
588
550
|
this.#movedByUser = true;
|
|
589
551
|
if (targetMonthStr !== this.#monthRequest) {
|
|
590
552
|
this.monthValue = targetMonthStr;
|
|
591
|
-
return;
|
|
553
|
+
if (targetMonthStr !== this.#paintedMonth) return;
|
|
592
554
|
}
|
|
593
555
|
this.#render();
|
|
594
556
|
const dateStr = toISODateString(date);
|
|
@@ -599,15 +561,12 @@ var CalendarController = class extends Controller {
|
|
|
599
561
|
if (this.maxValue && dateStr > this.maxValue) return true;
|
|
600
562
|
return false;
|
|
601
563
|
}
|
|
602
|
-
/** The day `selected` requests: the Value when it names a real day, else `""`. */
|
|
603
564
|
get #requestedDay() {
|
|
604
565
|
return parseISODateString(this.selectedValue) ? this.selectedValue : "";
|
|
605
566
|
}
|
|
606
|
-
/** The selection published for `requested`: the day while `min` / `max` allow it, else `""`. */
|
|
607
567
|
#publishable(requested) {
|
|
608
568
|
return requested && !this.#isDateOutOfBounds(requested) ? requested : "";
|
|
609
569
|
}
|
|
610
|
-
/** The selection the grid publishes now. */
|
|
611
570
|
get #selection() {
|
|
612
571
|
return this.#publishable(this.#requestedDay);
|
|
613
572
|
}
|
|
@@ -617,6 +576,10 @@ var CalendarController = class extends Controller {
|
|
|
617
576
|
this.#movedByUser = true;
|
|
618
577
|
const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
|
|
619
578
|
this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
|
|
579
|
+
if (this.monthValue === this.#paintedMonth) {
|
|
580
|
+
this.#syncFocusedDateWithMonth();
|
|
581
|
+
this.#repaintKeepingFocus();
|
|
582
|
+
}
|
|
620
583
|
}
|
|
621
584
|
#calculateShiftedMonthDate(baseDate, delta) {
|
|
622
585
|
const targetDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + delta, 1);
|
|
@@ -642,7 +605,7 @@ var CalendarController = class extends Controller {
|
|
|
642
605
|
}
|
|
643
606
|
#getStartOfWeekDate(date) {
|
|
644
607
|
const currentDay = date.getDay();
|
|
645
|
-
const shift = (currentDay - this
|
|
608
|
+
const shift = (currentDay - this.#safeWeekStart + 7) % 7;
|
|
646
609
|
const target = new Date(date);
|
|
647
610
|
target.setDate(date.getDate() - shift);
|
|
648
611
|
return target;
|
|
@@ -653,11 +616,6 @@ var CalendarController = class extends Controller {
|
|
|
653
616
|
target.setDate(start.getDate() + 6);
|
|
654
617
|
return target;
|
|
655
618
|
}
|
|
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
|
-
*/
|
|
661
619
|
#initializeFocusedDate() {
|
|
662
620
|
const selection = parseISODateString(this.#selection);
|
|
663
621
|
if (selection) {
|
|
@@ -693,7 +651,7 @@ var CalendarController = class extends Controller {
|
|
|
693
651
|
#calculateGridDays(year, month) {
|
|
694
652
|
const firstDay = new Date(year, month - 1, 1);
|
|
695
653
|
const dayOfWeek = firstDay.getDay();
|
|
696
|
-
const offset = (dayOfWeek - this
|
|
654
|
+
const offset = (dayOfWeek - this.#safeWeekStart + 7) % 7;
|
|
697
655
|
const days = [];
|
|
698
656
|
const current = new Date(firstDay);
|
|
699
657
|
current.setDate(firstDay.getDate() - offset);
|
|
@@ -703,8 +661,16 @@ var CalendarController = class extends Controller {
|
|
|
703
661
|
}
|
|
704
662
|
return days;
|
|
705
663
|
}
|
|
664
|
+
get #safeWeekStart() {
|
|
665
|
+
return this.#numbers.read(
|
|
666
|
+
this,
|
|
667
|
+
"weekStart",
|
|
668
|
+
this.weekStartValue,
|
|
669
|
+
_CalendarController.values.weekStart.default,
|
|
670
|
+
_CalendarController.valueConstraints.weekStart
|
|
671
|
+
);
|
|
672
|
+
}
|
|
706
673
|
};
|
|
707
674
|
|
|
708
675
|
export { CalendarController };
|
|
709
|
-
//# sourceMappingURL=calendar_controller.js.map
|
|
710
676
|
//# sourceMappingURL=calendar_controller.js.map
|