@marianmeres/stuic 3.181.0 → 3.183.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.
- package/README.md +1 -1
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte +101 -150
- package/dist/components/FieldsBuilder/FieldsBuilder.svelte.d.ts +10 -1
- package/dist/components/FieldsBuilder/README.md +82 -3
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte +624 -0
- package/dist/components/FieldsBuilder/_internal/ColumnsEditor.svelte.d.ts +41 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte +174 -0
- package/dist/components/FieldsBuilder/_internal/ExtrasEditor.svelte.d.ts +20 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.d.ts +27 -0
- package/dist/components/FieldsBuilder/_internal/column-meta.js +15 -0
- package/dist/components/FieldsBuilder/i18n-sk.js +17 -0
- package/dist/components/FieldsBuilder/i18n.d.ts +17 -0
- package/dist/components/FieldsBuilder/i18n.js +17 -0
- package/dist/components/FieldsBuilder/index.css +20 -0
- package/dist/components/FieldsBuilder/index.d.ts +2 -2
- package/dist/components/FieldsBuilder/types.d.ts +39 -0
- package/dist/components/FieldsBuilder/utils.d.ts +36 -1
- package/dist/components/FieldsBuilder/utils.js +180 -55
- package/dist/components/Input/FieldAssets.svelte +9 -132
- package/dist/components/Input/FieldSingleAsset.svelte +876 -0
- package/dist/components/Input/FieldSingleAsset.svelte.d.ts +135 -0
- package/dist/components/Input/FieldTable.svelte +907 -0
- package/dist/components/Input/FieldTable.svelte.d.ts +98 -0
- package/dist/components/Input/README.md +467 -20
- package/dist/components/Input/_internal/asset-helpers.d.ts +16 -0
- package/dist/components/Input/_internal/asset-helpers.js +46 -0
- package/dist/components/Input/_internal/paste-target.d.ts +15 -0
- package/dist/components/Input/_internal/paste-target.js +114 -0
- package/dist/components/Input/field-single-asset-i18n-sk.d.ts +21 -0
- package/dist/components/Input/field-single-asset-i18n-sk.js +49 -0
- package/dist/components/Input/field-single-asset-i18n.d.ts +68 -0
- package/dist/components/Input/field-single-asset-i18n.js +75 -0
- package/dist/components/Input/field-table-i18n-sk.d.ts +21 -0
- package/dist/components/Input/field-table-i18n-sk.js +41 -0
- package/dist/components/Input/field-table-i18n.d.ts +60 -0
- package/dist/components/Input/field-table-i18n.js +66 -0
- package/dist/components/Input/field-table-number.d.ts +48 -0
- package/dist/components/Input/field-table-number.js +86 -0
- package/dist/components/Input/index.css +801 -0
- package/dist/components/Input/index.d.ts +7 -0
- package/dist/components/Input/index.js +7 -0
- package/docs/domains/actions.md +3 -3
- package/docs/domains/components.md +45 -38
- package/package.json +1 -1
|
@@ -57,10 +57,102 @@ export function isKeyReserved(key, reservedKeys) {
|
|
|
57
57
|
? !!reservedKeys(key)
|
|
58
58
|
: reservedKeys.includes(key);
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The palette a column of `entry` may take: its own `columnTypes`, else the
|
|
62
|
+
* component's `types` without every entry that `supportsColumns` itself
|
|
63
|
+
* (columns never nest).
|
|
64
|
+
*/
|
|
65
|
+
export function resolveColumnTypes(entry, types) {
|
|
66
|
+
return entry.columnTypes ?? (types ?? []).filter((td) => !td.supportsColumns);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Materialize the `default` of every declared extra that the bag does not
|
|
70
|
+
* hold yet. Returns the same bag when nothing was seeded (so `undefined`
|
|
71
|
+
* stays `undefined` for a type without defaults).
|
|
72
|
+
*/
|
|
73
|
+
export function seedExtraDefaults(extras, entry) {
|
|
74
|
+
let next = extras;
|
|
75
|
+
for (const ex of entry?.extras ?? []) {
|
|
76
|
+
if (ex.default !== undefined && next?.[ex.key] === undefined) {
|
|
77
|
+
next = { ...(next ?? {}), [ex.key]: ex.default };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// the single-value rules, shared by the field loop and the column loop so the
|
|
84
|
+
// two cannot drift; each returns the FIRST offending message or undefined
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
function labelError(label, defaultLanguage, t) {
|
|
87
|
+
return getLocalizedText(label, defaultLanguage).trim()
|
|
88
|
+
? undefined
|
|
89
|
+
: t("err_label_required");
|
|
90
|
+
}
|
|
91
|
+
function keyError(key, rules, t) {
|
|
92
|
+
// presence is checked trimmed, but pattern/length run on the RAW key —
|
|
93
|
+
// a whitespace-padded key must fail here, not at the consumer's gate
|
|
94
|
+
const k = key ?? "";
|
|
95
|
+
if (!k.trim())
|
|
96
|
+
return t("err_key_required");
|
|
97
|
+
if (k.length > rules.maxLength)
|
|
98
|
+
return t("err_key_maxlength", { max: rules.maxLength });
|
|
99
|
+
if (!rules.pattern.test(k))
|
|
100
|
+
return t("err_key_pattern");
|
|
101
|
+
if (rules.isDuplicate(k.trim()))
|
|
102
|
+
return t(rules.duplicateMessageKey);
|
|
103
|
+
if (rules.isReserved?.(k))
|
|
104
|
+
return t("err_key_reserved");
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function optionsError(options, t) {
|
|
108
|
+
const list = options ?? [];
|
|
109
|
+
const values = list.map((o) => (o.value ?? "").trim());
|
|
110
|
+
if (!list.length)
|
|
111
|
+
return t("err_options_required");
|
|
112
|
+
if (values.some((v) => !v))
|
|
113
|
+
return t("err_option_value_required");
|
|
114
|
+
if (new Set(values).size !== values.length)
|
|
115
|
+
return t("err_option_value_duplicate");
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
// `maxlength` is re-checked here on purpose: the input's attribute stops
|
|
119
|
+
// typing, it does not bound a seeded or pasted-then-mutated value
|
|
120
|
+
function extrasError(extras, entry, displayLanguages, t) {
|
|
121
|
+
for (const ex of entry?.extras ?? []) {
|
|
122
|
+
if (ex.type !== "string" || !ex.maxlength)
|
|
123
|
+
continue;
|
|
124
|
+
const v = extras?.[ex.key];
|
|
125
|
+
if (typeof v === "string" && v.length > ex.maxlength) {
|
|
126
|
+
return t("err_extra_maxlength", {
|
|
127
|
+
label: getLocalizedText(ex.label, displayLanguages),
|
|
128
|
+
max: ex.maxlength,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function countKeys(items) {
|
|
135
|
+
const counts = new Map();
|
|
136
|
+
for (const it of items) {
|
|
137
|
+
const k = (it.key ?? "").trim();
|
|
138
|
+
if (k)
|
|
139
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
return counts;
|
|
142
|
+
}
|
|
143
|
+
/** First string member of an errors object (skips the `columnErrors` array). */
|
|
144
|
+
function firstMessage(errs) {
|
|
145
|
+
if (!errs)
|
|
146
|
+
return "";
|
|
147
|
+
return Object.values(errs).find((v) => typeof v === "string") ?? "";
|
|
148
|
+
}
|
|
60
149
|
/**
|
|
61
150
|
* Validate a list of field defs: label non-empty, key present/pattern/length/
|
|
62
151
|
* unique/not-reserved, choice types have at least one option with non-empty
|
|
63
152
|
* unique values, string extras within `maxlength`, `maxFields` not exceeded.
|
|
153
|
+
* A `supportsColumns` type additionally needs at least one column, at most
|
|
154
|
+
* `maxColumns`, and every column (of a known column type) passes the label /
|
|
155
|
+
* key (unique within the field) / options / extras rules above.
|
|
64
156
|
*
|
|
65
157
|
* This is client-side convenience only — a consumer persisting the list MUST
|
|
66
158
|
* re-validate server-side; this function is not a security boundary.
|
|
@@ -73,76 +165,109 @@ export function validateFieldDefs(defs, opts = {}) {
|
|
|
73
165
|
const displayLanguages = [opts.displayLanguage, opts.defaultLanguage].filter((l) => !!l);
|
|
74
166
|
const rowErrors = defs.map(() => null);
|
|
75
167
|
const put = (i, field, msg) => {
|
|
168
|
+
if (!msg)
|
|
169
|
+
return;
|
|
76
170
|
rowErrors[i] ??= {};
|
|
77
171
|
rowErrors[i][field] ??= msg;
|
|
78
172
|
};
|
|
79
173
|
// keys of ALL defs (incl. unknown types) occupy the key space
|
|
80
|
-
const keyCounts =
|
|
81
|
-
for (const d of defs) {
|
|
82
|
-
const k = (d.key ?? "").trim();
|
|
83
|
-
if (k)
|
|
84
|
-
keyCounts.set(k, (keyCounts.get(k) ?? 0) + 1);
|
|
85
|
-
}
|
|
174
|
+
const keyCounts = countKeys(defs);
|
|
86
175
|
defs.forEach((d, i) => {
|
|
87
176
|
// unknown type: keep as-is, do not block (see the doc comment above)
|
|
88
177
|
if (typeMap && !typeMap.has(d.type))
|
|
89
178
|
return;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const values = options.map((o) => (o.value ?? "").trim());
|
|
114
|
-
if (!options.length) {
|
|
115
|
-
put(i, "options", t("err_options_required"));
|
|
116
|
-
}
|
|
117
|
-
else if (values.some((v) => !v)) {
|
|
118
|
-
put(i, "options", t("err_option_value_required"));
|
|
119
|
-
}
|
|
120
|
-
else if (new Set(values).size !== values.length) {
|
|
121
|
-
put(i, "options", t("err_option_value_duplicate"));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// `maxlength` is re-checked here on purpose: the input's attribute stops
|
|
125
|
-
// typing, it does not bound a seeded or pasted-then-mutated value
|
|
126
|
-
for (const ex of typeMap?.get(d.type)?.extras ?? []) {
|
|
127
|
-
if (ex.type !== "string" || !ex.maxlength)
|
|
128
|
-
continue;
|
|
129
|
-
const v = d.extras?.[ex.key];
|
|
130
|
-
if (typeof v === "string" && v.length > ex.maxlength) {
|
|
131
|
-
put(i, "extras", t("err_extra_maxlength", {
|
|
132
|
-
label: getLocalizedText(ex.label, displayLanguages),
|
|
133
|
-
max: ex.maxlength,
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
179
|
+
const entry = typeMap?.get(d.type);
|
|
180
|
+
put(i, "label", labelError(d.label, opts.defaultLanguage, t));
|
|
181
|
+
put(i, "key", keyError(d.key, {
|
|
182
|
+
pattern: keyPattern,
|
|
183
|
+
maxLength: keyMaxLength,
|
|
184
|
+
isDuplicate: (k) => (keyCounts.get(k) ?? 0) > 1,
|
|
185
|
+
duplicateMessageKey: "err_key_duplicate",
|
|
186
|
+
isReserved: (k) => isKeyReserved(k, opts.reservedKeys),
|
|
187
|
+
}, t));
|
|
188
|
+
if (entry?.supportsOptions)
|
|
189
|
+
put(i, "options", optionsError(d.options, t));
|
|
190
|
+
if (entry?.supportsColumns) {
|
|
191
|
+
const { summary, columnErrors } = validateColumns(d.columns, entry, {
|
|
192
|
+
columnTypes: resolveColumnTypes(entry, opts.types),
|
|
193
|
+
keyPattern,
|
|
194
|
+
keyMaxLength,
|
|
195
|
+
defaultLanguage: opts.defaultLanguage,
|
|
196
|
+
displayLanguages,
|
|
197
|
+
t,
|
|
198
|
+
});
|
|
199
|
+
put(i, "columns", summary);
|
|
200
|
+
if (columnErrors)
|
|
201
|
+
rowErrors[i].columnErrors = columnErrors;
|
|
136
202
|
}
|
|
203
|
+
put(i, "extras", extrasError(d.extras, entry, displayLanguages, t));
|
|
137
204
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
// the summary is the first STRING member of the first erroneous row —
|
|
206
|
+
// `columnErrors` (an array) must never be picked here
|
|
207
|
+
let message = firstMessage(rowErrors.find(Boolean));
|
|
141
208
|
if (!message && opts.maxFields && defs.length > opts.maxFields) {
|
|
142
209
|
message = t("err_max_fields", { max: opts.maxFields });
|
|
143
210
|
}
|
|
144
211
|
return { valid: !message, message, rowErrors };
|
|
145
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* The column rules of one `supportsColumns` def. A column whose type is not
|
|
215
|
+
* in the column palette is NOT validated (it round-trips untouched, like an
|
|
216
|
+
* unknown field type) but its key still counts toward uniqueness. `reservedKeys`
|
|
217
|
+
* does not apply: it guards the field-key namespace, and a column key lives
|
|
218
|
+
* inside one field's rows.
|
|
219
|
+
*/
|
|
220
|
+
function validateColumns(columns, entry, opts) {
|
|
221
|
+
const { t } = opts;
|
|
222
|
+
const list = columns ?? [];
|
|
223
|
+
// a `supportsColumns` entry inside the column palette is a plain column
|
|
224
|
+
// type here — nothing nests — so only `type` / `supportsOptions` / `extras` matter
|
|
225
|
+
const colTypeMap = new Map(opts.columnTypes.map((td) => [td.type, td]));
|
|
226
|
+
const keyCounts = countKeys(list);
|
|
227
|
+
let summary;
|
|
228
|
+
if (!list.length)
|
|
229
|
+
summary = t("err_columns_required");
|
|
230
|
+
else if (entry.maxColumns && list.length > entry.maxColumns) {
|
|
231
|
+
summary = t("err_max_columns", { max: entry.maxColumns });
|
|
232
|
+
}
|
|
233
|
+
const columnErrors = list.map((c) => {
|
|
234
|
+
const ct = colTypeMap.get(c.type);
|
|
235
|
+
if (!ct)
|
|
236
|
+
return null;
|
|
237
|
+
const e = {};
|
|
238
|
+
const label = labelError(c.label, opts.defaultLanguage, t);
|
|
239
|
+
if (label)
|
|
240
|
+
e.label = label;
|
|
241
|
+
const key = keyError(c.key, {
|
|
242
|
+
pattern: opts.keyPattern,
|
|
243
|
+
maxLength: opts.keyMaxLength,
|
|
244
|
+
isDuplicate: (k) => (keyCounts.get(k) ?? 0) > 1,
|
|
245
|
+
duplicateMessageKey: "err_column_key_duplicate",
|
|
246
|
+
}, t);
|
|
247
|
+
if (key)
|
|
248
|
+
e.key = key;
|
|
249
|
+
if (ct.supportsOptions) {
|
|
250
|
+
const options = optionsError(c.options, t);
|
|
251
|
+
if (options)
|
|
252
|
+
e.options = options;
|
|
253
|
+
}
|
|
254
|
+
const extras = extrasError(c.extras, ct, opts.displayLanguages, t);
|
|
255
|
+
if (extras)
|
|
256
|
+
e.extras = extras;
|
|
257
|
+
return Object.keys(e).length ? e : null;
|
|
258
|
+
});
|
|
259
|
+
const firstIdx = columnErrors.findIndex(Boolean);
|
|
260
|
+
if (!summary && firstIdx >= 0) {
|
|
261
|
+
summary = t("err_column", {
|
|
262
|
+
position: firstIdx + 1,
|
|
263
|
+
message: firstMessage(columnErrors[firstIdx]),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
summary,
|
|
268
|
+
columnErrors: firstIdx >= 0 ? columnErrors : undefined,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
146
271
|
/**
|
|
147
272
|
* A small general-purpose palette — handy for demos and for consumers with no
|
|
148
273
|
* opinion. `types` is a required prop, so nobody gets this by accident.
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
import SpinnerCircleOscillate from "../Spinner/SpinnerCircleOscillate.svelte";
|
|
40
40
|
import { isTHCNotEmpty, type THC } from "../Thc/Thc.svelte";
|
|
41
41
|
import InputWrap from "./_internal/InputWrap.svelte";
|
|
42
|
+
import { isAcceptedType } from "./_internal/asset-helpers.js";
|
|
43
|
+
import {
|
|
44
|
+
extractClipboardFiles,
|
|
45
|
+
registerPasteTarget,
|
|
46
|
+
} from "./_internal/paste-target.js";
|
|
42
47
|
import type { InputWrapClassProps } from "./types.js";
|
|
43
48
|
import Button from "../Button/Button.svelte";
|
|
44
49
|
|
|
@@ -92,27 +97,6 @@
|
|
|
92
97
|
blobUrl: string;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
function is_accepted_type(allowedAccept?: string, type?: string) {
|
|
96
|
-
// on empty allow
|
|
97
|
-
if (!allowedAccept || !type) return true;
|
|
98
|
-
|
|
99
|
-
// normalize allowed (eg "images/*, video/mpg")
|
|
100
|
-
const allowed = (allowedAccept ?? "")
|
|
101
|
-
.split(",")
|
|
102
|
-
.map((v) => `${v || ""}`.trim().toLowerCase())
|
|
103
|
-
.map((v) => {
|
|
104
|
-
if (v.includes("*")) {
|
|
105
|
-
v = v.slice(0, v.indexOf("*"));
|
|
106
|
-
}
|
|
107
|
-
return v;
|
|
108
|
-
})
|
|
109
|
-
.filter(Boolean);
|
|
110
|
-
|
|
111
|
-
if (!allowed.length) return true;
|
|
112
|
-
|
|
113
|
-
return allowed.some((a) => type.toLowerCase().startsWith(a));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
100
|
export function getAssetIcon(ext?: string) {
|
|
117
101
|
const map: Record<string, CallableFunction> = {
|
|
118
102
|
archive: iconFileZip,
|
|
@@ -131,96 +115,6 @@
|
|
|
131
115
|
return map[getFileTypeLabel(ext ?? "unknown")] ?? iconFile;
|
|
132
116
|
}
|
|
133
117
|
|
|
134
|
-
// --- Clipboard paste plumbing (see the `pasteable` prop) -------------------
|
|
135
|
-
// All mounted pasteable instances coordinate through ONE document-level
|
|
136
|
-
// `paste` listener (installed while at least one is mounted). Document-level
|
|
137
|
-
// (rather than a listener on the field wrapper) because browsers dispatch
|
|
138
|
-
// `paste` at the focused/selection node — with focus on <body> the event
|
|
139
|
-
// never bubbles through the field, which made the feature look dead unless
|
|
140
|
-
// the field was clicked first.
|
|
141
|
-
type PasteTarget = {
|
|
142
|
-
el: HTMLElement;
|
|
143
|
-
handle: (e: ClipboardEvent) => void;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const paste_targets = new Set<PasteTarget>();
|
|
147
|
-
|
|
148
|
-
// True only when NOTHING is focused (browsers park focus on <body>/<html>).
|
|
149
|
-
// The fallback below must never fire while the user has deliberately focused
|
|
150
|
-
// some other element — a text input obviously, but also any other widget
|
|
151
|
-
// (e.g. a different, non-pasteable FieldAssets): pasting "into" the thing
|
|
152
|
-
// they focused must not teleport files to an unrelated field.
|
|
153
|
-
function is_unclaimed_focus(el: Element | null): boolean {
|
|
154
|
-
return !el || el === document.body || el === document.documentElement;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Text-entry elements own their pastes even when they live INSIDE the field
|
|
158
|
-
// (consumer content via the label/description/below snippets) — an editor's
|
|
159
|
-
// image paste must insert into the editor, not upload into the field.
|
|
160
|
-
function is_text_entry(el: Element | null): boolean {
|
|
161
|
-
if (!el) return false;
|
|
162
|
-
if ((el as HTMLElement).isContentEditable) return true;
|
|
163
|
-
return ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// A field hidden by CSS (kept-mounted inactive tab panel etc.) must not
|
|
167
|
-
// claim the no-focus fallback — the user would see nothing happen.
|
|
168
|
-
function is_visible(el: HTMLElement): boolean {
|
|
169
|
-
return el.checkVisibility?.() ?? el.offsetParent !== null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// A modal/dialog (drawer, modal) takes focus on open, so `active` is the
|
|
173
|
-
// dialog panel — or a control inside it — and NEVER <body>. When the field
|
|
174
|
-
// lives in such a dialog, treat a non-text focus within that SAME dialog as
|
|
175
|
-
// unclaimed too, so a bare Ctrl/Cmd-V attaches with no prior click. Scoped to
|
|
176
|
-
// a shared [aria-modal]/[role=dialog] ancestor on purpose: it must not make
|
|
177
|
-
// the field greedy on ordinary (non-modal) pages, where a deliberately
|
|
178
|
-
// focused control elsewhere still owns its paste.
|
|
179
|
-
function shares_modal(fieldEl: HTMLElement, active: Element | null): boolean {
|
|
180
|
-
if (!active) return false;
|
|
181
|
-
const modal = fieldEl.closest?.("[aria-modal='true'],[role='dialog']");
|
|
182
|
-
return !!modal && modal.contains(active);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function on_document_paste(e: ClipboardEvent) {
|
|
186
|
-
// someone (an editor, another paste handler) already claimed it
|
|
187
|
-
if (e.defaultPrevented) return;
|
|
188
|
-
const active = document.activeElement;
|
|
189
|
-
// 1) the field holding focus always wins — unless focus sits in a
|
|
190
|
-
// text-entry element nested inside it: stand down entirely
|
|
191
|
-
for (const t of paste_targets) {
|
|
192
|
-
if (t.el.contains(active)) {
|
|
193
|
-
if (is_text_entry(active)) return;
|
|
194
|
-
return t.handle(e);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// 2) fall back to the SINGLE mounted + visible pasteable field for a paste
|
|
198
|
-
// NOT owned by a focused text-entry element, when focus is EITHER unclaimed
|
|
199
|
-
// (fresh page, focus on <body>) OR parked on a non-text control inside the
|
|
200
|
-
// SAME modal/dialog as the field (the drawer/modal case: the panel or the
|
|
201
|
-
// row that opened it holds focus, so <body> is never active). A bare
|
|
202
|
-
// Ctrl/Cmd-V then works with no prior click. With several fields mounted the
|
|
203
|
-
// routing would be ambiguous, so focus (a click on the field) must decide.
|
|
204
|
-
if (paste_targets.size === 1 && !is_text_entry(active)) {
|
|
205
|
-
const [t] = paste_targets;
|
|
206
|
-
if (
|
|
207
|
-
is_visible(t.el) &&
|
|
208
|
-
(is_unclaimed_focus(active) || shares_modal(t.el, active))
|
|
209
|
-
) {
|
|
210
|
-
t.handle(e);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function register_paste_target(t: PasteTarget): () => void {
|
|
216
|
-
if (!paste_targets.size) document.addEventListener("paste", on_document_paste);
|
|
217
|
-
paste_targets.add(t);
|
|
218
|
-
return () => {
|
|
219
|
-
paste_targets.delete(t);
|
|
220
|
-
if (!paste_targets.size) document.removeEventListener("paste", on_document_paste);
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
118
|
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
225
119
|
|
|
226
120
|
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
@@ -554,7 +448,7 @@
|
|
|
554
448
|
// processAssets (which would otherwise run with an empty batch).
|
|
555
449
|
if (!incoming.length) return;
|
|
556
450
|
|
|
557
|
-
if (accept && incoming.some((f) => !
|
|
451
|
+
if (accept && incoming.some((f) => !isAcceptedType(accept, f.type, f.name))) {
|
|
558
452
|
const msg = t("invalid_type", { accept });
|
|
559
453
|
if (notifications) notifications.error(msg);
|
|
560
454
|
else alert(msg);
|
|
@@ -627,29 +521,12 @@
|
|
|
627
521
|
}
|
|
628
522
|
|
|
629
523
|
// --- Clipboard paste (opt-in via `pasteable`) -------------------------------
|
|
630
|
-
// Wired through the
|
|
631
|
-
//
|
|
524
|
+
// Wired through the document-level `paste` listener shared with FieldSingleAsset
|
|
525
|
+
// (see `_internal/paste-target.ts`): the instance registers itself while mounted, and receives the paste
|
|
632
526
|
// when it holds focus — or, as the only mounted pasteable field, when the
|
|
633
527
|
// paste is unclaimed (focus not in a text-entry element). Pasted files go
|
|
634
528
|
// through `handleIncomingFiles`, inheriting accept/cardinality validation and
|
|
635
529
|
// the processAssets upload — no synthetic `change` event.
|
|
636
|
-
function extractClipboardFiles(e: ClipboardEvent): File[] {
|
|
637
|
-
const dt = e.clipboardData;
|
|
638
|
-
if (!dt) return [];
|
|
639
|
-
const out: File[] = [];
|
|
640
|
-
// Prefer items (lets us keep only file-kind entries, e.g. a pasted
|
|
641
|
-
// screenshot); fall back to `.files` for browsers that only populate that.
|
|
642
|
-
for (let i = 0; i < (dt.items?.length ?? 0); i++) {
|
|
643
|
-
const it = dt.items[i];
|
|
644
|
-
if (it?.kind === "file") {
|
|
645
|
-
const f = it.getAsFile();
|
|
646
|
-
if (f) out.push(f);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (!out.length && dt.files?.length) out.push(...dt.files);
|
|
650
|
-
return out;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
530
|
function handlePaste(e: ClipboardEvent) {
|
|
654
531
|
if (!pasteable || typeof processAssets !== "function") return;
|
|
655
532
|
const files = extractClipboardFiles(e);
|
|
@@ -676,7 +553,7 @@
|
|
|
676
553
|
if (!pasteable || typeof processAssets !== "function" || !wrapEl) return;
|
|
677
554
|
if (disabled || isLoading) return;
|
|
678
555
|
const el = wrapEl;
|
|
679
|
-
const unregister =
|
|
556
|
+
const unregister = registerPasteTarget({ el, handle: handlePaste });
|
|
680
557
|
// clicking still focuses the field: gives the `:focus-within` ring (the
|
|
681
558
|
// visible "paste lands here" affordance) and lets THIS field win the
|
|
682
559
|
// routing when several pasteable fields are mounted
|