stimeo-ui 0.2.0 → 0.2.1
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 +59 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/carousel_controller.js +272 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +213 -0
- 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 +168 -0
- data/dist/controllers/date_range_picker_controller.js +417 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +168 -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/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +41 -18
- data/dist/controllers/lazy_frame_controller.js +33 -11
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menubar_controller.js +433 -0
- data/dist/controllers/multi_select_controller.js +472 -0
- data/dist/controllers/navigation_menu_controller.js +384 -0
- data/dist/controllers/overflow_indicator_controller.js +178 -27
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/range_slider_controller.js +166 -0
- data/dist/controllers/read_more_controller.js +194 -0
- data/dist/controllers/scroll_area_controller.js +15 -2
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +8 -4
- data/dist/controllers/scrollspy_controller.js +33 -11
- data/dist/controllers/separator_controller.js +87 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +8 -4
- data/dist/controllers/sticky_observer_controller.js +88 -20
- data/dist/controllers/tags_input_controller.js +275 -0
- data/dist/controllers/theme_controller.js +20 -10
- data/dist/controllers/time_picker_controller.js +212 -0
- data/dist/controllers/toast_controller.js +36 -9
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +275 -0
- data/dist/index.js +811 -295
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +28 -2
data/dist/index.js
CHANGED
|
@@ -1814,28 +1814,176 @@ var ClipboardController = class extends Controller {
|
|
|
1814
1814
|
}
|
|
1815
1815
|
}
|
|
1816
1816
|
};
|
|
1817
|
+
|
|
1818
|
+
// src/utils/transition_completion.ts
|
|
1819
|
+
function timeMs(value) {
|
|
1820
|
+
const trimmed = value.trim();
|
|
1821
|
+
const amount = Number.parseFloat(trimmed);
|
|
1822
|
+
if (!Number.isFinite(amount)) return 0;
|
|
1823
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
1824
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
1825
|
+
return 0;
|
|
1826
|
+
}
|
|
1827
|
+
function cssList(value) {
|
|
1828
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1829
|
+
}
|
|
1830
|
+
function transitionTimings(style) {
|
|
1831
|
+
const properties = cssList(style.transitionProperty);
|
|
1832
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
1833
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
1834
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
1835
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
1836
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
1837
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
1838
|
+
property,
|
|
1839
|
+
totalMs: Math.max(
|
|
1840
|
+
0,
|
|
1841
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
1842
|
+
)
|
|
1843
|
+
}));
|
|
1844
|
+
}
|
|
1845
|
+
function maxTotalMs(timings) {
|
|
1846
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
1847
|
+
}
|
|
1848
|
+
function maxTransitionTotalMs(style) {
|
|
1849
|
+
return maxTotalMs(transitionTimings(style));
|
|
1850
|
+
}
|
|
1851
|
+
var TransitionCompletion = class {
|
|
1852
|
+
#timers = new SafeTimeout();
|
|
1853
|
+
#element = null;
|
|
1854
|
+
#complete = null;
|
|
1855
|
+
#pendingProperties = null;
|
|
1856
|
+
#deadline = 0;
|
|
1857
|
+
/**
|
|
1858
|
+
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
1859
|
+
* transition (including when `getComputedStyle` is unavailable).
|
|
1860
|
+
*
|
|
1861
|
+
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
1862
|
+
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
1863
|
+
*/
|
|
1864
|
+
wait(element, complete, options = {}) {
|
|
1865
|
+
this.cancel();
|
|
1866
|
+
const requested = options.timeoutMs ?? 0;
|
|
1867
|
+
const override = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
1868
|
+
const timings = typeof window.getComputedStyle === "function" ? transitionTimings(window.getComputedStyle(element)) : [];
|
|
1869
|
+
const maximum = maxTotalMs(timings);
|
|
1870
|
+
if (maximum <= 0 && override <= 0) {
|
|
1871
|
+
complete();
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this.#element = element;
|
|
1875
|
+
this.#complete = complete;
|
|
1876
|
+
this.#deadline = Date.now() + maximum;
|
|
1877
|
+
const activeProperties = this.#activeTransitionProperties(element);
|
|
1878
|
+
this.#pendingProperties = activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);
|
|
1879
|
+
element.addEventListener("transitionend", this.#onTerminal);
|
|
1880
|
+
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
1881
|
+
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
1882
|
+
}
|
|
1883
|
+
/** Cancels the pending wait without invoking its completion callback. */
|
|
1884
|
+
cancel() {
|
|
1885
|
+
this.#complete = null;
|
|
1886
|
+
this.#teardown();
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Handles terminal events from the observed element only.
|
|
1890
|
+
*
|
|
1891
|
+
* For explicit property lists, every declared positive-time property must
|
|
1892
|
+
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
1893
|
+
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
1894
|
+
*/
|
|
1895
|
+
#onTerminal = (event) => {
|
|
1896
|
+
if (event.target !== this.#element) return;
|
|
1897
|
+
const transitionEvent = event;
|
|
1898
|
+
if (transitionEvent.pseudoElement) return;
|
|
1899
|
+
if (this.#pendingProperties) {
|
|
1900
|
+
const propertyName = transitionEvent.propertyName;
|
|
1901
|
+
const active = this.#activeTransitionProperties(this.#element);
|
|
1902
|
+
if (active.has(propertyName)) return;
|
|
1903
|
+
if (!this.#pendingProperties.delete(propertyName)) return;
|
|
1904
|
+
if (this.#pendingProperties.size > 0) return;
|
|
1905
|
+
if (active.size > 0) {
|
|
1906
|
+
this.#pendingProperties = active;
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
this.#finish();
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (Date.now() >= this.#deadline) this.#finish();
|
|
1913
|
+
};
|
|
1914
|
+
/**
|
|
1915
|
+
* Returns active CSS transition properties expanded to the names reported by
|
|
1916
|
+
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
1917
|
+
*/
|
|
1918
|
+
#activeTransitionProperties(element) {
|
|
1919
|
+
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
1920
|
+
try {
|
|
1921
|
+
const properties = element.getAnimations().flatMap((animation) => {
|
|
1922
|
+
if (animation.playState === "idle" || animation.playState === "finished") return [];
|
|
1923
|
+
const effect = animation.effect;
|
|
1924
|
+
if (effect?.pseudoElement) return [];
|
|
1925
|
+
const target = effect?.target;
|
|
1926
|
+
if (target && target !== element) return [];
|
|
1927
|
+
const property = animation.transitionProperty;
|
|
1928
|
+
return typeof property === "string" && property.length > 0 ? [property] : [];
|
|
1929
|
+
});
|
|
1930
|
+
return new Set(properties);
|
|
1931
|
+
} catch {
|
|
1932
|
+
return /* @__PURE__ */ new Set();
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
1936
|
+
#explicitPendingProperties(timings) {
|
|
1937
|
+
if (timings.some(({ property }) => property === "all")) return null;
|
|
1938
|
+
const pending = new Set(
|
|
1939
|
+
timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property)
|
|
1940
|
+
);
|
|
1941
|
+
return pending.size > 0 ? pending : null;
|
|
1942
|
+
}
|
|
1943
|
+
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
1944
|
+
#finish() {
|
|
1945
|
+
const complete = this.#complete;
|
|
1946
|
+
if (!complete) return;
|
|
1947
|
+
this.#complete = null;
|
|
1948
|
+
this.#teardown();
|
|
1949
|
+
complete();
|
|
1950
|
+
}
|
|
1951
|
+
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
1952
|
+
#teardown() {
|
|
1953
|
+
this.#timers.clearAll();
|
|
1954
|
+
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
1955
|
+
this.#element?.removeEventListener("transitioncancel", this.#onTerminal);
|
|
1956
|
+
this.#element = null;
|
|
1957
|
+
this.#pendingProperties = null;
|
|
1958
|
+
this.#deadline = 0;
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
// src/controllers/collapsible_controller.ts
|
|
1817
1963
|
var CollapsibleController = class extends Controller {
|
|
1818
1964
|
static targets = ["trigger", "content"];
|
|
1819
1965
|
static values = {
|
|
1820
1966
|
open: { type: Boolean, default: false }
|
|
1821
1967
|
};
|
|
1822
1968
|
static actions = ["toggle"];
|
|
1969
|
+
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
1970
|
+
#transition = new TransitionCompletion();
|
|
1971
|
+
/** Distinguishes dynamic target churn from the callbacks that precede `connect()`. */
|
|
1972
|
+
#connected = false;
|
|
1823
1973
|
/**
|
|
1824
|
-
*
|
|
1825
|
-
* Tracked so {@link disconnect} can detach it and a reopen can supersede it.
|
|
1826
|
-
*/
|
|
1827
|
-
#pendingTransitionEnd = null;
|
|
1828
|
-
/**
|
|
1829
|
-
* Establishes the initial open/closed state without animating.
|
|
1974
|
+
* Establishes the initial open/closed state without waiting for a close transition.
|
|
1830
1975
|
*
|
|
1831
1976
|
* The DOM is the source of truth on reconnect (Turbo cache restore / morph): an
|
|
1832
1977
|
* **explicit** state attribute — `aria-expanded="true"`/`"false"` (or, with no
|
|
1833
1978
|
* trigger, `data-state="open"`/`"closed"`) — is honored verbatim so a region the
|
|
1834
1979
|
* user opened *or* closed survives a back-navigation, even when the declarative
|
|
1835
|
-
* `open` Value disagrees.
|
|
1836
|
-
*
|
|
1980
|
+
* `open` Value disagrees. An already-open region remains open without a
|
|
1981
|
+
* close/reopen cycle. The Value only seeds a genuinely fresh render where no
|
|
1982
|
+
* state attribute is present yet; any opening animation in that case belongs to
|
|
1983
|
+
* the consumer's CSS. Mirrors `sidebar`'s `#restoreCollapsed`.
|
|
1837
1984
|
*/
|
|
1838
1985
|
connect() {
|
|
1986
|
+
this.#connected = true;
|
|
1839
1987
|
this.#apply(this.#initialOpen(), false);
|
|
1840
1988
|
}
|
|
1841
1989
|
/** Resolves the connect-time state: explicit DOM state wins, else the `open` Value. */
|
|
@@ -1852,7 +2000,27 @@ var CollapsibleController = class extends Controller {
|
|
|
1852
2000
|
return this.openValue;
|
|
1853
2001
|
}
|
|
1854
2002
|
disconnect() {
|
|
1855
|
-
this.#
|
|
2003
|
+
this.#connected = false;
|
|
2004
|
+
this.#transition.cancel();
|
|
2005
|
+
}
|
|
2006
|
+
/** Reconciles a replacement trigger target with the content's live state. */
|
|
2007
|
+
triggerTargetConnected(trigger) {
|
|
2008
|
+
if (!this.#connected || !this.hasContentTarget) return;
|
|
2009
|
+
trigger.setAttribute(
|
|
2010
|
+
"aria-expanded",
|
|
2011
|
+
this.contentTarget.getAttribute("data-state") === "open" ? "true" : "false"
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
/** Reconciles a replacement content target with the disclosure's live state. */
|
|
2015
|
+
contentTargetConnected(content) {
|
|
2016
|
+
if (!this.#connected) return;
|
|
2017
|
+
this.#transition.cancel();
|
|
2018
|
+
const open = this.hasTriggerTarget ? this.triggerTarget.getAttribute("aria-expanded") === "true" : content.getAttribute("data-state") === "open";
|
|
2019
|
+
this.#applyContent(content, open, false);
|
|
2020
|
+
}
|
|
2021
|
+
/** Cancels a wait tied to a content target that was removed or replaced. */
|
|
2022
|
+
contentTargetDisconnected() {
|
|
2023
|
+
this.#transition.cancel();
|
|
1856
2024
|
}
|
|
1857
2025
|
/** Toggles the region open/closed. Bound via `data-action` (click). */
|
|
1858
2026
|
toggle() {
|
|
@@ -1875,16 +2043,21 @@ var CollapsibleController = class extends Controller {
|
|
|
1875
2043
|
* spec requires.
|
|
1876
2044
|
*
|
|
1877
2045
|
* @param open - Target state.
|
|
1878
|
-
* @param
|
|
1879
|
-
* `hidden` immediately
|
|
2046
|
+
* @param waitForCloseTransition - When `false` (initial `connect`) the close
|
|
2047
|
+
* path applies `hidden` immediately. This flag does not suppress consumer CSS
|
|
2048
|
+
* on the open path.
|
|
1880
2049
|
*/
|
|
1881
|
-
#apply(open,
|
|
2050
|
+
#apply(open, waitForCloseTransition) {
|
|
1882
2051
|
if (this.hasTriggerTarget) {
|
|
1883
2052
|
this.triggerTarget.setAttribute("aria-expanded", open ? "true" : "false");
|
|
1884
2053
|
}
|
|
1885
2054
|
if (!this.hasContentTarget) return;
|
|
1886
2055
|
const content = this.contentTarget;
|
|
1887
|
-
this.#
|
|
2056
|
+
this.#transition.cancel();
|
|
2057
|
+
this.#applyContent(content, open, waitForCloseTransition);
|
|
2058
|
+
}
|
|
2059
|
+
/** Reflects one content target without relying on a later target lookup. */
|
|
2060
|
+
#applyContent(content, open, waitForCloseTransition) {
|
|
1888
2061
|
if (open) {
|
|
1889
2062
|
content.hidden = false;
|
|
1890
2063
|
content.style.setProperty("--stimeo-collapsible-content-height", `${content.scrollHeight}px`);
|
|
@@ -1892,44 +2065,23 @@ var CollapsibleController = class extends Controller {
|
|
|
1892
2065
|
return;
|
|
1893
2066
|
}
|
|
1894
2067
|
content.setAttribute("data-state", "closed");
|
|
1895
|
-
if (
|
|
2068
|
+
if (waitForCloseTransition) {
|
|
1896
2069
|
this.#applyHiddenAfterTransition(content);
|
|
1897
2070
|
} else {
|
|
1898
2071
|
content.hidden = true;
|
|
1899
2072
|
}
|
|
1900
2073
|
}
|
|
1901
2074
|
/**
|
|
1902
|
-
* Re-applies `hidden` once the close transition
|
|
1903
|
-
* reopen mid-transition
|
|
1904
|
-
*
|
|
2075
|
+
* Re-applies `hidden` once the close transition settles. Guarded against a
|
|
2076
|
+
* reopen mid-transition; the shared waiter also guarantees a bounded fallback
|
|
2077
|
+
* when the browser emits no terminal transition event.
|
|
1905
2078
|
*/
|
|
1906
2079
|
#applyHiddenAfterTransition(content) {
|
|
1907
|
-
|
|
1908
|
-
if (event.target !== content) return;
|
|
1909
|
-
this.#detachTransitionEnd();
|
|
2080
|
+
this.#transition.wait(content, () => {
|
|
1910
2081
|
if (content.getAttribute("data-state") === "closed") {
|
|
1911
2082
|
content.hidden = true;
|
|
1912
2083
|
}
|
|
1913
|
-
};
|
|
1914
|
-
this.#pendingTransitionEnd = handler;
|
|
1915
|
-
content.addEventListener("transitionend", handler);
|
|
1916
|
-
}
|
|
1917
|
-
#detachTransitionEnd() {
|
|
1918
|
-
if (this.#pendingTransitionEnd && this.hasContentTarget) {
|
|
1919
|
-
this.contentTarget.removeEventListener("transitionend", this.#pendingTransitionEnd);
|
|
1920
|
-
}
|
|
1921
|
-
this.#pendingTransitionEnd = null;
|
|
1922
|
-
}
|
|
1923
|
-
/**
|
|
1924
|
-
* First `transition-duration` of `element` in milliseconds. Browsers normalize
|
|
1925
|
-
* computed `<time>` to seconds (`0.2s`), but `ms` is parsed defensively. A zero
|
|
1926
|
-
* here — including the consumer's reduced-motion CSS — takes the immediate path.
|
|
1927
|
-
*/
|
|
1928
|
-
#transitionMs(element) {
|
|
1929
|
-
const first = window.getComputedStyle(element).transitionDuration.split(",")[0]?.trim() ?? "";
|
|
1930
|
-
const amount = Number.parseFloat(first);
|
|
1931
|
-
if (Number.isNaN(amount)) return 0;
|
|
1932
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
2084
|
+
});
|
|
1933
2085
|
}
|
|
1934
2086
|
};
|
|
1935
2087
|
var COLOR_PROPERTY = "--stimeo-color";
|
|
@@ -2988,6 +3140,13 @@ var ContextMenuController = class extends Controller {
|
|
|
2988
3140
|
return this.hasMenuTarget && !this.menuTarget.hidden;
|
|
2989
3141
|
}
|
|
2990
3142
|
};
|
|
3143
|
+
|
|
3144
|
+
// src/utils/reduced_motion.ts
|
|
3145
|
+
function prefersReducedMotion() {
|
|
3146
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/controllers/count_up_controller.ts
|
|
2991
3150
|
var CountUpController = class extends Controller {
|
|
2992
3151
|
static values = {
|
|
2993
3152
|
duration: { type: Number, default: 1200 },
|
|
@@ -3019,7 +3178,7 @@ var CountUpController = class extends Controller {
|
|
|
3019
3178
|
this.#finalText = this.element.textContent ?? "";
|
|
3020
3179
|
const target = Number.parseInt(this.#finalText.replace(/[^0-9-]/g, ""), 10);
|
|
3021
3180
|
if (Number.isNaN(target)) return;
|
|
3022
|
-
if (
|
|
3181
|
+
if (prefersReducedMotion()) {
|
|
3023
3182
|
this.element.setAttribute("data-count-up-done", "true");
|
|
3024
3183
|
this.dispatch("end", { detail: { value: target } });
|
|
3025
3184
|
return;
|
|
@@ -4255,15 +4414,6 @@ var DismissibleController = class extends Controller {
|
|
|
4255
4414
|
return true;
|
|
4256
4415
|
}
|
|
4257
4416
|
};
|
|
4258
|
-
function maxTransitionMs(value) {
|
|
4259
|
-
const durations = value.split(",").map((part) => {
|
|
4260
|
-
const trimmed = part.trim();
|
|
4261
|
-
if (trimmed.endsWith("ms")) return Number.parseFloat(trimmed);
|
|
4262
|
-
if (trimmed.endsWith("s")) return Number.parseFloat(trimmed) * 1e3;
|
|
4263
|
-
return 0;
|
|
4264
|
-
});
|
|
4265
|
-
return durations.length === 0 ? 0 : Math.max(...durations);
|
|
4266
|
-
}
|
|
4267
4417
|
var DrawerController = class extends Controller {
|
|
4268
4418
|
static targets = ["trigger", "overlay", "panel"];
|
|
4269
4419
|
static values = {
|
|
@@ -4271,13 +4421,17 @@ var DrawerController = class extends Controller {
|
|
|
4271
4421
|
open: { type: Boolean, default: false }
|
|
4272
4422
|
};
|
|
4273
4423
|
static actions = ["close", "closeOnBackdrop", "open"];
|
|
4424
|
+
/** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
|
|
4425
|
+
#activePanel = null;
|
|
4274
4426
|
/** Owns the modal side effects; Escape closes, focus falls back to the trigger. */
|
|
4275
|
-
#trap = new FocusTrap(() => this.panelTarget, {
|
|
4427
|
+
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
4276
4428
|
onEscape: () => this.close(),
|
|
4277
4429
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
4278
4430
|
});
|
|
4279
|
-
/**
|
|
4280
|
-
#
|
|
4431
|
+
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
4432
|
+
#transition = new TransitionCompletion();
|
|
4433
|
+
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
4434
|
+
#connected = false;
|
|
4281
4435
|
/**
|
|
4282
4436
|
* Reflects placement and establishes the initial open/closed state.
|
|
4283
4437
|
*
|
|
@@ -4290,6 +4444,8 @@ var DrawerController = class extends Controller {
|
|
|
4290
4444
|
* after a reconnect and must be re-activated.
|
|
4291
4445
|
*/
|
|
4292
4446
|
connect() {
|
|
4447
|
+
this.#connected = true;
|
|
4448
|
+
this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
|
|
4293
4449
|
this.#reflectPlacement();
|
|
4294
4450
|
const shouldOpen = this.#isOpen || this.openValue;
|
|
4295
4451
|
this.#applyClosedState();
|
|
@@ -4297,8 +4453,31 @@ var DrawerController = class extends Controller {
|
|
|
4297
4453
|
}
|
|
4298
4454
|
/** Reverts the modal side effects and pending hide if torn down while open. */
|
|
4299
4455
|
disconnect() {
|
|
4300
|
-
this.#
|
|
4456
|
+
this.#connected = false;
|
|
4457
|
+
this.#transition.cancel();
|
|
4301
4458
|
this.#trap.deactivate({ restoreFocus: false });
|
|
4459
|
+
this.#activePanel = null;
|
|
4460
|
+
}
|
|
4461
|
+
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
4462
|
+
panelTargetConnected(panel) {
|
|
4463
|
+
if (!this.#connected || this.#activePanel?.isConnected && this.#activePanel !== panel) return;
|
|
4464
|
+
this.#adoptPanel(panel);
|
|
4465
|
+
}
|
|
4466
|
+
/** Closes and releases modal side effects when the actively trapped panel disappears. */
|
|
4467
|
+
panelTargetDisconnected(panel) {
|
|
4468
|
+
if (panel !== this.#activePanel) return;
|
|
4469
|
+
this.#transition.cancel();
|
|
4470
|
+
this.#activePanel = null;
|
|
4471
|
+
if (!this.#connected) return;
|
|
4472
|
+
panel.setAttribute("data-state", "closed");
|
|
4473
|
+
panel.hidden = true;
|
|
4474
|
+
if (this.hasOverlayTarget) {
|
|
4475
|
+
this.overlayTarget.setAttribute("data-state", "closed");
|
|
4476
|
+
this.overlayTarget.hidden = true;
|
|
4477
|
+
}
|
|
4478
|
+
this.openValue = false;
|
|
4479
|
+
this.#trap.deactivate();
|
|
4480
|
+
if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
|
|
4302
4481
|
}
|
|
4303
4482
|
/** Keeps `data-placement` in sync if the value changes at runtime. */
|
|
4304
4483
|
placementValueChanged() {
|
|
@@ -4307,7 +4486,8 @@ var DrawerController = class extends Controller {
|
|
|
4307
4486
|
/** Opens the drawer: reveals it, syncs `data-state`, traps focus. */
|
|
4308
4487
|
open() {
|
|
4309
4488
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
4310
|
-
this.#
|
|
4489
|
+
this.#transition.cancel();
|
|
4490
|
+
this.#activePanel = this.panelTarget;
|
|
4311
4491
|
this.panelTarget.hidden = false;
|
|
4312
4492
|
if (this.hasOverlayTarget) this.overlayTarget.hidden = false;
|
|
4313
4493
|
void this.panelTarget.offsetWidth;
|
|
@@ -4337,6 +4517,32 @@ var DrawerController = class extends Controller {
|
|
|
4337
4517
|
#reflectPlacement() {
|
|
4338
4518
|
if (this.hasPanelTarget) this.panelTarget.setAttribute("data-placement", this.#placement);
|
|
4339
4519
|
}
|
|
4520
|
+
/** Reconciles a replacement panel and companion overlay from its explicit DOM state. */
|
|
4521
|
+
#adoptPanel(panel) {
|
|
4522
|
+
this.#transition.cancel();
|
|
4523
|
+
const trapWasActive = this.#trap.active;
|
|
4524
|
+
this.#activePanel = panel;
|
|
4525
|
+
panel.setAttribute("data-placement", this.#placement);
|
|
4526
|
+
if (panel.getAttribute("data-state") === "open") {
|
|
4527
|
+
panel.hidden = false;
|
|
4528
|
+
if (this.hasOverlayTarget) {
|
|
4529
|
+
this.overlayTarget.setAttribute("data-state", "open");
|
|
4530
|
+
this.overlayTarget.hidden = false;
|
|
4531
|
+
}
|
|
4532
|
+
this.openValue = true;
|
|
4533
|
+
if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });
|
|
4534
|
+
this.#trap.activate();
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
panel.setAttribute("data-state", "closed");
|
|
4538
|
+
panel.hidden = true;
|
|
4539
|
+
if (this.hasOverlayTarget) {
|
|
4540
|
+
this.overlayTarget.setAttribute("data-state", "closed");
|
|
4541
|
+
this.overlayTarget.hidden = true;
|
|
4542
|
+
}
|
|
4543
|
+
this.openValue = false;
|
|
4544
|
+
this.#trap.deactivate();
|
|
4545
|
+
}
|
|
4340
4546
|
/** Validated placement (`left`/`right`/`top`/`bottom`), defaulting to `right`. */
|
|
4341
4547
|
get #placement() {
|
|
4342
4548
|
const value = this.placementValue;
|
|
@@ -4355,24 +4561,13 @@ var DrawerController = class extends Controller {
|
|
|
4355
4561
|
}
|
|
4356
4562
|
/**
|
|
4357
4563
|
* Applies `hidden` once the panel's close transition ends, so the exit slide
|
|
4358
|
-
* can play.
|
|
4359
|
-
*
|
|
4360
|
-
* that would never fire.
|
|
4564
|
+
* can play. The shared waiter hides synchronously for 0ms transitions and
|
|
4565
|
+
* supplies a bounded fallback when the browser emits no terminal event.
|
|
4361
4566
|
*/
|
|
4362
4567
|
#hideAfterTransition() {
|
|
4363
4568
|
const panel = this.panelTarget;
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
this.#applyHidden();
|
|
4367
|
-
return;
|
|
4368
|
-
}
|
|
4369
|
-
const onEnd = (event) => {
|
|
4370
|
-
if (event.target !== panel) return;
|
|
4371
|
-
this.#cancelPendingHide();
|
|
4372
|
-
this.#applyHidden();
|
|
4373
|
-
};
|
|
4374
|
-
this.#pendingHide = () => panel.removeEventListener("transitionend", onEnd);
|
|
4375
|
-
panel.addEventListener("transitionend", onEnd);
|
|
4569
|
+
this.#activePanel = panel;
|
|
4570
|
+
this.#transition.wait(panel, () => this.#applyHidden(panel));
|
|
4376
4571
|
}
|
|
4377
4572
|
/**
|
|
4378
4573
|
* Runs once the close transition has finished: applies `hidden` to the panel
|
|
@@ -4381,16 +4576,11 @@ var DrawerController = class extends Controller {
|
|
|
4381
4576
|
* {@link FocusTrap} teardown to here — rather than at {@link close} time — keeps
|
|
4382
4577
|
* the background unreachable and focus trapped for the whole exit animation.
|
|
4383
4578
|
*/
|
|
4384
|
-
#applyHidden() {
|
|
4385
|
-
|
|
4579
|
+
#applyHidden(panel) {
|
|
4580
|
+
panel.hidden = true;
|
|
4386
4581
|
if (this.hasOverlayTarget) this.overlayTarget.hidden = true;
|
|
4387
4582
|
this.#trap.deactivate();
|
|
4388
4583
|
}
|
|
4389
|
-
/** Removes any pending `transitionend` hide listener. */
|
|
4390
|
-
#cancelPendingHide() {
|
|
4391
|
-
this.#pendingHide?.();
|
|
4392
|
-
this.#pendingHide = null;
|
|
4393
|
-
}
|
|
4394
4584
|
/** Whether the drawer is open (tracked via `data-state`, not `hidden`). */
|
|
4395
4585
|
get #isOpen() {
|
|
4396
4586
|
return this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
|
|
@@ -5047,13 +5237,10 @@ var FlashController = class extends Controller {
|
|
|
5047
5237
|
finalize();
|
|
5048
5238
|
}
|
|
5049
5239
|
}
|
|
5050
|
-
/**
|
|
5240
|
+
/** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */
|
|
5051
5241
|
#transitionMs(el) {
|
|
5052
5242
|
if (typeof window.getComputedStyle !== "function") return 0;
|
|
5053
|
-
|
|
5054
|
-
const amount = Number.parseFloat(first);
|
|
5055
|
-
if (Number.isNaN(amount)) return 0;
|
|
5056
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
5243
|
+
return maxTransitionTotalMs(window.getComputedStyle(el));
|
|
5057
5244
|
}
|
|
5058
5245
|
};
|
|
5059
5246
|
var FocusController = class extends Controller {
|
|
@@ -5634,7 +5821,7 @@ var HighlightController = class extends Controller {
|
|
|
5634
5821
|
}
|
|
5635
5822
|
/** Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion). */
|
|
5636
5823
|
#highlight(el) {
|
|
5637
|
-
if (
|
|
5824
|
+
if (prefersReducedMotion()) return;
|
|
5638
5825
|
el.setAttribute("data-highlight", "true");
|
|
5639
5826
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
5640
5827
|
this.#timeouts.set(() => {
|
|
@@ -5642,9 +5829,6 @@ var HighlightController = class extends Controller {
|
|
|
5642
5829
|
this.dispatch("end", { target: el, detail: { element: el } });
|
|
5643
5830
|
}, this.durationValue);
|
|
5644
5831
|
}
|
|
5645
|
-
#prefersReducedMotion() {
|
|
5646
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
5647
|
-
}
|
|
5648
5832
|
};
|
|
5649
5833
|
|
|
5650
5834
|
// src/utils/scroll_dismiss.ts
|
|
@@ -6018,6 +6202,12 @@ var InputMaskController = class extends Controller {
|
|
|
6018
6202
|
};
|
|
6019
6203
|
|
|
6020
6204
|
// src/utils/intersection_watcher.ts
|
|
6205
|
+
function isBeforeRootStart(entry) {
|
|
6206
|
+
const rect = entry.boundingClientRect;
|
|
6207
|
+
if (rect.width === 0 && rect.height === 0) return false;
|
|
6208
|
+
const rootTop = entry.rootBounds?.top ?? 0;
|
|
6209
|
+
return rect.bottom <= rootTop;
|
|
6210
|
+
}
|
|
6021
6211
|
var IntersectionWatcher = class {
|
|
6022
6212
|
#onEntries;
|
|
6023
6213
|
#observer = null;
|
|
@@ -6033,6 +6223,11 @@ var IntersectionWatcher = class {
|
|
|
6033
6223
|
* (Re)creates the observer and observes `targets`. Returns `false` — leaving
|
|
6034
6224
|
* the watcher inert — without `IntersectionObserver` support (very old
|
|
6035
6225
|
* browsers; the caller's no-JS fallback stays in charge) or with no targets.
|
|
6226
|
+
*
|
|
6227
|
+
* @throws Whatever the platform throws for an invalid `rootMargin`/`threshold`
|
|
6228
|
+
* or a failing `observe()`. The exception is passed through unchanged, but
|
|
6229
|
+
* the watcher rolls back first: every target observed so far is released and
|
|
6230
|
+
* `active` stays `false`, so a caller that retries starts from a clean slate.
|
|
6036
6231
|
*/
|
|
6037
6232
|
start(targets, options = {}) {
|
|
6038
6233
|
this.stop();
|
|
@@ -6040,25 +6235,42 @@ var IntersectionWatcher = class {
|
|
|
6040
6235
|
const list = Array.isArray(targets) ? targets : [targets];
|
|
6041
6236
|
if (list.length === 0) return false;
|
|
6042
6237
|
const root = "root" in options ? options.root ?? null : options.rootSelector ? document.querySelector(options.rootSelector) : null;
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6238
|
+
let observer = null;
|
|
6239
|
+
try {
|
|
6240
|
+
observer = new IntersectionObserver(
|
|
6241
|
+
(entries) => {
|
|
6242
|
+
if (this.#active && this.#observer === observer) this.#onEntries(entries);
|
|
6243
|
+
},
|
|
6244
|
+
{ root, rootMargin: options.rootMargin, threshold: options.threshold }
|
|
6245
|
+
);
|
|
6246
|
+
for (const target of list) observer.observe(target);
|
|
6247
|
+
this.#observer = observer;
|
|
6248
|
+
this.#active = true;
|
|
6249
|
+
return true;
|
|
6250
|
+
} catch (error) {
|
|
6251
|
+
observer?.disconnect();
|
|
6252
|
+
this.#observer = null;
|
|
6253
|
+
this.#active = false;
|
|
6254
|
+
throw error;
|
|
6255
|
+
}
|
|
6052
6256
|
}
|
|
6053
6257
|
/**
|
|
6054
6258
|
* Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
|
|
6055
6259
|
* only reports *changes*, but `observe()` always reports the present state,
|
|
6056
6260
|
* so unobserve→observe turns "still intersecting" into a fresh callback.
|
|
6261
|
+
*
|
|
6262
|
+
* @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
|
|
6263
|
+
* first, so it never stays live with a half-rearmed target.
|
|
6057
6264
|
*/
|
|
6058
6265
|
rearm(target) {
|
|
6059
6266
|
if (!this.#observer) return;
|
|
6060
|
-
|
|
6061
|
-
|
|
6267
|
+
try {
|
|
6268
|
+
this.#observer.unobserve(target);
|
|
6269
|
+
this.#observer.observe(target);
|
|
6270
|
+
} catch (error) {
|
|
6271
|
+
this.stop();
|
|
6272
|
+
throw error;
|
|
6273
|
+
}
|
|
6062
6274
|
}
|
|
6063
6275
|
/** Severs the observer; late queued callbacks become no-ops via the guard. */
|
|
6064
6276
|
stop() {
|
|
@@ -6095,7 +6307,7 @@ var IntersectionController = class extends Controller {
|
|
|
6095
6307
|
this.element.style.setProperty(RATIO_PROPERTY, String(ratio));
|
|
6096
6308
|
this.dispatch("change", { detail: { intersecting, ratio } });
|
|
6097
6309
|
this.#syncIntersecting(intersecting, ratio, entry);
|
|
6098
|
-
this.#syncPassed(!intersecting &&
|
|
6310
|
+
this.#syncPassed(!intersecting && isBeforeRootStart(entry));
|
|
6099
6311
|
}
|
|
6100
6312
|
}
|
|
6101
6313
|
connect() {
|
|
@@ -6143,7 +6355,7 @@ var IntersectionController = class extends Controller {
|
|
|
6143
6355
|
if (this.onceValue) this.#watcher.stop();
|
|
6144
6356
|
} else if (!intersecting && previous === "true") {
|
|
6145
6357
|
this.dispatch("exit", {
|
|
6146
|
-
detail: { ratio, position:
|
|
6358
|
+
detail: { ratio, position: isBeforeRootStart(entry) ? "before" : "after" }
|
|
6147
6359
|
});
|
|
6148
6360
|
}
|
|
6149
6361
|
}
|
|
@@ -6159,11 +6371,6 @@ var IntersectionController = class extends Controller {
|
|
|
6159
6371
|
const changed = previous === null ? passed : previous === "true" !== passed;
|
|
6160
6372
|
if (changed) this.dispatch("passed", { detail: { passed } });
|
|
6161
6373
|
}
|
|
6162
|
-
/** True when the element sits entirely before the root's start (top) edge. */
|
|
6163
|
-
#isBefore(entry) {
|
|
6164
|
-
const rootTop = entry.rootBounds?.top ?? 0;
|
|
6165
|
-
return entry.boundingClientRect.bottom <= rootTop;
|
|
6166
|
-
}
|
|
6167
6374
|
/** The configured `threshold`, clamped to the 0..1 the observer accepts. */
|
|
6168
6375
|
#clampedThreshold() {
|
|
6169
6376
|
return Math.min(1, Math.max(0, this.thresholdValue));
|
|
@@ -8317,6 +8524,26 @@ var OtpController = class extends Controller {
|
|
|
8317
8524
|
}
|
|
8318
8525
|
}
|
|
8319
8526
|
};
|
|
8527
|
+
|
|
8528
|
+
// src/utils/logical_scroll.ts
|
|
8529
|
+
function isRtl(element) {
|
|
8530
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8531
|
+
}
|
|
8532
|
+
function logicalScrollMetrics(element, horizontal) {
|
|
8533
|
+
const max = Math.max(
|
|
8534
|
+
0,
|
|
8535
|
+
horizontal ? element.scrollWidth - element.clientWidth : element.scrollHeight - element.clientHeight
|
|
8536
|
+
);
|
|
8537
|
+
const raw = horizontal ? element.scrollLeft : element.scrollTop;
|
|
8538
|
+
const position = horizontal && isRtl(element) ? -raw : raw;
|
|
8539
|
+
return { position: Math.min(max, Math.max(0, position)), max };
|
|
8540
|
+
}
|
|
8541
|
+
function physicalScrollDelta(element, horizontal, logicalDelta) {
|
|
8542
|
+
return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;
|
|
8543
|
+
}
|
|
8544
|
+
|
|
8545
|
+
// src/controllers/overflow_indicator_controller.ts
|
|
8546
|
+
var DIRECTION_BUTTON_SELECTOR = "[data-stimeo--overflow-indicator-direction-param]";
|
|
8320
8547
|
var OverflowIndicatorController = class extends Controller {
|
|
8321
8548
|
static targets = ["viewport"];
|
|
8322
8549
|
static values = {
|
|
@@ -8325,38 +8552,50 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
8325
8552
|
};
|
|
8326
8553
|
static actions = ["scrollByPage", "update"];
|
|
8327
8554
|
static events = ["change"];
|
|
8328
|
-
#layout = new LayoutObserver(() =>
|
|
8555
|
+
#layout = new LayoutObserver(() => {
|
|
8556
|
+
if (this.#connected) this.update();
|
|
8557
|
+
});
|
|
8558
|
+
#connected = false;
|
|
8559
|
+
#observedViewport = null;
|
|
8560
|
+
#observedContent = /* @__PURE__ */ new Set();
|
|
8329
8561
|
#mutationObserver = null;
|
|
8562
|
+
#pendingButtonDisables = /* @__PURE__ */ new Map();
|
|
8330
8563
|
/** Last reported room, so `change` fires only on transitions. */
|
|
8331
8564
|
#state = null;
|
|
8332
8565
|
connect() {
|
|
8333
|
-
|
|
8334
|
-
this.#
|
|
8335
|
-
this.#layout.observeViewport();
|
|
8336
|
-
if (typeof MutationObserver !== "undefined") {
|
|
8337
|
-
this.#mutationObserver = new MutationObserver(() => this.update());
|
|
8338
|
-
this.#mutationObserver.observe(this.viewportTarget, {
|
|
8339
|
-
childList: true,
|
|
8340
|
-
subtree: true,
|
|
8341
|
-
characterData: true
|
|
8342
|
-
});
|
|
8343
|
-
}
|
|
8344
|
-
this.update();
|
|
8566
|
+
this.#connected = true;
|
|
8567
|
+
this.#syncViewport();
|
|
8345
8568
|
}
|
|
8346
8569
|
disconnect() {
|
|
8570
|
+
this.#connected = false;
|
|
8571
|
+
this.#stopObservingViewport();
|
|
8347
8572
|
this.#layout.disconnect();
|
|
8348
|
-
this.#
|
|
8349
|
-
this.#mutationObserver = null;
|
|
8573
|
+
this.#clearPendingButtonDisables();
|
|
8350
8574
|
this.#state = null;
|
|
8351
8575
|
}
|
|
8352
|
-
|
|
8576
|
+
viewportTargetConnected() {
|
|
8577
|
+
this.#syncViewport();
|
|
8578
|
+
}
|
|
8579
|
+
viewportTargetDisconnected(viewport) {
|
|
8580
|
+
if (this.#observedViewport === viewport) this.#stopObservingViewport();
|
|
8581
|
+
this.#syncViewport();
|
|
8582
|
+
}
|
|
8583
|
+
orientationValueChanged() {
|
|
8584
|
+
if (this.#connected) this.update();
|
|
8585
|
+
}
|
|
8586
|
+
thresholdValueChanged() {
|
|
8587
|
+
if (this.#connected) this.update();
|
|
8588
|
+
}
|
|
8589
|
+
/**
|
|
8590
|
+
* Re-measures remaining scroll room and reflects the state hooks.
|
|
8591
|
+
* Public so it can be wired to the viewport's `scroll`.
|
|
8592
|
+
*/
|
|
8353
8593
|
update() {
|
|
8354
8594
|
if (!this.hasViewportTarget) return;
|
|
8355
8595
|
const vp = this.viewportTarget;
|
|
8356
8596
|
const horizontal = this.orientationValue !== "vertical";
|
|
8357
|
-
const t = this
|
|
8358
|
-
const scrollPos
|
|
8359
|
-
const maxScroll = horizontal ? vp.scrollWidth - vp.clientWidth : vp.scrollHeight - vp.clientHeight;
|
|
8597
|
+
const t = this.#threshold;
|
|
8598
|
+
const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontal);
|
|
8360
8599
|
const start = scrollPos > t;
|
|
8361
8600
|
const end = scrollPos < maxScroll - t;
|
|
8362
8601
|
vp.setAttribute("data-overflow-start", start ? "true" : "false");
|
|
@@ -8372,11 +8611,13 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
8372
8611
|
if (!this.hasViewportTarget) return;
|
|
8373
8612
|
const direction = this.#directionFromEvent(event);
|
|
8374
8613
|
if (!direction) return;
|
|
8614
|
+
if (this.#state && !this.#state[direction]) return;
|
|
8375
8615
|
const vp = this.viewportTarget;
|
|
8376
8616
|
const horizontal = this.orientationValue !== "vertical";
|
|
8377
8617
|
const page = horizontal ? vp.clientWidth : vp.clientHeight;
|
|
8378
|
-
const
|
|
8379
|
-
const
|
|
8618
|
+
const logicalDelta = direction === "start" ? -page : page;
|
|
8619
|
+
const delta = physicalScrollDelta(vp, horizontal, logicalDelta);
|
|
8620
|
+
const behavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
8380
8621
|
if (horizontal) {
|
|
8381
8622
|
vp.scrollBy({ left: delta, behavior });
|
|
8382
8623
|
} else {
|
|
@@ -8385,10 +8626,16 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
8385
8626
|
}
|
|
8386
8627
|
/** Mirrors remaining room onto any direction buttons by toggling `disabled`. */
|
|
8387
8628
|
#syncButtons(start, end) {
|
|
8388
|
-
const
|
|
8389
|
-
"[data-stimeo--overflow-indicator
|
|
8390
|
-
|
|
8629
|
+
for (const button of [...this.#pendingButtonDisables.keys()]) {
|
|
8630
|
+
if (!button.isConnected || button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
|
|
8631
|
+
this.#cancelPendingButtonDisable(button);
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8634
|
+
const buttons = this.element.querySelectorAll(DIRECTION_BUTTON_SELECTOR);
|
|
8391
8635
|
for (const button of buttons) {
|
|
8636
|
+
if (button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
|
|
8637
|
+
continue;
|
|
8638
|
+
}
|
|
8392
8639
|
const direction = button.getAttribute("data-stimeo--overflow-indicator-direction-param");
|
|
8393
8640
|
if (direction === "start") this.#toggleButton(button, start);
|
|
8394
8641
|
else if (direction === "end") this.#toggleButton(button, end);
|
|
@@ -8401,7 +8648,11 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
8401
8648
|
* whole control disabled) is therefore never blindly re-enabled.
|
|
8402
8649
|
*/
|
|
8403
8650
|
#toggleButton(button, hasRoom) {
|
|
8651
|
+
if (!this.#pendingButtonDisables.has(button) && button.hasAttribute("data-overflow-indicator-pending-disabled")) {
|
|
8652
|
+
this.#cancelPendingButtonDisable(button);
|
|
8653
|
+
}
|
|
8404
8654
|
if (hasRoom) {
|
|
8655
|
+
this.#cancelPendingButtonDisable(button);
|
|
8405
8656
|
if (button.hasAttribute("data-overflow-indicator-disabled")) {
|
|
8406
8657
|
button.disabled = false;
|
|
8407
8658
|
button.removeAttribute("data-overflow-indicator-disabled");
|
|
@@ -8409,17 +8660,121 @@ var OverflowIndicatorController = class extends Controller {
|
|
|
8409
8660
|
return;
|
|
8410
8661
|
}
|
|
8411
8662
|
if (button.disabled) return;
|
|
8663
|
+
if (document.activeElement === button) {
|
|
8664
|
+
this.#deferButtonDisable(button);
|
|
8665
|
+
} else {
|
|
8666
|
+
this.#disableButton(button);
|
|
8667
|
+
}
|
|
8668
|
+
}
|
|
8669
|
+
/**
|
|
8670
|
+
* Keeps a boundary button focusable until native blur, exposing its temporary
|
|
8671
|
+
* inoperability with `aria-disabled` and making its action a no-op meanwhile.
|
|
8672
|
+
*/
|
|
8673
|
+
#deferButtonDisable(button) {
|
|
8674
|
+
if (this.#pendingButtonDisables.has(button)) return;
|
|
8675
|
+
button.setAttribute("data-overflow-indicator-pending-disabled", "");
|
|
8676
|
+
button.setAttribute(
|
|
8677
|
+
"data-overflow-indicator-aria-disabled",
|
|
8678
|
+
button.getAttribute("aria-disabled") ?? ""
|
|
8679
|
+
);
|
|
8680
|
+
button.setAttribute("aria-disabled", "true");
|
|
8681
|
+
const onBlur = () => {
|
|
8682
|
+
this.#cancelPendingButtonDisable(button);
|
|
8683
|
+
if (!button.disabled) this.#disableButton(button);
|
|
8684
|
+
};
|
|
8685
|
+
this.#pendingButtonDisables.set(button, { onBlur });
|
|
8686
|
+
button.addEventListener("blur", onBlur);
|
|
8687
|
+
}
|
|
8688
|
+
#disableButton(button) {
|
|
8412
8689
|
button.disabled = true;
|
|
8413
8690
|
button.setAttribute("data-overflow-indicator-disabled", "");
|
|
8414
8691
|
}
|
|
8692
|
+
#cancelPendingButtonDisable(button) {
|
|
8693
|
+
const pending = this.#pendingButtonDisables.get(button);
|
|
8694
|
+
if (pending) button.removeEventListener("blur", pending.onBlur);
|
|
8695
|
+
this.#pendingButtonDisables.delete(button);
|
|
8696
|
+
button.removeAttribute("data-overflow-indicator-pending-disabled");
|
|
8697
|
+
const displaced = button.getAttribute("data-overflow-indicator-aria-disabled");
|
|
8698
|
+
if (displaced !== null) {
|
|
8699
|
+
button.removeAttribute("data-overflow-indicator-aria-disabled");
|
|
8700
|
+
if (button.getAttribute("aria-disabled") === "true") {
|
|
8701
|
+
if (displaced === "") button.removeAttribute("aria-disabled");
|
|
8702
|
+
else button.setAttribute("aria-disabled", displaced);
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
#clearPendingButtonDisables() {
|
|
8707
|
+
for (const button of [...this.#pendingButtonDisables.keys()]) {
|
|
8708
|
+
this.#cancelPendingButtonDisable(button);
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
/** Moves resize/mutation/load observation to the current viewport target. */
|
|
8712
|
+
#syncViewport() {
|
|
8713
|
+
if (!this.#connected) return;
|
|
8714
|
+
const next = this.hasViewportTarget ? this.viewportTarget : null;
|
|
8715
|
+
if (next === this.#observedViewport) return;
|
|
8716
|
+
this.#stopObservingViewport();
|
|
8717
|
+
if (!next) return;
|
|
8718
|
+
this.#observedViewport = next;
|
|
8719
|
+
this.#layout.observe(next);
|
|
8720
|
+
this.#layout.observeViewport();
|
|
8721
|
+
next.addEventListener("load", this.#onContentLoad, true);
|
|
8722
|
+
this.#syncContentObservation();
|
|
8723
|
+
if (typeof MutationObserver !== "undefined") {
|
|
8724
|
+
this.#mutationObserver = new MutationObserver(() => {
|
|
8725
|
+
if (!this.#connected || this.#observedViewport !== next) return;
|
|
8726
|
+
this.#syncContentObservation();
|
|
8727
|
+
this.update();
|
|
8728
|
+
});
|
|
8729
|
+
this.#mutationObserver.observe(this.element, {
|
|
8730
|
+
childList: true,
|
|
8731
|
+
subtree: true,
|
|
8732
|
+
characterData: true,
|
|
8733
|
+
attributes: true,
|
|
8734
|
+
attributeFilter: ["class", "style", "hidden"]
|
|
8735
|
+
});
|
|
8736
|
+
}
|
|
8737
|
+
this.#state = null;
|
|
8738
|
+
this.update();
|
|
8739
|
+
}
|
|
8740
|
+
/** Observes direct content boxes whose resize can change the viewport's scroll extent. */
|
|
8741
|
+
#syncContentObservation() {
|
|
8742
|
+
const next = new Set(this.#observedViewport?.children ?? []);
|
|
8743
|
+
for (const content of this.#observedContent) {
|
|
8744
|
+
if (!next.has(content)) {
|
|
8745
|
+
this.#layout.unobserve(content);
|
|
8746
|
+
this.#observedContent.delete(content);
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
for (const content of next) {
|
|
8750
|
+
if (!this.#observedContent.has(content)) {
|
|
8751
|
+
this.#observedContent.add(content);
|
|
8752
|
+
this.#layout.observe(content);
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
#stopObservingViewport() {
|
|
8757
|
+
this.#mutationObserver?.disconnect();
|
|
8758
|
+
this.#mutationObserver = null;
|
|
8759
|
+
this.#observedViewport?.removeEventListener("load", this.#onContentLoad, true);
|
|
8760
|
+
if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);
|
|
8761
|
+
for (const content of this.#observedContent) this.#layout.unobserve(content);
|
|
8762
|
+
this.#observedContent.clear();
|
|
8763
|
+
this.#observedViewport = null;
|
|
8764
|
+
this.#layout.unobserveViewport();
|
|
8765
|
+
}
|
|
8766
|
+
#onContentLoad = () => {
|
|
8767
|
+
if (this.#connected) this.update();
|
|
8768
|
+
};
|
|
8769
|
+
get #threshold() {
|
|
8770
|
+
const value = this.thresholdValue;
|
|
8771
|
+
return Number.isFinite(value) ? Math.max(0, value) : 1;
|
|
8772
|
+
}
|
|
8415
8773
|
#directionFromEvent(event) {
|
|
8416
8774
|
const params = event.params;
|
|
8417
8775
|
const direction = params?.direction;
|
|
8418
8776
|
return direction === "start" || direction === "end" ? direction : null;
|
|
8419
8777
|
}
|
|
8420
|
-
#prefersReducedMotion() {
|
|
8421
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8422
|
-
}
|
|
8423
8778
|
};
|
|
8424
8779
|
var OverflowMenuController = class extends Controller {
|
|
8425
8780
|
static targets = ["items", "more"];
|
|
@@ -10110,29 +10465,49 @@ var ReadMoreController = class extends Controller {
|
|
|
10110
10465
|
collapsed: { type: Boolean, default: true }
|
|
10111
10466
|
};
|
|
10112
10467
|
static actions = ["toggle"];
|
|
10113
|
-
|
|
10114
|
-
#
|
|
10468
|
+
#connected = false;
|
|
10469
|
+
#collapsed = true;
|
|
10470
|
+
#observedContent = null;
|
|
10471
|
+
#contentMutationObserver = null;
|
|
10472
|
+
#deferredHideTrigger = null;
|
|
10473
|
+
#update = () => {
|
|
10474
|
+
if (this.#connected) this.#evaluateOverflow();
|
|
10475
|
+
};
|
|
10476
|
+
#layout = new LayoutObserver(this.#update);
|
|
10477
|
+
#onDeferredHideBlur = () => {
|
|
10478
|
+
this.#clearDeferredHide();
|
|
10479
|
+
this.#update();
|
|
10480
|
+
};
|
|
10115
10481
|
connect() {
|
|
10116
|
-
this.#
|
|
10117
|
-
this.#
|
|
10118
|
-
|
|
10119
|
-
this.#layout.observe(this.contentTarget);
|
|
10120
|
-
this.#layout.observeViewport();
|
|
10121
|
-
}
|
|
10482
|
+
this.#connected = true;
|
|
10483
|
+
this.#collapsed = this.#initialCollapsed();
|
|
10484
|
+
this.#syncTargets();
|
|
10122
10485
|
}
|
|
10123
10486
|
disconnect() {
|
|
10487
|
+
this.#connected = false;
|
|
10488
|
+
this.#stopObservingContent();
|
|
10124
10489
|
this.#layout.disconnect();
|
|
10125
10490
|
}
|
|
10491
|
+
contentTargetConnected() {
|
|
10492
|
+
this.#syncTargets();
|
|
10493
|
+
}
|
|
10494
|
+
contentTargetDisconnected() {
|
|
10495
|
+
this.#syncTargets();
|
|
10496
|
+
}
|
|
10497
|
+
triggerTargetConnected() {
|
|
10498
|
+
this.#syncTargets();
|
|
10499
|
+
}
|
|
10500
|
+
triggerTargetDisconnected(trigger) {
|
|
10501
|
+
if (this.#deferredHideTrigger === trigger) this.#clearDeferredHide();
|
|
10502
|
+
this.#syncTargets();
|
|
10503
|
+
}
|
|
10126
10504
|
/** Toggles between the collapsed (clamped) and expanded states. */
|
|
10127
10505
|
toggle() {
|
|
10128
|
-
|
|
10506
|
+
if (!this.#connected) return;
|
|
10507
|
+
this.#collapsed = !this.#collapsed;
|
|
10508
|
+
this.#reflect();
|
|
10129
10509
|
this.#evaluateOverflow();
|
|
10130
10510
|
}
|
|
10131
|
-
/** Whether the content is currently collapsed (clamped). */
|
|
10132
|
-
get #isCollapsed() {
|
|
10133
|
-
return this.hasContentTarget ? this.contentTarget.getAttribute("data-state") !== "expanded" : this.collapsedValue;
|
|
10134
|
-
}
|
|
10135
|
-
/** Connect-time state: an explicit `data-state` wins, else the `collapsed` Value. */
|
|
10136
10511
|
#initialCollapsed() {
|
|
10137
10512
|
if (this.hasContentTarget) {
|
|
10138
10513
|
const state = this.contentTarget.getAttribute("data-state");
|
|
@@ -10141,29 +10516,76 @@ var ReadMoreController = class extends Controller {
|
|
|
10141
10516
|
}
|
|
10142
10517
|
return this.collapsedValue;
|
|
10143
10518
|
}
|
|
10144
|
-
|
|
10145
|
-
#reflect(collapsed) {
|
|
10519
|
+
#reflect() {
|
|
10146
10520
|
if (this.hasContentTarget) {
|
|
10147
|
-
this.contentTarget.setAttribute("data-state", collapsed ? "collapsed" : "expanded");
|
|
10521
|
+
this.contentTarget.setAttribute("data-state", this.#collapsed ? "collapsed" : "expanded");
|
|
10148
10522
|
}
|
|
10149
10523
|
if (this.hasTriggerTarget) {
|
|
10150
|
-
this.triggerTarget.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
10524
|
+
this.triggerTarget.setAttribute("aria-expanded", this.#collapsed ? "false" : "true");
|
|
10151
10525
|
}
|
|
10152
10526
|
}
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10527
|
+
#syncTargets() {
|
|
10528
|
+
if (!this.#connected) return;
|
|
10529
|
+
this.#syncContentObservation();
|
|
10530
|
+
this.#reflect();
|
|
10531
|
+
this.#evaluateOverflow();
|
|
10532
|
+
}
|
|
10533
|
+
#syncContentObservation() {
|
|
10534
|
+
const next = this.hasContentTarget ? this.contentTarget : null;
|
|
10535
|
+
if (next === this.#observedContent) return;
|
|
10536
|
+
this.#stopObservingContent();
|
|
10537
|
+
if (!next) return;
|
|
10538
|
+
this.#observedContent = next;
|
|
10539
|
+
this.#layout.observe(next);
|
|
10540
|
+
this.#layout.observeViewport();
|
|
10541
|
+
next.addEventListener("load", this.#update, true);
|
|
10542
|
+
if (typeof MutationObserver !== "undefined") {
|
|
10543
|
+
this.#contentMutationObserver = new MutationObserver(this.#update);
|
|
10544
|
+
this.#contentMutationObserver.observe(next, {
|
|
10545
|
+
childList: true,
|
|
10546
|
+
subtree: true,
|
|
10547
|
+
characterData: true
|
|
10548
|
+
});
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
#stopObservingContent() {
|
|
10552
|
+
this.#clearDeferredHide();
|
|
10553
|
+
if (this.#observedContent) {
|
|
10554
|
+
this.#layout.unobserve(this.#observedContent);
|
|
10555
|
+
this.#observedContent.removeEventListener("load", this.#update, true);
|
|
10556
|
+
}
|
|
10557
|
+
this.#observedContent = null;
|
|
10558
|
+
this.#contentMutationObserver?.disconnect();
|
|
10559
|
+
this.#contentMutationObserver = null;
|
|
10560
|
+
this.#layout.unobserveViewport();
|
|
10561
|
+
}
|
|
10562
|
+
#deferHide(trigger) {
|
|
10563
|
+
if (this.#deferredHideTrigger === trigger) return;
|
|
10564
|
+
this.#clearDeferredHide();
|
|
10565
|
+
this.#deferredHideTrigger = trigger;
|
|
10566
|
+
trigger.addEventListener("blur", this.#onDeferredHideBlur);
|
|
10567
|
+
}
|
|
10568
|
+
#clearDeferredHide() {
|
|
10569
|
+
this.#deferredHideTrigger?.removeEventListener("blur", this.#onDeferredHideBlur);
|
|
10570
|
+
this.#deferredHideTrigger = null;
|
|
10571
|
+
}
|
|
10158
10572
|
#evaluateOverflow() {
|
|
10159
10573
|
if (!this.hasTriggerTarget || !this.hasContentTarget) return;
|
|
10160
|
-
|
|
10161
|
-
|
|
10574
|
+
const trigger = this.triggerTarget;
|
|
10575
|
+
const content = this.contentTarget;
|
|
10576
|
+
const useful = !this.#collapsed || content.scrollHeight > content.clientHeight;
|
|
10577
|
+
if (useful) {
|
|
10578
|
+
this.#clearDeferredHide();
|
|
10579
|
+
trigger.hidden = false;
|
|
10162
10580
|
return;
|
|
10163
10581
|
}
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10582
|
+
if (document.activeElement === trigger) {
|
|
10583
|
+
trigger.hidden = false;
|
|
10584
|
+
this.#deferHide(trigger);
|
|
10585
|
+
return;
|
|
10586
|
+
}
|
|
10587
|
+
this.#clearDeferredHide();
|
|
10588
|
+
trigger.hidden = true;
|
|
10167
10589
|
}
|
|
10168
10590
|
};
|
|
10169
10591
|
var PROGRESS_PROPERTY = "--stimeo--reading-progress";
|
|
@@ -10627,8 +11049,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
10627
11049
|
*/
|
|
10628
11050
|
#measurePosition(vp) {
|
|
10629
11051
|
const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && vp.scrollHeight <= vp.clientHeight + EDGE_EPSILON;
|
|
10630
|
-
const scrollPos
|
|
10631
|
-
const maxScroll = horizontalPrimary ? vp.scrollWidth - vp.clientWidth : vp.scrollHeight - vp.clientHeight;
|
|
11052
|
+
const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontalPrimary);
|
|
10632
11053
|
if (maxScroll <= EDGE_EPSILON) return { position: "start", progress: 0 };
|
|
10633
11054
|
const progress = Math.min(1, Math.max(0, scrollPos / maxScroll));
|
|
10634
11055
|
if (scrollPos <= EDGE_EPSILON) return { position: "start", progress };
|
|
@@ -10803,7 +11224,7 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
10803
11224
|
}
|
|
10804
11225
|
/** Scrolls the source to the top and, optionally, moves focus to a safe target. */
|
|
10805
11226
|
toTop() {
|
|
10806
|
-
const behavior =
|
|
11227
|
+
const behavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
10807
11228
|
this.#scrollSource.scrollTo({ top: 0, behavior });
|
|
10808
11229
|
if (this.focusSelectorValue) {
|
|
10809
11230
|
const target = document.querySelector(this.focusSelectorValue);
|
|
@@ -10851,9 +11272,6 @@ var ScrollVisibilityController = class extends Controller {
|
|
|
10851
11272
|
}
|
|
10852
11273
|
return this.#scrollSource.scrollTop;
|
|
10853
11274
|
}
|
|
10854
|
-
#prefersReducedMotion() {
|
|
10855
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
10856
|
-
}
|
|
10857
11275
|
};
|
|
10858
11276
|
var ScrollspyController = class extends Controller {
|
|
10859
11277
|
static targets = ["link"];
|
|
@@ -11092,15 +11510,23 @@ var SeparatorController = class extends Controller {
|
|
|
11092
11510
|
}
|
|
11093
11511
|
}
|
|
11094
11512
|
};
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11513
|
+
|
|
11514
|
+
// src/utils/safe_storage.ts
|
|
11515
|
+
function readLocalStorage(key) {
|
|
11516
|
+
try {
|
|
11517
|
+
return window.localStorage.getItem(key);
|
|
11518
|
+
} catch {
|
|
11519
|
+
return null;
|
|
11520
|
+
}
|
|
11521
|
+
}
|
|
11522
|
+
function writeLocalStorage(key, value) {
|
|
11523
|
+
try {
|
|
11524
|
+
window.localStorage.setItem(key, value);
|
|
11525
|
+
} catch {
|
|
11526
|
+
}
|
|
11103
11527
|
}
|
|
11528
|
+
|
|
11529
|
+
// src/controllers/sidebar_controller.ts
|
|
11104
11530
|
var SidebarController = class extends Controller {
|
|
11105
11531
|
static targets = ["trigger", "panel", "backdrop"];
|
|
11106
11532
|
static values = {
|
|
@@ -11108,9 +11534,11 @@ var SidebarController = class extends Controller {
|
|
|
11108
11534
|
key: { type: String, default: "" },
|
|
11109
11535
|
collapsed: { type: Boolean, default: false }
|
|
11110
11536
|
};
|
|
11111
|
-
static actions = ["close", "open", "toggle"];
|
|
11537
|
+
static actions = ["beforeCache", "close", "open", "toggle"];
|
|
11538
|
+
/** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
|
|
11539
|
+
#activePanel = null;
|
|
11112
11540
|
/** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
|
|
11113
|
-
#trap = new FocusTrap(() => this.panelTarget, {
|
|
11541
|
+
#trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
|
|
11114
11542
|
onEscape: () => this.close(),
|
|
11115
11543
|
fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
|
|
11116
11544
|
});
|
|
@@ -11120,18 +11548,79 @@ var SidebarController = class extends Controller {
|
|
|
11120
11548
|
#collapsed = false;
|
|
11121
11549
|
/** The matched media query (`min-width: breakpoint`), watched for mode changes. */
|
|
11122
11550
|
#mql = null;
|
|
11123
|
-
/**
|
|
11124
|
-
#
|
|
11551
|
+
/** Exact normalized query currently represented by the active media-query listener. */
|
|
11552
|
+
#mqlQuery = null;
|
|
11553
|
+
/** Owns the cancellable close-transition wait and its bounded fallback. */
|
|
11554
|
+
#transition = new TransitionCompletion();
|
|
11555
|
+
/** Distinguishes dynamic target churn from callbacks around controller teardown. */
|
|
11556
|
+
#connected = false;
|
|
11125
11557
|
connect() {
|
|
11558
|
+
this.#connected = true;
|
|
11559
|
+
this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
|
|
11126
11560
|
this.#collapsed = this.#restoreCollapsed();
|
|
11127
|
-
this.#
|
|
11561
|
+
this.#mqlQuery = this.#breakpointQuery;
|
|
11562
|
+
this.#mql = this.#matchBreakpoint(this.#mqlQuery);
|
|
11128
11563
|
this.#mql?.addEventListener("change", this.#onMediaChange);
|
|
11129
11564
|
this.#applyMode(this.#computeMode());
|
|
11130
11565
|
}
|
|
11131
11566
|
disconnect() {
|
|
11567
|
+
this.#connected = false;
|
|
11132
11568
|
this.#mql?.removeEventListener("change", this.#onMediaChange);
|
|
11133
|
-
this.#
|
|
11569
|
+
this.#mql = null;
|
|
11570
|
+
this.#mqlQuery = null;
|
|
11571
|
+
this.#transition.cancel();
|
|
11134
11572
|
this.#trap.deactivate({ restoreFocus: false });
|
|
11573
|
+
this.#activePanel = null;
|
|
11574
|
+
}
|
|
11575
|
+
/** Adopts a panel target added by a Turbo morph after the controller connected. */
|
|
11576
|
+
panelTargetConnected(panel) {
|
|
11577
|
+
if (!this.#connected || this.#activePanel?.isConnected && this.#activePanel !== panel) return;
|
|
11578
|
+
this.#adoptPanel(panel);
|
|
11579
|
+
}
|
|
11580
|
+
/** Closes and releases overlay side effects when the actively trapped panel disappears. */
|
|
11581
|
+
panelTargetDisconnected(panel) {
|
|
11582
|
+
if (panel !== this.#activePanel) return;
|
|
11583
|
+
this.#transition.cancel();
|
|
11584
|
+
this.#activePanel = null;
|
|
11585
|
+
if (!this.#connected) return;
|
|
11586
|
+
panel.setAttribute("data-state", "closed");
|
|
11587
|
+
panel.hidden = true;
|
|
11588
|
+
this.#hideBackdrop();
|
|
11589
|
+
this.#setExpandedAttr(false);
|
|
11590
|
+
this.#trap.deactivate();
|
|
11591
|
+
if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
|
|
11592
|
+
}
|
|
11593
|
+
/**
|
|
11594
|
+
* Rebinds responsive observation when `breakpoint` changes at runtime.
|
|
11595
|
+
*
|
|
11596
|
+
* Stimulus calls value callbacks before `connect()`, so the connected guard
|
|
11597
|
+
* prevents an eager subscription. Equivalent normalized queries retain the
|
|
11598
|
+
* existing listener instead of allocating duplicate `MediaQueryList` objects.
|
|
11599
|
+
*/
|
|
11600
|
+
breakpointValueChanged() {
|
|
11601
|
+
if (!this.#connected) return;
|
|
11602
|
+
const query = this.#breakpointQuery;
|
|
11603
|
+
if (this.#mqlQuery === query) return;
|
|
11604
|
+
this.#mql?.removeEventListener("change", this.#onMediaChange);
|
|
11605
|
+
this.#mqlQuery = query;
|
|
11606
|
+
this.#mql = this.#matchBreakpoint(query);
|
|
11607
|
+
this.#mql?.addEventListener("change", this.#onMediaChange);
|
|
11608
|
+
const next = this.#computeMode();
|
|
11609
|
+
if (next !== this.#mode) this.#applyMode(next);
|
|
11610
|
+
}
|
|
11611
|
+
/**
|
|
11612
|
+
* Sanitizes overlay markup before Turbo snapshots the page.
|
|
11613
|
+
*
|
|
11614
|
+
* Inline state is durable and remains untouched. Overlay state is transient:
|
|
11615
|
+
* pending transitions are cancelled, controller-owned open attributes are
|
|
11616
|
+
* closed immediately, and modal side effects are released without moving
|
|
11617
|
+
* focus during navigation.
|
|
11618
|
+
*/
|
|
11619
|
+
beforeCache() {
|
|
11620
|
+
if (!this.#connected || !this.#isOverlay) return;
|
|
11621
|
+
this.#transition.cancel();
|
|
11622
|
+
this.#trap.deactivate({ restoreFocus: false });
|
|
11623
|
+
this.#setOverlayClosedImmediate();
|
|
11135
11624
|
}
|
|
11136
11625
|
/** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
|
|
11137
11626
|
toggle() {
|
|
@@ -11157,17 +11646,48 @@ var SidebarController = class extends Controller {
|
|
|
11157
11646
|
this.#mode = mode;
|
|
11158
11647
|
if (this.hasPanelTarget) this.panelTarget.setAttribute("data-mode", mode);
|
|
11159
11648
|
if (mode === "inline") {
|
|
11160
|
-
this.#
|
|
11649
|
+
this.#transition.cancel();
|
|
11161
11650
|
this.#trap.deactivate({ restoreFocus: false });
|
|
11162
11651
|
if (this.hasPanelTarget) this.panelTarget.hidden = false;
|
|
11163
11652
|
this.#hideBackdrop();
|
|
11164
11653
|
this.#applyInlineState(this.#collapsed);
|
|
11165
11654
|
} else {
|
|
11166
|
-
this.#
|
|
11655
|
+
this.#transition.cancel();
|
|
11167
11656
|
this.#trap.deactivate({ restoreFocus: false });
|
|
11168
11657
|
this.#setOverlayClosedImmediate();
|
|
11169
11658
|
}
|
|
11170
11659
|
}
|
|
11660
|
+
/** Reconciles a replacement panel with the current responsive mode and DOM state. */
|
|
11661
|
+
#adoptPanel(panel) {
|
|
11662
|
+
this.#transition.cancel();
|
|
11663
|
+
const trapWasActive = this.#trap.active;
|
|
11664
|
+
this.#activePanel = panel;
|
|
11665
|
+
panel.setAttribute("data-mode", this.#mode);
|
|
11666
|
+
if (this.#mode === "inline") {
|
|
11667
|
+
panel.hidden = false;
|
|
11668
|
+
panel.setAttribute("data-state", this.#collapsed ? "collapsed" : "expanded");
|
|
11669
|
+
this.#hideBackdrop();
|
|
11670
|
+
this.#setExpandedAttr(!this.#collapsed);
|
|
11671
|
+
this.#trap.deactivate({ restoreFocus: false });
|
|
11672
|
+
return;
|
|
11673
|
+
}
|
|
11674
|
+
if (panel.getAttribute("data-state") === "open") {
|
|
11675
|
+
panel.hidden = false;
|
|
11676
|
+
if (this.hasBackdropTarget) {
|
|
11677
|
+
this.backdropTarget.setAttribute("data-state", "open");
|
|
11678
|
+
this.backdropTarget.hidden = false;
|
|
11679
|
+
}
|
|
11680
|
+
this.#setExpandedAttr(true);
|
|
11681
|
+
if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });
|
|
11682
|
+
this.#trap.activate();
|
|
11683
|
+
return;
|
|
11684
|
+
}
|
|
11685
|
+
panel.setAttribute("data-state", "closed");
|
|
11686
|
+
panel.hidden = true;
|
|
11687
|
+
this.#hideBackdrop();
|
|
11688
|
+
this.#setExpandedAttr(false);
|
|
11689
|
+
this.#trap.deactivate();
|
|
11690
|
+
}
|
|
11171
11691
|
#onMediaChange = (event) => {
|
|
11172
11692
|
const next = event.matches ? "inline" : "overlay";
|
|
11173
11693
|
if (next !== this.#mode) this.#applyMode(next);
|
|
@@ -11175,13 +11695,14 @@ var SidebarController = class extends Controller {
|
|
|
11175
11695
|
#computeMode() {
|
|
11176
11696
|
return this.#mql?.matches ?? true ? "inline" : "overlay";
|
|
11177
11697
|
}
|
|
11178
|
-
#matchBreakpoint() {
|
|
11698
|
+
#matchBreakpoint(query = this.#breakpointQuery) {
|
|
11179
11699
|
if (typeof window.matchMedia !== "function") return null;
|
|
11180
|
-
return window.matchMedia(
|
|
11700
|
+
return window.matchMedia(query);
|
|
11181
11701
|
}
|
|
11182
11702
|
// --- Inline (rail) ---------------------------------------------------------
|
|
11183
11703
|
/** Sets, reflects, and persists the inline collapsed preference. */
|
|
11184
11704
|
#setCollapsed(collapsed) {
|
|
11705
|
+
if (collapsed === this.#collapsed) return;
|
|
11185
11706
|
this.#collapsed = collapsed;
|
|
11186
11707
|
this.#applyInlineState(collapsed);
|
|
11187
11708
|
this.#persistCollapsed(collapsed);
|
|
@@ -11197,7 +11718,8 @@ var SidebarController = class extends Controller {
|
|
|
11197
11718
|
/** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
|
|
11198
11719
|
#openOverlay() {
|
|
11199
11720
|
if (!this.hasPanelTarget || this.#isOverlayOpen) return;
|
|
11200
|
-
this.#
|
|
11721
|
+
this.#transition.cancel();
|
|
11722
|
+
this.#activePanel = this.panelTarget;
|
|
11201
11723
|
this.panelTarget.hidden = false;
|
|
11202
11724
|
if (this.hasBackdropTarget) this.backdropTarget.hidden = false;
|
|
11203
11725
|
void this.panelTarget.offsetWidth;
|
|
@@ -11226,28 +11748,18 @@ var SidebarController = class extends Controller {
|
|
|
11226
11748
|
}
|
|
11227
11749
|
/**
|
|
11228
11750
|
* Applies `hidden` once the close transition ends so the exit slide can play,
|
|
11229
|
-
* then reverts the modal side effects.
|
|
11230
|
-
*
|
|
11231
|
-
*
|
|
11751
|
+
* then reverts the modal side effects. The shared waiter completes
|
|
11752
|
+
* synchronously for 0ms transitions and supplies a bounded fallback when the
|
|
11753
|
+
* browser emits no terminal event.
|
|
11232
11754
|
*/
|
|
11233
11755
|
#hideAfterTransition() {
|
|
11234
11756
|
const panel = this.panelTarget;
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
this.#applyOverlayHidden();
|
|
11238
|
-
return;
|
|
11239
|
-
}
|
|
11240
|
-
const onEnd = (event) => {
|
|
11241
|
-
if (event.target !== panel) return;
|
|
11242
|
-
this.#cancelPendingHide();
|
|
11243
|
-
this.#applyOverlayHidden();
|
|
11244
|
-
};
|
|
11245
|
-
this.#pendingHide = () => panel.removeEventListener("transitionend", onEnd);
|
|
11246
|
-
panel.addEventListener("transitionend", onEnd);
|
|
11757
|
+
this.#activePanel = panel;
|
|
11758
|
+
this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));
|
|
11247
11759
|
}
|
|
11248
11760
|
/** Hides the panel/backdrop and tears down the trap after the exit transition. */
|
|
11249
|
-
#applyOverlayHidden() {
|
|
11250
|
-
|
|
11761
|
+
#applyOverlayHidden(panel) {
|
|
11762
|
+
panel.hidden = true;
|
|
11251
11763
|
this.#hideBackdrop();
|
|
11252
11764
|
this.#trap.deactivate();
|
|
11253
11765
|
}
|
|
@@ -11256,10 +11768,6 @@ var SidebarController = class extends Controller {
|
|
|
11256
11768
|
this.backdropTarget.setAttribute("data-state", "closed");
|
|
11257
11769
|
this.backdropTarget.hidden = true;
|
|
11258
11770
|
}
|
|
11259
|
-
#cancelPendingHide() {
|
|
11260
|
-
this.#pendingHide?.();
|
|
11261
|
-
this.#pendingHide = null;
|
|
11262
|
-
}
|
|
11263
11771
|
// --- Shared helpers --------------------------------------------------------
|
|
11264
11772
|
#setExpandedAttr(expanded) {
|
|
11265
11773
|
if (this.hasTriggerTarget) {
|
|
@@ -11277,11 +11785,8 @@ var SidebarController = class extends Controller {
|
|
|
11277
11785
|
#restoreCollapsed() {
|
|
11278
11786
|
const key = this.#storageKey;
|
|
11279
11787
|
if (key) {
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
if (stored !== null) return stored === "1";
|
|
11283
|
-
} catch {
|
|
11284
|
-
}
|
|
11788
|
+
const stored = readLocalStorage(key);
|
|
11789
|
+
if (stored !== null) return stored === "1";
|
|
11285
11790
|
}
|
|
11286
11791
|
const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
|
|
11287
11792
|
if (domState === "collapsed") return true;
|
|
@@ -11292,14 +11797,17 @@ var SidebarController = class extends Controller {
|
|
|
11292
11797
|
#persistCollapsed(collapsed) {
|
|
11293
11798
|
const key = this.#storageKey;
|
|
11294
11799
|
if (!key) return;
|
|
11295
|
-
|
|
11296
|
-
localStorage.setItem(key, collapsed ? "1" : "0");
|
|
11297
|
-
} catch {
|
|
11298
|
-
}
|
|
11800
|
+
writeLocalStorage(key, collapsed ? "1" : "0");
|
|
11299
11801
|
}
|
|
11300
11802
|
get #storageKey() {
|
|
11301
11803
|
return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
|
|
11302
11804
|
}
|
|
11805
|
+
/** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */
|
|
11806
|
+
get #breakpointQuery() {
|
|
11807
|
+
const value = this.breakpointValue;
|
|
11808
|
+
const breakpoint = Number.isFinite(value) && value >= 0 ? value : 768;
|
|
11809
|
+
return `(min-width: ${breakpoint}px)`;
|
|
11810
|
+
}
|
|
11303
11811
|
get #isOverlay() {
|
|
11304
11812
|
return this.#mode === "overlay";
|
|
11305
11813
|
}
|
|
@@ -11859,23 +12367,32 @@ var StepperController = class extends Controller {
|
|
|
11859
12367
|
};
|
|
11860
12368
|
static actions = ["goto", "next", "prev"];
|
|
11861
12369
|
static events = ["change"];
|
|
12370
|
+
#isConnected = false;
|
|
11862
12371
|
/** Normalizes an out-of-range initial `index` and renders the initial state. */
|
|
11863
12372
|
connect() {
|
|
11864
|
-
this
|
|
11865
|
-
this.#
|
|
12373
|
+
this.#isConnected = true;
|
|
12374
|
+
this.#normalizeAndRender();
|
|
12375
|
+
}
|
|
12376
|
+
disconnect() {
|
|
12377
|
+
this.#isConnected = false;
|
|
12378
|
+
}
|
|
12379
|
+
/** Re-renders when Turbo Morph or application code changes `index` at runtime. */
|
|
12380
|
+
indexValueChanged() {
|
|
12381
|
+
if (!this.#isConnected) return;
|
|
12382
|
+
this.#normalizeAndRender();
|
|
11866
12383
|
}
|
|
11867
12384
|
/** Advances to the next step (ignored at the last step). */
|
|
11868
12385
|
next() {
|
|
11869
|
-
this.#moveTo(this.indexValue + 1);
|
|
12386
|
+
this.#moveTo(this.#clampIndex(this.indexValue) + 1);
|
|
11870
12387
|
}
|
|
11871
12388
|
/** Returns to the previous step (ignored at the first step). */
|
|
11872
12389
|
prev() {
|
|
11873
|
-
this.#moveTo(this.indexValue - 1);
|
|
12390
|
+
this.#moveTo(this.#clampIndex(this.indexValue) - 1);
|
|
11874
12391
|
}
|
|
11875
12392
|
/** Jumps to the step carried in the action's `index` param. */
|
|
11876
12393
|
goto(event) {
|
|
11877
12394
|
const target = Number(event.params.index);
|
|
11878
|
-
if (!Number.isFinite(target)) return;
|
|
12395
|
+
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
11879
12396
|
this.#moveTo(target);
|
|
11880
12397
|
}
|
|
11881
12398
|
/**
|
|
@@ -11885,16 +12402,24 @@ var StepperController = class extends Controller {
|
|
|
11885
12402
|
*/
|
|
11886
12403
|
#moveTo(target) {
|
|
11887
12404
|
const total = this.stepTargets.length;
|
|
12405
|
+
if (!Number.isFinite(target) || !Number.isInteger(target)) return;
|
|
11888
12406
|
if (target < 0 || target >= total) return;
|
|
11889
|
-
|
|
11890
|
-
if (
|
|
11891
|
-
|
|
12407
|
+
const current = this.#clampIndex(this.indexValue);
|
|
12408
|
+
if (target === current) return;
|
|
12409
|
+
if (this.linearValue && target > current + 1) return;
|
|
12410
|
+
const previous = current;
|
|
11892
12411
|
this.indexValue = target;
|
|
11893
|
-
this.#render();
|
|
12412
|
+
this.#render(target);
|
|
11894
12413
|
this.dispatch("change", {
|
|
11895
12414
|
detail: { index: target, previous, step: this.stepTargets[target] }
|
|
11896
12415
|
});
|
|
11897
12416
|
}
|
|
12417
|
+
/** Normalizes the public Value and reflects it without dispatching an action event. */
|
|
12418
|
+
#normalizeAndRender() {
|
|
12419
|
+
const normalized = this.#clampIndex(this.indexValue);
|
|
12420
|
+
if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;
|
|
12421
|
+
this.#render(normalized);
|
|
12422
|
+
}
|
|
11898
12423
|
/**
|
|
11899
12424
|
* Derives each step's `data-state` and the current button's `aria-current`.
|
|
11900
12425
|
*
|
|
@@ -11902,8 +12427,7 @@ var StepperController = class extends Controller {
|
|
|
11902
12427
|
* contract assumes one operable button per step. If a step needs multiple
|
|
11903
12428
|
* buttons, mark the navigational one first (or this would target the wrong one).
|
|
11904
12429
|
*/
|
|
11905
|
-
#render() {
|
|
11906
|
-
const current = this.indexValue;
|
|
12430
|
+
#render(current) {
|
|
11907
12431
|
this.stepTargets.forEach((step, index) => {
|
|
11908
12432
|
step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
|
|
11909
12433
|
const button = step.querySelector("button");
|
|
@@ -11918,7 +12442,7 @@ var StepperController = class extends Controller {
|
|
|
11918
12442
|
/** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
|
|
11919
12443
|
#clampIndex(index) {
|
|
11920
12444
|
const last = this.stepTargets.length - 1;
|
|
11921
|
-
if (last < 0) return 0;
|
|
12445
|
+
if (last < 0 || !Number.isFinite(index)) return 0;
|
|
11922
12446
|
return Math.min(last, Math.max(0, Math.trunc(index)));
|
|
11923
12447
|
}
|
|
11924
12448
|
};
|
|
@@ -12008,12 +12532,9 @@ var StickToBottomController = class extends Controller {
|
|
|
12008
12532
|
return this.hasContentTarget ? this.contentTarget : this.element;
|
|
12009
12533
|
}
|
|
12010
12534
|
#behavior() {
|
|
12011
|
-
if (
|
|
12535
|
+
if (prefersReducedMotion()) return "auto";
|
|
12012
12536
|
return this.behaviorValue === "smooth" ? "smooth" : "auto";
|
|
12013
12537
|
}
|
|
12014
|
-
#prefersReducedMotion() {
|
|
12015
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
12016
|
-
}
|
|
12017
12538
|
};
|
|
12018
12539
|
var StickyObserverController = class extends Controller {
|
|
12019
12540
|
static targets = ["sentinel", "element"];
|
|
@@ -12026,23 +12547,63 @@ var StickyObserverController = class extends Controller {
|
|
|
12026
12547
|
#watcher = new IntersectionWatcher((entries) => this.#onIntersect(entries));
|
|
12027
12548
|
/** Last reported stuck state, so `change` fires only on transitions. */
|
|
12028
12549
|
#stuck = null;
|
|
12550
|
+
/** Target currently owned by the watcher, used to avoid duplicate restarts. */
|
|
12551
|
+
#observedSentinel = null;
|
|
12552
|
+
#connected = false;
|
|
12029
12553
|
#onIntersect(entries) {
|
|
12030
|
-
const entry
|
|
12031
|
-
|
|
12032
|
-
|
|
12554
|
+
for (const entry of entries) {
|
|
12555
|
+
if (!this.#connected || !this.#watcher.active) return;
|
|
12556
|
+
this.#setStuck(!entry.isIntersecting && isBeforeRootStart(entry));
|
|
12557
|
+
}
|
|
12033
12558
|
}
|
|
12034
12559
|
connect() {
|
|
12035
|
-
|
|
12036
|
-
this.#
|
|
12037
|
-
|
|
12038
|
-
rootMargin: `-${this.offsetValue}px 0px 0px 0px`,
|
|
12039
|
-
threshold: [0]
|
|
12040
|
-
});
|
|
12560
|
+
this.#connected = true;
|
|
12561
|
+
this.#stuck = null;
|
|
12562
|
+
this.#syncObserver();
|
|
12041
12563
|
}
|
|
12042
12564
|
disconnect() {
|
|
12565
|
+
this.#connected = false;
|
|
12043
12566
|
this.#watcher.stop();
|
|
12567
|
+
this.#observedSentinel = null;
|
|
12044
12568
|
this.#stuck = null;
|
|
12045
12569
|
}
|
|
12570
|
+
/** Starts observation when a sentinel is inserted after connection. */
|
|
12571
|
+
sentinelTargetConnected() {
|
|
12572
|
+
if (this.#connected) this.#syncObserver();
|
|
12573
|
+
}
|
|
12574
|
+
/** Stops or transfers observation when the current sentinel is removed. */
|
|
12575
|
+
sentinelTargetDisconnected() {
|
|
12576
|
+
if (this.#connected) this.#syncObserver();
|
|
12577
|
+
}
|
|
12578
|
+
/** Reflects the last snapshot onto an element inserted after that snapshot. */
|
|
12579
|
+
elementTargetConnected(element) {
|
|
12580
|
+
if (this.#stuck !== null) {
|
|
12581
|
+
element.setAttribute("data-stuck", this.#stuck ? "true" : "false");
|
|
12582
|
+
}
|
|
12583
|
+
}
|
|
12584
|
+
/** Rebuilds the observer when Turbo morphs the configured root. */
|
|
12585
|
+
rootSelectorValueChanged() {
|
|
12586
|
+
if (this.#connected) this.#syncObserver(true);
|
|
12587
|
+
}
|
|
12588
|
+
/** Rebuilds the observer when Turbo morphs the configured top offset. */
|
|
12589
|
+
offsetValueChanged() {
|
|
12590
|
+
if (this.#connected) this.#syncObserver(true);
|
|
12591
|
+
}
|
|
12592
|
+
#syncObserver(force = false) {
|
|
12593
|
+
const sentinel = this.hasSentinelTarget ? this.sentinelTarget : null;
|
|
12594
|
+
if (!force && sentinel === this.#observedSentinel && this.#watcher.active) return;
|
|
12595
|
+
this.#watcher.stop();
|
|
12596
|
+
this.#observedSentinel = null;
|
|
12597
|
+
if (!sentinel) return;
|
|
12598
|
+
const configuredOffset = this.offsetValue;
|
|
12599
|
+
const offset = Number.isFinite(configuredOffset) ? configuredOffset : 0;
|
|
12600
|
+
const started = this.#watcher.start(sentinel, {
|
|
12601
|
+
rootSelector: this.rootSelectorValue,
|
|
12602
|
+
rootMargin: `${-offset}px 0px 0px 0px`,
|
|
12603
|
+
threshold: [0]
|
|
12604
|
+
});
|
|
12605
|
+
if (started) this.#observedSentinel = sentinel;
|
|
12606
|
+
}
|
|
12046
12607
|
/** Reflects the stuck state onto the sticky element and emits `change`. */
|
|
12047
12608
|
#setStuck(next) {
|
|
12048
12609
|
if (next === this.#stuck) return;
|
|
@@ -12700,19 +13261,12 @@ var ThemeController = class extends Controller {
|
|
|
12700
13261
|
}
|
|
12701
13262
|
/** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */
|
|
12702
13263
|
#readStored() {
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
return isMode(value) ? value : null;
|
|
12706
|
-
} catch {
|
|
12707
|
-
return null;
|
|
12708
|
-
}
|
|
13264
|
+
const value = readLocalStorage(this.storageKeyValue);
|
|
13265
|
+
return isMode(value) ? value : null;
|
|
12709
13266
|
}
|
|
12710
13267
|
/** Persists the mode, swallowing storage errors (private mode / quota). */
|
|
12711
13268
|
#writeStored(mode) {
|
|
12712
|
-
|
|
12713
|
-
window.localStorage.setItem(this.storageKeyValue, mode);
|
|
12714
|
-
} catch {
|
|
12715
|
-
}
|
|
13269
|
+
writeLocalStorage(this.storageKeyValue, mode);
|
|
12716
13270
|
}
|
|
12717
13271
|
};
|
|
12718
13272
|
var AM = 0;
|
|
@@ -13168,8 +13722,7 @@ var ToastController = class extends Controller {
|
|
|
13168
13722
|
this.listTarget.removeChild(element);
|
|
13169
13723
|
this.dispatch("dismiss", { detail: { item: element, reason } });
|
|
13170
13724
|
};
|
|
13171
|
-
const
|
|
13172
|
-
const duration = cssTimeToMs(transitions);
|
|
13725
|
+
const duration = maxTransitionTotalMs(window.getComputedStyle(element));
|
|
13173
13726
|
if (duration > 0) {
|
|
13174
13727
|
this.#timers.set(finalize, duration);
|
|
13175
13728
|
} else {
|
|
@@ -13241,12 +13794,6 @@ var ToastController = class extends Controller {
|
|
|
13241
13794
|
this.#rafHandles.delete(element);
|
|
13242
13795
|
}
|
|
13243
13796
|
};
|
|
13244
|
-
function cssTimeToMs(value) {
|
|
13245
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
13246
|
-
const amount = Number.parseFloat(first);
|
|
13247
|
-
if (Number.isNaN(amount)) return 0;
|
|
13248
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
13249
|
-
}
|
|
13250
13797
|
var ToggleGroupController = class extends Controller {
|
|
13251
13798
|
static targets = ["item"];
|
|
13252
13799
|
static values = {
|
|
@@ -13535,12 +14082,6 @@ var TooltipController = class extends Controller {
|
|
|
13535
14082
|
}
|
|
13536
14083
|
};
|
|
13537
14084
|
var tokensOf = (value) => value.split(/\s+/).filter(Boolean);
|
|
13538
|
-
var firstTimeMs = (value) => {
|
|
13539
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
13540
|
-
const amount = Number.parseFloat(first);
|
|
13541
|
-
if (Number.isNaN(amount)) return 0;
|
|
13542
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
13543
|
-
};
|
|
13544
14085
|
var TransitionController = class extends Controller {
|
|
13545
14086
|
static values = {
|
|
13546
14087
|
enter: { type: String, default: "" },
|
|
@@ -13553,9 +14094,9 @@ var TransitionController = class extends Controller {
|
|
|
13553
14094
|
};
|
|
13554
14095
|
static actions = ["enter", "leave", "toggle"];
|
|
13555
14096
|
static events = ["entered", "left"];
|
|
13556
|
-
|
|
14097
|
+
/** Owns the cancellable completion wait (terminal events + bounded fallback). */
|
|
14098
|
+
#transition = new TransitionCompletion();
|
|
13557
14099
|
#rafId = null;
|
|
13558
|
-
#endListener = null;
|
|
13559
14100
|
connect() {
|
|
13560
14101
|
this.#strip();
|
|
13561
14102
|
this.element.setAttribute("data-transition-state", this.element.hidden ? "left" : "entered");
|
|
@@ -13582,7 +14123,7 @@ var TransitionController = class extends Controller {
|
|
|
13582
14123
|
const isEnter = kind === "enter";
|
|
13583
14124
|
if (isEnter) this.element.hidden = false;
|
|
13584
14125
|
this.element.setAttribute("data-transition-state", isEnter ? "entering" : "leaving");
|
|
13585
|
-
if (
|
|
14126
|
+
if (prefersReducedMotion()) {
|
|
13586
14127
|
this.#finish(kind);
|
|
13587
14128
|
return;
|
|
13588
14129
|
}
|
|
@@ -13594,12 +14135,13 @@ var TransitionController = class extends Controller {
|
|
|
13594
14135
|
this.#rafId = null;
|
|
13595
14136
|
this.#remove(from);
|
|
13596
14137
|
this.#add(to);
|
|
13597
|
-
this.#
|
|
14138
|
+
this.#transition.wait(this.element, () => this.#finish(kind), {
|
|
14139
|
+
timeoutMs: this.timeoutValue
|
|
14140
|
+
});
|
|
13598
14141
|
});
|
|
13599
14142
|
}
|
|
13600
14143
|
/** Settles the element into the completed state, clearing the stage classes. */
|
|
13601
14144
|
#finish(kind) {
|
|
13602
|
-
this.#cleanupEnd();
|
|
13603
14145
|
this.#strip();
|
|
13604
14146
|
if (kind === "enter") {
|
|
13605
14147
|
this.element.setAttribute("data-transition-state", "entered");
|
|
@@ -13610,29 +14152,13 @@ var TransitionController = class extends Controller {
|
|
|
13610
14152
|
this.dispatch("left", { detail: {} });
|
|
13611
14153
|
}
|
|
13612
14154
|
}
|
|
13613
|
-
/** Resolves on the element's own `transitionend`, with a safety timeout fallback. */
|
|
13614
|
-
#awaitEnd(done) {
|
|
13615
|
-
this.#endListener = (event) => {
|
|
13616
|
-
if (event.target === this.element) done();
|
|
13617
|
-
};
|
|
13618
|
-
this.element.addEventListener("transitionend", this.#endListener);
|
|
13619
|
-
const ms = this.timeoutValue > 0 ? this.timeoutValue : this.#duration();
|
|
13620
|
-
this.#timers.set(done, ms);
|
|
13621
|
-
}
|
|
13622
|
-
#cleanupEnd() {
|
|
13623
|
-
if (this.#endListener) {
|
|
13624
|
-
this.element.removeEventListener("transitionend", this.#endListener);
|
|
13625
|
-
this.#endListener = null;
|
|
13626
|
-
}
|
|
13627
|
-
this.#timers.clearAll();
|
|
13628
|
-
}
|
|
13629
14155
|
/** Cancels any in-flight transition (interruption / teardown). */
|
|
13630
14156
|
#cancel() {
|
|
13631
14157
|
if (this.#rafId !== null) {
|
|
13632
14158
|
this.#cancelRaf(this.#rafId);
|
|
13633
14159
|
this.#rafId = null;
|
|
13634
14160
|
}
|
|
13635
|
-
this.#
|
|
14161
|
+
this.#transition.cancel();
|
|
13636
14162
|
this.#strip();
|
|
13637
14163
|
}
|
|
13638
14164
|
#add(...lists) {
|
|
@@ -13654,13 +14180,6 @@ var TransitionController = class extends Controller {
|
|
|
13654
14180
|
this.leaveToValue
|
|
13655
14181
|
);
|
|
13656
14182
|
}
|
|
13657
|
-
/** Auto-computed safety duration (transition time + delay, with a small buffer). */
|
|
13658
|
-
#duration() {
|
|
13659
|
-
if (typeof window.getComputedStyle !== "function") return 0;
|
|
13660
|
-
const style = window.getComputedStyle(this.element);
|
|
13661
|
-
const total = firstTimeMs(style.transitionDuration) + firstTimeMs(style.transitionDelay);
|
|
13662
|
-
return total > 0 ? total + 50 : 0;
|
|
13663
|
-
}
|
|
13664
14183
|
#raf(callback) {
|
|
13665
14184
|
if (typeof window.requestAnimationFrame === "function") {
|
|
13666
14185
|
return window.requestAnimationFrame(() => callback());
|
|
@@ -13671,9 +14190,6 @@ var TransitionController = class extends Controller {
|
|
|
13671
14190
|
if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(id);
|
|
13672
14191
|
else window.clearTimeout(id);
|
|
13673
14192
|
}
|
|
13674
|
-
#prefersReducedMotion() {
|
|
13675
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
13676
|
-
}
|
|
13677
14193
|
};
|
|
13678
14194
|
var TYPEAHEAD_TIMEOUT2 = 500;
|
|
13679
14195
|
var TreeViewController = class extends Controller {
|