stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/checkbox_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/form_reset_watcher.ts
|
|
6
3
|
var FormResetWatcher = class {
|
|
7
4
|
#owns;
|
|
8
5
|
#onReset;
|
|
9
6
|
#frames = /* @__PURE__ */ new Set();
|
|
10
7
|
#generation = 0;
|
|
11
|
-
/**
|
|
12
|
-
* @param owns - whether `form` owns a control this controller derives from.
|
|
13
|
-
* @param onReset - the reconciliation pass, run once per honoured reset.
|
|
14
|
-
*/
|
|
15
8
|
constructor(owns, onReset) {
|
|
16
9
|
this.#owns = owns;
|
|
17
10
|
this.#onReset = onReset;
|
|
18
11
|
}
|
|
19
|
-
/** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
|
|
20
12
|
observe() {
|
|
21
13
|
document.addEventListener("reset", this.#handleReset, true);
|
|
22
14
|
}
|
|
23
|
-
/** Unsubscribes and drops every pending pass; call from `disconnect()`. */
|
|
24
15
|
disconnect() {
|
|
25
16
|
document.removeEventListener("reset", this.#handleReset, true);
|
|
26
17
|
this.#generation += 1;
|
|
@@ -44,57 +35,38 @@ var FormResetWatcher = class {
|
|
|
44
35
|
this.#frames.add(frame);
|
|
45
36
|
});
|
|
46
37
|
};
|
|
47
|
-
/** Runs the pass unless a listener cancelled the reset before it landed. */
|
|
48
38
|
#settle(event) {
|
|
49
39
|
if (!event.defaultPrevented) this.#onReset();
|
|
50
40
|
}
|
|
51
41
|
};
|
|
52
42
|
|
|
53
|
-
// src/utils/listener_set.ts
|
|
54
43
|
var ListenerSet = class {
|
|
55
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
56
44
|
#abort = new AbortController();
|
|
57
|
-
/**
|
|
58
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
59
|
-
*
|
|
60
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
61
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
62
|
-
* longer has a second place to drift from.
|
|
63
|
-
*/
|
|
64
45
|
add(target, type, handler, options) {
|
|
65
46
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
66
47
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
69
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
70
|
-
*/
|
|
71
48
|
dispose() {
|
|
72
49
|
this.#abort.abort();
|
|
73
50
|
this.#abort = new AbortController();
|
|
74
51
|
}
|
|
75
52
|
};
|
|
76
53
|
|
|
77
|
-
// src/utils/microtask_coalescer.ts
|
|
78
54
|
var MicrotaskCoalescer = class {
|
|
79
55
|
#run;
|
|
80
56
|
#queued = false;
|
|
81
57
|
#active = false;
|
|
82
58
|
#generation = 0;
|
|
83
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
84
59
|
constructor(run) {
|
|
85
60
|
this.#run = run;
|
|
86
61
|
}
|
|
87
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
88
62
|
activate() {
|
|
89
63
|
this.#active = true;
|
|
90
64
|
}
|
|
91
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
92
65
|
cancel() {
|
|
93
66
|
this.#active = false;
|
|
94
67
|
this.#queued = false;
|
|
95
68
|
this.#generation += 1;
|
|
96
69
|
}
|
|
97
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
98
70
|
schedule() {
|
|
99
71
|
if (!this.#active || this.#queued) return;
|
|
100
72
|
this.#queued = true;
|
|
@@ -107,82 +79,92 @@ var MicrotaskCoalescer = class {
|
|
|
107
79
|
}
|
|
108
80
|
};
|
|
109
81
|
|
|
110
|
-
|
|
82
|
+
var MorphRenderWatcher = class {
|
|
83
|
+
#listeners = new ListenerSet();
|
|
84
|
+
#pass;
|
|
85
|
+
#element = null;
|
|
86
|
+
#onMorph = () => this.#pass.schedule();
|
|
87
|
+
constructor(run) {
|
|
88
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
89
|
+
}
|
|
90
|
+
observe(element) {
|
|
91
|
+
if (this.#element === element) return;
|
|
92
|
+
this.disconnect();
|
|
93
|
+
this.#element = element;
|
|
94
|
+
this.#pass.activate();
|
|
95
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
96
|
+
}
|
|
97
|
+
schedule() {
|
|
98
|
+
this.#pass.schedule();
|
|
99
|
+
}
|
|
100
|
+
disconnect() {
|
|
101
|
+
this.#listeners.dispose();
|
|
102
|
+
this.#pass.cancel();
|
|
103
|
+
this.#element = null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
111
107
|
var CheckboxController = class extends Controller {
|
|
112
108
|
static targets = ["parent", "child"];
|
|
113
109
|
static actions = ["onChildChange", "onParentChange"];
|
|
114
110
|
static events = ["change", "reconcile"];
|
|
115
|
-
|
|
116
|
-
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFromChildren());
|
|
117
|
-
#listeners = new ListenerSet();
|
|
111
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileFromChildren());
|
|
118
112
|
#formReset = new FormResetWatcher(
|
|
119
113
|
(form) => this.#hasCheckboxOwnedBy(form),
|
|
120
114
|
() => this.#reconcile.schedule()
|
|
121
115
|
);
|
|
122
|
-
/** Aggregate this root last settled on, so a derived repair is reported once. */
|
|
123
116
|
#committedState = null;
|
|
124
|
-
/** Watches authored checked-attribute changes on retained target elements. */
|
|
125
117
|
#checkedObserver = new MutationObserver((records) => {
|
|
126
118
|
if (records.some((record) => this.#isManagedCheckbox(record.target))) {
|
|
127
119
|
this.#reconcile.schedule();
|
|
128
120
|
}
|
|
129
121
|
});
|
|
130
|
-
/** Reflects the initial aggregate and starts retained-DOM reconciliation. */
|
|
131
122
|
connect() {
|
|
132
|
-
this.#reconcile.activate();
|
|
133
123
|
this.#syncFromChildren();
|
|
124
|
+
this.#reconcile.observe(this.element);
|
|
134
125
|
this.#checkedObserver.observe(this.element, {
|
|
135
126
|
attributes: true,
|
|
136
127
|
attributeFilter: ["checked"],
|
|
137
128
|
subtree: true
|
|
138
129
|
});
|
|
139
|
-
this.#listeners.add(this.element, "turbo:morph-element", this.#onMorph);
|
|
140
130
|
this.#formReset.observe();
|
|
141
131
|
}
|
|
142
|
-
/** Releases the observer, global reset listener, and every pending reconciliation. */
|
|
143
132
|
disconnect() {
|
|
144
|
-
this.#reconcile.
|
|
133
|
+
this.#reconcile.disconnect();
|
|
145
134
|
this.#checkedObserver.disconnect();
|
|
146
|
-
this.#listeners.dispose();
|
|
147
135
|
this.#formReset.disconnect();
|
|
148
136
|
}
|
|
149
|
-
/** Reconciles the aggregate for a parent added or replaced at runtime. */
|
|
150
137
|
parentTargetConnected() {
|
|
151
138
|
this.#reconcile.schedule();
|
|
152
139
|
}
|
|
153
|
-
/** Reconciles the aggregate after a parent target leaves the group. */
|
|
154
140
|
parentTargetDisconnected() {
|
|
155
141
|
this.#reconcile.schedule();
|
|
156
142
|
}
|
|
157
|
-
/** Reconciles the aggregate for a child added at runtime. */
|
|
158
143
|
childTargetConnected() {
|
|
159
144
|
this.#reconcile.schedule();
|
|
160
145
|
}
|
|
161
|
-
/** Reconciles the aggregate after a child leaves the group. */
|
|
162
146
|
childTargetDisconnected() {
|
|
163
147
|
this.#reconcile.schedule();
|
|
164
148
|
}
|
|
165
|
-
/** Cascades the parent's state to every child. Bound via `data-action` (change). */
|
|
166
149
|
onParentChange() {
|
|
167
150
|
if (!this.hasParentTarget) return;
|
|
151
|
+
const previous = this.#committedState;
|
|
168
152
|
const checked = this.parentTarget.checked;
|
|
169
153
|
for (const child of this.childTargets) {
|
|
170
154
|
child.checked = checked;
|
|
171
155
|
}
|
|
172
156
|
this.#reflect(checked ? "all" : "none", true);
|
|
157
|
+
if (this.#committedState === previous) return;
|
|
173
158
|
const detail = this.#settledDetail();
|
|
174
159
|
if (detail) this.dispatch("change", { detail });
|
|
175
160
|
}
|
|
176
|
-
/** Recomputes the parent from its children. Bound via `data-action` (change). */
|
|
177
161
|
onChildChange() {
|
|
162
|
+
const previous = this.#committedState;
|
|
178
163
|
this.#syncFromChildren();
|
|
164
|
+
if (this.#committedState === previous) return;
|
|
179
165
|
const detail = this.#settledDetail();
|
|
180
166
|
if (detail) this.dispatch("change", { detail });
|
|
181
167
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Announces an aggregate this pass derived rather than the user. `change` stays
|
|
184
|
-
* reserved for the two public actions, so automation never reads a repair as an edit.
|
|
185
|
-
*/
|
|
186
168
|
#reconcileFromChildren() {
|
|
187
169
|
const previous = this.#committedState;
|
|
188
170
|
this.#syncFromChildren();
|
|
@@ -190,20 +172,9 @@ var CheckboxController = class extends Controller {
|
|
|
190
172
|
const detail = this.#settledDetail();
|
|
191
173
|
if (detail) this.dispatch("reconcile", { detail });
|
|
192
174
|
}
|
|
193
|
-
/**
|
|
194
|
-
* Derives the parent's `checked`/`indeterminate` and the root `data-state` from
|
|
195
|
-
* the children. Writing state and reporting it are separate so the caller — not
|
|
196
|
-
* a flag threaded through the write — decides which event describes the cause.
|
|
197
|
-
*/
|
|
198
175
|
#syncFromChildren() {
|
|
199
176
|
this.#reflect(this.#aggregate(), this.childTargets.length > 0);
|
|
200
177
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Reflects one aggregate state.
|
|
203
|
-
*
|
|
204
|
-
* @param writeParent - whether the state is authoritative over the `parent`
|
|
205
|
-
* target's own `checked` / `indeterminate`.
|
|
206
|
-
*/
|
|
207
178
|
#reflect(state, writeParent) {
|
|
208
179
|
if (writeParent && this.hasParentTarget) {
|
|
209
180
|
this.parentTarget.checked = state === "all";
|
|
@@ -212,29 +183,17 @@ var CheckboxController = class extends Controller {
|
|
|
212
183
|
this.element.setAttribute("data-state", state);
|
|
213
184
|
this.#committedState = state;
|
|
214
185
|
}
|
|
215
|
-
/** The settled aggregate as event detail, or `null` before anything has settled. */
|
|
216
186
|
#settledDetail() {
|
|
217
187
|
const state = this.#committedState;
|
|
218
188
|
if (state === null) return null;
|
|
219
189
|
return { checked: state === "all", indeterminate: state === "partial", state };
|
|
220
190
|
}
|
|
221
|
-
/** Reconciles retained targets after Turbo has finished morphing their live state. */
|
|
222
|
-
#onMorph = () => {
|
|
223
|
-
this.#reconcile.schedule();
|
|
224
|
-
};
|
|
225
|
-
/** Whether a form owns at least one current parent or child target. */
|
|
226
191
|
#hasCheckboxOwnedBy(form) {
|
|
227
192
|
return [...this.parentTargets, ...this.childTargets].some((checkbox) => checkbox.form === form);
|
|
228
193
|
}
|
|
229
|
-
/** Whether an observed attribute mutation belongs to this controller's target set. */
|
|
230
194
|
#isManagedCheckbox(node) {
|
|
231
195
|
return this.parentTargets.some((checkbox) => checkbox === node) || this.childTargets.some((checkbox) => checkbox === node);
|
|
232
196
|
}
|
|
233
|
-
/**
|
|
234
|
-
* Computes the aggregate state. With children it counts them; with none it
|
|
235
|
-
* reads the parent so a lone tri-state checkbox still reports a state without
|
|
236
|
-
* its externally-set `indeterminate` being clobbered.
|
|
237
|
-
*/
|
|
238
197
|
#aggregate() {
|
|
239
198
|
const children = this.childTargets;
|
|
240
199
|
if (children.length === 0) {
|
|
@@ -249,5 +208,4 @@ var CheckboxController = class extends Controller {
|
|
|
249
208
|
};
|
|
250
209
|
|
|
251
210
|
export { CheckboxController };
|
|
252
|
-
//# sourceMappingURL=checkbox_controller.js.map
|
|
253
211
|
//# sourceMappingURL=checkbox_controller.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/clipboard_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -13,79 +10,140 @@ function announce(message, options = {}) {
|
|
|
13
10
|
);
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
function setDefaultAttribute(element, name, value) {
|
|
14
|
+
if (element.hasAttribute(name)) return false;
|
|
15
|
+
element.setAttribute(name, value);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
var ListenerSet = class {
|
|
20
|
+
#abort = new AbortController();
|
|
21
|
+
add(target, type, handler, options) {
|
|
22
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
23
|
+
}
|
|
24
|
+
dispose() {
|
|
25
|
+
this.#abort.abort();
|
|
26
|
+
this.#abort = new AbortController();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var MicrotaskCoalescer = class {
|
|
31
|
+
#run;
|
|
32
|
+
#queued = false;
|
|
33
|
+
#active = false;
|
|
34
|
+
#generation = 0;
|
|
35
|
+
constructor(run) {
|
|
36
|
+
this.#run = run;
|
|
28
37
|
}
|
|
29
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
30
38
|
activate() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
this.#active = true;
|
|
40
|
+
}
|
|
41
|
+
cancel() {
|
|
42
|
+
this.#active = false;
|
|
43
|
+
this.#queued = false;
|
|
44
|
+
this.#generation += 1;
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
schedule() {
|
|
47
|
+
if (!this.#active || this.#queued) return;
|
|
48
|
+
this.#queued = true;
|
|
49
|
+
const generation = this.#generation;
|
|
50
|
+
queueMicrotask(() => {
|
|
51
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
52
|
+
this.#queued = false;
|
|
53
|
+
this.#run();
|
|
54
|
+
});
|
|
42
55
|
}
|
|
43
56
|
};
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
element
|
|
58
|
+
var MorphRenderWatcher = class {
|
|
59
|
+
#listeners = new ListenerSet();
|
|
60
|
+
#pass;
|
|
61
|
+
#element = null;
|
|
62
|
+
#onMorph = () => this.#pass.schedule();
|
|
63
|
+
constructor(run) {
|
|
64
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
65
|
+
}
|
|
66
|
+
observe(element) {
|
|
67
|
+
if (this.#element === element) return;
|
|
68
|
+
this.disconnect();
|
|
69
|
+
this.#element = element;
|
|
70
|
+
this.#pass.activate();
|
|
71
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
72
|
+
}
|
|
73
|
+
schedule() {
|
|
74
|
+
this.#pass.schedule();
|
|
75
|
+
}
|
|
76
|
+
disconnect() {
|
|
77
|
+
this.#listeners.dispose();
|
|
78
|
+
this.#pass.cancel();
|
|
79
|
+
this.#element = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
84
|
+
|
|
85
|
+
var NUMBER_BOUNDS = {
|
|
86
|
+
timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
|
|
87
|
+
function matchesNumberBounds(value, bounds) {
|
|
88
|
+
if (!Number.isFinite(value)) {
|
|
89
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
90
|
+
if (direction === null) return false;
|
|
91
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
92
|
+
}
|
|
93
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
94
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
95
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
96
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
97
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
49
98
|
return true;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
|
|
101
|
+
function readNumber(raw, fallback, bounds) {
|
|
102
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var NumberValueReader = class {
|
|
106
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
107
|
+
read(owner, name, raw, fallback, bounds) {
|
|
108
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
109
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
110
|
+
this.#lastRejected.delete(name);
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
113
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
114
|
+
const literal = owner.element.getAttribute(attribute);
|
|
115
|
+
if (literal === null) {
|
|
116
|
+
this.#lastRejected.delete(name);
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
120
|
+
this.#lastRejected.set(name, literal);
|
|
121
|
+
console.warn(
|
|
122
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
53
129
|
var TimerRegistry = class {
|
|
54
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
55
130
|
ids = /* @__PURE__ */ new Set();
|
|
56
|
-
/**
|
|
57
|
-
* Cancels a single tracked timer.
|
|
58
|
-
*
|
|
59
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
60
|
-
* registry), so callers can clear defensively without guarding.
|
|
61
|
-
*/
|
|
62
131
|
clear(id) {
|
|
63
132
|
if (this.ids.delete(id)) {
|
|
64
133
|
this.cancel(id);
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
69
|
-
* to guarantee no timer outlives the element.
|
|
70
|
-
*/
|
|
71
136
|
clearAll() {
|
|
72
137
|
for (const id of this.ids) {
|
|
73
138
|
this.cancel(id);
|
|
74
139
|
}
|
|
75
140
|
this.ids.clear();
|
|
76
141
|
}
|
|
77
|
-
/** Number of timers currently tracked (pending). */
|
|
78
142
|
get size() {
|
|
79
143
|
return this.ids.size;
|
|
80
144
|
}
|
|
81
145
|
};
|
|
82
146
|
var SafeTimeout = class extends TimerRegistry {
|
|
83
|
-
/**
|
|
84
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
85
|
-
*
|
|
86
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
87
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
88
|
-
*/
|
|
89
147
|
set(callback, delay) {
|
|
90
148
|
const id = this.schedule(() => {
|
|
91
149
|
this.ids.delete(id);
|
|
@@ -102,9 +160,9 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
102
160
|
}
|
|
103
161
|
};
|
|
104
162
|
|
|
105
|
-
// src/controllers/clipboard_controller.ts
|
|
106
163
|
var TRANSIENT_STATES = /* @__PURE__ */ new Set(["copied", "error"]);
|
|
107
|
-
var ClipboardController = class extends Controller {
|
|
164
|
+
var ClipboardController = class _ClipboardController extends Controller {
|
|
165
|
+
#numbers = new NumberValueReader();
|
|
108
166
|
static targets = ["source", "button", "feedback"];
|
|
109
167
|
static values = {
|
|
110
168
|
text: { type: String, default: "" },
|
|
@@ -114,63 +172,39 @@ var ClipboardController = class extends Controller {
|
|
|
114
172
|
announceCopiedText: { type: String, default: "" },
|
|
115
173
|
announceErrorText: { type: String, default: "" }
|
|
116
174
|
};
|
|
175
|
+
static valueConstraints = {
|
|
176
|
+
feedbackDuration: NUMBER_BOUNDS.timer
|
|
177
|
+
};
|
|
117
178
|
static actions = ["copy"];
|
|
118
179
|
static events = ["copy"];
|
|
119
|
-
/**
|
|
120
|
-
* The pending return to idle — the only timer this controller schedules, so
|
|
121
|
-
* `clearAll()` is exactly "drop the auto-clear" and needs no id of its own.
|
|
122
|
-
*/
|
|
123
180
|
#timers = new SafeTimeout();
|
|
124
|
-
/** Returns the completion state to idle for the snapshot Turbo takes. */
|
|
125
|
-
#beforeCache = new BeforeCacheReset(() => this.#rewind());
|
|
126
|
-
/**
|
|
127
|
-
* Whether this connection is still live. `copy()` suspends on the Clipboard API,
|
|
128
|
-
* and a teardown that lands while it is suspended must win: the continuation
|
|
129
|
-
* would otherwise write to an element nobody owns and arm a timer past the
|
|
130
|
-
* `clearAll()` that was supposed to be the last word. Stimulus also runs the label
|
|
131
|
-
* callbacks ahead of `connect()`, while this is still false: a result restored with
|
|
132
|
-
* the markup is `connect()`'s to settle, not theirs.
|
|
133
|
-
*/
|
|
134
181
|
#connected = false;
|
|
182
|
+
#result = null;
|
|
183
|
+
#feedback = null;
|
|
184
|
+
#written = /* @__PURE__ */ new Map();
|
|
185
|
+
#morphRender = new MorphRenderWatcher(() => this.#renderResult());
|
|
135
186
|
connect() {
|
|
136
187
|
this.#connected = true;
|
|
137
188
|
this.#adopt();
|
|
138
|
-
this.#
|
|
189
|
+
this.#result = null;
|
|
190
|
+
this.#feedback = this.hasFeedbackTarget ? {
|
|
191
|
+
element: this.feedbackTarget,
|
|
192
|
+
original: this.feedbackTarget.textContent ?? "",
|
|
193
|
+
text: this.feedbackTarget.textContent ?? ""
|
|
194
|
+
} : null;
|
|
195
|
+
this.#morphRender.observe(this.element);
|
|
139
196
|
}
|
|
140
197
|
disconnect() {
|
|
141
198
|
this.#connected = false;
|
|
142
|
-
this.#
|
|
199
|
+
this.#morphRender.disconnect();
|
|
143
200
|
this.#timers.clearAll();
|
|
144
201
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Follows a `copiedLabel` swapped in place while a successful copy is shown.
|
|
147
|
-
*
|
|
148
|
-
* Render only: nothing is announced or dispatched, and the return to idle keeps the
|
|
149
|
-
* deadline the copy set. The slot changes only while `data-state` is `copied` and it
|
|
150
|
-
* still reads the previous label — the one the copy wrote there — so text someone
|
|
151
|
-
* else put in the slot stays. Stimulus also runs this ahead of `connect()`, which
|
|
152
|
-
* settles a result restored with the markup itself, so nothing is written then.
|
|
153
|
-
*/
|
|
154
202
|
copiedLabelValueChanged(label, previous) {
|
|
155
203
|
this.#followLabel("copied", label, previous);
|
|
156
204
|
}
|
|
157
|
-
/**
|
|
158
|
-
* Follows an `errorLabel` swapped in place while a failed copy is shown, on the same
|
|
159
|
-
* terms as {@link ClipboardController.copiedLabelValueChanged}: only while
|
|
160
|
-
* `data-state` is `error` and the slot still reads the previous label.
|
|
161
|
-
*/
|
|
162
205
|
errorLabelValueChanged(label, previous) {
|
|
163
206
|
this.#followLabel("error", label, previous);
|
|
164
207
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Copies the resolved text and reports the outcome. Bound via `data-action`
|
|
167
|
-
* (click). Dispatches `stimeo--clipboard:copy` with `{ success, text, message }`
|
|
168
|
-
* once per completed attempt — including on failure — so consumers can react
|
|
169
|
-
* either way. `message` is the authored label for that outcome, so a part that
|
|
170
|
-
* shows wording — `stimeo--clipboard:copy->stimeo--toast#show` — needs nothing
|
|
171
|
-
* else; wiring both that and the announcer reads the same result twice.
|
|
172
|
-
* An attempt whose connection ended while it was in flight reports nothing.
|
|
173
|
-
*/
|
|
174
208
|
async copy() {
|
|
175
209
|
const text = this.#resolveText();
|
|
176
210
|
let success = false;
|
|
@@ -185,10 +219,6 @@ var ClipboardController = class extends Controller {
|
|
|
185
219
|
const message = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
186
220
|
this.dispatch("copy", { detail: { success, text, message } });
|
|
187
221
|
}
|
|
188
|
-
/**
|
|
189
|
-
* The text to copy: the explicit `text` value when set, otherwise the source
|
|
190
|
-
* target's current value (inputs/textareas) or text content.
|
|
191
|
-
*/
|
|
192
222
|
#resolveText() {
|
|
193
223
|
if (this.textValue.length > 0) return this.textValue;
|
|
194
224
|
if (!this.hasSourceTarget) return "";
|
|
@@ -198,92 +228,83 @@ var ClipboardController = class extends Controller {
|
|
|
198
228
|
}
|
|
199
229
|
return source.textContent ?? "";
|
|
200
230
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Reads the current state back from the DOM.
|
|
203
|
-
*
|
|
204
|
-
* A `copied` or `error` found at connect time is this controller's own output
|
|
205
|
-
* from a connection that is gone, and so is the timer that would have cleared it
|
|
206
|
-
* — nothing else would ever return the element to `idle`. Any other authored
|
|
207
|
-
* value belongs to the consumer and only a missing attribute takes the default.
|
|
208
|
-
*/
|
|
209
231
|
#adopt() {
|
|
210
232
|
if (this.#inTransientState()) {
|
|
233
|
+
const restored = this.#written.size === 0;
|
|
211
234
|
this.#reset();
|
|
235
|
+
if (restored && this.hasFeedbackTarget) this.feedbackTarget.textContent = "";
|
|
212
236
|
return;
|
|
213
237
|
}
|
|
214
238
|
setDefaultAttribute(this.element, "data-state", "idle");
|
|
215
239
|
}
|
|
216
|
-
/** Whether `data-state` currently holds one of the values this controller writes. */
|
|
217
240
|
#inTransientState() {
|
|
218
241
|
const state = this.element.getAttribute("data-state");
|
|
219
242
|
return state !== null && TRANSIENT_STATES.has(state);
|
|
220
243
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Returns the completion state to idle for the snapshot Turbo is about to take,
|
|
223
|
-
* so a page reached with the Back button does not report a copy that happened
|
|
224
|
-
* before the navigation. Only a state this controller wrote is rewound — an
|
|
225
|
-
* authored one is the consumer's and has to survive into the snapshot, exactly as
|
|
226
|
-
* `connect()` leaves it alone. State only — no `copy` is dispatched, which would
|
|
227
|
-
* claim a fresh copy ran.
|
|
228
|
-
*/
|
|
229
|
-
#rewind() {
|
|
230
|
-
if (!this.#inTransientState()) return;
|
|
231
|
-
this.#timers.clearAll();
|
|
232
|
-
this.#reset();
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Reflects the result, announces it, and schedules the return to idle.
|
|
236
|
-
*
|
|
237
|
-
* @stimeoRenderRoot
|
|
238
|
-
*/
|
|
239
244
|
#reportResult(success) {
|
|
240
|
-
this
|
|
241
|
-
|
|
242
|
-
this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
243
|
-
}
|
|
245
|
+
this.#result = success;
|
|
246
|
+
this.#renderResult(true);
|
|
244
247
|
this.#announceResult(success);
|
|
245
248
|
this.#scheduleReset();
|
|
246
249
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
#renderResult(force = false) {
|
|
251
|
+
if (this.#result === null) {
|
|
252
|
+
setDefaultAttribute(this.element, "data-state", "idle");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.element.setAttribute("data-state", this.#result ? "copied" : "error");
|
|
256
|
+
if (!this.hasFeedbackTarget) return;
|
|
257
|
+
const element = this.feedbackTarget;
|
|
258
|
+
const held = this.#feedback;
|
|
259
|
+
if (!force && (!held || element !== held.element || element.firstElementChild !== null || element.textContent !== held.text && element.textContent !== held.original))
|
|
260
|
+
return;
|
|
261
|
+
const text = this.#result ? this.copiedLabelValue : this.errorLabelValue;
|
|
262
|
+
this.#feedback = {
|
|
263
|
+
element,
|
|
264
|
+
original: held?.element === element ? held.original : element.textContent ?? "",
|
|
265
|
+
text
|
|
266
|
+
};
|
|
267
|
+
element.textContent = text;
|
|
268
|
+
this.#written.set(element, text);
|
|
269
|
+
}
|
|
253
270
|
#announceResult(success) {
|
|
254
271
|
announce(success ? this.announceCopiedTextValue : this.announceErrorTextValue);
|
|
255
272
|
}
|
|
256
|
-
/**
|
|
257
|
-
* Arms the return to idle for the result just shown.
|
|
258
|
-
*
|
|
259
|
-
* @stimeoRuntimeOnly `feedbackDuration` is the delay of the one timer this call arms, and the
|
|
260
|
-
* reset that timer runs reads no Value.
|
|
261
|
-
*/
|
|
262
273
|
#scheduleReset() {
|
|
263
274
|
this.#timers.clearAll();
|
|
264
|
-
if (this
|
|
265
|
-
this.#timers.set(() => this.#reset(), this
|
|
275
|
+
if (this.#safeFeedbackDuration > 0) {
|
|
276
|
+
this.#timers.set(() => this.#reset(), this.#safeFeedbackDuration);
|
|
266
277
|
}
|
|
267
278
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Rewrites the slot to `label` while `state` is shown and the slot still reads
|
|
270
|
-
* `previous`, the label this controller wrote for that result.
|
|
271
|
-
*/
|
|
272
279
|
#followLabel(state, label, previous) {
|
|
273
280
|
if (!this.#connected) return;
|
|
274
281
|
if (this.element.getAttribute("data-state") !== state || !this.hasFeedbackTarget) return;
|
|
275
282
|
if (this.feedbackTarget.textContent !== previous) return;
|
|
276
283
|
this.feedbackTarget.textContent = label;
|
|
284
|
+
this.#written.set(this.feedbackTarget, label);
|
|
285
|
+
if (this.#feedback?.element === this.feedbackTarget) this.#feedback.text = label;
|
|
277
286
|
}
|
|
278
|
-
/** Returns to the idle state and empties the completion slot. */
|
|
279
287
|
#reset() {
|
|
288
|
+
this.#result = null;
|
|
280
289
|
this.element.setAttribute("data-state", "idle");
|
|
281
|
-
|
|
282
|
-
this.
|
|
290
|
+
for (const [element, text] of this.#written) {
|
|
291
|
+
if (!this.feedbackTargets.includes(element)) continue;
|
|
292
|
+
if (element.firstElementChild === null && element.textContent === text) {
|
|
293
|
+
element.textContent = "";
|
|
294
|
+
}
|
|
283
295
|
}
|
|
296
|
+
this.#written.clear();
|
|
297
|
+
}
|
|
298
|
+
get #safeFeedbackDuration() {
|
|
299
|
+
return this.#numbers.read(
|
|
300
|
+
this,
|
|
301
|
+
"feedbackDuration",
|
|
302
|
+
this.feedbackDurationValue,
|
|
303
|
+
_ClipboardController.values.feedbackDuration.default,
|
|
304
|
+
_ClipboardController.valueConstraints.feedbackDuration
|
|
305
|
+
);
|
|
284
306
|
}
|
|
285
307
|
};
|
|
286
308
|
|
|
287
309
|
export { ClipboardController };
|
|
288
|
-
//# sourceMappingURL=clipboard_controller.js.map
|
|
289
310
|
//# sourceMappingURL=clipboard_controller.js.map
|