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,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/direct_upload_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;
|
|
@@ -19,36 +16,6 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
// src/utils/before_cache_reset.ts
|
|
23
|
-
var BeforeCacheReset = class _BeforeCacheReset {
|
|
24
|
-
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
25
|
-
static #subscribers = /* @__PURE__ */ new Set();
|
|
26
|
-
/** The shared listener; installed while at least one instance is subscribed. */
|
|
27
|
-
static #onBeforeCache = () => {
|
|
28
|
-
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
29
|
-
};
|
|
30
|
-
#rewind;
|
|
31
|
-
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
32
|
-
constructor(rewind) {
|
|
33
|
-
this.#rewind = rewind;
|
|
34
|
-
}
|
|
35
|
-
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
36
|
-
activate() {
|
|
37
|
-
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
38
|
-
_BeforeCacheReset.#subscribers.add(this);
|
|
39
|
-
if (first) {
|
|
40
|
-
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
44
|
-
deactivate() {
|
|
45
|
-
_BeforeCacheReset.#subscribers.delete(this);
|
|
46
|
-
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
47
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// src/utils/declared_value.ts
|
|
52
19
|
function parseDeclared(raw, parse, fallback) {
|
|
53
20
|
try {
|
|
54
21
|
return parse(raw);
|
|
@@ -68,43 +35,24 @@ function validSelector(element, raw, fallback) {
|
|
|
68
35
|
);
|
|
69
36
|
}
|
|
70
37
|
|
|
71
|
-
// src/utils/safe_timeout.ts
|
|
72
38
|
var TimerRegistry = class {
|
|
73
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
74
39
|
ids = /* @__PURE__ */ new Set();
|
|
75
|
-
/**
|
|
76
|
-
* Cancels a single tracked timer.
|
|
77
|
-
*
|
|
78
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
79
|
-
* registry), so callers can clear defensively without guarding.
|
|
80
|
-
*/
|
|
81
40
|
clear(id) {
|
|
82
41
|
if (this.ids.delete(id)) {
|
|
83
42
|
this.cancel(id);
|
|
84
43
|
}
|
|
85
44
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
88
|
-
* to guarantee no timer outlives the element.
|
|
89
|
-
*/
|
|
90
45
|
clearAll() {
|
|
91
46
|
for (const id of this.ids) {
|
|
92
47
|
this.cancel(id);
|
|
93
48
|
}
|
|
94
49
|
this.ids.clear();
|
|
95
50
|
}
|
|
96
|
-
/** Number of timers currently tracked (pending). */
|
|
97
51
|
get size() {
|
|
98
52
|
return this.ids.size;
|
|
99
53
|
}
|
|
100
54
|
};
|
|
101
55
|
var SafeTimeout = class extends TimerRegistry {
|
|
102
|
-
/**
|
|
103
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
104
|
-
*
|
|
105
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
106
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
107
|
-
*/
|
|
108
56
|
set(callback, delay) {
|
|
109
57
|
const id = this.schedule(() => {
|
|
110
58
|
this.ids.delete(id);
|
|
@@ -121,14 +69,37 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
121
69
|
}
|
|
122
70
|
};
|
|
123
71
|
|
|
124
|
-
|
|
72
|
+
var KeyedTimers = class {
|
|
73
|
+
#timers = new SafeTimeout();
|
|
74
|
+
#pending = /* @__PURE__ */ new Map();
|
|
75
|
+
set(key, callback, delay) {
|
|
76
|
+
this.clear(key);
|
|
77
|
+
const id = this.#timers.set(() => {
|
|
78
|
+
this.#pending.delete(key);
|
|
79
|
+
callback();
|
|
80
|
+
}, delay);
|
|
81
|
+
this.#pending.set(key, id);
|
|
82
|
+
}
|
|
83
|
+
clear(key) {
|
|
84
|
+
this.#timers.clear(this.#pending.get(key) ?? -1);
|
|
85
|
+
this.#pending.delete(key);
|
|
86
|
+
}
|
|
87
|
+
clearAll() {
|
|
88
|
+
this.#timers.clearAll();
|
|
89
|
+
this.#pending.clear();
|
|
90
|
+
}
|
|
91
|
+
has(key) {
|
|
92
|
+
return this.#pending.has(key);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
125
96
|
function cloneTemplateRoot(template) {
|
|
126
97
|
const root = template.content.firstElementChild;
|
|
127
98
|
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
128
99
|
}
|
|
129
100
|
|
|
130
|
-
// src/controllers/direct_upload_controller.ts
|
|
131
101
|
var REMOVE_DELAY = 4e3;
|
|
102
|
+
var GENERATED_ATTRIBUTE = "generated";
|
|
132
103
|
var DirectUploadController = class extends Controller {
|
|
133
104
|
static targets = ["list", "row"];
|
|
134
105
|
static values = {
|
|
@@ -138,10 +109,9 @@ var DirectUploadController = class extends Controller {
|
|
|
138
109
|
scope: { type: String, default: "" }
|
|
139
110
|
};
|
|
140
111
|
static events = ["progress", "done", "error", "reconcile"];
|
|
141
|
-
#
|
|
112
|
+
#removals = new KeyedTimers();
|
|
142
113
|
#rows = /* @__PURE__ */ new Map();
|
|
143
|
-
#
|
|
144
|
-
/** The validated `scope` selector; a broken declaration falls back to `""`. */
|
|
114
|
+
#connected = false;
|
|
145
115
|
#scopeSelector = "";
|
|
146
116
|
#onInitialize = (event) => {
|
|
147
117
|
if (!this.#inScope(event)) return;
|
|
@@ -165,26 +135,47 @@ var DirectUploadController = class extends Controller {
|
|
|
165
135
|
this.#complete(this.#key(detail.id), this.#name(detail));
|
|
166
136
|
};
|
|
167
137
|
connect() {
|
|
138
|
+
this.#connected = true;
|
|
168
139
|
document.addEventListener("direct-upload:initialize", this.#onInitialize);
|
|
169
140
|
document.addEventListener("direct-upload:progress", this.#onProgress);
|
|
170
141
|
document.addEventListener("direct-upload:error", this.#onError);
|
|
171
142
|
document.addEventListener("direct-upload:end", this.#onEnd);
|
|
172
|
-
this.#
|
|
173
|
-
this.#
|
|
143
|
+
this.#discardInherited();
|
|
144
|
+
this.#syncRemovals();
|
|
174
145
|
}
|
|
175
146
|
disconnect() {
|
|
147
|
+
this.#connected = false;
|
|
176
148
|
document.removeEventListener("direct-upload:initialize", this.#onInitialize);
|
|
177
149
|
document.removeEventListener("direct-upload:progress", this.#onProgress);
|
|
178
150
|
document.removeEventListener("direct-upload:error", this.#onError);
|
|
179
151
|
document.removeEventListener("direct-upload:end", this.#onEnd);
|
|
180
|
-
this.#
|
|
181
|
-
|
|
152
|
+
this.#removals.clearAll();
|
|
153
|
+
}
|
|
154
|
+
get #generatedAttribute() {
|
|
155
|
+
return `data-${this.identifier}-${GENERATED_ATTRIBUTE}`;
|
|
156
|
+
}
|
|
157
|
+
#discardInherited() {
|
|
158
|
+
if (this.#rows.size > 0 || !this.hasListTarget) return;
|
|
159
|
+
const ids = [];
|
|
160
|
+
for (const row of Array.from(this.listTarget.children)) {
|
|
161
|
+
const id = row.getAttribute(this.#generatedAttribute);
|
|
162
|
+
if (id === null) continue;
|
|
163
|
+
row.remove();
|
|
164
|
+
ids.push(id);
|
|
165
|
+
}
|
|
166
|
+
if (ids.length === 0) return;
|
|
167
|
+
this.#syncAggregate();
|
|
168
|
+
this.dispatch("reconcile", { detail: { ids } });
|
|
182
169
|
}
|
|
183
|
-
/** Validates `scope` once so the per-event path never parses or throws. */
|
|
184
170
|
scopeValueChanged() {
|
|
185
171
|
this.#scopeSelector = validSelector(this.element, this.scopeValue, "");
|
|
186
172
|
}
|
|
187
|
-
|
|
173
|
+
removeOnDoneValueChanged() {
|
|
174
|
+
if (this.#connected) this.#syncRemovals();
|
|
175
|
+
}
|
|
176
|
+
listTargetDisconnected() {
|
|
177
|
+
if (this.#connected) this.#syncAggregate();
|
|
178
|
+
}
|
|
188
179
|
#updateProgress(id, percent, name) {
|
|
189
180
|
const row = this.#rowFor(id, name);
|
|
190
181
|
if (row === null || this.#isSettled(row)) return;
|
|
@@ -192,12 +183,6 @@ var DirectUploadController = class extends Controller {
|
|
|
192
183
|
this.#syncAggregate();
|
|
193
184
|
this.dispatch("progress", { detail: { id, percent: clamped } });
|
|
194
185
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Marks a not-yet-settled row done at 100%, announces it, and emits `done`.
|
|
197
|
-
*
|
|
198
|
-
* @stimeoRuntimeOnly `announceDoneText` words the one announcement of this completion and
|
|
199
|
-
* `removeOnDone` decides whether it arms the row's removal.
|
|
200
|
-
*/
|
|
201
186
|
#complete(id, name) {
|
|
202
187
|
const row = this.#rowFor(id, name);
|
|
203
188
|
if (row === null || this.#isSettled(row)) return;
|
|
@@ -206,18 +191,8 @@ var DirectUploadController = class extends Controller {
|
|
|
206
191
|
this.#syncAggregate();
|
|
207
192
|
this.#announce(this.announceDoneTextValue, name, row);
|
|
208
193
|
this.dispatch("done", { detail: { id } });
|
|
209
|
-
if (this.removeOnDoneValue)
|
|
210
|
-
this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);
|
|
211
|
-
}
|
|
194
|
+
if (this.removeOnDoneValue) this.#scheduleRemoval(id, row);
|
|
212
195
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Marks a not-yet-settled row failed, announces it, and emits `error`.
|
|
215
|
-
* Returns whether the failure is rendered by this widget (used to decide the
|
|
216
|
-
* `direct-upload:error` default), which also holds when the row already
|
|
217
|
-
* displays an earlier failure.
|
|
218
|
-
*
|
|
219
|
-
* @stimeoRuntimeOnly `announceErrorText` words the one announcement of this failure.
|
|
220
|
-
*/
|
|
221
196
|
#fail(id, error, name) {
|
|
222
197
|
const row = this.#rowFor(id, name);
|
|
223
198
|
if (row === null) return false;
|
|
@@ -227,27 +202,25 @@ var DirectUploadController = class extends Controller {
|
|
|
227
202
|
this.dispatch("error", { detail: { id, error } });
|
|
228
203
|
return true;
|
|
229
204
|
}
|
|
230
|
-
/** Whether the row reached a terminal state; settled rows are never rewritten. */
|
|
231
205
|
#isSettled(row) {
|
|
232
206
|
const state = row.getAttribute("data-upload-state");
|
|
233
207
|
return state === "done" || state === "error";
|
|
234
208
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
*/
|
|
241
|
-
#rescheduleRemovals() {
|
|
242
|
-
if (!this.removeOnDoneValue) return;
|
|
209
|
+
#syncRemovals() {
|
|
210
|
+
if (!this.removeOnDoneValue) {
|
|
211
|
+
this.#removals.clearAll();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
243
214
|
this.#prune();
|
|
244
215
|
for (const [id, row] of this.#rows) {
|
|
245
|
-
if (row.getAttribute("data-upload-state") === "done") {
|
|
246
|
-
this.#
|
|
216
|
+
if (row.getAttribute("data-upload-state") === "done" && !this.#removals.has(id)) {
|
|
217
|
+
this.#scheduleRemoval(id, row);
|
|
247
218
|
}
|
|
248
219
|
}
|
|
249
220
|
}
|
|
250
|
-
|
|
221
|
+
#scheduleRemoval(id, row) {
|
|
222
|
+
this.#removals.set(id, () => this.#removeRow(row), REMOVE_DELAY);
|
|
223
|
+
}
|
|
251
224
|
#rowFor(id, name) {
|
|
252
225
|
const key = this.#key(id);
|
|
253
226
|
const existing = this.#rows.get(key);
|
|
@@ -256,13 +229,13 @@ var DirectUploadController = class extends Controller {
|
|
|
256
229
|
this.#applyName(existing, name);
|
|
257
230
|
return existing;
|
|
258
231
|
}
|
|
259
|
-
existing
|
|
260
|
-
this.#rows.delete(key);
|
|
232
|
+
this.#retire(key, existing);
|
|
261
233
|
}
|
|
262
234
|
if (!this.hasRowTarget || !this.hasListTarget) return null;
|
|
263
235
|
const clone = cloneTemplateRoot(this.rowTarget);
|
|
264
236
|
if (!clone) return null;
|
|
265
237
|
this.#applyName(clone, name);
|
|
238
|
+
clone.setAttribute(this.#generatedAttribute, key);
|
|
266
239
|
clone.setAttribute("data-upload-state", "uploading");
|
|
267
240
|
this.#applyProgress(clone, 0);
|
|
268
241
|
this.listTarget.appendChild(clone);
|
|
@@ -270,20 +243,12 @@ var DirectUploadController = class extends Controller {
|
|
|
270
243
|
this.#syncAggregate();
|
|
271
244
|
return clone;
|
|
272
245
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Writes the event's file name into `[data-field="name"]` and, unless the row
|
|
275
|
-
* already carries a non-blank `aria-label` — authored on the template or
|
|
276
|
-
* applied by an earlier event — makes it the accessible name too. The visible
|
|
277
|
-
* name never depends on the label: an authored label keeps its wording while
|
|
278
|
-
* the field still shows which file this row tracks.
|
|
279
|
-
*/
|
|
280
246
|
#applyName(row, name) {
|
|
281
247
|
if (name.length === 0) return;
|
|
282
248
|
this.#setField(row, "name", name);
|
|
283
249
|
if ((row.getAttribute("aria-label") ?? "").trim().length > 0) return;
|
|
284
250
|
row.setAttribute("aria-label", name);
|
|
285
251
|
}
|
|
286
|
-
/** Writes one progress value to every per-row hook; returns the clamped percent. */
|
|
287
252
|
#applyProgress(row, percent) {
|
|
288
253
|
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
289
254
|
row.setAttribute("aria-valuenow", String(clamped));
|
|
@@ -292,17 +257,10 @@ var DirectUploadController = class extends Controller {
|
|
|
292
257
|
this.#setField(row, "percent", `${clamped}%`);
|
|
293
258
|
return clamped;
|
|
294
259
|
}
|
|
295
|
-
#removeRow(
|
|
296
|
-
const row = this.#rows.get(id);
|
|
297
|
-
if (row === void 0) return;
|
|
260
|
+
#removeRow(row) {
|
|
298
261
|
row.remove();
|
|
299
|
-
this.#rows.delete(id);
|
|
300
262
|
this.#syncAggregate();
|
|
301
263
|
}
|
|
302
|
-
/**
|
|
303
|
-
* Reflects the average progress across live rows on the controller element,
|
|
304
|
-
* withdrawing both hooks once no rows remain.
|
|
305
|
-
*/
|
|
306
264
|
#syncAggregate() {
|
|
307
265
|
this.#prune();
|
|
308
266
|
if (this.#rows.size === 0) {
|
|
@@ -318,50 +276,21 @@ var DirectUploadController = class extends Controller {
|
|
|
318
276
|
this.element.setAttribute("data-upload-progress", String(overall));
|
|
319
277
|
this.element.style.setProperty("--stimeo--upload-progress", `${overall}%`);
|
|
320
278
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Whether a bookkept row is still this widget's live UI: connected, and — when
|
|
323
|
-
* a `list` target is present — inside the *current* one, so re-pointing the
|
|
324
|
-
* target attribute at a new element retires rows kept alive in the old list.
|
|
325
|
-
*/
|
|
326
279
|
#tracksRow(row) {
|
|
327
280
|
if (!row.isConnected) return false;
|
|
328
281
|
return !this.hasListTarget || this.listTarget.contains(row);
|
|
329
282
|
}
|
|
330
|
-
/**
|
|
331
|
-
* Retires rows that left the live UI (list swap, external removal). A retired
|
|
332
|
-
* clone is removed outright — generated rows only ever live under the current
|
|
333
|
-
* `list`, so the before-cache rewind never has an untracked leftover to miss.
|
|
334
|
-
*/
|
|
335
283
|
#prune() {
|
|
336
284
|
for (const [id, row] of this.#rows) {
|
|
337
285
|
if (this.#tracksRow(row)) continue;
|
|
338
|
-
row
|
|
339
|
-
this.#rows.delete(id);
|
|
286
|
+
this.#retire(id, row);
|
|
340
287
|
}
|
|
341
288
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*/
|
|
347
|
-
#rewindForCache() {
|
|
348
|
-
const ids = [...this.#rows.keys()];
|
|
349
|
-
this.#reset();
|
|
350
|
-
if (ids.length > 0) this.dispatch("reconcile", { detail: { ids } });
|
|
351
|
-
}
|
|
352
|
-
#reset() {
|
|
353
|
-
for (const row of this.#rows.values()) row.remove();
|
|
354
|
-
this.#rows.clear();
|
|
355
|
-
this.#timeouts.clearAll();
|
|
356
|
-
this.#syncAggregate();
|
|
289
|
+
#retire(id, row) {
|
|
290
|
+
row.remove();
|
|
291
|
+
this.#rows.delete(id);
|
|
292
|
+
this.#removals.clear(id);
|
|
357
293
|
}
|
|
358
|
-
/**
|
|
359
|
-
* Sends one consumer-worded message to the page's shared announcer. `{name}`
|
|
360
|
-
* resolves to the row's displayed name first — the event that settles an
|
|
361
|
-
* upload may omit the file although an earlier event already named the row —
|
|
362
|
-
* then the event's own name, then the accessible name (which an authored
|
|
363
|
-
* label owns, so it is the last resort, not the primary source).
|
|
364
|
-
*/
|
|
365
294
|
#announce(template, name, row) {
|
|
366
295
|
const stored = this.#field(row, "name")?.textContent ?? "";
|
|
367
296
|
const label = stored.length > 0 ? stored : name.length > 0 ? name : row.getAttribute("aria-label") ?? "";
|
|
@@ -377,17 +306,9 @@ var DirectUploadController = class extends Controller {
|
|
|
377
306
|
#detail(event) {
|
|
378
307
|
return event.detail ?? {};
|
|
379
308
|
}
|
|
380
|
-
/** The event's file name, or `""` when it carries none. */
|
|
381
309
|
#name(detail) {
|
|
382
310
|
return detail.file?.name ?? "";
|
|
383
311
|
}
|
|
384
|
-
/**
|
|
385
|
-
* Whether an event belongs to this controller. With `scope` set, only events
|
|
386
|
-
* whose target (the file input) sits inside an element matching `scope` are
|
|
387
|
-
* handled, so several upload widgets on one page do not cross-populate.
|
|
388
|
-
* Resolved with `closest()` from the target itself, so the chatty `progress`
|
|
389
|
-
* stream never pays a document-wide query. Empty `scope` handles all.
|
|
390
|
-
*/
|
|
391
312
|
#inScope(event) {
|
|
392
313
|
if (this.#scopeSelector.length === 0) return true;
|
|
393
314
|
const target = event.target;
|
|
@@ -399,5 +320,4 @@ var DirectUploadController = class extends Controller {
|
|
|
399
320
|
};
|
|
400
321
|
|
|
401
322
|
export { DirectUploadController };
|
|
402
|
-
//# sourceMappingURL=direct_upload_controller.js.map
|
|
403
323
|
//# sourceMappingURL=direct_upload_controller.js.map
|
|
@@ -1,68 +1,34 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/dirty_form_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/listener_set.ts
|
|
6
3
|
var ListenerSet = class {
|
|
7
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
8
4
|
#abort = new AbortController();
|
|
9
|
-
/**
|
|
10
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
11
|
-
*
|
|
12
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
13
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
14
|
-
* longer has a second place to drift from.
|
|
15
|
-
*/
|
|
16
5
|
add(target, type, handler, options) {
|
|
17
6
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
18
7
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
21
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
22
|
-
*/
|
|
23
8
|
dispose() {
|
|
24
9
|
this.#abort.abort();
|
|
25
10
|
this.#abort = new AbortController();
|
|
26
11
|
}
|
|
27
12
|
};
|
|
28
13
|
|
|
29
|
-
// src/utils/safe_timeout.ts
|
|
30
14
|
var TimerRegistry = class {
|
|
31
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
32
15
|
ids = /* @__PURE__ */ new Set();
|
|
33
|
-
/**
|
|
34
|
-
* Cancels a single tracked timer.
|
|
35
|
-
*
|
|
36
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
37
|
-
* registry), so callers can clear defensively without guarding.
|
|
38
|
-
*/
|
|
39
16
|
clear(id) {
|
|
40
17
|
if (this.ids.delete(id)) {
|
|
41
18
|
this.cancel(id);
|
|
42
19
|
}
|
|
43
20
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
46
|
-
* to guarantee no timer outlives the element.
|
|
47
|
-
*/
|
|
48
21
|
clearAll() {
|
|
49
22
|
for (const id of this.ids) {
|
|
50
23
|
this.cancel(id);
|
|
51
24
|
}
|
|
52
25
|
this.ids.clear();
|
|
53
26
|
}
|
|
54
|
-
/** Number of timers currently tracked (pending). */
|
|
55
27
|
get size() {
|
|
56
28
|
return this.ids.size;
|
|
57
29
|
}
|
|
58
30
|
};
|
|
59
31
|
var SafeTimeout = class extends TimerRegistry {
|
|
60
|
-
/**
|
|
61
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
62
|
-
*
|
|
63
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
64
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
65
|
-
*/
|
|
66
32
|
set(callback, delay) {
|
|
67
33
|
const id = this.schedule(() => {
|
|
68
34
|
this.ids.delete(id);
|
|
@@ -79,21 +45,31 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
79
45
|
}
|
|
80
46
|
};
|
|
81
47
|
|
|
82
|
-
|
|
48
|
+
function sharedRegistry(key, create) {
|
|
49
|
+
const symbol = Symbol.for(key);
|
|
50
|
+
const scope = globalThis;
|
|
51
|
+
const existing = scope[symbol];
|
|
52
|
+
if (existing !== void 0) return existing;
|
|
53
|
+
const registry = create();
|
|
54
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
55
|
+
return registry;
|
|
56
|
+
}
|
|
57
|
+
|
|
83
58
|
var TransientHooks = class {
|
|
84
59
|
#attributes;
|
|
85
60
|
constructor(options) {
|
|
86
61
|
this.#attributes = options.attributes;
|
|
87
62
|
}
|
|
88
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
89
63
|
reset(element) {
|
|
90
64
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
91
65
|
}
|
|
92
66
|
};
|
|
93
67
|
|
|
94
|
-
// src/controllers/dirty_form_controller.ts
|
|
95
68
|
var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
|
|
96
|
-
var visitCoordinators =
|
|
69
|
+
var visitCoordinators = sharedRegistry(
|
|
70
|
+
"stimeo-ui.dirty-form.registry.v1",
|
|
71
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
72
|
+
);
|
|
97
73
|
var TurboVisitCoordinator = class {
|
|
98
74
|
#document;
|
|
99
75
|
#participants = /* @__PURE__ */ new Set();
|
|
@@ -127,7 +103,6 @@ var TurboVisitCoordinator = class {
|
|
|
127
103
|
}
|
|
128
104
|
this.#participants.add(participant);
|
|
129
105
|
}
|
|
130
|
-
/** Removes a participant and reports whether the coordinator became empty. */
|
|
131
106
|
remove(participant) {
|
|
132
107
|
this.#participants.delete(participant);
|
|
133
108
|
if (this.#participants.size > 0) return false;
|
|
@@ -163,7 +138,6 @@ var DirtyFormController = class extends Controller {
|
|
|
163
138
|
#beforeunloadBound = false;
|
|
164
139
|
#timeouts = new SafeTimeout();
|
|
165
140
|
#listeners = new ListenerSet();
|
|
166
|
-
/** Whether the active submission still represents the form's current values. */
|
|
167
141
|
#submitting = false;
|
|
168
142
|
#submitAttempt = null;
|
|
169
143
|
#submittedBaseline = null;
|
|
@@ -216,7 +190,6 @@ var DirtyFormController = class extends Controller {
|
|
|
216
190
|
event.returnValue = this.messageValue;
|
|
217
191
|
};
|
|
218
192
|
connect() {
|
|
219
|
-
this.#resetSubmission();
|
|
220
193
|
this.#baseline = this.#serialize();
|
|
221
194
|
this.#setDirty(false);
|
|
222
195
|
TRANSIENT.reset(this.element);
|
|
@@ -238,17 +211,14 @@ var DirtyFormController = class extends Controller {
|
|
|
238
211
|
this.#unbindBeforeUnload();
|
|
239
212
|
this.#resetSubmission();
|
|
240
213
|
}
|
|
241
|
-
/** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
|
|
242
214
|
markClean() {
|
|
243
215
|
this.#baseline = this.#serialize();
|
|
244
216
|
this.#setDirty(false);
|
|
245
217
|
}
|
|
246
|
-
/** Adopts restored values without erasing an already-dirty user revision. */
|
|
247
218
|
acceptRestore() {
|
|
248
219
|
if (this.#dirty) return;
|
|
249
220
|
this.markClean();
|
|
250
221
|
}
|
|
251
|
-
/** Recomputes dirty against the baseline and flips state when it changes. */
|
|
252
222
|
#evaluate() {
|
|
253
223
|
this.#setDirty(this.#serialize() !== this.#baseline);
|
|
254
224
|
}
|
|
@@ -264,7 +234,6 @@ var DirtyFormController = class extends Controller {
|
|
|
264
234
|
}
|
|
265
235
|
this.dispatch("dirty", { detail: { dirty } });
|
|
266
236
|
}
|
|
267
|
-
/** Evaluates this form for the document-level Turbo visit coordinator. */
|
|
268
237
|
#evaluateVisit(event) {
|
|
269
238
|
if (!this.#dirty || this.#guardSuppressed()) return null;
|
|
270
239
|
const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
|
|
@@ -293,7 +262,6 @@ var DirtyFormController = class extends Controller {
|
|
|
293
262
|
this.#submittedBaseline = null;
|
|
294
263
|
this.#activeSubmission = null;
|
|
295
264
|
}
|
|
296
|
-
/** Stable serialization of the form's controls for change detection. */
|
|
297
265
|
#serialize() {
|
|
298
266
|
const parts = [];
|
|
299
267
|
for (const el of Array.from(this.element.elements)) {
|
|
@@ -310,7 +278,6 @@ var DirtyFormController = class extends Controller {
|
|
|
310
278
|
}
|
|
311
279
|
return JSON.stringify(parts);
|
|
312
280
|
}
|
|
313
|
-
/** A stable key for a control, or null for elements without value semantics. */
|
|
314
281
|
#nameOf(el) {
|
|
315
282
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
316
283
|
return el.name || el.id || "";
|
|
@@ -320,5 +287,4 @@ var DirtyFormController = class extends Controller {
|
|
|
320
287
|
};
|
|
321
288
|
|
|
322
289
|
export { DirtyFormController };
|
|
323
|
-
//# sourceMappingURL=dirty_form_controller.js.map
|
|
324
290
|
//# sourceMappingURL=dirty_form_controller.js.map
|