@globalbrain/sefirot 4.0.0-3 → 4.1.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.
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type Component, computed, ref } from 'vue'
|
|
3
|
+
import { useTrans } from '../composables/Lang'
|
|
4
|
+
import { type Validatable } from '../composables/Validation'
|
|
5
|
+
import { formatSize } from '../support/File'
|
|
6
|
+
import SButton from './SButton.vue'
|
|
7
|
+
import SCard from './SCard.vue'
|
|
8
|
+
import SCardBlock from './SCardBlock.vue'
|
|
9
|
+
import SInputBase from './SInputBase.vue'
|
|
10
|
+
import SInputFileUploadItem from './SInputFileUploadItem.vue'
|
|
11
|
+
|
|
12
|
+
export type Size = 'mini' | 'small' | 'medium'
|
|
13
|
+
export type Color = 'neutral' | 'mute' | 'info' | 'success' | 'warning' | 'danger'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
size?: Size
|
|
17
|
+
label?: string
|
|
18
|
+
info?: string
|
|
19
|
+
note?: string
|
|
20
|
+
help?: string
|
|
21
|
+
text?: string
|
|
22
|
+
placeholder?: string
|
|
23
|
+
emptyText?: string
|
|
24
|
+
accept?: string
|
|
25
|
+
multiple?: boolean
|
|
26
|
+
checkIcon?: Component
|
|
27
|
+
checkText?: string
|
|
28
|
+
checkColor?: Color
|
|
29
|
+
value?: File[]
|
|
30
|
+
modelValue?: File[]
|
|
31
|
+
hideError?: boolean
|
|
32
|
+
validation?: Validatable
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
'update:model-value': [files: File[]]
|
|
37
|
+
'change': [files: File[]]
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const { t } = useTrans({
|
|
41
|
+
en: {
|
|
42
|
+
button_text: 'Choose File',
|
|
43
|
+
empty_text: 'No file selected',
|
|
44
|
+
selected_files: (c: number) => c === 1 ? `${c} file` : `${c} files`
|
|
45
|
+
},
|
|
46
|
+
ja: {
|
|
47
|
+
button_text: 'ファイルを選択',
|
|
48
|
+
empty_text: 'ファイルが選択されていません',
|
|
49
|
+
selected_files: (c: number) => `ファイル数: ${c}`
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const _value = computed(() => {
|
|
54
|
+
return props.modelValue !== undefined
|
|
55
|
+
? props.modelValue
|
|
56
|
+
: props.value !== undefined ? props.value : []
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const input = ref<HTMLInputElement | null>(null)
|
|
60
|
+
|
|
61
|
+
const classes = computed(() => [props.size ?? 'small'])
|
|
62
|
+
|
|
63
|
+
const totalFileCountText = computed(() => {
|
|
64
|
+
return t.selected_files(_value.value.length)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const totalFileSizeText = computed(() => {
|
|
68
|
+
return formatSize(_value.value)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function open() {
|
|
72
|
+
input.value!.click()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function onChange(e: Event) {
|
|
76
|
+
const files = Array.from((e.target as HTMLInputElement).files ?? [])
|
|
77
|
+
|
|
78
|
+
// When files are empty, that means user have "canceled" the file selection
|
|
79
|
+
// so we should do nothing in this case.
|
|
80
|
+
if (files.length === 0) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const newFiles = [..._value.value, ...files]
|
|
85
|
+
|
|
86
|
+
emit('update:model-value', newFiles)
|
|
87
|
+
emit('change', newFiles)
|
|
88
|
+
|
|
89
|
+
props.validation?.$touch()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onRemove(index: number) {
|
|
93
|
+
const files = _value.value.filter((_, i) => i !== index)
|
|
94
|
+
|
|
95
|
+
emit('update:model-value', files)
|
|
96
|
+
emit('change', files)
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<SInputBase
|
|
102
|
+
class="SInputFile"
|
|
103
|
+
:class="classes"
|
|
104
|
+
:label="label"
|
|
105
|
+
:note="note"
|
|
106
|
+
:info="info"
|
|
107
|
+
:help="help"
|
|
108
|
+
:check-icon="checkIcon"
|
|
109
|
+
:check-text="checkText"
|
|
110
|
+
:check-color="checkColor"
|
|
111
|
+
:validation="validation"
|
|
112
|
+
:hide-error="hideError"
|
|
113
|
+
>
|
|
114
|
+
<template #default="{ hasError }">
|
|
115
|
+
<input
|
|
116
|
+
ref="input"
|
|
117
|
+
class="input"
|
|
118
|
+
type="file"
|
|
119
|
+
:accept="accept"
|
|
120
|
+
multiple
|
|
121
|
+
@change="onChange"
|
|
122
|
+
>
|
|
123
|
+
<SCard :mode="hasError ? 'danger' : undefined">
|
|
124
|
+
<SCardBlock class="header">
|
|
125
|
+
<SButton
|
|
126
|
+
size="small"
|
|
127
|
+
:label="text ?? t.button_text"
|
|
128
|
+
@click="open"
|
|
129
|
+
/>
|
|
130
|
+
<p v-if="placeholder" class="placeholder">
|
|
131
|
+
{{ placeholder }}
|
|
132
|
+
</p>
|
|
133
|
+
</SCardBlock>
|
|
134
|
+
<template v-if="_value.length">
|
|
135
|
+
<SInputFileUploadItem
|
|
136
|
+
v-for="file, i in _value"
|
|
137
|
+
:key="file.name"
|
|
138
|
+
:file="file"
|
|
139
|
+
@remove="() => { onRemove(i) }"
|
|
140
|
+
/>
|
|
141
|
+
</template>
|
|
142
|
+
<template v-else>
|
|
143
|
+
<SCardBlock class="empty">
|
|
144
|
+
<p class="empty-text">
|
|
145
|
+
{{ emptyText ?? t.empty_text }}
|
|
146
|
+
</p>
|
|
147
|
+
</SCardBlock>
|
|
148
|
+
</template>
|
|
149
|
+
<SCardBlock class="footer">
|
|
150
|
+
<div class="footer-left">
|
|
151
|
+
<p class="footer-file-count">
|
|
152
|
+
{{ totalFileCountText }}
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="footer-right">
|
|
156
|
+
<p class="footer-file-size">
|
|
157
|
+
{{ totalFileSizeText }}
|
|
158
|
+
</p>
|
|
159
|
+
<div class="footer-spacer" />
|
|
160
|
+
</div>
|
|
161
|
+
</SCardBlock>
|
|
162
|
+
</SCard>
|
|
163
|
+
</template>
|
|
164
|
+
<template v-if="$slots.info" #info>
|
|
165
|
+
<slot name="info" />
|
|
166
|
+
</template>
|
|
167
|
+
</SInputBase>
|
|
168
|
+
</template>
|
|
169
|
+
|
|
170
|
+
<style lang="postcss" scoped>
|
|
171
|
+
.input {
|
|
172
|
+
display: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.header {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 12px;
|
|
179
|
+
height: 48px;
|
|
180
|
+
padding: 0 8px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.placeholder {
|
|
184
|
+
line-height: 24px;
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
color: var(--c-text-2);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.empty {
|
|
190
|
+
display: flex;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
align-items: center;
|
|
193
|
+
height: 48px;
|
|
194
|
+
padding: 0 16px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.empty-text {
|
|
198
|
+
line-height: 20px;
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
color: var(--c-text-2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.footer {
|
|
204
|
+
display: flex;
|
|
205
|
+
justify-content: space-between;
|
|
206
|
+
align-items: center;
|
|
207
|
+
height: 48px;
|
|
208
|
+
padding: 0 8px 0 16px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.footer-right {
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 8px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.footer-file-count,
|
|
218
|
+
.footer-file-size {
|
|
219
|
+
line-height: 24px;
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
color: var(--c-text-2);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.footer-spacer {
|
|
225
|
+
flex-shrink: 0;
|
|
226
|
+
width: 32px;
|
|
227
|
+
height: 32px;
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { formatSize } from '../support/File'
|
|
4
|
+
import SButton from './SButton.vue'
|
|
5
|
+
import SCardBlock from './SCardBlock.vue'
|
|
6
|
+
import IconFileText from '~icons/ph/file-text'
|
|
7
|
+
import IconTrash from '~icons/ph/trash'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
file: File
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
defineEmits<{
|
|
14
|
+
'remove': []
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const fileSize = computed(() => {
|
|
18
|
+
return formatSize(props.file)
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<SCardBlock class="SInputFileUploadItem">
|
|
24
|
+
<p class="name">
|
|
25
|
+
<IconFileText class="name-icon" />
|
|
26
|
+
<span class="name-text">{{ file.name }}</span>
|
|
27
|
+
</p>
|
|
28
|
+
<div class="size">{{ fileSize }}</div>
|
|
29
|
+
<SButton
|
|
30
|
+
size="small"
|
|
31
|
+
type="text"
|
|
32
|
+
mode="mute"
|
|
33
|
+
:icon="IconTrash"
|
|
34
|
+
@click="$emit('remove')"
|
|
35
|
+
/>
|
|
36
|
+
</SCardBlock>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<style lang="postcss" scoped>
|
|
40
|
+
.SInputFileUploadItem {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
height: 48px;
|
|
45
|
+
padding: 0 8px 0 16px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.name {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
flex-grow: 1;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
text-overflow: ellipsis;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.name-icon {
|
|
59
|
+
width: 16px;
|
|
60
|
+
height: 16px;
|
|
61
|
+
color: var(--c-text-2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.name-text {
|
|
65
|
+
line-height: 24px;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
color: var(--c-text-1);
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
text-overflow: ellipsis;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.size {
|
|
74
|
+
line-height: 24px;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
color: var(--c-text-2);
|
|
77
|
+
}
|
|
78
|
+
</style>
|
package/lib/support/File.ts
CHANGED
|
@@ -6,3 +6,19 @@ export function getExtension(file: File): string {
|
|
|
6
6
|
|
|
7
7
|
return name.slice((name.lastIndexOf('.') - 1 >>> 0) + 2)
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats the file size in bytes to a human-readable format. It also accepts
|
|
12
|
+
* array of files and returns the total size.
|
|
13
|
+
*/
|
|
14
|
+
export function formatSize(files: File | File[]): string {
|
|
15
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
|
16
|
+
files = Array.isArray(files) ? files : [files]
|
|
17
|
+
let size = files.reduce((previous, file) => previous + file.size, 0)
|
|
18
|
+
let index = 0
|
|
19
|
+
while (size >= 1024 && index < units.length) {
|
|
20
|
+
size /= 1024
|
|
21
|
+
index++
|
|
22
|
+
}
|
|
23
|
+
return `${size.toFixed(2)}${units[index]}`
|
|
24
|
+
}
|
package/package.json
CHANGED