stimeo-ui 0.14.0 → 0.16.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 +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -20,6 +20,39 @@ function canTakeFocus(element) {
|
|
|
20
20
|
return !inheritsFieldsetDisabled(element);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/utils/microtask_coalescer.ts
|
|
24
|
+
var MicrotaskCoalescer = class {
|
|
25
|
+
#run;
|
|
26
|
+
#queued = false;
|
|
27
|
+
#active = false;
|
|
28
|
+
#generation = 0;
|
|
29
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
30
|
+
constructor(run) {
|
|
31
|
+
this.#run = run;
|
|
32
|
+
}
|
|
33
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
34
|
+
activate() {
|
|
35
|
+
this.#active = true;
|
|
36
|
+
}
|
|
37
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
38
|
+
cancel() {
|
|
39
|
+
this.#active = false;
|
|
40
|
+
this.#queued = false;
|
|
41
|
+
this.#generation += 1;
|
|
42
|
+
}
|
|
43
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
44
|
+
schedule() {
|
|
45
|
+
if (!this.#active || this.#queued) return;
|
|
46
|
+
this.#queued = true;
|
|
47
|
+
const generation = this.#generation;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
50
|
+
this.#queued = false;
|
|
51
|
+
this.#run();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
23
56
|
// src/utils/before_cache_reset.ts
|
|
24
57
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
25
58
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -81,53 +114,57 @@ var TabindexLoan = class {
|
|
|
81
114
|
};
|
|
82
115
|
|
|
83
116
|
// src/controllers/pagination_controller.ts
|
|
84
|
-
var PaginationController = class
|
|
117
|
+
var PaginationController = class extends Controller {
|
|
85
118
|
static targets = ["page", "prev", "next"];
|
|
86
119
|
static values = {
|
|
87
120
|
page: { type: Number, default: 1 },
|
|
88
121
|
total: { type: Number, default: 1 }
|
|
89
122
|
};
|
|
90
123
|
static actions = ["next", "prev", "select"];
|
|
91
|
-
static events = ["change"];
|
|
92
|
-
/** Marks a `disabled` this controller applied at a boundary
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
static events = ["change", "reconcile"];
|
|
125
|
+
/** Marks a `disabled` this controller applied at a boundary, in its own namespace. */
|
|
126
|
+
get #boundaryAttribute() {
|
|
127
|
+
return `data-${this.identifier}-boundary-disabled`;
|
|
128
|
+
}
|
|
95
129
|
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
96
130
|
#tabindex = new TabindexLoan();
|
|
97
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Collapses the Value and target callbacks of one mutation into one pass, and
|
|
133
|
+
* refuses the ones Stimulus delivers before `connect()`, which renders itself.
|
|
134
|
+
*/
|
|
135
|
+
#repaint = new MicrotaskCoalescer(() => this.#reconcilePage());
|
|
136
|
+
/** The page shown last, which the next move of the current page is measured from. */
|
|
137
|
+
#shown = 1;
|
|
138
|
+
/** Renders the initial state from the clamped `page` and `total`. */
|
|
98
139
|
connect() {
|
|
99
|
-
this.#
|
|
100
|
-
this.#
|
|
140
|
+
this.#repaint.activate();
|
|
141
|
+
this.#shown = this.#page;
|
|
142
|
+
this.#render();
|
|
101
143
|
}
|
|
102
|
-
/**
|
|
144
|
+
/** Drops a pending pass and reverts the one attribute added outside the state hooks. */
|
|
103
145
|
disconnect() {
|
|
104
|
-
this.#
|
|
146
|
+
this.#repaint.cancel();
|
|
105
147
|
this.#tabindex.returnAll();
|
|
106
148
|
}
|
|
107
149
|
/** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
|
|
108
150
|
pageValueChanged() {
|
|
109
|
-
|
|
110
|
-
this.#normalizeAndRender();
|
|
151
|
+
this.#repaint.schedule();
|
|
111
152
|
}
|
|
112
153
|
/** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
|
|
113
154
|
totalValueChanged() {
|
|
114
|
-
|
|
115
|
-
this.#normalizeAndRender();
|
|
155
|
+
this.#repaint.schedule();
|
|
116
156
|
}
|
|
117
157
|
/** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
|
|
118
158
|
pageTargetConnected() {
|
|
119
|
-
|
|
120
|
-
this.#render();
|
|
159
|
+
this.#repaint.schedule();
|
|
121
160
|
}
|
|
122
161
|
/** Syncs a `prev` button appended/replaced at runtime. */
|
|
123
162
|
prevTargetConnected() {
|
|
124
|
-
|
|
125
|
-
this.#render();
|
|
163
|
+
this.#repaint.schedule();
|
|
126
164
|
}
|
|
127
165
|
/** Syncs a `next` button appended/replaced at runtime. */
|
|
128
166
|
nextTargetConnected() {
|
|
129
|
-
|
|
130
|
-
this.#render();
|
|
167
|
+
this.#repaint.schedule();
|
|
131
168
|
}
|
|
132
169
|
/** Makes the clicked page button (its `data-page`) current. */
|
|
133
170
|
select(event) {
|
|
@@ -153,22 +190,31 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
153
190
|
const target = this.#clamp(page);
|
|
154
191
|
if (target === previous) return;
|
|
155
192
|
this.pageValue = target;
|
|
193
|
+
this.#shown = target;
|
|
156
194
|
this.#render();
|
|
157
195
|
this.dispatch("change", {
|
|
158
196
|
detail: { page: target, total: this.#total, previous }
|
|
159
197
|
});
|
|
160
198
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Renders one settled batch of Value and target changes, and reports a current
|
|
201
|
+
* page that moved from the one shown before as `reconcile`.
|
|
202
|
+
*/
|
|
203
|
+
#reconcilePage() {
|
|
204
|
+
const previous = this.#shown;
|
|
165
205
|
const page = this.#page;
|
|
166
|
-
|
|
206
|
+
this.#shown = page;
|
|
167
207
|
this.#render();
|
|
208
|
+
if (page !== previous) {
|
|
209
|
+
this.dispatch("reconcile", { detail: { page, total: this.#total, previous } });
|
|
210
|
+
}
|
|
168
211
|
}
|
|
169
212
|
/**
|
|
170
213
|
* Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
|
|
171
214
|
*
|
|
215
|
+
* It reads `page` and `total` through their clamps and never writes either Value,
|
|
216
|
+
* so a declaration outside the range stays in the attributes as the page wrote it.
|
|
217
|
+
*
|
|
172
218
|
* @stimeoRenderRoot
|
|
173
219
|
*/
|
|
174
220
|
#render() {
|
|
@@ -201,7 +247,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
201
247
|
#release(button, atBoundary) {
|
|
202
248
|
if (!button || atBoundary || !this.#owns(button)) return;
|
|
203
249
|
button.disabled = false;
|
|
204
|
-
button.removeAttribute(
|
|
250
|
+
button.removeAttribute(this.#boundaryAttribute);
|
|
205
251
|
}
|
|
206
252
|
/**
|
|
207
253
|
* Disables a boundary button, first moving focus off it when it is the active
|
|
@@ -212,7 +258,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
212
258
|
if (button.disabled && !this.#owns(button)) return;
|
|
213
259
|
if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
|
|
214
260
|
button.disabled = true;
|
|
215
|
-
button.setAttribute(
|
|
261
|
+
button.setAttribute(this.#boundaryAttribute, "");
|
|
216
262
|
}
|
|
217
263
|
/**
|
|
218
264
|
* Moves focus to `opposite` (already resolved to `null` when it will stay
|
|
@@ -249,7 +295,7 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
249
295
|
}
|
|
250
296
|
/** Whether the button's current `disabled` was applied by boundary control. */
|
|
251
297
|
#owns(button) {
|
|
252
|
-
return button.hasAttribute(
|
|
298
|
+
return button.hasAttribute(this.#boundaryAttribute);
|
|
253
299
|
}
|
|
254
300
|
};
|
|
255
301
|
|
|
@@ -84,10 +84,51 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
// src/utils/state_regions.ts
|
|
88
|
+
var StateRegions = class {
|
|
89
|
+
#whenTrue;
|
|
90
|
+
#whenFalse;
|
|
91
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.#whenTrue = options.whenTrue;
|
|
94
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
95
|
+
}
|
|
96
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
97
|
+
reflect(host, isTrue) {
|
|
98
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
99
|
+
if (!this.#whenFalse) {
|
|
100
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
104
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
105
|
+
for (const region of shown) this.#give(region);
|
|
106
|
+
for (const region of hiddenSide) this.#give(region);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
110
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
111
|
+
}
|
|
112
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
113
|
+
#write(region, hidden) {
|
|
114
|
+
if (hidden) this.#taken.add(region);
|
|
115
|
+
else this.#taken.delete(region);
|
|
116
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
117
|
+
}
|
|
118
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
119
|
+
#give(region) {
|
|
120
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
121
|
+
}
|
|
122
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
123
|
+
#inside(host, regions) {
|
|
124
|
+
return regions.filter((region) => host.contains(region));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
87
128
|
// src/controllers/password_reveal_controller.ts
|
|
88
129
|
var MAX_DELAY = 2 ** 31 - 1;
|
|
89
130
|
var PasswordRevealController = class extends Controller {
|
|
90
|
-
static targets = ["input", "toggle"];
|
|
131
|
+
static targets = ["input", "toggle", "onLabel", "offLabel"];
|
|
91
132
|
static values = {
|
|
92
133
|
autoHide: { type: Number, default: 0 }
|
|
93
134
|
};
|
|
@@ -105,6 +146,11 @@ var PasswordRevealController = class extends Controller {
|
|
|
105
146
|
* document does not answer this — unloading the controller leaves it there.
|
|
106
147
|
*/
|
|
107
148
|
#connected = false;
|
|
149
|
+
/** Owns `hidden` on the label pair the toggle button carries. */
|
|
150
|
+
#labels = new StateRegions({
|
|
151
|
+
whenTrue: () => this.onLabelTargets,
|
|
152
|
+
whenFalse: () => this.offLabelTargets
|
|
153
|
+
});
|
|
108
154
|
/** Masks the field before Turbo copies the page into its snapshot. */
|
|
109
155
|
#beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());
|
|
110
156
|
connect() {
|
|
@@ -144,6 +190,14 @@ var PasswordRevealController = class extends Controller {
|
|
|
144
190
|
toggleTargetConnected() {
|
|
145
191
|
this.#reflect(this.#isVisible);
|
|
146
192
|
}
|
|
193
|
+
/** Describes the state to a revealed-side label that arrives after connect. */
|
|
194
|
+
onLabelTargetConnected() {
|
|
195
|
+
this.#reflect(this.#isVisible);
|
|
196
|
+
}
|
|
197
|
+
/** Describes the state to a masked-side label that arrives after connect. */
|
|
198
|
+
offLabelTargetConnected() {
|
|
199
|
+
this.#reflect(this.#isVisible);
|
|
200
|
+
}
|
|
147
201
|
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
148
202
|
toggle() {
|
|
149
203
|
this.#setVisible(!this.#isVisible);
|
|
@@ -187,6 +241,8 @@ var PasswordRevealController = class extends Controller {
|
|
|
187
241
|
* the opposite of what the declaration asked for. A value that is no delay at
|
|
188
242
|
* all stays out of the positive range {@link PasswordRevealController.#arm}
|
|
189
243
|
* requires, so it schedules nothing.
|
|
244
|
+
*
|
|
245
|
+
* @stimeoRuntimeOnly `autoHide` is the delay of the one re-masking timer it arms.
|
|
190
246
|
*/
|
|
191
247
|
get #autoHideDelay() {
|
|
192
248
|
return Math.min(this.autoHideValue, MAX_DELAY);
|
|
@@ -202,10 +258,11 @@ var PasswordRevealController = class extends Controller {
|
|
|
202
258
|
this.inputTarget.type = "password";
|
|
203
259
|
this.#reflect(false);
|
|
204
260
|
}
|
|
205
|
-
/** Reflects the visible state onto `aria-pressed
|
|
261
|
+
/** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */
|
|
206
262
|
#reflect(visible) {
|
|
207
263
|
if (this.hasToggleTarget) {
|
|
208
264
|
this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
|
|
265
|
+
this.#labels.reflect(this.toggleTarget, visible);
|
|
209
266
|
}
|
|
210
267
|
this.element.setAttribute("data-state", visible ? "visible" : "hidden");
|
|
211
268
|
}
|
|
@@ -127,7 +127,20 @@ function parseStringList(raw, fallback = []) {
|
|
|
127
127
|
return parsed.filter((entry) => typeof entry === "string");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// src/utils/transient_hooks.ts
|
|
131
|
+
var TransientHooks = class {
|
|
132
|
+
#attributes;
|
|
133
|
+
constructor(options) {
|
|
134
|
+
this.#attributes = options.attributes;
|
|
135
|
+
}
|
|
136
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
137
|
+
reset(element) {
|
|
138
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
130
142
|
// src/controllers/persist_controller.ts
|
|
143
|
+
var TRANSIENT = new TransientHooks({ attributes: ["data-persist-restored"] });
|
|
131
144
|
var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
|
|
132
145
|
var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
|
|
133
146
|
var DEFAULT_EXCLUDE = ["authenticity_token", "_method", "utf8"];
|
|
@@ -197,7 +210,6 @@ var PersistController = class extends Controller {
|
|
|
197
210
|
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
198
211
|
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
199
212
|
this.#forcedRestore.clear();
|
|
200
|
-
this.element.removeAttribute("data-persist-restored");
|
|
201
213
|
this.element.addEventListener("input", this.#onInput);
|
|
202
214
|
this.element.addEventListener("change", this.#onInput);
|
|
203
215
|
this.#syncClearOn();
|
|
@@ -207,7 +219,7 @@ var PersistController = class extends Controller {
|
|
|
207
219
|
attributeFilter: [
|
|
208
220
|
"checked",
|
|
209
221
|
"data-controller",
|
|
210
|
-
|
|
222
|
+
`data-${this.identifier}-target`,
|
|
211
223
|
"id",
|
|
212
224
|
"multiple",
|
|
213
225
|
"name",
|
|
@@ -230,7 +242,7 @@ var PersistController = class extends Controller {
|
|
|
230
242
|
this.#restoreDynamic.cancel();
|
|
231
243
|
this.#forcedRestore.clear();
|
|
232
244
|
this.#timeouts.clearAll();
|
|
233
|
-
this.element
|
|
245
|
+
TRANSIENT.reset(this.element);
|
|
234
246
|
this.#payload = null;
|
|
235
247
|
}
|
|
236
248
|
/** Drops the active draft after storage confirms the removal. */
|
|
@@ -329,7 +341,7 @@ var PersistController = class extends Controller {
|
|
|
329
341
|
}
|
|
330
342
|
/** Reads, validates, and restores the active namespace. */
|
|
331
343
|
#loadActiveDraft() {
|
|
332
|
-
this.element
|
|
344
|
+
TRANSIENT.reset(this.element);
|
|
333
345
|
this.#payload = null;
|
|
334
346
|
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
335
347
|
this.#forcedRestore.clear();
|
|
@@ -481,7 +493,7 @@ var PersistController = class extends Controller {
|
|
|
481
493
|
}
|
|
482
494
|
/** Whether this instance, rather than a nested Persist host, owns a field. */
|
|
483
495
|
#ownsField(field) {
|
|
484
|
-
return field.closest(
|
|
496
|
+
return field.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
485
497
|
}
|
|
486
498
|
/** Marks all currently eligible fields without applying a payload. */
|
|
487
499
|
#markCurrentFieldsKnown() {
|
|
@@ -515,7 +527,12 @@ var PersistController = class extends Controller {
|
|
|
515
527
|
const name = value.trim();
|
|
516
528
|
return name.length > 0 && !/\s/.test(name) ? name : null;
|
|
517
529
|
}
|
|
518
|
-
/**
|
|
530
|
+
/**
|
|
531
|
+
* Collects dynamic controls and select-option changes into one restore pass.
|
|
532
|
+
*
|
|
533
|
+
* @stimeoRuntimeOnly `key` decides whether an id change moves the storage namespace; it decides
|
|
534
|
+
* nothing shown.
|
|
535
|
+
*/
|
|
519
536
|
#onMutations(records) {
|
|
520
537
|
let rootIdChanged = false;
|
|
521
538
|
for (const record of records) {
|
|
@@ -563,7 +580,12 @@ var PersistController = class extends Controller {
|
|
|
563
580
|
this.#forcedRestore.add(field);
|
|
564
581
|
}
|
|
565
582
|
}
|
|
566
|
-
/**
|
|
583
|
+
/**
|
|
584
|
+
* Resolves the logical key from the Value, falling back to the host id.
|
|
585
|
+
*
|
|
586
|
+
* @stimeoRuntimeOnly `key` names the storage namespace; the restored fields come from what is
|
|
587
|
+
* stored there.
|
|
588
|
+
*/
|
|
567
589
|
#resolveLogicalKey() {
|
|
568
590
|
const key = this.keyValue || this.element.id;
|
|
569
591
|
return key.length > 0 ? key : null;
|
|
@@ -572,7 +594,7 @@ var PersistController = class extends Controller {
|
|
|
572
594
|
#storageKey(logicalKey) {
|
|
573
595
|
return `${STORAGE_PREFIX}${logicalKey}`;
|
|
574
596
|
}
|
|
575
|
-
/** Normalizes invalid debounce
|
|
597
|
+
/** Normalizes an invalid `debounce` Value to the `debounce` default. */
|
|
576
598
|
get #debounceDelay() {
|
|
577
599
|
const value = this.debounceValue;
|
|
578
600
|
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
|
|
@@ -72,7 +72,66 @@ function ownerOf(candidates, node) {
|
|
|
72
72
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/utils/owned_pointer_session.ts
|
|
76
|
+
var OwnedPointerSession = class {
|
|
77
|
+
#pointerId;
|
|
78
|
+
#owner;
|
|
79
|
+
#handlers;
|
|
80
|
+
#abort = new AbortController();
|
|
81
|
+
#active = true;
|
|
82
|
+
constructor(start, owner, handlers) {
|
|
83
|
+
this.#pointerId = start.pointerId;
|
|
84
|
+
this.#owner = owner;
|
|
85
|
+
this.#handlers = handlers;
|
|
86
|
+
const { signal } = this.#abort;
|
|
87
|
+
owner.ownerDocument.addEventListener("pointermove", this.#onMove, { signal });
|
|
88
|
+
owner.ownerDocument.addEventListener("pointerup", this.#onEndEvent, { signal });
|
|
89
|
+
owner.ownerDocument.addEventListener("pointercancel", this.#onEndEvent, { signal });
|
|
90
|
+
try {
|
|
91
|
+
owner.setPointerCapture?.(this.#pointerId);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Releases capture and listeners, then reports the end exactly once. */
|
|
96
|
+
end() {
|
|
97
|
+
this.#finish("teardown");
|
|
98
|
+
}
|
|
99
|
+
#finish(kind) {
|
|
100
|
+
if (!this.#active) return;
|
|
101
|
+
this.#active = false;
|
|
102
|
+
this.#abort.abort();
|
|
103
|
+
try {
|
|
104
|
+
this.#owner.releasePointerCapture?.(this.#pointerId);
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
this.#handlers.end?.(kind);
|
|
108
|
+
}
|
|
109
|
+
#owns(event) {
|
|
110
|
+
return this.#active && event.pointerId === this.#pointerId;
|
|
111
|
+
}
|
|
112
|
+
#onMove = (event) => {
|
|
113
|
+
if (this.#owns(event)) this.#handlers.move(event);
|
|
114
|
+
};
|
|
115
|
+
#onEndEvent = (event) => {
|
|
116
|
+
if (this.#owns(event)) this.#finish(event.type === "pointerup" ? "up" : "cancel");
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/utils/transient_hooks.ts
|
|
121
|
+
var TransientHooks = class {
|
|
122
|
+
#attributes;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.#attributes = options.attributes;
|
|
125
|
+
}
|
|
126
|
+
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
127
|
+
reset(element) {
|
|
128
|
+
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
75
132
|
// src/controllers/pointer_drag_controller.ts
|
|
133
|
+
var TRANSIENT_DRAG = new TransientHooks({ attributes: ["data-dragging"] });
|
|
134
|
+
var TRANSIENT_GRAB = new TransientHooks({ attributes: ["data-grabbed"] });
|
|
76
135
|
var NATIVE_KEY_OWNERS = "input, textarea, select, button, a[href], summary, [contenteditable]";
|
|
77
136
|
var PointerDragController = class _PointerDragController extends Controller {
|
|
78
137
|
static targets = ["handle"];
|
|
@@ -93,14 +152,12 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
93
152
|
#keyboard = null;
|
|
94
153
|
/** Committed follow offset from past drops; a new drag's deltas add onto it. */
|
|
95
154
|
#followBase = { x: 0, y: 0 };
|
|
96
|
-
/** Aborts in-progress pointer-drag listeners on drag end / teardown. */
|
|
97
|
-
#dragAbort = null;
|
|
98
155
|
/** Decides whether a mid-session disconnect() is an in-page move or a detach. */
|
|
99
156
|
#gate = new DetachGate();
|
|
100
157
|
connect() {
|
|
101
158
|
this.#gate.cancel();
|
|
102
|
-
if (!this.#pointer) this.element
|
|
103
|
-
if (!this.#keyboard) this.element
|
|
159
|
+
if (!this.#pointer) TRANSIENT_DRAG.reset(this.element);
|
|
160
|
+
if (!this.#keyboard) TRANSIENT_GRAB.reset(this.element);
|
|
104
161
|
if (this.followValue && !this.#pointer && !this.#keyboard) {
|
|
105
162
|
this.#followBase = this.#parseFollowBase();
|
|
106
163
|
}
|
|
@@ -170,15 +227,19 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
170
227
|
}
|
|
171
228
|
this.#teardownSessions();
|
|
172
229
|
}
|
|
173
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Arms a pointer drag on a handle; `start` waits for the threshold.
|
|
232
|
+
*
|
|
233
|
+
* @stimeoRuntimeOnly `disabled` decides whether this one pointer press starts a gesture.
|
|
234
|
+
*/
|
|
174
235
|
#onPointerDown = (event) => {
|
|
175
236
|
if (this.disabledValue || this.#pointer || this.#keyboard || event.button !== 0) return;
|
|
176
237
|
const handle = this.#handleFor(event.target);
|
|
177
238
|
if (!handle) return;
|
|
178
239
|
event.preventDefault();
|
|
179
240
|
handle.focus();
|
|
180
|
-
|
|
181
|
-
|
|
241
|
+
const session = {
|
|
242
|
+
pointer: null,
|
|
182
243
|
handle,
|
|
183
244
|
originX: event.clientX,
|
|
184
245
|
originY: event.clientY,
|
|
@@ -187,17 +248,18 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
187
248
|
dy: 0,
|
|
188
249
|
pointerType: this.#pointerTypeOf(event)
|
|
189
250
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
document.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
|
|
196
|
-
document.addEventListener("pointercancel", this.#onPointerCancel, { signal: abort.signal });
|
|
251
|
+
session.pointer = new OwnedPointerSession(event, handle, {
|
|
252
|
+
move: (move) => this.#trackMove(session, move),
|
|
253
|
+
end: (kind) => this.#finishPointer(session, kind)
|
|
254
|
+
});
|
|
255
|
+
this.#pointer = session;
|
|
197
256
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Applies one tracked move: threshold, hooks, follow offset, events.
|
|
259
|
+
*
|
|
260
|
+
* @stimeoRuntimeOnly `threshold` decides when this one pointer move starts the drag.
|
|
261
|
+
*/
|
|
262
|
+
#trackMove(session, event) {
|
|
201
263
|
const [dx, dy] = this.#filterAxis(
|
|
202
264
|
event.clientX - session.originX,
|
|
203
265
|
event.clientY - session.originY
|
|
@@ -216,25 +278,32 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
216
278
|
this.dispatch("move", {
|
|
217
279
|
detail: { dx, dy, x: event.clientX, y: event.clientY, pointerType: session.pointerType }
|
|
218
280
|
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Closes the session and reports only the outcome the caller does not own.
|
|
284
|
+
*
|
|
285
|
+
* A teardown (detach, Escape, mid-drag disable, `reset`) is announced by the
|
|
286
|
+
* path that asked for it, so this one stays silent; `pointerup` below the
|
|
287
|
+
* threshold was a plain click, and `pointercancel` is an OS gesture takeover.
|
|
288
|
+
*/
|
|
289
|
+
#finishPointer(session, kind) {
|
|
223
290
|
const { started, dx, dy, pointerType } = session;
|
|
224
|
-
this.#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
291
|
+
this.#pointer = null;
|
|
292
|
+
this.element.removeAttribute("data-dragging");
|
|
293
|
+
if (kind === "teardown" || !started) return;
|
|
294
|
+
if (kind === "up") {
|
|
295
|
+
this.#followCommit(dx, dy);
|
|
296
|
+
this.dispatch("end", { detail: { dx, dy, pointerType } });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.#dispatchCancel(pointerType);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Keyboard alternative: Space/Enter grab & drop, arrows move, Escape cancels.
|
|
303
|
+
*
|
|
304
|
+
* @stimeoRuntimeOnly `disabled` decides whether this one key is taken and `keyboardStep` is the
|
|
305
|
+
* distance of one keyboard move.
|
|
306
|
+
*/
|
|
238
307
|
#onKeydown = (event) => {
|
|
239
308
|
if (this.disabledValue) return;
|
|
240
309
|
const handle = this.#handleFor(event.target);
|
|
@@ -316,26 +385,19 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
316
385
|
return null;
|
|
317
386
|
}
|
|
318
387
|
}
|
|
319
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Zeroes the delta on the locked axis (`axis` = x | y | both).
|
|
390
|
+
*
|
|
391
|
+
* @stimeoRuntimeOnly `axis` filters the offset of one move to the allowed direction.
|
|
392
|
+
*/
|
|
320
393
|
#filterAxis(dx, dy) {
|
|
321
394
|
if (this.axisValue === "x") return [dx, 0];
|
|
322
395
|
if (this.axisValue === "y") return [0, dy];
|
|
323
396
|
return [dx, dy];
|
|
324
397
|
}
|
|
325
|
-
/**
|
|
398
|
+
/** Silently closes a live pointer session; the caller announces the outcome. */
|
|
326
399
|
#endPointerSession() {
|
|
327
|
-
this.#
|
|
328
|
-
this.#pointer = null;
|
|
329
|
-
this.#dragAbort?.abort();
|
|
330
|
-
this.#dragAbort = null;
|
|
331
|
-
this.element.removeAttribute("data-dragging");
|
|
332
|
-
}
|
|
333
|
-
/** Releases the pointer capture the active session set (idempotent, safe). */
|
|
334
|
-
#releasePointerCapture() {
|
|
335
|
-
const session = this.#pointer;
|
|
336
|
-
if (session?.handle.hasPointerCapture(session.pointerId)) {
|
|
337
|
-
session.handle.releasePointerCapture(session.pointerId);
|
|
338
|
-
}
|
|
400
|
+
this.#pointer?.pointer?.end();
|
|
339
401
|
}
|
|
340
402
|
#clearKeyboardSession() {
|
|
341
403
|
this.#keyboard?.handle.removeAttribute("data-grabbed");
|
|
@@ -372,18 +434,33 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
372
434
|
this.#followReset();
|
|
373
435
|
this.dispatch("cancel", { detail: { pointerType } });
|
|
374
436
|
}
|
|
375
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Applies the in-flight offset to the element's `translate` (follow only).
|
|
439
|
+
*
|
|
440
|
+
* @stimeoRuntimeOnly `follow` decides whether this one move writes the offset as a translate; the
|
|
441
|
+
* offset itself comes from the gesture.
|
|
442
|
+
*/
|
|
376
443
|
#followMove(dx, dy) {
|
|
377
444
|
if (!this.followValue) return;
|
|
378
445
|
this.#applyFollow(this.#followBase.x + dx, this.#followBase.y + dy);
|
|
379
446
|
}
|
|
380
|
-
/**
|
|
447
|
+
/**
|
|
448
|
+
* Folds a drop's deltas into the committed base offset (follow only).
|
|
449
|
+
*
|
|
450
|
+
* @stimeoRuntimeOnly `follow` decides whether the end of this one gesture keeps the offset as a
|
|
451
|
+
* translate.
|
|
452
|
+
*/
|
|
381
453
|
#followCommit(dx, dy) {
|
|
382
454
|
if (!this.followValue) return;
|
|
383
455
|
this.#followBase = { x: this.#followBase.x + dx, y: this.#followBase.y + dy };
|
|
384
456
|
this.#applyFollow(this.#followBase.x, this.#followBase.y);
|
|
385
457
|
}
|
|
386
|
-
/**
|
|
458
|
+
/**
|
|
459
|
+
* Snaps back to the committed position (follow only) — the cancel contract.
|
|
460
|
+
*
|
|
461
|
+
* @stimeoRuntimeOnly `follow` decides whether cancelling this one gesture clears the translate it
|
|
462
|
+
* wrote.
|
|
463
|
+
*/
|
|
387
464
|
#followReset() {
|
|
388
465
|
if (!this.followValue) return;
|
|
389
466
|
this.#applyFollow(this.#followBase.x, this.#followBase.y);
|
|
@@ -438,6 +515,8 @@ var PointerDragController = class _PointerDragController extends Controller {
|
|
|
438
515
|
* idempotent) and focusability for the keyboard path (`tabindex="0"` only when
|
|
439
516
|
* the author supplied none — prefer a real `<button>` handle). Both additions
|
|
440
517
|
* are marker-owned so `#restoreHandle` reverts them symmetrically on teardown.
|
|
518
|
+
*
|
|
519
|
+
* @stimeoRenderRoot
|
|
441
520
|
*/
|
|
442
521
|
#prepareHandle(handle) {
|
|
443
522
|
const lent = this.#touchActionForAxis();
|