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/file_dropzone_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,91 +16,190 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
function sharedRegistry(key, create) {
|
|
20
|
+
const symbol = Symbol.for(key);
|
|
21
|
+
const scope = globalThis;
|
|
22
|
+
const existing = scope[symbol];
|
|
23
|
+
if (existing !== void 0) return existing;
|
|
24
|
+
const registry = create();
|
|
25
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
26
|
+
return registry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAttributeValue(value) {
|
|
30
|
+
return value === null || typeof value === "string";
|
|
31
|
+
}
|
|
32
|
+
function readLeaseRecord(element, record) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
35
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
36
|
+
const [original, written, ...rest] = parsed;
|
|
37
|
+
const linked = rest[0] !== false;
|
|
38
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
39
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
40
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var AttributeLease = class _AttributeLease {
|
|
46
|
+
static #registry = sharedRegistry(
|
|
47
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
48
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
49
|
+
);
|
|
50
|
+
#attribute;
|
|
51
|
+
#record;
|
|
52
|
+
#base;
|
|
53
|
+
#held = /* @__PURE__ */ new Set();
|
|
54
|
+
constructor(attribute, owner, options = {}) {
|
|
55
|
+
this.#attribute = attribute;
|
|
56
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
57
|
+
this.#base = options.base === true;
|
|
58
|
+
const names = _AttributeLease.#registry.names;
|
|
59
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
60
|
+
}
|
|
61
|
+
write(element, value) {
|
|
62
|
+
const stack = this.#stack(element);
|
|
63
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
64
|
+
this.#held.add(element);
|
|
65
|
+
const shows = !this.#covered(element, stack, hold);
|
|
66
|
+
hold.written = value;
|
|
67
|
+
this.#mark(element, stack);
|
|
68
|
+
if (shows) this.#reflect(element, value);
|
|
69
|
+
}
|
|
70
|
+
return(element) {
|
|
71
|
+
const held = this.#held.delete(element);
|
|
72
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
73
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
74
|
+
const hold = this.#take(stack);
|
|
75
|
+
if (!hold) return false;
|
|
76
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
77
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
78
|
+
const index = stack.indexOf(hold);
|
|
79
|
+
stack.splice(index, 1);
|
|
80
|
+
const above = stack[index];
|
|
81
|
+
if (above) above.linked &&= hold.linked;
|
|
82
|
+
this.#mark(element, stack);
|
|
83
|
+
if (!owns) return true;
|
|
84
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
85
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
returnAll() {
|
|
89
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
90
|
+
}
|
|
91
|
+
#stack(element) {
|
|
92
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
93
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
94
|
+
stacks.set(element, byAttribute);
|
|
95
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
96
|
+
byAttribute.set(this.#attribute, stack);
|
|
97
|
+
return stack;
|
|
98
|
+
}
|
|
99
|
+
#seed(element) {
|
|
100
|
+
const found = [];
|
|
101
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
102
|
+
const read = readLeaseRecord(element, record);
|
|
103
|
+
if (read) found.push({ ...read, record });
|
|
104
|
+
}
|
|
105
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
106
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
107
|
+
lease: null,
|
|
108
|
+
record,
|
|
109
|
+
written,
|
|
110
|
+
original,
|
|
111
|
+
linked: linked && index > 0
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
#take(stack) {
|
|
115
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
116
|
+
if (hold) hold.lease = this;
|
|
117
|
+
return hold;
|
|
118
|
+
}
|
|
119
|
+
#acquire(element, stack) {
|
|
120
|
+
const current = element.getAttribute(this.#attribute);
|
|
121
|
+
const top = stack.at(-1);
|
|
122
|
+
const bottom = stack[0];
|
|
123
|
+
const over = top?.written === current ? top : void 0;
|
|
124
|
+
const from = this.#base ? bottom : over;
|
|
125
|
+
const hold = {
|
|
126
|
+
lease: this,
|
|
127
|
+
record: this.#record,
|
|
128
|
+
written: current,
|
|
129
|
+
original: from ? from.original : current,
|
|
130
|
+
linked: !this.#base && over !== void 0
|
|
131
|
+
};
|
|
132
|
+
if (this.#base && bottom) {
|
|
133
|
+
bottom.linked = true;
|
|
134
|
+
stack.unshift(hold);
|
|
135
|
+
} else stack.push(hold);
|
|
136
|
+
return hold;
|
|
137
|
+
}
|
|
138
|
+
#covered(element, stack, hold) {
|
|
139
|
+
const current = element.getAttribute(this.#attribute);
|
|
140
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
141
|
+
}
|
|
142
|
+
#mark(element, stack) {
|
|
143
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
144
|
+
stack.forEach((hold, index) => {
|
|
145
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
146
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
147
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
148
|
+
records.set(
|
|
149
|
+
hold.record,
|
|
150
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
for (const [name, recorded] of records) {
|
|
154
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
155
|
+
if (recorded === null) element.removeAttribute(name);
|
|
156
|
+
else element.setAttribute(name, recorded);
|
|
41
157
|
}
|
|
42
158
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
159
|
+
#reflect(element, value) {
|
|
160
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
161
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
162
|
+
else element.setAttribute(this.#attribute, value);
|
|
48
163
|
}
|
|
49
164
|
};
|
|
50
165
|
|
|
51
|
-
// src/utils/detach_gate.ts
|
|
52
166
|
var DetachGate = class _DetachGate {
|
|
53
|
-
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
54
167
|
#pending = false;
|
|
55
|
-
/**
|
|
56
|
-
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
57
|
-
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
58
|
-
* reconnect half of an in-page move from a first connect: a controller whose
|
|
59
|
-
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
60
|
-
* counter) must skip it for the move, where nothing actually restarted.
|
|
61
|
-
*/
|
|
62
168
|
get pending() {
|
|
63
169
|
return this.#pending;
|
|
64
170
|
}
|
|
65
|
-
/**
|
|
66
|
-
* True when the disconnect is definitely a real detach — the element left
|
|
67
|
-
* the document, or `data-controller` no longer lists the identifier. False
|
|
68
|
-
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
69
|
-
*/
|
|
70
171
|
static isDetached(host) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return !tokens.includes(host.identifier);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
77
|
-
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
78
|
-
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
79
|
-
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
80
|
-
* element within the same mutation batch, before the checkpoint drains.
|
|
81
|
-
*/
|
|
172
|
+
return !host.element.isConnected || !_DetachGate.#listed(host);
|
|
173
|
+
}
|
|
82
174
|
disconnected(host, teardown) {
|
|
83
|
-
|
|
175
|
+
const carried = _DetachGate.#carried(host);
|
|
176
|
+
if (!carried && _DetachGate.isDetached(host)) {
|
|
84
177
|
this.#pending = false;
|
|
85
178
|
teardown();
|
|
86
179
|
return;
|
|
87
180
|
}
|
|
88
181
|
this.#pending = true;
|
|
89
|
-
|
|
182
|
+
const probe = () => {
|
|
90
183
|
if (!this.#pending) return;
|
|
91
184
|
this.#pending = false;
|
|
92
185
|
teardown();
|
|
93
|
-
}
|
|
186
|
+
};
|
|
187
|
+
if (carried) setTimeout(probe, 0);
|
|
188
|
+
else queueMicrotask(probe);
|
|
189
|
+
}
|
|
190
|
+
static #carried(host) {
|
|
191
|
+
const element = host.element;
|
|
192
|
+
return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
|
|
193
|
+
}
|
|
194
|
+
static #listed(host) {
|
|
195
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
196
|
+
return tokens.includes(host.identifier);
|
|
94
197
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
97
|
-
* an in-page move) and from the head of any teardown path not routed through
|
|
98
|
-
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
99
|
-
* never run the teardown a second time.
|
|
100
|
-
*/
|
|
101
198
|
cancel() {
|
|
102
199
|
this.#pending = false;
|
|
103
200
|
}
|
|
104
201
|
};
|
|
105
202
|
|
|
106
|
-
// src/utils/element_part.ts
|
|
107
203
|
function matchingPart(root, selector) {
|
|
108
204
|
return root.matches(selector) ? root : root.querySelector(selector);
|
|
109
205
|
}
|
|
@@ -122,7 +218,14 @@ function writeLabel(slot, text) {
|
|
|
122
218
|
else slot.prepend(text);
|
|
123
219
|
}
|
|
124
220
|
|
|
125
|
-
|
|
221
|
+
function ownerIndex(candidates, node) {
|
|
222
|
+
if (!(node instanceof Node)) return -1;
|
|
223
|
+
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
224
|
+
}
|
|
225
|
+
function ownerOf(candidates, node) {
|
|
226
|
+
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
227
|
+
}
|
|
228
|
+
|
|
126
229
|
function inheritsFieldsetDisabled(control) {
|
|
127
230
|
let fieldset = control.closest("fieldset[disabled]");
|
|
128
231
|
while (fieldset) {
|
|
@@ -133,12 +236,54 @@ function inheritsFieldsetDisabled(control) {
|
|
|
133
236
|
return false;
|
|
134
237
|
}
|
|
135
238
|
|
|
136
|
-
|
|
239
|
+
var NUMBER_BOUNDS = {
|
|
240
|
+
nonNegative: { finite: true, min: 0 }};
|
|
241
|
+
function matchesNumberBounds(value, bounds) {
|
|
242
|
+
if (!Number.isFinite(value)) {
|
|
243
|
+
const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
|
|
244
|
+
if (direction === null) return false;
|
|
245
|
+
if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
|
|
246
|
+
}
|
|
247
|
+
if (bounds.min !== void 0 && value < bounds.min) return false;
|
|
248
|
+
if (bounds.max !== void 0 && value > bounds.max) return false;
|
|
249
|
+
if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
|
|
250
|
+
if (bounds.integer && !Number.isInteger(value)) return false;
|
|
251
|
+
if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readNumber(raw, fallback, bounds) {
|
|
256
|
+
return matchesNumberBounds(raw, bounds) ? raw : fallback;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var NumberValueReader = class {
|
|
260
|
+
#lastRejected = /* @__PURE__ */ new Map();
|
|
261
|
+
read(owner, name, raw, fallback, bounds) {
|
|
262
|
+
const resolved = readNumber(raw, fallback, bounds);
|
|
263
|
+
if (matchesNumberBounds(raw, bounds)) {
|
|
264
|
+
this.#lastRejected.delete(name);
|
|
265
|
+
return resolved;
|
|
266
|
+
}
|
|
267
|
+
const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
|
|
268
|
+
const literal = owner.element.getAttribute(attribute);
|
|
269
|
+
if (literal === null) {
|
|
270
|
+
this.#lastRejected.delete(name);
|
|
271
|
+
return resolved;
|
|
272
|
+
}
|
|
273
|
+
if (this.#lastRejected.get(name) !== literal) {
|
|
274
|
+
this.#lastRejected.set(name, literal);
|
|
275
|
+
console.warn(
|
|
276
|
+
`Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return resolved;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
137
283
|
function targetSelector(identifier, name) {
|
|
138
284
|
return `[data-${identifier}-target~="${name}"]`;
|
|
139
285
|
}
|
|
140
286
|
|
|
141
|
-
// src/utils/template_row.ts
|
|
142
287
|
function cloneTemplateRoot(template) {
|
|
143
288
|
const root = template.content.firstElementChild;
|
|
144
289
|
return root instanceof HTMLElement ? root.cloneNode(true) : null;
|
|
@@ -149,22 +294,15 @@ var TemplateRow = class {
|
|
|
149
294
|
constructor(options) {
|
|
150
295
|
this.#options = options;
|
|
151
296
|
}
|
|
152
|
-
/** The attribute selector for one declared part, in this controller's namespace. */
|
|
153
297
|
selector(name) {
|
|
154
298
|
return targetSelector(this.#options.identifier, name);
|
|
155
299
|
}
|
|
156
|
-
/** Re-arms the once-per-connection diagnostic. */
|
|
157
300
|
connect() {
|
|
158
301
|
this.#warned = false;
|
|
159
302
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Names one missing part on the console, at most once per connection, and
|
|
162
|
-
* returns `null` so a caller can hand it straight back.
|
|
163
|
-
*/
|
|
164
303
|
report(missing) {
|
|
165
304
|
return this.#say(`lacks ${missing}`);
|
|
166
305
|
}
|
|
167
|
-
/** Writes one diagnostic per connection, saying what the template got wrong. */
|
|
168
306
|
#say(problem) {
|
|
169
307
|
if (this.#warned) return null;
|
|
170
308
|
this.#warned = true;
|
|
@@ -172,10 +310,6 @@ var TemplateRow = class {
|
|
|
172
310
|
console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
|
|
173
311
|
return null;
|
|
174
312
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Clones the row and resolves its parts, or names what the template got wrong
|
|
177
|
-
* and returns `null`. `values` fills the button's authored `aria-label`.
|
|
178
|
-
*/
|
|
179
313
|
instantiate(template, values) {
|
|
180
314
|
const { root: rootName, required, optional, button: buttonName } = this.#options;
|
|
181
315
|
const root = cloneTemplateRoot(template);
|
|
@@ -203,7 +337,6 @@ var TemplateRow = class {
|
|
|
203
337
|
button.setAttribute("aria-label", fillTemplate(name, values));
|
|
204
338
|
return { root, slots, button };
|
|
205
339
|
}
|
|
206
|
-
/** The row may be its own part; otherwise the first descendant carrying the target. */
|
|
207
340
|
#resolve(root, name) {
|
|
208
341
|
return matchingPart(root, this.selector(name));
|
|
209
342
|
}
|
|
@@ -212,11 +345,11 @@ var TemplateRow = class {
|
|
|
212
345
|
}
|
|
213
346
|
};
|
|
214
347
|
|
|
215
|
-
// src/controllers/file_dropzone_controller.ts
|
|
216
348
|
var DRAGOVER_ATTRIBUTE = "data-dragover";
|
|
217
349
|
var INVALID_ATTRIBUTE = "invalid";
|
|
218
|
-
var
|
|
219
|
-
|
|
350
|
+
var GENERATED_ATTRIBUTE = "generated";
|
|
351
|
+
var FileDropzoneController = class _FileDropzoneController extends Controller {
|
|
352
|
+
#numbers = new NumberValueReader();
|
|
220
353
|
get #invalidAttribute() {
|
|
221
354
|
return `data-${this.identifier}-${INVALID_ATTRIBUTE}`;
|
|
222
355
|
}
|
|
@@ -243,13 +376,17 @@ var FileDropzoneController = class extends Controller {
|
|
|
243
376
|
announceRejectedDuplicateText: { type: String, default: "" },
|
|
244
377
|
announceRejectedCountText: { type: String, default: "" }
|
|
245
378
|
};
|
|
379
|
+
static valueConstraints = {
|
|
380
|
+
maxSize: NUMBER_BOUNDS.nonNegative,
|
|
381
|
+
maxFiles: NUMBER_BOUNDS.nonNegative
|
|
382
|
+
};
|
|
246
383
|
static actions = ["onChange", "onDragLeave", "onDragOver", "onDrop", "openDialog"];
|
|
247
384
|
static events = ["change", "reject", "reconcile"];
|
|
248
|
-
/** Selected files paired with their rendered item and any preview objectURL. */
|
|
249
385
|
#entries = [];
|
|
250
|
-
/** Whether a drag is currently over the zone; the source for `data-dragover`. */
|
|
251
386
|
#dragging = false;
|
|
252
|
-
|
|
387
|
+
#invalid = false;
|
|
388
|
+
#dragoverHook = new AttributeLease(DRAGOVER_ATTRIBUTE, this.identifier);
|
|
389
|
+
#invalidHook = new AttributeLease(this.#invalidAttribute, this.identifier);
|
|
253
390
|
#rows = new TemplateRow({
|
|
254
391
|
identifier: this.identifier,
|
|
255
392
|
root: "item",
|
|
@@ -260,84 +397,97 @@ var FileDropzoneController = class extends Controller {
|
|
|
260
397
|
noun: "item template"
|
|
261
398
|
});
|
|
262
399
|
#gate = new DetachGate();
|
|
263
|
-
#
|
|
264
|
-
|
|
400
|
+
#listWatch = new MutationObserver((records) => {
|
|
401
|
+
const lost = records.some(
|
|
402
|
+
(record) => Array.from(record.removedNodes).some(
|
|
403
|
+
(node) => this.#entries.some((entry) => entry.item === node)
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
if (!lost) return;
|
|
407
|
+
this.#rehome();
|
|
408
|
+
this.#paintZone();
|
|
409
|
+
});
|
|
410
|
+
#connected = false;
|
|
411
|
+
get #generatedAttribute() {
|
|
412
|
+
return `data-${this.identifier}-${GENERATED_ATTRIBUTE}`;
|
|
413
|
+
}
|
|
265
414
|
connect() {
|
|
415
|
+
const moved = this.#gate.pending;
|
|
266
416
|
this.#gate.cancel();
|
|
417
|
+
this.#connected = true;
|
|
267
418
|
this.#rows.connect();
|
|
268
|
-
this.#
|
|
419
|
+
if (!moved) this.#discardInherited();
|
|
269
420
|
}
|
|
270
|
-
/**
|
|
271
|
-
* Releases the delegated listeners. The selection itself survives an in-page
|
|
272
|
-
* move and is released only once the gate proves a real detach — revoking a
|
|
273
|
-
* preview URL on a move would leave a live item pointing at a dead `blob:`.
|
|
274
|
-
*/
|
|
275
421
|
disconnect() {
|
|
422
|
+
this.#connected = false;
|
|
276
423
|
for (const list of this.listTargets) list.removeEventListener("click", this.#onItemClick);
|
|
277
|
-
this.#
|
|
424
|
+
this.#listWatch.disconnect();
|
|
278
425
|
this.#gate.disconnected(this, () => this.#teardown());
|
|
279
426
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
427
|
+
#discardInherited() {
|
|
428
|
+
let discarded = false;
|
|
429
|
+
for (const list of this.listTargets) {
|
|
430
|
+
for (const item of Array.from(list.children)) {
|
|
431
|
+
if (!item.hasAttribute(this.#generatedAttribute)) continue;
|
|
432
|
+
item.remove();
|
|
433
|
+
discarded = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const zone of this.zoneTargets) {
|
|
437
|
+
this.#dragoverHook.return(zone);
|
|
438
|
+
this.#invalidHook.return(zone);
|
|
439
|
+
}
|
|
440
|
+
if (!discarded) return;
|
|
441
|
+
this.#dragging = false;
|
|
442
|
+
this.#invalid = false;
|
|
443
|
+
this.#syncInput();
|
|
444
|
+
this.dispatch("reconcile", { detail: { files: this.#files } });
|
|
445
|
+
}
|
|
286
446
|
listTargetConnected(list) {
|
|
287
|
-
this.#
|
|
447
|
+
list.addEventListener("click", this.#onItemClick);
|
|
448
|
+
this.#listWatch.observe(list, { childList: true });
|
|
449
|
+
this.#rehome();
|
|
288
450
|
}
|
|
289
|
-
/** Releases only the list target that actually disconnected. */
|
|
290
451
|
listTargetDisconnected(list) {
|
|
291
452
|
list.removeEventListener("click", this.#onItemClick);
|
|
453
|
+
if (this.#connected) this.#rehome();
|
|
292
454
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
455
|
+
inputTargetConnected() {
|
|
456
|
+
if (this.#connected) this.#syncInput();
|
|
457
|
+
}
|
|
458
|
+
inputTargetDisconnected() {
|
|
459
|
+
if (this.#connected) this.#syncInput();
|
|
460
|
+
}
|
|
461
|
+
zoneTargetConnected() {
|
|
462
|
+
if (this.#connected) this.#paintZone();
|
|
463
|
+
}
|
|
464
|
+
zoneTargetDisconnected(zone) {
|
|
465
|
+
if (this.zoneTargets.includes(zone)) return;
|
|
466
|
+
this.#dragoverHook.return(zone);
|
|
467
|
+
this.#invalidHook.return(zone);
|
|
468
|
+
if (this.#connected) this.#paintZone();
|
|
299
469
|
}
|
|
300
|
-
/** Opens the native file dialog. Bound via `data-action` (trigger click). */
|
|
301
470
|
openDialog() {
|
|
302
471
|
if (this.#isDisabled) return;
|
|
303
472
|
this.inputTarget.click();
|
|
304
473
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* the dialog just returned, so the accepted set is written back over it once
|
|
308
|
-
* the batch is validated.
|
|
309
|
-
*/
|
|
310
|
-
onChange() {
|
|
311
|
-
const files = this.inputTarget.files;
|
|
474
|
+
onChange(event) {
|
|
475
|
+
const files = (ownerOf(this.inputTargets, event?.target) ?? this.inputTarget).files;
|
|
312
476
|
if (files) this.#addFiles(files);
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Marks the zone as a drop target and announces the affordance once per drag.
|
|
317
|
-
* Bound via `data-action` (dragover). Leaving the default alone is what refuses
|
|
318
|
-
* the drop, so a disabled field and a drag an inner dropzone already claimed
|
|
319
|
-
* both fall through untouched.
|
|
320
|
-
*/
|
|
477
|
+
}
|
|
321
478
|
onDragOver(event) {
|
|
322
479
|
if (event.defaultPrevented || this.#isDisabled) return;
|
|
323
480
|
event.preventDefault();
|
|
324
481
|
if (this.#dragging) return;
|
|
325
482
|
this.#dragging = true;
|
|
326
|
-
|
|
483
|
+
this.#paintZone();
|
|
327
484
|
announce(fillTemplate(this.announceDragTextValue, { total: this.#entries.length }));
|
|
328
485
|
}
|
|
329
|
-
/**
|
|
330
|
-
* Clears the drag-over flag when the pointer leaves the zone. Bound via
|
|
331
|
-
* `data-action` (dragleave). `dragleave` bubbles from every descendant the
|
|
332
|
-
* pointer crosses, so the flag only drops when the element being entered is
|
|
333
|
-
* outside the zone (or there is none, the pointer having left the window).
|
|
334
|
-
*/
|
|
335
486
|
onDragLeave(event) {
|
|
336
487
|
const next = event.relatedTarget;
|
|
337
488
|
if (this.hasZoneTarget && next instanceof Node && this.zoneTarget.contains(next)) return;
|
|
338
489
|
this.#endDrag();
|
|
339
490
|
}
|
|
340
|
-
/** Accepts dropped files, clearing the drag-over state. Bound via `data-action` (drop). */
|
|
341
491
|
onDrop(event) {
|
|
342
492
|
if (event.defaultPrevented) return;
|
|
343
493
|
this.#endDrag();
|
|
@@ -345,15 +495,6 @@ var FileDropzoneController = class extends Controller {
|
|
|
345
495
|
event.preventDefault();
|
|
346
496
|
if (event.dataTransfer?.files) this.#addFiles(event.dataTransfer.files);
|
|
347
497
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Removes the file whose remove button was clicked. Delegated on the list
|
|
350
|
-
* container rather than bound per item via `data-action`, so it works the instant
|
|
351
|
-
* an item is appended without waiting on Stimulus to wire a freshly created element.
|
|
352
|
-
* Only the item's declared `remove` target counts, so an authored second control
|
|
353
|
-
* inside an item does what it says instead of silently discarding the file. The
|
|
354
|
-
* button has to belong to a tracked item, which is what makes a list this
|
|
355
|
-
* controller no longer renders into inert — its items moved out with it.
|
|
356
|
-
*/
|
|
357
498
|
#onItemClick = (event) => {
|
|
358
499
|
const button = event.target.closest(
|
|
359
500
|
`button${this.#rows.selector("remove")}`
|
|
@@ -361,15 +502,9 @@ var FileDropzoneController = class extends Controller {
|
|
|
361
502
|
const index = this.#entries.findIndex((entry) => entry.item.contains(button));
|
|
362
503
|
if (index !== -1) this.#removeAt(index);
|
|
363
504
|
};
|
|
364
|
-
/**
|
|
365
|
-
* Validates each incoming file, renders the accepted ones, and reports the batch.
|
|
366
|
-
*
|
|
367
|
-
* @stimeoRuntimeOnly The limits decide which files of this one drop are taken, and the texts word
|
|
368
|
-
* the announcements it makes.
|
|
369
|
-
*/
|
|
370
505
|
#addFiles(files) {
|
|
371
506
|
this.#rehome();
|
|
372
|
-
|
|
507
|
+
this.#invalid = false;
|
|
373
508
|
const rejected = /* @__PURE__ */ new Map();
|
|
374
509
|
const turnedAway = [];
|
|
375
510
|
let addedName = "";
|
|
@@ -377,7 +512,7 @@ var FileDropzoneController = class extends Controller {
|
|
|
377
512
|
for (const file of Array.from(files)) {
|
|
378
513
|
const reason = this.#validate(file);
|
|
379
514
|
if (reason !== null) {
|
|
380
|
-
|
|
515
|
+
this.#invalid = true;
|
|
381
516
|
const batch = rejected.get(reason);
|
|
382
517
|
if (batch) batch.count += 1;
|
|
383
518
|
else rejected.set(reason, { name: file.name, count: 1 });
|
|
@@ -388,6 +523,7 @@ var FileDropzoneController = class extends Controller {
|
|
|
388
523
|
if (added === 0) addedName = file.name;
|
|
389
524
|
added += 1;
|
|
390
525
|
}
|
|
526
|
+
this.#paintZone();
|
|
391
527
|
this.#syncInput();
|
|
392
528
|
if (added > 0) {
|
|
393
529
|
this.#announce(this.announceAddedTextValue, addedName, added);
|
|
@@ -400,14 +536,9 @@ var FileDropzoneController = class extends Controller {
|
|
|
400
536
|
this.#announce(this.#rejectText(reason), batch.name, batch.count);
|
|
401
537
|
}
|
|
402
538
|
}
|
|
403
|
-
/**
|
|
404
|
-
* Returns the rejection reason for `file`, or `null` when it is acceptable.
|
|
405
|
-
* The file's own defects are decided first, so a full list still tells the user
|
|
406
|
-
* which files it would never have taken.
|
|
407
|
-
*/
|
|
408
539
|
#validate(file) {
|
|
409
540
|
if (!this.#matchesAccept(file)) return "type";
|
|
410
|
-
if (this
|
|
541
|
+
if (this.#safeMaxSize > 0 && file.size > this.#safeMaxSize) return "size";
|
|
411
542
|
if (!this.allowDuplicatesValue && this.#entries.some((entry) => this.#isSame(entry.file, file))) {
|
|
412
543
|
return "duplicate";
|
|
413
544
|
}
|
|
@@ -415,21 +546,9 @@ var FileDropzoneController = class extends Controller {
|
|
|
415
546
|
if (limit > 0 && this.#entries.length >= limit) return "count";
|
|
416
547
|
return null;
|
|
417
548
|
}
|
|
418
|
-
/**
|
|
419
|
-
* Whether two files are the same selection. `File` objects from separate picks
|
|
420
|
-
* are never the same reference, so identity is the triple the platform exposes.
|
|
421
|
-
*/
|
|
422
549
|
#isSame(a, b) {
|
|
423
550
|
return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
|
|
424
551
|
}
|
|
425
|
-
/**
|
|
426
|
-
* Builds one preview item (name, optional thumbnail, remove button) and reports
|
|
427
|
-
* whether it was rendered.
|
|
428
|
-
*
|
|
429
|
-
* A refused row leaves the addition a no-op — nothing about the selection, the
|
|
430
|
-
* native input, the announcements, or the events changes — and the row reports
|
|
431
|
-
* why on the console once per connection.
|
|
432
|
-
*/
|
|
433
552
|
#appendFile(file) {
|
|
434
553
|
if (!this.hasListTarget) {
|
|
435
554
|
this.#rows.report('a "list" target to render into');
|
|
@@ -442,6 +561,7 @@ var FileDropzoneController = class extends Controller {
|
|
|
442
561
|
const row = this.#rows.instantiate(this.itemTemplateTarget, { name: file.name });
|
|
443
562
|
if (!row) return false;
|
|
444
563
|
const item = row.root;
|
|
564
|
+
item.setAttribute(this.#generatedAttribute, "");
|
|
445
565
|
writeLabel(row.slots.name, file.name);
|
|
446
566
|
const thumb = row.slots.thumb;
|
|
447
567
|
let url;
|
|
@@ -457,7 +577,6 @@ var FileDropzoneController = class extends Controller {
|
|
|
457
577
|
this.#entries.push({ file, item, url });
|
|
458
578
|
return true;
|
|
459
579
|
}
|
|
460
|
-
/** Removes entry `index`, revokes its preview, and re-homes focus. */
|
|
461
580
|
#removeAt(index) {
|
|
462
581
|
const entry = this.#entries[index];
|
|
463
582
|
if (!entry) return;
|
|
@@ -474,7 +593,6 @@ var FileDropzoneController = class extends Controller {
|
|
|
474
593
|
(buttons[index] ?? buttons[buttons.length - 1])?.focus();
|
|
475
594
|
}
|
|
476
595
|
}
|
|
477
|
-
/** Whether `file` satisfies the input's `accept` list (empty accepts all). */
|
|
478
596
|
#matchesAccept(file) {
|
|
479
597
|
const accept = this.inputTarget.accept.trim();
|
|
480
598
|
if (accept === "") return true;
|
|
@@ -488,15 +606,9 @@ var FileDropzoneController = class extends Controller {
|
|
|
488
606
|
return type === token;
|
|
489
607
|
});
|
|
490
608
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Sends one consumer-worded message to the page's shared announcer. `{name}` is
|
|
493
|
-
* the file the message is about, `{count}` how many files it covers, and
|
|
494
|
-
* `{total}` how many are selected once the batch has settled.
|
|
495
|
-
*/
|
|
496
609
|
#announce(template, name, count) {
|
|
497
610
|
announce(fillTemplate(template, { name, count, total: this.#entries.length }));
|
|
498
611
|
}
|
|
499
|
-
/** The consumer's wording for one rejection reason. */
|
|
500
612
|
#rejectText(reason) {
|
|
501
613
|
switch (reason) {
|
|
502
614
|
case "type":
|
|
@@ -509,11 +621,6 @@ var FileDropzoneController = class extends Controller {
|
|
|
509
621
|
return this.announceRejectedCountTextValue;
|
|
510
622
|
}
|
|
511
623
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Mirrors the accepted set onto the native input, so a plain form submit carries
|
|
514
|
-
* the dropped files. Skipped where `DataTransfer` cannot be constructed: the
|
|
515
|
-
* widget keeps working and the consumer still receives every `File` on `change`.
|
|
516
|
-
*/
|
|
517
624
|
#syncInput() {
|
|
518
625
|
if (!this.hasInputTarget) return;
|
|
519
626
|
const transfer = this.#newTransfer();
|
|
@@ -521,7 +628,6 @@ var FileDropzoneController = class extends Controller {
|
|
|
521
628
|
for (const entry of this.#entries) transfer.items.add(entry.file);
|
|
522
629
|
this.inputTarget.files = transfer.files;
|
|
523
630
|
}
|
|
524
|
-
/** A usable empty `DataTransfer`, or `null` where the platform has none. */
|
|
525
631
|
#newTransfer() {
|
|
526
632
|
try {
|
|
527
633
|
return new DataTransfer();
|
|
@@ -529,35 +635,23 @@ var FileDropzoneController = class extends Controller {
|
|
|
529
635
|
return null;
|
|
530
636
|
}
|
|
531
637
|
}
|
|
532
|
-
/**
|
|
533
|
-
* Moves surviving preview items back under the current list. A morph that
|
|
534
|
-
* empties the list in place leaves the selection with no rendering, and the
|
|
535
|
-
* files it holds cannot be rebuilt from the DOM, so the items are re-homed
|
|
536
|
-
* rather than forgotten.
|
|
537
|
-
*/
|
|
538
638
|
#rehome() {
|
|
539
639
|
if (!this.hasListTarget) return;
|
|
540
640
|
for (const entry of this.#entries) {
|
|
541
641
|
if (!this.listTarget.contains(entry.item)) this.listTarget.appendChild(entry.item);
|
|
542
642
|
}
|
|
543
643
|
}
|
|
544
|
-
/** Drops the drag-over state, whether the drag ended in a drop or left the zone. */
|
|
545
644
|
#endDrag() {
|
|
546
645
|
this.#dragging = false;
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
* rewind reports itself as `reconcile` instead.
|
|
554
|
-
*/
|
|
555
|
-
#rewindForCache() {
|
|
556
|
-
const had = this.#entries.length > 0;
|
|
557
|
-
this.#reset();
|
|
558
|
-
if (had) this.dispatch("reconcile", { detail: { files: this.#files } });
|
|
646
|
+
this.#paintZone();
|
|
647
|
+
}
|
|
648
|
+
#paintZone() {
|
|
649
|
+
if (!this.hasZoneTarget) return;
|
|
650
|
+
this.#dragoverHook.write(this.zoneTarget, this.#dragging ? "" : null);
|
|
651
|
+
this.#invalidHook.write(this.zoneTarget, this.#invalid ? "" : null);
|
|
559
652
|
}
|
|
560
653
|
#reset() {
|
|
654
|
+
this.#invalid = false;
|
|
561
655
|
for (const entry of this.#entries) {
|
|
562
656
|
if (entry.url) URL.revokeObjectURL(entry.url);
|
|
563
657
|
entry.item.remove();
|
|
@@ -565,23 +659,17 @@ var FileDropzoneController = class extends Controller {
|
|
|
565
659
|
this.#entries.length = 0;
|
|
566
660
|
this.#syncInput();
|
|
567
661
|
this.#endDrag();
|
|
568
|
-
if (this.hasZoneTarget) this.zoneTarget.removeAttribute(this.#invalidAttribute);
|
|
569
662
|
}
|
|
570
|
-
/** Releases the selection once the disconnect is known to be a real detach. */
|
|
571
663
|
#teardown() {
|
|
572
|
-
this.#gate.cancel();
|
|
573
664
|
this.#reset();
|
|
574
665
|
}
|
|
575
|
-
/** Whether the field refuses input, natively or through an ancestor `fieldset`. */
|
|
576
666
|
get #isDisabled() {
|
|
577
667
|
return this.inputTarget.disabled || inheritsFieldsetDisabled(this.inputTarget);
|
|
578
668
|
}
|
|
579
|
-
/** Effective file cap: `maxFiles`, or 1 when the input is single-select. */
|
|
580
669
|
get #effectiveMaxFiles() {
|
|
581
|
-
if (this
|
|
670
|
+
if (this.#safeMaxFiles > 0) return this.#safeMaxFiles;
|
|
582
671
|
return this.inputTarget.multiple ? 0 : 1;
|
|
583
672
|
}
|
|
584
|
-
/** The declared remove button of each rendered item, in selection order. */
|
|
585
673
|
get #removeButtons() {
|
|
586
674
|
const buttons = [];
|
|
587
675
|
for (const entry of this.#entries) {
|
|
@@ -593,12 +681,28 @@ var FileDropzoneController = class extends Controller {
|
|
|
593
681
|
}
|
|
594
682
|
return buttons;
|
|
595
683
|
}
|
|
596
|
-
/** The accepted files in selection order. */
|
|
597
684
|
get #files() {
|
|
598
685
|
return this.#entries.map((entry) => entry.file);
|
|
599
686
|
}
|
|
687
|
+
get #safeMaxSize() {
|
|
688
|
+
return this.#numbers.read(
|
|
689
|
+
this,
|
|
690
|
+
"maxSize",
|
|
691
|
+
this.maxSizeValue,
|
|
692
|
+
_FileDropzoneController.values.maxSize.default,
|
|
693
|
+
_FileDropzoneController.valueConstraints.maxSize
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
get #safeMaxFiles() {
|
|
697
|
+
return this.#numbers.read(
|
|
698
|
+
this,
|
|
699
|
+
"maxFiles",
|
|
700
|
+
this.maxFilesValue,
|
|
701
|
+
_FileDropzoneController.values.maxFiles.default,
|
|
702
|
+
_FileDropzoneController.valueConstraints.maxFiles
|
|
703
|
+
);
|
|
704
|
+
}
|
|
600
705
|
};
|
|
601
706
|
|
|
602
707
|
export { FileDropzoneController };
|
|
603
|
-
//# sourceMappingURL=file_dropzone_controller.js.map
|
|
604
708
|
//# sourceMappingURL=file_dropzone_controller.js.map
|