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,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/local_time_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/intl_format.ts
|
|
6
3
|
var formatters = /* @__PURE__ */ new WeakMap();
|
|
7
4
|
function intlFormatter(factory, locale, options, fallbackLocale) {
|
|
8
5
|
const cache = cacheFor(factory);
|
|
@@ -31,32 +28,37 @@ function construct(factory, locale, options, fallbackLocale) {
|
|
|
31
28
|
}
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
// src/utils/locale.ts
|
|
35
31
|
function resolveLocale(element, declared = "") {
|
|
36
32
|
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
var ListenerSet = class {
|
|
36
|
+
#abort = new AbortController();
|
|
37
|
+
add(target, type, handler, options) {
|
|
38
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
39
|
+
}
|
|
40
|
+
dispose() {
|
|
41
|
+
this.#abort.abort();
|
|
42
|
+
this.#abort = new AbortController();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
40
46
|
var MicrotaskCoalescer = class {
|
|
41
47
|
#run;
|
|
42
48
|
#queued = false;
|
|
43
49
|
#active = false;
|
|
44
50
|
#generation = 0;
|
|
45
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
46
51
|
constructor(run) {
|
|
47
52
|
this.#run = run;
|
|
48
53
|
}
|
|
49
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
50
54
|
activate() {
|
|
51
55
|
this.#active = true;
|
|
52
56
|
}
|
|
53
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
54
57
|
cancel() {
|
|
55
58
|
this.#active = false;
|
|
56
59
|
this.#queued = false;
|
|
57
60
|
this.#generation += 1;
|
|
58
61
|
}
|
|
59
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
60
62
|
schedule() {
|
|
61
63
|
if (!this.#active || this.#queued) return;
|
|
62
64
|
this.#queued = true;
|
|
@@ -69,7 +71,31 @@ var MicrotaskCoalescer = class {
|
|
|
69
71
|
}
|
|
70
72
|
};
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
var MorphRenderWatcher = class {
|
|
75
|
+
#listeners = new ListenerSet();
|
|
76
|
+
#pass;
|
|
77
|
+
#element = null;
|
|
78
|
+
#onMorph = () => this.#pass.schedule();
|
|
79
|
+
constructor(run) {
|
|
80
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
81
|
+
}
|
|
82
|
+
observe(element) {
|
|
83
|
+
if (this.#element === element) return;
|
|
84
|
+
this.disconnect();
|
|
85
|
+
this.#element = element;
|
|
86
|
+
this.#pass.activate();
|
|
87
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
88
|
+
}
|
|
89
|
+
schedule() {
|
|
90
|
+
this.#pass.schedule();
|
|
91
|
+
}
|
|
92
|
+
disconnect() {
|
|
93
|
+
this.#listeners.dispose();
|
|
94
|
+
this.#pass.cancel();
|
|
95
|
+
this.#element = null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
73
99
|
var STYLES = /* @__PURE__ */ new Set(["full", "long", "medium", "short"]);
|
|
74
100
|
function toStyle(value) {
|
|
75
101
|
return STYLES.has(value) ? value : void 0;
|
|
@@ -83,55 +109,48 @@ var LocalTimeController = class extends Controller {
|
|
|
83
109
|
titleFormat: { type: String, default: "" }
|
|
84
110
|
};
|
|
85
111
|
static events = ["format"];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
112
|
+
#resync = new MorphRenderWatcher(() => {
|
|
113
|
+
const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;
|
|
114
|
+
const notify = this.#formatPending || datetimeChanged;
|
|
115
|
+
this.#formatPending = false;
|
|
116
|
+
this.#render(notify);
|
|
117
|
+
});
|
|
118
|
+
#formatPending = false;
|
|
92
119
|
#datetimeWatch = new MutationObserver(() => {
|
|
120
|
+
this.#formatPending = true;
|
|
93
121
|
this.#resync.schedule();
|
|
94
122
|
});
|
|
95
123
|
connect() {
|
|
96
|
-
this.#resync.
|
|
124
|
+
this.#resync.observe(this.element);
|
|
97
125
|
this.#datetimeWatch.observe(this.element, { attributeFilter: ["datetime"] });
|
|
126
|
+
this.#formatPending = false;
|
|
98
127
|
this.#render();
|
|
99
128
|
}
|
|
100
129
|
disconnect() {
|
|
101
|
-
this.#resync.
|
|
130
|
+
this.#resync.disconnect();
|
|
102
131
|
this.#datetimeWatch.disconnect();
|
|
103
132
|
}
|
|
104
|
-
/** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */
|
|
105
133
|
localeValueChanged() {
|
|
134
|
+
this.#formatPending = true;
|
|
106
135
|
this.#resync.schedule();
|
|
107
136
|
}
|
|
108
|
-
/** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */
|
|
109
137
|
timeZoneValueChanged() {
|
|
138
|
+
this.#formatPending = true;
|
|
110
139
|
this.#resync.schedule();
|
|
111
140
|
}
|
|
112
|
-
/** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */
|
|
113
141
|
dateStyleValueChanged() {
|
|
142
|
+
this.#formatPending = true;
|
|
114
143
|
this.#resync.schedule();
|
|
115
144
|
}
|
|
116
|
-
/** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */
|
|
117
145
|
timeStyleValueChanged() {
|
|
146
|
+
this.#formatPending = true;
|
|
118
147
|
this.#resync.schedule();
|
|
119
148
|
}
|
|
120
|
-
/** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */
|
|
121
149
|
titleFormatValueChanged() {
|
|
150
|
+
this.#formatPending = true;
|
|
122
151
|
this.#resync.schedule();
|
|
123
152
|
}
|
|
124
|
-
|
|
125
|
-
* Formats the instant in `datetime` against the current Values and writes it out.
|
|
126
|
-
*
|
|
127
|
-
* The `format` event rides with every pass, including a repaint a morph triggers:
|
|
128
|
-
* its condition is that formatting was applied, and a repaint applies it with a
|
|
129
|
-
* new result. A pass that cannot format writes nothing and emits nothing, so the
|
|
130
|
-
* authored absolute text stays as the fallback.
|
|
131
|
-
*
|
|
132
|
-
* @stimeoRenderRoot
|
|
133
|
-
*/
|
|
134
|
-
#render() {
|
|
153
|
+
#render(notify = true) {
|
|
135
154
|
const date = this.#parse();
|
|
136
155
|
if (date === null) return;
|
|
137
156
|
const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);
|
|
@@ -139,31 +158,14 @@ var LocalTimeController = class extends Controller {
|
|
|
139
158
|
this.element.textContent = formatted;
|
|
140
159
|
const title = this.#title(date);
|
|
141
160
|
if (title !== null) this.element.setAttribute("title", title);
|
|
142
|
-
this.dispatch("format", { detail: { formatted } });
|
|
161
|
+
if (notify) this.dispatch("format", { detail: { formatted } });
|
|
143
162
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Parses the UTC `datetime` attribute into a {@link Date}, or `null`. Whitespace
|
|
146
|
-
* around the attribute value is tolerated.
|
|
147
|
-
*/
|
|
148
163
|
#parse() {
|
|
149
164
|
const raw = this.element.getAttribute("datetime");
|
|
150
165
|
if (!raw) return null;
|
|
151
166
|
const ms = Date.parse(this.#asUtc(raw.trim()));
|
|
152
167
|
return Number.isNaN(ms) ? null : new Date(ms);
|
|
153
168
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Reads a timezone-less date-time as UTC — the input contract of this
|
|
156
|
-
* controller — since `Date.parse` would otherwise read `"2026-06-08T12:30:00"` in
|
|
157
|
-
* the *runtime's* local zone, contradicting "the server emits UTC". Values that
|
|
158
|
-
* already carry `Z` or a `±hh:mm` offset (and bare `YYYY-MM-DD` dates, already
|
|
159
|
-
* parsed as UTC) are returned unchanged.
|
|
160
|
-
*
|
|
161
|
-
* HTML accepts a space where ISO 8601 wants `T`, and `Date.parse` of that form is
|
|
162
|
-
* left to each engine, so a whole value shaped that way is normalized to the `T`
|
|
163
|
-
* separator first. The pattern is anchored: a value trailing anything else — a
|
|
164
|
-
* zone word such as `"2026-06-08 12:30:00 UTC"` — is handed to `Date.parse` as
|
|
165
|
-
* authored instead of being turned into a string nothing can parse.
|
|
166
|
-
*/
|
|
167
169
|
#asUtc(value) {
|
|
168
170
|
const isoLike = value.replace(
|
|
169
171
|
/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$/,
|
|
@@ -173,21 +175,10 @@ var LocalTimeController = class extends Controller {
|
|
|
173
175
|
const hasZone = /(Z|[+-]\d{2}:?\d{2})$/.test(isoLike);
|
|
174
176
|
return hasTime && !hasZone ? `${isoLike}Z` : isoLike;
|
|
175
177
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Builds the optional detailed `title`. `titleFormat` is an `Intl` style
|
|
178
|
-
* keyword applied to *both* date and time; empty (the default) adds no title.
|
|
179
|
-
*/
|
|
180
178
|
#title(date) {
|
|
181
179
|
if (this.titleFormatValue.length === 0) return null;
|
|
182
180
|
return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);
|
|
183
181
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Formats `date` with `Intl.DateTimeFormat`, including each style only when it
|
|
186
|
-
* is a valid keyword (so a consumer can show date-only or time-only by clearing
|
|
187
|
-
* the other). Returns `null` when neither style is usable or the declaration
|
|
188
|
-
* names a locale or time zone `Intl` rejects, so the caller can leave the
|
|
189
|
-
* authored text untouched.
|
|
190
|
-
*/
|
|
191
182
|
#applyFormat(date, dateStyle, timeStyle) {
|
|
192
183
|
const options = {
|
|
193
184
|
dateStyle: toStyle(dateStyle),
|
|
@@ -205,5 +196,4 @@ var LocalTimeController = class extends Controller {
|
|
|
205
196
|
};
|
|
206
197
|
|
|
207
198
|
export { LocalTimeController };
|
|
208
|
-
//# sourceMappingURL=local_time_controller.js.map
|
|
209
199
|
//# sourceMappingURL=local_time_controller.js.map
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/masonry_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/layout_observer.ts
|
|
6
3
|
var LayoutObserver = class {
|
|
7
4
|
#callback;
|
|
8
5
|
#resizeObserverFactory;
|
|
9
6
|
#resizeObserver = null;
|
|
10
7
|
#observingViewport = false;
|
|
11
8
|
#loadContainer = null;
|
|
12
|
-
/** Stable bound handler so add/removeEventListener target the same reference. */
|
|
13
9
|
#handleViewportResize = () => {
|
|
14
10
|
this.#callback();
|
|
15
11
|
};
|
|
16
|
-
/** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
|
|
17
12
|
#handleDescendantLoad = () => {
|
|
18
13
|
this.#callback();
|
|
19
14
|
};
|
|
@@ -21,11 +16,6 @@ var LayoutObserver = class {
|
|
|
21
16
|
this.#callback = callback;
|
|
22
17
|
this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
|
|
23
18
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Starts observing an element's size. Repeated calls observe additional
|
|
26
|
-
* elements through the same shared observer. No-ops when no
|
|
27
|
-
* `ResizeObserver` implementation is available.
|
|
28
|
-
*/
|
|
29
19
|
observe(element) {
|
|
30
20
|
if (!this.#resizeObserverFactory) return;
|
|
31
21
|
if (!this.#resizeObserver) {
|
|
@@ -35,47 +25,28 @@ var LayoutObserver = class {
|
|
|
35
25
|
}
|
|
36
26
|
this.#resizeObserver.observe(element);
|
|
37
27
|
}
|
|
38
|
-
/** Stops observing a single element while leaving any others in place. */
|
|
39
28
|
unobserve(element) {
|
|
40
29
|
this.#resizeObserver?.unobserve(element);
|
|
41
30
|
}
|
|
42
|
-
/** Starts observing viewport resizes. Idempotent: the listener is added once. */
|
|
43
31
|
observeViewport() {
|
|
44
32
|
if (this.#observingViewport) return;
|
|
45
33
|
this.#observingViewport = true;
|
|
46
34
|
window.addEventListener("resize", this.#handleViewportResize);
|
|
47
35
|
}
|
|
48
|
-
/** Stops observing viewport resizes without affecting element observation. */
|
|
49
36
|
unobserveViewport() {
|
|
50
37
|
if (!this.#observingViewport) return;
|
|
51
38
|
this.#observingViewport = false;
|
|
52
39
|
window.removeEventListener("resize", this.#handleViewportResize);
|
|
53
40
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Starts reporting a `load` from anywhere inside `container` — an image or a
|
|
56
|
-
* frame settling changes the box it sits in, and it measures as zero high until
|
|
57
|
-
* then. `load` does not bubble, so the subscription is a capture-phase listener
|
|
58
|
-
* on the container itself and nothing the caller spells.
|
|
59
|
-
*
|
|
60
|
-
* **One container at a time.** A further call moves the observation, so a widget
|
|
61
|
-
* whose content element is swapped at runtime releases the element it let go by
|
|
62
|
-
* naming the new one — there is no second place for the release to drift from.
|
|
63
|
-
*/
|
|
64
41
|
observeDescendantLoads(container) {
|
|
65
42
|
this.unobserveDescendantLoads();
|
|
66
43
|
this.#loadContainer = container;
|
|
67
44
|
container.addEventListener("load", this.#handleDescendantLoad, true);
|
|
68
45
|
}
|
|
69
|
-
/** Stops reporting descendant loads without affecting element or viewport observation. */
|
|
70
46
|
unobserveDescendantLoads() {
|
|
71
47
|
this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
|
|
72
48
|
this.#loadContainer = null;
|
|
73
49
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Releases every observation: disconnects the {@link ResizeObserver} and removes
|
|
76
|
-
* the viewport and descendant-load listeners. Safe to call multiple times. Call
|
|
77
|
-
* this from a controller's `disconnect()`.
|
|
78
|
-
*/
|
|
79
50
|
disconnect() {
|
|
80
51
|
this.#resizeObserver?.disconnect();
|
|
81
52
|
this.#resizeObserver = null;
|
|
@@ -84,27 +55,22 @@ var LayoutObserver = class {
|
|
|
84
55
|
}
|
|
85
56
|
};
|
|
86
57
|
|
|
87
|
-
// src/utils/microtask_coalescer.ts
|
|
88
58
|
var MicrotaskCoalescer = class {
|
|
89
59
|
#run;
|
|
90
60
|
#queued = false;
|
|
91
61
|
#active = false;
|
|
92
62
|
#generation = 0;
|
|
93
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
94
63
|
constructor(run) {
|
|
95
64
|
this.#run = run;
|
|
96
65
|
}
|
|
97
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
98
66
|
activate() {
|
|
99
67
|
this.#active = true;
|
|
100
68
|
}
|
|
101
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
102
69
|
cancel() {
|
|
103
70
|
this.#active = false;
|
|
104
71
|
this.#queued = false;
|
|
105
72
|
this.#generation += 1;
|
|
106
73
|
}
|
|
107
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
108
74
|
schedule() {
|
|
109
75
|
if (!this.#active || this.#queued) return;
|
|
110
76
|
this.#queued = true;
|
|
@@ -117,69 +83,87 @@ var MicrotaskCoalescer = class {
|
|
|
117
83
|
}
|
|
118
84
|
};
|
|
119
85
|
|
|
120
|
-
|
|
86
|
+
var NUMBER_BOUNDS = {
|
|
87
|
+
finite: { finite: true }};
|
|
88
|
+
function matchesNumberBounds(value, bounds) {
|
|
89
|
+
if (!Number.isFinite(value)) {
|
|
90
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
91
|
+
if (direction === null) return false;
|
|
92
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
93
|
+
}
|
|
94
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
95
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
96
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
97
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
98
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readNumber(raw, fallback, bounds) {
|
|
103
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var NumberValueReader = class {
|
|
107
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
108
|
+
read(owner, name, raw, fallback, bounds) {
|
|
109
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
110
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
111
|
+
this.#lastRejected.delete(name);
|
|
112
|
+
return resolved;
|
|
113
|
+
}
|
|
114
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
115
|
+
const literal = owner.element.getAttribute(attribute);
|
|
116
|
+
if (literal === null) {
|
|
117
|
+
this.#lastRejected.delete(name);
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
121
|
+
this.#lastRejected.set(name, literal);
|
|
122
|
+
console.warn(
|
|
123
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
121
130
|
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
122
131
|
var DEFAULT_MIN_COLUMN_WIDTH = 240;
|
|
123
132
|
var DEFAULT_GAP = 16;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
var MasonryController = class extends Controller {
|
|
133
|
+
var MasonryController = class _MasonryController extends Controller {
|
|
134
|
+
#numbers = new NumberValueReader();
|
|
128
135
|
static targets = ["item"];
|
|
129
136
|
static values = {
|
|
130
137
|
minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },
|
|
131
138
|
gap: { type: Number, default: DEFAULT_GAP }
|
|
132
139
|
};
|
|
140
|
+
static valueConstraints = {
|
|
141
|
+
minColumnWidth: NUMBER_BOUNDS.finite,
|
|
142
|
+
gap: NUMBER_BOUNDS.finite
|
|
143
|
+
};
|
|
133
144
|
static events = ["layout"];
|
|
134
|
-
/**
|
|
135
|
-
* The declared numbers after validation, so the layout path never sees a value
|
|
136
|
-
* it cannot compute with. Both are resolved once per declaration change rather
|
|
137
|
-
* than on every pass.
|
|
138
|
-
*/
|
|
139
145
|
#minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;
|
|
140
146
|
#gap = DEFAULT_GAP;
|
|
141
|
-
/**
|
|
142
|
-
* Collapses every re-layout trigger of one DOM mutation into a single pass, and
|
|
143
|
-
* refuses to run before `connect()` or after `disconnect()`.
|
|
144
|
-
*
|
|
145
|
-
* The triggers arrive in bursts — a resize stream, a morph that syncs several
|
|
146
|
-
* attributes, a batch of rows — and each pass measures every item, so folding
|
|
147
|
-
* them keeps the work proportional to the batch rather than to the events in it.
|
|
148
|
-
*/
|
|
149
147
|
#reconcile = new MicrotaskCoalescer(() => this.#relayout());
|
|
150
|
-
/** Items that left the target set and still carry the column hook. */
|
|
151
148
|
#released = /* @__PURE__ */ new Set();
|
|
152
149
|
#layout = new LayoutObserver(() => this.#reconcile.schedule());
|
|
153
150
|
#mutationObserver = null;
|
|
154
|
-
/** Last published column count, so `layout` fires only on real changes. */
|
|
155
151
|
#lastColumns = 0;
|
|
156
|
-
/** Resolves the declared column width once, falling back when it is unreadable. */
|
|
157
152
|
minColumnWidthValueChanged() {
|
|
158
|
-
this.#minColumnWidth =
|
|
153
|
+
this.#minColumnWidth = this.#safeMinColumnWidth;
|
|
159
154
|
this.#reconcile.schedule();
|
|
160
155
|
}
|
|
161
|
-
/** Resolves the declared gap once, falling back when it is unreadable. */
|
|
162
156
|
gapValueChanged() {
|
|
163
|
-
this.#gap =
|
|
157
|
+
this.#gap = this.#safeGap;
|
|
164
158
|
this.#reconcile.schedule();
|
|
165
159
|
}
|
|
166
|
-
/** Packs an element that became an item without moving in the DOM. */
|
|
167
160
|
itemTargetConnected() {
|
|
168
161
|
this.#reconcile.schedule();
|
|
169
162
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Queues the column hook of an element that stopped being an item for removal.
|
|
172
|
-
*
|
|
173
|
-
* The removal is queued rather than immediate because teardown reports every
|
|
174
|
-
* target as disconnected: doing it here would strip the whole grid just before
|
|
175
|
-
* a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
|
|
176
|
-
* with the pass, so only a genuine target change reaches it.
|
|
177
|
-
*/
|
|
178
163
|
itemTargetDisconnected(item) {
|
|
179
164
|
this.#released.add(item);
|
|
180
165
|
this.#reconcile.schedule();
|
|
181
166
|
}
|
|
182
|
-
/** Observes size/content changes and performs the first layout pass. */
|
|
183
167
|
connect() {
|
|
184
168
|
this.#layout.observe(this.element);
|
|
185
169
|
this.#layout.observeViewport();
|
|
@@ -191,31 +175,13 @@ var MasonryController = class extends Controller {
|
|
|
191
175
|
this.#relayout();
|
|
192
176
|
this.#reconcile.activate();
|
|
193
177
|
}
|
|
194
|
-
/** Releases every observation so nothing fires after detach. */
|
|
195
178
|
disconnect() {
|
|
196
179
|
this.#reconcile.cancel();
|
|
197
|
-
this.#released.clear();
|
|
198
180
|
this.#layout.disconnect();
|
|
199
181
|
this.#mutationObserver?.disconnect();
|
|
200
182
|
this.#mutationObserver = null;
|
|
201
183
|
this.#lastColumns = 0;
|
|
202
184
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Recomputes the column count and assigns every item to the shortest column.
|
|
205
|
-
* Runs automatically on connect, on resize, on item add/remove, when a declared
|
|
206
|
-
* number changes, and when a descendant resource loads (private — there is no
|
|
207
|
-
* public action; the observers, the target callbacks and the capture-phase
|
|
208
|
-
* `load` listener drive it). Items are walked in DOM order; each lands in the
|
|
209
|
-
* column with the least accumulated height, which keeps the packing balanced
|
|
210
|
-
* without reordering the DOM.
|
|
211
|
-
*
|
|
212
|
-
* Every box is measured before anything is written. Interleaving the two would
|
|
213
|
-
* make a consumer's `data-column` rule invalidate style once per item, and the
|
|
214
|
-
* next measurement then has to settle layout again — once per item instead of
|
|
215
|
-
* once per pass. The assignment is independent of the measurement because the
|
|
216
|
-
* columns are uniform in width, so the order of the two passes does not change
|
|
217
|
-
* the result.
|
|
218
|
-
*/
|
|
219
185
|
#relayout() {
|
|
220
186
|
const items = this.itemTargets;
|
|
221
187
|
const columns = this.#columnCount();
|
|
@@ -251,20 +217,31 @@ var MasonryController = class extends Controller {
|
|
|
251
217
|
this.dispatch("layout", { detail: { columns } });
|
|
252
218
|
}
|
|
253
219
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Derives how many columns fit: `floor((width + gap) / (minColumnWidth + gap))`,
|
|
256
|
-
* never fewer than one. When the width is unmeasurable (detached, or a layout
|
|
257
|
-
* engine that reports `0`), it falls back to a single column so every item still
|
|
258
|
-
* gets a valid `data-column`.
|
|
259
|
-
*/
|
|
260
220
|
#columnCount() {
|
|
261
221
|
const width = this.element.getBoundingClientRect().width;
|
|
262
222
|
const denominator = this.#minColumnWidth + this.#gap;
|
|
263
223
|
if (width <= 0 || denominator <= 0) return 1;
|
|
264
224
|
return Math.max(1, Math.floor((width + this.#gap) / denominator));
|
|
265
225
|
}
|
|
226
|
+
get #safeMinColumnWidth() {
|
|
227
|
+
return this.#numbers.read(
|
|
228
|
+
this,
|
|
229
|
+
"minColumnWidth",
|
|
230
|
+
this.minColumnWidthValue,
|
|
231
|
+
_MasonryController.values.minColumnWidth.default,
|
|
232
|
+
_MasonryController.valueConstraints.minColumnWidth
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
get #safeGap() {
|
|
236
|
+
return this.#numbers.read(
|
|
237
|
+
this,
|
|
238
|
+
"gap",
|
|
239
|
+
this.gapValue,
|
|
240
|
+
_MasonryController.values.gap.default,
|
|
241
|
+
_MasonryController.valueConstraints.gap
|
|
242
|
+
);
|
|
243
|
+
}
|
|
266
244
|
};
|
|
267
245
|
|
|
268
246
|
export { MasonryController };
|
|
269
|
-
//# sourceMappingURL=masonry_controller.js.map
|
|
270
247
|
//# sourceMappingURL=masonry_controller.js.map
|