Index: README.md
===================================================================
--- README.md	(revision 76e46d7f0a4df764d7c6f271bd194766229f4d19)
+++ README.md	(revision 54176ee8abe2033da05f8e84d73bea143054abc9)
@@ -4,3 +4,2 @@
 more info at: https://pub.js29a.info.pl/pub/wiki/UZ
 
-demo: https://vite.js29a.usermd.net/#/0
Index: p/upload.php
===================================================================
--- php/upload.php	(revision 76e46d7f0a4df764d7c6f271bd194766229f4d19)
+++ 	(revision )
@@ -1,18 +1,0 @@
-<?php
-
-header('content-type: application/json');
-header('Access-Control-Allow-Origin: *');
-
-if($_FILES['file']['name'] == 'err.html') {
-  http_response_code(404);
-
-  echo json_encode([
-    'status' => 'error',
-    'file' => $_FILES['file']['name']
-  ]);
-}
-else
-  echo json_encode([
-    'status' => 'ok',
-    'file' => $_FILES['file']['name']
-  ]);
Index: c/UploadTest.vue
===================================================================
--- src/UploadTest.vue	(revision 76e46d7f0a4df764d7c6f271bd194766229f4d19)
+++ 	(revision )
@@ -1,243 +1,0 @@
-<script setup lang="ts">
-
-import { ref, getCurrentInstance } from 'vue'
-
-import UploadZone from './UploadZone.vue'
-
-const inst = getCurrentInstance()
-
-const auto_start = ref(false)
-const auto_reset = ref(false)
-const keep_going = ref(false)
-const max_jobs = ref(3)
-
-const can_reset = ref(false)
-const can_start = ref(false)
-const can_pick = ref(false)
-const can_abort = ref(false)
-
-const reset = (): void => {
-  (inst as any).refs.uploader.reset()
-}
-
-const start = (): void => {
-  (inst as any).refs.uploader.start()
-}
-
-const pick = (): void => {
-  (inst as any).refs.uploader.pick()
-}
-
-const abort = (): void => {
-  (inst as any).refs.uploader.abort()
-}
-
-const log = (code: string, args: any[]) => {
-  console.log.apply(null, ['>', code].concat(args))
-}
-
-</script>
-
-<template>
-  <div class='over'>
-    <button @click='pick' :disabled='!can_pick'>pick</button>
-    <span>&nbsp;</span>
-    <button @click='start' :disabled='!can_start'>start</button>
-    <span>&nbsp;</span>
-    <button @click='abort' :disabled='!can_abort'>abort</button>
-    <span>&nbsp;</span>
-    <button @click='reset' :disabled='!can_reset'>reset</button>
-    <p />
-    <input type='checkbox' v-model='auto_start' id='auto-start' />
-    <label for='auto-start'>&nbsp;auto-start</label><br />
-    <input type='checkbox' v-model='auto_reset' id='auto-reset' />
-    <label for='auto-reset'>&nbsp;auto-reset</label><br />
-    <input type='checkbox' v-model='keep_going' id='keep-going' />
-    <label for='keep-going'>&nbsp;keep-going</label><br />
-    <p />
-
-    <component class='uploader' :is='UploadZone' target='https://vite.js29a.usermd.net/upload.php'
-               :max-jobs='max_jobs' :auto-start='auto_start' :auto-reset='auto_reset'
-               :keep-going='keep_going' ref='uploader'
-               @update:canPick='(flag: boolean) => can_pick = flag'
-               @update:canStart='(flag: boolean) => can_start = flag'
-               @update:canAbort='(flag: boolean) => can_abort = flag'
-               @update:canReset='(flag: boolean) => can_reset = flag'
-               @debug='(code: string, args: any[]) => log(code, args)'>
-      <template #idle='{ pick }'>
-        <div class='idle'>
-          idle
-          <br />
-          <button @click='pick()'>pick</button>
-        </div>
-      </template>
-
-      <template #hover>
-        <div class='hover'>
-          hover
-        </div>
-      </template>
-
-      <template #wait='{ start, queue }'>
-        <div class='wait'>
-          wait
-          <br />
-          <button @click='pick()'>pick</button>
-          <br />
-          <button @click='start()'>start</button>
-          <br/>
-          <div v-for='file in queue'>{{ file.name }}</div>
-        </div>
-      </template>
-
-      <template #uploading='{ progress, current, queue, abort, errors }'>
-        <div class='uploading'>
-          uploading
-          <br />
-          <button @click='abort()'>abort</button>
-          <br />
-          {{ progress.length }} / {{ current.length }} / {{ queue.length }}
-          <b>errors:</b> <span v-for='error in errors'>{{ error.file.name }}</span>
-          <b>progress:</b> <div v-for='item in progress'>{{ item }}</div>
-          <b>current:</b>
-          <div v-for='item in current'>
-            {{ item.file.name }}: {{ item.current }} of {{ item.total }}
-          </div>
-          <b>remaining:</b>
-          <div v-for='file in queue'>{{ file.name }}</div>
-        </div>
-      </template>
-
-      <template #done='{ result, reset }'>
-        <div class='done'>
-          done
-          <br />
-          <button @click='reset()'>reset</button>
-          <br />
-          <div v-for='item in result'>{{ item }}</div>
-        </div>
-      </template>
-
-      <template #error='{ error, reset }'>
-        <div class='error'>
-          error
-          <br />
-          <button @click='reset()'>reset</button>
-          <div>{{ error }}</div>
-        </div>
-      </template>
-
-      <template #aborted='{ result, reset }'>
-        <div class='aborted'>
-          aborted
-          <br />
-          <button @click='reset()'>reset</button>
-          <div v-for='item in result'>{{ item }}</div>
-        </div>
-      </template>
-
-      <template #with-errors='{ result, errors }'>
-        <div class='with-errors'>
-          with errors
-          <br />
-          <b>errors(s):</b>
-          <div v-for='item in errors'>{{ item.file.name }} / {{ item.error }}</div>
-          <b>ok:</b>
-          <div v-for='item in result'>{{ item }}</div>
-        </div>
-      </template>
-    </component>
-  </div>
-</template>
-
-<style scoped>
-
-.over {
-  position: absolute;
-  border-radius: 20px;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-}
-
-.uploader {
-  border: 3px dashed #fff;
-  width: 512px;
-  height: 384px;
-  display: inline-block;
-}
-
-.idle,
-.hover,
-.wait,
-.uploading,
-.done,
-.error,
-.aborted,
-.with-errors {
-  width: 100%;
-  height: 100%;
-  border: 1px solid #0ff;
-  padding: 5px;
-  overflow: auto;
-}
-
-.status {
-  width: 25%;
-  height: auto;
-  background-color: #555;
-  color: #fff;
-  position: absolute;
-  right: 0px;
-  bottom: 0px;
-  padding: 2px 2px 2px 2px;
-  text-align: center;
-  font-weight: bold;
-  border-radius: 10px 0px 0px 0px;
-  border: 1px solid #0ff;
-  opacity: 0.75;
-}
-
-.idle {
-  background-color: #088;
-  color: black;
-}
-
-.hover {
-  background-color: #088;
-  color: white;
-  cursor: copy;
-}
-
-.wait {
-  background-color: #880;
-  color: black;
-}
-
-.uploading {
-  background-color: #050;
-  color: white;
-}
-
-.done {
-  background-color: #00f;
-  color: white;
-}
-
-.error {
-  background-color: #800;
-  color: white;
-}
-
-.aborted {
-  background-color: #300;
-  color: white;
-}
-
-.with-errors {
-  background-color: #800;
-  color: white;
-}
-
-</style>
-
Index: src/UploadTest0.vue
===================================================================
--- src/UploadTest0.vue	(revision 54176ee8abe2033da05f8e84d73bea143054abc9)
+++ src/UploadTest0.vue	(revision 54176ee8abe2033da05f8e84d73bea143054abc9)
@@ -0,0 +1,201 @@
+<script setup lang="ts">
+
+import { ref, getCurrentInstance } from 'vue'
+
+import UploadZone from './UploadZone.vue'
+
+const inst = getCurrentInstance()
+
+const auto_start = ref(false)
+const auto_reset = ref(false)
+const keep_going = ref(false)
+const max_jobs = ref(3)
+
+const reset = (): void => {
+  (inst as any).refs.uploader.reset()
+}
+
+const start = (): void => {
+  (inst as any).refs.uploader.start()
+}
+
+</script>
+
+<template>
+  <component class='uploader' :is='UploadZone' target='https://vite.js29a.usermd.net/upload.php'
+             :max-jobs='max_jobs' :auto-start='auto_start' :auto-reset='auto_reset'
+             :keep-going='keep_going' ref='uploader'>
+    <template #idle='{ pick }'>
+      <div class='idle'>
+        idle
+        <br />
+        <button @click='pick()'>pick</button>
+      </div>
+    </template>
+
+    <template #hover>
+      <div class='hover'>
+        hover
+      </div>
+    </template>
+
+    <template #wait='{ start, queue }'>
+      <div class='wait'>
+        wait
+        <br />
+        <button @click='pick()'>pick</button>
+        <br />
+        <button @click='start()'>start</button>
+        <br/>
+        <div v-for='file in queue'>{{ file.name }}</div>
+      </div>
+    </template>
+
+    <template #uploading='{ progress, current, queue, abort, errors }'>
+      <div class='uploading'>
+        uploading
+        <br />
+        <button @click='abort()'>abort</button>
+        <br />
+        {{ progress.length }} / {{ current.length }} / {{ queue.length }}
+        <br />
+        <button @click='abort()'>abort</button>
+        <b>errors:</b> <span v-for='error in errors'>{{ error.file.name }}</span>
+        <b>progress:</b> <div v-for='item in progress'>{{ item }}</div>
+        <b>current:</b>
+        <div v-for='item in current'>
+          {{ item.file.name }}: {{ item.current }} of {{ item.total }}
+        </div>
+        <b>remaining:</b>
+        <div v-for='file in queue'>{{ file.name }}</div>
+      </div>
+    </template>
+
+    <template #done='{ result, reset }'>
+      <div class='done'>
+        done
+        <br />
+        <button @click='reset()'>reset</button>
+        <br />
+        <div v-for='item in result'>{{ item }}</div>
+      </div>
+    </template>
+
+    <template #error='{ error, reset }'>
+      <div class='error'>
+        error
+        <br />
+        <button @click='reset()'>reset</button>
+        <div>{{ error }}</div>
+      </div>
+    </template>
+
+    <template #aborted='{ result, reset }'>
+      <div class='aborted'>
+        aborted
+        <br />
+        <button @click='reset()'>reset</button>
+        <div v-for='item in result'>{{ item }}</div>
+      </div>
+    </template>
+
+    <template #with-errors='{ result, errors }'>
+      <div class='with-errors'>
+        with errors
+        <br />
+        <b>errors(s):</b>
+        <div v-for='item in errors'>{{ item.file.name }} / {{ item.error }}</div>
+        <b>ok:</b>
+        <div v-for='item in result'>{{ item }}</div>
+      </div>
+    </template>
+  </component>
+</template>
+
+<style scoped>
+
+.uploader {
+  border: 3px dashed #fff;
+  width: 512px;
+  height: 384px;
+  position: absolute;
+  border-radius: 20px;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.idle,
+.hover,
+.wait,
+.uploading,
+.done,
+.error,
+.aborted,
+.with-errors {
+  width: 100%;
+  height: 100%;
+  border: 1px solid #0ff;
+  padding: 5px;
+  overflow: auto;
+}
+
+.status {
+  width: 25%;
+  height: auto;
+  background-color: #555;
+  color: #fff;
+  position: absolute;
+  right: 0px;
+  bottom: 0px;
+  padding: 2px 2px 2px 2px;
+  text-align: center;
+  font-weight: bold;
+  border-radius: 10px 0px 0px 0px;
+  border: 1px solid #0ff;
+  opacity: 0.75;
+}
+
+.idle {
+  background-color: #088;
+  color: black;
+}
+
+.hover {
+  background-color: #088;
+  color: white;
+  cursor: copy;
+}
+
+.wait {
+  background-color: #880;
+  color: black;
+}
+
+.uploading {
+  background-color: #050;
+  color: white;
+}
+
+.done {
+  background-color: #00f;
+  color: white;
+}
+
+.error {
+  background-color: #800;
+  color: white;
+}
+
+.aborted {
+  background-color: #300;
+  color: white;
+}
+
+.with-errors {
+  background-color: #800;
+  color: white;
+}
+
+</style>
+
Index: c/UploadZone.vue
===================================================================
--- src/UploadZone.vue	(revision 76e46d7f0a4df764d7c6f271bd194766229f4d19)
+++ 	(revision )
@@ -1,405 +1,0 @@
-<script setup lang="ts">
-
-import { ref, computed, defineProps, withDefaults, defineEmits, getCurrentInstance, defineExpose, watch, nextTick } from 'vue'
-
-import axios from 'axios'
-
-interface Props {
-  target: string
-  tag?: string
-  maxJobs?: number
-  autoStart?: boolean | string
-  autoReset?: boolean | string
-  keepGoing?: boolean | string
-}
-
-const props = withDefaults(defineProps<Props>(), {
-  maxJobs: 3,
-  autoStart: false,
-  autoReset: false,
-  keepGoing: false,
-  tag: 'div'
-})
-
-enum State {
-  s_idle = 'idle',
-  s_hover = 'hover',
-  s_wait = 'wait',
-  s_uploading = 'uploading',
-  s_done = 'done',
-  s_error = 'error',
-  s_with_errors = 'with_errors',
-  s_aborted = 'aborted'
-}
-
-const emit = defineEmits<{
-  (e: 'update:canReset', flag: boolean): void
-  (e: 'update:canPick', flag: boolean): void
-  (e: 'update:canStart', flag: boolean): void
-  (e: 'update:canAbort', flag: boolean): void
-  (e: 'update:state', new_state: State): void
-
-  (e: 'files:add', evt: any, files: any[]): void
-  (e: 'files:clear'): void
-
-  (e: 'drag:over', evt: any): void
-  (e: 'drag:out', evt: any): void
-
-  (e: 'upload:start', queue: any[]): void
-  (e: 'upload:progress', progress: any[], current: any[], queue: any[]): void
-
-  (e: 'upload:done', result: any[], errors: any[]): void
-  (e: 'upload:error', error: any): void
-  (e: 'upload:aborted', progress: any[], current: any[], queue: any[], errors: any[]): void
-
-  (e: 'debug', ... args: any[]): void
-}>()
-
-let abort_controller: any = null
-
-const do_emit = (code: string, ... args: any[]) => {
-  // @ts-ignore
-  emit.apply(null, [code].concat(args))
-  emit.apply(null, ['debug', code, args])
-}
-
-const inst = getCurrentInstance()
-
-const state = ref(State.s_idle)
-
-const queue = ref([] as any[])
-const progress = ref({} as any)
-const results = ref([] as any[])
-const current = ref([] as any[])
-const error = ref({} as any)
-
-const errors = ref([] as any)
-
-let files: any[] = []
-
-let cur_jobs = 0
-let cur_file = 0
-
-let aborted = false
-
-const extract_files = (evt: any): any[] => {
-  const files = []
-
-  if(evt.dataTransfer && evt.dataTransfer.items !== undefined) {
-    for(const item of evt.dataTransfer.items)
-      if(item.kind == 'file')
-        files.push(item.getAsFile())
-  }
-  else
-    if(evt.dataTransfer)
-      for(const file of evt.dataTransfer.files)
-        files.push(file)
-    else // XXX elem click
-      for(const file of evt.target.files)
-        files.push(file)
-
-  return files
-}
-
-const drop = (evt: any): void => {
-  if(state.value != State.s_hover && state.value != State.s_idle && state.value != State.s_wait)
-    return
-
-  state.value = State.s_wait
-  files = files.concat(extract_files(evt))
-  do_emit('files:add', [evt, files])
-  queue.value = files
-
-  if(props.autoStart || props.autoStart === '')
-    start_cb()
-}
-
-const drag_over = (evt: any): void => {
-  if(state.value != State.s_idle)
-    return
-
-  state.value = State.s_hover
-
-  do_emit('drag:over', evt)
-}
-
-const drag_out = (evt: any): void => {
-  if(state.value != State.s_hover)
-    return
-
-  state.value = State.s_idle
-
-  do_emit('drag:out', evt)
-}
-
-const start_cb = (): void => {
-  if(state.value != State.s_wait)
-    return
-
-  try {
-    abort_controller = new AbortController()
-  }
-  catch {
-  }
-
-  do_emit('upload:start', [files])
-
-  aborted = false
-
-  state.value = State.s_uploading
-
-  results.value = []
-  progress.value = []
-  current.value = []
-  errors.value = []
-
-  error.value = {}
-  cur_file = 0
-  cur_jobs = 0
-
-  while(cur_jobs < props.maxJobs && cur_file < files.length)
-    start_job()
-}
-
-const reset_cb = (): void => {
-  if(state.value == State.s_idle)
-    return
-
-  state.value = State.s_idle
-  files = []
-  do_emit('files:clear', [])
-}
-
-const pick_cb = (): void => {
-  if(state.value != State.s_idle && state.value != State.s_wait)
-    return
-
-  const inp = document.createElement('input')
-  inp.setAttribute('type', 'file')
-  inp.setAttribute('multiple', 'true')
-  inp.addEventListener('input', (evt: any): void => {
-    drop(evt)
-  })
-
-  inp.click()
-}
-
-const abort_cb = (): void => {
-  if(state.value != State.s_uploading)
-    return
-
-  aborted = true
-}
-
-const start_job = () => {
-  cur_jobs += 1
-
-  const fd = new FormData()
-  const cur = files[cur_file]
-  fd.append('file', files[cur_file])
-
-  current.value.push({
-    file: files[cur_file],
-    current: 0,
-    total: files[cur_file].size
-  })
-
-  queue.value = queue.value.filter((file) => {
-    return file !== cur
-  })
-
-  do_emit('upload:progress', progress.value, current.value, queue.value)
-
-  cur_file += 1
-
-  const options = {
-    method: 'post',
-    url: props.target,
-    data: fd,
-    onUploadProgress: (evt: any) => {
-      if(aborted && abort_controller)
-        abort_controller.abort()
-
-      current.value.forEach((item) => {
-        if(item.file === cur) {
-          item.current = evt.loaded
-          item.total = evt.total
-        }
-      })
-
-      do_emit('upload:progress', progress.value, current.value, queue.value)
-    }
-  } as any
-
-  if(abort_controller)
-    options.signal = abort_controller.signal
-
-  axios(options)
-    .then((res) => {
-      current.value = current.value.filter((item) => {
-        return item.file !== cur
-      })
-
-      do_emit('upload:progress', progress.value, current.value, queue.value)
-
-      progress.value.push(res.data)
-      results.value.push(res.data)
-
-      cur_jobs -= 1
-
-      if(aborted) {
-        if(props.autoReset || props.autoReset === '') {
-          state.value = State.s_idle
-          files = [] // # XXX can drop w/o reset
-          do_emit('files:clear', [])
-        }
-        else
-          if(state.value != State.s_aborted) {
-            state.value = State.s_aborted
-            do_emit('upload:aborted', progress.value, current.value, queue.value, errors.value)
-          }
-
-        return
-      }
-
-      if(cur_file == files.length && !cur_jobs) {
-        files = [] // XXX can drop w/o reset
-        do_emit('files:clear', [])
-
-        if(errors.value.length)
-          if(props.keepGoing || props.keepGoing === '') {
-            state.value = State.s_with_errors
-            do_emit('upload:done', results.value, errors.value)
-          }
-          else
-            state.value = State.s_error
-        else
-          if(props.autoReset || props.autoReset === '')
-            state.value = State.s_idle
-          else {
-            state.value = State.s_done
-            do_emit('upload:done', results.value, errors.value)
-          }
-
-        return
-      }
-
-      while(cur_jobs < props.maxJobs && cur_file < files.length)
-        start_job()
-    })
-    .catch((err) => {
-      if(aborted && err.name == 'CanceledError') {
-        state.value = State.s_aborted
-        return err
-      }
-
-      if(props.keepGoing || props.keepGoing === '') {
-        current.value = current.value.filter((item) => {
-          return item.file !== cur
-        })
-
-        errors.value.push({
-          error: err.response,
-          file: cur
-        })
-
-        do_emit('upload:error', err.response)
-
-        cur_jobs -= 1
-
-        if(cur_file == files.length && !cur_jobs) {
-          state.value = State.s_with_errors
-          do_emit('upload:done', results.value, errors.value)
-          return
-        }
-
-        while(cur_jobs < props.maxJobs && cur_file < files.length)
-          start_job()
-      }
-      else {
-        state.value = State.s_error
-        error.value = err.response
-        do_emit('upload:error', err.response)
-      }
-    })
-}
-
-const can_pick = computed({
-  get: () => {
-    return state.value == State.s_idle || state.value == State.s_wait
-  },
-  set: () => {
-  }
-})
-
-const can_start = computed({
-  get: () => {
-    return state.value == State.s_wait
-  },
-  set: () => {
-  }
-})
-
-const can_abort = computed({
-  get: () => {
-    return state.value == State.s_uploading
-  },
-  set: () => {
-  }
-})
-
-const can_reset = computed({
-  get: () => {
-    return state.value != State.s_idle
-  },
-  set: () => {
-  }
-})
-
-const on_state_changed = (): void => {
-  do_emit('update:canPick', can_pick.value)
-  do_emit('update:canStart', can_start.value)
-  do_emit('update:canAbort', can_abort.value)
-  do_emit('update:canReset', can_reset.value)
-
-  do_emit('update:state', state.value)
-}
-
-watch(() => { return state.value }, (t: State, f: State) => { nextTick(on_state_changed) } )
-
-nextTick(on_state_changed)
-
-defineExpose({
-  reset: reset_cb,
-  start: start_cb,
-  pick: pick_cb,
-  abort: abort_cb,
-
-  can_pick,
-  can_start,
-  can_abort,
-  can_reset
-})
-
-</script>
-
-<template bindings="">
-  <component is='props.tag' @drop.prevent='drop' @dragover.prevent='drag_over' @dragleave.prevent='drag_out'>
-    <slot name='idle' v-if='state == "idle"' v-bind:pick='pick_cb' />
-    <slot name='hover' v-if='state == "hover"' />
-    <slot name='wait' v-if='state == "wait"' v-bind:start='start_cb'
-          v-bind:queue='queue' v-bind:reset='reset_cb' />
-    <slot name='uploading' v-if='state == "uploading"'
-          v-bind:progress='progress'
-          v-bind:current='current'
-          v-bind:queue='queue'
-          v-bind:errors='errors'
-          v-bind:abort='abort_cb' />
-    <slot name='done' v-if='state == "done"' v-bind:result='results' v-bind:reset='reset_cb' />
-    <slot name='error' v-if='state == "error"' v-bind:error='error' v-bind:reset='reset_cb' />
-    <slot name='aborted' v-if='state == "aborted"' v-bind:result='results' v-bind:reset='reset_cb' />
-    <slot name='with-errors' v-if='state == "with_errors"' v-bind:result='results' v-bind:errors='errors'
-          v-bind:reset='reset_cb' />
-  </component>
-</template>
-
Index: src/UploadZone0.vue
===================================================================
--- src/UploadZone0.vue	(revision 54176ee8abe2033da05f8e84d73bea143054abc9)
+++ src/UploadZone0.vue	(revision 54176ee8abe2033da05f8e84d73bea143054abc9)
@@ -0,0 +1,299 @@
+<script setup lang="ts">
+
+import { ref, defineProps, withDefaults, defineEmits, getCurrentInstance, defineExpose } from 'vue'
+
+import $ from 'jQuery'
+
+interface Props {
+  target: string
+  tag?: string
+  maxJobs?: number
+  autoStart?: boolean | string
+  autoReset?: boolean | string
+  keepGoing?: boolean | string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  maxJobs: 3,
+  autoStart: false,
+  autoReset: false,
+  keepGoing: false,
+  tag: 'div'
+})
+
+const emit = defineEmits<{
+  (e: 'drag:over', evt: any): void
+  (e: 'drag:out', evt: any): void
+  (e: 'drop', evt: any, files: any[]): void
+  (e: 'upload:progress', vec: any[]): void
+  (e: 'upload:done', vec: any[]): void
+}>()
+
+const inst = getCurrentInstance()
+
+enum Mode {
+  m_idle = 'idle',
+  m_hover = 'hover',
+  m_wait = 'wait',
+  m_uploading = 'uploading',
+  m_done = 'done',
+  m_error = 'error',
+  m_with_errors = 'with_errors', // XXX av when keep-going set
+  m_aborted = 'aborted'
+}
+
+const mode = ref('idle' as Mode)
+
+const queue = ref([] as any[])
+const progress = ref({} as any)
+const results = ref([] as any[])
+const current = ref([] as any[])
+const error = ref({} as any)
+
+const errors = ref([] as any)
+
+let files: any[] = []
+
+let cur_jobs = 0
+let cur_file = 0
+
+let aborted = false
+
+const extract_files = (evt: any): any[] => {
+  const files = []
+
+  if(evt.dataTransfer && evt.dataTransfer.items !== undefined) {
+    for(const item of evt.dataTransfer.items)
+      if(item.kind == 'file')
+        files.push(item.getAsFile())
+  }
+  else
+    if(evt.dataTransfer)
+      for(const file of evt.dataTransfer.files)
+        files.push(file)
+    else // XXX elem click
+      for(const file of evt.target.files)
+        files.push(file)
+
+  return files
+}
+
+const drop = (evt: any): void => {
+  if(mode.value != Mode.m_hover && mode.value != Mode.m_idle && mode.value != Mode.m_wait)
+    return
+
+  mode.value = Mode.m_wait
+  files = files.concat(extract_files(evt))
+  emit('drop', evt, files)
+  queue.value = files
+
+  if(props.autoStart || props.autoStart === '')
+    start_cb()
+}
+
+const drag_over = (evt: any): void => {
+  if(mode.value != Mode.m_idle)
+    return
+
+  mode.value = Mode.m_hover
+  emit('drag:over', evt)
+}
+
+const drag_out = (evt: any): void => {
+  if(mode.value != Mode.m_hover)
+    return
+
+  mode.value = Mode.m_idle
+  //console.log('out', evt)
+  emit('drag:out', evt)
+}
+
+const start_cb = (): void => {
+  if(mode.value != Mode.m_wait)
+    return
+
+  aborted = false
+
+  mode.value = Mode.m_uploading
+
+  results.value = []
+  progress.value = []
+  current.value = []
+  errors.value = []
+
+  error.value = {}
+  cur_file = 0
+  cur_jobs = 0
+
+  while(cur_jobs < props.maxJobs && cur_file < files.length)
+    start_job()
+}
+
+const reset_cb = (): void => {
+  mode.value = Mode.m_idle
+  files = []
+}
+
+const pick_cb = (): void => {
+  if(mode.value != Mode.m_idle && Mode.value != Mode.m_wait)
+    return
+
+  const inp = document.createElement('input')
+  inp.setAttribute('type', 'file')
+  inp.setAttribute('multiple', 'true')
+  inp.addEventListener('input', (evt: any): void => {
+    drop(evt)
+  })
+  inp.click()
+}
+
+const abort_cb = (): void => {
+  if(mode.value != Mode.m_uploading)
+    return
+
+  aborted = true
+}
+
+const start_job = () => {
+  cur_jobs += 1
+
+  const fd = new FormData()
+  const cur = files[cur_file]
+  fd.append('file', files[cur_file])
+
+  current.value.push({
+    file: files[cur_file],
+    current: 0,
+    total: files[cur_file].size
+  })
+
+  queue.value = queue.value.filter(queue.value, (file) => {
+    return file !== cur
+  })
+
+  cur_file += 1
+
+  $.ajax({
+    url: props.target,
+    type: 'POST',
+    data: fd,
+    contentType: false,
+    processData: false,
+    xhr: (): any => {
+      const xhr = new window.XMLHttpRequest()
+
+      xhr.upload.addEventListener('progress', (evt): void => {
+        queue.value.forEach(current.value, (item) => {
+          if(item.file === cur) {
+            item.current = evt.loaded
+            item.total = evt.total
+          }
+        })
+      })
+
+      return xhr
+    }
+  })
+    .then((res: any) => {
+      current.value = queue.value.filter(current.value, (item) => {
+        return item.file !== cur
+      })
+
+      progress.value.push(res)
+      results.value.push(res)
+
+      emit('upload:progress', progress.value)
+
+      cur_jobs -= 1
+
+      if(aborted) {
+        if(props.autoReset || props.autoReset === '') {
+          mode.value = Mode.m_idle
+          files = [] // # XXX can drop w/o reset
+        }
+        else
+          mode.value = Mode.m_aborted
+
+        return
+      }
+
+      if(cur_file == files.length && !cur_jobs) {
+        emit('upload:done', results.value)
+
+        files = [] // XXX can drop w/o reset
+
+        if(errors.value.length) {
+          if(props.keepGoing || props.keepGoing === '')
+            mode.value = Mode.m_with_errors
+          else
+            mode.value = Mode.m_error
+        }
+        else
+          if(props.autoReset || props.autoReset === '')
+            mode.value = Mode.m_idle
+          else
+            mode.value = Mode.m_done
+
+        return
+      }
+
+      while(cur_jobs < props.maxJobs && cur_file < files.length)
+        start_job()
+    })
+    .catch((err: any) => {
+      if(props.keepGoing || props.keepGoing === '') {
+        current.value = current.value.filter(current.value, (item) => {
+          return item.file !== cur
+        })
+
+        errors.value.push({
+          error: err,
+          file: cur
+        })
+
+        cur_jobs -= 1
+
+        if(cur_file == files.length && !cur_jobs) {
+          mode.value = Mode.m_with_errors
+          return
+        }
+
+        while(cur_jobs < props.maxJobs && cur_file < files.length)
+          start_job()
+      }
+      else {
+        mode.value = Mode.m_error
+        error.value = err
+      }
+    })
+}
+
+defineExpose({
+  reset: reset_cb,
+  start: start_cb,
+  pick: pick_cb,
+  abort: abort_cb
+})
+
+</script>
+
+<template bindings="">
+  <component is='props.tag' @drop.prevent='drop' @dragover.prevent='drag_over' @dragleave.prevent='drag_out'>
+    <slot name='idle' v-if='mode == "idle"' v-bind:pick='pick_cb' />
+    <slot name='hover' v-if='mode == "hover"' />
+    <slot name='wait' v-if='mode == "wait"' v-bind:start='start_cb'
+          v-bind:queue='queue' v-bind:reset='reset_cb' />
+    <slot name='uploading' v-if='mode == "uploading"'
+          v-bind:progress='progress'
+          v-bind:current='current'
+          v-bind:queue='queue'
+          v-bind:errors='errors'
+          v-bind:abort='abort_cb' />
+    <slot name='done' v-if='mode == "done"' v-bind:result='results' v-bind:reset='reset_cb' />
+    <slot name='error' v-if='mode == "error"' v-bind:error='error' v-bind:reset='reset_cb' />
+    <slot name='aborted' v-if='mode == "aborted"' v-bind:result='results' v-bind:reset='reset_cb' />
+    <slot name='with-errors' v-if='mode == "with_errors"' v-bind:result='results' v-bind:errors='errors'
+          v-bind:reset='reset_cb' />
+  </component>
+</template>
+
