@streamscloud/kit 0.9.14 → 0.9.16
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/dist/core/files/file-validation-localization.d.ts +12 -0
- package/dist/core/files/file-validation-localization.js +75 -0
- package/dist/core/files/file-validation-rule-sets.d.ts +8 -0
- package/dist/core/files/file-validation-rule-sets.js +24 -0
- package/dist/core/files/file-validation-rules.d.ts +20 -11
- package/dist/core/files/file-validation-rules.js +73 -16
- package/dist/core/files/file-validation-types.d.ts +9 -2
- package/dist/core/files/file-validator.d.ts +3 -3
- package/dist/core/files/file-validator.js +16 -2
- package/dist/core/files/index.d.ts +2 -1
- package/dist/core/files/index.js +1 -0
- package/dist/ui/dialog/cmp.dialog-container.svelte +5 -0
- package/dist/ui/file-uploader/cmp.file-uploader.svelte +2 -7
- package/dist/ui/file-uploader/cmp.file-uploader.svelte.d.ts +8 -5
- package/dist/ui/open-file-button/cmp.open-file-button.svelte +2 -6
- package/dist/ui/open-file-button/cmp.open-file-button.svelte.d.ts +7 -4
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class FileValidationLocalization {
|
|
2
|
+
get invalidFile(): string;
|
|
3
|
+
get unsupportedType(): string;
|
|
4
|
+
get audioOnly(): string;
|
|
5
|
+
get maxSize(): (size: string) => string;
|
|
6
|
+
get extensions(): (list: string) => string;
|
|
7
|
+
get maxDimensions(): (width: number, height: number) => string;
|
|
8
|
+
get minDimensions(): (width: number, height: number) => string;
|
|
9
|
+
get maxDuration(): (seconds: number) => string;
|
|
10
|
+
get aspectRatio(): (ratios: string) => string;
|
|
11
|
+
get maxBitrate(): (kbps: number) => string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AppLocale } from '../locale';
|
|
2
|
+
export class FileValidationLocalization {
|
|
3
|
+
get invalidFile() {
|
|
4
|
+
return loc.invalidFile[AppLocale.current];
|
|
5
|
+
}
|
|
6
|
+
get unsupportedType() {
|
|
7
|
+
return loc.unsupportedType[AppLocale.current];
|
|
8
|
+
}
|
|
9
|
+
get audioOnly() {
|
|
10
|
+
return loc.audioOnly[AppLocale.current];
|
|
11
|
+
}
|
|
12
|
+
get maxSize() {
|
|
13
|
+
return loc.maxSize[AppLocale.current];
|
|
14
|
+
}
|
|
15
|
+
get extensions() {
|
|
16
|
+
return loc.extensions[AppLocale.current];
|
|
17
|
+
}
|
|
18
|
+
get maxDimensions() {
|
|
19
|
+
return loc.maxDimensions[AppLocale.current];
|
|
20
|
+
}
|
|
21
|
+
get minDimensions() {
|
|
22
|
+
return loc.minDimensions[AppLocale.current];
|
|
23
|
+
}
|
|
24
|
+
get maxDuration() {
|
|
25
|
+
return loc.maxDuration[AppLocale.current];
|
|
26
|
+
}
|
|
27
|
+
get aspectRatio() {
|
|
28
|
+
return loc.aspectRatio[AppLocale.current];
|
|
29
|
+
}
|
|
30
|
+
get maxBitrate() {
|
|
31
|
+
return loc.maxBitrate[AppLocale.current];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const loc = {
|
|
35
|
+
invalidFile: {
|
|
36
|
+
en: 'Invalid file',
|
|
37
|
+
no: 'Ugyldig fil'
|
|
38
|
+
},
|
|
39
|
+
unsupportedType: {
|
|
40
|
+
en: 'Unsupported file type',
|
|
41
|
+
no: 'Filtypen støttes ikke'
|
|
42
|
+
},
|
|
43
|
+
audioOnly: {
|
|
44
|
+
en: 'Audio files only',
|
|
45
|
+
no: 'Kun lydfiler'
|
|
46
|
+
},
|
|
47
|
+
maxSize: {
|
|
48
|
+
en: (size) => `Maximum file size: ${size}`,
|
|
49
|
+
no: (size) => `Maks filstørrelse: ${size}`
|
|
50
|
+
},
|
|
51
|
+
extensions: {
|
|
52
|
+
en: (list) => `Allowed file types: ${list}`,
|
|
53
|
+
no: (list) => `Tillatte filtyper: ${list}`
|
|
54
|
+
},
|
|
55
|
+
maxDimensions: {
|
|
56
|
+
en: (width, height) => `Maximum dimensions: ${width}×${height}px`,
|
|
57
|
+
no: (width, height) => `Maks oppløsning: ${width}×${height}px`
|
|
58
|
+
},
|
|
59
|
+
minDimensions: {
|
|
60
|
+
en: (width, height) => `Minimum dimensions: ${width}×${height}px`,
|
|
61
|
+
no: (width, height) => `Min oppløsning: ${width}×${height}px`
|
|
62
|
+
},
|
|
63
|
+
maxDuration: {
|
|
64
|
+
en: (seconds) => `Maximum duration: ${seconds}s`,
|
|
65
|
+
no: (seconds) => `Maks varighet: ${seconds}s`
|
|
66
|
+
},
|
|
67
|
+
aspectRatio: {
|
|
68
|
+
en: (ratios) => `Allowed aspect ratio: ${ratios}`,
|
|
69
|
+
no: (ratios) => `Tillatt sideforhold: ${ratios}`
|
|
70
|
+
},
|
|
71
|
+
maxBitrate: {
|
|
72
|
+
en: (kbps) => `Maximum bitrate: ${kbps} kbps`,
|
|
73
|
+
no: (kbps) => `Maks bitrate: ${kbps} kbps`
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FileValidationRule, FileValidationRuleSets } from './file-validation-types';
|
|
2
|
+
export declare const toRuleSets: (rules: FileValidationRule[] | FileValidationRuleSets) => FileValidationRuleSets;
|
|
3
|
+
/**
|
|
4
|
+
* Union of every rule's `accept` across all sets, de-duplicated — the native picker filter /
|
|
5
|
+
* drag-over hint a containing cmp shows so a file matching any set can be selected.
|
|
6
|
+
*/
|
|
7
|
+
export declare const deriveAccept: (rules: FileValidationRule[] | FileValidationRuleSets) => string;
|
|
8
|
+
export declare const pickRuleSet: (file: File, sets: FileValidationRuleSets) => FileValidationRule[] | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { matchesAcceptedFileTypes } from './file-types';
|
|
2
|
+
const isRuleSets = (rules) => Array.isArray(rules[0]);
|
|
3
|
+
export const toRuleSets = (rules) => rules.length === 0 ? [] : isRuleSets(rules) ? rules : [rules];
|
|
4
|
+
const acceptTokens = (set) => set
|
|
5
|
+
.flatMap((rule) => (rule.accept ? rule.accept.split(',') : []))
|
|
6
|
+
.map((token) => token.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
/**
|
|
9
|
+
* Union of every rule's `accept` across all sets, de-duplicated — the native picker filter /
|
|
10
|
+
* drag-over hint a containing cmp shows so a file matching any set can be selected.
|
|
11
|
+
*/
|
|
12
|
+
export const deriveAccept = (rules) => {
|
|
13
|
+
const tokens = toRuleSets(rules).flatMap(acceptTokens);
|
|
14
|
+
return [...new Set(tokens)].join(',');
|
|
15
|
+
};
|
|
16
|
+
export const pickRuleSet = (file, sets) => {
|
|
17
|
+
for (const set of sets) {
|
|
18
|
+
const accept = acceptTokens(set).join(',');
|
|
19
|
+
if (!accept || matchesAcceptedFileTypes(file, accept)) {
|
|
20
|
+
return set;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
import type { FileValidationRule } from './file-validation-types';
|
|
2
2
|
/**
|
|
3
|
-
* Built-in rule factories. Each takes a constraint
|
|
4
|
-
*
|
|
5
|
-
* factory additionally attaches `.accept` so
|
|
6
|
-
* `OpenFileButton`) can auto-derive the native picker filter /
|
|
3
|
+
* Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
|
|
4
|
+
* rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
|
|
5
|
+
* literals — defaults follow `AppLocale`). The `Mime` factory additionally attaches `.accept` so
|
|
6
|
+
* containing UI cmps (`FileUploader`, `OpenFileButton`) can auto-derive the native picker filter /
|
|
7
|
+
* drag-over hint.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```ts
|
|
10
11
|
* const rules = [
|
|
11
|
-
* FileRules.Mime('image/*'
|
|
12
|
+
* FileRules.Mime('image/*'),
|
|
12
13
|
* FileRules.MaxSize(5 * 1024 * 1024, 'Image must be under 5 MB.')
|
|
13
14
|
* ];
|
|
14
15
|
* const results = await FileValidator.validateMany(files, rules);
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
export declare const FileRules: {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
AudioBitrate: (maxKbps: number, message?: string) => FileValidationRule;
|
|
20
|
+
AudioDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
21
|
+
AudioOnly: (message?: string) => FileValidationRule;
|
|
22
|
+
Extensions: (extensions: string[], message?: string) => FileValidationRule;
|
|
20
23
|
ImageDimensions: (max: {
|
|
21
24
|
width: number;
|
|
22
25
|
height: number;
|
|
23
|
-
}, message
|
|
26
|
+
}, message?: string) => FileValidationRule;
|
|
24
27
|
ImageMinDimensions: (min: {
|
|
25
28
|
width: number;
|
|
26
29
|
height: number;
|
|
27
|
-
}, message
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
}, message?: string) => FileValidationRule;
|
|
31
|
+
MaxSize: (bytes: number, message?: string) => FileValidationRule;
|
|
32
|
+
Mime: (accept: string, message?: string) => FileValidationRule;
|
|
33
|
+
VideoAspectRatio: (ratios: string[], message?: string, tolerancePercent?: number) => FileValidationRule;
|
|
34
|
+
VideoDimensions: (max: {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}, message?: string) => FileValidationRule;
|
|
38
|
+
VideoDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
30
39
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { StringHelper } from '../utils';
|
|
2
|
+
import { FileHelper } from './file-helper';
|
|
1
3
|
import { matchesAcceptedFileTypes } from './file-types';
|
|
4
|
+
import { FileValidationLocalization } from './file-validation-localization';
|
|
2
5
|
const readImageDimensions = (file) => new Promise((resolve, reject) => {
|
|
3
6
|
const url = URL.createObjectURL(file);
|
|
4
7
|
const img = new Image();
|
|
@@ -26,42 +29,96 @@ const readMediaDuration = (file, kind) => new Promise((resolve, reject) => {
|
|
|
26
29
|
};
|
|
27
30
|
el.src = url;
|
|
28
31
|
});
|
|
32
|
+
const readVideoDimensions = (file) => new Promise((resolve, reject) => {
|
|
33
|
+
const url = URL.createObjectURL(file);
|
|
34
|
+
const video = document.createElement('video');
|
|
35
|
+
video.preload = 'metadata';
|
|
36
|
+
video.onloadedmetadata = () => {
|
|
37
|
+
URL.revokeObjectURL(url);
|
|
38
|
+
resolve({ width: video.videoWidth, height: video.videoHeight });
|
|
39
|
+
};
|
|
40
|
+
video.onerror = () => {
|
|
41
|
+
URL.revokeObjectURL(url);
|
|
42
|
+
reject(new Error('Failed to read video dimensions'));
|
|
43
|
+
};
|
|
44
|
+
video.src = url;
|
|
45
|
+
});
|
|
46
|
+
const parseRatio = (ratio) => {
|
|
47
|
+
const [w, h] = ratio.split(':').map((part) => parseFloat(part));
|
|
48
|
+
return h ? w / h : NaN;
|
|
49
|
+
};
|
|
50
|
+
const formatExtensions = (extensions) => extensions.map((ext) => '.' + ext.replace(/^\./, '')).join(', ');
|
|
51
|
+
const messages = new FileValidationLocalization();
|
|
29
52
|
/**
|
|
30
|
-
* Built-in rule factories. Each takes a constraint
|
|
31
|
-
*
|
|
32
|
-
* factory additionally attaches `.accept` so
|
|
33
|
-
* `OpenFileButton`) can auto-derive the native picker filter /
|
|
53
|
+
* Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
|
|
54
|
+
* rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
|
|
55
|
+
* literals — defaults follow `AppLocale`). The `Mime` factory additionally attaches `.accept` so
|
|
56
|
+
* containing UI cmps (`FileUploader`, `OpenFileButton`) can auto-derive the native picker filter /
|
|
57
|
+
* drag-over hint.
|
|
34
58
|
*
|
|
35
59
|
* @example
|
|
36
60
|
* ```ts
|
|
37
61
|
* const rules = [
|
|
38
|
-
* FileRules.Mime('image/*'
|
|
62
|
+
* FileRules.Mime('image/*'),
|
|
39
63
|
* FileRules.MaxSize(5 * 1024 * 1024, 'Image must be under 5 MB.')
|
|
40
64
|
* ];
|
|
41
65
|
* const results = await FileValidator.validateMany(files, rules);
|
|
42
66
|
* ```
|
|
43
67
|
*/
|
|
44
68
|
export const FileRules = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
AudioBitrate: (maxKbps, message) => async (file) => {
|
|
70
|
+
const duration = await readMediaDuration(file, 'audio');
|
|
71
|
+
if (!duration) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const kbps = (file.size * 8) / duration / 1000;
|
|
75
|
+
return kbps > maxKbps ? (message ?? messages.maxBitrate(maxKbps)) : null;
|
|
76
|
+
},
|
|
77
|
+
AudioDuration: (maxSeconds, message) => async (file) => {
|
|
78
|
+
const duration = await readMediaDuration(file, 'audio');
|
|
79
|
+
return duration > maxSeconds ? (message ?? messages.maxDuration(maxSeconds)) : null;
|
|
80
|
+
},
|
|
81
|
+
AudioOnly: (message) => async (file) => {
|
|
82
|
+
const { width, height } = await readVideoDimensions(file);
|
|
83
|
+
return width > 0 || height > 0 ? (message ?? messages.audioOnly) : null;
|
|
84
|
+
},
|
|
85
|
+
Extensions: (extensions, message) => (file) => {
|
|
86
|
+
const extension = FileHelper.getFileExtension(file.name).toLowerCase();
|
|
87
|
+
const allowed = extensions.map((ext) => ext.replace(/^\./, '').toLowerCase());
|
|
88
|
+
return allowed.includes(extension) ? null : (message ?? messages.extensions(formatExtensions(extensions)));
|
|
50
89
|
},
|
|
51
90
|
ImageDimensions: (max, message) => async (file) => {
|
|
52
91
|
const { width, height } = await readImageDimensions(file);
|
|
53
|
-
return width > max.width || height > max.height ? message : null;
|
|
92
|
+
return width > max.width || height > max.height ? (message ?? messages.maxDimensions(max.width, max.height)) : null;
|
|
54
93
|
},
|
|
55
94
|
ImageMinDimensions: (min, message) => async (file) => {
|
|
56
95
|
const { width, height } = await readImageDimensions(file);
|
|
57
|
-
return width < min.width || height < min.height ? message : null;
|
|
96
|
+
return width < min.width || height < min.height ? (message ?? messages.minDimensions(min.width, min.height)) : null;
|
|
58
97
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
MaxSize: (bytes, message) => (file) => file.size > bytes ? (message ?? messages.maxSize(StringHelper.toFileSizeString(bytes))) : null,
|
|
99
|
+
Mime: (accept, message) => {
|
|
100
|
+
const rule = (file) => (matchesAcceptedFileTypes(file, accept) ? null : (message ?? messages.unsupportedType));
|
|
101
|
+
rule.accept = accept;
|
|
102
|
+
return rule;
|
|
103
|
+
},
|
|
104
|
+
VideoAspectRatio: (ratios, message, tolerancePercent = 1) => async (file) => {
|
|
105
|
+
const { width, height } = await readVideoDimensions(file);
|
|
106
|
+
if (!width || !height) {
|
|
107
|
+
return message ?? messages.aspectRatio(ratios.join(', '));
|
|
108
|
+
}
|
|
109
|
+
const actual = width / height;
|
|
110
|
+
const matches = ratios.some((ratio) => {
|
|
111
|
+
const target = parseRatio(ratio);
|
|
112
|
+
return Number.isFinite(target) && Math.abs((actual / target) * 100 - 100) <= tolerancePercent;
|
|
113
|
+
});
|
|
114
|
+
return matches ? null : (message ?? messages.aspectRatio(ratios.join(', ')));
|
|
115
|
+
},
|
|
116
|
+
VideoDimensions: (max, message) => async (file) => {
|
|
117
|
+
const { width, height } = await readVideoDimensions(file);
|
|
118
|
+
return width > max.width || height > max.height ? (message ?? messages.maxDimensions(max.width, max.height)) : null;
|
|
62
119
|
},
|
|
63
120
|
VideoDuration: (maxSeconds, message) => async (file) => {
|
|
64
121
|
const duration = await readMediaDuration(file, 'video');
|
|
65
|
-
return duration > maxSeconds ? message : null;
|
|
122
|
+
return duration > maxSeconds ? (message ?? messages.maxDuration(maxSeconds)) : null;
|
|
66
123
|
}
|
|
67
124
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single async validation rule. Returns `null` (or empty array) when the file is valid,
|
|
3
|
-
* a string (or array of strings) when invalid. The
|
|
4
|
-
* when
|
|
3
|
+
* a string (or array of strings) when invalid. The message is supplied by the rule factory;
|
|
4
|
+
* when omitted it falls back to a localized default (`FileValidationLocalization`).
|
|
5
5
|
*
|
|
6
6
|
* Optional `accept` property: when set (only the `FileRules.Mime` factory does this), the
|
|
7
7
|
* containing UI cmps (`FileUploader`, `OpenFileButton`) auto-derive the native picker filter
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
export type FileValidationRule = ((file: File) => string | string[] | null | Promise<string | string[] | null>) & {
|
|
11
11
|
accept?: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* One or more rule sets. A flat `FileValidationRule[]` is a single set applied to every file.
|
|
15
|
+
* A `FileValidationRule[][]` is several sets: per file, the first set whose `accept` matches
|
|
16
|
+
* (a set with no `accept` rule matches anything) is applied — lets one picker accept different
|
|
17
|
+
* file types with different rules. A file matching no set is rejected as an unsupported type.
|
|
18
|
+
*/
|
|
19
|
+
export type FileValidationRuleSets = FileValidationRule[][];
|
|
13
20
|
/** Aggregate validation result for a single file across one or more rules. */
|
|
14
21
|
export type FileValidationResult = {
|
|
15
22
|
file: File;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { FileValidationResult, FileValidationRule } from './file-validation-types';
|
|
1
|
+
import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from './file-validation-types';
|
|
2
2
|
export declare class FileValidator {
|
|
3
|
-
static validate(file: File, rules: FileValidationRule[]): Promise<FileValidationResult>;
|
|
4
|
-
static validateMany(files: File[], rules: FileValidationRule[]): Promise<FileValidationResult[]>;
|
|
3
|
+
static validate(file: File, rules: FileValidationRule[] | FileValidationRuleSets): Promise<FileValidationResult>;
|
|
4
|
+
static validateMany(files: File[], rules: FileValidationRule[] | FileValidationRuleSets): Promise<FileValidationResult[]>;
|
|
5
5
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import { FileValidationLocalization } from './file-validation-localization';
|
|
2
|
+
import { pickRuleSet, toRuleSets } from './file-validation-rule-sets';
|
|
1
3
|
const toArray = (v) => (v === null ? [] : Array.isArray(v) ? v : [v]);
|
|
4
|
+
const messages = new FileValidationLocalization();
|
|
2
5
|
export class FileValidator {
|
|
3
6
|
static async validate(file, rules) {
|
|
7
|
+
const sets = toRuleSets(rules);
|
|
8
|
+
const set = sets.length > 1 ? pickRuleSet(file, sets) : (sets[0] ?? []);
|
|
9
|
+
if (set === null) {
|
|
10
|
+
return { file, isValid: false, errors: [messages.unsupportedType] };
|
|
11
|
+
}
|
|
4
12
|
const errors = [];
|
|
5
|
-
for (const rule of
|
|
6
|
-
|
|
13
|
+
for (const rule of set) {
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await rule(file);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { file, isValid: false, errors: [messages.invalidFile] };
|
|
20
|
+
}
|
|
7
21
|
const msgs = toArray(result);
|
|
8
22
|
if (msgs.length > 0) {
|
|
9
23
|
errors.push(...msgs);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
|
|
2
|
-
export type { FileValidationResult, FileValidationRule } from './file-validation-types';
|
|
2
|
+
export type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from './file-validation-types';
|
|
3
3
|
export type { BlobKind, BlobKinds, BlobUpload, BlobUploadStrategy, UploadingFile, UploadingFileStatus } from './upload-types';
|
|
4
4
|
export type { UploadMediaStoreOptions } from './upload-media-store.svelte';
|
|
5
5
|
export { toBlobWithName, toFileWithUploadUrl } from './types';
|
|
@@ -8,6 +8,7 @@ export { openFile } from './open-file';
|
|
|
8
8
|
export { FileHelper } from './file-helper';
|
|
9
9
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
10
10
|
export { AcceptFileType, matchesAcceptedFileTypes } from './file-types';
|
|
11
|
+
export { deriveAccept } from './file-validation-rule-sets';
|
|
11
12
|
export { resizeBlob, resizeImage } from './image-resizer';
|
|
12
13
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
13
14
|
export { FilesProvider } from './files-provider';
|
package/dist/core/files/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { openFile } from './open-file';
|
|
|
5
5
|
export { FileHelper } from './file-helper';
|
|
6
6
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
7
7
|
export { AcceptFileType, matchesAcceptedFileTypes } from './file-types';
|
|
8
|
+
export { deriveAccept } from './file-validation-rule-sets';
|
|
8
9
|
export { resizeBlob, resizeImage } from './image-resizer';
|
|
9
10
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
10
11
|
export { FilesProvider } from './files-provider';
|
|
@@ -4,6 +4,7 @@ import { untrack } from 'svelte';
|
|
|
4
4
|
let { dialog } = $props();
|
|
5
5
|
let dialogElement = $state();
|
|
6
6
|
const isActive = $derived(Dialogs.active(dialog.containerId)?.id === dialog.id);
|
|
7
|
+
const isStacked = $derived(Dialogs.dialogs(dialog.containerId).findIndex((d) => d.id === dialog.id) > 0);
|
|
7
8
|
const canDismiss = $derived(!dialog.controller.settings.nonCancelable && dialog.controller.settings.closeOnEsc);
|
|
8
9
|
const DialogView = $derived(dialog.view);
|
|
9
10
|
// TEMP: Workaround for Chromium bug where preventDefault() on cancel event
|
|
@@ -65,6 +66,7 @@ const isStretchContent = $derived(position === 'full-screen' || position === 'fu
|
|
|
65
66
|
bind:this={dialogElement}
|
|
66
67
|
class="dialog-container"
|
|
67
68
|
class:dialog-container--inactive={!isActive}
|
|
69
|
+
class:dialog-container--stacked={isStacked}
|
|
68
70
|
class:dialog-container--size-small={size === 'small'}
|
|
69
71
|
class:dialog-container--size-medium={size === 'medium'}
|
|
70
72
|
class:dialog-container--size-default={size === 'default'}
|
|
@@ -150,6 +152,9 @@ DialogContainer — wraps the native `<dialog>` element with sizing, positioning
|
|
|
150
152
|
background: var(--_dialog-container--backdrop);
|
|
151
153
|
animation: dialog-container-backdrop-fade-in var(--sc-kit--duration--slow) var(--sc-kit--ease--out) both;
|
|
152
154
|
}
|
|
155
|
+
.dialog-container--stacked::backdrop {
|
|
156
|
+
animation: none;
|
|
157
|
+
}
|
|
153
158
|
.dialog-container--inactive::backdrop {
|
|
154
159
|
background: transparent;
|
|
155
160
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
<script lang="ts">import { FileValidator } from '../../core/files';
|
|
1
|
+
<script lang="ts">import { deriveAccept, FileValidator } from '../../core/files';
|
|
2
2
|
import { Icon } from '../icon';
|
|
3
3
|
import IconArrowUpload from '@fluentui/svg-icons/icons/arrow_upload_24_regular.svg?raw';
|
|
4
4
|
const { multiple = false, validationRules, disabled = false, title = '', description = '', on } = $props();
|
|
5
|
-
|
|
6
|
-
// string. Single source of truth — consumer writes the MIME pattern once on the rule.
|
|
7
|
-
const accept = $derived(validationRules
|
|
8
|
-
?.map((r) => r.accept)
|
|
9
|
-
.filter((a) => !!a)
|
|
10
|
-
.join(',') ?? '');
|
|
5
|
+
const accept = $derived(deriveAccept(validationRules ?? []));
|
|
11
6
|
let inputRef = $state.raw(undefined);
|
|
12
7
|
let dragOver = $state(false);
|
|
13
8
|
// Snapshot of MIME types captured at dragenter — DataTransfer.files is empty during drag
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import type { FileValidationResult, FileValidationRule } from '../../core/files';
|
|
1
|
+
import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
|
|
2
2
|
type Props = {
|
|
3
3
|
/** Allow multi-file selection. @default false */
|
|
4
4
|
multiple?: boolean;
|
|
5
5
|
/**
|
|
6
|
-
* Validation rules applied to each picked / dropped file
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Validation rules applied to each picked / dropped file — a single set
|
|
7
|
+
* (`FileValidationRule[]`) or several sets (`FileValidationRule[][]`). With several sets,
|
|
8
|
+
* each file is validated by the first set whose `accept` matches it (a set with no `accept`
|
|
9
|
+
* rule matches anything; a file matching none is rejected). The native picker filter and
|
|
10
|
+
* drag-over visual hint are auto-derived from every set's `FileRules.Mime(...)` rule's
|
|
11
|
+
* `accept` — pass it once via the rule, the cmp wires the rest.
|
|
9
12
|
*/
|
|
10
|
-
validationRules?: FileValidationRule[];
|
|
13
|
+
validationRules?: FileValidationRule[] | FileValidationRuleSets;
|
|
11
14
|
disabled?: boolean;
|
|
12
15
|
/** Headline shown inside the drop zone. */
|
|
13
16
|
title?: string;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
<script lang="ts">import { FileValidator, openFile } from '../../core/files';
|
|
1
|
+
<script lang="ts">import { deriveAccept, FileValidator, openFile } from '../../core/files';
|
|
2
2
|
import { Button } from '../button';
|
|
3
3
|
const { size = 'md', variant = 'secondary', disabled = false, multiple = false, capture, validationRules, icon, iconPosition = 'leading', children, 'aria-label': ariaLabel, on } = $props();
|
|
4
|
-
|
|
5
|
-
const accept = $derived(validationRules
|
|
6
|
-
?.map((r) => r.accept)
|
|
7
|
-
.filter((a) => !!a)
|
|
8
|
-
.join(',') ?? '');
|
|
4
|
+
const accept = $derived(deriveAccept(validationRules ?? []));
|
|
9
5
|
const handleClick = async () => {
|
|
10
6
|
const files = await openFile({ multiple, accept, capture });
|
|
11
7
|
if (files.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileValidationResult, FileValidationRule } from '../../core/files';
|
|
1
|
+
import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
|
|
2
2
|
import type { ButtonSize, ButtonVariant } from '../button';
|
|
3
3
|
import type { IconProp } from '../icon';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
@@ -12,10 +12,13 @@ type Props = {
|
|
|
12
12
|
/** Optional file-picker capture hint (mobile camera / mic). */
|
|
13
13
|
capture?: 'user' | 'environment';
|
|
14
14
|
/**
|
|
15
|
-
* Rules applied to picked files
|
|
16
|
-
*
|
|
15
|
+
* Rules applied to picked files — a single set (`FileValidationRule[]`) or several sets
|
|
16
|
+
* (`FileValidationRule[][]`). With several sets, each file is validated by the first set
|
|
17
|
+
* whose `accept` matches it (a set with no `accept` rule matches anything; a file matching
|
|
18
|
+
* none is rejected). The native picker `accept` filter is auto-derived from every set's
|
|
19
|
+
* `FileRules.Mime(...)` rule — pass the MIME pattern once via the rule.
|
|
17
20
|
*/
|
|
18
|
-
validationRules?: FileValidationRule[];
|
|
21
|
+
validationRules?: FileValidationRule[] | FileValidationRuleSets;
|
|
19
22
|
/** Icon — forwarded to the underlying Button. */
|
|
20
23
|
icon?: IconProp;
|
|
21
24
|
/** Icon position. @default 'leading' */
|