@v1nt1248/3nclient-lib 0.1.47 → 0.1.49

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.
@@ -12,3 +12,4 @@ export declare function html2text(html: string): string;
12
12
  */
13
13
  export declare function prepareDateAsSting(ts: number): string;
14
14
  export declare function capitalize(text: string): string;
15
+ export declare function formatFileSize(size: number): string;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@v1nt1248/3nclient-lib",
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "v1nt1248",
5
- "version": "0.1.47",
5
+ "version": "0.1.49",
6
6
  "description": "Library for 3NWeb clients",
7
7
  "type": "module",
8
8
  "files": [
@@ -11,6 +11,8 @@ import Ui3nEmoji from './ui3n-emoji/ui3n-emoji.vue';
11
11
  import type { Ui3nEmojiProps, Ui3nEmojiEmits } from './ui3n-emoji/types';
12
12
  import Ui3nInput from './ui3n-input/ui3n-input.vue';
13
13
  import type { Ui3nInputProps, Ui3nInputEmits } from './ui3n-input/types';
14
+ import Ui3nInputFile from './ui3n-input-file/ui3n-input-file.vue';
15
+ import type { Ui3nInputFileProps, Ui3nInputFileEmits } from './ui3n-input-file/types';
14
16
  import Ui3nText from './ui3n-text/ui3n-text.vue';
15
17
  import type { Ui3nTextProps, Ui3nTextEmits } from './ui3n-text/types';
16
18
  import Ui3nCheckbox from './ui3n-checkbox/ui3n-checkbox.vue';
@@ -101,6 +103,9 @@ export {
101
103
  Ui3nInput,
102
104
  Ui3nInputProps,
103
105
  Ui3nInputEmits,
106
+ Ui3nInputFile,
107
+ Ui3nInputFileProps,
108
+ Ui3nInputFileEmits,
104
109
  Ui3nText,
105
110
  Ui3nTextProps,
106
111
  Ui3nTextEmits,
@@ -0,0 +1,14 @@
1
+ export interface Ui3nInputFileProps {
2
+ modelValue?: File[] | FileList;
3
+ buttonText?: string;
4
+ multiple?: boolean;
5
+ maxNumberOfFiles?: number | string;
6
+ maxFileSize?: number;
7
+ allowedFileTypes?: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface Ui3nInputFileEmits {
12
+ (event: 'update:modelValue', value: File[] | FileList): void;
13
+ (event: 'error', value: string): void;
14
+ }
@@ -0,0 +1,167 @@
1
+ <template>
2
+ <div :class="$style.ui3nInputFile">
3
+ <label
4
+ :for="id"
5
+ :class="[$style.label, disabled && $style.disabled]"
6
+ @click.stop.prevent="selectFiles"
7
+ >
8
+ {{ buttonText }}
9
+ </label>
10
+
11
+ <input
12
+ :id="id"
13
+ ref="fileInput"
14
+ type="file"
15
+ name="file"
16
+ hidden
17
+ :multiple="multiple"
18
+ :accept="allowedFileTypes"
19
+ :disabled="disabled"
20
+ @change="onChange"
21
+ />
22
+ </div>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ import { ref } from 'vue';
27
+ import size from 'lodash/size';
28
+ import last from 'lodash/last';
29
+ import trim from 'lodash/trim';
30
+ import { getRandomId, formatFileSize } from '@/utils';
31
+ import type { Ui3nInputFileProps, Ui3nInputFileEmits } from './types';
32
+ import type { Nullable } from '@/components/types';
33
+
34
+ const props = withDefaults(defineProps<Ui3nInputFileProps>(), {
35
+ modelValue: () => [],
36
+ buttonText: 'Select file',
37
+ allowedFileTypes: '*',
38
+ });
39
+ const emits = defineEmits<Ui3nInputFileEmits>();
40
+
41
+ const id = getRandomId(4);
42
+ const fileInput = ref<Nullable<HTMLInputElement>>(null);
43
+
44
+ function selectFiles(ev: MouseEvent) {
45
+ if (props.disabled) {
46
+ return;
47
+ }
48
+
49
+ ev.stopImmediatePropagation();
50
+ fileInput.value!.click();
51
+ }
52
+
53
+ function validateFileSize(file: File) {
54
+ if (!file) {
55
+ return false;
56
+ }
57
+
58
+ if (!props.maxFileSize) {
59
+ return true;
60
+ }
61
+
62
+ const { name, size } = file;
63
+ if (size > props.maxFileSize) {
64
+ emits('error', `The [${name}] file is too big. Maximum file size ${formatFileSize(props.maxFileSize)}`);
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ function validateFileType(file: File) {
71
+ if (!file) {
72
+ return false;
73
+ }
74
+
75
+ const { name } = file;
76
+ const fileExt = `.${last(name.split('.'))}`.toLowerCase();
77
+ const allowedFileTypes = props.allowedFileTypes.split(',').map(type => trim(type).toLowerCase());
78
+
79
+ const isValid = allowedFileTypes.includes('*') || allowedFileTypes.includes(fileExt);
80
+ if (!isValid) {
81
+ emits('error', `The [${name}] file type is not valid.`);
82
+ }
83
+ return isValid;
84
+ }
85
+
86
+ function validateDuplicateFile(file: File) {
87
+ if (!file) {
88
+ return false;
89
+ }
90
+
91
+ const { name } = file;
92
+ const isFoundDuplicate = [...props.modelValue].find(file => file.name === name);
93
+
94
+ if (isFoundDuplicate) {
95
+ emits('error', `The [${name}] file has already been downloaded.`);
96
+ }
97
+
98
+ return !isFoundDuplicate;
99
+ }
100
+
101
+ function validateCount(files: File[] = []) {
102
+ if (!props.maxNumberOfFiles) {
103
+ return true;
104
+ }
105
+
106
+ const total = files.reduce((acc, file) => {
107
+ const { size } = file;
108
+ if (size) {
109
+ acc += size;
110
+ }
111
+ return acc;
112
+ }, 0);
113
+
114
+ const isValid = total <= Number(props.maxNumberOfFiles);
115
+ if (!isValid) {
116
+ emits('error', `You cannot select more than ${props.maxNumberOfFiles} files.`);
117
+ }
118
+ return isValid;
119
+ }
120
+
121
+ function processFiles(files: File[]) {
122
+ if (size(files)) {
123
+ if (!validateCount(files)) {
124
+ return;
125
+ }
126
+
127
+ const inputValue = files
128
+ .filter(f => validateDuplicateFile(f))
129
+ .filter(f => validateFileSize(f))
130
+ .filter(f => validateFileType(f));
131
+
132
+ if (size(inputValue) > 0) {
133
+ emits('update:modelValue', [...props.modelValue, ...inputValue]);
134
+ }
135
+ }
136
+ }
137
+
138
+ function onChange() {
139
+ processFiles([...fileInput.value!.files!]);
140
+ fileInput.value!.value = '';
141
+ }
142
+ </script>
143
+
144
+ <style lang="scss" module>
145
+ .ui3nInputFile {
146
+ position: relative;
147
+ width: max-content;
148
+ }
149
+
150
+ .label {
151
+ font-size: var(--font-12);
152
+ font-weight: 500;
153
+ line-height: var(--font-16);
154
+ color: var(--color-text-button-secondary-default);
155
+ padding: 0 var(--space-xs);
156
+ cursor: pointer;
157
+
158
+ &:hover {
159
+ color: var(--color-text-button-secondary-hover);
160
+ }
161
+
162
+ &.disabled {
163
+ cursor: default;
164
+ opacity: 0.5;
165
+ }
166
+ }
167
+ </style>