stimeo-ui 0.4.0 → 0.5.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 +68 -0
- data/dist/controllers/aspect_ratio_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +5 -1
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +6 -2
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +5 -1
- data/dist/controllers/date_range_picker_controller.js +5 -1
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/empty_state_controller.js +107 -16
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +94 -22
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +2 -0
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +3 -1
- data/dist/controllers/network_status_controller.js +1 -3
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +1 -1
- data/dist/controllers/pagination_controller.js +5 -1
- data/dist/controllers/password_strength_controller.js +1 -1
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +7 -3
- data/dist/controllers/range_slider_controller.js +385 -94
- data/dist/controllers/rating_controller.js +2 -0
- data/dist/controllers/relative_time_controller.js +2 -0
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/skeleton_controller.js +71 -3
- data/dist/controllers/slider_controller.js +325 -48
- data/dist/controllers/spinner_controller.js +18 -3
- data/dist/controllers/step_indicator_controller.js +3 -1
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +6 -3
- data/dist/controllers/tree_view_controller.js +19 -1
- data/dist/index.js +1214 -307
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
data/dist/index.js
CHANGED
|
@@ -857,7 +857,7 @@ var AspectRatioController = class extends Controller {
|
|
|
857
857
|
};
|
|
858
858
|
/** Applies the ratio on connect and whenever the value changes. */
|
|
859
859
|
ratioValueChanged() {
|
|
860
|
-
this.element.style.setProperty("--stimeo
|
|
860
|
+
this.element.style.setProperty("--stimeo--aspect-ratio", this.#normalizeRatio(this.ratioValue));
|
|
861
861
|
}
|
|
862
862
|
/**
|
|
863
863
|
* Normalizes a ratio string to a valid CSS `<ratio>`:
|
|
@@ -1346,7 +1346,11 @@ var BreadcrumbController = class extends Controller {
|
|
|
1346
1346
|
this.ellipsisTarget.hidden = true;
|
|
1347
1347
|
return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;
|
|
1348
1348
|
}
|
|
1349
|
-
/**
|
|
1349
|
+
/**
|
|
1350
|
+
* Applies the collapsed/expanded state to the items, ellipsis, and trigger.
|
|
1351
|
+
*
|
|
1352
|
+
* @stimeoRenderRoot
|
|
1353
|
+
*/
|
|
1350
1354
|
#render() {
|
|
1351
1355
|
const collapsed = this.#overflowing && !this.#expanded;
|
|
1352
1356
|
const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;
|
|
@@ -2078,7 +2082,11 @@ var CarouselController = class extends Controller {
|
|
|
2078
2082
|
this.#syncTimer();
|
|
2079
2083
|
if (changed) this.dispatch("change", { detail: { index, total: this.slideTargets.length } });
|
|
2080
2084
|
}
|
|
2081
|
-
/**
|
|
2085
|
+
/**
|
|
2086
|
+
* Reflects `this.#index` onto slides and pickers (state hooks + roving).
|
|
2087
|
+
*
|
|
2088
|
+
* @stimeoRenderRoot
|
|
2089
|
+
*/
|
|
2082
2090
|
#render({ focus }) {
|
|
2083
2091
|
this.slideTargets.forEach((slide, i) => {
|
|
2084
2092
|
const active = i === this.#index;
|
|
@@ -2287,6 +2295,15 @@ var CheckboxController = class extends Controller {
|
|
|
2287
2295
|
return "partial";
|
|
2288
2296
|
}
|
|
2289
2297
|
};
|
|
2298
|
+
|
|
2299
|
+
// src/utils/default_attribute.ts
|
|
2300
|
+
function setDefaultAttribute(element, name, value) {
|
|
2301
|
+
if (element.hasAttribute(name)) return false;
|
|
2302
|
+
element.setAttribute(name, value);
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// src/controllers/clipboard_controller.ts
|
|
2290
2307
|
var ClipboardController = class extends Controller {
|
|
2291
2308
|
static targets = ["source", "button", "feedback"];
|
|
2292
2309
|
static values = {
|
|
@@ -2306,9 +2323,7 @@ var ClipboardController = class extends Controller {
|
|
|
2306
2323
|
*/
|
|
2307
2324
|
#resetTimerId = null;
|
|
2308
2325
|
connect() {
|
|
2309
|
-
|
|
2310
|
-
this.element.setAttribute("data-state", "idle");
|
|
2311
|
-
}
|
|
2326
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
2312
2327
|
}
|
|
2313
2328
|
disconnect() {
|
|
2314
2329
|
this.#timers.clearAll();
|
|
@@ -2615,7 +2630,10 @@ var CollapsibleController = class extends Controller {
|
|
|
2615
2630
|
#applyContent(content, open, waitForCloseTransition) {
|
|
2616
2631
|
if (open) {
|
|
2617
2632
|
content.hidden = false;
|
|
2618
|
-
content.style.setProperty(
|
|
2633
|
+
content.style.setProperty(
|
|
2634
|
+
"--stimeo--collapsible-content-height",
|
|
2635
|
+
`${content.scrollHeight}px`
|
|
2636
|
+
);
|
|
2619
2637
|
content.setAttribute("data-state", "open");
|
|
2620
2638
|
return;
|
|
2621
2639
|
}
|
|
@@ -2648,7 +2666,7 @@ function toFiniteNumber(raw) {
|
|
|
2648
2666
|
}
|
|
2649
2667
|
|
|
2650
2668
|
// src/controllers/color_picker_controller.ts
|
|
2651
|
-
var COLOR_PROPERTY = "--stimeo
|
|
2669
|
+
var COLOR_PROPERTY = "--stimeo--color";
|
|
2652
2670
|
var CHANNEL_RANGE = {
|
|
2653
2671
|
hue: [0, 360],
|
|
2654
2672
|
saturation: [0, 100],
|
|
@@ -2777,7 +2795,11 @@ var ColorPickerController = class extends Controller {
|
|
|
2777
2795
|
this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));
|
|
2778
2796
|
this.#render();
|
|
2779
2797
|
}
|
|
2780
|
-
/**
|
|
2798
|
+
/**
|
|
2799
|
+
* Reflects the model onto sliders, the hex input, preview, and form field.
|
|
2800
|
+
*
|
|
2801
|
+
* @stimeoRenderRoot
|
|
2802
|
+
*/
|
|
2781
2803
|
#render() {
|
|
2782
2804
|
for (const slider of this.sliderTargets) {
|
|
2783
2805
|
const channel = this.#channelOf(slider);
|
|
@@ -3904,8 +3926,8 @@ var ContextMenuController = class extends Controller {
|
|
|
3904
3926
|
onDismiss: () => this.#closeAndRestore(),
|
|
3905
3927
|
claims: claimsWhileFocusWithin(this.element)
|
|
3906
3928
|
});
|
|
3907
|
-
this.menuTarget.style.setProperty("--stimeo
|
|
3908
|
-
this.menuTarget.style.setProperty("--stimeo
|
|
3929
|
+
this.menuTarget.style.setProperty("--stimeo--context-menu-x", `${x}px`);
|
|
3930
|
+
this.menuTarget.style.setProperty("--stimeo--context-menu-y", `${y}px`);
|
|
3909
3931
|
this.menuTarget.hidden = false;
|
|
3910
3932
|
if (this.hasRegionTarget) this.regionTarget.setAttribute("data-state", "open");
|
|
3911
3933
|
this.#navigableItems[0]?.focus();
|
|
@@ -4248,7 +4270,11 @@ var CountdownController = class extends Controller {
|
|
|
4248
4270
|
const raw = this.#isDown ? this.#reference - now : now - this.#reference;
|
|
4249
4271
|
return Math.max(0, raw);
|
|
4250
4272
|
}
|
|
4251
|
-
/**
|
|
4273
|
+
/**
|
|
4274
|
+
* Writes the amount into the day/hour/minute/second slots.
|
|
4275
|
+
*
|
|
4276
|
+
* @stimeoRenderRoot
|
|
4277
|
+
*/
|
|
4252
4278
|
#render(amount) {
|
|
4253
4279
|
const totalSeconds = Math.floor(amount / SECOND_MS);
|
|
4254
4280
|
this.#renderedAmount = totalSeconds * SECOND_MS;
|
|
@@ -4853,7 +4879,11 @@ var DateRangePickerController = class extends Controller {
|
|
|
4853
4879
|
this.#focusedDate = target;
|
|
4854
4880
|
this.#render();
|
|
4855
4881
|
}
|
|
4856
|
-
/**
|
|
4882
|
+
/**
|
|
4883
|
+
* Builds the six-week grid and binds range/roving/disabled state per cell.
|
|
4884
|
+
*
|
|
4885
|
+
* @stimeoRenderRoot
|
|
4886
|
+
*/
|
|
4857
4887
|
#render() {
|
|
4858
4888
|
const info = parseISOMonthString(this.#viewMonth);
|
|
4859
4889
|
if (!info) return;
|
|
@@ -5079,7 +5109,7 @@ var DirectUploadController = class extends Controller {
|
|
|
5079
5109
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
5080
5110
|
row.setAttribute("aria-valuenow", String(clamped));
|
|
5081
5111
|
row.setAttribute("aria-valuetext", `${clamped}%`);
|
|
5082
|
-
row.style.setProperty("--stimeo
|
|
5112
|
+
row.style.setProperty("--stimeo--upload-progress", `${clamped}%`);
|
|
5083
5113
|
this.#setField(row, "percent", `${clamped}%`);
|
|
5084
5114
|
this.#syncAggregate();
|
|
5085
5115
|
this.dispatch("progress", { detail: { id, percent: clamped } });
|
|
@@ -5117,7 +5147,7 @@ var DirectUploadController = class extends Controller {
|
|
|
5117
5147
|
}
|
|
5118
5148
|
clone.setAttribute("aria-valuenow", "0");
|
|
5119
5149
|
clone.setAttribute("data-upload-state", "uploading");
|
|
5120
|
-
clone.style.setProperty("--stimeo
|
|
5150
|
+
clone.style.setProperty("--stimeo--upload-progress", "0%");
|
|
5121
5151
|
this.listTarget.appendChild(clone);
|
|
5122
5152
|
this.#rows.set(key, clone);
|
|
5123
5153
|
return clone;
|
|
@@ -5141,7 +5171,7 @@ var DirectUploadController = class extends Controller {
|
|
|
5141
5171
|
}
|
|
5142
5172
|
const overall = Math.round(total / this.#rows.size);
|
|
5143
5173
|
this.element.setAttribute("data-upload-progress", String(overall));
|
|
5144
|
-
this.element.style.setProperty("--stimeo
|
|
5174
|
+
this.element.style.setProperty("--stimeo--upload-progress", `${overall}%`);
|
|
5145
5175
|
}
|
|
5146
5176
|
/** Writes a consumer label (with `%{name}` substituted) to the status region. */
|
|
5147
5177
|
#announce(label, row) {
|
|
@@ -5787,23 +5817,120 @@ var EmptyStateController = class extends Controller {
|
|
|
5787
5817
|
};
|
|
5788
5818
|
static events = ["change"];
|
|
5789
5819
|
#observer = null;
|
|
5820
|
+
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
5821
|
+
#connected = false;
|
|
5790
5822
|
/** Last applied empty state; `null` until the first sync so connect emits nothing. */
|
|
5791
5823
|
#empty = null;
|
|
5792
5824
|
connect() {
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
this.#observer.observe(this.listTarget, { childList: true });
|
|
5797
|
-
}
|
|
5798
|
-
this.#apply();
|
|
5825
|
+
this.#connected = true;
|
|
5826
|
+
this.#syncObservation();
|
|
5827
|
+
this.#update();
|
|
5799
5828
|
}
|
|
5800
5829
|
disconnect() {
|
|
5830
|
+
this.#connected = false;
|
|
5831
|
+
this.#stopObserving();
|
|
5832
|
+
}
|
|
5833
|
+
/** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */
|
|
5834
|
+
listTargetConnected() {
|
|
5835
|
+
this.#resync();
|
|
5836
|
+
}
|
|
5837
|
+
/** Releases the observation when the `list` element leaves the target set. */
|
|
5838
|
+
listTargetDisconnected() {
|
|
5839
|
+
this.#resync();
|
|
5840
|
+
}
|
|
5841
|
+
/** Syncs an `empty` element that arrives — or is replaced — at runtime. */
|
|
5842
|
+
emptyTargetConnected() {
|
|
5843
|
+
this.#resync();
|
|
5844
|
+
}
|
|
5845
|
+
/**
|
|
5846
|
+
* Syncs the `empty` element that remains when one leaves the target set. A
|
|
5847
|
+
* single-target getter resolves to the first `empty` element in document order,
|
|
5848
|
+
* so a swap that inserts the replacement *before* removing the original (Turbo
|
|
5849
|
+
* Stream `after` / `before` / `append` followed by `remove`) leaves the
|
|
5850
|
+
* replacement untouched until the original goes — this callback is that moment.
|
|
5851
|
+
*/
|
|
5852
|
+
emptyTargetDisconnected() {
|
|
5853
|
+
this.#resync();
|
|
5854
|
+
}
|
|
5855
|
+
/** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */
|
|
5856
|
+
itemSelectorValueChanged() {
|
|
5857
|
+
this.#resync();
|
|
5858
|
+
}
|
|
5859
|
+
/**
|
|
5860
|
+
* Re-points the observation and re-renders after a target or selector change.
|
|
5861
|
+
* The `#connected` guard is load-bearing: Stimulus runs value and target
|
|
5862
|
+
* callbacks for the initial markup *before* `connect()` and runs target
|
|
5863
|
+
* callbacks during teardown *after* `disconnect()`, and re-observing there
|
|
5864
|
+
* would outlive the controller.
|
|
5865
|
+
*/
|
|
5866
|
+
#resync() {
|
|
5867
|
+
if (!this.#connected) return;
|
|
5868
|
+
this.#syncObservation();
|
|
5869
|
+
this.#update();
|
|
5870
|
+
}
|
|
5871
|
+
/**
|
|
5872
|
+
* Points the mutation observation at the current `list` target — re-resolved on
|
|
5873
|
+
* every sync rather than captured at connect, so an element swapped in at
|
|
5874
|
+
* runtime is observed instead of the detached original.
|
|
5875
|
+
*
|
|
5876
|
+
* The observation covers exactly what the count predicate reads. With no
|
|
5877
|
+
* `itemSelector` the count is the child element count, which only `childList`
|
|
5878
|
+
* can change. With one, the predicate reads the children themselves, so
|
|
5879
|
+
* attribute and descendant mutations are watched too — and the controller's own
|
|
5880
|
+
* writes are filtered back out, or toggling `hidden` would re-enter the render.
|
|
5881
|
+
*/
|
|
5882
|
+
#syncObservation() {
|
|
5883
|
+
this.#stopObserving();
|
|
5884
|
+
if (!this.hasListTarget || typeof MutationObserver === "undefined") return;
|
|
5885
|
+
const watchesItems = this.itemSelectorValue.length > 0;
|
|
5886
|
+
this.#observer = new MutationObserver((records) => {
|
|
5887
|
+
if (records.some((record) => this.#affectsCount(record))) this.#update();
|
|
5888
|
+
});
|
|
5889
|
+
this.#observer.observe(this.listTarget, {
|
|
5890
|
+
childList: true,
|
|
5891
|
+
subtree: watchesItems,
|
|
5892
|
+
attributes: watchesItems
|
|
5893
|
+
});
|
|
5894
|
+
}
|
|
5895
|
+
#stopObserving() {
|
|
5801
5896
|
this.#observer?.disconnect();
|
|
5802
5897
|
this.#observer = null;
|
|
5803
5898
|
}
|
|
5804
|
-
/**
|
|
5805
|
-
|
|
5806
|
-
|
|
5899
|
+
/**
|
|
5900
|
+
* Whether a mutation can change the item count. An attribute written on a
|
|
5901
|
+
* target this controller owns is its own echo — `hidden` on `list` / `empty`,
|
|
5902
|
+
* and the hooks on the controller element when the list *is* that element.
|
|
5903
|
+
*/
|
|
5904
|
+
#affectsCount(record) {
|
|
5905
|
+
if (record.type !== "attributes") return true;
|
|
5906
|
+
const own = record.target === this.listTarget || this.hasEmptyTarget && record.target === this.emptyTarget;
|
|
5907
|
+
return !own;
|
|
5908
|
+
}
|
|
5909
|
+
/**
|
|
5910
|
+
* Renders the count, then reports a crossed boundary. The announcement copy is
|
|
5911
|
+
* read here rather than while rendering: it is the wording of the report, not
|
|
5912
|
+
* an input to what is displayed.
|
|
5913
|
+
*/
|
|
5914
|
+
#update() {
|
|
5915
|
+
const count = this.#render();
|
|
5916
|
+
if (count === null) return;
|
|
5917
|
+
const empty = count === 0;
|
|
5918
|
+
const crossed = this.#empty !== null && empty !== this.#empty;
|
|
5919
|
+
this.#empty = empty;
|
|
5920
|
+
if (!crossed) return;
|
|
5921
|
+
this.dispatch("change", { detail: { count, empty } });
|
|
5922
|
+
announce(
|
|
5923
|
+
fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
|
|
5924
|
+
);
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* Syncs visibility and the state hooks to the current item count, and returns
|
|
5928
|
+
* it. `null` when there is no `list` target to count.
|
|
5929
|
+
*
|
|
5930
|
+
* @stimeoRenderRoot
|
|
5931
|
+
*/
|
|
5932
|
+
#render() {
|
|
5933
|
+
if (!this.hasListTarget) return null;
|
|
5807
5934
|
const count = this.#count();
|
|
5808
5935
|
const empty = count === 0;
|
|
5809
5936
|
this.element.setAttribute("data-count", String(count));
|
|
@@ -5814,13 +5941,7 @@ var EmptyStateController = class extends Controller {
|
|
|
5814
5941
|
}
|
|
5815
5942
|
this.listTarget.hidden = empty;
|
|
5816
5943
|
if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;
|
|
5817
|
-
|
|
5818
|
-
this.dispatch("change", { detail: { count, empty } });
|
|
5819
|
-
announce(
|
|
5820
|
-
fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
|
|
5821
|
-
);
|
|
5822
|
-
}
|
|
5823
|
-
this.#empty = empty;
|
|
5944
|
+
return count;
|
|
5824
5945
|
}
|
|
5825
5946
|
/** Item count: element children matching `itemSelector`, or all element children. */
|
|
5826
5947
|
#count() {
|
|
@@ -6083,29 +6204,119 @@ var FlashController = class extends Controller {
|
|
|
6083
6204
|
static events = ["show", "dismiss"];
|
|
6084
6205
|
#timers = new SafeTimeout();
|
|
6085
6206
|
#observer = null;
|
|
6207
|
+
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
6208
|
+
#connected = false;
|
|
6086
6209
|
/** Auto-dismiss timer state keyed by message element. */
|
|
6087
6210
|
#state = /* @__PURE__ */ new Map();
|
|
6088
6211
|
/** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
|
|
6089
6212
|
#order = [];
|
|
6090
|
-
|
|
6091
|
-
|
|
6213
|
+
/**
|
|
6214
|
+
* Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}
|
|
6215
|
+
* releases the bookkeeping above *before* the transition wait, so for that window the
|
|
6216
|
+
* element is in the DOM but in neither collection — without this set a re-scan would
|
|
6217
|
+
* read it as a brand-new flash and show it a second time.
|
|
6218
|
+
*/
|
|
6219
|
+
#leaving = /* @__PURE__ */ new Set();
|
|
6220
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
6221
|
+
#onEnter = (event) => this.#pause(event.currentTarget, event.type === "focusin" ? "focus" : "hover");
|
|
6222
|
+
#onLeave = (event) => this.#resume(event.currentTarget, event.type === "focusout" ? "focus" : "hover");
|
|
6092
6223
|
connect() {
|
|
6093
|
-
|
|
6224
|
+
this.#connected = true;
|
|
6094
6225
|
for (const message of this.messageTargets) {
|
|
6095
|
-
this.#process(message, true);
|
|
6096
|
-
}
|
|
6097
|
-
if (typeof MutationObserver !== "undefined") {
|
|
6098
|
-
this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));
|
|
6099
|
-
this.#observer.observe(this.regionTarget, { childList: true, subtree: true });
|
|
6226
|
+
if (this.#owns(message)) this.#process(message, true);
|
|
6100
6227
|
}
|
|
6228
|
+
this.#syncObservation();
|
|
6229
|
+
this.#beforeCache.activate();
|
|
6101
6230
|
}
|
|
6102
6231
|
disconnect() {
|
|
6103
|
-
this.#
|
|
6104
|
-
this.#
|
|
6232
|
+
this.#connected = false;
|
|
6233
|
+
this.#beforeCache.deactivate();
|
|
6234
|
+
this.#stopObserving();
|
|
6105
6235
|
this.#timers.clearAll();
|
|
6106
6236
|
for (const message of this.#order) this.#unbindPause(message);
|
|
6107
6237
|
this.#state.clear();
|
|
6108
6238
|
this.#order.length = 0;
|
|
6239
|
+
this.#leaving.clear();
|
|
6240
|
+
}
|
|
6241
|
+
/**
|
|
6242
|
+
* Takes the managed flashes out of the page just before Turbo freezes it, so a
|
|
6243
|
+
* restored snapshot carries no notification the visitor has already received: the
|
|
6244
|
+
* fresh `connect()` there reads a leftover flash as a brand-new one and announces it
|
|
6245
|
+
* a second time. A message that never auto-dismisses (`duration: 0`) is one of these
|
|
6246
|
+
* too — that value governs the timer, not what belongs in a cached page. Removal
|
|
6247
|
+
* only: `dismiss` reports a dismissal, and freezing the page is not one.
|
|
6248
|
+
*/
|
|
6249
|
+
#rewindForCache() {
|
|
6250
|
+
for (const message of [...this.#order]) {
|
|
6251
|
+
message.remove();
|
|
6252
|
+
this.#forget(message);
|
|
6253
|
+
}
|
|
6254
|
+
for (const message of this.#leaving) message.remove();
|
|
6255
|
+
this.#leaving.clear();
|
|
6256
|
+
}
|
|
6257
|
+
/** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */
|
|
6258
|
+
regionTargetConnected() {
|
|
6259
|
+
this.#resync();
|
|
6260
|
+
}
|
|
6261
|
+
/** Releases the observation when the `region` element leaves the target set. */
|
|
6262
|
+
regionTargetDisconnected() {
|
|
6263
|
+
this.#resync();
|
|
6264
|
+
}
|
|
6265
|
+
/**
|
|
6266
|
+
* Whether this controller owns `message`. Ownership is the current `region`'s
|
|
6267
|
+
* subtree: a message target anywhere else in the controller's scope is the
|
|
6268
|
+
* consumer's, and so is one in a region that has gone away. The initial scan, a
|
|
6269
|
+
* re-scan after a `region` swap, and a departure from the target set all resolve
|
|
6270
|
+
* ownership through this one test; the observation gets it structurally, by watching
|
|
6271
|
+
* that subtree and nothing else.
|
|
6272
|
+
*/
|
|
6273
|
+
#owns(message) {
|
|
6274
|
+
return this.hasRegionTarget && this.regionTarget.contains(message);
|
|
6275
|
+
}
|
|
6276
|
+
/**
|
|
6277
|
+
* Releases a message that left the target set (a Turbo Stream `remove`, the consumer
|
|
6278
|
+
* detaching the node, or a morph that rewrote the target attribute in place): it
|
|
6279
|
+
* stops occupying a `max` slot, and both its pending auto-dismiss and an already
|
|
6280
|
+
* scheduled removal are cancelled. A move *within* the region keeps all of them —
|
|
6281
|
+
* which is why the element must still be a message to be treated as one: ownership
|
|
6282
|
+
* alone reads an in-place attribute rewrite as a move, and a node outside the target
|
|
6283
|
+
* set belongs to the consumer, so nothing here may dismiss it.
|
|
6284
|
+
*/
|
|
6285
|
+
messageTargetDisconnected(message) {
|
|
6286
|
+
if (!this.#connected) return;
|
|
6287
|
+
const moved = this.#owns(message) && message.matches(MESSAGE_SELECTOR);
|
|
6288
|
+
if (moved) return;
|
|
6289
|
+
this.#forget(message);
|
|
6290
|
+
this.#leaving.delete(message);
|
|
6291
|
+
}
|
|
6292
|
+
/**
|
|
6293
|
+
* Re-points the observation after a `region` swap and picks up the messages the
|
|
6294
|
+
* new element brought with it (dynamic inserts, so their own `role` announces
|
|
6295
|
+
* them). The `#connected` guard is load-bearing: Stimulus runs target callbacks
|
|
6296
|
+
* for the initial markup *before* `connect()` and again during teardown *after*
|
|
6297
|
+
* `disconnect()`, and re-observing there would outlive the controller.
|
|
6298
|
+
*/
|
|
6299
|
+
#resync() {
|
|
6300
|
+
if (!this.#connected) return;
|
|
6301
|
+
this.#syncObservation();
|
|
6302
|
+
for (const message of this.messageTargets) {
|
|
6303
|
+
if (this.#owns(message)) this.#process(message, false);
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
/**
|
|
6307
|
+
* Points the mutation observation at the current `region` target, re-resolved on
|
|
6308
|
+
* every sync rather than captured at connect, so an element swapped in at runtime
|
|
6309
|
+
* is observed instead of the detached original.
|
|
6310
|
+
*/
|
|
6311
|
+
#syncObservation() {
|
|
6312
|
+
this.#stopObserving();
|
|
6313
|
+
if (!this.hasRegionTarget || typeof MutationObserver === "undefined") return;
|
|
6314
|
+
this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));
|
|
6315
|
+
this.#observer.observe(this.regionTarget, { childList: true, subtree: true });
|
|
6316
|
+
}
|
|
6317
|
+
#stopObserving() {
|
|
6318
|
+
this.#observer?.disconnect();
|
|
6319
|
+
this.#observer = null;
|
|
6109
6320
|
}
|
|
6110
6321
|
/**
|
|
6111
6322
|
* Pause-on-hover/focus listeners, bound and unbound as a pair so the two sides
|
|
@@ -6151,6 +6362,7 @@ var FlashController = class extends Controller {
|
|
|
6151
6362
|
*/
|
|
6152
6363
|
#process(message, bridge) {
|
|
6153
6364
|
if (this.#state.has(message) || this.#order.includes(message)) return;
|
|
6365
|
+
if (this.#leaving.has(message)) return;
|
|
6154
6366
|
const type = message.getAttribute("data-flash-type") ?? "";
|
|
6155
6367
|
const assertive = ASSERTIVE_TYPES.has(type);
|
|
6156
6368
|
if (!message.hasAttribute("role")) {
|
|
@@ -6183,33 +6395,53 @@ var FlashController = class extends Controller {
|
|
|
6183
6395
|
const existing = this.#state.get(message);
|
|
6184
6396
|
if (existing?.id) this.#timers.clear(existing.id);
|
|
6185
6397
|
const id = this.#timers.set(() => this.#beginDismiss(message, "timeout"), duration);
|
|
6186
|
-
this.#state.set(message, {
|
|
6398
|
+
this.#state.set(message, {
|
|
6399
|
+
id,
|
|
6400
|
+
startedAt: Date.now(),
|
|
6401
|
+
remaining: duration,
|
|
6402
|
+
paused: existing?.paused ?? /* @__PURE__ */ new Set()
|
|
6403
|
+
});
|
|
6187
6404
|
}
|
|
6188
|
-
/**
|
|
6189
|
-
|
|
6405
|
+
/**
|
|
6406
|
+
* Pauses a message's auto-dismiss, banking the time left (hover/focus, WCAG 2.2.1).
|
|
6407
|
+
* Hover and focus are independent reasons: the remaining time is banked on the
|
|
6408
|
+
* first of them, and {@link FlashController.#resume} waits for the last one.
|
|
6409
|
+
*/
|
|
6410
|
+
#pause(message, reason) {
|
|
6190
6411
|
const timer = this.#state.get(message);
|
|
6191
|
-
if (!timer
|
|
6412
|
+
if (!timer) return;
|
|
6413
|
+
timer.paused.add(reason);
|
|
6414
|
+
if (timer.id === 0) return;
|
|
6192
6415
|
this.#timers.clear(timer.id);
|
|
6193
|
-
const remaining = Math.max(
|
|
6194
|
-
this.#state.set(message, { id: 0, startedAt: 0, remaining });
|
|
6416
|
+
const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));
|
|
6417
|
+
this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });
|
|
6195
6418
|
}
|
|
6196
6419
|
/** Resumes a paused message's auto-dismiss with the banked time. */
|
|
6197
|
-
#resume(message) {
|
|
6420
|
+
#resume(message, reason) {
|
|
6198
6421
|
const timer = this.#state.get(message);
|
|
6199
6422
|
if (!timer) return;
|
|
6200
|
-
|
|
6423
|
+
timer.paused.delete(reason);
|
|
6424
|
+
if (timer.paused.size > 0) return;
|
|
6425
|
+
if (timer.id !== 0) return;
|
|
6201
6426
|
this.#startTimer(message, timer.remaining);
|
|
6202
6427
|
}
|
|
6203
|
-
/**
|
|
6204
|
-
#
|
|
6428
|
+
/** Releases every per-message resource: timer, stacking slot, pause listeners. */
|
|
6429
|
+
#forget(message) {
|
|
6205
6430
|
const timer = this.#state.get(message);
|
|
6206
6431
|
if (timer?.id) this.#timers.clear(timer.id);
|
|
6207
6432
|
this.#state.delete(message);
|
|
6208
6433
|
const index = this.#order.indexOf(message);
|
|
6209
6434
|
if (index !== -1) this.#order.splice(index, 1);
|
|
6435
|
+
this.#unbindPause(message);
|
|
6436
|
+
}
|
|
6437
|
+
/** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
|
|
6438
|
+
#beginDismiss(message, reason) {
|
|
6439
|
+
if (!this.#state.has(message) && !this.#order.includes(message)) return;
|
|
6440
|
+
this.#forget(message);
|
|
6441
|
+
this.#leaving.add(message);
|
|
6210
6442
|
message.setAttribute("data-flash-state", "leaving");
|
|
6211
6443
|
const finalize = () => {
|
|
6212
|
-
this.#
|
|
6444
|
+
if (!this.#leaving.delete(message)) return;
|
|
6213
6445
|
message.remove();
|
|
6214
6446
|
this.dispatch("dismiss", { detail: { element: message, reason } });
|
|
6215
6447
|
};
|
|
@@ -6478,8 +6710,7 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
6478
6710
|
};
|
|
6479
6711
|
/** Suppresses native bubbles and binds the submit / blur / input listeners. */
|
|
6480
6712
|
connect() {
|
|
6481
|
-
if (
|
|
6482
|
-
this.element.setAttribute("novalidate", "");
|
|
6713
|
+
if (setDefaultAttribute(this.element, "novalidate", "")) {
|
|
6483
6714
|
this.element.setAttribute(_FormValidationController.#NOVALIDATE_MARKER, "");
|
|
6484
6715
|
}
|
|
6485
6716
|
document.addEventListener("submit", this.#onSubmit, true);
|
|
@@ -6661,6 +6892,16 @@ var FormValidationController = class _FormValidationController extends Controlle
|
|
|
6661
6892
|
var DetachGate = class _DetachGate {
|
|
6662
6893
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
6663
6894
|
#pending = false;
|
|
6895
|
+
/**
|
|
6896
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
6897
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
6898
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
6899
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
6900
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
6901
|
+
*/
|
|
6902
|
+
get pending() {
|
|
6903
|
+
return this.#pending;
|
|
6904
|
+
}
|
|
6664
6905
|
/**
|
|
6665
6906
|
* True when the disconnect is definitely a real detach — the element left
|
|
6666
6907
|
* the document, or `data-controller` no longer lists the identifier. False
|
|
@@ -6765,7 +7006,17 @@ var FrameLoadingController = class extends Controller {
|
|
|
6765
7006
|
#gate = new DetachGate();
|
|
6766
7007
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
6767
7008
|
#loading = false;
|
|
6768
|
-
|
|
7009
|
+
/**
|
|
7010
|
+
* The optional targets this controller revealed, and the content it marked inert.
|
|
7011
|
+
* Held as references rather than re-resolved on the way out: a detach that keeps
|
|
7012
|
+
* the element takes the identifier off `data-controller` first, and a scope
|
|
7013
|
+
* without its identifier stops resolving targets — the elements to tidy would be
|
|
7014
|
+
* unreachable exactly when the tidying matters. They double as the ownership
|
|
7015
|
+
* marker, so a `hidden` or an `inert` the consumer wrote is never taken over.
|
|
7016
|
+
*/
|
|
7017
|
+
#revealedSkeleton = null;
|
|
7018
|
+
#revealedOverlay = null;
|
|
7019
|
+
#inertTarget = null;
|
|
6769
7020
|
#previousFocus = null;
|
|
6770
7021
|
/** The id of the retreated element, used to re-find it if the load replaced it. */
|
|
6771
7022
|
#previousFocusId = "";
|
|
@@ -6792,31 +7043,88 @@ var FrameLoadingController = class extends Controller {
|
|
|
6792
7043
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
6793
7044
|
}
|
|
6794
7045
|
/**
|
|
6795
|
-
* Drops the held finish and the loading bookkeeping on a real detach
|
|
6796
|
-
*
|
|
6797
|
-
*
|
|
7046
|
+
* Drops the held finish and the loading bookkeeping on a real detach, returning
|
|
7047
|
+
* the frame to its idle form. No reconnect is coming, so nothing is left that
|
|
7048
|
+
* could finish the load and clear the hooks — a detach that keeps the element
|
|
7049
|
+
* (a morph dropping the identifier, an exit from a scoped observed root) would
|
|
7050
|
+
* otherwise strand it busy and inert. Focus is left where it is: the element is
|
|
7051
|
+
* leaving this controller's care, and moving it now would be an unexplained jump.
|
|
6798
7052
|
*/
|
|
6799
7053
|
#teardown() {
|
|
6800
7054
|
this.#gate.cancel();
|
|
6801
7055
|
this.#timeouts.clearAll();
|
|
6802
7056
|
this.#floor.cancel();
|
|
7057
|
+
if (this.#loading) this.#rewindHooks();
|
|
6803
7058
|
this.#loading = false;
|
|
6804
7059
|
this.#previousFocus = null;
|
|
6805
7060
|
}
|
|
6806
7061
|
/**
|
|
6807
|
-
* Returns the frame to its
|
|
6808
|
-
*
|
|
6809
|
-
*
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
7062
|
+
* Returns the frame to its idle form for the snapshot Turbo is about to take, so
|
|
7063
|
+
* a page reached with the Back button does not restore a frame that is busy and
|
|
7064
|
+
* inert with nothing left to finish it. State only — no `end` event and no focus
|
|
7065
|
+
* move, because the load did not actually complete.
|
|
7066
|
+
*
|
|
7067
|
+
* The load is abandoned rather than paused, so the flag and any finish the floor
|
|
7068
|
+
* still holds drop along with the hooks. A kept finish would surface after the
|
|
7069
|
+
* rewind as exactly the three things this pass exists to avoid — an `end`, a
|
|
7070
|
+
* completion announcement, and a focus move — and a kept flag would leave the
|
|
7071
|
+
* next fetch on a page that survives a cancelled visit skipping the loading
|
|
7072
|
+
* state, its idempotence guard already satisfied.
|
|
6812
7073
|
*/
|
|
6813
7074
|
#rewindForCache() {
|
|
6814
7075
|
if (!this.#loading) return;
|
|
7076
|
+
this.#loading = false;
|
|
7077
|
+
this.#floor.cancel();
|
|
7078
|
+
this.#rewindHooks();
|
|
7079
|
+
}
|
|
7080
|
+
/**
|
|
7081
|
+
* Clears every hook the loading state writes. Shared by the three ways a load can
|
|
7082
|
+
* stop — completion, detach, snapshot — so none of them can drift into tidying
|
|
7083
|
+
* only part of it.
|
|
7084
|
+
*/
|
|
7085
|
+
#rewindHooks() {
|
|
6815
7086
|
this.element.removeAttribute("aria-busy");
|
|
6816
7087
|
this.element.removeAttribute("data-frame-loading");
|
|
6817
|
-
if (this
|
|
6818
|
-
if (this
|
|
7088
|
+
if (this.#revealedSkeleton) this.#revealedSkeleton.hidden = true;
|
|
7089
|
+
if (this.#revealedOverlay) this.#revealedOverlay.hidden = true;
|
|
7090
|
+
this.#revealedSkeleton = null;
|
|
7091
|
+
this.#revealedOverlay = null;
|
|
7092
|
+
this.#clearInert();
|
|
7093
|
+
}
|
|
7094
|
+
/**
|
|
7095
|
+
* Re-shows a `skeleton` that arrived mid-load. Turbo's frame renderer empties the
|
|
7096
|
+
* frame and re-inserts the response's children, so a response's authored (hidden)
|
|
7097
|
+
* skeleton can land while a later fetch is still running, and only the controller
|
|
7098
|
+
* knows the frame is still busy.
|
|
7099
|
+
*/
|
|
7100
|
+
skeletonTargetConnected() {
|
|
7101
|
+
if (this.#loading) this.#revealSkeleton();
|
|
7102
|
+
}
|
|
7103
|
+
/** Re-shows an `overlay` that arrived mid-load — the same swap as the skeleton. */
|
|
7104
|
+
overlayTargetConnected() {
|
|
7105
|
+
if (this.#loading) this.#revealOverlay();
|
|
7106
|
+
}
|
|
7107
|
+
/**
|
|
7108
|
+
* Re-blocks a `content` that arrived mid-load, so the stale copy stays unusable.
|
|
7109
|
+
* The element that left is released first and ownership is then decided afresh, so
|
|
7110
|
+
* an `inert` the replacement authored stays the consumer's.
|
|
7111
|
+
*/
|
|
7112
|
+
contentTargetConnected() {
|
|
7113
|
+
if (!this.#loading) return;
|
|
6819
7114
|
this.#clearInert();
|
|
7115
|
+
this.#applyInert();
|
|
7116
|
+
}
|
|
7117
|
+
/** Reveals the optional `skeleton`, noting it as this controller's to hide again. */
|
|
7118
|
+
#revealSkeleton() {
|
|
7119
|
+
if (!this.hasSkeletonTarget) return;
|
|
7120
|
+
this.#revealedSkeleton = this.skeletonTarget;
|
|
7121
|
+
this.skeletonTarget.hidden = false;
|
|
7122
|
+
}
|
|
7123
|
+
/** Reveals the optional `overlay`, noting it as this controller's to hide again. */
|
|
7124
|
+
#revealOverlay() {
|
|
7125
|
+
if (!this.hasOverlayTarget) return;
|
|
7126
|
+
this.#revealedOverlay = this.overlayTarget;
|
|
7127
|
+
this.overlayTarget.hidden = false;
|
|
6820
7128
|
}
|
|
6821
7129
|
/** Enters the loading state: hooks, skeleton/overlay, inert content, focus retreat. */
|
|
6822
7130
|
#begin() {
|
|
@@ -6824,8 +7132,8 @@ var FrameLoadingController = class extends Controller {
|
|
|
6824
7132
|
this.#floor.begin();
|
|
6825
7133
|
this.element.setAttribute("aria-busy", "true");
|
|
6826
7134
|
this.element.setAttribute("data-frame-loading", "true");
|
|
6827
|
-
|
|
6828
|
-
|
|
7135
|
+
this.#revealSkeleton();
|
|
7136
|
+
this.#revealOverlay();
|
|
6829
7137
|
this.#applyInert();
|
|
6830
7138
|
this.#retreatFocus();
|
|
6831
7139
|
this.dispatch("start", { detail: {} });
|
|
@@ -6834,11 +7142,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
6834
7142
|
/** Leaves the loading state: restore hooks, hide skeleton/overlay, restore focus. */
|
|
6835
7143
|
#finish() {
|
|
6836
7144
|
this.#loading = false;
|
|
6837
|
-
this
|
|
6838
|
-
this.element.removeAttribute("data-frame-loading");
|
|
6839
|
-
if (this.hasSkeletonTarget) this.skeletonTarget.hidden = true;
|
|
6840
|
-
if (this.hasOverlayTarget) this.overlayTarget.hidden = true;
|
|
6841
|
-
this.#clearInert();
|
|
7145
|
+
this.#rewindHooks();
|
|
6842
7146
|
this.#restoreFocus();
|
|
6843
7147
|
this.dispatch("end", { detail: {} });
|
|
6844
7148
|
announce(fillTemplate(this.announceReadyTextValue, {}));
|
|
@@ -6847,12 +7151,11 @@ var FrameLoadingController = class extends Controller {
|
|
|
6847
7151
|
#applyInert() {
|
|
6848
7152
|
if (!this.hasContentTarget || this.contentTarget.hasAttribute("inert")) return;
|
|
6849
7153
|
this.contentTarget.setAttribute("inert", "");
|
|
6850
|
-
this.#
|
|
7154
|
+
this.#inertTarget = this.contentTarget;
|
|
6851
7155
|
}
|
|
6852
7156
|
#clearInert() {
|
|
6853
|
-
|
|
6854
|
-
this.#
|
|
6855
|
-
if (this.hasContentTarget) this.contentTarget.removeAttribute("inert");
|
|
7157
|
+
this.#inertTarget?.removeAttribute("inert");
|
|
7158
|
+
this.#inertTarget = null;
|
|
6856
7159
|
}
|
|
6857
7160
|
/** Saves and blurs focus if it sits inside the frame about to go stale. */
|
|
6858
7161
|
#retreatFocus() {
|
|
@@ -6889,6 +7192,7 @@ var FrameLoadingController = class extends Controller {
|
|
|
6889
7192
|
}
|
|
6890
7193
|
}
|
|
6891
7194
|
};
|
|
7195
|
+
var hookOwners = /* @__PURE__ */ new WeakMap();
|
|
6892
7196
|
var HighlightController = class extends Controller {
|
|
6893
7197
|
static values = {
|
|
6894
7198
|
duration: { type: Number, default: 1500 },
|
|
@@ -6896,9 +7200,18 @@ var HighlightController = class extends Controller {
|
|
|
6896
7200
|
};
|
|
6897
7201
|
static events = ["start", "end"];
|
|
6898
7202
|
#timeouts = new SafeTimeout();
|
|
7203
|
+
/**
|
|
7204
|
+
* The removal timer this connection has outstanding for an element. Held weakly so
|
|
7205
|
+
* a row that leaves the DOM is not retained, and dropped wholesale on `disconnect()`
|
|
7206
|
+
* so a cleared id can never be matched against a recycled one. Which connection owns
|
|
7207
|
+
* an element's hook is answered by the shared owner registry above.
|
|
7208
|
+
*/
|
|
7209
|
+
#pending = /* @__PURE__ */ new WeakMap();
|
|
6899
7210
|
#observer = null;
|
|
6900
7211
|
connect() {
|
|
7212
|
+
this.#clearArrivedHook(this.element);
|
|
6901
7213
|
if (this.observeValue) {
|
|
7214
|
+
for (const child of this.element.children) this.#clearArrivedHook(child);
|
|
6902
7215
|
if (typeof MutationObserver !== "undefined") {
|
|
6903
7216
|
this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));
|
|
6904
7217
|
this.#observer.observe(this.element, { childList: true });
|
|
@@ -6911,6 +7224,12 @@ var HighlightController = class extends Controller {
|
|
|
6911
7224
|
this.#observer?.disconnect();
|
|
6912
7225
|
this.#observer = null;
|
|
6913
7226
|
this.#timeouts.clearAll();
|
|
7227
|
+
this.#pending = /* @__PURE__ */ new WeakMap();
|
|
7228
|
+
}
|
|
7229
|
+
/** Drops a hook that arrived with the DOM, along with this connection's claim on it. */
|
|
7230
|
+
#clearArrivedHook(el) {
|
|
7231
|
+
if (hookOwners.get(el) === this) hookOwners.delete(el);
|
|
7232
|
+
el.removeAttribute("data-highlight");
|
|
6914
7233
|
}
|
|
6915
7234
|
/** Highlights every element child added by a childList mutation. */
|
|
6916
7235
|
#onMutations(mutations) {
|
|
@@ -6923,12 +7242,33 @@ var HighlightController = class extends Controller {
|
|
|
6923
7242
|
/** Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion). */
|
|
6924
7243
|
#highlight(el) {
|
|
6925
7244
|
if (prefersReducedMotion()) return;
|
|
7245
|
+
this.#releasePending(el);
|
|
6926
7246
|
el.setAttribute("data-highlight", "true");
|
|
6927
7247
|
this.dispatch("start", { target: el, detail: { element: el } });
|
|
6928
|
-
this.#timeouts.set(() => {
|
|
7248
|
+
const id = this.#timeouts.set(() => {
|
|
7249
|
+
this.#pending.delete(el);
|
|
7250
|
+
hookOwners.delete(el);
|
|
6929
7251
|
el.removeAttribute("data-highlight");
|
|
6930
7252
|
this.dispatch("end", { target: el, detail: { element: el } });
|
|
6931
7253
|
}, this.durationValue);
|
|
7254
|
+
this.#pending.set(el, id);
|
|
7255
|
+
hookOwners.set(el, this);
|
|
7256
|
+
}
|
|
7257
|
+
/**
|
|
7258
|
+
* Releases whichever removal timer holds `el`'s hook. The row may have been
|
|
7259
|
+
* highlighted inside a different watched container before it moved here, and that
|
|
7260
|
+
* container's timer is reachable only through the shared owner registry.
|
|
7261
|
+
*/
|
|
7262
|
+
#releasePending(el) {
|
|
7263
|
+
const owner = hookOwners.get(el);
|
|
7264
|
+
if (owner !== void 0 && owner !== this) owner.#cancelPending(el);
|
|
7265
|
+
this.#cancelPending(el);
|
|
7266
|
+
}
|
|
7267
|
+
/** Releases `el`'s pending removal timer, if it has one. */
|
|
7268
|
+
#cancelPending(el) {
|
|
7269
|
+
this.#timeouts.clear(this.#pending.get(el) ?? -1);
|
|
7270
|
+
this.#pending.delete(el);
|
|
7271
|
+
hookOwners.delete(el);
|
|
6932
7272
|
}
|
|
6933
7273
|
};
|
|
6934
7274
|
|
|
@@ -7078,6 +7418,12 @@ var IdleController = class extends Controller {
|
|
|
7078
7418
|
#prompted = false;
|
|
7079
7419
|
/** Timestamp of the last activity; the timers self-reschedule against it. */
|
|
7080
7420
|
#lastActivity = 0;
|
|
7421
|
+
/**
|
|
7422
|
+
* Activity types actually registered on `document`, so `disconnect()` unbinds the
|
|
7423
|
+
* same set even when `events` changed while connected (a Turbo morph can rewrite
|
|
7424
|
+
* the Value in place, and the removal must match the registration, not the Value).
|
|
7425
|
+
*/
|
|
7426
|
+
#boundEvents = [];
|
|
7081
7427
|
#onActivity = () => {
|
|
7082
7428
|
this.#lastActivity = Date.now();
|
|
7083
7429
|
if (this.#idle || this.#prompted) {
|
|
@@ -7092,16 +7438,21 @@ var IdleController = class extends Controller {
|
|
|
7092
7438
|
if (document.visibilityState === "visible") this.#onActivity();
|
|
7093
7439
|
};
|
|
7094
7440
|
connect() {
|
|
7095
|
-
|
|
7441
|
+
this.#idle = false;
|
|
7442
|
+
this.#prompted = false;
|
|
7443
|
+
this.element.removeAttribute("data-idle");
|
|
7444
|
+
this.#boundEvents = [...this.eventsValue];
|
|
7445
|
+
for (const type of this.#boundEvents) {
|
|
7096
7446
|
document.addEventListener(type, this.#onActivity, { passive: true, capture: true });
|
|
7097
7447
|
}
|
|
7098
7448
|
document.addEventListener("visibilitychange", this.#onVisibility);
|
|
7099
7449
|
this.#arm();
|
|
7100
7450
|
}
|
|
7101
7451
|
disconnect() {
|
|
7102
|
-
for (const type of this
|
|
7452
|
+
for (const type of this.#boundEvents) {
|
|
7103
7453
|
document.removeEventListener(type, this.#onActivity, { capture: true });
|
|
7104
7454
|
}
|
|
7455
|
+
this.#boundEvents = [];
|
|
7105
7456
|
document.removeEventListener("visibilitychange", this.#onVisibility);
|
|
7106
7457
|
this.#timeouts.clearAll();
|
|
7107
7458
|
}
|
|
@@ -7992,6 +8343,8 @@ var LocalTimeController = class extends Controller {
|
|
|
7992
8343
|
* its condition is that formatting was applied, and a repaint applies it with a
|
|
7993
8344
|
* new result. A pass that cannot format writes nothing and emits nothing, so the
|
|
7994
8345
|
* authored absolute text stays as the fallback.
|
|
8346
|
+
*
|
|
8347
|
+
* @stimeoRenderRoot
|
|
7995
8348
|
*/
|
|
7996
8349
|
#render() {
|
|
7997
8350
|
const date = this.#parse();
|
|
@@ -8067,7 +8420,7 @@ var LocalTimeController = class extends Controller {
|
|
|
8067
8420
|
return this.localeValue || this.element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
8068
8421
|
}
|
|
8069
8422
|
};
|
|
8070
|
-
var COLUMNS_PROPERTY = "--stimeo
|
|
8423
|
+
var COLUMNS_PROPERTY = "--stimeo--masonry-columns";
|
|
8071
8424
|
var MasonryController = class extends Controller {
|
|
8072
8425
|
static targets = ["item"];
|
|
8073
8426
|
static values = {
|
|
@@ -9026,6 +9379,8 @@ var MeterController = class extends Controller {
|
|
|
9026
9379
|
* Reflects value/range onto ARIA, the segment onto `data-state`, and the ratio.
|
|
9027
9380
|
* The reading is derived once and returned, so the `change` detail reports the
|
|
9028
9381
|
* same numbers the DOM just received.
|
|
9382
|
+
*
|
|
9383
|
+
* @stimeoRenderRoot
|
|
9029
9384
|
*/
|
|
9030
9385
|
#render() {
|
|
9031
9386
|
const value = this.#clamp(this.valueValue);
|
|
@@ -9037,7 +9392,7 @@ var MeterController = class extends Controller {
|
|
|
9037
9392
|
this.element.setAttribute("aria-valuemin", String(this.minValue));
|
|
9038
9393
|
this.element.setAttribute("aria-valuemax", String(this.maxValue));
|
|
9039
9394
|
this.element.setAttribute("aria-valuenow", String(reading.value));
|
|
9040
|
-
this.element.style.setProperty("--stimeo
|
|
9395
|
+
this.element.style.setProperty("--stimeo--meter-ratio", String(reading.ratio));
|
|
9041
9396
|
this.element.setAttribute("data-state", reading.state);
|
|
9042
9397
|
this.#applyValueText(reading);
|
|
9043
9398
|
return reading;
|
|
@@ -10175,9 +10530,7 @@ var NetworkStatusController = class extends Controller {
|
|
|
10175
10530
|
#showOffline() {
|
|
10176
10531
|
this.#timers.clearAll();
|
|
10177
10532
|
if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
|
|
10178
|
-
if (this.hasOfflineTarget)
|
|
10179
|
-
this.offlineTarget.hidden = false;
|
|
10180
|
-
}
|
|
10533
|
+
if (this.hasOfflineTarget) this.offlineTarget.hidden = false;
|
|
10181
10534
|
}
|
|
10182
10535
|
/** Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`. */
|
|
10183
10536
|
#showOnline() {
|
|
@@ -10191,6 +10544,114 @@ var NetworkStatusController = class extends Controller {
|
|
|
10191
10544
|
}
|
|
10192
10545
|
}
|
|
10193
10546
|
};
|
|
10547
|
+
|
|
10548
|
+
// src/utils/stepped_value.ts
|
|
10549
|
+
function effectiveStep(step) {
|
|
10550
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
10551
|
+
}
|
|
10552
|
+
function snapSteppedValue(raw, range) {
|
|
10553
|
+
if (!(range.min <= range.max)) return finiteFallback(range.min, range.max);
|
|
10554
|
+
const input = Number.isNaN(raw) ? finiteFallback(range.min, range.max) : raw;
|
|
10555
|
+
const clamped = Math.min(range.max, Math.max(range.min, input));
|
|
10556
|
+
const step = effectiveStep(range.step);
|
|
10557
|
+
const base = stepBase(range);
|
|
10558
|
+
const candidates = [];
|
|
10559
|
+
if (Number.isFinite(range.min)) candidates.push(range.min);
|
|
10560
|
+
if (Number.isFinite(range.max)) candidates.push(range.max);
|
|
10561
|
+
const gridPosition = (clamped - base) / step;
|
|
10562
|
+
if (Number.isFinite(gridPosition)) {
|
|
10563
|
+
addGridCandidate(candidates, Math.floor(gridPosition), range, base, step);
|
|
10564
|
+
addGridCandidate(candidates, Math.ceil(gridPosition), range, base, step);
|
|
10565
|
+
}
|
|
10566
|
+
if (candidates.length === 0) return clamped;
|
|
10567
|
+
let nearest = candidates[0];
|
|
10568
|
+
let nearestDistance = Math.abs(clamped - nearest);
|
|
10569
|
+
for (const candidate of candidates.slice(1)) {
|
|
10570
|
+
const distance = Math.abs(clamped - candidate);
|
|
10571
|
+
if (distance < nearestDistance || nearlyEqual(distance, nearestDistance) && candidate > nearest) {
|
|
10572
|
+
nearest = candidate;
|
|
10573
|
+
nearestDistance = distance;
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
return nearest;
|
|
10577
|
+
}
|
|
10578
|
+
function stepSteppedValue(current, count, range) {
|
|
10579
|
+
const value = snapSteppedValue(current, range);
|
|
10580
|
+
const distance = Math.abs(Math.trunc(count));
|
|
10581
|
+
if (!Number.isFinite(distance) || distance === 0) return value;
|
|
10582
|
+
const direction = Math.sign(count);
|
|
10583
|
+
const adjacent = adjacentSteppedValue(value, direction, range);
|
|
10584
|
+
const raw = adjacent + direction * (distance - 1) * effectiveStep(range.step);
|
|
10585
|
+
return snapSteppedValue(raw, range);
|
|
10586
|
+
}
|
|
10587
|
+
function adjacentSteppedValue(current, direction, range) {
|
|
10588
|
+
const step = effectiveStep(range.step);
|
|
10589
|
+
const base = stepBase(range);
|
|
10590
|
+
const candidates = [];
|
|
10591
|
+
if (direction > 0) {
|
|
10592
|
+
const position2 = (current - base) / step;
|
|
10593
|
+
if (Number.isFinite(position2)) {
|
|
10594
|
+
let gridIndex = Math.floor(position2) + 1;
|
|
10595
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
10596
|
+
if (candidate < current || nearlyEqual(candidate, current)) {
|
|
10597
|
+
gridIndex += 1;
|
|
10598
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
10599
|
+
}
|
|
10600
|
+
if (candidate > current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
10601
|
+
candidates.push(clamp(candidate, range));
|
|
10602
|
+
}
|
|
10603
|
+
}
|
|
10604
|
+
return clamp(Math.min(...candidates), range);
|
|
10605
|
+
}
|
|
10606
|
+
const position = (current - base) / step;
|
|
10607
|
+
if (Number.isFinite(position)) {
|
|
10608
|
+
let gridIndex = Math.ceil(position) - 1;
|
|
10609
|
+
let candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
10610
|
+
if (candidate > current || nearlyEqual(candidate, current)) {
|
|
10611
|
+
gridIndex -= 1;
|
|
10612
|
+
candidate = cleanGridValue(base + gridIndex * step, base, step);
|
|
10613
|
+
}
|
|
10614
|
+
if (candidate < current && !nearlyEqual(candidate, current) && within(candidate, range.min, range.max)) {
|
|
10615
|
+
candidates.push(clamp(candidate, range));
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10618
|
+
return clamp(Math.max(...candidates), range);
|
|
10619
|
+
}
|
|
10620
|
+
function addGridCandidate(candidates, index, range, base, step) {
|
|
10621
|
+
const candidate = cleanGridValue(base + index * step, base, step);
|
|
10622
|
+
if (within(candidate, range.min, range.max)) candidates.push(clamp(candidate, range));
|
|
10623
|
+
}
|
|
10624
|
+
function stepBase(range) {
|
|
10625
|
+
if (range.base !== void 0 && Number.isFinite(range.base)) return range.base;
|
|
10626
|
+
return Number.isFinite(range.min) ? range.min : 0;
|
|
10627
|
+
}
|
|
10628
|
+
function cleanGridValue(value, base, step) {
|
|
10629
|
+
const precision = Math.max(decimalPlaces(base), decimalPlaces(step));
|
|
10630
|
+
return precision <= 100 ? Number(value.toFixed(precision)) : value;
|
|
10631
|
+
}
|
|
10632
|
+
function decimalPlaces(value) {
|
|
10633
|
+
const [coefficient = "", exponentText] = Math.abs(value).toString().toLowerCase().split("e");
|
|
10634
|
+
const fractionLength = coefficient.split(".")[1]?.length ?? 0;
|
|
10635
|
+
const exponent = exponentText === void 0 ? 0 : Number(exponentText);
|
|
10636
|
+
return Math.max(0, fractionLength - exponent);
|
|
10637
|
+
}
|
|
10638
|
+
function within(value, min, max) {
|
|
10639
|
+
return (value > min || nearlyEqual(value, min)) && (value < max || nearlyEqual(value, max));
|
|
10640
|
+
}
|
|
10641
|
+
function clamp(value, range) {
|
|
10642
|
+
return Math.min(range.max, Math.max(range.min, value));
|
|
10643
|
+
}
|
|
10644
|
+
function nearlyEqual(left, right) {
|
|
10645
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
10646
|
+
return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * scale;
|
|
10647
|
+
}
|
|
10648
|
+
function finiteFallback(min, max) {
|
|
10649
|
+
if (Number.isFinite(min)) return min;
|
|
10650
|
+
if (Number.isFinite(max)) return max;
|
|
10651
|
+
return 0;
|
|
10652
|
+
}
|
|
10653
|
+
|
|
10654
|
+
// src/controllers/number_input_controller.ts
|
|
10194
10655
|
var NumberInputController = class _NumberInputController extends Controller {
|
|
10195
10656
|
static targets = ["input", "increment", "decrement"];
|
|
10196
10657
|
static values = {
|
|
@@ -10224,24 +10685,24 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10224
10685
|
#repeatedDuringHold = false;
|
|
10225
10686
|
/** True when the next `click` is the trailing one after a hold and must be ignored. */
|
|
10226
10687
|
#suppressNextClick = false;
|
|
10688
|
+
/** Collapses runtime range/step changes into one silent input reconciliation. */
|
|
10689
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcile());
|
|
10227
10690
|
/** Normalizes any initial value and wires the focus/hold pointer guards. */
|
|
10228
10691
|
connect() {
|
|
10692
|
+
this.#repaint.activate();
|
|
10229
10693
|
if (!this.hasInputTarget) return;
|
|
10230
|
-
|
|
10231
|
-
this.#write(this.#normalize(this.#currentValue()));
|
|
10232
|
-
} else {
|
|
10233
|
-
this.#updateButtons(this.#currentValue());
|
|
10234
|
-
}
|
|
10694
|
+
this.#reconcile();
|
|
10235
10695
|
this.#guards = new AbortController();
|
|
10236
10696
|
const { signal } = this.#guards;
|
|
10237
|
-
if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget,
|
|
10238
|
-
if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -
|
|
10697
|
+
if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, 1, signal);
|
|
10698
|
+
if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -1, signal);
|
|
10239
10699
|
window.addEventListener("pointerup", this.#stopHold, { signal });
|
|
10240
10700
|
window.addEventListener("pointercancel", this.#stopHold, { signal });
|
|
10241
10701
|
window.addEventListener("blur", this.#stopHold, { signal });
|
|
10242
10702
|
}
|
|
10243
10703
|
/** Releases the pointer guards and tears down every pending timer and hold state. */
|
|
10244
10704
|
disconnect() {
|
|
10705
|
+
this.#repaint.cancel();
|
|
10245
10706
|
this.#guards?.abort();
|
|
10246
10707
|
this.#guards = null;
|
|
10247
10708
|
this.#holdActive = false;
|
|
@@ -10250,16 +10711,28 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10250
10711
|
this.#holdTimeouts.clearAll();
|
|
10251
10712
|
this.#holdIntervals.clearAll();
|
|
10252
10713
|
}
|
|
10714
|
+
/** Silently reconciles a minimum changed by application code or a Turbo morph. */
|
|
10715
|
+
minValueChanged() {
|
|
10716
|
+
this.#repaint.schedule();
|
|
10717
|
+
}
|
|
10718
|
+
/** Silently reconciles a maximum changed by application code or a Turbo morph. */
|
|
10719
|
+
maxValueChanged() {
|
|
10720
|
+
this.#repaint.schedule();
|
|
10721
|
+
}
|
|
10722
|
+
/** Silently reconciles a step changed by application code or a Turbo morph. */
|
|
10723
|
+
stepValueChanged() {
|
|
10724
|
+
this.#repaint.schedule();
|
|
10725
|
+
}
|
|
10253
10726
|
/** Increases by one step. Bound via `data-action` (click). */
|
|
10254
10727
|
increment() {
|
|
10255
10728
|
if (this.#consumeSuppressedClick()) return;
|
|
10256
|
-
this.#
|
|
10729
|
+
this.#commitStep(1);
|
|
10257
10730
|
this.inputTarget.focus();
|
|
10258
10731
|
}
|
|
10259
10732
|
/** Decreases by one step. Bound via `data-action` (click). */
|
|
10260
10733
|
decrement() {
|
|
10261
10734
|
if (this.#consumeSuppressedClick()) return;
|
|
10262
|
-
this.#
|
|
10735
|
+
this.#commitStep(-1);
|
|
10263
10736
|
this.inputTarget.focus();
|
|
10264
10737
|
}
|
|
10265
10738
|
/** Clamps and snaps a typed value. Bound via `data-action` (change). */
|
|
@@ -10270,20 +10743,19 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10270
10743
|
/** Keyboard stepping per the APG spinbutton model. */
|
|
10271
10744
|
onKeydown(event) {
|
|
10272
10745
|
if (isReservedArrowChord(event)) return;
|
|
10273
|
-
const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;
|
|
10274
10746
|
let next = null;
|
|
10275
10747
|
switch (event.key) {
|
|
10276
10748
|
case "ArrowUp":
|
|
10277
|
-
next = this.#currentValue()
|
|
10749
|
+
next = stepSteppedValue(this.#currentValue(), 1, this.#steppedRange);
|
|
10278
10750
|
break;
|
|
10279
10751
|
case "ArrowDown":
|
|
10280
|
-
next = this.#currentValue() - this
|
|
10752
|
+
next = stepSteppedValue(this.#currentValue(), -1, this.#steppedRange);
|
|
10281
10753
|
break;
|
|
10282
10754
|
case "PageUp":
|
|
10283
|
-
next = this.#currentValue() +
|
|
10755
|
+
next = this.pageStepValue > 0 ? this.#currentValue() + this.pageStepValue : stepSteppedValue(this.#currentValue(), 10, this.#steppedRange);
|
|
10284
10756
|
break;
|
|
10285
10757
|
case "PageDown":
|
|
10286
|
-
next = this.#currentValue() -
|
|
10758
|
+
next = this.pageStepValue > 0 ? this.#currentValue() - this.pageStepValue : stepSteppedValue(this.#currentValue(), -10, this.#steppedRange);
|
|
10287
10759
|
break;
|
|
10288
10760
|
case "Home":
|
|
10289
10761
|
if (!Number.isFinite(this.minValue)) return;
|
|
@@ -10304,14 +10776,14 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10304
10776
|
* hold; leaving the button while held stops it (the global listeners cover
|
|
10305
10777
|
* release/cancel/blur).
|
|
10306
10778
|
*/
|
|
10307
|
-
#wireButton(button,
|
|
10308
|
-
button.addEventListener("pointerdown", (event) => this.#armHold(event, button,
|
|
10779
|
+
#wireButton(button, direction, signal) {
|
|
10780
|
+
button.addEventListener("pointerdown", (event) => this.#armHold(event, button, direction), {
|
|
10309
10781
|
signal
|
|
10310
10782
|
});
|
|
10311
10783
|
button.addEventListener("pointerleave", this.#stopHold, { signal });
|
|
10312
10784
|
}
|
|
10313
10785
|
/** Starts a hold: focus the input, then schedule the first repeat after a delay. */
|
|
10314
|
-
#armHold(event, button,
|
|
10786
|
+
#armHold(event, button, direction) {
|
|
10315
10787
|
const pointerButton = event.button;
|
|
10316
10788
|
if (typeof pointerButton === "number" && pointerButton !== 0) return;
|
|
10317
10789
|
if (button.disabled) return;
|
|
@@ -10322,13 +10794,13 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10322
10794
|
this.#repeatedDuringHold = false;
|
|
10323
10795
|
this.#suppressNextClick = false;
|
|
10324
10796
|
this.#holdTimeouts.set(() => {
|
|
10325
|
-
if (!this.#
|
|
10797
|
+
if (!this.#commitStep(direction)) {
|
|
10326
10798
|
this.#stopHold();
|
|
10327
10799
|
return;
|
|
10328
10800
|
}
|
|
10329
10801
|
this.#repeatedDuringHold = true;
|
|
10330
10802
|
this.#holdIntervals.set(() => {
|
|
10331
|
-
if (!this.#
|
|
10803
|
+
if (!this.#commitStep(direction)) this.#stopHold();
|
|
10332
10804
|
}, _NumberInputController.#HOLD_REPEAT_MS);
|
|
10333
10805
|
}, _NumberInputController.#HOLD_DELAY_MS);
|
|
10334
10806
|
}
|
|
@@ -10369,6 +10841,23 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10369
10841
|
if (changed) this.dispatch("change", { detail: { value } });
|
|
10370
10842
|
return changed;
|
|
10371
10843
|
}
|
|
10844
|
+
/** Commits an adjacent endpoint/grid value and reports whether it moved. */
|
|
10845
|
+
#commitStep(count) {
|
|
10846
|
+
return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
|
|
10847
|
+
}
|
|
10848
|
+
/**
|
|
10849
|
+
* Silently reflects the current value after a range or step morph.
|
|
10850
|
+
*
|
|
10851
|
+
* @stimeoRenderRoot
|
|
10852
|
+
*/
|
|
10853
|
+
#reconcile() {
|
|
10854
|
+
if (!this.hasInputTarget) return;
|
|
10855
|
+
if (this.inputTarget.value.trim() !== "") {
|
|
10856
|
+
this.#write(this.#normalize(this.#currentValue()));
|
|
10857
|
+
} else {
|
|
10858
|
+
this.#updateButtons(this.#currentValue());
|
|
10859
|
+
}
|
|
10860
|
+
}
|
|
10372
10861
|
/** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
|
|
10373
10862
|
#write(value) {
|
|
10374
10863
|
this.inputTarget.value = String(value);
|
|
@@ -10415,11 +10904,11 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
10415
10904
|
}
|
|
10416
10905
|
/** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */
|
|
10417
10906
|
#normalize(raw) {
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
return
|
|
10907
|
+
return snapSteppedValue(raw, this.#steppedRange);
|
|
10908
|
+
}
|
|
10909
|
+
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
10910
|
+
get #steppedRange() {
|
|
10911
|
+
return { min: this.minValue, max: this.maxValue, step: this.stepValue };
|
|
10423
10912
|
}
|
|
10424
10913
|
};
|
|
10425
10914
|
var TOGGLED_MARKER = "data-optimistic-toggled";
|
|
@@ -10929,7 +11418,7 @@ var INDEX = "data-stimeo--overflow-menu-index";
|
|
|
10929
11418
|
var BOUNDARY = "data-stimeo--overflow-menu-boundary";
|
|
10930
11419
|
var SAVED_ROLE = "data-stimeo--overflow-menu-role";
|
|
10931
11420
|
var SAVED_TABINDEX = "data-stimeo--overflow-menu-tabindex";
|
|
10932
|
-
var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-menu
|
|
11421
|
+
var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-saved-menu";
|
|
10933
11422
|
var MENU_TARGET = "data-stimeo--menu-target";
|
|
10934
11423
|
var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
|
|
10935
11424
|
var OverflowMenuController = class extends Controller {
|
|
@@ -11443,7 +11932,11 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
11443
11932
|
if (!Object.is(page, this.pageValue)) this.pageValue = page;
|
|
11444
11933
|
this.#render();
|
|
11445
11934
|
}
|
|
11446
|
-
/**
|
|
11935
|
+
/**
|
|
11936
|
+
* Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
|
|
11937
|
+
*
|
|
11938
|
+
* @stimeoRenderRoot
|
|
11939
|
+
*/
|
|
11447
11940
|
#render() {
|
|
11448
11941
|
const page = this.#page;
|
|
11449
11942
|
for (const button of this.pageTargets) {
|
|
@@ -11650,7 +12143,7 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
|
|
|
11650
12143
|
this.#toggle("data-strength", band, band.length > 0);
|
|
11651
12144
|
this.#toggle("data-below-min", "true", score > 0 && score < this.minScoreValue);
|
|
11652
12145
|
const ratio = max > 0 ? score / max : 0;
|
|
11653
|
-
this.element.style.setProperty("--stimeo
|
|
12146
|
+
this.element.style.setProperty("--stimeo--password-strength", String(ratio));
|
|
11654
12147
|
}
|
|
11655
12148
|
#writeLabel(label) {
|
|
11656
12149
|
if (this.hasLabelTarget) this.labelTarget.textContent = label;
|
|
@@ -12548,20 +13041,24 @@ var ProgressController = class extends Controller {
|
|
|
12548
13041
|
get #ratio() {
|
|
12549
13042
|
return rangeFraction(this.valueValue, this.minValue, this.maxValue);
|
|
12550
13043
|
}
|
|
12551
|
-
/**
|
|
13044
|
+
/**
|
|
13045
|
+
* Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio.
|
|
13046
|
+
*
|
|
13047
|
+
* @stimeoRenderRoot
|
|
13048
|
+
*/
|
|
12552
13049
|
#render() {
|
|
12553
13050
|
this.element.setAttribute("aria-valuemin", String(this.minValue));
|
|
12554
13051
|
this.element.setAttribute("aria-valuemax", String(this.maxValue));
|
|
12555
13052
|
if (this.indeterminateValue) {
|
|
12556
13053
|
this.element.removeAttribute("aria-valuenow");
|
|
12557
13054
|
this.#clearOwnValueText();
|
|
12558
|
-
this.element.style.removeProperty("--stimeo
|
|
13055
|
+
this.element.style.removeProperty("--stimeo--progress-ratio");
|
|
12559
13056
|
this.element.setAttribute("data-state", "indeterminate");
|
|
12560
13057
|
return;
|
|
12561
13058
|
}
|
|
12562
13059
|
const value = this.#clamp(this.valueValue);
|
|
12563
13060
|
this.element.setAttribute("aria-valuenow", String(value));
|
|
12564
|
-
this.element.style.setProperty("--stimeo
|
|
13061
|
+
this.element.style.setProperty("--stimeo--progress-ratio", String(this.#ratio));
|
|
12565
13062
|
this.element.setAttribute("data-state", "determinate");
|
|
12566
13063
|
this.#applyValueText(value);
|
|
12567
13064
|
}
|
|
@@ -12671,13 +13168,70 @@ var RadioGroupController = class extends Controller {
|
|
|
12671
13168
|
return radio.getAttribute("data-value") ?? "";
|
|
12672
13169
|
}
|
|
12673
13170
|
};
|
|
12674
|
-
|
|
12675
|
-
|
|
13171
|
+
|
|
13172
|
+
// src/utils/owned_pointer_session.ts
|
|
13173
|
+
var OwnedPointerSession = class {
|
|
13174
|
+
pointerId;
|
|
13175
|
+
#owner;
|
|
13176
|
+
#handlers;
|
|
13177
|
+
#abort = new AbortController();
|
|
13178
|
+
#active = true;
|
|
13179
|
+
constructor(start, owner, handlers) {
|
|
13180
|
+
this.pointerId = start.pointerId;
|
|
13181
|
+
this.#owner = owner;
|
|
13182
|
+
this.#handlers = handlers;
|
|
13183
|
+
const { signal } = this.#abort;
|
|
13184
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
13185
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
13186
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
13187
|
+
owner.addEventListener("lostpointercapture", this.#onLostCapture, { signal });
|
|
13188
|
+
try {
|
|
13189
|
+
owner.setPointerCapture?.(this.pointerId);
|
|
13190
|
+
} catch {
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13193
|
+
/** Whether this session still owns its pointer and listeners. */
|
|
13194
|
+
get active() {
|
|
13195
|
+
return this.#active;
|
|
13196
|
+
}
|
|
13197
|
+
/** Whether `event` belongs to the initiating pointer of the live session. */
|
|
13198
|
+
owns(event) {
|
|
13199
|
+
return this.#active && event.pointerId === this.pointerId;
|
|
13200
|
+
}
|
|
13201
|
+
/** Releases capture/listeners and invokes the end callback exactly once. */
|
|
13202
|
+
end() {
|
|
13203
|
+
if (!this.#active) return;
|
|
13204
|
+
this.#active = false;
|
|
13205
|
+
this.#abort.abort();
|
|
13206
|
+
try {
|
|
13207
|
+
this.#owner.releasePointerCapture?.(this.pointerId);
|
|
13208
|
+
} catch {
|
|
13209
|
+
}
|
|
13210
|
+
this.#handlers.end?.();
|
|
13211
|
+
}
|
|
13212
|
+
#onMove = (event) => {
|
|
13213
|
+
if (this.owns(event)) this.#handlers.move(event);
|
|
13214
|
+
};
|
|
13215
|
+
#onEndEvent = (event) => {
|
|
13216
|
+
if (this.owns(event)) this.end();
|
|
13217
|
+
};
|
|
13218
|
+
#onLostCapture = (event) => {
|
|
13219
|
+
const pointerId = event.pointerId;
|
|
13220
|
+
if (typeof pointerId === "number" && pointerId !== this.pointerId) return;
|
|
13221
|
+
this.end();
|
|
13222
|
+
};
|
|
13223
|
+
};
|
|
13224
|
+
|
|
13225
|
+
// src/controllers/range_slider_controller.ts
|
|
13226
|
+
var START_PROPERTY = "--stimeo--range-slider-start";
|
|
13227
|
+
var END_PROPERTY = "--stimeo--range-slider-end";
|
|
13228
|
+
var DEFAULT_MIN = 0;
|
|
13229
|
+
var DEFAULT_MAX = 100;
|
|
12676
13230
|
var RangeSliderController = class extends Controller {
|
|
12677
13231
|
static targets = ["track", "startThumb", "endThumb"];
|
|
12678
13232
|
static values = {
|
|
12679
|
-
min: { type: Number, default:
|
|
12680
|
-
max: { type: Number, default:
|
|
13233
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
13234
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
12681
13235
|
step: { type: Number, default: 1 },
|
|
12682
13236
|
start: { type: Number, default: 0 },
|
|
12683
13237
|
end: { type: Number, default: 100 },
|
|
@@ -12685,31 +13239,27 @@ var RangeSliderController = class extends Controller {
|
|
|
12685
13239
|
};
|
|
12686
13240
|
static actions = ["onKeydown", "onPointerDown"];
|
|
12687
13241
|
static events = ["change"];
|
|
12688
|
-
/**
|
|
12689
|
-
#
|
|
13242
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
13243
|
+
#drag = null;
|
|
12690
13244
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
12691
13245
|
get #mirrored() {
|
|
12692
13246
|
return this.logicalTrackValue && isRtl(this.element);
|
|
12693
13247
|
}
|
|
12694
|
-
/** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
|
|
12695
13248
|
/**
|
|
12696
13249
|
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
12697
13250
|
* pass Stimulus delivers before `connect()`.
|
|
12698
13251
|
*/
|
|
12699
|
-
#repaint = new MicrotaskCoalescer(() =>
|
|
12700
|
-
this.#render(this.startValue, this.endValue);
|
|
12701
|
-
});
|
|
13252
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
12702
13253
|
connect() {
|
|
12703
13254
|
this.#repaint.activate();
|
|
12704
|
-
const
|
|
12705
|
-
const
|
|
12706
|
-
this.#commit(
|
|
13255
|
+
const range = this.#effectiveRange;
|
|
13256
|
+
const pair = this.#currentPair(range);
|
|
13257
|
+
this.#commit(pair.start, pair.end, null, false);
|
|
12707
13258
|
}
|
|
12708
13259
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
12709
13260
|
disconnect() {
|
|
12710
13261
|
this.#repaint.cancel();
|
|
12711
|
-
this.#
|
|
12712
|
-
this.#dragAbort = null;
|
|
13262
|
+
this.#endDrag();
|
|
12713
13263
|
}
|
|
12714
13264
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
12715
13265
|
minValueChanged() {
|
|
@@ -12719,141 +13269,275 @@ var RangeSliderController = class extends Controller {
|
|
|
12719
13269
|
maxValueChanged() {
|
|
12720
13270
|
this.#repaint.schedule();
|
|
12721
13271
|
}
|
|
13272
|
+
/** Repaints when application code (or a Turbo morph) changes `step` at runtime. */
|
|
13273
|
+
stepValueChanged() {
|
|
13274
|
+
this.#repaint.schedule();
|
|
13275
|
+
}
|
|
13276
|
+
/** Repaints when application code (or a Turbo morph) changes `start` at runtime. */
|
|
13277
|
+
startValueChanged() {
|
|
13278
|
+
this.#repaint.schedule();
|
|
13279
|
+
}
|
|
13280
|
+
/** Repaints when application code (or a Turbo morph) changes `end` at runtime. */
|
|
13281
|
+
endValueChanged() {
|
|
13282
|
+
this.#repaint.schedule();
|
|
13283
|
+
}
|
|
13284
|
+
/** Hydrates a replacement start thumb and restores live-drag focus ownership. */
|
|
13285
|
+
startThumbTargetConnected(thumb) {
|
|
13286
|
+
const range = this.#effectiveRange;
|
|
13287
|
+
const pair = this.#currentPair(range);
|
|
13288
|
+
this.#renderStartThumb(thumb, pair, range);
|
|
13289
|
+
this.#renderFractions(pair, range);
|
|
13290
|
+
if (this.#drag?.kind === "start" && this.#drag.thumb === null) {
|
|
13291
|
+
this.#drag.thumb = thumb;
|
|
13292
|
+
thumb.focus();
|
|
13293
|
+
}
|
|
13294
|
+
this.#repaint.schedule();
|
|
13295
|
+
}
|
|
13296
|
+
/** Drops a stale start-thumb reference without orphaning the track gesture. */
|
|
13297
|
+
startThumbTargetDisconnected(thumb) {
|
|
13298
|
+
if (this.#drag?.kind === "start" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
13299
|
+
}
|
|
13300
|
+
/** Hydrates a replacement end thumb and restores live-drag focus ownership. */
|
|
13301
|
+
endThumbTargetConnected(thumb) {
|
|
13302
|
+
const range = this.#effectiveRange;
|
|
13303
|
+
const pair = this.#currentPair(range);
|
|
13304
|
+
this.#renderEndThumb(thumb, pair, range);
|
|
13305
|
+
this.#renderFractions(pair, range);
|
|
13306
|
+
if (this.#drag?.kind === "end" && this.#drag.thumb === null) {
|
|
13307
|
+
this.#drag.thumb = thumb;
|
|
13308
|
+
thumb.focus();
|
|
13309
|
+
}
|
|
13310
|
+
this.#repaint.schedule();
|
|
13311
|
+
}
|
|
13312
|
+
/** Drops a stale end-thumb reference without orphaning the track gesture. */
|
|
13313
|
+
endThumbTargetDisconnected(thumb) {
|
|
13314
|
+
if (this.#drag?.kind === "end" && this.#drag.thumb === thumb) this.#drag.thumb = null;
|
|
13315
|
+
}
|
|
13316
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
13317
|
+
trackTargetDisconnected(track) {
|
|
13318
|
+
if (this.#drag?.track === track) this.#endDrag();
|
|
13319
|
+
}
|
|
12722
13320
|
/** Keyboard stepping for whichever thumb is focused (the action's element). */
|
|
12723
13321
|
onKeydown(event) {
|
|
12724
13322
|
if (isReservedArrowChord(event)) return;
|
|
12725
13323
|
const thumb = event.currentTarget;
|
|
12726
13324
|
const isStart = this.hasStartThumbTarget && thumb === this.startThumbTarget;
|
|
12727
|
-
const
|
|
12728
|
-
const
|
|
12729
|
-
const
|
|
12730
|
-
|
|
13325
|
+
const isEnd = this.hasEndThumbTarget && thumb === this.endThumbTarget;
|
|
13326
|
+
const effectiveRange = this.#effectiveRange;
|
|
13327
|
+
const pair = this.#currentPair(effectiveRange);
|
|
13328
|
+
let kind;
|
|
13329
|
+
let current;
|
|
13330
|
+
let range;
|
|
13331
|
+
if (isStart) {
|
|
13332
|
+
kind = "start";
|
|
13333
|
+
current = pair.start;
|
|
13334
|
+
range = {
|
|
13335
|
+
min: effectiveRange.min,
|
|
13336
|
+
max: pair.end,
|
|
13337
|
+
step: effectiveRange.step,
|
|
13338
|
+
base: effectiveRange.min
|
|
13339
|
+
};
|
|
13340
|
+
} else {
|
|
13341
|
+
if (!isEnd) return;
|
|
13342
|
+
kind = "end";
|
|
13343
|
+
current = pair.end;
|
|
13344
|
+
range = {
|
|
13345
|
+
min: pair.start,
|
|
13346
|
+
max: effectiveRange.max,
|
|
13347
|
+
step: effectiveRange.step,
|
|
13348
|
+
base: effectiveRange.min
|
|
13349
|
+
};
|
|
13350
|
+
}
|
|
12731
13351
|
let next = null;
|
|
12732
13352
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
12733
13353
|
case "ArrowRight":
|
|
12734
13354
|
case "ArrowUp":
|
|
12735
|
-
next = current
|
|
13355
|
+
next = stepSteppedValue(current, 1, range);
|
|
12736
13356
|
break;
|
|
12737
13357
|
case "ArrowLeft":
|
|
12738
13358
|
case "ArrowDown":
|
|
12739
|
-
next = current -
|
|
13359
|
+
next = stepSteppedValue(current, -1, range);
|
|
12740
13360
|
break;
|
|
12741
13361
|
case "PageUp":
|
|
12742
|
-
next = current
|
|
13362
|
+
next = stepSteppedValue(current, 10, range);
|
|
12743
13363
|
break;
|
|
12744
13364
|
case "PageDown":
|
|
12745
|
-
next = current -
|
|
13365
|
+
next = stepSteppedValue(current, -10, range);
|
|
12746
13366
|
break;
|
|
12747
13367
|
case "Home":
|
|
12748
|
-
next =
|
|
13368
|
+
next = range.min;
|
|
12749
13369
|
break;
|
|
12750
13370
|
case "End":
|
|
12751
|
-
next =
|
|
13371
|
+
next = range.max;
|
|
12752
13372
|
break;
|
|
12753
13373
|
default:
|
|
12754
13374
|
return;
|
|
12755
13375
|
}
|
|
12756
13376
|
event.preventDefault();
|
|
12757
|
-
this.#moveThumb(
|
|
13377
|
+
this.#moveThumb(kind, next);
|
|
12758
13378
|
}
|
|
12759
13379
|
/** Begins a pointer drag on the track, moving the thumb nearest the press. */
|
|
12760
13380
|
onPointerDown(event) {
|
|
12761
|
-
if (!this.hasTrackTarget)
|
|
13381
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasStartThumbTarget || !this.hasEndThumbTarget) {
|
|
13382
|
+
return;
|
|
13383
|
+
}
|
|
13384
|
+
const track = this.trackTarget;
|
|
12762
13385
|
const mirrored = this.#mirrored;
|
|
12763
|
-
const value = this.#valueFromClientX(event.clientX, mirrored);
|
|
13386
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
12764
13387
|
if (value === null) return;
|
|
12765
13388
|
event.preventDefault();
|
|
12766
|
-
const
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
13389
|
+
const pair = this.#currentPair(this.#effectiveRange);
|
|
13390
|
+
const kind = this.#nearestThumb(value, pair);
|
|
13391
|
+
let thumb;
|
|
13392
|
+
if (kind === "start") thumb = this.startThumbTarget;
|
|
13393
|
+
else thumb = this.endThumbTarget;
|
|
13394
|
+
this.#moveThumb(kind, value);
|
|
13395
|
+
thumb.focus();
|
|
13396
|
+
const drag = { pointer: null, track, thumb, kind };
|
|
13397
|
+
drag.pointer = new OwnedPointerSession(event, track, {
|
|
13398
|
+
move: (move) => {
|
|
13399
|
+
if (!track.isConnected) {
|
|
13400
|
+
this.#endDrag();
|
|
13401
|
+
return;
|
|
13402
|
+
}
|
|
13403
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
13404
|
+
if (moved !== null) this.#moveThumb(kind, moved);
|
|
13405
|
+
},
|
|
13406
|
+
end: () => {
|
|
13407
|
+
if (this.#drag === drag) this.#drag = null;
|
|
13408
|
+
}
|
|
13409
|
+
});
|
|
13410
|
+
this.#drag = drag;
|
|
12787
13411
|
}
|
|
12788
13412
|
/** Maps a pointer X coordinate to a raw value using the track geometry. */
|
|
12789
|
-
#valueFromClientX(clientX, mirrored) {
|
|
12790
|
-
const rect =
|
|
13413
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
13414
|
+
const rect = track.getBoundingClientRect();
|
|
12791
13415
|
if (rect.width === 0) return null;
|
|
12792
13416
|
const offset = (clientX - rect.left) / rect.width;
|
|
12793
13417
|
const fraction = mirrored ? 1 - offset : offset;
|
|
12794
|
-
|
|
13418
|
+
const range = this.#effectiveRange;
|
|
13419
|
+
return range.min + fraction * (range.max - range.min);
|
|
13420
|
+
}
|
|
13421
|
+
/**
|
|
13422
|
+
* Chooses the closest thumb while keeping an overlapped pair expandable.
|
|
13423
|
+
* Ordinary midpoint ties stay deterministic on `start`; at an overlap, a
|
|
13424
|
+
* press above the shared value selects `end` and a press below selects
|
|
13425
|
+
* `start`.
|
|
13426
|
+
*/
|
|
13427
|
+
#nearestThumb(value, pair) {
|
|
13428
|
+
const startDistance = Math.abs(value - pair.start);
|
|
13429
|
+
const endDistance = Math.abs(value - pair.end);
|
|
13430
|
+
if (endDistance < startDistance) return "end";
|
|
13431
|
+
if (pair.start === pair.end && value > pair.start) return "end";
|
|
13432
|
+
return "start";
|
|
12795
13433
|
}
|
|
12796
13434
|
/** Moves one thumb to a new raw value, keeping the pair ordered. */
|
|
12797
|
-
#moveThumb(
|
|
12798
|
-
|
|
12799
|
-
|
|
13435
|
+
#moveThumb(kind, raw) {
|
|
13436
|
+
const pair = this.#currentPair(this.#effectiveRange);
|
|
13437
|
+
if (kind === "start") {
|
|
13438
|
+
this.#commit(raw, pair.end, "start", true);
|
|
12800
13439
|
} else {
|
|
12801
|
-
this.#commit(
|
|
13440
|
+
this.#commit(pair.start, raw, "end", true);
|
|
12802
13441
|
}
|
|
12803
13442
|
}
|
|
12804
13443
|
/**
|
|
12805
13444
|
* Clamps and snaps `start`/`end`, enforces `start ≤ end`, stores the pair, and
|
|
12806
13445
|
* reflects it onto the thumbs' ARIA attributes and the range custom
|
|
12807
|
-
* properties. Dispatches `change` only
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
13446
|
+
* properties. Dispatches `stimeo--range-slider:change` only when a
|
|
13447
|
+
* user-driven update changes the normalized pair, with
|
|
13448
|
+
* `{ start: number, end: number }` in `detail`.
|
|
13449
|
+
*/
|
|
13450
|
+
#commit(start, end, moving, notify) {
|
|
13451
|
+
const range = this.#effectiveRange;
|
|
13452
|
+
const previous = this.#currentPair(range);
|
|
13453
|
+
let nextStart = snapSteppedValue(start, range);
|
|
13454
|
+
let nextEnd = snapSteppedValue(end, range);
|
|
12814
13455
|
if (nextStart > nextEnd) {
|
|
12815
|
-
if (
|
|
12816
|
-
else nextEnd = nextStart;
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
this.
|
|
12820
|
-
this
|
|
12821
|
-
|
|
13456
|
+
if (moving === "start") nextStart = nextEnd;
|
|
13457
|
+
else if (moving === "end") nextEnd = nextStart;
|
|
13458
|
+
else [nextStart, nextEnd] = [nextEnd, nextStart];
|
|
13459
|
+
}
|
|
13460
|
+
if (!Object.is(this.startValue, nextStart)) this.startValue = nextStart;
|
|
13461
|
+
if (!Object.is(this.endValue, nextEnd)) this.endValue = nextEnd;
|
|
13462
|
+
const pair = { start: nextStart, end: nextEnd };
|
|
13463
|
+
this.#renderPair(pair, range);
|
|
13464
|
+
if (notify && (nextStart !== previous.start || nextEnd !== previous.end)) {
|
|
12822
13465
|
this.dispatch("change", { detail: { start: nextStart, end: nextEnd } });
|
|
12823
13466
|
}
|
|
12824
13467
|
}
|
|
12825
|
-
/**
|
|
12826
|
-
|
|
13468
|
+
/**
|
|
13469
|
+
* Reflects morph-supplied Values without writing them back or dispatching.
|
|
13470
|
+
*
|
|
13471
|
+
* @stimeoRenderRoot
|
|
13472
|
+
*/
|
|
13473
|
+
#render() {
|
|
13474
|
+
const range = this.#effectiveRange;
|
|
13475
|
+
this.#renderPair(this.#currentPair(range), range);
|
|
13476
|
+
}
|
|
13477
|
+
/** Reflects one normalized pair onto both thumbs and CSS properties. */
|
|
13478
|
+
#renderPair(pair, range) {
|
|
12827
13479
|
if (this.hasStartThumbTarget) {
|
|
12828
|
-
this.startThumbTarget
|
|
12829
|
-
this.startThumbTarget.setAttribute("aria-valuemax", String(end));
|
|
12830
|
-
this.startThumbTarget.setAttribute("aria-valuenow", String(start));
|
|
13480
|
+
this.#renderStartThumb(this.startThumbTarget, pair, range);
|
|
12831
13481
|
}
|
|
12832
13482
|
if (this.hasEndThumbTarget) {
|
|
12833
|
-
this.endThumbTarget
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
|
|
12837
|
-
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
);
|
|
12841
|
-
this.
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
13483
|
+
this.#renderEndThumb(this.endThumbTarget, pair, range);
|
|
13484
|
+
}
|
|
13485
|
+
this.#renderFractions(pair, range);
|
|
13486
|
+
}
|
|
13487
|
+
/** Writes only changed ARIA for the lower thumb. */
|
|
13488
|
+
#renderStartThumb(thumb, pair, range) {
|
|
13489
|
+
this.#setAria(thumb, "aria-valuemin", range.min);
|
|
13490
|
+
this.#setAria(thumb, "aria-valuemax", pair.end);
|
|
13491
|
+
this.#setAria(thumb, "aria-valuenow", pair.start);
|
|
13492
|
+
}
|
|
13493
|
+
/** Writes only changed ARIA for the upper thumb. */
|
|
13494
|
+
#renderEndThumb(thumb, pair, range) {
|
|
13495
|
+
this.#setAria(thumb, "aria-valuemin", pair.start);
|
|
13496
|
+
this.#setAria(thumb, "aria-valuemax", range.max);
|
|
13497
|
+
this.#setAria(thumb, "aria-valuenow", pair.end);
|
|
13498
|
+
}
|
|
13499
|
+
/** Writes one numeric ARIA attribute only when its serialized value changed. */
|
|
13500
|
+
#setAria(thumb, name, value) {
|
|
13501
|
+
const next = String(value);
|
|
13502
|
+
if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
|
|
13503
|
+
}
|
|
13504
|
+
/** Writes the two behavior-only fraction hooks only when they changed. */
|
|
13505
|
+
#renderFractions(pair, range) {
|
|
13506
|
+
const start = String(rangeFraction(pair.start, range.min, range.max));
|
|
13507
|
+
const end = String(rangeFraction(pair.end, range.min, range.max));
|
|
13508
|
+
if (this.element.style.getPropertyValue(START_PROPERTY) !== start) {
|
|
13509
|
+
this.element.style.setProperty(START_PROPERTY, start);
|
|
13510
|
+
}
|
|
13511
|
+
if (this.element.style.getPropertyValue(END_PROPERTY) !== end) {
|
|
13512
|
+
this.element.style.setProperty(END_PROPERTY, end);
|
|
13513
|
+
}
|
|
13514
|
+
}
|
|
13515
|
+
/** Current normalized and ordered pair derived from live declarative Values. */
|
|
13516
|
+
#currentPair(range) {
|
|
13517
|
+
const rawStart = Number.isFinite(this.startValue) ? this.startValue : range.min;
|
|
13518
|
+
const rawEnd = Number.isFinite(this.endValue) ? this.endValue : range.max;
|
|
13519
|
+
const start = snapSteppedValue(rawStart, range);
|
|
13520
|
+
const end = snapSteppedValue(rawEnd, range);
|
|
13521
|
+
if (start <= end) return { start, end };
|
|
13522
|
+
return { start: end, end: start };
|
|
13523
|
+
}
|
|
13524
|
+
/**
|
|
13525
|
+
* Finite ordered range used by ARIA, keyboard, pointer, and CSS reflection.
|
|
13526
|
+
* Invalid endpoints fall back to the public defaults; an authored `max`
|
|
13527
|
+
* below `min` collapses to the finite minimum.
|
|
13528
|
+
*/
|
|
13529
|
+
get #effectiveRange() {
|
|
13530
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
|
|
13531
|
+
const authoredMax = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
|
|
13532
|
+
return { min, max: Math.max(min, authoredMax), step: this.stepValue, base: min };
|
|
13533
|
+
}
|
|
13534
|
+
/** Ends the current pointer session without dispatching another change. */
|
|
13535
|
+
#endDrag() {
|
|
13536
|
+
const drag = this.#drag;
|
|
13537
|
+
this.#drag = null;
|
|
13538
|
+
drag?.pointer?.end();
|
|
12852
13539
|
}
|
|
12853
13540
|
};
|
|
12854
|
-
function isUserMovingStart(start, prevStart, end, prevEnd) {
|
|
12855
|
-
return start !== prevStart && end === prevEnd;
|
|
12856
|
-
}
|
|
12857
13541
|
var RatingController = class extends Controller {
|
|
12858
13542
|
static targets = ["symbol", "field"];
|
|
12859
13543
|
static values = {
|
|
@@ -12949,6 +13633,8 @@ var RatingController = class extends Controller {
|
|
|
12949
13633
|
* Applies `value` (already clamped) everywhere, then dispatches `change`.
|
|
12950
13634
|
* Use for user-driven changes; on connect call `#apply` directly so
|
|
12951
13635
|
* initialization mirrors state without emitting an event.
|
|
13636
|
+
*
|
|
13637
|
+
* @stimeoRenderRoot
|
|
12952
13638
|
*/
|
|
12953
13639
|
#render(value, { focus }) {
|
|
12954
13640
|
this.#apply(value, { focus });
|
|
@@ -13264,6 +13950,8 @@ var RelativeTimeController = class extends Controller {
|
|
|
13264
13950
|
* polling can stop: a *past* timestamp that fell back to the absolute text can
|
|
13265
13951
|
* never leave it again, and a locale the runtime rejects has nothing to render
|
|
13266
13952
|
* until that value is corrected.
|
|
13953
|
+
*
|
|
13954
|
+
* @stimeoRenderRoot
|
|
13267
13955
|
*/
|
|
13268
13956
|
#applyAndComputeDelay() {
|
|
13269
13957
|
const deltaMs = this.#targetMs - Date.now();
|
|
@@ -13678,7 +14366,7 @@ var ScrollAreaController = class extends Controller {
|
|
|
13678
14366
|
this.#syncKeyboardReach(vp, overflowing);
|
|
13679
14367
|
const { position, progress } = this.#measurePosition(vp);
|
|
13680
14368
|
this.element.setAttribute("data-scroll", position);
|
|
13681
|
-
this.element.style.setProperty("--stimeo
|
|
14369
|
+
this.element.style.setProperty("--stimeo--scroll-progress", String(progress));
|
|
13682
14370
|
const edge = position === "start" ? "start" : position === "end" ? "end" : null;
|
|
13683
14371
|
if (overflowing && edge && edge !== this.#lastEdge) {
|
|
13684
14372
|
this.#lastEdge = edge;
|
|
@@ -14372,19 +15060,13 @@ var SeparatorController = class extends Controller {
|
|
|
14372
15060
|
static actions = ["onKeydown"];
|
|
14373
15061
|
static events = ["change"];
|
|
14374
15062
|
connect() {
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
}
|
|
14378
|
-
if (!this.element.hasAttribute("aria-orientation")) {
|
|
14379
|
-
this.element.setAttribute("aria-orientation", this.orientationValue);
|
|
14380
|
-
}
|
|
15063
|
+
setDefaultAttribute(this.element, "role", "separator");
|
|
15064
|
+
setDefaultAttribute(this.element, "aria-orientation", this.orientationValue);
|
|
14381
15065
|
if (this.focusableValue) {
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
this
|
|
14386
|
-
this.#setDefault("aria-valuemax", "100");
|
|
14387
|
-
this.#setDefault("aria-valuenow", String(this.#clamp(this.#value)));
|
|
15066
|
+
setDefaultAttribute(this.element, "tabindex", "0");
|
|
15067
|
+
setDefaultAttribute(this.element, "aria-valuemin", "0");
|
|
15068
|
+
setDefaultAttribute(this.element, "aria-valuemax", "100");
|
|
15069
|
+
setDefaultAttribute(this.element, "aria-valuenow", String(this.#clamp(this.#value)));
|
|
14388
15070
|
}
|
|
14389
15071
|
}
|
|
14390
15072
|
/** Adjusts the value on arrow / Home / End keys (focusable variant only). */
|
|
@@ -14438,11 +15120,6 @@ var SeparatorController = class extends Controller {
|
|
|
14438
15120
|
const parsed = Number.parseFloat(this.element.getAttribute(name) ?? "");
|
|
14439
15121
|
return Number.isNaN(parsed) ? fallback : parsed;
|
|
14440
15122
|
}
|
|
14441
|
-
#setDefault(name, value) {
|
|
14442
|
-
if (!this.element.hasAttribute(name)) {
|
|
14443
|
-
this.element.setAttribute(name, value);
|
|
14444
|
-
}
|
|
14445
|
-
}
|
|
14446
15123
|
};
|
|
14447
15124
|
|
|
14448
15125
|
// src/utils/safe_storage.ts
|
|
@@ -14761,14 +15438,16 @@ var SkeletonController = class extends Controller {
|
|
|
14761
15438
|
static events = ["ready"];
|
|
14762
15439
|
#timers = new SafeTimeout();
|
|
14763
15440
|
#floor = new MinDurationFloor(this.#timers);
|
|
15441
|
+
#gate = new DetachGate();
|
|
14764
15442
|
connect() {
|
|
14765
|
-
|
|
15443
|
+
const moved = this.#gate.pending;
|
|
15444
|
+
this.#gate.cancel();
|
|
15445
|
+
if (!moved && this.#state !== "ready") {
|
|
14766
15446
|
this.#enterLoading();
|
|
14767
15447
|
}
|
|
14768
15448
|
}
|
|
14769
15449
|
disconnect() {
|
|
14770
|
-
this.#
|
|
14771
|
-
this.#floor.cancel();
|
|
15450
|
+
this.#gate.disconnected(this, () => this.#teardown());
|
|
14772
15451
|
}
|
|
14773
15452
|
/** Swaps to the real content. Honors `minDuration` to prevent a flash. */
|
|
14774
15453
|
ready() {
|
|
@@ -14797,6 +15476,17 @@ var SkeletonController = class extends Controller {
|
|
|
14797
15476
|
this.dispatch("ready", { detail: {} });
|
|
14798
15477
|
announce(fillTemplate(this.announceReadyTextValue, {}));
|
|
14799
15478
|
}
|
|
15479
|
+
/**
|
|
15480
|
+
* Drops the held reveal on a real detach. The markup keeps whatever it last
|
|
15481
|
+
* held: an element on its way out of the document has no reader left, and one
|
|
15482
|
+
* whose `data-controller` dropped the identifier no longer resolves its own
|
|
15483
|
+
* targets, so the rollback could only ever be partial.
|
|
15484
|
+
*/
|
|
15485
|
+
#teardown() {
|
|
15486
|
+
this.#gate.cancel();
|
|
15487
|
+
this.#timers.clearAll();
|
|
15488
|
+
this.#floor.cancel();
|
|
15489
|
+
}
|
|
14800
15490
|
/** Current lifecycle phase as reflected on `data-state`. */
|
|
14801
15491
|
get #state() {
|
|
14802
15492
|
return this.element.getAttribute("data-state") ?? "loading";
|
|
@@ -14814,40 +15504,79 @@ var SliderController = class extends Controller {
|
|
|
14814
15504
|
};
|
|
14815
15505
|
static actions = ["onKeydown", "onPointerDown"];
|
|
14816
15506
|
static events = ["change"];
|
|
14817
|
-
/**
|
|
14818
|
-
#
|
|
15507
|
+
/** One initiating pointer owns each live drag and its stable target snapshot. */
|
|
15508
|
+
#drag = null;
|
|
15509
|
+
/** Collapses a morph that swaps several render Values into one silent repaint. */
|
|
15510
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
14819
15511
|
/** Whether the consumer declared a mirroring track and the direction mirrors it. */
|
|
14820
15512
|
get #mirrored() {
|
|
14821
15513
|
return this.logicalTrackValue && isRtl(this.element);
|
|
14822
15514
|
}
|
|
14823
15515
|
/** Clamps the initial value and renders the starting position. */
|
|
14824
15516
|
connect() {
|
|
14825
|
-
this.#
|
|
15517
|
+
this.#repaint.activate();
|
|
15518
|
+
this.#commit(this.valueValue, false);
|
|
14826
15519
|
}
|
|
14827
15520
|
/** Cancels any active pointer drag so document listeners never leak. */
|
|
14828
15521
|
disconnect() {
|
|
14829
|
-
this.#
|
|
14830
|
-
this.#
|
|
15522
|
+
this.#repaint.cancel();
|
|
15523
|
+
this.#endDrag();
|
|
15524
|
+
}
|
|
15525
|
+
/** Silently repaints a minimum changed by application code or a Turbo morph. */
|
|
15526
|
+
minValueChanged() {
|
|
15527
|
+
this.#repaint.schedule();
|
|
15528
|
+
}
|
|
15529
|
+
/** Silently repaints a maximum changed by application code or a Turbo morph. */
|
|
15530
|
+
maxValueChanged() {
|
|
15531
|
+
this.#repaint.schedule();
|
|
15532
|
+
}
|
|
15533
|
+
/** Silently repaints a step changed by application code or a Turbo morph. */
|
|
15534
|
+
stepValueChanged() {
|
|
15535
|
+
this.#repaint.schedule();
|
|
15536
|
+
}
|
|
15537
|
+
/** Silently repaints a value changed by application code or a Turbo morph. */
|
|
15538
|
+
valueValueChanged() {
|
|
15539
|
+
this.#repaint.schedule();
|
|
15540
|
+
}
|
|
15541
|
+
/** Hydrates a thumb inserted or replaced at runtime with the current ARIA state. */
|
|
15542
|
+
thumbTargetConnected(thumb) {
|
|
15543
|
+
const value = this.#currentValue();
|
|
15544
|
+
this.#renderThumb(thumb, value);
|
|
15545
|
+
this.#renderFraction(value);
|
|
15546
|
+
if (this.#drag && this.#drag.thumb === null) {
|
|
15547
|
+
this.#drag.thumb = thumb;
|
|
15548
|
+
thumb.focus();
|
|
15549
|
+
}
|
|
15550
|
+
this.#repaint.schedule();
|
|
15551
|
+
}
|
|
15552
|
+
/** Drops the stale focus owner while allowing a live track gesture to continue. */
|
|
15553
|
+
thumbTargetDisconnected(thumb) {
|
|
15554
|
+
if (this.#drag?.thumb === thumb) this.#drag.thumb = null;
|
|
15555
|
+
}
|
|
15556
|
+
/** Ends a gesture whose geometry target disappeared or ceased being a target. */
|
|
15557
|
+
trackTargetDisconnected(track) {
|
|
15558
|
+
if (this.#drag?.track === track) this.#endDrag();
|
|
14831
15559
|
}
|
|
14832
15560
|
/** Handles keyboard stepping per the APG slider model. */
|
|
14833
15561
|
onKeydown(event) {
|
|
14834
15562
|
if (isReservedArrowChord(event)) return;
|
|
14835
|
-
const big = this.stepValue * 10;
|
|
14836
15563
|
let next = null;
|
|
15564
|
+
const current = this.#currentValue();
|
|
15565
|
+
const range = this.#steppedRange;
|
|
14837
15566
|
switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
|
|
14838
15567
|
case "ArrowRight":
|
|
14839
15568
|
case "ArrowUp":
|
|
14840
|
-
next =
|
|
15569
|
+
next = stepSteppedValue(current, 1, range);
|
|
14841
15570
|
break;
|
|
14842
15571
|
case "ArrowLeft":
|
|
14843
15572
|
case "ArrowDown":
|
|
14844
|
-
next =
|
|
15573
|
+
next = stepSteppedValue(current, -1, range);
|
|
14845
15574
|
break;
|
|
14846
15575
|
case "PageUp":
|
|
14847
|
-
next =
|
|
15576
|
+
next = stepSteppedValue(current, 10, range);
|
|
14848
15577
|
break;
|
|
14849
15578
|
case "PageDown":
|
|
14850
|
-
next =
|
|
15579
|
+
next = stepSteppedValue(current, -10, range);
|
|
14851
15580
|
break;
|
|
14852
15581
|
case "Home":
|
|
14853
15582
|
next = this.minValue;
|
|
@@ -14859,56 +15588,102 @@ var SliderController = class extends Controller {
|
|
|
14859
15588
|
return;
|
|
14860
15589
|
}
|
|
14861
15590
|
event.preventDefault();
|
|
14862
|
-
this.#
|
|
15591
|
+
this.#commit(next, true);
|
|
14863
15592
|
}
|
|
14864
15593
|
/** Begins a pointer drag: sets the value and tracks subsequent movement. */
|
|
14865
15594
|
onPointerDown(event) {
|
|
14866
|
-
if (!this.hasTrackTarget) return;
|
|
14867
|
-
|
|
15595
|
+
if (event.button !== 0 || this.#drag || !this.hasTrackTarget || !this.hasThumbTarget) return;
|
|
15596
|
+
const track = this.trackTarget;
|
|
15597
|
+
const thumb = this.thumbTarget;
|
|
14868
15598
|
const mirrored = this.#mirrored;
|
|
14869
|
-
this.#
|
|
14870
|
-
if (
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
const
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
15599
|
+
const value = this.#valueFromClientX(event.clientX, mirrored, track);
|
|
15600
|
+
if (value === null) return;
|
|
15601
|
+
event.preventDefault();
|
|
15602
|
+
this.#commit(value, true);
|
|
15603
|
+
thumb.focus();
|
|
15604
|
+
const drag = { pointer: null, track, thumb };
|
|
15605
|
+
drag.pointer = new OwnedPointerSession(event, track, {
|
|
15606
|
+
move: (move) => {
|
|
15607
|
+
if (!track.isConnected) {
|
|
15608
|
+
this.#endDrag();
|
|
15609
|
+
return;
|
|
15610
|
+
}
|
|
15611
|
+
const moved = this.#valueFromClientX(move.clientX, mirrored, track);
|
|
15612
|
+
if (moved !== null) this.#commit(moved, true);
|
|
15613
|
+
},
|
|
15614
|
+
end: () => {
|
|
15615
|
+
if (this.#drag === drag) this.#drag = null;
|
|
15616
|
+
}
|
|
15617
|
+
});
|
|
15618
|
+
this.#drag = drag;
|
|
14882
15619
|
}
|
|
14883
|
-
/** Maps a pointer X coordinate to a value using
|
|
14884
|
-
#
|
|
14885
|
-
const rect =
|
|
14886
|
-
if (rect.width === 0) return;
|
|
15620
|
+
/** Maps a pointer X coordinate to a raw value using a stable track snapshot. */
|
|
15621
|
+
#valueFromClientX(clientX, mirrored, track) {
|
|
15622
|
+
const rect = track.getBoundingClientRect();
|
|
15623
|
+
if (rect.width === 0) return null;
|
|
14887
15624
|
const offset = (clientX - rect.left) / rect.width;
|
|
14888
15625
|
const fraction = mirrored ? 1 - offset : offset;
|
|
14889
|
-
this
|
|
15626
|
+
return this.minValue + fraction * (this.maxValue - this.minValue);
|
|
14890
15627
|
}
|
|
14891
15628
|
/**
|
|
14892
|
-
*
|
|
14893
|
-
*
|
|
14894
|
-
*
|
|
14895
|
-
* change — symmetric with `range-slider` — unless `silent` (the initial
|
|
14896
|
-
* connect render, which is not a user edit).
|
|
15629
|
+
* Stores a normalized value and renders synchronously for responsive input.
|
|
15630
|
+
* Morph callbacks use {@link #render} instead, so they never write Values back
|
|
15631
|
+
* or dispatch a user-facing change event.
|
|
14897
15632
|
*/
|
|
14898
|
-
#
|
|
14899
|
-
const
|
|
14900
|
-
const
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
this.
|
|
15633
|
+
#commit(raw, notify) {
|
|
15634
|
+
const previous = this.#currentValue();
|
|
15635
|
+
const value = snapSteppedValue(raw, this.#steppedRange);
|
|
15636
|
+
if (!Object.is(this.valueValue, value)) this.valueValue = value;
|
|
15637
|
+
this.#renderValue(value);
|
|
15638
|
+
if (notify && value !== previous) this.dispatch("change", { detail: { value } });
|
|
15639
|
+
}
|
|
15640
|
+
/**
|
|
15641
|
+
* Reflects the normalized current Value without mutating or dispatching it.
|
|
15642
|
+
*
|
|
15643
|
+
* @stimeoRenderRoot
|
|
15644
|
+
*/
|
|
15645
|
+
#render() {
|
|
15646
|
+
this.#renderValue(this.#currentValue());
|
|
15647
|
+
}
|
|
15648
|
+
/** Reflects one normalized value on the current target and CSS output. */
|
|
15649
|
+
#renderValue(value) {
|
|
14904
15650
|
if (this.hasThumbTarget) {
|
|
14905
|
-
this.thumbTarget
|
|
14906
|
-
|
|
14907
|
-
|
|
15651
|
+
this.#renderThumb(this.thumbTarget, value);
|
|
15652
|
+
}
|
|
15653
|
+
this.#renderFraction(value);
|
|
15654
|
+
}
|
|
15655
|
+
/** Writes only ARIA values that differ, including on a replacement target. */
|
|
15656
|
+
#renderThumb(thumb, value) {
|
|
15657
|
+
const attributes = {
|
|
15658
|
+
"aria-valuemin": String(this.minValue),
|
|
15659
|
+
"aria-valuemax": String(this.maxValue),
|
|
15660
|
+
"aria-valuenow": String(value)
|
|
15661
|
+
};
|
|
15662
|
+
for (const [name, next] of Object.entries(attributes)) {
|
|
15663
|
+
if (thumb.getAttribute(name) !== next) thumb.setAttribute(name, next);
|
|
14908
15664
|
}
|
|
15665
|
+
}
|
|
15666
|
+
/** Writes the behavior-only positioning hook only when its value changed. */
|
|
15667
|
+
#renderFraction(value) {
|
|
14909
15668
|
const fraction = rangeFraction(value, this.minValue, this.maxValue);
|
|
14910
|
-
|
|
14911
|
-
if (
|
|
15669
|
+
const next = String(fraction);
|
|
15670
|
+
if (this.element.style.getPropertyValue(FRACTION_PROPERTY) !== next) {
|
|
15671
|
+
this.element.style.setProperty(FRACTION_PROPERTY, next);
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
/** Current normalized value derived from the live declarative inputs. */
|
|
15675
|
+
#currentValue() {
|
|
15676
|
+
return snapSteppedValue(this.valueValue, this.#steppedRange);
|
|
15677
|
+
}
|
|
15678
|
+
/** Shared range configuration; finite endpoints remain allowed off the grid. */
|
|
15679
|
+
get #steppedRange() {
|
|
15680
|
+
return { min: this.minValue, max: this.maxValue, step: this.stepValue };
|
|
15681
|
+
}
|
|
15682
|
+
/** Ends the current pointer session without dispatching another change. */
|
|
15683
|
+
#endDrag() {
|
|
15684
|
+
const drag = this.#drag;
|
|
15685
|
+
this.#drag = null;
|
|
15686
|
+
drag?.pointer?.end();
|
|
14912
15687
|
}
|
|
14913
15688
|
};
|
|
14914
15689
|
var SmartStickyHeaderController = class extends Controller {
|
|
@@ -15191,9 +15966,7 @@ var SpinnerController = class extends Controller {
|
|
|
15191
15966
|
this.element.setAttribute("data-state", "idle");
|
|
15192
15967
|
return;
|
|
15193
15968
|
}
|
|
15194
|
-
|
|
15195
|
-
this.element.setAttribute("data-state", "idle");
|
|
15196
|
-
}
|
|
15969
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
15197
15970
|
}
|
|
15198
15971
|
/**
|
|
15199
15972
|
* Re-applies the current phase to an indicator that arrived after `connect()`.
|
|
@@ -15391,6 +16164,8 @@ var StepIndicatorController = class extends Controller {
|
|
|
15391
16164
|
* A pure function of the step set and `current`, so running it again writes the
|
|
15392
16165
|
* same values — which is what lets the action path paint synchronously (the event
|
|
15393
16166
|
* goes out after the DOM is updated) while a coalesced pass may still follow.
|
|
16167
|
+
*
|
|
16168
|
+
* @stimeoRenderRoot
|
|
15394
16169
|
*/
|
|
15395
16170
|
#render() {
|
|
15396
16171
|
const total = this.stepTargets.length;
|
|
@@ -15404,7 +16179,7 @@ var StepIndicatorController = class extends Controller {
|
|
|
15404
16179
|
}
|
|
15405
16180
|
});
|
|
15406
16181
|
const ratio = total > 1 ? current / (total - 1) : 0;
|
|
15407
|
-
this.element.style.setProperty("--stimeo
|
|
16182
|
+
this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
|
|
15408
16183
|
}
|
|
15409
16184
|
/**
|
|
15410
16185
|
* Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
|
|
@@ -15485,6 +16260,8 @@ var StepperController = class extends Controller {
|
|
|
15485
16260
|
* `aria-current="step"` is placed on the step's **first** `<button>`; the markup
|
|
15486
16261
|
* contract assumes one operable button per step. If a step needs multiple
|
|
15487
16262
|
* buttons, mark the navigational one first (or this would target the wrong one).
|
|
16263
|
+
*
|
|
16264
|
+
* @stimeoRenderRoot
|
|
15488
16265
|
*/
|
|
15489
16266
|
#render(current) {
|
|
15490
16267
|
this.stepTargets.forEach((step, index) => {
|
|
@@ -15889,39 +16666,163 @@ var SubmitOnceController = class extends Controller {
|
|
|
15889
16666
|
}
|
|
15890
16667
|
}
|
|
15891
16668
|
};
|
|
16669
|
+
|
|
16670
|
+
// src/utils/interactive_host.ts
|
|
16671
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
16672
|
+
function isInteractiveHost(element) {
|
|
16673
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
16674
|
+
let current = element;
|
|
16675
|
+
while (current) {
|
|
16676
|
+
const raw = current.getAttribute("contenteditable");
|
|
16677
|
+
if (raw !== null) {
|
|
16678
|
+
const value = raw.trim().toLowerCase();
|
|
16679
|
+
if (value === "false") return false;
|
|
16680
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
16681
|
+
}
|
|
16682
|
+
current = current.parentElement;
|
|
16683
|
+
}
|
|
16684
|
+
return false;
|
|
16685
|
+
}
|
|
16686
|
+
|
|
16687
|
+
// src/controllers/switch_controller.ts
|
|
16688
|
+
var OBSERVED_ATTRIBUTES = [
|
|
16689
|
+
"role",
|
|
16690
|
+
"aria-checked",
|
|
16691
|
+
"tabindex",
|
|
16692
|
+
"type",
|
|
16693
|
+
"href",
|
|
16694
|
+
"contenteditable",
|
|
16695
|
+
"controls"
|
|
16696
|
+
];
|
|
16697
|
+
var OBSERVED_ANCESTOR_ATTRIBUTES = ["contenteditable"];
|
|
15892
16698
|
var SwitchController = class extends Controller {
|
|
15893
16699
|
static actions = ["onKeydown", "toggle"];
|
|
15894
16700
|
static events = ["changed"];
|
|
16701
|
+
/** Defaults this instance introduced and may therefore remove safely. */
|
|
16702
|
+
#ownedDefaults = /* @__PURE__ */ new Set();
|
|
16703
|
+
/** Controller writes that must not be mistaken for authored morph changes. */
|
|
16704
|
+
#internalAttributeValues = /* @__PURE__ */ new Map();
|
|
16705
|
+
#attributeObserver = new MutationObserver((records) => {
|
|
16706
|
+
this.#releaseAuthoredDefaults(records);
|
|
16707
|
+
this.#attributeObserver.disconnect();
|
|
16708
|
+
this.#reconcileDefaults();
|
|
16709
|
+
this.#observeAttributes();
|
|
16710
|
+
});
|
|
16711
|
+
/** Blocks disabled pointer/native-key activation before consumer click handlers. */
|
|
16712
|
+
#onClickCapture = (event) => {
|
|
16713
|
+
if (!this.#isSupportedHost || !this.#isActivationDisabled) return;
|
|
16714
|
+
event.preventDefault();
|
|
16715
|
+
event.stopImmediatePropagation();
|
|
16716
|
+
};
|
|
15895
16717
|
/** Ensures the switch exposes a role and is keyboard-reachable. */
|
|
15896
16718
|
connect() {
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
|
|
15901
|
-
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
|
|
16719
|
+
this.#reconcileDefaults();
|
|
16720
|
+
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
16721
|
+
this.#observeAttributes();
|
|
16722
|
+
}
|
|
16723
|
+
/** Releases the explicit click guard and retained-element attribute observer. */
|
|
16724
|
+
disconnect() {
|
|
16725
|
+
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
16726
|
+
this.#attributeObserver.disconnect();
|
|
16727
|
+
this.#internalAttributeValues.clear();
|
|
15906
16728
|
}
|
|
15907
16729
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
15908
16730
|
toggle() {
|
|
16731
|
+
if (!this.#isSupportedHost || this.#isActivationDisabled) return;
|
|
15909
16732
|
this.#checked = !this.#checked;
|
|
15910
16733
|
}
|
|
15911
16734
|
/**
|
|
15912
|
-
* Activates the switch on Space/Enter for non-native hosts and
|
|
15913
|
-
*
|
|
15914
|
-
*
|
|
15915
|
-
*
|
|
16735
|
+
* Activates the switch on Space/Enter for non-native hosts and suppresses key
|
|
16736
|
+
* repeat. Bound via `data-action` (keydown). A native `<button type="button">`
|
|
16737
|
+
* owns the initial key-to-click synthesis; repeated keydowns are canceled before
|
|
16738
|
+
* the browser can synthesize additional clicks.
|
|
15916
16739
|
*/
|
|
15917
16740
|
onKeydown(event) {
|
|
15918
16741
|
if (event.defaultPrevented) return;
|
|
15919
|
-
if (this
|
|
15920
|
-
if (event.
|
|
15921
|
-
if (
|
|
16742
|
+
if (!this.#isSupportedHost) return;
|
|
16743
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
16744
|
+
if (this.#isActivationDisabled) {
|
|
15922
16745
|
event.preventDefault();
|
|
15923
|
-
|
|
16746
|
+
event.stopImmediatePropagation();
|
|
16747
|
+
return;
|
|
16748
|
+
}
|
|
16749
|
+
if (this.element instanceof HTMLButtonElement) {
|
|
16750
|
+
if (event.repeat) event.preventDefault();
|
|
16751
|
+
return;
|
|
16752
|
+
}
|
|
16753
|
+
event.preventDefault();
|
|
16754
|
+
if (event.repeat) return;
|
|
16755
|
+
this.toggle();
|
|
16756
|
+
}
|
|
16757
|
+
/** Re-adds only missing defaults, preserving every authored attribute value. */
|
|
16758
|
+
#reconcileDefaults() {
|
|
16759
|
+
if (!this.#isSupportedHost) {
|
|
16760
|
+
this.#removeOwnedDefaults();
|
|
16761
|
+
return;
|
|
16762
|
+
}
|
|
16763
|
+
this.#setOwnedDefault("role", "switch");
|
|
16764
|
+
this.#setOwnedDefault("aria-checked", "false");
|
|
16765
|
+
if (!(this.element instanceof HTMLButtonElement)) {
|
|
16766
|
+
this.#setOwnedDefault("tabindex", "0");
|
|
16767
|
+
}
|
|
16768
|
+
}
|
|
16769
|
+
/** Records a newly introduced default without claiming authored markup. */
|
|
16770
|
+
#setOwnedDefault(name, value) {
|
|
16771
|
+
if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);
|
|
16772
|
+
}
|
|
16773
|
+
/**
|
|
16774
|
+
* Gives ownership back when a retained-element morph authors a value. Missing
|
|
16775
|
+
* owned attributes stay owned so reconciliation can restore them.
|
|
16776
|
+
*/
|
|
16777
|
+
#releaseAuthoredDefaults(records) {
|
|
16778
|
+
for (const record of records) {
|
|
16779
|
+
if (record.target === this.element) {
|
|
16780
|
+
const name = record.attributeName;
|
|
16781
|
+
if (name && this.#ownedDefaults.has(name)) {
|
|
16782
|
+
const value = this.element.getAttribute(name);
|
|
16783
|
+
if (value !== null && this.#internalAttributeValues.get(name) !== value) {
|
|
16784
|
+
this.#ownedDefaults.delete(name);
|
|
16785
|
+
}
|
|
16786
|
+
}
|
|
16787
|
+
}
|
|
16788
|
+
}
|
|
16789
|
+
this.#internalAttributeValues.clear();
|
|
16790
|
+
}
|
|
16791
|
+
/** Removes only defaults introduced by this instance when a host becomes invalid. */
|
|
16792
|
+
#removeOwnedDefaults() {
|
|
16793
|
+
for (const name of this.#ownedDefaults) this.element.removeAttribute(name);
|
|
16794
|
+
this.#ownedDefaults.clear();
|
|
16795
|
+
this.#internalAttributeValues.clear();
|
|
16796
|
+
}
|
|
16797
|
+
/** Watches retained host attributes that Turbo can morph without reconnecting. */
|
|
16798
|
+
#observeAttributes() {
|
|
16799
|
+
this.#attributeObserver.observe(this.element, {
|
|
16800
|
+
attributes: true,
|
|
16801
|
+
attributeFilter: OBSERVED_ATTRIBUTES
|
|
16802
|
+
});
|
|
16803
|
+
let ancestor = this.element.parentElement;
|
|
16804
|
+
while (ancestor) {
|
|
16805
|
+
this.#attributeObserver.observe(ancestor, {
|
|
16806
|
+
attributes: true,
|
|
16807
|
+
attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES
|
|
16808
|
+
});
|
|
16809
|
+
ancestor = ancestor.parentElement;
|
|
16810
|
+
}
|
|
16811
|
+
}
|
|
16812
|
+
/** Whether the host has one activation model that this controller can own. */
|
|
16813
|
+
get #isSupportedHost() {
|
|
16814
|
+
if (this.element instanceof HTMLButtonElement) return this.element.type === "button";
|
|
16815
|
+
return !isInteractiveHost(this.element);
|
|
16816
|
+
}
|
|
16817
|
+
/** Whether ARIA or native HTML semantics make the supported switch inoperable. */
|
|
16818
|
+
get #isActivationDisabled() {
|
|
16819
|
+
let current = this.element;
|
|
16820
|
+
while (current) {
|
|
16821
|
+
if (current.getAttribute("aria-disabled") === "true") return true;
|
|
16822
|
+
current = current.parentElement;
|
|
15924
16823
|
}
|
|
16824
|
+
if (!(this.element instanceof HTMLButtonElement)) return false;
|
|
16825
|
+
return this.element.disabled || inheritsFieldsetDisabled(this.element);
|
|
15925
16826
|
}
|
|
15926
16827
|
/** Whether the switch is currently on. */
|
|
15927
16828
|
get #checked() {
|
|
@@ -15929,7 +16830,9 @@ var SwitchController = class extends Controller {
|
|
|
15929
16830
|
}
|
|
15930
16831
|
/** Reflects the new state on `aria-checked` and notifies listeners. */
|
|
15931
16832
|
set #checked(value) {
|
|
15932
|
-
|
|
16833
|
+
const reflected = value ? "true" : "false";
|
|
16834
|
+
this.#internalAttributeValues.set("aria-checked", reflected);
|
|
16835
|
+
this.element.setAttribute("aria-checked", reflected);
|
|
15933
16836
|
this.dispatch("changed", { detail: { checked: value } });
|
|
15934
16837
|
}
|
|
15935
16838
|
};
|
|
@@ -16240,7 +17143,7 @@ var TextareaAutosizeController = class extends Controller {
|
|
|
16240
17143
|
} else {
|
|
16241
17144
|
el.removeAttribute("data-at-max-rows");
|
|
16242
17145
|
}
|
|
16243
|
-
el.style.setProperty("--stimeo
|
|
17146
|
+
el.style.setProperty("--stimeo--textarea-rows", String(rows));
|
|
16244
17147
|
if (height !== this.#lastHeight) {
|
|
16245
17148
|
this.#lastHeight = height;
|
|
16246
17149
|
this.dispatch("resize", { detail: { height, rows } });
|
|
@@ -16497,7 +17400,6 @@ var TimePickerController = class extends Controller {
|
|
|
16497
17400
|
}
|
|
16498
17401
|
/** Propagates a wrap carry from `kind` into the next larger segment. */
|
|
16499
17402
|
#carry(kind, amount) {
|
|
16500
|
-
if (amount === 0) return;
|
|
16501
17403
|
if (kind === "second") this.#step("minute", amount);
|
|
16502
17404
|
else if (kind === "minute") this.#step("hour", amount);
|
|
16503
17405
|
}
|
|
@@ -16559,15 +17461,19 @@ var TimePickerController = class extends Controller {
|
|
|
16559
17461
|
segment.setAttribute("aria-valuemax", String(max));
|
|
16560
17462
|
segment.textContent = text;
|
|
16561
17463
|
}
|
|
16562
|
-
/** Composes `HH:MM[:SS]`
|
|
17464
|
+
/** Composes `HH:MM[:SS]` and notifies both native-form and widget consumers. */
|
|
16563
17465
|
#syncField(notify) {
|
|
16564
17466
|
const h24 = this.#hours24();
|
|
16565
17467
|
const parts = [pad(h24), pad(this.#state.minute)];
|
|
16566
17468
|
if (this.secondsValue) parts.push(pad(this.#state.second));
|
|
16567
17469
|
const value = parts.join(":");
|
|
16568
|
-
|
|
17470
|
+
const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;
|
|
17471
|
+
if (fieldChanged) {
|
|
16569
17472
|
this.fieldTarget.value = value;
|
|
16570
17473
|
}
|
|
17474
|
+
if (notify && fieldChanged) {
|
|
17475
|
+
this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
17476
|
+
}
|
|
16571
17477
|
if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
16572
17478
|
this.#lastValue = value;
|
|
16573
17479
|
}
|
|
@@ -17461,7 +18367,7 @@ var TransitionController = class extends Controller {
|
|
|
17461
18367
|
else window.clearTimeout(id);
|
|
17462
18368
|
}
|
|
17463
18369
|
};
|
|
17464
|
-
var NESTED_INTERACTIVE =
|
|
18370
|
+
var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
|
|
17465
18371
|
var TreeViewController = class extends Controller {
|
|
17466
18372
|
static targets = ["item", "group"];
|
|
17467
18373
|
static actions = ["onClick", "onKeydown", "toggle"];
|
|
@@ -17722,6 +18628,7 @@ var TreeViewController = class extends Controller {
|
|
|
17722
18628
|
if (target.closest('[role="treeitem"]') !== item) return null;
|
|
17723
18629
|
const control = target.closest(NESTED_INTERACTIVE);
|
|
17724
18630
|
if (control && item.contains(control)) return null;
|
|
18631
|
+
if (isInteractiveHost(target)) return null;
|
|
17725
18632
|
return item;
|
|
17726
18633
|
}
|
|
17727
18634
|
/**
|