webcake-ui-kit 1.2.16 → 1.2.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-ui-kit",
3
- "version": "1.2.16",
3
+ "version": "1.2.17",
4
4
  "description": "UI Kit for Vue 2 && 3 - Pure Options API",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -66,6 +66,7 @@
66
66
  "./WkToggleGroup": "./src/components/toggle-group/ToggleGroup.vue",
67
67
  "./WkTooltip": "./src/components/tooltip/Tooltip.vue",
68
68
  "./WkTypography": "./src/components/typography/Typography.vue",
69
+ "./WkUpload": "./src/components/upload/Upload.vue",
69
70
  "./components/*": "./src/components/*",
70
71
  "./icons": "./src/icons/index.js",
71
72
  "./package.json": "./package.json"
@@ -0,0 +1,534 @@
1
+ <template>
2
+ <div
3
+ class="ui-upload"
4
+ :class="[
5
+ `ui-upload--${listType}`,
6
+ {
7
+ 'ui-upload--disabled': disabled,
8
+ 'ui-upload--drag-over': dragOver
9
+ }
10
+ ]"
11
+ >
12
+ <!-- Dropzone trigger (default / picture list types) -->
13
+ <div
14
+ v-if="!isCard"
15
+ class="ui-upload__dropzone"
16
+ :class="{ 'ui-upload__dropzone--over': dragOver, 'ui-upload__dropzone--disabled': disabled }"
17
+ role="button"
18
+ :tabindex="disabled ? -1 : 0"
19
+ :aria-disabled="disabled ? 'true' : null"
20
+ @click="openFileDialog"
21
+ @keydown.enter.prevent="openFileDialog"
22
+ @keydown.space.prevent="openFileDialog"
23
+ @dragover.prevent="onDragOver"
24
+ @dragleave.prevent="onDragLeave"
25
+ @drop.prevent="onDrop"
26
+ @paste="onPaste"
27
+ >
28
+ <slot name="trigger">
29
+ <div class="ui-upload__dropzone-inner">
30
+ <span class="ui-upload__feature-icon">
31
+ <WkiUpload :size="20" />
32
+ </span>
33
+ <div class="ui-upload__dropzone-text">
34
+ <p class="ui-upload__dropzone-title">
35
+ {{ title }} <span class="ui-upload__link">{{ browseText }}</span>
36
+ </p>
37
+ <p class="ui-upload__dropzone-hint">{{ hint }}</p>
38
+ </div>
39
+ </div>
40
+ </slot>
41
+ </div>
42
+
43
+ <!-- File list (rows for default/picture, grid of tiles for picture-card/circle) -->
44
+ <UploadList
45
+ :items="visibleFileList"
46
+ :list-type="listType"
47
+ :show-preview-icon="showFlags.showPreviewIcon"
48
+ :show-remove-icon="showFlags.showRemoveIcon"
49
+ :show-download-icon="showFlags.showDownloadIcon"
50
+ :hide-list="showUploadList === false"
51
+ >
52
+ <template v-if="isCard" #trigger>
53
+ <div
54
+ v-if="showCardTrigger"
55
+ class="ui-upload__card-trigger"
56
+ :class="{ 'ui-upload__card-trigger--over': dragOver, 'ui-upload__card-trigger--disabled': disabled }"
57
+ role="button"
58
+ :tabindex="disabled ? -1 : 0"
59
+ :aria-disabled="disabled ? 'true' : null"
60
+ @click="openFileDialog"
61
+ @keydown.enter.prevent="openFileDialog"
62
+ @keydown.space.prevent="openFileDialog"
63
+ @dragover.prevent="onDragOver"
64
+ @dragleave.prevent="onDragLeave"
65
+ @drop.prevent="onDrop"
66
+ @paste="onPaste"
67
+ >
68
+ <slot name="trigger">
69
+ <WkiPlus :size="20" />
70
+ <span v-if="browseText" class="ui-upload__card-trigger-text">{{ browseText }}</span>
71
+ </slot>
72
+ </div>
73
+ </template>
74
+ </UploadList>
75
+
76
+ <input
77
+ ref="input"
78
+ type="file"
79
+ class="ui-upload__input"
80
+ :accept="accept"
81
+ :multiple="multiple"
82
+ :webkitdirectory="directory ? '' : null"
83
+ :directory="directory ? '' : null"
84
+ @change="onInputChange"
85
+ />
86
+
87
+ <Dialog
88
+ v-if="previewVisible"
89
+ :open="previewVisible"
90
+ centered
91
+ :title="previewTitle"
92
+ :footer="false"
93
+ :width="520"
94
+ @change="onPreviewChange"
95
+ >
96
+ <div class="ui-upload__preview">
97
+ <img :src="previewImageSrc" :alt="previewTitle" class="ui-upload__preview-img" />
98
+ </div>
99
+ </Dialog>
100
+ </div>
101
+ </template>
102
+
103
+ <script>
104
+ import Dialog from '../dialog/Dialog.vue'
105
+ import UploadList from './UploadList.vue'
106
+ import WkiUpload from '../../icons/Upload.vue'
107
+ import WkiPlus from '../../icons/Plus.vue'
108
+ import {
109
+ LIST_IGNORE,
110
+ attrAccept,
111
+ defaultRequest,
112
+ file2Obj,
113
+ genUid,
114
+ getFileItem,
115
+ isImageFileType,
116
+ isImageUrl,
117
+ previewImage,
118
+ removeFileItem,
119
+ updateFileList
120
+ } from './upload-utils.js'
121
+
122
+ export default {
123
+ name: 'Upload',
124
+
125
+ components: { Dialog, UploadList, WkiUpload, WkiPlus },
126
+
127
+ provide() {
128
+ return { wkUpload: this }
129
+ },
130
+
131
+ props: {
132
+ // --- request config ---
133
+ action: { type: [String, Function], default: '' },
134
+ method: { type: String, default: 'post' },
135
+ headers: { type: Object, default: () => ({}) },
136
+ data: { type: [Object, Function], default: () => ({}) },
137
+ name: { type: String, default: 'file' },
138
+ withCredentials: { type: Boolean, default: false },
139
+
140
+ // --- data (controlled + uncontrolled) ---
141
+ fileList: { type: Array, default: null },
142
+ defaultFileList: { type: Array, default: () => [] },
143
+
144
+ // --- selection behavior ---
145
+ multiple: { type: Boolean, default: false },
146
+ maxCount: { type: Number, default: 0 },
147
+ maxSize: { type: Number, default: 0 },
148
+ accept: { type: String, default: '' },
149
+ disabled: { type: Boolean, default: false },
150
+ directory: { type: Boolean, default: false },
151
+ pastable: { type: Boolean, default: false },
152
+
153
+ // --- presentation ---
154
+ listType: {
155
+ type: String,
156
+ default: 'default',
157
+ validator: v => ['default', 'picture', 'picture-card', 'picture-circle'].includes(v)
158
+ },
159
+ showUploadList: { type: [Boolean, Object], default: true },
160
+
161
+ // --- trigger copy (dropzone / add-tile) ---
162
+ title: { type: String, default: 'Drop files here or' },
163
+ browseText: { type: String, default: 'browse files' },
164
+ hint: { type: String, default: 'SVG, PNG, JPG or GIF' },
165
+
166
+ // --- logic hooks (function props) ---
167
+ beforeUpload: { type: Function, default: null },
168
+ transformFile: { type: Function, default: null },
169
+ customRequest: { type: Function, default: null },
170
+ beforeRemove: { type: Function, default: null },
171
+ previewFile: { type: Function, default: null },
172
+ isImageUrl: { type: Function, default: null }
173
+ },
174
+
175
+ emits: ['change', 'preview', 'remove', 'download', 'progress', 'drop', 'reject', 'update:fileList'],
176
+
177
+ data() {
178
+ // NB: stateFileList is populated in created(), not here — Vue 3 does not guarantee
179
+ // methods (normalize/ensureUid) are bound during data(), unlike Vue 2.
180
+ return {
181
+ stateFileList: [],
182
+ dragOver: false,
183
+ previewVisible: false,
184
+ previewImageSrc: '',
185
+ previewTitle: ''
186
+ }
187
+ },
188
+
189
+ computed: {
190
+ isCard() {
191
+ return this.listType === 'picture-card' || this.listType === 'picture-circle'
192
+ },
193
+ visibleFileList() {
194
+ return this.stateFileList.filter(f => f.status !== 'removed')
195
+ },
196
+ showCardTrigger() {
197
+ if (this.disabled) return false
198
+ if (this.maxCount && this.visibleFileList.length >= this.maxCount) return false
199
+ // Single-file card (maxCount 1 or !multiple) hides the add-tile once a file exists —
200
+ // the existing tile itself is the replace surface.
201
+ if (!this.multiple && this.visibleFileList.length >= 1) return false
202
+ return true
203
+ },
204
+ showFlags() {
205
+ const s = this.showUploadList
206
+ if (s && typeof s === 'object') {
207
+ return {
208
+ showPreviewIcon: s.showPreviewIcon !== false,
209
+ showRemoveIcon: s.showRemoveIcon !== false,
210
+ showDownloadIcon: s.showDownloadIcon === true
211
+ }
212
+ }
213
+ return { showPreviewIcon: true, showRemoveIcon: true, showDownloadIcon: false }
214
+ }
215
+ },
216
+
217
+ watch: {
218
+ fileList(val) {
219
+ // Controlled mode: parent is the source of truth — re-sync our working copy.
220
+ if (val != null) this.stateFileList = this.normalize(val)
221
+ }
222
+ },
223
+
224
+ created() {
225
+ this.reqs = {}
226
+ this._uidCache = typeof WeakMap !== 'undefined' ? new WeakMap() : null
227
+ this.stateFileList = this.normalize(this.fileList != null ? this.fileList : this.defaultFileList)
228
+ },
229
+
230
+ // Declare both teardown hooks: Vue 2.7 fires beforeDestroy, Vue 3.4 fires beforeUnmount.
231
+ beforeUnmount() {
232
+ this.abortAll()
233
+ },
234
+ // eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle, vue/no-dupe-keys
235
+ beforeDestroy() {
236
+ this.abortAll()
237
+ },
238
+
239
+ methods: {
240
+ // ---- normalization -------------------------------------------------
241
+ ensureUid(item) {
242
+ if (!this._uidCache) return genUid()
243
+ if (this._uidCache.has(item)) return this._uidCache.get(item)
244
+ const id = genUid()
245
+ this._uidCache.set(item, id)
246
+ return id
247
+ },
248
+ normalize(list) {
249
+ const self = this
250
+ return (list || []).map(item => ({
251
+ uid: item.uid != null ? item.uid : self.ensureUid(item),
252
+ name: item.name,
253
+ size: item.size,
254
+ type: item.type,
255
+ percent: item.percent != null ? item.percent : item.status === 'uploading' ? 0 : 100,
256
+ status: item.status || 'done',
257
+ url: item.url,
258
+ thumbUrl: item.thumbUrl,
259
+ originFileObj: item.originFileObj,
260
+ response: item.response,
261
+ error: item.error,
262
+ loaded: item.loaded,
263
+ total: item.total
264
+ }))
265
+ },
266
+
267
+ // ---- file dialog / drop / paste -----------------------------------
268
+ openFileDialog() {
269
+ if (this.disabled) return
270
+ if (this.$refs.input) this.$refs.input.click()
271
+ },
272
+ onInputChange(e) {
273
+ const files = e.target && e.target.files ? Array.prototype.slice.call(e.target.files) : []
274
+ this.uploadFiles(files)
275
+ // Reset so selecting the same file again re-triggers change.
276
+ if (e.target) e.target.value = ''
277
+ },
278
+ onDragOver() {
279
+ if (this.disabled) return
280
+ this.dragOver = true
281
+ },
282
+ onDragLeave() {
283
+ this.dragOver = false
284
+ },
285
+ onDrop(e) {
286
+ this.dragOver = false
287
+ if (this.disabled) return
288
+ this.$emit('drop', e)
289
+ const dt = e.dataTransfer
290
+ if (!dt) return
291
+ const files = dt.files ? Array.prototype.slice.call(dt.files) : []
292
+ if (files.length) this.uploadFiles(files)
293
+ },
294
+ onPaste(e) {
295
+ if (!this.pastable || this.disabled) return
296
+ const cd = e.clipboardData
297
+ if (!cd) return
298
+ const files = []
299
+ if (cd.items) {
300
+ Array.prototype.slice.call(cd.items).forEach(it => {
301
+ if (it.kind === 'file') {
302
+ const f = it.getAsFile()
303
+ if (f) files.push(f)
304
+ }
305
+ })
306
+ }
307
+ if (files.length) {
308
+ e.preventDefault()
309
+ this.uploadFiles(files)
310
+ }
311
+ },
312
+
313
+ // ---- pipeline ------------------------------------------------------
314
+ uploadFiles(rawFiles) {
315
+ let files = rawFiles.slice()
316
+ // maxCount === 1 → replace with the last selected.
317
+ if (this.maxCount === 1) {
318
+ files = files.slice(-1)
319
+ } else if (this.maxCount) {
320
+ const remaining = this.maxCount - this.visibleFileList.length
321
+ if (remaining <= 0) {
322
+ files.forEach(f => this.$emit('reject', { file: f, reason: 'maxCount' }))
323
+ return
324
+ }
325
+ if (files.length > remaining) {
326
+ files.slice(remaining).forEach(f => this.$emit('reject', { file: f, reason: 'maxCount' }))
327
+ files = files.slice(0, remaining)
328
+ }
329
+ }
330
+ const batch = files
331
+ files.forEach(raw => this.processFile(raw, batch))
332
+ },
333
+
334
+ async processFile(raw, batch) {
335
+ // Static validation first — fail fast without touching the list.
336
+ if (!attrAccept(raw, this.accept)) {
337
+ this.$emit('reject', { file: raw, reason: 'accept' })
338
+ return
339
+ }
340
+ if (this.maxSize && raw.size > this.maxSize) {
341
+ this.$emit('reject', { file: raw, reason: 'size' })
342
+ return
343
+ }
344
+
345
+ const uid = genUid()
346
+ try {
347
+ raw.uid = uid
348
+ } catch (err) {
349
+ // Some environments expose File as non-extensible — tracking still works via file2Obj.
350
+ }
351
+
352
+ let file = raw
353
+ let before = true
354
+ if (this.beforeUpload) {
355
+ try {
356
+ before = this.beforeUpload(raw, batch)
357
+ if (before && typeof before.then === 'function') before = await before
358
+ } catch (err) {
359
+ before = false
360
+ }
361
+ }
362
+
363
+ if (before === LIST_IGNORE) return
364
+
365
+ // beforeUpload may return a replacement File/Blob to upload.
366
+ if (before && before !== true && typeof before === 'object' && (before instanceof Blob || before.name)) {
367
+ file = before
368
+ }
369
+
370
+ if (before === false) {
371
+ // Added to the list but not auto-uploaded (consumer will upload manually).
372
+ this.addFile(file2Obj(file, uid), { autoUpload: false })
373
+ return
374
+ }
375
+
376
+ if (this.transformFile) {
377
+ try {
378
+ let t = this.transformFile(file)
379
+ if (t && typeof t.then === 'function') t = await t
380
+ if (t) file = t
381
+ } catch (err) {
382
+ // keep original file on transform failure
383
+ }
384
+ }
385
+
386
+ const item = file2Obj(file, uid)
387
+ this.addFile(item, { autoUpload: true, rawFile: file })
388
+ },
389
+
390
+ addFile(item, opts) {
391
+ const options = opts || {}
392
+ let nextList
393
+ if (this.maxCount === 1) nextList = [item]
394
+ else nextList = updateFileList(item, this.stateFileList)
395
+ this.commit(item, nextList)
396
+
397
+ // Generate a local thumbnail for image files (picture list types).
398
+ if (isImageFileType(item.type)) {
399
+ previewImage(item).then(url => {
400
+ if (!url) return
401
+ const current = getFileItem(item, this.stateFileList)
402
+ if (current) this.setFile({ ...current, thumbUrl: url })
403
+ })
404
+ }
405
+
406
+ if (options.autoUpload) this.post(item, options.rawFile || item.originFileObj)
407
+ else this.setFile({ ...item, status: 'done', percent: 100 })
408
+ },
409
+
410
+ post(item, rawFile) {
411
+ const actionVal = typeof this.action === 'function' ? this.action(rawFile) : this.action
412
+ const dataVal = typeof this.data === 'function' ? this.data(rawFile) : this.data
413
+ const uid = item.uid
414
+
415
+ // No endpoint and no custom uploader → treat as a manual/selected entry.
416
+ if (!this.customRequest && !actionVal) {
417
+ this.setFile({ ...item, status: 'done', percent: 100 })
418
+ return
419
+ }
420
+
421
+ const request = this.customRequest || defaultRequest
422
+ const handle = request({
423
+ action: actionVal,
424
+ method: this.method,
425
+ headers: this.headers,
426
+ data: dataVal,
427
+ name: this.name,
428
+ filename: this.name,
429
+ withCredentials: this.withCredentials,
430
+ file: rawFile,
431
+ onProgress: e => this.onFileProgress(uid, e),
432
+ onSuccess: (res, xhr) => this.onFileSuccess(uid, res, xhr),
433
+ onError: (err, res) => this.onFileError(uid, err, res)
434
+ })
435
+
436
+ if (handle && typeof handle.abort === 'function') this.reqs[uid] = handle
437
+ },
438
+
439
+ onFileProgress(uid, e) {
440
+ const item = getFileItem({ uid }, this.stateFileList)
441
+ if (!item || item.status !== 'uploading') return
442
+ const percent = Math.round(e.percent || 0)
443
+ const updated = { ...item, percent, status: 'uploading', loaded: e.loaded, total: e.total }
444
+ this.setFile(updated)
445
+ this.$emit('progress', { percent, loaded: e.loaded, total: e.total }, updated)
446
+ },
447
+ onFileSuccess(uid, res) {
448
+ this.clearReq(uid)
449
+ const item = getFileItem({ uid }, this.stateFileList)
450
+ if (!item) return
451
+ this.setFile({ ...item, status: 'done', percent: 100, response: res })
452
+ },
453
+ onFileError(uid, err, res) {
454
+ this.clearReq(uid)
455
+ const item = getFileItem({ uid }, this.stateFileList)
456
+ if (!item) return
457
+ this.setFile({ ...item, status: 'error', error: err, response: res })
458
+ },
459
+
460
+ // ---- list mutation (immutable + emit) ------------------------------
461
+ setFile(file) {
462
+ const nextList = updateFileList(file, this.stateFileList)
463
+ this.commit(file, nextList)
464
+ },
465
+ commit(file, nextList) {
466
+ this.stateFileList = nextList
467
+ this.$emit('change', { file, fileList: nextList })
468
+ this.$emit('update:fileList', nextList)
469
+ },
470
+
471
+ // ---- item actions (called by UploadItem via inject) ----------------
472
+ async handleRemove(file) {
473
+ if (this.beforeRemove) {
474
+ try {
475
+ let r = this.beforeRemove(file, this.stateFileList)
476
+ if (r && typeof r.then === 'function') r = await r
477
+ if (r === false) return
478
+ } catch (err) {
479
+ return
480
+ }
481
+ }
482
+ this.abortReq(file.uid)
483
+ const removedList = removeFileItem(file, this.stateFileList)
484
+ if (removedList === null) return
485
+ const removedFile = { ...file, status: 'removed' }
486
+ this.stateFileList = removedList
487
+ this.$emit('remove', removedFile)
488
+ this.$emit('change', { file: removedFile, fileList: removedList })
489
+ this.$emit('update:fileList', removedList)
490
+ },
491
+ handleRetry(file) {
492
+ if (!file.originFileObj) return
493
+ const item = { ...file, status: 'uploading', percent: 0, error: undefined }
494
+ this.setFile(item)
495
+ this.post(item, file.originFileObj)
496
+ },
497
+ handlePreview(file) {
498
+ this.$emit('preview', file)
499
+ const src = file.url || file.thumbUrl
500
+ if (src && this.resolveIsImage(file)) {
501
+ this.previewImageSrc = src
502
+ this.previewTitle = file.name || 'Preview'
503
+ this.previewVisible = true
504
+ }
505
+ },
506
+ handleDownload(file) {
507
+ this.$emit('download', file)
508
+ if (file.url && typeof window !== 'undefined') window.open(file.url, '_blank')
509
+ },
510
+
511
+ // ---- helpers -------------------------------------------------------
512
+ resolveIsImage(file) {
513
+ if (typeof this.isImageUrl === 'function') return this.isImageUrl(file)
514
+ return isImageUrl(file)
515
+ },
516
+ abortReq(uid) {
517
+ const req = this.reqs[uid]
518
+ if (req && typeof req.abort === 'function') req.abort()
519
+ this.clearReq(uid)
520
+ },
521
+ clearReq(uid) {
522
+ if (this.reqs[uid]) delete this.reqs[uid]
523
+ },
524
+ abortAll() {
525
+ Object.keys(this.reqs || {}).forEach(uid => this.abortReq(uid))
526
+ },
527
+ onPreviewChange(v) {
528
+ this.previewVisible = v
529
+ }
530
+ }
531
+ }
532
+ </script>
533
+
534
+ <style src="./upload.css" scoped></style>