vue-chunk-uploader 1.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 akbarjoody
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # vue-chunk-uploader
2
+
3
+ A Vue 3 chunked file uploader compatible with the [resumable.js](https://github.com/23/resumable.js) protocol.
4
+
5
+ The upload core is UI-agnostic — you choose which adapter to use.
6
+
7
+ ## Choose a UI
8
+
9
+ | Import | UI | Required dependency |
10
+ |--------|----|---------------------|
11
+ | `vue-chunk-uploader/quasar` | Quasar | `quasar` |
12
+ | `vue-chunk-uploader/vuetify` | Vuetify 3 | `vuetify` |
13
+ | `vue-chunk-uploader/native` | Plain HTML | — |
14
+ | `vue-chunk-uploader` | Core only (no UI) | — |
15
+
16
+ > **Note about MUI:** Material UI is for React. In the Vue ecosystem, the closest equivalent is **Vuetify**. For other UIs (Element Plus, Naive UI, MUI/React, etc.), use `native` or the `useChunkUpload` composable and build your own UI.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install vue-chunk-uploader axios vue
22
+ ```
23
+
24
+ Depending on the UI:
25
+
26
+ ```bash
27
+ # Quasar
28
+ npm install quasar
29
+
30
+ # or Vuetify
31
+ npm install vuetify
32
+ ```
33
+
34
+ ## Usage — Quasar
35
+
36
+ ```js
37
+ import { createApp } from 'vue'
38
+ import { Quasar } from 'quasar'
39
+ import VueChunkUploader from 'vue-chunk-uploader/quasar'
40
+ import axios from 'axios'
41
+
42
+ const api = axios.create({ baseURL: 'https://api.example.com', withCredentials: true })
43
+
44
+ const app = createApp(App)
45
+ app.use(Quasar)
46
+ app.use(VueChunkUploader, { httpClient: api })
47
+ app.mount('#app')
48
+ ```
49
+
50
+ ```vue
51
+ <template>
52
+ <ChunkUploader
53
+ v-model="file"
54
+ url="/api/document/chunk/upload"
55
+ filled
56
+ bottom-slots
57
+ label="File"
58
+ @onSuccess="onSuccess"
59
+ @onError="onError"
60
+ />
61
+ </template>
62
+
63
+ <script setup>
64
+ import { ref } from 'vue'
65
+ // or: import { ChunkUploader } from 'vue-chunk-uploader/quasar'
66
+
67
+ const file = ref(null)
68
+ </script>
69
+ ```
70
+
71
+ ## Usage — Vuetify
72
+
73
+ ```js
74
+ import VueChunkUploader from 'vue-chunk-uploader/vuetify'
75
+
76
+ app.use(VueChunkUploader, { httpClient: api })
77
+ ```
78
+
79
+ ```vue
80
+ <template>
81
+ <ChunkUploader
82
+ v-model="file"
83
+ url="/api/document/chunk/upload"
84
+ label="File"
85
+ variant="outlined"
86
+ @onSuccess="onSuccess"
87
+ />
88
+ </template>
89
+ ```
90
+
91
+ ## Usage — Native (no UI framework)
92
+
93
+ ```js
94
+ import VueChunkUploader from 'vue-chunk-uploader/native'
95
+ import 'vue-chunk-uploader/style.css'
96
+
97
+ app.use(VueChunkUploader, { httpClient: api })
98
+ ```
99
+
100
+ Or build a custom UI with the composable:
101
+
102
+ ```vue
103
+ <script setup>
104
+ import { ref } from 'vue'
105
+ import { useChunkUpload } from 'vue-chunk-uploader'
106
+
107
+ const file = ref(null)
108
+ const props = {
109
+ modelValue: file,
110
+ url: '/api/upload',
111
+ autoUpload: true,
112
+ fields: {},
113
+ }
114
+ const emit = (event, payload) => {
115
+ if (event === 'update:modelValue') file.value = payload
116
+ }
117
+
118
+ const { progress, hasError, uploadFile, clear } = useChunkUpload(props, emit)
119
+ </script>
120
+ ```
121
+
122
+ ## Manual upload (`autoUpload=false`)
123
+
124
+ ```vue
125
+ <template>
126
+ <ChunkUploader
127
+ ref="uploader"
128
+ v-model="file"
129
+ url="/api/document/chunk/upload"
130
+ :auto-upload="false"
131
+ />
132
+ <button @click="submit">Upload</button>
133
+ </template>
134
+
135
+ <script setup>
136
+ import { ref } from 'vue'
137
+
138
+ const file = ref(null)
139
+ const uploader = ref(null)
140
+
141
+ async function submit() {
142
+ // Extra fields are sent only with the last chunk
143
+ await uploader.value.uploadFile({ document_id: 123 })
144
+ }
145
+ </script>
146
+ ```
147
+
148
+ ## Core only (no UI)
149
+
150
+ ```js
151
+ import { chunk, setDefaultHttpClient } from 'vue-chunk-uploader'
152
+ import axios from 'axios'
153
+
154
+ setDefaultHttpClient(axios.create({ baseURL: '/api' }))
155
+
156
+ await chunk(
157
+ '/document/chunk/upload',
158
+ file,
159
+ { document_id: 1 },
160
+ (percent) => console.log(percent),
161
+ (err) => console.error(err),
162
+ (res) => console.log(res),
163
+ { chunkSize: 1024 * 1024 },
164
+ )
165
+ ```
166
+
167
+ ## Props
168
+
169
+ | Prop | Type | Default | Description |
170
+ |------|------|---------|-------------|
171
+ | `modelValue` / `v-model` | `File` | `null` | Selected file |
172
+ | `url` | `string` | — | Upload endpoint |
173
+ | `autoUpload` | `boolean` | `true` | Upload automatically after selection |
174
+ | `fields` | `object` | `{}` | Extra fields (sent with the last chunk) |
175
+ | `httpClient` | `AxiosInstance` | default / axios | HTTP client |
176
+ | `chunkSize` | `number` | `1MB` | Chunk size |
177
+ | `maxRequestsPerMinute` | `number` | `80` | Client-side rate limit |
178
+ | `rateLimitWindowMs` | `number` | `60000` | Rate-limit window |
179
+
180
+ UI-specific attrs are forwarded as well (e.g. `q-file` or `v-file-input` props).
181
+
182
+ ## Events
183
+
184
+ | Event | Payload |
185
+ |-------|---------|
186
+ | `update:modelValue` | `File \| null` |
187
+ | `onSelectFile` | uploader module |
188
+ | `onProgress` | `percent: number` |
189
+ | `onError` | `error` |
190
+ | `onSuccess` | `response` |
191
+
192
+ ## Exposed methods
193
+
194
+ | Method | Description |
195
+ |--------|-------------|
196
+ | `uploadFile(extraFormData?)` | Starts the upload; returns a Promise |
197
+
198
+ ## License
199
+
200
+ MIT
@@ -0,0 +1,183 @@
1
+ import { ref as z, watch as F, unref as d } from "vue";
2
+ import L from "axios";
3
+ let y = null;
4
+ function M(t) {
5
+ y = t;
6
+ }
7
+ function _(t) {
8
+ return t || y || L;
9
+ }
10
+ const R = 1024 * 1024, W = 80, g = 6e4, p = [], q = (t) => new Promise((e) => setTimeout(e, t)), U = async (t, e) => {
11
+ const n = Date.now();
12
+ for (; p.length && n - p[0] > e; )
13
+ p.shift();
14
+ if (p.length >= t) {
15
+ const a = p[0], r = e - (n - a) + 5;
16
+ return await q(r), U(t, e);
17
+ }
18
+ p.push(Date.now());
19
+ }, E = async (t, e, n, a) => {
20
+ const {
21
+ chunkSize: r,
22
+ chunkNumber: i,
23
+ blockCount: l,
24
+ identifier: o,
25
+ httpClient: m,
26
+ maxRequestsPerMinute: h,
27
+ rateLimitWindowMs: b,
28
+ onProgress: f,
29
+ onSuccess: k,
30
+ onError: u
31
+ } = a, C = i * r, S = Math.min(e.size, C + r);
32
+ let w = r;
33
+ i + 1 === l && (w = e.size - C);
34
+ const s = new FormData();
35
+ if (s.append("resumableChunkNumber", i + 1), s.append("resumableChunkSize", w), s.append("resumableCurrentChunkSize", w), s.append("resumableTotalSize", e.size), s.append("resumableType", e.type), s.append("resumableIdentifier", o), s.append("resumableFilename", e.name), s.append("resumableRelativePath", e.name), s.append("resumableTotalChunks", l), s.append("file", e.slice(C, S), e.name), Object.keys(n).length > 0 && i + 1 === l)
36
+ for (const [c, T] of Object.entries(n))
37
+ c !== "file" && s.append(c, T);
38
+ await U(h, b);
39
+ const N = _(m);
40
+ try {
41
+ const c = await N.post(t, s, {
42
+ headers: {
43
+ Accept: "application/json"
44
+ }
45
+ });
46
+ return f == null || f(parseInt(S / e.size * 100, 10), c), S === e.size ? (k == null || k(c), c) : E(t, e, n, {
47
+ ...a,
48
+ chunkNumber: i + 1
49
+ });
50
+ } catch (c) {
51
+ throw u == null || u(c), c;
52
+ }
53
+ };
54
+ function P(t, e, n = {}, a, r, i, l = {}) {
55
+ const o = l.chunkSize ?? R, m = Math.ceil(e.size / o) || 1, h = `${e.size}-${String(e.name).replace(/\./g, "")}`;
56
+ return E(t, e, n, {
57
+ blockCount: m,
58
+ identifier: h,
59
+ chunkNumber: 0,
60
+ chunkSize: o,
61
+ httpClient: l.httpClient,
62
+ maxRequestsPerMinute: l.maxRequestsPerMinute ?? W,
63
+ rateLimitWindowMs: l.rateLimitWindowMs ?? g,
64
+ onProgress: a,
65
+ onError: r,
66
+ onSuccess: i
67
+ });
68
+ }
69
+ const v = {
70
+ chunk: P,
71
+ setDefaultHttpClient: M
72
+ }, I = {
73
+ modelValue: {
74
+ type: [Object, File, Blob],
75
+ default: null
76
+ },
77
+ autoUpload: {
78
+ type: Boolean,
79
+ default: !0
80
+ },
81
+ url: {
82
+ type: String,
83
+ required: !0
84
+ },
85
+ fields: {
86
+ type: Object,
87
+ default: () => ({})
88
+ },
89
+ httpClient: {
90
+ type: Object,
91
+ default: null
92
+ },
93
+ chunkSize: {
94
+ type: Number,
95
+ default: void 0
96
+ },
97
+ maxRequestsPerMinute: {
98
+ type: Number,
99
+ default: void 0
100
+ },
101
+ rateLimitWindowMs: {
102
+ type: Number,
103
+ default: void 0
104
+ }
105
+ }, j = [
106
+ "update:modelValue",
107
+ "onSelectFile",
108
+ "onSendFile",
109
+ "onProgress",
110
+ "onError",
111
+ "onSuccess"
112
+ ];
113
+ function A(t, e) {
114
+ const n = z(!1), a = z(0), r = () => {
115
+ n.value = !1, a.value = 0;
116
+ }, i = async (o = {}) => {
117
+ const m = d(t.modelValue);
118
+ if (!m)
119
+ throw new Error("No file selected");
120
+ const h = {
121
+ ...d(t.fields),
122
+ ...o
123
+ }, b = {
124
+ httpClient: d(t.httpClient) || void 0,
125
+ chunkSize: d(t.chunkSize),
126
+ maxRequestsPerMinute: d(t.maxRequestsPerMinute),
127
+ rateLimitWindowMs: d(t.rateLimitWindowMs)
128
+ };
129
+ return new Promise((f, k) => {
130
+ P(
131
+ d(t.url),
132
+ m,
133
+ h,
134
+ (u) => {
135
+ a.value = u, e("onProgress", u);
136
+ },
137
+ (u) => {
138
+ n.value = !0, e("onError", u), k(u);
139
+ },
140
+ (u) => {
141
+ e("onSuccess", u), f(u);
142
+ },
143
+ b
144
+ );
145
+ });
146
+ }, l = () => {
147
+ r(), e("update:modelValue", null);
148
+ };
149
+ return F(
150
+ () => d(t.modelValue),
151
+ async (o, m) => {
152
+ if (o !== m && (r(), e("onSelectFile", v), d(t.autoUpload) && o))
153
+ try {
154
+ await i();
155
+ } catch {
156
+ }
157
+ }
158
+ ), {
159
+ hasError: n,
160
+ progress: a,
161
+ resetState: r,
162
+ uploadFile: i,
163
+ clear: l
164
+ };
165
+ }
166
+ function O(t) {
167
+ return {
168
+ install(e, n = {}) {
169
+ n.httpClient && M(n.httpClient);
170
+ const a = n.componentName || "ChunkUploader";
171
+ e.component(a, t);
172
+ }
173
+ };
174
+ }
175
+ export {
176
+ P as a,
177
+ v as b,
178
+ O as c,
179
+ j as d,
180
+ I as e,
181
+ M as s,
182
+ A as u
183
+ };
@@ -0,0 +1 @@
1
+ "use strict";const s=require("vue"),F=require("axios");let w=null;function y(t){w=t}function L(t){return t||w||F}const _=1024*1024,R=80,q=6e4,h=[],g=t=>new Promise(e=>setTimeout(e,t)),M=async(t,e)=>{const n=Date.now();for(;h.length&&n-h[0]>e;)h.shift();if(h.length>=t){const u=h[0],i=e-(n-u)+5;return await g(i),M(t,e)}h.push(Date.now())},P=async(t,e,n,u)=>{const{chunkSize:i,chunkNumber:l,blockCount:o,identifier:c,httpClient:p,maxRequestsPerMinute:m,rateLimitWindowMs:C,onProgress:f,onSuccess:k,onError:a}=u,b=l*i,S=Math.min(e.size,b+i);let U=i;l+1===o&&(U=e.size-b);const r=new FormData;if(r.append("resumableChunkNumber",l+1),r.append("resumableChunkSize",U),r.append("resumableCurrentChunkSize",U),r.append("resumableTotalSize",e.size),r.append("resumableType",e.type),r.append("resumableIdentifier",c),r.append("resumableFilename",e.name),r.append("resumableRelativePath",e.name),r.append("resumableTotalChunks",o),r.append("file",e.slice(b,S),e.name),Object.keys(n).length>0&&l+1===o)for(const[d,T]of Object.entries(n))d!=="file"&&r.append(d,T);await M(m,C);const N=L(p);try{const d=await N.post(t,r,{headers:{Accept:"application/json"}});return f==null||f(parseInt(S/e.size*100,10),d),S===e.size?(k==null||k(d),d):P(t,e,n,{...u,chunkNumber:l+1})}catch(d){throw a==null||a(d),d}};function z(t,e,n={},u,i,l,o={}){const c=o.chunkSize??_,p=Math.ceil(e.size/c)||1,m=`${e.size}-${String(e.name).replace(/\./g,"")}`;return P(t,e,n,{blockCount:p,identifier:m,chunkNumber:0,chunkSize:c,httpClient:o.httpClient,maxRequestsPerMinute:o.maxRequestsPerMinute??R,rateLimitWindowMs:o.rateLimitWindowMs??q,onProgress:u,onError:i,onSuccess:l})}const E={chunk:z,setDefaultHttpClient:y},v={modelValue:{type:[Object,File,Blob],default:null},autoUpload:{type:Boolean,default:!0},url:{type:String,required:!0},fields:{type:Object,default:()=>({})},httpClient:{type:Object,default:null},chunkSize:{type:Number,default:void 0},maxRequestsPerMinute:{type:Number,default:void 0},rateLimitWindowMs:{type:Number,default:void 0}},D=["update:modelValue","onSelectFile","onSendFile","onProgress","onError","onSuccess"];function W(t,e){const n=s.ref(!1),u=s.ref(0),i=()=>{n.value=!1,u.value=0},l=async(c={})=>{const p=s.unref(t.modelValue);if(!p)throw new Error("No file selected");const m={...s.unref(t.fields),...c},C={httpClient:s.unref(t.httpClient)||void 0,chunkSize:s.unref(t.chunkSize),maxRequestsPerMinute:s.unref(t.maxRequestsPerMinute),rateLimitWindowMs:s.unref(t.rateLimitWindowMs)};return new Promise((f,k)=>{z(s.unref(t.url),p,m,a=>{u.value=a,e("onProgress",a)},a=>{n.value=!0,e("onError",a),k(a)},a=>{e("onSuccess",a),f(a)},C)})},o=()=>{i(),e("update:modelValue",null)};return s.watch(()=>s.unref(t.modelValue),async(c,p)=>{if(c!==p&&(i(),e("onSelectFile",E),s.unref(t.autoUpload)&&c))try{await l()}catch{}}),{hasError:n,progress:u,resetState:i,uploadFile:l,clear:o}}function I(t){return{install(e,n={}){n.httpClient&&y(n.httpClient);const u=n.componentName||"ChunkUploader";e.component(u,t)}}}exports.chunk=z;exports.chunkUploader=E;exports.chunkUploaderEmits=D;exports.chunkUploaderProps=v;exports.createChunkUploaderPlugin=I;exports.setDefaultHttpClient=y;exports.useChunkUpload=W;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./createPlugin-DoOjwZfd.cjs"),u={chunk:e.chunk,setDefaultHttpClient:e.setDefaultHttpClient,useChunkUpload:e.useChunkUpload,createChunkUploaderPlugin:e.createChunkUploaderPlugin};exports.chunk=e.chunk;exports.chunkUploader=e.chunkUploader;exports.chunkUploaderEmits=e.chunkUploaderEmits;exports.chunkUploaderProps=e.chunkUploaderProps;exports.createChunkUploaderPlugin=e.createChunkUploaderPlugin;exports.setDefaultHttpClient=e.setDefaultHttpClient;exports.useChunkUpload=e.useChunkUpload;exports.default=u;
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { c as a, u as e, s, a as o } from "./createPlugin-DZ7dugEN.js";
2
+ import { b as p, d as l, e as d } from "./createPlugin-DZ7dugEN.js";
3
+ const r = {
4
+ chunk: o,
5
+ setDefaultHttpClient: s,
6
+ useChunkUpload: e,
7
+ createChunkUploaderPlugin: a
8
+ };
9
+ export {
10
+ o as chunk,
11
+ p as chunkUploader,
12
+ l as chunkUploaderEmits,
13
+ d as chunkUploaderProps,
14
+ a as createChunkUploaderPlugin,
15
+ r as default,
16
+ s as setDefaultHttpClient,
17
+ e as useChunkUpload
18
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),t=require("./createPlugin-DoOjwZfd.cjs"),g=(o,l)=>{const a=o.__vccOpts||o;for(const[c,r]of l)a[c]=r;return a},C={class:"vue-chunk-uploader-native","data-ui":"native"},U={class:"vue-chunk-uploader-native__row"},V=["data-error","data-done"],y=["value"],B=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:t.chunkUploaderProps,emits:t.chunkUploaderEmits,setup(o,{expose:l,emit:a}){const c=o,r=a,s=e.ref(null),{hasError:h,progress:n,uploadFile:_,clear:f}=t.useChunkUpload(c,r),m=u=>{var p;const d=((p=u.target.files)==null?void 0:p[0])??null;r("update:modelValue",d)},v=()=>{s.value&&(s.value.value=""),f()};return l({uploadFile:_}),(u,d)=>(e.openBlock(),e.createElementBlock("div",C,[e.createElementVNode("div",U,[e.createElementVNode("input",e.mergeProps({ref_key:"inputRef",ref:s,type:"file"},u.$attrs,{onChange:m}),null,16),u.modelValue?(e.openBlock(),e.createElementBlock("button",{key:0,type:"button",class:"vue-chunk-uploader-native__clear",onClick:v}," × ")):e.createCommentVNode("",!0),e.createElementVNode("span",{class:"vue-chunk-uploader-native__badge","data-error":e.unref(h)||void 0,"data-done":e.unref(n)>=100||void 0},[e.unref(n)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(n))+"%",1)],64)):(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createTextVNode("✓")],64))],8,V)]),e.unref(n)>0&&e.unref(n)<100?(e.openBlock(),e.createElementBlock("progress",{key:0,class:"vue-chunk-uploader-native__progress",value:e.unref(n),max:"100"},null,8,y)):e.createCommentVNode("",!0)]))}}),i=g(B,[["__scopeId","data-v-0be4ee9c"]]),k=t.createChunkUploaderPlugin(i);exports.chunk=t.chunk;exports.chunkUploader=t.chunkUploader;exports.setDefaultHttpClient=t.setDefaultHttpClient;exports.useChunkUpload=t.useChunkUpload;exports.ChunkUploader=i;exports.VueChunkUploader=k;exports.default=k;
package/dist/native.js ADDED
@@ -0,0 +1,66 @@
1
+ import { ref as U, openBlock as a, createElementBlock as o, createElementVNode as p, mergeProps as b, createCommentVNode as h, unref as e, Fragment as k, createTextVNode as f, toDisplayString as V } from "vue";
2
+ import { d as x, e as E, u as N, c as P } from "./createPlugin-DZ7dugEN.js";
3
+ import { a as $, b as q, s as z } from "./createPlugin-DZ7dugEN.js";
4
+ const B = (n, l) => {
5
+ const r = n.__vccOpts || n;
6
+ for (const [c, s] of l)
7
+ r[c] = s;
8
+ return r;
9
+ }, D = {
10
+ class: "vue-chunk-uploader-native",
11
+ "data-ui": "native"
12
+ }, F = { class: "vue-chunk-uploader-native__row" }, O = ["data-error", "data-done"], R = ["value"], j = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
13
+ __name: "ChunkUploader",
14
+ props: E,
15
+ emits: x,
16
+ setup(n, { expose: l, emit: r }) {
17
+ const c = n, s = r, d = U(null), { hasError: v, progress: t, uploadFile: m, clear: g } = N(c, s), C = (u) => {
18
+ var _;
19
+ const i = ((_ = u.target.files) == null ? void 0 : _[0]) ?? null;
20
+ s("update:modelValue", i);
21
+ }, y = () => {
22
+ d.value && (d.value.value = ""), g();
23
+ };
24
+ return l({ uploadFile: m }), (u, i) => (a(), o("div", D, [
25
+ p("div", F, [
26
+ p("input", b({
27
+ ref_key: "inputRef",
28
+ ref: d,
29
+ type: "file"
30
+ }, u.$attrs, { onChange: C }), null, 16),
31
+ u.modelValue ? (a(), o("button", {
32
+ key: 0,
33
+ type: "button",
34
+ class: "vue-chunk-uploader-native__clear",
35
+ onClick: y
36
+ }, " × ")) : h("", !0),
37
+ p("span", {
38
+ class: "vue-chunk-uploader-native__badge",
39
+ "data-error": e(v) || void 0,
40
+ "data-done": e(t) >= 100 || void 0
41
+ }, [
42
+ e(t) < 100 ? (a(), o(k, { key: 0 }, [
43
+ f(V(e(t)) + "%", 1)
44
+ ], 64)) : (a(), o(k, { key: 1 }, [
45
+ f("✓")
46
+ ], 64))
47
+ ], 8, O)
48
+ ]),
49
+ e(t) > 0 && e(t) < 100 ? (a(), o("progress", {
50
+ key: 0,
51
+ class: "vue-chunk-uploader-native__progress",
52
+ value: e(t),
53
+ max: "100"
54
+ }, null, 8, R)) : h("", !0)
55
+ ]));
56
+ }
57
+ }), w = /* @__PURE__ */ B(j, [["__scopeId", "data-v-0be4ee9c"]]), I = P(w);
58
+ export {
59
+ w as ChunkUploader,
60
+ I as VueChunkUploader,
61
+ $ as chunk,
62
+ q as chunkUploader,
63
+ I as default,
64
+ z as setDefaultHttpClient,
65
+ N as useChunkUpload
66
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),o=require("./createPlugin-DoOjwZfd.cjs"),a=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:o.chunkUploaderProps,emits:o.chunkUploaderEmits,setup(p,{expose:i,emit:d}){const m=p,l=d,{hasError:u,progress:n,uploadFile:k,clear:C}=o.useChunkUpload(m,l);return i({uploadFile:k}),(s,t)=>{const h=e.resolveComponent("q-linear-progress"),r=e.resolveComponent("q-icon"),_=e.resolveComponent("q-btn"),f=e.resolveComponent("q-file");return e.openBlock(),e.createBlock(f,e.mergeProps(s.$attrs,{"model-value":s.modelValue,"onUpdate:modelValue":t[1]||(t[1]=v=>l("update:modelValue",v))}),{hint:e.withCtx(()=>[e.unref(n)>0&&e.unref(n)<100?(e.openBlock(),e.createBlock(h,{key:0,stripe:"",value:e.unref(n)/100,color:e.unref(u)?"negative":"positive",class:"q-mt-sm"},null,8,["value","color"])):e.createCommentVNode("",!0)]),prepend:e.withCtx(()=>[e.createVNode(r,{name:"cloud_upload",onClick:t[0]||(t[0]=e.withModifiers(()=>{},["stop"]))})]),append:e.withCtx(()=>[e.createVNode(_,{rounded:"",color:e.unref(u)?"negative":"positive"},{default:e.withCtx(()=>[e.unref(n)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(n))+" % ",1)],64)):(e.openBlock(),e.createBlock(r,{key:1,name:"done_all",color:"white",size:"24px"}))]),_:1},8,["color"]),e.createVNode(r,{name:"close",class:"cursor-pointer",onClick:e.unref(C)},null,8,["onClick"])]),_:1},16,["model-value"])}}}),c=o.createChunkUploaderPlugin(a);exports.chunk=o.chunk;exports.chunkUploader=o.chunkUploader;exports.setDefaultHttpClient=o.setDefaultHttpClient;exports.useChunkUpload=o.useChunkUpload;exports.ChunkUploader=a;exports.VueChunkUploader=c;exports.default=c;
package/dist/quasar.js ADDED
@@ -0,0 +1,68 @@
1
+ import { resolveComponent as n, openBlock as r, createBlock as a, mergeProps as U, withCtx as l, createVNode as p, unref as e, createElementBlock as V, Fragment as x, createTextVNode as b, toDisplayString as y, withModifiers as w, createCommentVNode as B } from "vue";
2
+ import { d as E, e as N, u as P, c as D } from "./createPlugin-DZ7dugEN.js";
3
+ import { a as O, b as S, s as T } from "./createPlugin-DZ7dugEN.js";
4
+ const F = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
5
+ __name: "ChunkUploader",
6
+ props: N,
7
+ emits: E,
8
+ setup(m, { expose: d, emit: k }) {
9
+ const _ = m, i = k, { hasError: c, progress: o, uploadFile: f, clear: h } = P(_, i);
10
+ return d({ uploadFile: f }), (u, t) => {
11
+ const C = n("q-linear-progress"), s = n("q-icon"), g = n("q-btn"), q = n("q-file");
12
+ return r(), a(q, U(u.$attrs, {
13
+ "model-value": u.modelValue,
14
+ "onUpdate:modelValue": t[1] || (t[1] = (v) => i("update:modelValue", v))
15
+ }), {
16
+ hint: l(() => [
17
+ e(o) > 0 && e(o) < 100 ? (r(), a(C, {
18
+ key: 0,
19
+ stripe: "",
20
+ value: e(o) / 100,
21
+ color: e(c) ? "negative" : "positive",
22
+ class: "q-mt-sm"
23
+ }, null, 8, ["value", "color"])) : B("", !0)
24
+ ]),
25
+ prepend: l(() => [
26
+ p(s, {
27
+ name: "cloud_upload",
28
+ onClick: t[0] || (t[0] = w(() => {
29
+ }, ["stop"]))
30
+ })
31
+ ]),
32
+ append: l(() => [
33
+ p(g, {
34
+ rounded: "",
35
+ color: e(c) ? "negative" : "positive"
36
+ }, {
37
+ default: l(() => [
38
+ e(o) < 100 ? (r(), V(x, { key: 0 }, [
39
+ b(y(e(o)) + " % ", 1)
40
+ ], 64)) : (r(), a(s, {
41
+ key: 1,
42
+ name: "done_all",
43
+ color: "white",
44
+ size: "24px"
45
+ }))
46
+ ]),
47
+ _: 1
48
+ }, 8, ["color"]),
49
+ p(s, {
50
+ name: "close",
51
+ class: "cursor-pointer",
52
+ onClick: e(h)
53
+ }, null, 8, ["onClick"])
54
+ ]),
55
+ _: 1
56
+ }, 16, ["model-value"]);
57
+ };
58
+ }
59
+ }), A = D(F);
60
+ export {
61
+ F as ChunkUploader,
62
+ A as VueChunkUploader,
63
+ O as chunk,
64
+ S as chunkUploader,
65
+ A as default,
66
+ T as setDefaultHttpClient,
67
+ P as useChunkUpload
68
+ };
@@ -0,0 +1 @@
1
+ .vue-chunk-uploader-native__row[data-v-0be4ee9c]{display:flex;align-items:center;gap:.5rem}.vue-chunk-uploader-native__badge[data-v-0be4ee9c]{display:inline-flex;align-items:center;justify-content:center;min-width:3rem;padding:.15rem .5rem;border-radius:999px;font-size:.85rem;background:#16a34a;color:#fff}.vue-chunk-uploader-native__badge[data-error][data-v-0be4ee9c]{background:#dc2626}.vue-chunk-uploader-native__clear[data-v-0be4ee9c]{border:0;background:transparent;cursor:pointer;font-size:1.25rem;line-height:1}.vue-chunk-uploader-native__progress[data-v-0be4ee9c]{display:block;width:100%;margin-top:.5rem;height:.5rem}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),o=require("./createPlugin-DoOjwZfd.cjs"),U={class:"vue-chunk-uploader-vuetify"},u=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:o.chunkUploaderProps,emits:o.chunkUploaderEmits,setup(s,{expose:p,emit:i}){const d=s,t=i,{hasError:l,progress:r,uploadFile:m,clear:k}=o.useChunkUpload(d,t),h=n=>{const c=Array.isArray(n)?n[0]??null:n;t("update:modelValue",c)};return p({uploadFile:m}),(n,c)=>{const _=e.resolveComponent("v-icon"),f=e.resolveComponent("v-chip"),v=e.resolveComponent("v-file-input"),C=e.resolveComponent("v-progress-linear");return e.openBlock(),e.createElementBlock("div",U,[e.createVNode(v,e.mergeProps(n.$attrs,{"model-value":n.modelValue,error:e.unref(l)||void 0,clearable:"","prepend-icon":"mdi-cloud-upload","onUpdate:modelValue":h,"onClick:clear":e.unref(k)}),{append:e.withCtx(()=>[e.createVNode(f,{color:e.unref(l)?"error":e.unref(r)>=100?"success":"primary",size:"small",label:""},{default:e.withCtx(()=>[e.unref(r)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(r))+"%",1)],64)):(e.openBlock(),e.createBlock(_,{key:1,icon:"mdi-check-all",size:"small"}))]),_:1},8,["color"])]),_:1},16,["model-value","error","onClick:clear"]),e.unref(r)>0&&e.unref(r)<100?(e.openBlock(),e.createBlock(C,{key:0,class:"mt-1","model-value":e.unref(r),color:e.unref(l)?"error":"success",height:"6",rounded:"",striped:""},null,8,["model-value","color"])):e.createCommentVNode("",!0)])}}}),a=o.createChunkUploaderPlugin(u);exports.chunk=o.chunk;exports.chunkUploader=o.chunkUploader;exports.setDefaultHttpClient=o.setDefaultHttpClient;exports.useChunkUpload=o.useChunkUpload;exports.ChunkUploader=u;exports.VueChunkUploader=a;exports.default=a;
@@ -0,0 +1,65 @@
1
+ import { resolveComponent as l, openBlock as n, createElementBlock as c, createVNode as p, mergeProps as V, unref as e, withCtx as i, Fragment as b, createTextVNode as x, toDisplayString as A, createBlock as u, createCommentVNode as B } from "vue";
2
+ import { d as E, e as F, u as N, c as P } from "./createPlugin-DZ7dugEN.js";
3
+ import { a as T, b as $, s as q } from "./createPlugin-DZ7dugEN.js";
4
+ const z = { class: "vue-chunk-uploader-vuetify" }, D = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
5
+ __name: "ChunkUploader",
6
+ props: F,
7
+ emits: E,
8
+ setup(d, { expose: m, emit: _ }) {
9
+ const h = d, a = _, { hasError: t, progress: o, uploadFile: k, clear: f } = N(h, a), v = (r) => {
10
+ const s = Array.isArray(r) ? r[0] ?? null : r;
11
+ a("update:modelValue", s);
12
+ };
13
+ return m({ uploadFile: k }), (r, s) => {
14
+ const C = l("v-icon"), U = l("v-chip"), g = l("v-file-input"), y = l("v-progress-linear");
15
+ return n(), c("div", z, [
16
+ p(g, V(r.$attrs, {
17
+ "model-value": r.modelValue,
18
+ error: e(t) || void 0,
19
+ clearable: "",
20
+ "prepend-icon": "mdi-cloud-upload",
21
+ "onUpdate:modelValue": v,
22
+ "onClick:clear": e(f)
23
+ }), {
24
+ append: i(() => [
25
+ p(U, {
26
+ color: e(t) ? "error" : e(o) >= 100 ? "success" : "primary",
27
+ size: "small",
28
+ label: ""
29
+ }, {
30
+ default: i(() => [
31
+ e(o) < 100 ? (n(), c(b, { key: 0 }, [
32
+ x(A(e(o)) + "%", 1)
33
+ ], 64)) : (n(), u(C, {
34
+ key: 1,
35
+ icon: "mdi-check-all",
36
+ size: "small"
37
+ }))
38
+ ]),
39
+ _: 1
40
+ }, 8, ["color"])
41
+ ]),
42
+ _: 1
43
+ }, 16, ["model-value", "error", "onClick:clear"]),
44
+ e(o) > 0 && e(o) < 100 ? (n(), u(y, {
45
+ key: 0,
46
+ class: "mt-1",
47
+ "model-value": e(o),
48
+ color: e(t) ? "error" : "success",
49
+ height: "6",
50
+ rounded: "",
51
+ striped: ""
52
+ }, null, 8, ["model-value", "color"])) : B("", !0)
53
+ ]);
54
+ };
55
+ }
56
+ }), H = P(D);
57
+ export {
58
+ D as ChunkUploader,
59
+ H as VueChunkUploader,
60
+ T as chunk,
61
+ $ as chunkUploader,
62
+ H as default,
63
+ q as setDefaultHttpClient,
64
+ N as useChunkUpload
65
+ };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "vue-chunk-uploader",
3
+ "version": "1.1.0",
4
+ "description": "Vue 3 chunked/resumable file uploader with selectable UI adapters (Quasar, Vuetify, native)",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./quasar": {
14
+ "import": "./dist/quasar.js",
15
+ "require": "./dist/quasar.cjs"
16
+ },
17
+ "./vuetify": {
18
+ "import": "./dist/vuetify.js",
19
+ "require": "./dist/vuetify.cjs"
20
+ },
21
+ "./native": {
22
+ "import": "./dist/native.js",
23
+ "require": "./dist/native.cjs"
24
+ },
25
+ "./style.css": "./dist/vue-chunk-uploader.css"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "sideEffects": [
32
+ "**/*.css",
33
+ "**/*.vue"
34
+ ],
35
+ "scripts": {
36
+ "build": "vite build",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "keywords": [
40
+ "vue",
41
+ "vue3",
42
+ "quasar",
43
+ "vuetify",
44
+ "uploader",
45
+ "chunk",
46
+ "resumable",
47
+ "file-upload",
48
+ "chunked-upload"
49
+ ],
50
+ "author": "akbarjoody",
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/akbarjoody/vue-chunk-uploader.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/akbarjoody/vue-chunk-uploader/issues"
58
+ },
59
+ "homepage": "https://github.com/akbarjoody/vue-chunk-uploader#readme",
60
+ "peerDependencies": {
61
+ "axios": "^1.0.0",
62
+ "quasar": "^2.0.0",
63
+ "vue": "^3.2.0",
64
+ "vuetify": "^3.0.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "quasar": {
68
+ "optional": true
69
+ },
70
+ "vuetify": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@vitejs/plugin-vue": "^5.2.1",
76
+ "axios": "^1.9.0",
77
+ "quasar": "^2.18.5",
78
+ "vite": "^6.2.0",
79
+ "vue": "^3.5.13",
80
+ "vuetify": "^3.7.0"
81
+ }
82
+ }
@@ -0,0 +1,151 @@
1
+ import axios from 'axios'
2
+
3
+ /** @type {import('axios').AxiosInstance | null} */
4
+ let defaultHttpClient = null
5
+
6
+ /**
7
+ * Set a default HTTP client used when none is passed to `chunk()`.
8
+ * @param {import('axios').AxiosInstance} client
9
+ */
10
+ export function setDefaultHttpClient(client) {
11
+ defaultHttpClient = client
12
+ }
13
+
14
+ /**
15
+ * @returns {import('axios').AxiosInstance}
16
+ */
17
+ function resolveHttpClient(httpClient) {
18
+ if (httpClient) return httpClient
19
+ if (defaultHttpClient) return defaultHttpClient
20
+ return axios
21
+ }
22
+
23
+ export const DEFAULT_CHUNK_SIZE = 1024 * 1024
24
+ export const DEFAULT_MAX_REQUESTS_PER_MINUTE = 80
25
+ export const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000
26
+
27
+ const requestTimestamps = []
28
+
29
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
30
+
31
+ const waitForRateLimit = async (maxRequests, windowMs) => {
32
+ const now = Date.now()
33
+
34
+ while (requestTimestamps.length && now - requestTimestamps[0] > windowMs) {
35
+ requestTimestamps.shift()
36
+ }
37
+
38
+ if (requestTimestamps.length >= maxRequests) {
39
+ const earliest = requestTimestamps[0]
40
+ const waitMs = windowMs - (now - earliest) + 5
41
+ await sleep(waitMs)
42
+ return waitForRateLimit(maxRequests, windowMs)
43
+ }
44
+
45
+ requestTimestamps.push(Date.now())
46
+ }
47
+
48
+ const uploadChunk = async (endpoint, file, formData, options) => {
49
+ const {
50
+ chunkSize,
51
+ chunkNumber,
52
+ blockCount,
53
+ identifier,
54
+ httpClient,
55
+ maxRequestsPerMinute,
56
+ rateLimitWindowMs,
57
+ onProgress,
58
+ onSuccess,
59
+ onError,
60
+ } = options
61
+
62
+ const start = chunkNumber * chunkSize
63
+ const end = Math.min(file.size, start + chunkSize)
64
+
65
+ let currentChunkSize = chunkSize
66
+ if (chunkNumber + 1 === blockCount) {
67
+ currentChunkSize = file.size - start
68
+ }
69
+
70
+ const params = new FormData()
71
+ params.append('resumableChunkNumber', chunkNumber + 1)
72
+ params.append('resumableChunkSize', currentChunkSize)
73
+ params.append('resumableCurrentChunkSize', currentChunkSize)
74
+ params.append('resumableTotalSize', file.size)
75
+ params.append('resumableType', file.type)
76
+ params.append('resumableIdentifier', identifier)
77
+ params.append('resumableFilename', file.name)
78
+ params.append('resumableRelativePath', file.name)
79
+ params.append('resumableTotalChunks', blockCount)
80
+ params.append('file', file.slice(start, end), file.name)
81
+
82
+ if (Object.keys(formData).length > 0 && chunkNumber + 1 === blockCount) {
83
+ for (const [key, value] of Object.entries(formData)) {
84
+ if (key !== 'file') {
85
+ params.append(key, value)
86
+ }
87
+ }
88
+ }
89
+
90
+ await waitForRateLimit(maxRequestsPerMinute, rateLimitWindowMs)
91
+
92
+ const client = resolveHttpClient(httpClient)
93
+
94
+ try {
95
+ const res = await client.post(endpoint, params, {
96
+ headers: {
97
+ Accept: 'application/json',
98
+ },
99
+ })
100
+
101
+ onProgress?.(parseInt((end / file.size) * 100, 10), res)
102
+
103
+ if (end === file.size) {
104
+ onSuccess?.(res)
105
+ return res
106
+ }
107
+
108
+ return uploadChunk(endpoint, file, formData, {
109
+ ...options,
110
+ chunkNumber: chunkNumber + 1,
111
+ })
112
+ } catch (err) {
113
+ onError?.(err)
114
+ throw err
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Start a chunked upload.
120
+ */
121
+ export function chunk(
122
+ endpoint,
123
+ file,
124
+ formData = {},
125
+ onProgress,
126
+ onError,
127
+ onSuccess,
128
+ config = {},
129
+ ) {
130
+ const chunkSize = config.chunkSize ?? DEFAULT_CHUNK_SIZE
131
+ const blockCount = Math.ceil(file.size / chunkSize) || 1
132
+ const identifier = `${file.size}-${String(file.name).replace(/\./g, '')}`
133
+
134
+ return uploadChunk(endpoint, file, formData, {
135
+ blockCount,
136
+ identifier,
137
+ chunkNumber: 0,
138
+ chunkSize,
139
+ httpClient: config.httpClient,
140
+ maxRequestsPerMinute: config.maxRequestsPerMinute ?? DEFAULT_MAX_REQUESTS_PER_MINUTE,
141
+ rateLimitWindowMs: config.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS,
142
+ onProgress,
143
+ onError,
144
+ onSuccess,
145
+ })
146
+ }
147
+
148
+ export default {
149
+ chunk,
150
+ setDefaultHttpClient,
151
+ }
@@ -0,0 +1,18 @@
1
+ import { setDefaultHttpClient } from './chunkUploader.js'
2
+
3
+ /**
4
+ * Create a Vue plugin that registers a UI-specific ChunkUploader component.
5
+ * @param {import('vue').Component} Component
6
+ */
7
+ export function createChunkUploaderPlugin(Component) {
8
+ return {
9
+ install(app, options = {}) {
10
+ if (options.httpClient) {
11
+ setDefaultHttpClient(options.httpClient)
12
+ }
13
+
14
+ const name = options.componentName || 'ChunkUploader'
15
+ app.component(name, Component)
16
+ },
17
+ }
18
+ }
@@ -0,0 +1,133 @@
1
+ import { ref, unref, watch } from 'vue'
2
+ import chunkUploader, { chunk } from './chunkUploader.js'
3
+
4
+ export const chunkUploaderProps = {
5
+ modelValue: {
6
+ type: [Object, File, Blob],
7
+ default: null,
8
+ },
9
+ autoUpload: {
10
+ type: Boolean,
11
+ default: true,
12
+ },
13
+ url: {
14
+ type: String,
15
+ required: true,
16
+ },
17
+ fields: {
18
+ type: Object,
19
+ default: () => ({}),
20
+ },
21
+ httpClient: {
22
+ type: Object,
23
+ default: null,
24
+ },
25
+ chunkSize: {
26
+ type: Number,
27
+ default: undefined,
28
+ },
29
+ maxRequestsPerMinute: {
30
+ type: Number,
31
+ default: undefined,
32
+ },
33
+ rateLimitWindowMs: {
34
+ type: Number,
35
+ default: undefined,
36
+ },
37
+ }
38
+
39
+ export const chunkUploaderEmits = [
40
+ 'update:modelValue',
41
+ 'onSelectFile',
42
+ 'onSendFile',
43
+ 'onProgress',
44
+ 'onError',
45
+ 'onSuccess',
46
+ ]
47
+
48
+ /**
49
+ * Shared upload state/logic for all UI adapters.
50
+ * @param {Record<string, any>} props
51
+ * @param {(event: string, ...args: any[]) => void} emit
52
+ */
53
+ export function useChunkUpload(props, emit) {
54
+ const hasError = ref(false)
55
+ const progress = ref(0)
56
+
57
+ const resetState = () => {
58
+ hasError.value = false
59
+ progress.value = 0
60
+ }
61
+
62
+ const uploadFile = async (extraFormData = {}) => {
63
+ const file = unref(props.modelValue)
64
+ if (!file) {
65
+ throw new Error('No file selected')
66
+ }
67
+
68
+ const formData = {
69
+ ...unref(props.fields),
70
+ ...extraFormData,
71
+ }
72
+
73
+ const config = {
74
+ httpClient: unref(props.httpClient) || undefined,
75
+ chunkSize: unref(props.chunkSize),
76
+ maxRequestsPerMinute: unref(props.maxRequestsPerMinute),
77
+ rateLimitWindowMs: unref(props.rateLimitWindowMs),
78
+ }
79
+
80
+ return new Promise((resolve, reject) => {
81
+ chunk(
82
+ unref(props.url),
83
+ file,
84
+ formData,
85
+ (percent) => {
86
+ progress.value = percent
87
+ emit('onProgress', percent)
88
+ },
89
+ (err) => {
90
+ hasError.value = true
91
+ emit('onError', err)
92
+ reject(err)
93
+ },
94
+ (res) => {
95
+ emit('onSuccess', res)
96
+ resolve(res)
97
+ },
98
+ config,
99
+ )
100
+ })
101
+ }
102
+
103
+ const clear = () => {
104
+ resetState()
105
+ emit('update:modelValue', null)
106
+ }
107
+
108
+ watch(
109
+ () => unref(props.modelValue),
110
+ async (newVal, oldVal) => {
111
+ if (newVal === oldVal) return
112
+
113
+ resetState()
114
+ emit('onSelectFile', chunkUploader)
115
+
116
+ if (unref(props.autoUpload) && newVal) {
117
+ try {
118
+ await uploadFile()
119
+ } catch {
120
+ // error already emitted via onError
121
+ }
122
+ }
123
+ },
124
+ )
125
+
126
+ return {
127
+ hasError,
128
+ progress,
129
+ resetState,
130
+ uploadFile,
131
+ clear,
132
+ }
133
+ }
package/src/index.js ADDED
@@ -0,0 +1,32 @@
1
+ import chunkUploader, { chunk, setDefaultHttpClient } from './core/chunkUploader.js'
2
+ import {
3
+ useChunkUpload,
4
+ chunkUploaderProps,
5
+ chunkUploaderEmits,
6
+ } from './core/useChunkUpload.js'
7
+ import { createChunkUploaderPlugin } from './core/createPlugin.js'
8
+
9
+ /**
10
+ * Core package entry — no UI framework bundled.
11
+ *
12
+ * Choose a UI adapter explicitly:
13
+ * import ... from 'vue-chunk-uploader/quasar'
14
+ * import ... from 'vue-chunk-uploader/vuetify'
15
+ * import ... from 'vue-chunk-uploader/native'
16
+ */
17
+ export {
18
+ chunkUploader,
19
+ chunk,
20
+ setDefaultHttpClient,
21
+ useChunkUpload,
22
+ chunkUploaderProps,
23
+ chunkUploaderEmits,
24
+ createChunkUploaderPlugin,
25
+ }
26
+
27
+ export default {
28
+ chunk,
29
+ setDefaultHttpClient,
30
+ useChunkUpload,
31
+ createChunkUploaderPlugin,
32
+ }
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <div class="vue-chunk-uploader-native" data-ui="native">
3
+ <div class="vue-chunk-uploader-native__row">
4
+ <input
5
+ ref="inputRef"
6
+ type="file"
7
+ v-bind="$attrs"
8
+ @change="onChange"
9
+ />
10
+ <button
11
+ v-if="modelValue"
12
+ type="button"
13
+ class="vue-chunk-uploader-native__clear"
14
+ @click="onClear"
15
+ >
16
+ ×
17
+ </button>
18
+ <span
19
+ class="vue-chunk-uploader-native__badge"
20
+ :data-error="hasError || undefined"
21
+ :data-done="progress >= 100 || undefined"
22
+ >
23
+ <template v-if="progress < 100">{{ progress }}%</template>
24
+ <template v-else>✓</template>
25
+ </span>
26
+ </div>
27
+
28
+ <progress
29
+ v-if="progress > 0 && progress < 100"
30
+ class="vue-chunk-uploader-native__progress"
31
+ :value="progress"
32
+ max="100"
33
+ />
34
+ </div>
35
+ </template>
36
+
37
+ <script setup>
38
+ import { ref } from 'vue'
39
+ import {
40
+ chunkUploaderProps,
41
+ chunkUploaderEmits,
42
+ useChunkUpload,
43
+ } from '../../core/useChunkUpload.js'
44
+
45
+ defineOptions({ inheritAttrs: false })
46
+
47
+ const props = defineProps(chunkUploaderProps)
48
+ const emit = defineEmits(chunkUploaderEmits)
49
+
50
+ const inputRef = ref(null)
51
+ const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
52
+
53
+ const onChange = (event) => {
54
+ const file = event.target.files?.[0] ?? null
55
+ emit('update:modelValue', file)
56
+ }
57
+
58
+ const onClear = () => {
59
+ if (inputRef.value) {
60
+ inputRef.value.value = ''
61
+ }
62
+ clear()
63
+ }
64
+
65
+ defineExpose({ uploadFile })
66
+ </script>
67
+
68
+ <style scoped>
69
+ .vue-chunk-uploader-native__row {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 0.5rem;
73
+ }
74
+
75
+ .vue-chunk-uploader-native__badge {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ min-width: 3rem;
80
+ padding: 0.15rem 0.5rem;
81
+ border-radius: 999px;
82
+ font-size: 0.85rem;
83
+ background: #16a34a;
84
+ color: #fff;
85
+ }
86
+
87
+ .vue-chunk-uploader-native__badge[data-error] {
88
+ background: #dc2626;
89
+ }
90
+
91
+ .vue-chunk-uploader-native__clear {
92
+ border: 0;
93
+ background: transparent;
94
+ cursor: pointer;
95
+ font-size: 1.25rem;
96
+ line-height: 1;
97
+ }
98
+
99
+ .vue-chunk-uploader-native__progress {
100
+ display: block;
101
+ width: 100%;
102
+ margin-top: 0.5rem;
103
+ height: 0.5rem;
104
+ }
105
+ </style>
@@ -0,0 +1,17 @@
1
+ import ChunkUploader from './ChunkUploader.vue'
2
+ import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
3
+ import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
4
+ import { useChunkUpload } from '../../core/useChunkUpload.js'
5
+
6
+ const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
7
+
8
+ export {
9
+ ChunkUploader,
10
+ VueChunkUploader,
11
+ chunkUploader,
12
+ chunk,
13
+ setDefaultHttpClient,
14
+ useChunkUpload,
15
+ }
16
+
17
+ export default VueChunkUploader
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <q-file
3
+ v-bind="$attrs"
4
+ :model-value="modelValue"
5
+ @update:model-value="(value) => emit('update:modelValue', value)"
6
+ >
7
+ <template #hint>
8
+ <q-linear-progress
9
+ v-if="progress > 0 && progress < 100"
10
+ stripe
11
+ :value="progress / 100"
12
+ :color="hasError ? 'negative' : 'positive'"
13
+ class="q-mt-sm"
14
+ />
15
+ </template>
16
+
17
+ <template #prepend>
18
+ <q-icon name="cloud_upload" @click.stop />
19
+ </template>
20
+
21
+ <template #append>
22
+ <q-btn rounded :color="hasError ? 'negative' : 'positive'">
23
+ <template v-if="progress < 100">
24
+ {{ progress }} %
25
+ </template>
26
+ <template v-else>
27
+ <q-icon name="done_all" color="white" size="24px" />
28
+ </template>
29
+ </q-btn>
30
+ <q-icon name="close" class="cursor-pointer" @click="clear" />
31
+ </template>
32
+ </q-file>
33
+ </template>
34
+
35
+ <script setup>
36
+ import {
37
+ chunkUploaderProps,
38
+ chunkUploaderEmits,
39
+ useChunkUpload,
40
+ } from '../../core/useChunkUpload.js'
41
+
42
+ defineOptions({ inheritAttrs: false })
43
+
44
+ const props = defineProps(chunkUploaderProps)
45
+ const emit = defineEmits(chunkUploaderEmits)
46
+
47
+ const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
48
+
49
+ defineExpose({ uploadFile })
50
+ </script>
@@ -0,0 +1,17 @@
1
+ import ChunkUploader from './ChunkUploader.vue'
2
+ import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
3
+ import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
4
+ import { useChunkUpload } from '../../core/useChunkUpload.js'
5
+
6
+ const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
7
+
8
+ export {
9
+ ChunkUploader,
10
+ VueChunkUploader,
11
+ chunkUploader,
12
+ chunk,
13
+ setDefaultHttpClient,
14
+ useChunkUpload,
15
+ }
16
+
17
+ export default VueChunkUploader
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <div class="vue-chunk-uploader-vuetify">
3
+ <v-file-input
4
+ v-bind="$attrs"
5
+ :model-value="modelValue"
6
+ :error="hasError || undefined"
7
+ clearable
8
+ prepend-icon="mdi-cloud-upload"
9
+ @update:model-value="onFileUpdate"
10
+ @click:clear="clear"
11
+ >
12
+ <template #append>
13
+ <v-chip
14
+ :color="hasError ? 'error' : progress >= 100 ? 'success' : 'primary'"
15
+ size="small"
16
+ label
17
+ >
18
+ <template v-if="progress < 100">{{ progress }}%</template>
19
+ <v-icon v-else icon="mdi-check-all" size="small" />
20
+ </v-chip>
21
+ </template>
22
+ </v-file-input>
23
+
24
+ <v-progress-linear
25
+ v-if="progress > 0 && progress < 100"
26
+ class="mt-1"
27
+ :model-value="progress"
28
+ :color="hasError ? 'error' : 'success'"
29
+ height="6"
30
+ rounded
31
+ striped
32
+ />
33
+ </div>
34
+ </template>
35
+
36
+ <script setup>
37
+ import {
38
+ chunkUploaderProps,
39
+ chunkUploaderEmits,
40
+ useChunkUpload,
41
+ } from '../../core/useChunkUpload.js'
42
+
43
+ defineOptions({ inheritAttrs: false })
44
+
45
+ const props = defineProps(chunkUploaderProps)
46
+ const emit = defineEmits(chunkUploaderEmits)
47
+
48
+ const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
49
+
50
+ const onFileUpdate = (value) => {
51
+ // Vuetify may give File | File[] | null
52
+ const file = Array.isArray(value) ? value[0] ?? null : value
53
+ emit('update:modelValue', file)
54
+ }
55
+
56
+ defineExpose({ uploadFile })
57
+ </script>
@@ -0,0 +1,17 @@
1
+ import ChunkUploader from './ChunkUploader.vue'
2
+ import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
3
+ import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
4
+ import { useChunkUpload } from '../../core/useChunkUpload.js'
5
+
6
+ const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
7
+
8
+ export {
9
+ ChunkUploader,
10
+ VueChunkUploader,
11
+ chunkUploader,
12
+ chunk,
13
+ setDefaultHttpClient,
14
+ useChunkUpload,
15
+ }
16
+
17
+ export default VueChunkUploader