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
Line 
1<script setup lang="ts">
2
3import { ref, computed, defineProps, withDefaults, defineEmits, getCurrentInstance, defineExpose, watch, nextTick } from 'vue'
4
5import axios from 'axios'
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
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
35const emit = defineEmits<{
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
40 (e: 'update:state', new_state: State): void
41
42 (e: 'files:add', evt: any, files: any[]): void
43 (e: 'files:clear'): void
44
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
58let abort_controller: any = null
59
60const do_emit = (code: string, ... args: any[]) => {
61 // @ts-ignore
62 emit.apply(null, [code].concat(args))
63 emit.apply(null, ['debug', code, args])
64}
65
66const inst = getCurrentInstance()
67
68const state = ref(State.s_idle)
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)
97 else // XXX elem click
98 for(const file of evt.target.files)
99 files.push(file)
100
101 return files
102}
103
104const drop = (evt: any): void => {
105 if(state.value != State.s_hover && state.value != State.s_idle && state.value != State.s_wait)
106 return
107
108 state.value = State.s_wait
109 files = files.concat(extract_files(evt))
110 do_emit('files:add', [evt, files])
111 queue.value = files
112
113 if(props.autoStart || props.autoStart === '')
114 start_cb()
115}
116
117const drag_over = (evt: any): void => {
118 if(state.value != State.s_idle)
119 return
120
121 state.value = State.s_hover
122
123 do_emit('drag:over', evt)
124}
125
126const drag_out = (evt: any): void => {
127 if(state.value != State.s_hover)
128 return
129
130 state.value = State.s_idle
131
132 do_emit('drag:out', evt)
133}
134
135const start_cb = (): void => {
136 if(state.value != State.s_wait)
137 return
138
139 try {
140 abort_controller = new AbortController()
141 }
142 catch {
143 }
144
145 do_emit('upload:start', [files])
146
147 aborted = false
148
149 state.value = State.s_uploading
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 => {
165 if(state.value == State.s_idle)
166 return
167
168 state.value = State.s_idle
169 files = []
170 do_emit('files:clear', [])
171}
172
173const pick_cb = (): void => {
174 if(state.value != State.s_idle && state.value != State.s_wait)
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 })
183
184 inp.click()
185}
186
187const abort_cb = (): void => {
188 if(state.value != State.s_uploading)
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
207 queue.value = queue.value.filter((file) => {
208 return file !== cur
209 })
210
211 do_emit('upload:progress', progress.value, current.value, queue.value)
212
213 cur_file += 1
214
215 const options = {
216 method: 'post',
217 url: props.target,
218 data: fd,
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 }
228 })
229
230 do_emit('upload:progress', progress.value, current.value, queue.value)
231 }
232 } as any
233
234 if(abort_controller)
235 options.signal = abort_controller.signal
236
237 axios(options)
238 .then((res) => {
239 current.value = current.value.filter((item) => {
240 return item.file !== cur
241 })
242
243 do_emit('upload:progress', progress.value, current.value, queue.value)
244
245 progress.value.push(res.data)
246 results.value.push(res.data)
247
248 cur_jobs -= 1
249
250 if(aborted) {
251 if(props.autoReset || props.autoReset === '') {
252 state.value = State.s_idle
253 files = [] // # XXX can drop w/o reset
254 do_emit('files:clear', [])
255 }
256 else
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 }
261
262 return
263 }
264
265 if(cur_file == files.length && !cur_jobs) {
266 files = [] // XXX can drop w/o reset
267 do_emit('files:clear', [])
268
269 if(errors.value.length)
270 if(props.keepGoing || props.keepGoing === '') {
271 state.value = State.s_with_errors
272 do_emit('upload:done', results.value, errors.value)
273 }
274 else
275 state.value = State.s_error
276 else
277 if(props.autoReset || props.autoReset === '')
278 state.value = State.s_idle
279 else {
280 state.value = State.s_done
281 do_emit('upload:done', results.value, errors.value)
282 }
283
284 return
285 }
286
287 while(cur_jobs < props.maxJobs && cur_file < files.length)
288 start_job()
289 })
290 .catch((err) => {
291 if(aborted && err.name == 'CanceledError') {
292 state.value = State.s_aborted
293 return err
294 }
295
296 if(props.keepGoing || props.keepGoing === '') {
297 current.value = current.value.filter((item) => {
298 return item.file !== cur
299 })
300
301 errors.value.push({
302 error: err.response,
303 file: cur
304 })
305
306 do_emit('upload:error', err.response)
307
308 cur_jobs -= 1
309
310 if(cur_file == files.length && !cur_jobs) {
311 state.value = State.s_with_errors
312 do_emit('upload:done', results.value, errors.value)
313 return
314 }
315
316 while(cur_jobs < props.maxJobs && cur_file < files.length)
317 start_job()
318 }
319 else {
320 state.value = State.s_error
321 error.value = err.response
322 do_emit('upload:error', err.response)
323 }
324 })
325}
326
327const can_pick = computed({
328 get: () => {
329 return state.value == State.s_idle || state.value == State.s_wait
330 },
331 set: () => {
332 }
333})
334
335const can_start = computed({
336 get: () => {
337 return state.value == State.s_wait
338 },
339 set: () => {
340 }
341})
342
343const can_abort = computed({
344 get: () => {
345 return state.value == State.s_uploading
346 },
347 set: () => {
348 }
349})
350
351const can_reset = computed({
352 get: () => {
353 return state.value != State.s_idle
354 },
355 set: () => {
356 }
357})
358
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)
366}
367
368watch(() => { return state.value }, (t: State, f: State) => { nextTick(on_state_changed) } )
369
370nextTick(on_state_changed)
371
372defineExpose({
373 reset: reset_cb,
374 start: start_cb,
375 pick: pick_cb,
376 abort: abort_cb,
377
378 can_pick,
379 can_start,
380 can_abort,
381 can_reset
382})
383
384</script>
385
386<template bindings="">
387 <component is='props.tag' @drop.prevent='drop' @dragover.prevent='drag_over' @dragleave.prevent='drag_out'>
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'
391 v-bind:queue='queue' v-bind:reset='reset_cb' />
392 <slot name='uploading' v-if='state == "uploading"'
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' />
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'
402 v-bind:reset='reset_cb' />
403 </component>
404</template>
405
Note: See TracBrowser for help on using the repository browser.