stimeo-ui 0.8.0 → 0.9.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 +89 -0
- data/dist/controllers/bulk_select_controller.js +139 -28
- data/dist/controllers/clipboard_controller.js +102 -20
- data/dist/controllers/color_picker_controller.js +180 -43
- data/dist/controllers/data_grid_controller.js +150 -24
- data/dist/controllers/editable_controller.js +83 -30
- data/dist/controllers/filter_controller.js +32 -1
- data/dist/controllers/masonry_controller.js +129 -17
- data/dist/controllers/otp_controller.js +29 -16
- data/dist/controllers/reset_before_cache_controller.js +51 -5
- data/dist/controllers/resizable_controller.js +128 -55
- data/dist/index.js +860 -339
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
data/dist/index.js
CHANGED
|
@@ -1664,72 +1664,156 @@ var BreadcrumbController = class extends Controller {
|
|
|
1664
1664
|
return active instanceof HTMLElement && element.contains(active);
|
|
1665
1665
|
}
|
|
1666
1666
|
};
|
|
1667
|
+
|
|
1668
|
+
// src/utils/announce.ts
|
|
1669
|
+
function announce(message, options = {}) {
|
|
1670
|
+
const text = message.trim();
|
|
1671
|
+
if (text.length === 0) return;
|
|
1672
|
+
window.dispatchEvent(
|
|
1673
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
1674
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
1675
|
+
})
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
function fillTemplate(template, values) {
|
|
1679
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
1680
|
+
const replacement = values[name];
|
|
1681
|
+
return replacement === void 0 ? match : String(replacement);
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/controllers/bulk_select_controller.ts
|
|
1667
1686
|
var BulkSelectController = class extends Controller {
|
|
1668
1687
|
static targets = ["all", "item", "bar", "count", "selectAllPages"];
|
|
1669
1688
|
static values = {
|
|
1670
1689
|
totalCount: { type: Number, default: 0 },
|
|
1671
|
-
|
|
1690
|
+
announceText: { type: String, default: "" }
|
|
1672
1691
|
};
|
|
1673
1692
|
static actions = ["clear", "selectAllPages"];
|
|
1674
|
-
static events = ["change"];
|
|
1675
|
-
/** All-pages mode is a transient UI state
|
|
1676
|
-
*
|
|
1693
|
+
static events = ["change", "reconcile"];
|
|
1694
|
+
/** All-pages mode is a transient UI state, mirrored to `data-all-pages` so a
|
|
1695
|
+
* `connect()` over markup that already carries the attribute rehydrates the
|
|
1696
|
+
* mode — a morph, a Turbo Stream, or a server that renders it back. A restore
|
|
1697
|
+
* visit serves the server's markup instead, so the mode does not survive one. */
|
|
1677
1698
|
#allPagesMode = false;
|
|
1678
|
-
/** Last emitted figures, so a recompute
|
|
1699
|
+
/** Last emitted figures, so a recompute reports only on a real change. */
|
|
1679
1700
|
#lastCount = -1;
|
|
1680
1701
|
#lastAllPages = false;
|
|
1702
|
+
/** Collapses every signal from one DOM or Value update into one repair. */
|
|
1703
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileNow());
|
|
1681
1704
|
/** Delegated `change` handler covering the select-all box and every row. */
|
|
1682
1705
|
#onChange = (event) => {
|
|
1683
1706
|
const target = event.target;
|
|
1684
1707
|
if (!target) return;
|
|
1685
1708
|
if (this.hasAllTarget && target === this.allTarget) {
|
|
1686
1709
|
this.#applyAll();
|
|
1687
|
-
} else if (
|
|
1710
|
+
} else if (this.itemTargets.some((item) => item === target)) {
|
|
1688
1711
|
this.#exitAllPages();
|
|
1689
|
-
this.#recompute(
|
|
1712
|
+
this.#reportChange(this.#recompute());
|
|
1690
1713
|
}
|
|
1691
1714
|
};
|
|
1692
1715
|
connect() {
|
|
1693
1716
|
this.#allPagesMode = this.element.dataset.allPages === "true";
|
|
1717
|
+
this.#reconcile.activate();
|
|
1694
1718
|
this.element.addEventListener("change", this.#onChange);
|
|
1695
|
-
this.#recompute(
|
|
1719
|
+
this.#recompute();
|
|
1696
1720
|
}
|
|
1697
1721
|
disconnect() {
|
|
1698
1722
|
this.element.removeEventListener("change", this.#onChange);
|
|
1723
|
+
this.#reconcile.cancel();
|
|
1724
|
+
}
|
|
1725
|
+
/** Repairs the figures for a row that arrived at runtime. */
|
|
1726
|
+
itemTargetConnected() {
|
|
1727
|
+
this.#reconcile.schedule();
|
|
1728
|
+
}
|
|
1729
|
+
/** Repairs the figures after a row leaves, so a removed selection stops counting. */
|
|
1730
|
+
itemTargetDisconnected() {
|
|
1731
|
+
this.#reconcile.schedule();
|
|
1732
|
+
}
|
|
1733
|
+
/** Reflects the current selection onto a select-all box added at runtime. */
|
|
1734
|
+
allTargetConnected() {
|
|
1735
|
+
this.#reconcile.schedule();
|
|
1736
|
+
}
|
|
1737
|
+
/** Repairs the figures after the select-all box leaves. */
|
|
1738
|
+
allTargetDisconnected() {
|
|
1739
|
+
this.#reconcile.schedule();
|
|
1740
|
+
}
|
|
1741
|
+
/** Repaints the count for a total that changed at runtime, rejecting non-finite ones. */
|
|
1742
|
+
totalCountValueChanged() {
|
|
1743
|
+
if (!Number.isFinite(this.totalCountValue)) {
|
|
1744
|
+
this.totalCountValue = 0;
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
this.#reconcile.schedule();
|
|
1748
|
+
}
|
|
1749
|
+
/** Repaints so wording changed at runtime is used by the next announcement. */
|
|
1750
|
+
announceTextValueChanged() {
|
|
1751
|
+
this.#reconcile.schedule();
|
|
1699
1752
|
}
|
|
1700
1753
|
/** Clears every selection (rows + select-all) and exits all-pages mode. */
|
|
1701
1754
|
clear() {
|
|
1702
|
-
for (const item of this
|
|
1755
|
+
for (const item of this.itemTargets) item.checked = false;
|
|
1703
1756
|
if (this.hasAllTarget) {
|
|
1704
1757
|
this.allTarget.checked = false;
|
|
1705
1758
|
this.allTarget.indeterminate = false;
|
|
1706
1759
|
}
|
|
1707
1760
|
this.#exitAllPages();
|
|
1708
|
-
this.#recompute(
|
|
1761
|
+
this.#reportChange(this.#recompute());
|
|
1709
1762
|
}
|
|
1710
|
-
/**
|
|
1763
|
+
/**
|
|
1764
|
+
* Enters "select all across pages" mode: the count shows `totalCount`, and every
|
|
1765
|
+
* row on this page is checked.
|
|
1766
|
+
*
|
|
1767
|
+
* The mode's claim is that the whole set is selected, so leaving a visible row
|
|
1768
|
+
* unchecked would put the page and the count in open disagreement.
|
|
1769
|
+
*/
|
|
1711
1770
|
selectAllPages() {
|
|
1712
1771
|
this.#allPagesMode = true;
|
|
1713
|
-
this.#
|
|
1772
|
+
this.#checkEveryRow();
|
|
1773
|
+
this.#reportChange(this.#recompute());
|
|
1774
|
+
}
|
|
1775
|
+
/** Marks every row on this page selected. */
|
|
1776
|
+
#checkEveryRow() {
|
|
1777
|
+
for (const item of this.itemTargets) item.checked = true;
|
|
1714
1778
|
}
|
|
1715
1779
|
/** Mirrors the select-all box to every row, then recomputes. */
|
|
1716
1780
|
#applyAll() {
|
|
1717
1781
|
if (!this.hasAllTarget) return;
|
|
1718
1782
|
const { checked } = this.allTarget;
|
|
1719
|
-
for (const item of this
|
|
1783
|
+
for (const item of this.itemTargets) item.checked = checked;
|
|
1720
1784
|
this.#exitAllPages();
|
|
1721
|
-
this.#recompute(
|
|
1785
|
+
this.#reportChange(this.#recompute());
|
|
1722
1786
|
}
|
|
1723
1787
|
#exitAllPages() {
|
|
1724
1788
|
this.#allPagesMode = false;
|
|
1725
1789
|
}
|
|
1790
|
+
/** Repairs the derived state after the page moved rows or a render input. */
|
|
1791
|
+
#reconcileNow() {
|
|
1792
|
+
if (this.#allPagesMode) this.#checkEveryRow();
|
|
1793
|
+
const detail = this.#recompute();
|
|
1794
|
+
if (!detail) return;
|
|
1795
|
+
this.dispatch("reconcile", { detail });
|
|
1796
|
+
this.#announce(detail);
|
|
1797
|
+
}
|
|
1798
|
+
/** Reports a selection the user moved. */
|
|
1799
|
+
#reportChange(detail) {
|
|
1800
|
+
if (!detail) return;
|
|
1801
|
+
this.dispatch("change", { detail });
|
|
1802
|
+
this.#announce(detail);
|
|
1803
|
+
}
|
|
1804
|
+
/** Hands the count to the shared announcer, worded by the consumer. */
|
|
1805
|
+
#announce(detail) {
|
|
1806
|
+
announce(fillTemplate(this.announceTextValue, { count: detail.count }));
|
|
1807
|
+
}
|
|
1726
1808
|
/**
|
|
1727
1809
|
* Recomputes the count, the select-all checked/indeterminate state, and the bar
|
|
1728
|
-
* visibility from the current DOM.
|
|
1729
|
-
*
|
|
1810
|
+
* visibility from the current DOM. Returns the figures when the emitted count or
|
|
1811
|
+
* all-pages flag actually moved, and `null` when they did not.
|
|
1812
|
+
*
|
|
1813
|
+
* @stimeoRenderRoot
|
|
1730
1814
|
*/
|
|
1731
|
-
#recompute(
|
|
1732
|
-
const items = this
|
|
1815
|
+
#recompute() {
|
|
1816
|
+
const items = this.itemTargets;
|
|
1733
1817
|
const total = items.length;
|
|
1734
1818
|
const checked = items.filter((item) => item.checked).length;
|
|
1735
1819
|
const allPages = this.#allPagesMode;
|
|
@@ -1740,8 +1824,10 @@ var BulkSelectController = class extends Controller {
|
|
|
1740
1824
|
const count = allPages ? this.totalCountValue : checked;
|
|
1741
1825
|
const show = allPages || checked > 0;
|
|
1742
1826
|
if (this.hasBarTarget) {
|
|
1827
|
+
if (!show && this.hasAllTarget && this.barTarget.contains(document.activeElement)) {
|
|
1828
|
+
this.allTarget.focus();
|
|
1829
|
+
}
|
|
1743
1830
|
this.barTarget.hidden = !show;
|
|
1744
|
-
this.barTarget.setAttribute("aria-live", this.announceValue ? "polite" : "off");
|
|
1745
1831
|
}
|
|
1746
1832
|
if (this.hasCountTarget) this.countTarget.textContent = String(count);
|
|
1747
1833
|
this.element.setAttribute("data-selected-count", String(checked));
|
|
@@ -1750,15 +1836,7 @@ var BulkSelectController = class extends Controller {
|
|
|
1750
1836
|
const changed = count !== this.#lastCount || allPages !== this.#lastAllPages;
|
|
1751
1837
|
this.#lastCount = count;
|
|
1752
1838
|
this.#lastAllPages = allPages;
|
|
1753
|
-
|
|
1754
|
-
this.dispatch("change", { detail: { count, allPages } });
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
/** Live list of row checkboxes, queried from the DOM so dynamic rows count. */
|
|
1758
|
-
get #items() {
|
|
1759
|
-
return Array.from(
|
|
1760
|
-
this.element.querySelectorAll('[data-stimeo--bulk-select-target="item"]')
|
|
1761
|
-
);
|
|
1839
|
+
return changed ? { count, allPages } : null;
|
|
1762
1840
|
}
|
|
1763
1841
|
};
|
|
1764
1842
|
|
|
@@ -2718,25 +2796,6 @@ function hits(elements, node) {
|
|
|
2718
2796
|
function setAttributeIfChanged(element, name, value) {
|
|
2719
2797
|
if (element.getAttribute(name) !== value) element.setAttribute(name, value);
|
|
2720
2798
|
}
|
|
2721
|
-
|
|
2722
|
-
// src/utils/announce.ts
|
|
2723
|
-
function announce(message, options = {}) {
|
|
2724
|
-
const text = message.trim();
|
|
2725
|
-
if (text.length === 0) return;
|
|
2726
|
-
window.dispatchEvent(
|
|
2727
|
-
new CustomEvent("stimeo--announcer:announce", {
|
|
2728
|
-
detail: { message: text, assertive: options.assertive === true }
|
|
2729
|
-
})
|
|
2730
|
-
);
|
|
2731
|
-
}
|
|
2732
|
-
function fillTemplate(template, values) {
|
|
2733
|
-
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
2734
|
-
const replacement = values[name];
|
|
2735
|
-
return replacement === void 0 ? match : String(replacement);
|
|
2736
|
-
});
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// src/controllers/character_counter_controller.ts
|
|
2740
2799
|
var CharacterCounterController = class _CharacterCounterController extends Controller {
|
|
2741
2800
|
static targets = ["input", "output"];
|
|
2742
2801
|
static values = {
|
|
@@ -3150,45 +3209,59 @@ function setDefaultAttribute(element, name, value) {
|
|
|
3150
3209
|
}
|
|
3151
3210
|
|
|
3152
3211
|
// src/controllers/clipboard_controller.ts
|
|
3212
|
+
var TRANSIENT_STATES = /* @__PURE__ */ new Set(["copied", "error"]);
|
|
3153
3213
|
var ClipboardController = class extends Controller {
|
|
3154
3214
|
static targets = ["source", "button", "feedback"];
|
|
3155
3215
|
static values = {
|
|
3156
3216
|
text: { type: String, default: "" },
|
|
3157
3217
|
feedbackDuration: { type: Number, default: 2e3 },
|
|
3158
3218
|
copiedLabel: { type: String, default: "Copied" },
|
|
3159
|
-
errorLabel: { type: String, default: "Copy failed" }
|
|
3219
|
+
errorLabel: { type: String, default: "Copy failed" },
|
|
3220
|
+
announceCopiedText: { type: String, default: "" },
|
|
3221
|
+
announceErrorText: { type: String, default: "" }
|
|
3160
3222
|
};
|
|
3161
3223
|
static actions = ["copy"];
|
|
3162
3224
|
static events = ["copy"];
|
|
3163
|
-
/**
|
|
3225
|
+
/**
|
|
3226
|
+
* The pending return to idle — the only timer this controller schedules, so
|
|
3227
|
+
* `clearAll()` is exactly "drop the auto-clear" and needs no id of its own.
|
|
3228
|
+
*/
|
|
3164
3229
|
#timers = new SafeTimeout();
|
|
3230
|
+
/** Returns the completion state to idle for the snapshot Turbo takes. */
|
|
3231
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewind());
|
|
3165
3232
|
/**
|
|
3166
|
-
*
|
|
3167
|
-
* a
|
|
3168
|
-
*
|
|
3233
|
+
* Whether this connection is still live. `copy()` suspends on the Clipboard API,
|
|
3234
|
+
* and a teardown that lands while it is suspended must win: the continuation
|
|
3235
|
+
* would otherwise write to an element nobody owns and arm a timer past the
|
|
3236
|
+
* `clearAll()` that was supposed to be the last word.
|
|
3169
3237
|
*/
|
|
3170
|
-
#
|
|
3238
|
+
#connected = false;
|
|
3171
3239
|
connect() {
|
|
3172
|
-
|
|
3240
|
+
this.#connected = true;
|
|
3241
|
+
this.#adopt();
|
|
3242
|
+
this.#beforeCache.activate();
|
|
3173
3243
|
}
|
|
3174
3244
|
disconnect() {
|
|
3245
|
+
this.#connected = false;
|
|
3246
|
+
this.#beforeCache.deactivate();
|
|
3175
3247
|
this.#timers.clearAll();
|
|
3176
3248
|
}
|
|
3177
3249
|
/**
|
|
3178
3250
|
* Copies the resolved text and reports the outcome. Bound via `data-action`
|
|
3179
|
-
* (click).
|
|
3180
|
-
* — including on failure — so consumers can react either way.
|
|
3251
|
+
* (click). Dispatches `stimeo--clipboard:copy` with `{ success, text }` once per
|
|
3252
|
+
* completed attempt — including on failure — so consumers can react either way.
|
|
3253
|
+
* An attempt whose connection ended while it was in flight reports nothing.
|
|
3181
3254
|
*/
|
|
3182
3255
|
async copy() {
|
|
3183
3256
|
const text = this.#resolveText();
|
|
3184
3257
|
let success = false;
|
|
3185
3258
|
try {
|
|
3186
|
-
if (!navigator.clipboard?.writeText) throw new Error("Clipboard API unavailable");
|
|
3187
3259
|
await navigator.clipboard.writeText(text);
|
|
3188
3260
|
success = true;
|
|
3189
3261
|
} catch {
|
|
3190
3262
|
success = false;
|
|
3191
3263
|
}
|
|
3264
|
+
if (!this.#connected) return;
|
|
3192
3265
|
this.#reportResult(success);
|
|
3193
3266
|
this.dispatch("copy", { detail: { success, text } });
|
|
3194
3267
|
}
|
|
@@ -3205,24 +3278,52 @@ var ClipboardController = class extends Controller {
|
|
|
3205
3278
|
}
|
|
3206
3279
|
return source.textContent ?? "";
|
|
3207
3280
|
}
|
|
3208
|
-
/**
|
|
3281
|
+
/**
|
|
3282
|
+
* Reads the current state back from the DOM.
|
|
3283
|
+
*
|
|
3284
|
+
* A `copied` or `error` found at connect time is this controller's own output
|
|
3285
|
+
* from a connection that is gone, and so is the timer that would have cleared it
|
|
3286
|
+
* — nothing else would ever return the element to `idle`. Any other authored
|
|
3287
|
+
* value belongs to the consumer and only a missing attribute takes the default.
|
|
3288
|
+
*/
|
|
3289
|
+
#adopt() {
|
|
3290
|
+
if (this.#inTransientState()) {
|
|
3291
|
+
this.#reset();
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
3295
|
+
}
|
|
3296
|
+
/** Whether `data-state` currently holds one of the values this controller writes. */
|
|
3297
|
+
#inTransientState() {
|
|
3298
|
+
const state = this.element.getAttribute("data-state");
|
|
3299
|
+
return state !== null && TRANSIENT_STATES.has(state);
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Returns the completion state to idle for the snapshot Turbo is about to take,
|
|
3303
|
+
* so a page reached with the Back button does not report a copy that happened
|
|
3304
|
+
* before the navigation. Only a state this controller wrote is rewound — an
|
|
3305
|
+
* authored one is the consumer's and has to survive into the snapshot, exactly as
|
|
3306
|
+
* `connect()` leaves it alone. State only — no `copy` is dispatched, which would
|
|
3307
|
+
* claim a fresh copy ran.
|
|
3308
|
+
*/
|
|
3309
|
+
#rewind() {
|
|
3310
|
+
if (!this.#inTransientState()) return;
|
|
3311
|
+
this.#timers.clearAll();
|
|
3312
|
+
this.#reset();
|
|
3313
|
+
}
|
|
3314
|
+
/** Reflects the result, announces it, and schedules the return to idle. */
|
|
3209
3315
|
#reportResult(success) {
|
|
3210
3316
|
this.element.setAttribute("data-state", success ? "copied" : "error");
|
|
3211
3317
|
if (this.hasFeedbackTarget) {
|
|
3212
3318
|
this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
3213
3319
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
this.#resetTimerId = null;
|
|
3217
|
-
}
|
|
3320
|
+
announce(success ? this.announceCopiedTextValue : this.announceErrorTextValue);
|
|
3321
|
+
this.#timers.clearAll();
|
|
3218
3322
|
if (this.feedbackDurationValue > 0) {
|
|
3219
|
-
this.#
|
|
3220
|
-
this.#resetTimerId = null;
|
|
3221
|
-
this.#reset();
|
|
3222
|
-
}, this.feedbackDurationValue);
|
|
3323
|
+
this.#timers.set(() => this.#reset(), this.feedbackDurationValue);
|
|
3223
3324
|
}
|
|
3224
3325
|
}
|
|
3225
|
-
/** Returns to the idle state and
|
|
3326
|
+
/** Returns to the idle state and empties the completion slot. */
|
|
3226
3327
|
#reset() {
|
|
3227
3328
|
this.element.setAttribute("data-state", "idle");
|
|
3228
3329
|
if (this.hasFeedbackTarget) {
|
|
@@ -3511,8 +3612,62 @@ function toFiniteNumber(raw) {
|
|
|
3511
3612
|
return Number.isFinite(value) ? value : null;
|
|
3512
3613
|
}
|
|
3513
3614
|
|
|
3615
|
+
// src/utils/owned_pointer_session.ts
|
|
3616
|
+
var OwnedPointerSession = class {
|
|
3617
|
+
pointerId;
|
|
3618
|
+
#owner;
|
|
3619
|
+
#handlers;
|
|
3620
|
+
#abort = new AbortController();
|
|
3621
|
+
#active = true;
|
|
3622
|
+
constructor(start, owner, handlers) {
|
|
3623
|
+
this.pointerId = start.pointerId;
|
|
3624
|
+
this.#owner = owner;
|
|
3625
|
+
this.#handlers = handlers;
|
|
3626
|
+
const { signal } = this.#abort;
|
|
3627
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
3628
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
3629
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
3630
|
+
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
3631
|
+
try {
|
|
3632
|
+
owner.setPointerCapture?.(this.pointerId);
|
|
3633
|
+
} catch {
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
/** Whether this session still owns its pointer and listeners. */
|
|
3637
|
+
get active() {
|
|
3638
|
+
return this.#active;
|
|
3639
|
+
}
|
|
3640
|
+
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
3641
|
+
owns(event) {
|
|
3642
|
+
return this.#active && event.pointerId === this.pointerId;
|
|
3643
|
+
}
|
|
3644
|
+
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
3645
|
+
end() {
|
|
3646
|
+
if (!this.#active) return;
|
|
3647
|
+
this.#active = false;
|
|
3648
|
+
this.#abort.abort();
|
|
3649
|
+
try {
|
|
3650
|
+
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
this.#handlers.end?.();
|
|
3654
|
+
}
|
|
3655
|
+
#onMove = (event) => {
|
|
3656
|
+
if (this.owns(event)) this.#handlers.move(event);
|
|
3657
|
+
};
|
|
3658
|
+
#onEndEvent = (event) => {
|
|
3659
|
+
if (this.owns(event)) this.end();
|
|
3660
|
+
};
|
|
3661
|
+
#onLostCapture = (event) => {
|
|
3662
|
+
const pointerId = event.pointerId;
|
|
3663
|
+
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
3664
|
+
this.end();
|
|
3665
|
+
};
|
|
3666
|
+
};
|
|
3667
|
+
|
|
3514
3668
|
// src/controllers/color_picker_controller.ts
|
|
3515
3669
|
var COLOR_PROPERTY = "--stimeo--color";
|
|
3670
|
+
var VALUE_TEXT_ATTRIBUTE = "data-value-text";
|
|
3516
3671
|
var CHANNEL_RANGE = {
|
|
3517
3672
|
hue: [0, 360],
|
|
3518
3673
|
saturation: [0, 100],
|
|
@@ -3532,10 +3687,10 @@ var ColorPickerController = class extends Controller {
|
|
|
3532
3687
|
get #mirrored() {
|
|
3533
3688
|
return this.logicalTrackValue && isRtl(this.element);
|
|
3534
3689
|
}
|
|
3535
|
-
/** The current color in the editing model. */
|
|
3690
|
+
/** The current color in the editing model; its alpha is 100 while `alpha` is off. */
|
|
3536
3691
|
#color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
|
|
3537
|
-
/**
|
|
3538
|
-
#
|
|
3692
|
+
/** The pointer that owns the live drag, with the slider whose geometry maps it. */
|
|
3693
|
+
#drag = null;
|
|
3539
3694
|
/** Color the last repaint settled on, so a configuration-driven move is reported once. */
|
|
3540
3695
|
#committedHex = null;
|
|
3541
3696
|
/**
|
|
@@ -3546,25 +3701,48 @@ var ColorPickerController = class extends Controller {
|
|
|
3546
3701
|
/** Seeds the model from the initial hex value and renders every surface. */
|
|
3547
3702
|
connect() {
|
|
3548
3703
|
this.#repaint.activate();
|
|
3549
|
-
|
|
3550
|
-
if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };
|
|
3704
|
+
this.#adoptValue();
|
|
3551
3705
|
this.#render();
|
|
3552
3706
|
}
|
|
3553
3707
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
3554
3708
|
disconnect() {
|
|
3555
3709
|
this.#repaint.cancel();
|
|
3556
|
-
this.#
|
|
3557
|
-
this.#dragAbort = null;
|
|
3710
|
+
this.#endDrag();
|
|
3558
3711
|
}
|
|
3559
3712
|
/** Repaints when application code (or a Turbo morph) changes `alpha` at runtime. */
|
|
3560
3713
|
alphaValueChanged() {
|
|
3561
3714
|
this.#repaint.schedule();
|
|
3562
3715
|
}
|
|
3716
|
+
/** Adopts a color application code (or a Turbo morph) put in `value` at runtime. */
|
|
3717
|
+
valueValueChanged() {
|
|
3718
|
+
if (this.valueValue === this.#committedHex) return;
|
|
3719
|
+
this.#repaint.schedule();
|
|
3720
|
+
}
|
|
3721
|
+
/** Hydrates a channel slider inserted or replaced at runtime. */
|
|
3722
|
+
sliderTargetConnected(slider) {
|
|
3723
|
+
this.#renderSlider(slider);
|
|
3724
|
+
}
|
|
3725
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
3726
|
+
sliderTargetDisconnected(slider) {
|
|
3727
|
+
if (this.#drag?.slider === slider) this.#endDrag();
|
|
3728
|
+
}
|
|
3729
|
+
/** Fills a hex input inserted or replaced at runtime with the current color. */
|
|
3730
|
+
hexTargetConnected(hex) {
|
|
3731
|
+
this.#mirrorColor(hex, this.#hexString());
|
|
3732
|
+
}
|
|
3733
|
+
/** Fills a form field inserted or replaced at runtime with the current color. */
|
|
3734
|
+
fieldTargetConnected(field) {
|
|
3735
|
+
this.#mirrorColor(field, this.#hexString());
|
|
3736
|
+
}
|
|
3737
|
+
/** Publishes the current color on a preview inserted or replaced at runtime. */
|
|
3738
|
+
previewTargetConnected(preview) {
|
|
3739
|
+
this.#publishColor(preview, this.#hexString());
|
|
3740
|
+
}
|
|
3563
3741
|
/** Keyboard stepping on the focused channel slider (APG Slider model). */
|
|
3564
3742
|
onKeydown(event) {
|
|
3565
3743
|
if (isReservedArrowChord(event)) return;
|
|
3566
3744
|
const slider = event.currentTarget;
|
|
3567
|
-
const channel = this.#
|
|
3745
|
+
const channel = this.#editableChannel(slider);
|
|
3568
3746
|
if (!channel) return;
|
|
3569
3747
|
const [min, max] = this.#rangeOf(slider, channel);
|
|
3570
3748
|
const value = this.#color[channel];
|
|
@@ -3596,34 +3774,36 @@ var ColorPickerController = class extends Controller {
|
|
|
3596
3774
|
event.preventDefault();
|
|
3597
3775
|
this.#setChannel(channel, next, min, max);
|
|
3598
3776
|
}
|
|
3599
|
-
/** Begins a
|
|
3777
|
+
/** Begins a primary-button drag on a channel slider, owned by its own pointer. */
|
|
3600
3778
|
onPointerDown(event) {
|
|
3779
|
+
if (event.button !== 0 || this.#drag) return;
|
|
3601
3780
|
const slider = event.currentTarget;
|
|
3602
|
-
const channel = this.#
|
|
3781
|
+
const channel = this.#editableChannel(slider);
|
|
3603
3782
|
if (!channel) return;
|
|
3604
|
-
event.preventDefault();
|
|
3605
|
-
slider.focus();
|
|
3606
3783
|
const [min, max] = this.#rangeOf(slider, channel);
|
|
3607
3784
|
const mirrored = this.#mirrored;
|
|
3608
3785
|
const update = (clientX) => {
|
|
3609
3786
|
const rect = slider.getBoundingClientRect();
|
|
3610
|
-
if (rect.width === 0) return;
|
|
3787
|
+
if (rect.width === 0) return false;
|
|
3611
3788
|
const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
3612
3789
|
const fraction = mirrored ? 1 - offset : offset;
|
|
3613
3790
|
this.#setChannel(channel, min + fraction * (max - min), min, max);
|
|
3791
|
+
return true;
|
|
3614
3792
|
};
|
|
3615
|
-
update(event.clientX);
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3793
|
+
if (!update(event.clientX)) return;
|
|
3794
|
+
event.preventDefault();
|
|
3795
|
+
slider.focus();
|
|
3796
|
+
const drag = { pointer: null, slider };
|
|
3797
|
+
drag.pointer = new OwnedPointerSession(event, slider, {
|
|
3798
|
+
move: (move) => {
|
|
3799
|
+
if (slider.isConnected) update(move.clientX);
|
|
3800
|
+
else this.#endDrag();
|
|
3801
|
+
},
|
|
3802
|
+
end: () => {
|
|
3803
|
+
if (this.#drag === drag) this.#drag = null;
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
this.#drag = drag;
|
|
3627
3807
|
}
|
|
3628
3808
|
/** Parses the hex input on confirm and syncs every channel + surface. */
|
|
3629
3809
|
onHexInput() {
|
|
@@ -3633,9 +3813,22 @@ var ColorPickerController = class extends Controller {
|
|
|
3633
3813
|
this.hexTarget.value = this.#hexString();
|
|
3634
3814
|
return;
|
|
3635
3815
|
}
|
|
3636
|
-
this.#color = this
|
|
3816
|
+
this.#color = this.#opaqueUnlessEnabled(parsed);
|
|
3637
3817
|
this.#commitColor();
|
|
3638
3818
|
}
|
|
3819
|
+
/** Replaces the model with the color `value` names, leaving an unparsable one alone. */
|
|
3820
|
+
#adoptValue() {
|
|
3821
|
+
const parsed = hexToHsla(this.valueValue);
|
|
3822
|
+
if (parsed) this.#color = this.#opaqueUnlessEnabled(parsed);
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* The model a parsed color implies: alpha only survives while its channel is
|
|
3826
|
+
* enabled, because `hexString()` would otherwise emit `#RRGGBB` while `change`
|
|
3827
|
+
* reported `rgba.a < 1`.
|
|
3828
|
+
*/
|
|
3829
|
+
#opaqueUnlessEnabled(parsed) {
|
|
3830
|
+
return this.alphaValue ? parsed : { ...parsed, alpha: 100 };
|
|
3831
|
+
}
|
|
3639
3832
|
/** Clamps and snaps one channel to an integer, then re-renders + emits change. */
|
|
3640
3833
|
#setChannel(channel, raw, min, max) {
|
|
3641
3834
|
this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
|
|
@@ -3654,32 +3847,57 @@ var ColorPickerController = class extends Controller {
|
|
|
3654
3847
|
}
|
|
3655
3848
|
}
|
|
3656
3849
|
/**
|
|
3657
|
-
* Reflects the model onto sliders, the hex input, preview,
|
|
3850
|
+
* Reflects the model onto sliders, the hex input, preview, form field, and the
|
|
3851
|
+
* `value` Value it serializes into.
|
|
3658
3852
|
*
|
|
3659
3853
|
* @stimeoRenderRoot
|
|
3660
3854
|
*/
|
|
3661
3855
|
#render() {
|
|
3662
|
-
for (const slider of this.sliderTargets)
|
|
3663
|
-
const channel = this.#channelOf(slider);
|
|
3664
|
-
if (!channel) continue;
|
|
3665
|
-
const value = this.#color[channel];
|
|
3666
|
-
slider.setAttribute("aria-valuenow", String(value));
|
|
3667
|
-
slider.setAttribute("aria-valuetext", valueText(channel, value));
|
|
3668
|
-
}
|
|
3856
|
+
for (const slider of this.sliderTargets) this.#renderSlider(slider);
|
|
3669
3857
|
const hex = this.#hexString();
|
|
3670
3858
|
this.#committedHex = hex;
|
|
3671
|
-
if (this.
|
|
3672
|
-
|
|
3673
|
-
for (const
|
|
3674
|
-
this.
|
|
3859
|
+
if (this.valueValue !== hex) this.valueValue = hex;
|
|
3860
|
+
if (this.hasHexTarget) this.#mirrorColor(this.hexTarget, hex);
|
|
3861
|
+
for (const field of this.fieldTargets) this.#mirrorColor(field, hex);
|
|
3862
|
+
for (const preview of this.previewTargets) this.#publishColor(preview, hex);
|
|
3863
|
+
this.#publishColor(this.element, hex);
|
|
3864
|
+
}
|
|
3865
|
+
/** Writes one slider's announced range, value, and value text, skipping equal ones. */
|
|
3866
|
+
#renderSlider(slider) {
|
|
3867
|
+
const channel = this.#channelOf(slider);
|
|
3868
|
+
if (!channel) return;
|
|
3869
|
+
const [min, max] = this.#rangeOf(slider, channel);
|
|
3870
|
+
const value = this.#color[channel];
|
|
3871
|
+
const attributes = {
|
|
3872
|
+
"aria-valuemin": String(min),
|
|
3873
|
+
"aria-valuemax": String(max),
|
|
3874
|
+
"aria-valuenow": String(value),
|
|
3875
|
+
"aria-valuetext": this.#valueText(slider, channel, value)
|
|
3876
|
+
};
|
|
3877
|
+
for (const [name, next] of Object.entries(attributes)) {
|
|
3878
|
+
if (slider.getAttribute(name) !== next) slider.setAttribute(name, next);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
/** Mirrors the color into an input, leaving an already-equal value untouched. */
|
|
3882
|
+
#mirrorColor(input, hex) {
|
|
3883
|
+
if (input.value !== hex) input.value = hex;
|
|
3884
|
+
}
|
|
3885
|
+
/** Publishes the color as the consumer's CSS hook, skipping an equal value. */
|
|
3886
|
+
#publishColor(element, hex) {
|
|
3887
|
+
if (element.style.getPropertyValue(COLOR_PROPERTY) !== hex) {
|
|
3888
|
+
element.style.setProperty(COLOR_PROPERTY, hex);
|
|
3889
|
+
}
|
|
3675
3890
|
}
|
|
3676
3891
|
/**
|
|
3677
|
-
* Repaints after
|
|
3678
|
-
* settled on. Disabling alpha drops it from the model
|
|
3679
|
-
*
|
|
3892
|
+
* Repaints after a declarative input changed at runtime and reports a color this
|
|
3893
|
+
* controller settled on. Disabling alpha drops it from the model and an outside
|
|
3894
|
+
* `value` names another color, so the committed color can move without a user
|
|
3895
|
+
* edit; `change` stays reserved for the picker's own actions.
|
|
3680
3896
|
*/
|
|
3681
3897
|
#reconcileColor() {
|
|
3682
3898
|
const previous = this.#committedHex;
|
|
3899
|
+
if (previous !== null && this.valueValue !== previous) this.#adoptValue();
|
|
3900
|
+
if (!this.alphaValue) this.#color.alpha = 100;
|
|
3683
3901
|
this.#render();
|
|
3684
3902
|
if (previous !== null && this.#committedHex !== previous) {
|
|
3685
3903
|
this.dispatch("reconcile", { detail: this.#settledDetail() });
|
|
@@ -3700,7 +3918,27 @@ var ColorPickerController = class extends Controller {
|
|
|
3700
3918
|
/** Reads a slider's `data-channel`, if it is a known channel. */
|
|
3701
3919
|
#channelOf(slider) {
|
|
3702
3920
|
const channel = slider.getAttribute("data-channel");
|
|
3703
|
-
return channel && channel
|
|
3921
|
+
return channel && Object.hasOwn(CHANNEL_RANGE, channel) ? channel : null;
|
|
3922
|
+
}
|
|
3923
|
+
/**
|
|
3924
|
+
* The channel a slider edits, or null when this picker edits none through it. An
|
|
3925
|
+
* alpha slider authored while `alpha` is off edits nothing: moving it would leave
|
|
3926
|
+
* the model translucent behind an opaque `#RRGGBB`.
|
|
3927
|
+
*/
|
|
3928
|
+
#editableChannel(slider) {
|
|
3929
|
+
const channel = this.#channelOf(slider);
|
|
3930
|
+
return channel === "alpha" && !this.alphaValue ? null : channel;
|
|
3931
|
+
}
|
|
3932
|
+
/** The channel's announced text: the slider's template, or the built-in English. */
|
|
3933
|
+
#valueText(slider, channel, value) {
|
|
3934
|
+
const template = slider.getAttribute(VALUE_TEXT_ATTRIBUTE);
|
|
3935
|
+
return template ? template.replaceAll("{value}", String(value)) : defaultValueText(channel, value);
|
|
3936
|
+
}
|
|
3937
|
+
/** Ends the live drag so no further movement of that pointer reaches the model. */
|
|
3938
|
+
#endDrag() {
|
|
3939
|
+
const drag = this.#drag;
|
|
3940
|
+
this.#drag = null;
|
|
3941
|
+
drag?.pointer?.end();
|
|
3704
3942
|
}
|
|
3705
3943
|
/**
|
|
3706
3944
|
* A slider's `[min, max]` from aria-valuemin/max, falling back per channel.
|
|
@@ -3716,7 +3954,7 @@ var ColorPickerController = class extends Controller {
|
|
|
3716
3954
|
];
|
|
3717
3955
|
}
|
|
3718
3956
|
};
|
|
3719
|
-
function
|
|
3957
|
+
function defaultValueText(channel, value) {
|
|
3720
3958
|
const label = channel.charAt(0).toUpperCase() + channel.slice(1);
|
|
3721
3959
|
const unit = channel === "hue" ? "degrees" : "percent";
|
|
3722
3960
|
return `${label} ${value} ${unit}`;
|
|
@@ -5691,6 +5929,25 @@ function round(value, precision) {
|
|
|
5691
5929
|
if (!Number.isFinite(rounded)) return value;
|
|
5692
5930
|
return rounded === 0 ? 0 : rounded;
|
|
5693
5931
|
}
|
|
5932
|
+
|
|
5933
|
+
// src/utils/interactive_host.ts
|
|
5934
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
5935
|
+
function isInteractiveHost(element) {
|
|
5936
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
5937
|
+
let current = element;
|
|
5938
|
+
while (current) {
|
|
5939
|
+
const raw = current.getAttribute("contenteditable");
|
|
5940
|
+
if (raw !== null) {
|
|
5941
|
+
const value = raw.trim().toLowerCase();
|
|
5942
|
+
if (value === "false") return false;
|
|
5943
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
5944
|
+
}
|
|
5945
|
+
current = current.parentElement;
|
|
5946
|
+
}
|
|
5947
|
+
return false;
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
// src/controllers/data_grid_controller.ts
|
|
5694
5951
|
var SORT_CYCLE = ["none", "ascending", "descending"];
|
|
5695
5952
|
function nextSortDirection(current) {
|
|
5696
5953
|
const index = SORT_CYCLE.indexOf(current);
|
|
@@ -5704,8 +5961,15 @@ var DataGridController = class extends Controller {
|
|
|
5704
5961
|
};
|
|
5705
5962
|
static actions = ["onKeydown", "sort", "toggleSelect"];
|
|
5706
5963
|
static events = ["selectionchange", "sort"];
|
|
5707
|
-
/**
|
|
5708
|
-
|
|
5964
|
+
/**
|
|
5965
|
+
* Collapses the per-element target callbacks of one DOM mutation into a single
|
|
5966
|
+
* baseline pass, and refuses to run before `connect()` or after `disconnect()`.
|
|
5967
|
+
*
|
|
5968
|
+
* Stimulus reports every target one at a time, so an ungated pass would re-walk
|
|
5969
|
+
* the whole grid once per authored cell on mount and once per streamed cell
|
|
5970
|
+
* afterwards — quadratic in the cell count both times.
|
|
5971
|
+
*/
|
|
5972
|
+
#reconcile = new MicrotaskCoalescer(() => this.#restoreBaseline());
|
|
5709
5973
|
/**
|
|
5710
5974
|
* Establishes a single tab stop across all navigable cells/headers and brings
|
|
5711
5975
|
* the rows to their baseline.
|
|
@@ -5716,15 +5980,30 @@ var DataGridController = class extends Controller {
|
|
|
5716
5980
|
* attribute, so the Value callback does not fire a second time.
|
|
5717
5981
|
*/
|
|
5718
5982
|
connect() {
|
|
5983
|
+
this.#restoreBaseline();
|
|
5984
|
+
this.#reconcile.activate();
|
|
5985
|
+
}
|
|
5986
|
+
/** Closes the reconcile window so a queued pass cannot run against a detached tree. */
|
|
5987
|
+
disconnect() {
|
|
5988
|
+
this.#reconcile.cancel();
|
|
5989
|
+
}
|
|
5990
|
+
/**
|
|
5991
|
+
* Rebuilds both DOM-owned baselines from the live grid: exactly one navigable
|
|
5992
|
+
* cell is in the Tab sequence, and every selectable row carries an explicit
|
|
5993
|
+
* `aria-selected`.
|
|
5994
|
+
*
|
|
5995
|
+
* The tab stop keeps whichever cell already holds it, so a rebuild triggered by
|
|
5996
|
+
* an unrelated row arriving does not throw the user's position away; only when
|
|
5997
|
+
* no cell holds it — the grid is fresh, or the holder was removed — does the
|
|
5998
|
+
* first navigable cell take over. Without that fallback a grid whose active row
|
|
5999
|
+
* is removed keeps every cell at `-1` and drops out of the Tab sequence
|
|
6000
|
+
* entirely.
|
|
6001
|
+
*/
|
|
6002
|
+
#restoreBaseline() {
|
|
5719
6003
|
const cells = this.#navigableCells();
|
|
5720
6004
|
const active = cells.find((cell) => cell.tabIndex === 0) ?? cells[0];
|
|
5721
|
-
this.#setActiveCell(active, { focus: false });
|
|
6005
|
+
if (active) this.#setActiveCell(active, { focus: false }, cells);
|
|
5722
6006
|
this.#normalizeSelection();
|
|
5723
|
-
this.#connected = true;
|
|
5724
|
-
}
|
|
5725
|
-
/** Reopens the row callback for the next mount. */
|
|
5726
|
-
disconnect() {
|
|
5727
|
-
this.#connected = false;
|
|
5728
6007
|
}
|
|
5729
6008
|
/**
|
|
5730
6009
|
* Keeps `aria-multiselectable` in step with the `selection` Value. Fires on connect
|
|
@@ -5735,16 +6014,25 @@ var DataGridController = class extends Controller {
|
|
|
5735
6014
|
this.#syncSelectable();
|
|
5736
6015
|
this.#normalizeSelection();
|
|
5737
6016
|
}
|
|
5738
|
-
/**
|
|
5739
|
-
* Re-establishes the row baseline for a row added after connect.
|
|
5740
|
-
*
|
|
5741
|
-
* Each pass walks every row, and Stimulus reports the authored rows one by one
|
|
5742
|
-
* before `connect()`, so the mount is gated to keep it linear in the row count
|
|
5743
|
-
* rather than quadratic; `connect()` runs the single baseline pass instead.
|
|
5744
|
-
*/
|
|
6017
|
+
/** Re-establishes the baselines for a row added after connect. */
|
|
5745
6018
|
rowTargetConnected() {
|
|
5746
|
-
|
|
5747
|
-
|
|
6019
|
+
this.#reconcile.schedule();
|
|
6020
|
+
}
|
|
6021
|
+
/** Re-establishes the tab stop when a cell joins the grid after connect. */
|
|
6022
|
+
cellTargetConnected() {
|
|
6023
|
+
this.#reconcile.schedule();
|
|
6024
|
+
}
|
|
6025
|
+
/** Re-establishes the tab stop when a cell leaves the grid. */
|
|
6026
|
+
cellTargetDisconnected() {
|
|
6027
|
+
this.#reconcile.schedule();
|
|
6028
|
+
}
|
|
6029
|
+
/** Re-establishes the tab stop when a header joins the grid after connect. */
|
|
6030
|
+
columnHeaderTargetConnected() {
|
|
6031
|
+
this.#reconcile.schedule();
|
|
6032
|
+
}
|
|
6033
|
+
/** Re-establishes the tab stop when a header leaves the grid. */
|
|
6034
|
+
columnHeaderTargetDisconnected() {
|
|
6035
|
+
this.#reconcile.schedule();
|
|
5748
6036
|
}
|
|
5749
6037
|
/**
|
|
5750
6038
|
* Brings the authored rows to the shape the APG requires, without changing
|
|
@@ -5789,6 +6077,9 @@ var DataGridController = class extends Controller {
|
|
|
5789
6077
|
sort(event) {
|
|
5790
6078
|
const header = event.currentTarget;
|
|
5791
6079
|
if (!this.columnHeaderTargets.includes(header)) return;
|
|
6080
|
+
if (event.defaultPrevented) return;
|
|
6081
|
+
const control = this.#claimingControl(event, header);
|
|
6082
|
+
if (control && !(control instanceof HTMLButtonElement)) return;
|
|
5792
6083
|
const direction = nextSortDirection(header.getAttribute("aria-sort") ?? "none");
|
|
5793
6084
|
for (const other of this.columnHeaderTargets) {
|
|
5794
6085
|
other.setAttribute("aria-sort", other === header ? direction : "none");
|
|
@@ -5799,14 +6090,19 @@ var DataGridController = class extends Controller {
|
|
|
5799
6090
|
/** Toggles selection of the row owning the event target. Bound optionally. */
|
|
5800
6091
|
toggleSelect(event) {
|
|
5801
6092
|
if (this.selectionValue === "none") return;
|
|
5802
|
-
|
|
6093
|
+
if (event.defaultPrevented) return;
|
|
6094
|
+
const host = event.currentTarget;
|
|
6095
|
+
if (this.#claimedByDescendant(event, host)) return;
|
|
6096
|
+
const row = host.closest("[role='row']");
|
|
5803
6097
|
if (row && this.rowTargets.includes(row)) this.#toggleRow(row);
|
|
5804
6098
|
}
|
|
5805
6099
|
/** Grid navigation + sort/select activation. Bound to cells and headers. */
|
|
5806
6100
|
onKeydown(event) {
|
|
5807
6101
|
if (event.defaultPrevented) return;
|
|
5808
6102
|
if (isReservedArrowChord(event)) return;
|
|
6103
|
+
if (event.isComposing) return;
|
|
5809
6104
|
const cell = event.currentTarget;
|
|
6105
|
+
if (this.#claimedByDescendant(event, cell)) return;
|
|
5810
6106
|
const matrix = this.#matrix();
|
|
5811
6107
|
const position = this.#locate(matrix, cell);
|
|
5812
6108
|
if (!position) return;
|
|
@@ -5842,9 +6138,35 @@ var DataGridController = class extends Controller {
|
|
|
5842
6138
|
}
|
|
5843
6139
|
if (target) {
|
|
5844
6140
|
event.preventDefault();
|
|
5845
|
-
this.#setActiveCell(target, { focus: true });
|
|
6141
|
+
this.#setActiveCell(target, { focus: true }, matrix.flat());
|
|
5846
6142
|
}
|
|
5847
6143
|
}
|
|
6144
|
+
/**
|
|
6145
|
+
* Whether the event was addressed to a control inside `host` rather than to the
|
|
6146
|
+
* grid.
|
|
6147
|
+
*
|
|
6148
|
+
* Cells and headers hold consumer markup, and APG's grid pattern expects that
|
|
6149
|
+
* markup to include working controls — a row action button, an inline editor.
|
|
6150
|
+
* Those own their own keystrokes and clicks, so the grid stands down entirely
|
|
6151
|
+
* rather than acting in parallel. An editable host (its `contenteditable` state
|
|
6152
|
+
* is inherited, so the walk is explicit) counts the same way.
|
|
6153
|
+
*/
|
|
6154
|
+
#claimedByDescendant(event, host) {
|
|
6155
|
+
return this.#claimingControl(event, host) !== null;
|
|
6156
|
+
}
|
|
6157
|
+
/**
|
|
6158
|
+
* The nested control this event belongs to, or `null` when the host owns it.
|
|
6159
|
+
*
|
|
6160
|
+
* Naming the control, rather than answering yes or no, is what lets the click
|
|
6161
|
+
* path treat a sortable header's `<button>` as the activation it is while every
|
|
6162
|
+
* other control still takes the event away.
|
|
6163
|
+
*/
|
|
6164
|
+
#claimingControl(event, host) {
|
|
6165
|
+
const source = event.target;
|
|
6166
|
+
const control = source.closest(INTERACTIVE_HOST_SELECTOR);
|
|
6167
|
+
if (control && host.contains(control)) return control;
|
|
6168
|
+
return isInteractiveHost(source) ? source : null;
|
|
6169
|
+
}
|
|
5848
6170
|
/** Performs a header's sort or a cell row's selection toggle on activation. */
|
|
5849
6171
|
#activate(cell) {
|
|
5850
6172
|
if (this.columnHeaderTargets.includes(cell)) {
|
|
@@ -5875,11 +6197,22 @@ var DataGridController = class extends Controller {
|
|
|
5875
6197
|
const rows = this.rowTargets.filter((r) => r.getAttribute("aria-selected") === "true");
|
|
5876
6198
|
this.dispatch("selectionchange", { detail: { rows } });
|
|
5877
6199
|
}
|
|
5878
|
-
/**
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6200
|
+
/**
|
|
6201
|
+
* Makes `cell` the single tabbable cell (roving) and optionally focuses it.
|
|
6202
|
+
*
|
|
6203
|
+
* `cells` lets a caller that already walked the grid hand its collection over,
|
|
6204
|
+
* so one keystroke rebuilds the matrix once instead of twice. The write is
|
|
6205
|
+
* skipped where the attribute already holds the wanted value — comparing the
|
|
6206
|
+
* attribute rather than the IDL property, because a cell with no `tabindex` at
|
|
6207
|
+
* all reports `-1` and would then never receive the attribute it needs to be
|
|
6208
|
+
* focusable.
|
|
6209
|
+
*/
|
|
6210
|
+
#setActiveCell(cell, { focus }, cells) {
|
|
6211
|
+
for (const candidate of cells ?? this.#navigableCells()) {
|
|
6212
|
+
const wanted = candidate === cell ? "0" : "-1";
|
|
6213
|
+
if (candidate.getAttribute("tabindex") !== wanted) {
|
|
6214
|
+
candidate.setAttribute("tabindex", wanted);
|
|
6215
|
+
}
|
|
5883
6216
|
}
|
|
5884
6217
|
if (focus) cell.focus();
|
|
5885
6218
|
}
|
|
@@ -7271,33 +7604,64 @@ var EditableController = class extends Controller {
|
|
|
7271
7604
|
static values = {
|
|
7272
7605
|
submitOnBlur: { type: Boolean, default: true }
|
|
7273
7606
|
};
|
|
7274
|
-
static actions = ["
|
|
7607
|
+
static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
|
|
7275
7608
|
static events = ["cancel", "change"];
|
|
7276
7609
|
/** The value captured when edit mode began, used to detect real changes. */
|
|
7277
7610
|
#previousValue = "";
|
|
7611
|
+
/**
|
|
7612
|
+
* The value the last save replaced, or `null` when there is nothing to undo.
|
|
7613
|
+
* Cleared on connect: a value from before a page restore is unrecoverable, so
|
|
7614
|
+
* `revert()` must not resurrect one.
|
|
7615
|
+
*/
|
|
7616
|
+
#revertValue = null;
|
|
7278
7617
|
/**
|
|
7279
7618
|
* Owns IME lifecycle state for the edit surface, so a keydown that belongs to
|
|
7280
7619
|
* a composition (cancel or confirm) is never treated as an edit command.
|
|
7281
7620
|
*/
|
|
7282
7621
|
#composition = new CompositionTracker();
|
|
7622
|
+
/**
|
|
7623
|
+
* Watches focus leaving the editor from wherever it currently sits.
|
|
7624
|
+
*
|
|
7625
|
+
* `focusout` bubbles where `blur` does not, so one listener on the root sees
|
|
7626
|
+
* every departure — including one from a Save or Cancel button the consumer
|
|
7627
|
+
* placed beside the input. Binding the input alone would make the promise
|
|
7628
|
+
* "saves wherever focus moved" true only for focus that leaves the input
|
|
7629
|
+
* itself, and tabbing straight past an inner button would strand the editor
|
|
7630
|
+
* open.
|
|
7631
|
+
*/
|
|
7632
|
+
#onFocusOut = (event) => {
|
|
7633
|
+
const from = event.target;
|
|
7634
|
+
if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
|
|
7635
|
+
const next = event.relatedTarget;
|
|
7636
|
+
if (next instanceof Node && this.element.contains(next)) return;
|
|
7637
|
+
if (this.submitOnBlurValue) this.#commit(false);
|
|
7638
|
+
};
|
|
7283
7639
|
/** Establishes the initial display mode (display shown, input hidden). */
|
|
7284
7640
|
connect() {
|
|
7285
7641
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
7642
|
+
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
7643
|
+
this.#revertValue = null;
|
|
7286
7644
|
this.#setMode("display");
|
|
7287
7645
|
}
|
|
7288
|
-
/** Releases the composition listeners so nothing outlives the element. */
|
|
7646
|
+
/** Releases the composition and focus listeners so nothing outlives the element. */
|
|
7289
7647
|
disconnect() {
|
|
7290
7648
|
this.#composition.disconnect();
|
|
7649
|
+
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
7291
7650
|
}
|
|
7292
7651
|
/** Tracks an input added initially or after connect (e.g. a Turbo swap). */
|
|
7293
7652
|
inputTargetConnected(input) {
|
|
7294
7653
|
this.#composition.observe(input);
|
|
7654
|
+
this.#applyMode();
|
|
7295
7655
|
}
|
|
7296
7656
|
/** Removes composition listeners when the active input is replaced or removed. */
|
|
7297
7657
|
inputTargetDisconnected(input) {
|
|
7298
7658
|
this.#composition.unobserve(input);
|
|
7299
7659
|
}
|
|
7300
|
-
/**
|
|
7660
|
+
/** Re-hides or re-shows a display element that arrived after the mode was set. */
|
|
7661
|
+
displayTargetConnected() {
|
|
7662
|
+
this.#applyMode();
|
|
7663
|
+
}
|
|
7664
|
+
/** Enters edit mode: seeds the input from the declared value, focuses, selects. */
|
|
7301
7665
|
edit() {
|
|
7302
7666
|
if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
|
|
7303
7667
|
this.#previousValue = this.#currentValue;
|
|
@@ -7306,6 +7670,27 @@ var EditableController = class extends Controller {
|
|
|
7306
7670
|
this.inputTarget.focus();
|
|
7307
7671
|
this.inputTarget.select();
|
|
7308
7672
|
}
|
|
7673
|
+
/** Commits the edit and returns focus to the display element. */
|
|
7674
|
+
save() {
|
|
7675
|
+
this.#commit(true);
|
|
7676
|
+
}
|
|
7677
|
+
/** Discards edits, returns to display mode, and dispatches `cancel`. */
|
|
7678
|
+
cancel() {
|
|
7679
|
+
if (!this.#isEditing) return;
|
|
7680
|
+
this.#setMode("display");
|
|
7681
|
+
if (this.hasDisplayTarget) this.displayTarget.focus();
|
|
7682
|
+
this.dispatch("cancel", { detail: {} });
|
|
7683
|
+
}
|
|
7684
|
+
/**
|
|
7685
|
+
* Puts back the value the last save replaced — for a consumer whose server
|
|
7686
|
+
* rejected it. Silent by design: a `change` here would re-enter the same
|
|
7687
|
+
* handler that asked for the undo. One save, one undo.
|
|
7688
|
+
*/
|
|
7689
|
+
revert() {
|
|
7690
|
+
if (this.#revertValue === null || this.#isEditing) return;
|
|
7691
|
+
this.#writeValue(this.#revertValue);
|
|
7692
|
+
this.#revertValue = null;
|
|
7693
|
+
}
|
|
7309
7694
|
/** Adds `F2` as an editing entry point alongside the button's native activation. */
|
|
7310
7695
|
onDisplayKeydown(event) {
|
|
7311
7696
|
if (event.key === "F2") {
|
|
@@ -7319,56 +7704,57 @@ var EditableController = class extends Controller {
|
|
|
7319
7704
|
if (event.key === "Escape") {
|
|
7320
7705
|
if (event.defaultPrevented) return;
|
|
7321
7706
|
event.preventDefault();
|
|
7322
|
-
this
|
|
7707
|
+
this.cancel();
|
|
7323
7708
|
return;
|
|
7324
7709
|
}
|
|
7325
7710
|
if (event.key === "Enter") {
|
|
7326
7711
|
if (event.defaultPrevented) return;
|
|
7327
7712
|
if (this.#isMultiline && !(event.ctrlKey || event.metaKey)) return;
|
|
7328
7713
|
event.preventDefault();
|
|
7329
|
-
this.#
|
|
7714
|
+
this.#commit(true);
|
|
7330
7715
|
}
|
|
7331
7716
|
}
|
|
7332
|
-
/** Saves on blur when `submitOnBlur` is set; otherwise keeps editing. */
|
|
7333
|
-
onBlur() {
|
|
7334
|
-
if (!this.#isEditing) return;
|
|
7335
|
-
if (this.submitOnBlurValue) this.#save(false);
|
|
7336
|
-
}
|
|
7337
7717
|
/**
|
|
7338
|
-
* Returns to display mode,
|
|
7339
|
-
*
|
|
7718
|
+
* Returns to display mode, storing the input's value and dispatching `change`
|
|
7719
|
+
* when it differs from where editing began.
|
|
7340
7720
|
*
|
|
7341
7721
|
* @param restoreFocus - Move focus back to the display element (explicit
|
|
7342
|
-
*
|
|
7722
|
+
* commit) rather than honoring the user's new focus target (blur).
|
|
7343
7723
|
*/
|
|
7344
|
-
#
|
|
7345
|
-
if (!this.#isEditing) return;
|
|
7346
|
-
const value = this.inputTarget.value;
|
|
7724
|
+
#commit(restoreFocus) {
|
|
7725
|
+
if (!this.#isEditing || !this.hasInputTarget) return;
|
|
7726
|
+
const value = this.inputTarget.value.trim();
|
|
7347
7727
|
const previous = this.#previousValue;
|
|
7348
|
-
this
|
|
7728
|
+
this.#writeValue(value);
|
|
7349
7729
|
this.#setMode("display");
|
|
7350
|
-
if (restoreFocus) this.displayTarget.focus();
|
|
7730
|
+
if (restoreFocus && this.hasDisplayTarget) this.displayTarget.focus();
|
|
7351
7731
|
if (value !== previous) {
|
|
7732
|
+
this.#revertValue = previous;
|
|
7352
7733
|
this.dispatch("change", { detail: { value, previous } });
|
|
7353
7734
|
}
|
|
7354
7735
|
}
|
|
7355
|
-
/**
|
|
7356
|
-
#
|
|
7357
|
-
if (!this
|
|
7358
|
-
this
|
|
7359
|
-
|
|
7360
|
-
|
|
7736
|
+
/** Stores `value` wherever the display element declares it. */
|
|
7737
|
+
#writeValue(value) {
|
|
7738
|
+
if (!this.hasDisplayTarget) return;
|
|
7739
|
+
const display = this.displayTarget;
|
|
7740
|
+
if (display.hasAttribute("data-value")) display.dataset.value = value;
|
|
7741
|
+
else display.textContent = value;
|
|
7361
7742
|
}
|
|
7362
|
-
/**
|
|
7363
|
-
#
|
|
7364
|
-
|
|
7365
|
-
const editing = mode === "editing";
|
|
7743
|
+
/** Derives both elements' visibility from the mode currently in the DOM. */
|
|
7744
|
+
#applyMode() {
|
|
7745
|
+
const editing = this.#isEditing;
|
|
7366
7746
|
if (this.hasDisplayTarget) this.displayTarget.hidden = editing;
|
|
7367
7747
|
if (this.hasInputTarget) this.inputTarget.hidden = !editing;
|
|
7368
7748
|
}
|
|
7369
|
-
/**
|
|
7749
|
+
/** Records the mode, then brings both elements in line with it. */
|
|
7750
|
+
#setMode(mode) {
|
|
7751
|
+
this.element.dataset.mode = mode;
|
|
7752
|
+
this.#applyMode();
|
|
7753
|
+
}
|
|
7754
|
+
/** The value the display element declares, trimmed. */
|
|
7370
7755
|
get #currentValue() {
|
|
7371
|
-
|
|
7756
|
+
const display = this.displayTarget;
|
|
7757
|
+
return (display.dataset.value ?? display.textContent ?? "").trim();
|
|
7372
7758
|
}
|
|
7373
7759
|
/** Whether the editing control is a multi-line `<textarea>`. */
|
|
7374
7760
|
get #isMultiline() {
|
|
@@ -7980,15 +8366,46 @@ var FilterController = class extends Controller {
|
|
|
7980
8366
|
#onChange = () => {
|
|
7981
8367
|
this.apply();
|
|
7982
8368
|
};
|
|
8369
|
+
/**
|
|
8370
|
+
* Gates the declaration callback to the connected window.
|
|
8371
|
+
*
|
|
8372
|
+
* Stimulus delivers a Value callback ahead of `connect()` and again for every
|
|
8373
|
+
* runtime change; without the gate, merely connecting would emit an evaluation
|
|
8374
|
+
* — and its `change` — before `connect()` runs its own.
|
|
8375
|
+
*/
|
|
8376
|
+
#connected = false;
|
|
7983
8377
|
connect() {
|
|
7984
|
-
this
|
|
8378
|
+
this.#evaluate();
|
|
7985
8379
|
this.element.addEventListener("change", this.#onChange);
|
|
8380
|
+
this.#connected = true;
|
|
7986
8381
|
}
|
|
7987
8382
|
disconnect() {
|
|
8383
|
+
this.#connected = false;
|
|
7988
8384
|
this.element.removeEventListener("change", this.#onChange);
|
|
7989
8385
|
}
|
|
8386
|
+
/**
|
|
8387
|
+
* Re-evaluates when the match declaration changes at runtime.
|
|
8388
|
+
*
|
|
8389
|
+
* The declaration decides which items are shown, so an element kept across a
|
|
8390
|
+
* morph — where `connect()` does not run again — still has to follow it instead
|
|
8391
|
+
* of waiting for the next control interaction. The evaluation is the ordinary
|
|
8392
|
+
* one, `change` included: a declaration swap is an evaluation like any other.
|
|
8393
|
+
*/
|
|
8394
|
+
matchValueChanged() {
|
|
8395
|
+
if (!this.#connected) return;
|
|
8396
|
+
this.#evaluate();
|
|
8397
|
+
}
|
|
7990
8398
|
/** Re-derives every item's visibility from the active tokens and syncs groups/empty. */
|
|
7991
8399
|
apply() {
|
|
8400
|
+
this.#evaluate();
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8403
|
+
* The evaluation itself: every item's visibility from the active tokens, then
|
|
8404
|
+
* the groups and the empty element, then the `change` event.
|
|
8405
|
+
*
|
|
8406
|
+
* @stimeoRenderRoot
|
|
8407
|
+
*/
|
|
8408
|
+
#evaluate() {
|
|
7992
8409
|
const active = this.#activeTokens();
|
|
7993
8410
|
let visibleCount = 0;
|
|
7994
8411
|
for (const item of this.itemTargets) {
|
|
@@ -10500,14 +10917,37 @@ var LocalTimeController = class extends Controller {
|
|
|
10500
10917
|
}
|
|
10501
10918
|
};
|
|
10502
10919
|
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
10920
|
+
var DEFAULT_MIN_COLUMN_WIDTH = 240;
|
|
10921
|
+
var DEFAULT_GAP = 16;
|
|
10922
|
+
function usableNumber(value, fallback) {
|
|
10923
|
+
return Number.isFinite(value) ? value : fallback;
|
|
10924
|
+
}
|
|
10503
10925
|
var MasonryController = class extends Controller {
|
|
10504
10926
|
static targets = ["item"];
|
|
10505
10927
|
static values = {
|
|
10506
|
-
minColumnWidth: { type: Number, default:
|
|
10507
|
-
gap: { type: Number, default:
|
|
10928
|
+
minColumnWidth: { type: Number, default: DEFAULT_MIN_COLUMN_WIDTH },
|
|
10929
|
+
gap: { type: Number, default: DEFAULT_GAP }
|
|
10508
10930
|
};
|
|
10509
10931
|
static events = ["layout"];
|
|
10510
|
-
|
|
10932
|
+
/**
|
|
10933
|
+
* The declared numbers after validation, so the layout path never sees a value
|
|
10934
|
+
* it cannot compute with. Both are resolved once per declaration change rather
|
|
10935
|
+
* than on every pass.
|
|
10936
|
+
*/
|
|
10937
|
+
#minColumnWidth = DEFAULT_MIN_COLUMN_WIDTH;
|
|
10938
|
+
#gap = DEFAULT_GAP;
|
|
10939
|
+
/**
|
|
10940
|
+
* Collapses every re-layout trigger of one DOM mutation into a single pass, and
|
|
10941
|
+
* refuses to run before `connect()` or after `disconnect()`.
|
|
10942
|
+
*
|
|
10943
|
+
* The triggers arrive in bursts — a resize stream, a morph that syncs several
|
|
10944
|
+
* attributes, a batch of rows — and each pass measures every item, so folding
|
|
10945
|
+
* them keeps the work proportional to the batch rather than to the events in it.
|
|
10946
|
+
*/
|
|
10947
|
+
#reconcile = new MicrotaskCoalescer(() => this.#relayout());
|
|
10948
|
+
/** Items that left the target set and still carry the column hook. */
|
|
10949
|
+
#released = /* @__PURE__ */ new Set();
|
|
10950
|
+
#layout = new LayoutObserver(() => this.#reconcile.schedule());
|
|
10511
10951
|
#mutationObserver = null;
|
|
10512
10952
|
/** Last published column count, so `layout` fires only on real changes. */
|
|
10513
10953
|
#lastColumns = 0;
|
|
@@ -10517,20 +10957,49 @@ var MasonryController = class extends Controller {
|
|
|
10517
10957
|
* first pass ran before they settled; `load` does not bubble, so this is bound in
|
|
10518
10958
|
* the capture phase to catch every descendant.
|
|
10519
10959
|
*/
|
|
10520
|
-
#onLoad = () => this.#
|
|
10960
|
+
#onLoad = () => this.#reconcile.schedule();
|
|
10961
|
+
/** Resolves the declared column width once, falling back when it is unreadable. */
|
|
10962
|
+
minColumnWidthValueChanged() {
|
|
10963
|
+
this.#minColumnWidth = usableNumber(this.minColumnWidthValue, DEFAULT_MIN_COLUMN_WIDTH);
|
|
10964
|
+
this.#reconcile.schedule();
|
|
10965
|
+
}
|
|
10966
|
+
/** Resolves the declared gap once, falling back when it is unreadable. */
|
|
10967
|
+
gapValueChanged() {
|
|
10968
|
+
this.#gap = usableNumber(this.gapValue, DEFAULT_GAP);
|
|
10969
|
+
this.#reconcile.schedule();
|
|
10970
|
+
}
|
|
10971
|
+
/** Packs an element that became an item without moving in the DOM. */
|
|
10972
|
+
itemTargetConnected() {
|
|
10973
|
+
this.#reconcile.schedule();
|
|
10974
|
+
}
|
|
10975
|
+
/**
|
|
10976
|
+
* Queues the column hook of an element that stopped being an item for removal.
|
|
10977
|
+
*
|
|
10978
|
+
* The removal is queued rather than immediate because teardown reports every
|
|
10979
|
+
* target as disconnected: doing it here would strip the whole grid just before
|
|
10980
|
+
* a Turbo snapshot is taken. {@link MicrotaskCoalescer.cancel} drops the queue
|
|
10981
|
+
* with the pass, so only a genuine target change reaches it.
|
|
10982
|
+
*/
|
|
10983
|
+
itemTargetDisconnected(item) {
|
|
10984
|
+
this.#released.add(item);
|
|
10985
|
+
this.#reconcile.schedule();
|
|
10986
|
+
}
|
|
10521
10987
|
/** Observes size/content changes and performs the first layout pass. */
|
|
10522
10988
|
connect() {
|
|
10523
10989
|
this.#layout.observe(this.element);
|
|
10524
10990
|
this.#layout.observeViewport();
|
|
10525
10991
|
if (typeof MutationObserver !== "undefined") {
|
|
10526
|
-
this.#mutationObserver = new MutationObserver(() => this.#
|
|
10992
|
+
this.#mutationObserver = new MutationObserver(() => this.#reconcile.schedule());
|
|
10527
10993
|
this.#mutationObserver.observe(this.element, { childList: true, subtree: true });
|
|
10528
10994
|
}
|
|
10529
10995
|
this.element.addEventListener("load", this.#onLoad, true);
|
|
10530
10996
|
this.#relayout();
|
|
10997
|
+
this.#reconcile.activate();
|
|
10531
10998
|
}
|
|
10532
10999
|
/** Releases both observers and the load listener so nothing fires after detach. */
|
|
10533
11000
|
disconnect() {
|
|
11001
|
+
this.#reconcile.cancel();
|
|
11002
|
+
this.#released.clear();
|
|
10534
11003
|
this.#layout.disconnect();
|
|
10535
11004
|
this.#mutationObserver?.disconnect();
|
|
10536
11005
|
this.#mutationObserver = null;
|
|
@@ -10539,26 +11008,53 @@ var MasonryController = class extends Controller {
|
|
|
10539
11008
|
}
|
|
10540
11009
|
/**
|
|
10541
11010
|
* Recomputes the column count and assigns every item to the shortest column.
|
|
10542
|
-
* Runs automatically on connect, on resize, on item add/remove,
|
|
10543
|
-
* descendant resource loads (private — there is no
|
|
10544
|
-
*
|
|
10545
|
-
*
|
|
10546
|
-
*
|
|
11011
|
+
* Runs automatically on connect, on resize, on item add/remove, when a declared
|
|
11012
|
+
* number changes, and when a descendant resource loads (private — there is no
|
|
11013
|
+
* public action; the observers, the target callbacks and the capture-phase
|
|
11014
|
+
* `load` listener drive it). Items are walked in DOM order; each lands in the
|
|
11015
|
+
* column with the least accumulated height, which keeps the packing balanced
|
|
11016
|
+
* without reordering the DOM.
|
|
11017
|
+
*
|
|
11018
|
+
* Every box is measured before anything is written. Interleaving the two would
|
|
11019
|
+
* make a consumer's `data-column` rule invalidate style once per item, and the
|
|
11020
|
+
* next measurement then has to settle layout again — once per item instead of
|
|
11021
|
+
* once per pass. The assignment is independent of the measurement because the
|
|
11022
|
+
* columns are uniform in width, so the order of the two passes does not change
|
|
11023
|
+
* the result.
|
|
11024
|
+
*
|
|
11025
|
+
* @stimeoRenderRoot
|
|
10547
11026
|
*/
|
|
10548
11027
|
#relayout() {
|
|
10549
11028
|
const items = this.itemTargets;
|
|
10550
11029
|
const columns = this.#columnCount();
|
|
11030
|
+
const boxes = items.map((item) => item.getBoundingClientRect().height);
|
|
11031
|
+
let changed = false;
|
|
11032
|
+
if (this.#released.size > 0) {
|
|
11033
|
+
const owned = new Set(items);
|
|
11034
|
+
for (const released of this.#released) {
|
|
11035
|
+
if (owned.has(released)) continue;
|
|
11036
|
+
if (released.hasAttribute("data-column")) {
|
|
11037
|
+
released.removeAttribute("data-column");
|
|
11038
|
+
changed = true;
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
this.#released.clear();
|
|
11042
|
+
}
|
|
10551
11043
|
const heights = new Array(columns).fill(0);
|
|
10552
|
-
|
|
11044
|
+
items.forEach((item, index) => {
|
|
10553
11045
|
let shortest = 0;
|
|
10554
11046
|
for (let col = 1; col < columns; col++) {
|
|
10555
11047
|
if ((heights[col] ?? 0) < (heights[shortest] ?? 0)) shortest = col;
|
|
10556
11048
|
}
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
11049
|
+
const assigned = String(shortest);
|
|
11050
|
+
if (item.getAttribute("data-column") !== assigned) {
|
|
11051
|
+
item.setAttribute("data-column", assigned);
|
|
11052
|
+
changed = true;
|
|
11053
|
+
}
|
|
11054
|
+
heights[shortest] = (heights[shortest] ?? 0) + (boxes[index] ?? 0) + this.#gap;
|
|
11055
|
+
});
|
|
10560
11056
|
this.element.style.setProperty(COLUMNS_PROPERTY, String(columns));
|
|
10561
|
-
if (columns !== this.#lastColumns) {
|
|
11057
|
+
if (columns !== this.#lastColumns || changed) {
|
|
10562
11058
|
this.#lastColumns = columns;
|
|
10563
11059
|
this.dispatch("layout", { detail: { columns } });
|
|
10564
11060
|
}
|
|
@@ -10571,9 +11067,9 @@ var MasonryController = class extends Controller {
|
|
|
10571
11067
|
*/
|
|
10572
11068
|
#columnCount() {
|
|
10573
11069
|
const width = this.element.getBoundingClientRect().width;
|
|
10574
|
-
const denominator = this
|
|
11070
|
+
const denominator = this.#minColumnWidth + this.#gap;
|
|
10575
11071
|
if (width <= 0 || denominator <= 0) return 1;
|
|
10576
|
-
return Math.max(1, Math.floor((width + this
|
|
11072
|
+
return Math.max(1, Math.floor((width + this.#gap) / denominator));
|
|
10577
11073
|
}
|
|
10578
11074
|
};
|
|
10579
11075
|
var MenuController = class extends Controller {
|
|
@@ -13611,6 +14107,9 @@ function compilePattern(source) {
|
|
|
13611
14107
|
function hasModifier2(event) {
|
|
13612
14108
|
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
13613
14109
|
}
|
|
14110
|
+
function statesDiffer(left, right) {
|
|
14111
|
+
return left.value !== right.value || left.state !== right.state;
|
|
14112
|
+
}
|
|
13614
14113
|
var OtpController = class extends Controller {
|
|
13615
14114
|
static targets = ["field", "value", "error"];
|
|
13616
14115
|
static values = {
|
|
@@ -13622,8 +14121,8 @@ var OtpController = class extends Controller {
|
|
|
13622
14121
|
#pattern = new RegExp(`^${DEFAULT_PATTERN}$`);
|
|
13623
14122
|
/** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
|
|
13624
14123
|
#patternSource = DEFAULT_PATTERN;
|
|
13625
|
-
/**
|
|
13626
|
-
#
|
|
14124
|
+
/** Public state carried by the last dispatch; keeps a no-op sync silent. */
|
|
14125
|
+
#published = null;
|
|
13627
14126
|
/** Field whose confirming `input` after `compositionend` is already handled. */
|
|
13628
14127
|
#confirmedField = null;
|
|
13629
14128
|
/** True between connect and disconnect, so pre-connect Value changes stay silent. */
|
|
@@ -13659,7 +14158,7 @@ var OtpController = class extends Controller {
|
|
|
13659
14158
|
this.#beforeCache.activate();
|
|
13660
14159
|
this.#reconcile.activate();
|
|
13661
14160
|
this.#adopt();
|
|
13662
|
-
this.#
|
|
14161
|
+
this.#sync();
|
|
13663
14162
|
}
|
|
13664
14163
|
disconnect() {
|
|
13665
14164
|
this.#connected = false;
|
|
@@ -13851,19 +14350,23 @@ var OtpController = class extends Controller {
|
|
|
13851
14350
|
this.#markFilled(field, field.value);
|
|
13852
14351
|
}
|
|
13853
14352
|
/**
|
|
13854
|
-
* Absorbs a batch of field additions or removals as one
|
|
14353
|
+
* Absorbs a batch of field additions or removals as one state transition.
|
|
13855
14354
|
*
|
|
13856
|
-
* The page, not the user, moved the
|
|
14355
|
+
* The page, not the user, moved the state here, so it is reported as
|
|
13857
14356
|
* `reconcile`: automation listening for `change` must not read a re-render as
|
|
13858
14357
|
* an edit, and a passcode that happens to end up full must not fire the
|
|
13859
14358
|
* `complete` that submits it.
|
|
14359
|
+
*
|
|
14360
|
+
* Completeness moves on its own when the field count changes: dropping a
|
|
14361
|
+
* trailing empty field completes a passcode whose combined value never moved,
|
|
14362
|
+
* and adding one un-completes it. Comparing the whole derived state, not the
|
|
14363
|
+
* string it contains, is what makes those transitions reportable.
|
|
13860
14364
|
*/
|
|
13861
14365
|
#reconcileFields() {
|
|
13862
|
-
const previous = this.#
|
|
13863
|
-
const
|
|
13864
|
-
if (
|
|
13865
|
-
this
|
|
13866
|
-
this.dispatch("reconcile", { detail: { value: combined } });
|
|
14366
|
+
const previous = this.#published;
|
|
14367
|
+
const current = this.#sync();
|
|
14368
|
+
if (previous && !statesDiffer(previous, current)) return;
|
|
14369
|
+
this.dispatch("reconcile", { detail: { value: current.value } });
|
|
13867
14370
|
}
|
|
13868
14371
|
/**
|
|
13869
14372
|
* Validates the text an entry point received and distributes what it accepts.
|
|
@@ -13958,23 +14461,29 @@ var OtpController = class extends Controller {
|
|
|
13958
14461
|
const fields = this.fieldTargets;
|
|
13959
14462
|
return fields.length > 0 && fields.every((field) => field.value.length > 0);
|
|
13960
14463
|
}
|
|
13961
|
-
/**
|
|
14464
|
+
/**
|
|
14465
|
+
* Mirrors the combined value into the form and the root's readable state, and
|
|
14466
|
+
* records what was published so the next pass can compare against it.
|
|
14467
|
+
*/
|
|
13962
14468
|
#sync() {
|
|
13963
14469
|
const combined = this.#combinedValue();
|
|
13964
14470
|
if (this.hasValueTarget) {
|
|
13965
14471
|
this.valueTarget.value = combined;
|
|
13966
14472
|
}
|
|
13967
|
-
|
|
13968
|
-
|
|
14473
|
+
const state = this.#stateName(combined);
|
|
14474
|
+
this.#state.write(this.element, state);
|
|
14475
|
+
const published = { value: combined, state };
|
|
14476
|
+
this.#published = published;
|
|
14477
|
+
return published;
|
|
13969
14478
|
}
|
|
13970
14479
|
#stateName(combined) {
|
|
13971
14480
|
if (combined.length === 0) return "empty";
|
|
13972
14481
|
return this.#isComplete() ? "complete" : "partial";
|
|
13973
14482
|
}
|
|
13974
14483
|
#syncAndDispatch() {
|
|
13975
|
-
const
|
|
13976
|
-
|
|
13977
|
-
|
|
14484
|
+
const previous = this.#published;
|
|
14485
|
+
const { value: combined } = this.#sync();
|
|
14486
|
+
if (previous?.value === combined) return;
|
|
13978
14487
|
this.dispatch("change", { detail: { value: combined } });
|
|
13979
14488
|
if (this.#isComplete()) {
|
|
13980
14489
|
this.dispatch("complete", { detail: { value: combined } });
|
|
@@ -16215,25 +16724,6 @@ var ProgressController = class extends Controller {
|
|
|
16215
16724
|
this.element.removeAttribute(OWNED_VALUE_TEXT2);
|
|
16216
16725
|
}
|
|
16217
16726
|
};
|
|
16218
|
-
|
|
16219
|
-
// src/utils/interactive_host.ts
|
|
16220
|
-
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
16221
|
-
function isInteractiveHost(element) {
|
|
16222
|
-
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
16223
|
-
let current = element;
|
|
16224
|
-
while (current) {
|
|
16225
|
-
const raw = current.getAttribute("contenteditable");
|
|
16226
|
-
if (raw !== null) {
|
|
16227
|
-
const value = raw.trim().toLowerCase();
|
|
16228
|
-
if (value === "false") return false;
|
|
16229
|
-
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
16230
|
-
}
|
|
16231
|
-
current = current.parentElement;
|
|
16232
|
-
}
|
|
16233
|
-
return false;
|
|
16234
|
-
}
|
|
16235
|
-
|
|
16236
|
-
// src/controllers/radio_group_controller.ts
|
|
16237
16727
|
var OBSERVED_ATTRIBUTES4 = [
|
|
16238
16728
|
"aria-checked",
|
|
16239
16729
|
"aria-disabled",
|
|
@@ -16737,61 +17227,6 @@ var RadioGroupController = class extends Controller {
|
|
|
16737
17227
|
return radio.getAttribute("data-value") ?? "";
|
|
16738
17228
|
}
|
|
16739
17229
|
};
|
|
16740
|
-
|
|
16741
|
-
// src/utils/owned_pointer_session.ts
|
|
16742
|
-
var OwnedPointerSession = class {
|
|
16743
|
-
pointerId;
|
|
16744
|
-
#owner;
|
|
16745
|
-
#handlers;
|
|
16746
|
-
#abort = new AbortController();
|
|
16747
|
-
#active = true;
|
|
16748
|
-
constructor(start, owner, handlers) {
|
|
16749
|
-
this.pointerId = start.pointerId;
|
|
16750
|
-
this.#owner = owner;
|
|
16751
|
-
this.#handlers = handlers;
|
|
16752
|
-
const { signal } = this.#abort;
|
|
16753
|
-
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
16754
|
-
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
16755
|
-
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
16756
|
-
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
16757
|
-
try {
|
|
16758
|
-
owner.setPointerCapture?.(this.pointerId);
|
|
16759
|
-
} catch {
|
|
16760
|
-
}
|
|
16761
|
-
}
|
|
16762
|
-
/** Whether this session still owns its pointer and listeners. */
|
|
16763
|
-
get active() {
|
|
16764
|
-
return this.#active;
|
|
16765
|
-
}
|
|
16766
|
-
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
16767
|
-
owns(event) {
|
|
16768
|
-
return this.#active && event.pointerId === this.pointerId;
|
|
16769
|
-
}
|
|
16770
|
-
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
16771
|
-
end() {
|
|
16772
|
-
if (!this.#active) return;
|
|
16773
|
-
this.#active = false;
|
|
16774
|
-
this.#abort.abort();
|
|
16775
|
-
try {
|
|
16776
|
-
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
16777
|
-
} catch {
|
|
16778
|
-
}
|
|
16779
|
-
this.#handlers.end?.();
|
|
16780
|
-
}
|
|
16781
|
-
#onMove = (event) => {
|
|
16782
|
-
if (this.owns(event)) this.#handlers.move(event);
|
|
16783
|
-
};
|
|
16784
|
-
#onEndEvent = (event) => {
|
|
16785
|
-
if (this.owns(event)) this.end();
|
|
16786
|
-
};
|
|
16787
|
-
#onLostCapture = (event) => {
|
|
16788
|
-
const pointerId = event.pointerId;
|
|
16789
|
-
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
16790
|
-
this.end();
|
|
16791
|
-
};
|
|
16792
|
-
};
|
|
16793
|
-
|
|
16794
|
-
// src/controllers/range_slider_controller.ts
|
|
16795
17230
|
var START_PROPERTY = "--stimeo--range-slider-start";
|
|
16796
17231
|
var END_PROPERTY = "--stimeo--range-slider-end";
|
|
16797
17232
|
var DEFAULT_MIN = 0;
|
|
@@ -17641,6 +18076,26 @@ var RelativeTimeController = class extends Controller {
|
|
|
17641
18076
|
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
17642
18077
|
}
|
|
17643
18078
|
};
|
|
18079
|
+
var STATELESS_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden", "submit", "reset", "button", "image"]);
|
|
18080
|
+
function restoreField(element) {
|
|
18081
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
18082
|
+
element.value = element.defaultValue;
|
|
18083
|
+
return;
|
|
18084
|
+
}
|
|
18085
|
+
if (element instanceof HTMLSelectElement) {
|
|
18086
|
+
for (const option of element.options) option.selected = option.hasAttribute("selected");
|
|
18087
|
+
return;
|
|
18088
|
+
}
|
|
18089
|
+
if (element instanceof HTMLInputElement) {
|
|
18090
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
18091
|
+
element.checked = element.defaultChecked;
|
|
18092
|
+
} else if (element.type === "file") {
|
|
18093
|
+
element.value = "";
|
|
18094
|
+
} else if (!STATELESS_INPUT_TYPES.has(element.type)) {
|
|
18095
|
+
element.value = element.defaultValue;
|
|
18096
|
+
}
|
|
18097
|
+
}
|
|
18098
|
+
}
|
|
17644
18099
|
var ResetBeforeCacheController = class extends Controller {
|
|
17645
18100
|
static values = {
|
|
17646
18101
|
scope: { type: String, default: "" },
|
|
@@ -17648,8 +18103,32 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
17648
18103
|
};
|
|
17649
18104
|
static actions = ["reset"];
|
|
17650
18105
|
static events = ["reset", "request"];
|
|
18106
|
+
/** The `scope` declaration after validation; empty when it cannot be parsed. */
|
|
18107
|
+
#scopeSelector = "";
|
|
17651
18108
|
/** Runs the reset just before Turbo caches the snapshot. */
|
|
17652
18109
|
#onBeforeCache = () => this.reset();
|
|
18110
|
+
/**
|
|
18111
|
+
* Validates the scope declaration once, keeping only a selector the engine can
|
|
18112
|
+
* read.
|
|
18113
|
+
*
|
|
18114
|
+
* A selector reads back as an ordinary string, so a malformed one survives
|
|
18115
|
+
* until it is handed to the DOM — and this part runs from a single listener
|
|
18116
|
+
* whose whole job is to keep a cached page from freezing. Falling back to the
|
|
18117
|
+
* default keeps that job running with a visible, findable result instead of
|
|
18118
|
+
* silently taking the sweep down.
|
|
18119
|
+
*/
|
|
18120
|
+
scopeValueChanged() {
|
|
18121
|
+
const selector = this.scopeValue;
|
|
18122
|
+
if (selector.length > 0) {
|
|
18123
|
+
try {
|
|
18124
|
+
this.element.matches(selector);
|
|
18125
|
+
this.#scopeSelector = selector;
|
|
18126
|
+
return;
|
|
18127
|
+
} catch {
|
|
18128
|
+
}
|
|
18129
|
+
}
|
|
18130
|
+
this.#scopeSelector = "";
|
|
18131
|
+
}
|
|
17653
18132
|
connect() {
|
|
17654
18133
|
document.addEventListener("turbo:before-cache", this.#onBeforeCache);
|
|
17655
18134
|
}
|
|
@@ -17660,6 +18139,10 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
17660
18139
|
* Resets transient UI within scope to its initial state. Asks controllers to
|
|
17661
18140
|
* close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
|
|
17662
18141
|
* and finally emits `reset`. Safe to call any number of times (idempotent).
|
|
18142
|
+
*
|
|
18143
|
+
* `dispatchReset` decides only whether the `request` ask goes out; the cleanup
|
|
18144
|
+
* and the closing `reset` run either way. Both the listener and this action
|
|
18145
|
+
* reach the same sweep, so `reset` is emitted for a manual call too.
|
|
17663
18146
|
*/
|
|
17664
18147
|
reset() {
|
|
17665
18148
|
const root = this.#scopeRoot();
|
|
@@ -17678,9 +18161,7 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
17678
18161
|
if (element instanceof HTMLFormElement) element.reset();
|
|
17679
18162
|
}
|
|
17680
18163
|
for (const element of root.querySelectorAll("[data-reset-value]")) {
|
|
17681
|
-
|
|
17682
|
-
element.value = "";
|
|
17683
|
-
}
|
|
18164
|
+
restoreField(element);
|
|
17684
18165
|
}
|
|
17685
18166
|
for (const element of root.querySelectorAll("[data-reset-hidden]")) {
|
|
17686
18167
|
element.hidden = true;
|
|
@@ -17692,30 +18173,38 @@ var ResetBeforeCacheController = class extends Controller {
|
|
|
17692
18173
|
}
|
|
17693
18174
|
/** The scan root: a `scope` descendant when set, else the controller element. */
|
|
17694
18175
|
#scopeRoot() {
|
|
17695
|
-
if (!this
|
|
17696
|
-
return this.element.querySelector(this
|
|
18176
|
+
if (!this.#scopeSelector) return this.element;
|
|
18177
|
+
return this.element.querySelector(this.#scopeSelector) ?? this.element;
|
|
17697
18178
|
}
|
|
17698
18179
|
};
|
|
18180
|
+
var DEFAULT_MIN2 = 0;
|
|
18181
|
+
var DEFAULT_MAX2 = 100;
|
|
18182
|
+
var DEFAULT_VALUE = 50;
|
|
18183
|
+
var DEFAULT_STEP = 1;
|
|
17699
18184
|
var ResizableController = class extends Controller {
|
|
17700
18185
|
static targets = ["primary", "secondary", "separator"];
|
|
17701
18186
|
static values = {
|
|
17702
|
-
min: { type: Number, default:
|
|
17703
|
-
max: { type: Number, default:
|
|
17704
|
-
step: { type: Number, default:
|
|
17705
|
-
value: { type: Number, default:
|
|
18187
|
+
min: { type: Number, default: DEFAULT_MIN2 },
|
|
18188
|
+
max: { type: Number, default: DEFAULT_MAX2 },
|
|
18189
|
+
step: { type: Number, default: DEFAULT_STEP },
|
|
18190
|
+
value: { type: Number, default: DEFAULT_VALUE }
|
|
17706
18191
|
};
|
|
17707
18192
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
17708
18193
|
static events = ["change"];
|
|
17709
|
-
/**
|
|
17710
|
-
#valueBeforeCollapse =
|
|
18194
|
+
/** Where the divider sat before the current collapse; `null` when unknown. */
|
|
18195
|
+
#valueBeforeCollapse = null;
|
|
17711
18196
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
17712
18197
|
#dragAbort = null;
|
|
17713
18198
|
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
17714
18199
|
#paneTabindex = new TabindexLoan();
|
|
17715
18200
|
/** Releases the pane cycle listener bound in {@link connect}. */
|
|
17716
18201
|
#cycleAbort = null;
|
|
18202
|
+
/** Folds the range and position inputs of one morph batch into a single paint. */
|
|
18203
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
17717
18204
|
connect() {
|
|
17718
|
-
this
|
|
18205
|
+
this.element.removeAttribute("data-dragging");
|
|
18206
|
+
this.#repaint.activate();
|
|
18207
|
+
this.#render();
|
|
17719
18208
|
this.#cycleAbort = new AbortController();
|
|
17720
18209
|
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
17721
18210
|
signal: this.#cycleAbort.signal
|
|
@@ -17727,17 +18216,19 @@ var ResizableController = class extends Controller {
|
|
|
17727
18216
|
this.#dragAbort = null;
|
|
17728
18217
|
this.#cycleAbort?.abort();
|
|
17729
18218
|
this.#cycleAbort = null;
|
|
18219
|
+
this.#repaint.cancel();
|
|
17730
18220
|
this.#paneTabindex.returnAll();
|
|
18221
|
+
this.element.removeAttribute("data-dragging");
|
|
17731
18222
|
}
|
|
17732
18223
|
/** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
|
|
17733
18224
|
#onCycleKeydown = (event) => {
|
|
17734
18225
|
if (event.key !== "F6") return;
|
|
18226
|
+
if (event.isComposing) return;
|
|
17735
18227
|
if (event.defaultPrevented) return;
|
|
17736
18228
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
17737
18229
|
const panes = [];
|
|
17738
18230
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
17739
18231
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
17740
|
-
if (panes.length === 0) return;
|
|
17741
18232
|
const target = event.target;
|
|
17742
18233
|
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
17743
18234
|
const next = panes[(current + 1) % panes.length];
|
|
@@ -17746,12 +18237,21 @@ var ResizableController = class extends Controller {
|
|
|
17746
18237
|
this.#paneTabindex.lend(next);
|
|
17747
18238
|
next.focus();
|
|
17748
18239
|
};
|
|
17749
|
-
/**
|
|
17750
|
-
* Stimulus lifecycle callback when the valueValue changes.
|
|
17751
|
-
* Keeps CSS fractions and ARIA status completely aligned.
|
|
17752
|
-
*/
|
|
18240
|
+
/** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */
|
|
17753
18241
|
valueValueChanged() {
|
|
17754
|
-
this.#
|
|
18242
|
+
this.#repaint.schedule();
|
|
18243
|
+
}
|
|
18244
|
+
/** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */
|
|
18245
|
+
minValueChanged() {
|
|
18246
|
+
this.#repaint.schedule();
|
|
18247
|
+
}
|
|
18248
|
+
/** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */
|
|
18249
|
+
maxValueChanged() {
|
|
18250
|
+
this.#repaint.schedule();
|
|
18251
|
+
}
|
|
18252
|
+
/** Re-publishes range and position onto a separator swapped in after connect. */
|
|
18253
|
+
separatorTargetConnected() {
|
|
18254
|
+
this.#repaint.schedule();
|
|
17755
18255
|
}
|
|
17756
18256
|
/** Starts active pointer drag tracking and locks capture. */
|
|
17757
18257
|
onPointerDown(event) {
|
|
@@ -17772,44 +18272,44 @@ var ResizableController = class extends Controller {
|
|
|
17772
18272
|
onKeydown(event) {
|
|
17773
18273
|
if (isReservedArrowChord(event)) return;
|
|
17774
18274
|
if (!this.hasSeparatorTarget) return;
|
|
17775
|
-
const
|
|
17776
|
-
const isVertical =
|
|
18275
|
+
const { min, max } = this.#range;
|
|
18276
|
+
const isVertical = this.#isVertical;
|
|
17777
18277
|
let handled = true;
|
|
17778
|
-
let nextValue = this
|
|
18278
|
+
let nextValue = this.#position;
|
|
17779
18279
|
switch (event.key) {
|
|
17780
18280
|
case "ArrowLeft":
|
|
17781
18281
|
if (isVertical) {
|
|
17782
|
-
nextValue -= this
|
|
18282
|
+
nextValue -= this.#step;
|
|
17783
18283
|
} else {
|
|
17784
18284
|
handled = false;
|
|
17785
18285
|
}
|
|
17786
18286
|
break;
|
|
17787
18287
|
case "ArrowRight":
|
|
17788
18288
|
if (isVertical) {
|
|
17789
|
-
nextValue += this
|
|
18289
|
+
nextValue += this.#step;
|
|
17790
18290
|
} else {
|
|
17791
18291
|
handled = false;
|
|
17792
18292
|
}
|
|
17793
18293
|
break;
|
|
17794
18294
|
case "ArrowUp":
|
|
17795
18295
|
if (!isVertical) {
|
|
17796
|
-
nextValue -= this
|
|
18296
|
+
nextValue -= this.#step;
|
|
17797
18297
|
} else {
|
|
17798
18298
|
handled = false;
|
|
17799
18299
|
}
|
|
17800
18300
|
break;
|
|
17801
18301
|
case "ArrowDown":
|
|
17802
18302
|
if (!isVertical) {
|
|
17803
|
-
nextValue += this
|
|
18303
|
+
nextValue += this.#step;
|
|
17804
18304
|
} else {
|
|
17805
18305
|
handled = false;
|
|
17806
18306
|
}
|
|
17807
18307
|
break;
|
|
17808
18308
|
case "Home":
|
|
17809
|
-
nextValue =
|
|
18309
|
+
nextValue = min;
|
|
17810
18310
|
break;
|
|
17811
18311
|
case "End":
|
|
17812
|
-
nextValue =
|
|
18312
|
+
nextValue = max;
|
|
17813
18313
|
break;
|
|
17814
18314
|
case "Enter":
|
|
17815
18315
|
event.preventDefault();
|
|
@@ -17821,65 +18321,86 @@ var ResizableController = class extends Controller {
|
|
|
17821
18321
|
}
|
|
17822
18322
|
if (handled) {
|
|
17823
18323
|
event.preventDefault();
|
|
17824
|
-
this.valueValue = Math.max(
|
|
17825
|
-
this.#
|
|
18324
|
+
this.valueValue = Math.max(min, Math.min(nextValue, max));
|
|
18325
|
+
this.#render();
|
|
17826
18326
|
this.#dispatchChange();
|
|
17827
18327
|
}
|
|
17828
18328
|
}
|
|
17829
|
-
/** Double-click or Enter to collapse
|
|
18329
|
+
/** Double-click or Enter to collapse the primary pane, or put it back. */
|
|
17830
18330
|
toggle() {
|
|
17831
|
-
const
|
|
17832
|
-
if (this
|
|
17833
|
-
this.#valueBeforeCollapse = this
|
|
17834
|
-
this.valueValue =
|
|
18331
|
+
const { min, max } = this.#range;
|
|
18332
|
+
if (this.#position > min) {
|
|
18333
|
+
this.#valueBeforeCollapse = this.#position;
|
|
18334
|
+
this.valueValue = min;
|
|
17835
18335
|
} else {
|
|
17836
|
-
this.valueValue = this.#valueBeforeCollapse
|
|
18336
|
+
this.valueValue = this.#valueBeforeCollapse ?? max;
|
|
18337
|
+
this.#valueBeforeCollapse = null;
|
|
17837
18338
|
}
|
|
17838
|
-
this.#
|
|
18339
|
+
this.#render();
|
|
17839
18340
|
this.#dispatchChange();
|
|
17840
18341
|
}
|
|
17841
18342
|
#onPointerMove = (event) => {
|
|
17842
18343
|
if (!this.hasSeparatorTarget) return;
|
|
17843
18344
|
const rect = this.element.getBoundingClientRect();
|
|
17844
|
-
const
|
|
17845
|
-
const
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
} else {
|
|
17850
|
-
fraction = (event.clientY - rect.top) / rect.height;
|
|
17851
|
-
}
|
|
17852
|
-
fraction = Math.max(0, Math.min(fraction, 1));
|
|
17853
|
-
const percent = Math.round(fraction * 100);
|
|
17854
|
-
this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));
|
|
17855
|
-
this.#clampAndSync();
|
|
18345
|
+
const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
|
|
18346
|
+
const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
|
|
18347
|
+
const { min, max } = this.#range;
|
|
18348
|
+
this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
|
|
18349
|
+
this.#render();
|
|
17856
18350
|
};
|
|
17857
18351
|
#onPointerUp = (event) => {
|
|
17858
|
-
if (!this.hasSeparatorTarget) return;
|
|
17859
|
-
const separator = this.separatorTarget;
|
|
17860
|
-
separator.releasePointerCapture(event.pointerId);
|
|
17861
18352
|
this.element.removeAttribute("data-dragging");
|
|
17862
18353
|
this.#dragAbort?.abort();
|
|
17863
18354
|
this.#dragAbort = null;
|
|
18355
|
+
if (this.hasSeparatorTarget) {
|
|
18356
|
+
this.separatorTarget.releasePointerCapture(event.pointerId);
|
|
18357
|
+
}
|
|
17864
18358
|
this.#dispatchChange();
|
|
17865
18359
|
};
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
18360
|
+
/**
|
|
18361
|
+
* Publishes the position: the fraction consumer CSS multiplies a pane by, and
|
|
18362
|
+
* the range assistive tech reads off the separator. The clamped position is
|
|
18363
|
+
* written back to the Value so it, ARIA, CSS, and dispatched events agree.
|
|
18364
|
+
*
|
|
18365
|
+
* @stimeoRenderRoot
|
|
18366
|
+
*/
|
|
18367
|
+
#render() {
|
|
18368
|
+
const { min, max } = this.#range;
|
|
18369
|
+
const position = this.#position;
|
|
18370
|
+
if (this.valueValue !== position) {
|
|
18371
|
+
this.valueValue = position;
|
|
17870
18372
|
}
|
|
17871
|
-
|
|
17872
|
-
this.element.style.setProperty("--stimeo--resizable-fraction", String(fraction));
|
|
18373
|
+
this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
|
|
17873
18374
|
if (this.hasSeparatorTarget) {
|
|
17874
|
-
this.separatorTarget.setAttribute("aria-valuenow", String(
|
|
17875
|
-
this.separatorTarget.setAttribute("aria-valuemin", String(
|
|
17876
|
-
this.separatorTarget.setAttribute("aria-valuemax", String(
|
|
18375
|
+
this.separatorTarget.setAttribute("aria-valuenow", String(position));
|
|
18376
|
+
this.separatorTarget.setAttribute("aria-valuemin", String(min));
|
|
18377
|
+
this.separatorTarget.setAttribute("aria-valuemax", String(max));
|
|
17877
18378
|
}
|
|
17878
18379
|
}
|
|
17879
18380
|
#dispatchChange() {
|
|
17880
18381
|
const fraction = this.valueValue / 100;
|
|
17881
18382
|
this.dispatch("change", { detail: { value: this.valueValue, fraction } });
|
|
17882
18383
|
}
|
|
18384
|
+
/** The declared range after validation; an unreadable bound uses its default. */
|
|
18385
|
+
get #range() {
|
|
18386
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN2;
|
|
18387
|
+
const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX2;
|
|
18388
|
+
return { min, max: Math.max(min, max) };
|
|
18389
|
+
}
|
|
18390
|
+
/** The declared position, clamped into the validated range. */
|
|
18391
|
+
get #position() {
|
|
18392
|
+
const { min, max } = this.#range;
|
|
18393
|
+
const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;
|
|
18394
|
+
return Math.max(min, Math.min(value, max));
|
|
18395
|
+
}
|
|
18396
|
+
/** The keyboard increment; a non-positive or unreadable one uses the default. */
|
|
18397
|
+
get #step() {
|
|
18398
|
+
return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;
|
|
18399
|
+
}
|
|
18400
|
+
/** Whether the divider runs vertically; a separator that does not say is horizontal. */
|
|
18401
|
+
get #isVertical() {
|
|
18402
|
+
return this.hasSeparatorTarget && this.separatorTarget.getAttribute("aria-orientation") === "vertical";
|
|
18403
|
+
}
|
|
17883
18404
|
};
|
|
17884
18405
|
var RovingController = class extends Controller {
|
|
17885
18406
|
static targets = ["item"];
|
|
@@ -18973,16 +19494,16 @@ var ScrollspyController = class extends Controller {
|
|
|
18973
19494
|
return value.substring(1) || null;
|
|
18974
19495
|
}
|
|
18975
19496
|
};
|
|
18976
|
-
var
|
|
18977
|
-
var
|
|
19497
|
+
var DEFAULT_MIN3 = 0;
|
|
19498
|
+
var DEFAULT_MAX3 = 100;
|
|
18978
19499
|
var SeparatorController = class extends Controller {
|
|
18979
19500
|
static values = {
|
|
18980
19501
|
orientation: { type: String, default: "horizontal" },
|
|
18981
19502
|
focusable: { type: Boolean, default: false },
|
|
18982
|
-
min: { type: Number, default:
|
|
18983
|
-
max: { type: Number, default:
|
|
19503
|
+
min: { type: Number, default: DEFAULT_MIN3 },
|
|
19504
|
+
max: { type: Number, default: DEFAULT_MAX3 },
|
|
18984
19505
|
step: { type: Number, default: 1 },
|
|
18985
|
-
value: { type: Number, default:
|
|
19506
|
+
value: { type: Number, default: DEFAULT_MIN3 }
|
|
18986
19507
|
};
|
|
18987
19508
|
static actions = ["onKeydown"];
|
|
18988
19509
|
static events = ["change"];
|
|
@@ -19103,8 +19624,8 @@ var SeparatorController = class extends Controller {
|
|
|
19103
19624
|
}
|
|
19104
19625
|
/** Finite, ordered range shared by rendering and keyboard stepping. */
|
|
19105
19626
|
get #effectiveRange() {
|
|
19106
|
-
const min = Number.isFinite(this.minValue) ? this.minValue :
|
|
19107
|
-
const candidateMax = Number.isFinite(this.maxValue) ? this.maxValue :
|
|
19627
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN3;
|
|
19628
|
+
const candidateMax = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX3;
|
|
19108
19629
|
return {
|
|
19109
19630
|
min,
|
|
19110
19631
|
max: Math.max(min, candidateMax),
|