@marianmeres/stuic 3.182.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/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/README.md +217 -21
- 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/index.css +204 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.js +3 -0
- package/docs/domains/actions.md +3 -3
- package/docs/domains/components.md +22 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header,
|
|
|
175
175
|
|
|
176
176
|
### Forms & Inputs
|
|
177
177
|
|
|
178
|
-
FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldTable, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
|
|
178
|
+
FieldInput, FieldMoney, FieldDate, FieldDateRange, Calendar, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldSingleAsset, FieldOptions, FieldKeyValues, FieldTable, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, FieldCountry, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, RegisterFormModal, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
|
|
179
179
|
|
|
180
180
|
### Buttons & Controls
|
|
181
181
|
|
|
@@ -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
|