source: uz/src/UploadZone.vue

Last change on this file was 76e46d7, checked in by js29a <js29a@…>, 2 years ago

to axios

  • Property mode set to 100644
File size: 9.3 KB
RevLine 
[54176ee]1<script setup lang="ts">
2
[64249d6]3import { ref, computed, defineProps, withDefaults, defineEmits, getCurrentInstance, defineExpose, watch, nextTick } from 'vue'
[54176ee]4
[76e46d7]5import axios from 'axios'
[54176ee]6
7interface Props {
8 target: string
9 tag?: string
10 maxJobs?: number
11 autoStart?: boolean | string
12 autoReset?: boolean | string
13 keepGoing?: boolean | string
14}
15
16const props = withDefaults(defineProps<Props>(), {
17 maxJobs: 3,
18 autoStart: false,
19 autoReset: false,
20 keepGoing: false,
21 tag: 'div'
22})
23
[d45005e]24enum State {
25 s_idle = 'idle',
26 s_hover = 'hover',
27 s_wait = 'wait',
28 s_uploading = 'uploading',
29 s_done = 'done',
30 s_error = 'error',
31 s_with_errors = 'with_errors',
32 s_aborted = 'aborted'
33}
34
[54176ee]35const emit = defineEmits<{
[64249d6]36 (e: 'update:canReset', flag: boolean): void
37 (e: 'update:canPick', flag: boolean): void
38 (e: 'update:canStart', flag: boolean): void
39 (e: 'update:canAbort', flag: boolean): void
[d45005e]40 (e: 'update:state', new_state: State): void
[54176ee]41
[d45005e]42 (e: 'files:add', evt: any, files: any[]): void
43 (e: 'files:clear'): void
[54176ee]44
[d45005e]45 (e: 'drag:over', evt: any): void
46 (e: 'drag:out', evt: any): void
47
48 (e: 'upload:start', queue: any[]): void
49 (e: 'upload:progress', progress: any[], current: any[], queue: any[]): void
50
51 (e: 'upload:done', result: any[], errors: any[]): void
52 (e: 'upload:error', error: any): void
53 (e: 'upload:aborted', progress: any[], current: any[], queue: any[], errors: any[]): void
54
55 (e: 'debug', ... args: any[]): void
56}>()
57
[76e46d7]58let abort_controller: any = null
59
[d45005e]60const do_emit = (code: string, ... args: any[]) => {
61 // @ts-ignore
62 emit.apply(null, [code].concat(args))
63 emit.apply(null, ['debug', code, args])
[54176ee]64}
65
[d45005e]66const inst = getCurrentInstance()
67
68const state = ref(State.s_idle)
[54176ee]69
70const queue = ref([] as any[])
71const progress = ref({} as any)
72const results = ref([] as any[])
73const current = ref([] as any[])
74const error = ref({} as any)
75
76const errors = ref([] as any)
77
78let files: any[] = []
79
80let cur_jobs = 0
81let cur_file = 0
82
83let aborted = false
84
85const extract_files = (evt: any): any[] => {
86 const files = []
87
88 if(evt.dataTransfer && evt.dataTransfer.items !== undefined) {
89 for(const item of evt.dataTransfer.items)
90 if(item.kind == 'file')
91 files.push(item.getAsFile())
92 }
93 else
94 if(evt.dataTransfer)
95 for(const file of evt.dataTransfer.files)
96 files.push(file)
[76e46d7]97 else // XXX elem click
[54176ee]98 for(const file of evt.target.files)
99 files.push(file)
100
101 return files
102}
103
104const drop = (evt: any): void => {
[d45005e]105 if(state.value != State.s_hover && state.value != State.s_idle && state.value != State.s_wait)
[54176ee]106 return
107
[d45005e]108 state.value = State.s_wait
[54176ee]109 files = files.concat(extract_files(evt))
[d45005e]110 do_emit('files:add', [evt, files])
[54176ee]111 queue.value = files
112
113 if(props.autoStart || props.autoStart === '')
114 start_cb()
115}
116
117const drag_over = (evt: any): void => {
[d45005e]118 if(state.value != State.s_idle)
[54176ee]119 return
120
[d45005e]121 state.value = State.s_hover
122
123 do_emit('drag:over', evt)
[54176ee]124}
125
126const drag_out = (evt: any): void => {
[d45005e]127 if(state.value != State.s_hover)
[54176ee]128 return
129
[d45005e]130 state.value = State.s_idle
131
132 do_emit('drag:out', evt)
[54176ee]133}
134
135const start_cb = (): void => {
[d45005e]136 if(state.value != State.s_wait)
[54176ee]137 return
138
[76e46d7]139 try {
140 abort_controller = new AbortController()
141 }
142 catch {
143 }
144
[d45005e]145 do_emit('upload:start', [files])
146
[54176ee]147 aborted = false
148
[d45005e]149 state.value = State.s_uploading
[54176ee]150
151 results.value = []
152 progress.value = []
153 current.value = []
154 errors.value = []
155
156 error.value = {}
157 cur_file = 0
158 cur_jobs = 0
159
160 while(cur_jobs < props.maxJobs && cur_file < files.length)
161 start_job()
162}
163
164const reset_cb = (): void => {
[d45005e]165 if(state.value == State.s_idle)
[64249d6]166 return
167
[d45005e]168 state.value = State.s_idle
[54176ee]169 files = []
[d45005e]170 do_emit('files:clear', [])
[54176ee]171}
172
173const pick_cb = (): void => {
[d45005e]174 if(state.value != State.s_idle && state.value != State.s_wait)
[54176ee]175 return
176
177 const inp = document.createElement('input')
178 inp.setAttribute('type', 'file')
179 inp.setAttribute('multiple', 'true')
180 inp.addEventListener('input', (evt: any): void => {
181 drop(evt)
182 })
[d45005e]183
[54176ee]184 inp.click()
185}
186
187const abort_cb = (): void => {
[d45005e]188 if(state.value != State.s_uploading)
[54176ee]189 return
190
191 aborted = true
192}
193
194const start_job = () => {
195 cur_jobs += 1
196
197 const fd = new FormData()
198 const cur = files[cur_file]
199 fd.append('file', files[cur_file])
200
201 current.value.push({
202 file: files[cur_file],
203 current: 0,
204 total: files[cur_file].size
205 })
206
[64249d6]207 queue.value = queue.value.filter((file) => {
[54176ee]208 return file !== cur
209 })
210
[d45005e]211 do_emit('upload:progress', progress.value, current.value, queue.value)
212
[54176ee]213 cur_file += 1
214
[76e46d7]215 const options = {
216 method: 'post',
[54176ee]217 url: props.target,
218 data: fd,
[76e46d7]219 onUploadProgress: (evt: any) => {
220 if(aborted && abort_controller)
221 abort_controller.abort()
222
223 current.value.forEach((item) => {
224 if(item.file === cur) {
225 item.current = evt.loaded
226 item.total = evt.total
227 }
[54176ee]228 })
229
[76e46d7]230 do_emit('upload:progress', progress.value, current.value, queue.value)
[54176ee]231 }
[76e46d7]232 } as any
233
234 if(abort_controller)
235 options.signal = abort_controller.signal
236
237 axios(options)
238 .then((res) => {
[64249d6]239 current.value = current.value.filter((item) => {
[54176ee]240 return item.file !== cur
241 })
242
[d45005e]243 do_emit('upload:progress', progress.value, current.value, queue.value)
244
[76e46d7]245 progress.value.push(res.data)
246 results.value.push(res.data)
[54176ee]247
248 cur_jobs -= 1
249
250 if(aborted) {
251 if(props.autoReset || props.autoReset === '') {
[d45005e]252 state.value = State.s_idle
[76e46d7]253 files = [] // # XXX can drop w/o reset
[d45005e]254 do_emit('files:clear', [])
[54176ee]255 }
256 else
[d45005e]257 if(state.value != State.s_aborted) {
258 state.value = State.s_aborted
259 do_emit('upload:aborted', progress.value, current.value, queue.value, errors.value)
260 }
[54176ee]261
262 return
263 }
264
265 if(cur_file == files.length && !cur_jobs) {
[76e46d7]266 files = [] // XXX can drop w/o reset
[d45005e]267 do_emit('files:clear', [])
[54176ee]268
[64249d6]269 if(errors.value.length)
[d45005e]270 if(props.keepGoing || props.keepGoing === '') {
271 state.value = State.s_with_errors
272 do_emit('upload:done', results.value, errors.value)
273 }
[54176ee]274 else
[d45005e]275 state.value = State.s_error
[54176ee]276 else
277 if(props.autoReset || props.autoReset === '')
[d45005e]278 state.value = State.s_idle
279 else {
280 state.value = State.s_done
281 do_emit('upload:done', results.value, errors.value)
282 }
[54176ee]283
284 return
285 }
286
287 while(cur_jobs < props.maxJobs && cur_file < files.length)
288 start_job()
289 })
[76e46d7]290 .catch((err) => {
291 if(aborted && err.name == 'CanceledError') {
292 state.value = State.s_aborted
293 return err
294 }
295
[54176ee]296 if(props.keepGoing || props.keepGoing === '') {
[64249d6]297 current.value = current.value.filter((item) => {
[54176ee]298 return item.file !== cur
299 })
300
301 errors.value.push({
[76e46d7]302 error: err.response,
[54176ee]303 file: cur
304 })
305
[76e46d7]306 do_emit('upload:error', err.response)
[d45005e]307
[54176ee]308 cur_jobs -= 1
309
310 if(cur_file == files.length && !cur_jobs) {
[d45005e]311 state.value = State.s_with_errors
312 do_emit('upload:done', results.value, errors.value)
[54176ee]313 return
314 }
315
316 while(cur_jobs < props.maxJobs && cur_file < files.length)
317 start_job()
318 }
319 else {
[d45005e]320 state.value = State.s_error
[76e46d7]321 error.value = err.response
322 do_emit('upload:error', err.response)
[54176ee]323 }
324 })
325}
326
[64249d6]327const can_pick = computed({
328 get: () => {
[d45005e]329 return state.value == State.s_idle || state.value == State.s_wait
[64249d6]330 },
331 set: () => {
332 }
333})
334
335const can_start = computed({
336 get: () => {
[d45005e]337 return state.value == State.s_wait
[64249d6]338 },
339 set: () => {
340 }
341})
342
343const can_abort = computed({
344 get: () => {
[d45005e]345 return state.value == State.s_uploading
[64249d6]346 },
347 set: () => {
348 }
349})
350
351const can_reset = computed({
352 get: () => {
[d45005e]353 return state.value != State.s_idle
[64249d6]354 },
355 set: () => {
356 }
357})
358
[d45005e]359const on_state_changed = (): void => {
360 do_emit('update:canPick', can_pick.value)
361 do_emit('update:canStart', can_start.value)
362 do_emit('update:canAbort', can_abort.value)
363 do_emit('update:canReset', can_reset.value)
364
365 do_emit('update:state', state.value)
[64249d6]366}
367
[d45005e]368watch(() => { return state.value }, (t: State, f: State) => { nextTick(on_state_changed) } )
[64249d6]369
[d45005e]370nextTick(on_state_changed)
[64249d6]371
[54176ee]372defineExpose({
373 reset: reset_cb,
374 start: start_cb,
375 pick: pick_cb,
[64249d6]376 abort: abort_cb,
377
378 can_pick,
379 can_start,
380 can_abort,
381 can_reset
[54176ee]382})
383
384</script>
385
386<template bindings="">
387 <component is='props.tag' @drop.prevent='drop' @dragover.prevent='drag_over' @dragleave.prevent='drag_out'>
[d45005e]388 <slot name='idle' v-if='state == "idle"' v-bind:pick='pick_cb' />
389 <slot name='hover' v-if='state == "hover"' />
390 <slot name='wait' v-if='state == "wait"' v-bind:start='start_cb'
[54176ee]391 v-bind:queue='queue' v-bind:reset='reset_cb' />
[d45005e]392 <slot name='uploading' v-if='state == "uploading"'
[54176ee]393 v-bind:progress='progress'
394 v-bind:current='current'
395 v-bind:queue='queue'
396 v-bind:errors='errors'
397 v-bind:abort='abort_cb' />
[d45005e]398 <slot name='done' v-if='state == "done"' v-bind:result='results' v-bind:reset='reset_cb' />
399 <slot name='error' v-if='state == "error"' v-bind:error='error' v-bind:reset='reset_cb' />
400 <slot name='aborted' v-if='state == "aborted"' v-bind:result='results' v-bind:reset='reset_cb' />
401 <slot name='with-errors' v-if='state == "with_errors"' v-bind:result='results' v-bind:errors='errors'
[54176ee]402 v-bind:reset='reset_cb' />
403 </component>
404</template>
405
Note: See TracBrowser for help on using the repository browser.