@dnax/ui 0.0.75 → 0.0.76
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/components/QFilePicker.vue +22 -65
- package/components/QSpreadsheet.vue +593 -65
- package/index.ts +14 -0
- package/lib/cellSize.test.ts +52 -0
- package/lib/cellSize.ts +43 -0
- package/lib/fileList.test.ts +119 -0
- package/lib/fileList.ts +159 -0
- package/lib/pagination.test.ts +77 -0
- package/lib/pagination.ts +50 -0
- package/package.json +1 -1
- package/styles/main.css +179 -1
|
@@ -6,6 +6,8 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from "vue"
|
|
|
6
6
|
import { Icon } from "@iconify/vue"
|
|
7
7
|
import { icons } from "../lib/icons"
|
|
8
8
|
import { cn } from "../lib/utils"
|
|
9
|
+
import { fileIconKind, formatFileSize, selectFiles } from "../lib/fileList"
|
|
10
|
+
import type { FileIconKind } from "../lib/fileList"
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
11
13
|
/** Fichier(s) sélectionné(s) — File (simple) ou File[] (multiple) */
|
|
@@ -100,51 +102,20 @@ const fileKey = (file: File): number => {
|
|
|
100
102
|
return id
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
// — Icône par type de fichier (
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (type.startsWith("audio/")) return icons.fileAudio
|
|
113
|
-
if (ARCHIVE_EXT.test(name) || /zip|compressed/.test(type)) return icons.fileArchive
|
|
114
|
-
if (SPREADSHEET_EXT.test(name) || type.includes("spreadsheet") || type.includes("excel")) {
|
|
115
|
-
return icons.fileSpreadsheet
|
|
116
|
-
}
|
|
117
|
-
if (CODE_EXT.test(name)) return icons.fileCode
|
|
118
|
-
if (type.startsWith("image/")) return icons.fileImage
|
|
119
|
-
return icons.fileText
|
|
105
|
+
// — Icône par type de fichier (règles partagées avec QSpreadsheet `attachment`) —
|
|
106
|
+
const ICON_BY_KIND: Record<FileIconKind, string> = {
|
|
107
|
+
image: icons.fileImage,
|
|
108
|
+
video: icons.fileVideo,
|
|
109
|
+
audio: icons.fileAudio,
|
|
110
|
+
archive: icons.fileArchive,
|
|
111
|
+
spreadsheet: icons.fileSpreadsheet,
|
|
112
|
+
code: icons.fileCode,
|
|
113
|
+
file: icons.fileText,
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
const
|
|
123
|
-
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} Go`
|
|
124
|
-
if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} Mo`
|
|
125
|
-
if (bytes >= 1024) return `${Math.round(bytes / 1024)} Ko`
|
|
126
|
-
return `${bytes} o`
|
|
127
|
-
}
|
|
116
|
+
const iconFor = (file: File): string => ICON_BY_KIND[fileIconKind(file)]
|
|
128
117
|
|
|
129
|
-
|
|
130
|
-
const acceptsFile = (file: File): boolean => {
|
|
131
|
-
const accept = props.accept.trim()
|
|
132
|
-
if (!accept) return true
|
|
133
|
-
const patterns = accept
|
|
134
|
-
.split(",")
|
|
135
|
-
.map((p) => p.trim().toLowerCase())
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
if (patterns.length === 0) return true
|
|
138
|
-
const name = file.name.toLowerCase()
|
|
139
|
-
return patterns.some((p) => {
|
|
140
|
-
if (p === "image/*") return file.type.startsWith("image/")
|
|
141
|
-
if (p === "video/*") return file.type.startsWith("video/")
|
|
142
|
-
if (p === "audio/*") return file.type.startsWith("audio/")
|
|
143
|
-
if (p.startsWith(".")) return name.endsWith(p)
|
|
144
|
-
if (p.includes("/")) return file.type.toLowerCase() === p
|
|
145
|
-
return false
|
|
146
|
-
})
|
|
147
|
-
}
|
|
118
|
+
const formatSize = formatFileSize
|
|
148
119
|
|
|
149
120
|
const formatMaxSize = () => {
|
|
150
121
|
const bytes = props.maxFileSize
|
|
@@ -176,26 +147,12 @@ const onInputChange = (e: Event) => {
|
|
|
176
147
|
|
|
177
148
|
const addFiles = (incoming: File[]) => {
|
|
178
149
|
const current = files.value
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
continue
|
|
186
|
-
}
|
|
187
|
-
if (!acceptsFile(file)) {
|
|
188
|
-
rejections.push({ file, reason: "type" })
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
valid.push(file)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const quota = props.maxFiles > 0 ? props.maxFiles : Infinity
|
|
195
|
-
const remaining = quota === Infinity ? Infinity : Math.max(0, quota - current.length)
|
|
196
|
-
const accepted = remaining === Infinity ? valid : valid.slice(0, remaining)
|
|
197
|
-
const overflow = remaining === Infinity ? [] : valid.slice(remaining)
|
|
198
|
-
for (const file of overflow) rejections.push({ file, reason: "count" })
|
|
150
|
+
const { accepted, rejected } = selectFiles(incoming, {
|
|
151
|
+
accept: props.accept,
|
|
152
|
+
maxSize: props.maxFileSize,
|
|
153
|
+
maxFiles: props.maxFiles,
|
|
154
|
+
current: current.length,
|
|
155
|
+
})
|
|
199
156
|
|
|
200
157
|
if (accepted.length > 0) {
|
|
201
158
|
const next = props.multiple ? [...current, ...accepted] : accepted[0]!
|
|
@@ -203,14 +160,14 @@ const addFiles = (incoming: File[]) => {
|
|
|
203
160
|
for (const file of accepted) emit("add", file)
|
|
204
161
|
}
|
|
205
162
|
|
|
206
|
-
if (
|
|
207
|
-
const reasons = [...new Set(
|
|
163
|
+
if (rejected.length > 0) {
|
|
164
|
+
const reasons = [...new Set(rejected.map((r) => r.reason))]
|
|
208
165
|
const parts: string[] = []
|
|
209
166
|
if (reasons.includes("size")) parts.push(`Fichier trop lourd (max ${formatMaxSize()})`)
|
|
210
167
|
if (reasons.includes("type")) parts.push("Type de fichier non accepté")
|
|
211
168
|
if (reasons.includes("count")) parts.push(`Nombre maximum : ${props.maxFiles}`)
|
|
212
169
|
internalError.value = parts.join(" · ")
|
|
213
|
-
for (const r of
|
|
170
|
+
for (const r of rejected) emit("rejected", r.file, r.reason)
|
|
214
171
|
}
|
|
215
172
|
}
|
|
216
173
|
|