stimeo-ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +163 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +313 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +252 -0
- data/dist/controllers/combobox_controller.js +162 -23
- data/dist/controllers/command_palette_controller.js +194 -17
- data/dist/controllers/context_menu_controller.js +32 -10
- data/dist/controllers/count_up_controller.js +8 -1
- data/dist/controllers/currency_input_controller.js +147 -0
- data/dist/controllers/data_grid_controller.js +246 -0
- data/dist/controllers/date_range_picker_controller.js +441 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +169 -0
- data/dist/controllers/file_dropzone_controller.js +165 -0
- data/dist/controllers/filter_controller.js +86 -0
- data/dist/controllers/flash_controller.js +36 -5
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +67 -19
- data/dist/controllers/lazy_frame_controller.js +54 -11
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +2 -2
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +785 -0
- data/dist/controllers/multi_select_controller.js +755 -0
- data/dist/controllers/navigation_menu_controller.js +511 -0
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +246 -27
- data/dist/controllers/overflow_menu_controller.js +381 -57
- data/dist/controllers/pagination_controller.js +163 -32
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/persist_controller.js +6 -6
- data/dist/controllers/pointer_drag_controller.js +9 -1
- data/dist/controllers/popover_controller.js +2 -2
- data/dist/controllers/radio_group_controller.js +22 -3
- data/dist/controllers/range_slider_controller.js +192 -0
- data/dist/controllers/rating_controller.js +16 -2
- data/dist/controllers/read_more_controller.js +238 -0
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +101 -14
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +40 -6
- data/dist/controllers/scrollspy_controller.js +369 -74
- data/dist/controllers/separator_controller.js +96 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/skeleton_controller.js +1 -1
- data/dist/controllers/slider_controller.js +32 -6
- data/dist/controllers/sortable_controller.js +34 -3
- data/dist/controllers/spinner_controller.js +1 -1
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +9 -4
- data/dist/controllers/sticky_observer_controller.js +109 -20
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +295 -0
- data/dist/controllers/theme_controller.js +42 -13
- data/dist/controllers/time_picker_controller.js +231 -0
- data/dist/controllers/toast_controller.js +40 -14
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +691 -0
- data/dist/index.js +4256 -915
- data/lib/stimeo/ui/version.rb +2 -3
- metadata +28 -2
|
@@ -2,6 +2,61 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/overflow_indicator_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/blur_deferral.ts
|
|
6
|
+
var BlurDeferral = class {
|
|
7
|
+
/** Elements currently holding an update back, mapped to their `blur` listener. */
|
|
8
|
+
#pending = /* @__PURE__ */ new Map();
|
|
9
|
+
/** Called after a pending element blurs and has been detached. */
|
|
10
|
+
#onRelease;
|
|
11
|
+
/** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
|
|
12
|
+
constructor(onRelease) {
|
|
13
|
+
this.#onRelease = onRelease;
|
|
14
|
+
}
|
|
15
|
+
/** Number of elements currently holding an update back. */
|
|
16
|
+
get size() {
|
|
17
|
+
return this.#pending.size;
|
|
18
|
+
}
|
|
19
|
+
/** Snapshot of the pending elements, safe to iterate while releasing them. */
|
|
20
|
+
get elements() {
|
|
21
|
+
return [...this.#pending.keys()];
|
|
22
|
+
}
|
|
23
|
+
/** Whether `element` is currently holding an update back. */
|
|
24
|
+
has(element) {
|
|
25
|
+
return this.#pending.has(element);
|
|
26
|
+
}
|
|
27
|
+
/** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
|
|
28
|
+
defer(element) {
|
|
29
|
+
if (this.#pending.has(element)) return;
|
|
30
|
+
const onBlur = () => {
|
|
31
|
+
this.#detach(element);
|
|
32
|
+
this.#onRelease(element);
|
|
33
|
+
};
|
|
34
|
+
this.#pending.set(element, onBlur);
|
|
35
|
+
element.addEventListener("blur", onBlur);
|
|
36
|
+
}
|
|
37
|
+
/** Defers `element` as the only pending entry, cancelling any others. */
|
|
38
|
+
deferOnly(element) {
|
|
39
|
+
for (const pending of this.elements) {
|
|
40
|
+
if (pending !== element) this.#detach(pending);
|
|
41
|
+
}
|
|
42
|
+
this.defer(element);
|
|
43
|
+
}
|
|
44
|
+
/** Cancels `element`'s deferral without completing it; no-ops when not pending. */
|
|
45
|
+
release(element) {
|
|
46
|
+
this.#detach(element);
|
|
47
|
+
}
|
|
48
|
+
/** Cancels every deferral without completing any of them. */
|
|
49
|
+
releaseAll() {
|
|
50
|
+
for (const element of this.elements) this.#detach(element);
|
|
51
|
+
}
|
|
52
|
+
/** Removes the `blur` listener for `element` and forgets it. */
|
|
53
|
+
#detach(element) {
|
|
54
|
+
const onBlur = this.#pending.get(element);
|
|
55
|
+
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
56
|
+
this.#pending.delete(element);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
5
60
|
// src/utils/layout_observer.ts
|
|
6
61
|
var LayoutObserver = class {
|
|
7
62
|
#callback;
|
|
@@ -58,7 +113,30 @@ var LayoutObserver = class {
|
|
|
58
113
|
}
|
|
59
114
|
};
|
|
60
115
|
|
|
116
|
+
// src/utils/logical_scroll.ts
|
|
117
|
+
function isRtl(element) {
|
|
118
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
119
|
+
}
|
|
120
|
+
function logicalScrollMetrics(element, horizontal) {
|
|
121
|
+
const max = Math.max(
|
|
122
|
+
0,
|
|
123
|
+
horizontal ? element.scrollWidth - element.clientWidth : element.scrollHeight - element.clientHeight
|
|
124
|
+
);
|
|
125
|
+
const raw = horizontal ? element.scrollLeft : element.scrollTop;
|
|
126
|
+
const position = horizontal && isRtl(element) ? -raw : raw;
|
|
127
|
+
return { position: Math.min(max, Math.max(0, position)), max };
|
|
128
|
+
}
|
|
129
|
+
function physicalScrollDelta(element, horizontal, logicalDelta) {
|
|
130
|
+
return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/utils/reduced_motion.ts
|
|
134
|
+
function prefersReducedMotion() {
|
|
135
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
136
|
+
}
|
|
137
|
+
|
|
61
138
|
// src/controllers/overflow_indicator_controller.ts
|
|
139
|
+
var DIRECTION_BUTTON_SELECTOR = "[data-stimeo--overflow-indicator-direction-param]";
|
|
62
140
|
var OverflowIndicatorController = class extends Controller {
|
|
63
141
|
static targets = ["viewport"];
|
|
64
142
|
static values = {
|
|
@@ -67,38 +145,59 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
67
145
|
};
|
|
68
146
|
static actions = ["scrollByPage", "update"];
|
|
69
147
|
static events = ["change"];
|
|
70
|
-
#layout = new LayoutObserver(() =>
|
|
148
|
+
#layout = new LayoutObserver(() => {
|
|
149
|
+
if (this.#connected) this.update();
|
|
150
|
+
});
|
|
151
|
+
#connected = false;
|
|
152
|
+
#observedViewport = null;
|
|
153
|
+
#observedContent = /* @__PURE__ */ new Set();
|
|
71
154
|
#mutationObserver = null;
|
|
155
|
+
/**
|
|
156
|
+
* Boundary buttons whose native `disabled` is held back while they hold focus.
|
|
157
|
+
*
|
|
158
|
+
* Completing the deferral is what actually disables the button, so the release
|
|
159
|
+
* callback — unlike {@link #cancelPendingButtonDisable} — applies it.
|
|
160
|
+
*/
|
|
161
|
+
#pendingButtonDisables = new BlurDeferral((button) => {
|
|
162
|
+
this.#restorePendingMarkers(button);
|
|
163
|
+
if (!button.disabled) this.#disableButton(button);
|
|
164
|
+
});
|
|
72
165
|
/** Last reported room, so `change` fires only on transitions. */
|
|
73
166
|
#state = null;
|
|
74
167
|
connect() {
|
|
75
|
-
|
|
76
|
-
this.#
|
|
77
|
-
this.#layout.observeViewport();
|
|
78
|
-
if (typeof MutationObserver !== "undefined") {
|
|
79
|
-
this.#mutationObserver = new MutationObserver(() => this.update());
|
|
80
|
-
this.#mutationObserver.observe(this.viewportTarget, {
|
|
81
|
-
childList: true,
|
|
82
|
-
subtree: true,
|
|
83
|
-
characterData: true
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
this.update();
|
|
168
|
+
this.#connected = true;
|
|
169
|
+
this.#syncViewport();
|
|
87
170
|
}
|
|
88
171
|
disconnect() {
|
|
172
|
+
this.#connected = false;
|
|
173
|
+
this.#stopObservingViewport();
|
|
89
174
|
this.#layout.disconnect();
|
|
90
|
-
this.#
|
|
91
|
-
this.#mutationObserver = null;
|
|
175
|
+
this.#clearPendingButtonDisables();
|
|
92
176
|
this.#state = null;
|
|
93
177
|
}
|
|
94
|
-
|
|
178
|
+
viewportTargetConnected() {
|
|
179
|
+
this.#syncViewport();
|
|
180
|
+
}
|
|
181
|
+
viewportTargetDisconnected(viewport) {
|
|
182
|
+
if (this.#observedViewport === viewport) this.#stopObservingViewport();
|
|
183
|
+
this.#syncViewport();
|
|
184
|
+
}
|
|
185
|
+
orientationValueChanged() {
|
|
186
|
+
if (this.#connected) this.update();
|
|
187
|
+
}
|
|
188
|
+
thresholdValueChanged() {
|
|
189
|
+
if (this.#connected) this.update();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Re-measures remaining scroll room and reflects the state hooks.
|
|
193
|
+
* Public so it can be wired to the viewport's `scroll`.
|
|
194
|
+
*/
|
|
95
195
|
update() {
|
|
96
196
|
if (!this.hasViewportTarget) return;
|
|
97
197
|
const vp = this.viewportTarget;
|
|
98
198
|
const horizontal = this.orientationValue !== "vertical";
|
|
99
|
-
const t = this
|
|
100
|
-
const scrollPos
|
|
101
|
-
const maxScroll = horizontal ? vp.scrollWidth - vp.clientWidth : vp.scrollHeight - vp.clientHeight;
|
|
199
|
+
const t = this.#threshold;
|
|
200
|
+
const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontal);
|
|
102
201
|
const start = scrollPos > t;
|
|
103
202
|
const end = scrollPos < maxScroll - t;
|
|
104
203
|
vp.setAttribute("data-overflow-start", start ? "true" : "false");
|
|
@@ -114,11 +213,13 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
114
213
|
if (!this.hasViewportTarget) return;
|
|
115
214
|
const direction = this.#directionFromEvent(event);
|
|
116
215
|
if (!direction) return;
|
|
216
|
+
if (this.#state && !this.#state[direction]) return;
|
|
117
217
|
const vp = this.viewportTarget;
|
|
118
218
|
const horizontal = this.orientationValue !== "vertical";
|
|
119
219
|
const page = horizontal ? vp.clientWidth : vp.clientHeight;
|
|
120
|
-
const
|
|
121
|
-
const
|
|
220
|
+
const logicalDelta = direction === "start" ? -page : page;
|
|
221
|
+
const delta = physicalScrollDelta(vp, horizontal, logicalDelta);
|
|
222
|
+
const behavior = prefersReducedMotion() ? "instant" : "smooth";
|
|
122
223
|
if (horizontal) {
|
|
123
224
|
vp.scrollBy({ left: delta, behavior });
|
|
124
225
|
} else {
|
|
@@ -127,10 +228,16 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
127
228
|
}
|
|
128
229
|
/** Mirrors remaining room onto any direction buttons by toggling `disabled`. */
|
|
129
230
|
#syncButtons(start, end) {
|
|
130
|
-
const
|
|
131
|
-
"[data-stimeo--overflow-indicator
|
|
132
|
-
|
|
231
|
+
for (const button of this.#pendingButtonDisables.elements) {
|
|
232
|
+
if (!button.isConnected || button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
|
|
233
|
+
this.#cancelPendingButtonDisable(button);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const buttons = this.element.querySelectorAll(DIRECTION_BUTTON_SELECTOR);
|
|
133
237
|
for (const button of buttons) {
|
|
238
|
+
if (button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
134
241
|
const direction = button.getAttribute("data-stimeo--overflow-indicator-direction-param");
|
|
135
242
|
if (direction === "start") this.#toggleButton(button, start);
|
|
136
243
|
else if (direction === "end") this.#toggleButton(button, end);
|
|
@@ -143,7 +250,11 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
143
250
|
* whole control disabled) is therefore never blindly re-enabled.
|
|
144
251
|
*/
|
|
145
252
|
#toggleButton(button, hasRoom) {
|
|
253
|
+
if (!this.#pendingButtonDisables.has(button) && button.hasAttribute("data-overflow-indicator-pending-disabled")) {
|
|
254
|
+
this.#cancelPendingButtonDisable(button);
|
|
255
|
+
}
|
|
146
256
|
if (hasRoom) {
|
|
257
|
+
this.#cancelPendingButtonDisable(button);
|
|
147
258
|
if (button.hasAttribute("data-overflow-indicator-disabled")) {
|
|
148
259
|
button.disabled = false;
|
|
149
260
|
button.removeAttribute("data-overflow-indicator-disabled");
|
|
@@ -151,17 +262,125 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
151
262
|
return;
|
|
152
263
|
}
|
|
153
264
|
if (button.disabled) return;
|
|
265
|
+
if (document.activeElement === button) {
|
|
266
|
+
this.#deferButtonDisable(button);
|
|
267
|
+
} else {
|
|
268
|
+
this.#disableButton(button);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Keeps a boundary button focusable until native blur, exposing its temporary
|
|
273
|
+
* inoperability with `aria-disabled` and making its action a no-op meanwhile.
|
|
274
|
+
*/
|
|
275
|
+
#deferButtonDisable(button) {
|
|
276
|
+
if (this.#pendingButtonDisables.has(button)) return;
|
|
277
|
+
button.setAttribute("data-overflow-indicator-pending-disabled", "");
|
|
278
|
+
button.setAttribute(
|
|
279
|
+
"data-overflow-indicator-aria-disabled",
|
|
280
|
+
button.getAttribute("aria-disabled") ?? ""
|
|
281
|
+
);
|
|
282
|
+
button.setAttribute("aria-disabled", "true");
|
|
283
|
+
this.#pendingButtonDisables.defer(button);
|
|
284
|
+
}
|
|
285
|
+
#disableButton(button) {
|
|
154
286
|
button.disabled = true;
|
|
155
287
|
button.setAttribute("data-overflow-indicator-disabled", "");
|
|
156
288
|
}
|
|
289
|
+
/** Cancels a pending disable without applying it, restoring the displaced state. */
|
|
290
|
+
#cancelPendingButtonDisable(button) {
|
|
291
|
+
this.#pendingButtonDisables.release(button);
|
|
292
|
+
this.#restorePendingMarkers(button);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Drops the pending marker and gives back the `aria-disabled` it displaced.
|
|
296
|
+
*
|
|
297
|
+
* Reads the displaced value from the DOM rather than memory: a Turbo snapshot is
|
|
298
|
+
* cloned before `disconnect()` runs, so a restored page can arrive with markers
|
|
299
|
+
* this instance never wrote.
|
|
300
|
+
*/
|
|
301
|
+
#restorePendingMarkers(button) {
|
|
302
|
+
button.removeAttribute("data-overflow-indicator-pending-disabled");
|
|
303
|
+
const displaced = button.getAttribute("data-overflow-indicator-aria-disabled");
|
|
304
|
+
if (displaced !== null) {
|
|
305
|
+
button.removeAttribute("data-overflow-indicator-aria-disabled");
|
|
306
|
+
if (button.getAttribute("aria-disabled") === "true") {
|
|
307
|
+
if (displaced === "") button.removeAttribute("aria-disabled");
|
|
308
|
+
else button.setAttribute("aria-disabled", displaced);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
#clearPendingButtonDisables() {
|
|
313
|
+
for (const button of this.#pendingButtonDisables.elements) {
|
|
314
|
+
this.#cancelPendingButtonDisable(button);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Moves resize/mutation/load observation to the current viewport target. */
|
|
318
|
+
#syncViewport() {
|
|
319
|
+
if (!this.#connected) return;
|
|
320
|
+
const next = this.hasViewportTarget ? this.viewportTarget : null;
|
|
321
|
+
if (next === this.#observedViewport) return;
|
|
322
|
+
this.#stopObservingViewport();
|
|
323
|
+
if (!next) return;
|
|
324
|
+
this.#observedViewport = next;
|
|
325
|
+
this.#layout.observe(next);
|
|
326
|
+
this.#layout.observeViewport();
|
|
327
|
+
next.addEventListener("load", this.#onContentLoad, true);
|
|
328
|
+
this.#syncContentObservation();
|
|
329
|
+
if (typeof MutationObserver !== "undefined") {
|
|
330
|
+
this.#mutationObserver = new MutationObserver(() => {
|
|
331
|
+
if (!this.#connected || this.#observedViewport !== next) return;
|
|
332
|
+
this.#syncContentObservation();
|
|
333
|
+
this.update();
|
|
334
|
+
});
|
|
335
|
+
this.#mutationObserver.observe(this.element, {
|
|
336
|
+
childList: true,
|
|
337
|
+
subtree: true,
|
|
338
|
+
characterData: true,
|
|
339
|
+
attributes: true,
|
|
340
|
+
attributeFilter: ["class", "style", "hidden"]
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
this.#state = null;
|
|
344
|
+
this.update();
|
|
345
|
+
}
|
|
346
|
+
/** Observes direct content boxes whose resize can change the viewport's scroll extent. */
|
|
347
|
+
#syncContentObservation() {
|
|
348
|
+
const next = new Set(this.#observedViewport?.children ?? []);
|
|
349
|
+
for (const content of this.#observedContent) {
|
|
350
|
+
if (!next.has(content)) {
|
|
351
|
+
this.#layout.unobserve(content);
|
|
352
|
+
this.#observedContent.delete(content);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const content of next) {
|
|
356
|
+
if (!this.#observedContent.has(content)) {
|
|
357
|
+
this.#observedContent.add(content);
|
|
358
|
+
this.#layout.observe(content);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
#stopObservingViewport() {
|
|
363
|
+
this.#mutationObserver?.disconnect();
|
|
364
|
+
this.#mutationObserver = null;
|
|
365
|
+
this.#observedViewport?.removeEventListener("load", this.#onContentLoad, true);
|
|
366
|
+
if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);
|
|
367
|
+
for (const content of this.#observedContent) this.#layout.unobserve(content);
|
|
368
|
+
this.#observedContent.clear();
|
|
369
|
+
this.#observedViewport = null;
|
|
370
|
+
this.#layout.unobserveViewport();
|
|
371
|
+
}
|
|
372
|
+
#onContentLoad = () => {
|
|
373
|
+
if (this.#connected) this.update();
|
|
374
|
+
};
|
|
375
|
+
get #threshold() {
|
|
376
|
+
const value = this.thresholdValue;
|
|
377
|
+
return Number.isFinite(value) ? Math.max(0, value) : 1;
|
|
378
|
+
}
|
|
157
379
|
#directionFromEvent(event) {
|
|
158
380
|
const params = event.params;
|
|
159
381
|
const direction = params?.direction;
|
|
160
382
|
return direction === "start" || direction === "end" ? direction : null;
|
|
161
383
|
}
|
|
162
|
-
#prefersReducedMotion() {
|
|
163
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
164
|
-
}
|
|
165
384
|
};
|
|
166
385
|
|
|
167
386
|
export { OverflowIndicatorController };
|