source: uz/src/UploadZone.vue@ d45005e

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

events

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