├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── README.md
├── dist
├── vue-clip.js
└── vue-clip.min.js
├── index.js
├── karma.conf.js
├── package.json
├── src
├── File.js
├── Uploader.js
└── components
│ └── Clip
│ └── index.js
├── test
├── index.js
├── integration
│ └── clip.spec.js
└── unit
│ ├── file.spec.js
│ └── uploader.spec.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "latest" ]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_size = 2
6 | indent_style = space
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['html'],
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | globals: {
8 | describe: true,
9 | it: true,
10 | beforeEach: true,
11 | before: true
12 | },
13 | extends: 'standard',
14 | rules: {
15 | 'arrow-parens': 0,
16 | 'generator-star-spacing': 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | node_modules
3 | .DS_Store
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 | sudo: false
5 | install:
6 | - npm install --no-optional
7 | notifications:
8 | slack:
9 | secure: m91zkX2cLVDRDMBAUnR1d+hbZqtSHXLkuPencHadhJ3C3wm53Box8U25co/goAmjnW5HNJ1SMSIg+DojtgDhqTbReSh5gSbU0uU8YaF8smbvmUv3b2Q8PRCA7f6hQiea+a8+jAb7BOvwh66dV4Al/1DJ2b4tCjPuVuxQ96Wll7Pnj1S7yW/Hb8fQlr9wc+INXUZOe8erFin+508r5h1L4Xv0N5ZmNw+Gqvn2kPJD8f/YBPpx0AeZdDssTL0IOcol1+cDtDzMw5PAkGnqwamtxhnsw+i8OW4avFt1GrRNlz3eci5Cb3NQGjHxJf+JIALvBeSqkOEFJIFGqwAXMctJ9q8/7XyXk7jVFUg5+0Z74HIkBwdtLwi/BTyXMZAgsnDjndmR9HsuBP7OSTJF5/V7HCJZAaO9shEgS8DwR78owv9Fr5er5m9IMI+EgSH3qtb8iuuQaPtflbk+cPD3nmYbDqmPwkSCXcXRfq3IxdcV9hkiaAw52AIqqhnAXJWZfL6+Ct32i2mtSaov9FYtp/G0xb4tjrUAsDUd/AGmMJNEBVoHtP7mKjrVQ35cEtFwJr/8SmZxGvOaJXPaLs43dhXKa2tAGl11wF02d+Rz1HhbOoq9pJvJuqkLAVvRdBHUJrB4/hnTta5B0W5pe3mIgLw3AmOpk+s/H4hAP4Hp0gOWlPA=
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # 1.0.0 (2017-01-03)
3 |
4 |
5 | ### Features
6 |
7 | * implement vue-clip component ([14012a8](https://github.com/thetutlage/vue-clip/commit/14012a8))
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | Vue clip is a minimalistic and hackable file uploader for VueJs. I wrote this plugin due to the absence of well written file uploaders with fine-grained controls.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | #### Features
13 | 1. Written in vanilla Javascript.
14 | 2. Weighs **17.9KB ( Minified and Gzip )**, **57KB ( Minified )**.
15 | 3. Hackable to the core with custom events.
16 | 4. Does not pollute DOM by adding unnecessary markup. Infact the component will create a single div element.
17 |
18 | ## Quick Intro
19 |
20 | VIDEO
21 |
22 | ## Setup
23 | You can make use of module by installing it from `npm` or directly using it from CDN.
24 |
25 | #### Npm
26 |
27 | ```bash
28 | npm i --save vue-clip
29 | ```
30 |
31 | ```javascript
32 | import Vue from 'vue'
33 | import VueClip from 'vue-clip'
34 |
35 | Vue.use(VueClip)
36 | ```
37 |
38 | #### Globally
39 |
40 | Also, you can reference the script file via [CDN]() which will add a global component called `vue-clip` to the Vue instance.
41 |
42 | ## Basic Usage
43 |
44 | ```html
45 |
46 |
47 |
48 |
49 |
Click or Drag and Drop files here upload
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ file.name }} {{ file.status }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
77 | ```
78 |
79 | ## Configuration Options
80 |
81 | | Option | Possible Values | Description |
82 | |--------|-----------------|-------------|
83 | | url | String, Function | Url to be used for uploading files. This can be a string or a function ( in case your URL is dynamic ) |
84 | | method | String, Function | Http method to be used. Defaults to `post`.
85 | | parallelUploads | Number | Number of files to be uploaded in parallel.
86 | | maxFilesize | Number, Object | The file size **in MB** to be allowed. Also, you can pass an object with `limit` and `error message`.|
87 | | paramName | String | Param name to be used for uploading file(s). Defaults to `file`.|
88 | | uploadMultiple | Boolean | Whether or not to upload multiple files.|
89 | | headers | Object | Http headers to be sent along each request.|
90 | | maxFiles | Number, Object | a maximum number of files to be uploaded. You can also pass an object with `limit` and `error message`.|
91 | | acceptedFiles | Array, Object | File types to be accepted. `['image/*', 'application/pdf']`.
92 | | accept | Function | A custom function to be used for validating each file upload. This method receives a `done` callback. In the case of any errors, you must call done with a single error argument.
93 |
94 | #### maxFilesize
95 | The `maxFilesize` option defines the size of the file to be checked for when uploading files.
96 |
97 | ```js
98 | {
99 | maxFilesize: 1 // 1mb
100 | }
101 |
102 | // or
103 |
104 | {
105 | maxFilesize: {
106 | limit: 1,
107 | message: '{{ filesize }} is greater than the {{ maxFilesize }}'
108 | }
109 | }
110 | ```
111 |
112 | #### maxFiles
113 |
114 | The `maxFiles` option defines the maximum number of files to be uploaded.
115 |
116 | ```js
117 | {
118 | maxFiles: 5
119 | }
120 |
121 | // or
122 |
123 | {
124 | maxFiles: {
125 | limit: 5,
126 | message: 'You can only upload a max of 5 files'
127 | }
128 | }
129 | ```
130 |
131 | #### acceptedFiles
132 |
133 | The `acceptedFiles` option defines the mime types of files to be accepted.
134 |
135 | ```js
136 | // as an array of mime types
137 |
138 | {
139 | acceptedFiles: ['image/*', 'application/pdf']
140 | }
141 |
142 | // as an object with an array of mime types
143 | // and a custom error message
144 |
145 | {
146 | acceptedFiles: {
147 | extensions: ['image/*'],
148 | message: 'You are uploading an invalid file'
149 | }
150 | }
151 |
152 | // as a plain, comma-delimited string
153 |
154 | {
155 | acceptedFiles: 'image/*,application/pdf'
156 | }
157 | ```
158 |
159 | #### accept
160 |
161 | The `accept` is a low-level method to run manual validations and return a formatted error string ( in the case of error).
162 |
163 | ```js
164 | {
165 | accept: function (file, done) {
166 | if (file.size > (1024 * 1024)) {
167 | done('File must be smaller than 1MB')
168 | return
169 | }
170 |
171 | done()
172 | }
173 | }
174 | ```
175 |
176 | ## Dragging
177 |
178 | The most common requirement is to know when a user `starts` and `stops` dragging a file so that you can add some visual feedback to the UI. The easiest way is to make use of [Scoped slots](https://vuejs.org/v2/guide/components.html#Scoped-Slots).
179 |
180 | ```html
181 |
182 |
183 |
184 |
185 |
186 |
Click or Drag and Drop files here upload
187 |
188 |
189 |
190 |
191 |
192 |
193 |
198 | ```
199 |
200 | ## Events
201 |
202 | You can make use of `vue-clip` without writing any javascript code, but if you want low-level control over the upload behavior, consider listening to special events.
203 |
204 | #### onInit(uploader)
205 | Called every time the `vue-clip` is initiated and binds to DOM.
206 |
207 | ```html
208 |
209 |
210 |
211 |
212 |
213 |
224 | ```
225 |
226 | #### onAddedFile(file)
227 | This event is invoked every time a new file gets uploaded. You can listen for this event, you want to have access to each file object within your own parent component.
228 |
229 | ```html
230 |
231 |
232 |
233 |
234 |
235 |
252 | ```
253 |
254 | #### onRemovedFile(file)
255 | This event is invoked every time the file has been removed. This is the nice place to make a request to your server for deleting the file.
256 |
257 | ```html
258 |
259 |
260 |
261 |
262 |
263 |
278 | ```
279 |
280 | #### onSending(file, XHR, formData)
281 | This event is emitted before making the upload HTTP request. So this is the time to modify the HTTP request and send some custom attributes.
282 |
283 | ```html
284 |
285 |
286 |
287 |
288 |
289 |
300 | ```
301 |
302 | #### onComplete(file, status, xhr)
303 | This event is called when a file has been processed. It includes **error, success** both. `3rd argument` will be the xhr response, if the error is returned from the server when uploading the file.
304 |
305 | ```html
306 |
307 |
308 |
309 |
310 |
311 |
324 | ```
325 |
326 | #### onDragEnter
327 | This event is invoked as soon as the user starts dragging the file.
328 |
329 | ```html
330 |
331 |
332 |
333 |
334 |
335 |
346 | ```
347 |
348 | #### onDragLeave
349 | This event is invoked when the user stops dragging the file.
350 |
351 | ```html
352 |
353 |
354 |
355 |
356 |
357 |
368 | ```
369 |
370 | #### onDrop
371 | This event is invoked when the user drops a file on the vue-clip area.
372 |
373 | ```html
374 |
375 |
376 |
377 |
378 |
379 |
390 | ```
391 |
392 | #### onTotalProgress(progress, totalBytes, bytesSent)
393 | This event returns the total upload progress for all the files. Think of it as the global progress indicator for multiple files uploaded together.
394 |
395 | ```html
396 |
397 |
398 |
399 |
400 |
401 |
411 | ```
412 |
413 | #### onQueueComplete
414 | The event is called when all files in the queue have been uploaded to the server.
415 |
416 | ```html
417 |
418 |
419 |
420 |
421 |
422 |
432 | ```
433 |
434 | #### onMaxFiles
435 | The event is called when maxFiles upload limit has been reached. This event will be fired `n times`for each file exceeding the limit. For example
436 |
437 | - **maxFiles** - 3
438 | - **filesUploaded** - 5
439 | - **eventCalled** - 2 times with file instance
440 |
441 | ```html
442 |
443 |
444 |
445 |
446 |
447 |
457 | ```
458 |
459 | ## File Attributes
460 | The file instance sent along events has following attributes.
461 |
462 | | Attribute | Type | Description |
463 | |-----------|------|-------------|
464 | | name | String | The client name of the file |
465 | | status String | String | The file status, which can be `success`, `error`, `queued`, `added`. |
466 | | width | Number | The file width. Only for images. |
467 | | height | Number | The file height. Only for images. |
468 | | bytesSent | Number | The total bytes sent to the server so far. |
469 | | progress | Number | Total upload progress. |
470 | | total | Number | The total number of bytes to be sent to the server. |
471 | | type | String | The file mime-type. |
472 | | size | Number | The file size on user disk. |
473 | | dataUrl | String | File base64 data URL to be used for displaying images preview. |
474 | | xhrResponse | Object | Server xhrResponse. Only contains `response`, `responseText` and `statusCode` |
475 | | errorMessage | String | Error message when processing a file. If the error is returned from the server, it will be the value of XHR error. Also can be client errors for `maxSize` etc.|
476 | | customAttributes | Object | Each file needs some custom attributes, for example `server id` to be used for deleting the files.|
477 |
478 | #### Adding/Accessing Custom Attributes
479 | ```javascript
480 | file.addAttribute('id', xhr.response.id)
481 |
482 | // access id
483 | file.customAttributes.id
484 | ```
485 |
486 | ## Browser Support
487 |
488 | - Chrome 7+
489 | - Firefox 4+
490 | - IE 10+
491 | - Opera 12+
492 | - Safari 6+
493 |
494 |
495 | ## Things to consider
496 | Make sure you add class `dz-message` to the uploader action wrapped inside `clip-uploader-action` slot. This makes your entire action body clickable. There are ways to get around it, but I wanted to keep the API transparent, instead of adding a bunch of DOM elements behind the scenes.
497 |
498 | ```html
499 |
500 |
501 |
Click or Drag and Drop files here upload
502 |
503 |
504 | ```
505 |
506 |
--------------------------------------------------------------------------------
/dist/vue-clip.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var i=n(33),o=r(i),s={install:function(e){e.component("vue-clip",o.default)}};"undefined"!=typeof window&&"undefined"!=typeof window.Vue&&window.Vue.use(s),t.default=s},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){e.exports=!n(7)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(6),i=n(22),o=n(17),s=Object.defineProperty;t.f=n(2)?Object.defineProperty:function(e,t,n){if(r(e),t=o(t,!0),r(n),i)try{return s(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){var r=n(43),i=n(39);e.exports=function(e){return r(i(e))}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(8);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(26),i=n(12);e.exports=Object.keys||function(e){return r(e,i)}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t){var n=e.exports={version:"2.4.0"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t,n){var r=n(3),i=n(15);e.exports=n(2)?function(e,t,n){return r.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){t.f={}.propertyIsEnumerable},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(1),i="__core-js_shared__",o=r[i]||(r[i]={});e.exports=function(e){return o[e]||(o[e]={})}},function(e,t,n){var r=n(8);e.exports=function(e,t){if(!r(e))return e;var n,i;if(t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;if("function"==typeof(n=e.valueOf)&&!r(i=n.call(e)))return i;if(!t&&"function"==typeof(n=e.toString)&&!r(i=n.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},function(e,t,n){var r=n(1),i=n(11),o=n(23),s=n(29),u=n(3).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:r.Symbol||{});"_"==e.charAt(0)||e in t||u(t,e,{value:s.f(e)})}},function(e,t,n){var r=n(16)("wks"),i=n(10),o=n(1).Symbol,s="function"==typeof o,u=e.exports=function(e){return r[e]||(r[e]=s&&o[e]||(s?o:i)("Symbol."+e))};u.store=r},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){var r=n(8),i=n(1).document,o=r(i)&&r(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},function(e,t,n){e.exports=!n(2)&&!n(7)(function(){return 7!=Object.defineProperty(n(21)("div"),"a",{get:function(){return 7}}).a})},function(e,t){e.exports=!0},function(e,t,n){var r=n(26),i=n(12).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var r=n(5),i=n(4),o=n(37)(!1),s=n(27)("IE_PROTO");e.exports=function(e,t){var n,u=i(e),a=0,l=[];for(n in u)n!=s&&r(u,n)&&l.push(n);for(;t.length>a;)r(u,n=t[a++])&&(~o(l,n)||l.push(n));return l}},function(e,t,n){var r=n(16)("keys"),i=n(10);e.exports=function(e){return r[e]||(r[e]=i(e))}},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t,n){t.f=n(19)},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children=[],e.webpackPolyfill=1),e}},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n\n \n \n \n',f.props={},f.props.uploaderClass={type:String},f.props.options={type:Object,default:function(){return{}}},f.props.onAddedFile={type:Function,default:function(){return function(){}}},f.props.onRemovedFile={type:Function,default:function(){return function(){}}},f.props.onSending={type:Function,default:function(){return function(){}}},f.props.onDragEnter={type:Function,default:function(){return function(){}}},f.props.onDragLeave={type:Function,default:function(){return function(){}}},f.props.onDrop={type:Function,default:function(){return function(){}}},f.props.onTotalProgress={type:Function,default:function(){return function(){}}},f.props.onQueueComplete={type:Function,default:function(){return function(){}}},f.props.onMaxFiles={type:Function,default:function(){return function(){}}},f.props.onInit={type:Function,default:function(){return function(){}}},f.props.onComplete={type:Function,default:function(){return function(){}}},f.data=function(){return{files:[],dragCounter:0,uploader:null}},f.mounted=function(){var e=this,t=(0,l.default)(this.options),n=t.accept||function(e,t){t()};if(t.previewTemplate=this.$refs["clip-preview-template"].innerHTML,t.accept=function(t,r){var i=t.blobId;n(e.getFile(i),r)},"undefined"!=typeof t.maxFiles&&t.maxFiles instanceof Object==!0){var r=t.maxFiles,i=r.limit,s=r.message;t.maxFiles=i,t.dictMaxFilesExceeded=s}if("undefined"!=typeof t.maxFilesize&&t.maxFilesize instanceof Object==!0){var u=t.maxFilesize,a=u.limit,c=u.message;t.maxFilesize=a,t.dictFileTooBig=c}if("undefined"!=typeof t.acceptedFiles&&t.acceptedFiles instanceof Object==!0&&t.acceptedFiles instanceof Array==!1){var p=t.acceptedFiles,f=p.extensions,d=p.message;t.acceptedFiles=f.join(","),t.dictInvalidFileType=d}this.uploader=new o.default(t),this.bindEvents(),this.uploader.mount(this.$el.firstElementChild),this.onInit(this)},f.methods={},f.methods.bindEvents=function(){this.uploader.on("addedfile",this.addedFile.bind(this)),this.uploader.on("removedfile",this.removedFile.bind(this)),this.uploader.on("sending",this.sending.bind(this)),this.uploader.on("complete",this.complete.bind(this)),this.uploader.on("error",this.error.bind(this)),this.uploader.on("uploadprogress",this.uploadProgress.bind(this)),this.uploader.on("thumbnail",this.thumbnail.bind(this)),this.uploader.on("drop",this.drop.bind(this)),this.uploader.on("dragenter",this.dragEnter.bind(this)),this.uploader.on("dragleave",this.dragLeave.bind(this)),this.uploader.on("totaluploadprogress",this.totalUploadProgress.bind(this)),this.uploader.on("maxfilesexceeded",this.maxFilesExceeded.bind(this)),this.uploader.on("queuecomplete",this.queueComplete.bind(this))},f.methods.getFile=function(e){var t={};return this.files.forEach(function(n){n._file.blobId===e&&(t=n)}),t},f.methods.addedFile=function(e){var t=(0,p.default)();e.blobId=t,this.files.push(new u.default(e)),this.onAddedFile(this.getFile(t))},f.methods.removedFile=function(e){var t=e.blobId,n=this.getFile(t);n.updateStatus("removed"),this.onRemovedFile(n)},f.methods.sending=function(e,t,n){var r=e.blobId,i=this.getFile(r);this.onSending(i,t,n)},f.methods.complete=function(e){var t=e.blobId,n=e.status,r=e.xhr,i=void 0===r?{}:r,o=this.getFile(t);o.updateStatus(n),o.updateXhrResponse({response:i.response,responseText:i.responseText,statusCode:i.status}),this.onComplete(o,n,i)},f.methods.error=function(e,t){var n=e.blobId,r=e.status,i=this.getFile(n);i.updateStatus(r),i.updateErrorMessage(t)},f.methods.uploadProgress=function(e,t,n){var r=e.blobId,i=this.getFile(r);i.updateProgress(t),i.updateBytesSent(n)},f.methods.thumbnail=function(e,t){var n=e.blobId,r=this.getFile(n);r.updateDataUrl(t)},f.methods.drop=function(){this.dragCounter=0,this.onDrop(),this.onDragLeave()},f.methods.dragEnter=function(e){e.preventDefault(),this.dragCounter++,this.onDragEnter()},f.methods.dragLeave=function(){this.dragCounter--,0===this.dragCounter&&this.onDragLeave()},f.methods.totalUploadProgress=function(){this.onTotalProgress.apply(this,arguments)},f.methods.queueComplete=function(){this.onQueueComplete()},f.methods.maxFilesExceeded=function(e){var t=e.blobId,n=this.getFile(t);this.onMaxFiles(n)},f.methods.removeFile=function(e){this.uploader.removeFile(e._file)},f.methods.addFile=function(e){this.uploader.addFile(e)},f.methods.removeAllFiles=function(e){this.uploader.removeAllFiles(e)},t.default=f},function(e,t,n){e.exports={default:n(35),__esModule:!0}},function(e,t,n){n(56),n(55),n(57),n(58),e.exports=n(11).Symbol},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(4),i=n(54),o=n(53);e.exports=function(e){return function(t,n,s){var u,a=r(t),l=i(a.length),c=o(s,l);if(e&&n!=n){for(;l>c;)if(u=a[c++],u!=u)return!0}else for(;l>c;c++)if((e||c in a)&&a[c]===n)return e||c||0;return!e&&-1}}},function(e,t,n){var r=n(36);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(9),i=n(25),o=n(14);e.exports=function(e){var t=r(e),n=i.f;if(n)for(var s,u=n(e),a=o.f,l=0;u.length>l;)a.call(e,s=u[l++])&&t.push(s);return t}},function(e,t,n){var r=n(1),i=n(11),o=n(38),s=n(13),u="prototype",a=function(e,t,n){var l,c,p,f=e&a.F,d=e&a.G,h=e&a.S,m=e&a.P,v=e&a.B,g=e&a.W,y=d?i:i[t]||(i[t]={}),b=y[u],F=d?r:h?r[t]:(r[t]||{})[u];d&&(n=t);for(l in n)c=!f&&F&&void 0!==F[l],c&&l in y||(p=c?F[l]:n[l],y[l]=d&&"function"!=typeof F[l]?n[l]:v&&c?o(p,r):g&&F[l]==p?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t[u]=e[u],t}(p):m&&"function"==typeof p?o(Function.call,p):p,m&&((y.virtual||(y.virtual={}))[l]=p,e&a.R&&b&&!b[l]&&s(b,l,p)))};a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,e.exports=a},function(e,t,n){e.exports=n(1).document&&document.documentElement},function(e,t,n){var r=n(20);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==r(e)?e.split(""):Object(e)}},function(e,t,n){var r=n(20);e.exports=Array.isArray||function(e){return"Array"==r(e)}},function(e,t,n){var r=n(9),i=n(4);e.exports=function(e,t){for(var n,o=i(e),s=r(o),u=s.length,a=0;u>a;)if(o[n=s[a++]]===t)return n}},function(e,t,n){var r=n(10)("meta"),i=n(8),o=n(5),s=n(3).f,u=0,a=Object.isExtensible||function(){return!0},l=!n(7)(function(){return a(Object.preventExtensions({}))}),c=function(e){s(e,r,{value:{i:"O"+ ++u,w:{}}})},p=function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,r)){if(!a(e))return"F";if(!t)return"E";c(e)}return e[r].i},f=function(e,t){if(!o(e,r)){if(!a(e))return!0;if(!t)return!1;c(e)}return e[r].w},d=function(e){return l&&h.NEED&&a(e)&&!o(e,r)&&c(e),e},h=e.exports={KEY:r,NEED:!1,fastKey:p,getWeak:f,onFreeze:d}},function(e,t,n){var r=n(6),i=n(48),o=n(12),s=n(27)("IE_PROTO"),u=function(){},a="prototype",l=function(){var e,t=n(21)("iframe"),r=o.length,i="<",s=">";for(t.style.display="none",n(42).appendChild(t),t.src="javascript:",e=t.contentWindow.document,e.open(),e.write(i+"script"+s+"document.F=Object"+i+"/script"+s),e.close(),l=e.F;r--;)delete l[a][o[r]];return l()};e.exports=Object.create||function(e,t){var n;return null!==e?(u[a]=r(e),n=new u,u[a]=null,n[s]=e):n=l(),void 0===t?n:i(n,t)}},function(e,t,n){var r=n(3),i=n(6),o=n(9);e.exports=n(2)?Object.defineProperties:function(e,t){i(e);for(var n,s=o(t),u=s.length,a=0;u>a;)r.f(e,n=s[a++],t[n]);return e}},function(e,t,n){var r=n(14),i=n(15),o=n(4),s=n(17),u=n(5),a=n(22),l=Object.getOwnPropertyDescriptor;t.f=n(2)?l:function(e,t){if(e=o(e),t=s(t,!0),a)try{return l(e,t)}catch(e){}if(u(e,t))return i(!r.f.call(e,t),e[t])}},function(e,t,n){var r=n(4),i=n(24).f,o={}.toString,s="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],u=function(e){try{return i(e)}catch(e){return s.slice()}};e.exports.f=function(e){return s&&"[object Window]"==o.call(e)?u(e):i(r(e))}},function(e,t,n){e.exports=n(13)},function(e,t,n){var r=n(3).f,i=n(5),o=n(19)("toStringTag");e.exports=function(e,t,n){e&&!i(e=n?e:e.prototype,o)&&r(e,o,{configurable:!0,value:t})}},function(e,t,n){var r=n(28),i=Math.max,o=Math.min;e.exports=function(e,t){return e=r(e),e<0?i(e+t,0):o(e,t)}},function(e,t,n){var r=n(28),i=Math.min;e.exports=function(e){return e>0?i(r(e),9007199254740991):0}},function(e,t){},function(e,t,n){"use strict";var r=n(1),i=n(5),o=n(2),s=n(41),u=n(51),a=n(46).KEY,l=n(7),c=n(16),p=n(52),f=n(10),d=n(19),h=n(29),m=n(18),v=n(45),g=n(40),y=n(44),b=n(6),F=n(4),w=n(17),_=n(15),E=n(47),x=n(50),S=n(49),O=n(3),k=n(9),A=S.f,j=O.f,C=x.f,T=r.Symbol,L=r.JSON,z=L&&L.stringify,D="prototype",I=d("_hidden"),P=d("toPrimitive"),M={}.propertyIsEnumerable,N=c("symbol-registry"),U=c("symbols"),R=c("op-symbols"),W=Object[D],H="function"==typeof T,q=r.QObject,Q=!q||!q[D]||!q[D].findChild,B=o&&l(function(){return 7!=E(j({},"a",{get:function(){return j(this,"a",{value:7}).a}})).a})?function(e,t,n){var r=A(W,t);r&&delete W[t],j(e,t,n),r&&e!==W&&j(W,t,r)}:j,G=function(e){var t=U[e]=E(T[D]);return t._k=e,t},$=H&&"symbol"==typeof T.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof T},X=function(e,t,n){return e===W&&X(R,t,n),b(e),t=w(t,!0),b(n),i(U,t)?(n.enumerable?(i(e,I)&&e[I][t]&&(e[I][t]=!1),n=E(n,{enumerable:_(0,!1)})):(i(e,I)||j(e,I,_(1,{})),e[I][t]=!0),B(e,t,n)):j(e,t,n)},Y=function(e,t){b(e);for(var n,r=g(t=F(t)),i=0,o=r.length;o>i;)X(e,n=r[i++],t[n]);return e},V=function(e,t){return void 0===t?E(e):Y(E(e),t)},J=function(e){var t=M.call(this,e=w(e,!0));return!(this===W&&i(U,e)&&!i(R,e))&&(!(t||!i(this,e)||!i(U,e)||i(this,I)&&this[I][e])||t)},K=function(e,t){if(e=F(e),t=w(t,!0),e!==W||!i(U,t)||i(R,t)){var n=A(e,t);return!n||!i(U,t)||i(e,I)&&e[I][t]||(n.enumerable=!0),n}},Z=function(e){for(var t,n=C(F(e)),r=[],o=0;n.length>o;)i(U,t=n[o++])||t==I||t==a||r.push(t);return r},ee=function(e){for(var t,n=e===W,r=C(n?R:F(e)),o=[],s=0;r.length>s;)!i(U,t=r[s++])||n&&!i(W,t)||o.push(U[t]);return o};H||(T=function(){if(this instanceof T)throw TypeError("Symbol is not a constructor!");var e=f(arguments.length>0?arguments[0]:void 0),t=function(n){this===W&&t.call(R,n),i(this,I)&&i(this[I],e)&&(this[I][e]=!1),B(this,e,_(1,n))};return o&&Q&&B(W,e,{configurable:!0,set:t}),G(e)},u(T[D],"toString",function(){return this._k}),S.f=K,O.f=X,n(24).f=x.f=Z,n(14).f=J,n(25).f=ee,o&&!n(23)&&u(W,"propertyIsEnumerable",J,!0),h.f=function(e){return G(d(e))}),s(s.G+s.W+s.F*!H,{Symbol:T});for(var te="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),ne=0;te.length>ne;)d(te[ne++]);for(var te=k(d.store),ne=0;te.length>ne;)m(te[ne++]);s(s.S+s.F*!H,"Symbol",{for:function(e){return i(N,e+="")?N[e]:N[e]=T(e)},keyFor:function(e){if($(e))return v(N,e);throw TypeError(e+" is not a symbol!")},useSetter:function(){Q=!0},useSimple:function(){Q=!1}}),s(s.S+s.F*!H,"Object",{create:V,defineProperty:X,defineProperties:Y,getOwnPropertyDescriptor:K,getOwnPropertyNames:Z,getOwnPropertySymbols:ee}),L&&s(s.S+s.F*(!H||l(function(){var e=T();return"[null]"!=z([e])||"{}"!=z({a:e})||"{}"!=z(Object(e))})),"JSON",{stringify:function(e){if(void 0!==e&&!$(e)){for(var t,n,r=[e],i=1;arguments.length>i;)r.push(arguments[i++]);return t=r[1],"function"==typeof t&&(n=t),!n&&y(t)||(t=function(e,t){if(n&&(t=n.call(this,e,t)),!$(t))return t}),r[1]=t,z.apply(L,r)}}}),T[D][P]||n(13)(T[D],P,T[D].valueOf),p(T,"Symbol"),p(Math,"Math",!0),p(r.JSON,"JSON",!0)},function(e,t,n){n(18)("asyncIterator")},function(e,t,n){n(18)("observable")},function(e,t,n){(function(e){(function(){var t,n,r,i,o,s,u,a,l=[].slice,c={}.hasOwnProperty,p=function(e,t){function n(){this.constructor=e}for(var r in t)c.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e};u=function(){},n=function(){function e(){}return e.prototype.addEventListener=e.prototype.on,e.prototype.on=function(e,t){return this._callbacks=this._callbacks||{},this._callbacks[e]||(this._callbacks[e]=[]),this._callbacks[e].push(t),this},e.prototype.emit=function(){var e,t,n,r,i,o;if(r=arguments[0],e=2<=arguments.length?l.call(arguments,1):[],this._callbacks=this._callbacks||{},n=this._callbacks[r])for(i=0,o=n.length;i '),this.element.appendChild(n)),r=n.getElementsByTagName("span")[0],r&&(null!=r.textContent?r.textContent=this.options.dictFallbackMessage:null!=r.innerText&&(r.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(e){var t,n,r;return t={srcX:0,srcY:0,srcWidth:e.width,srcHeight:e.height},n=e.width/e.height,t.optWidth=this.options.thumbnailWidth,t.optHeight=this.options.thumbnailHeight,null==t.optWidth&&null==t.optHeight?(t.optWidth=t.srcWidth,t.optHeight=t.srcHeight):null==t.optWidth?t.optWidth=n*t.optHeight:null==t.optHeight&&(t.optHeight=1/n*t.optWidth),r=t.optWidth/t.optHeight,e.heightr?(t.srcHeight=e.height,t.srcWidth=t.srcHeight*r):(t.srcWidth=e.width,t.srcHeight=t.srcWidth/r),t.srcX=(e.width-t.srcWidth)/2,t.srcY=(e.height-t.srcHeight)/2,t},drop:function(e){return this.element.classList.remove("dz-drag-hover")},dragstart:u,dragend:function(e){return this.element.classList.remove("dz-drag-hover")},dragenter:function(e){return this.element.classList.add("dz-drag-hover")},dragover:function(e){return this.element.classList.add("dz-drag-hover")},dragleave:function(e){return this.element.classList.remove("dz-drag-hover")},paste:u,reset:function(){return this.element.classList.remove("dz-started")},addedfile:function(e){var n,r,i,o,s,u,a,l,c,p,f,d,h;if(this.element===this.previewsContainer&&this.element.classList.add("dz-started"),this.previewsContainer){for(e.previewElement=t.createElement(this.options.previewTemplate.trim()),e.previewTemplate=e.previewElement,this.previewsContainer.appendChild(e.previewElement),p=e.previewElement.querySelectorAll("[data-dz-name]"),o=0,a=p.length;o'+this.options.dictRemoveFile+" "),e.previewElement.appendChild(e._removeLink)),r=function(n){return function(r){return r.preventDefault(),r.stopPropagation(),e.status===t.UPLOADING?t.confirm(n.options.dictCancelUploadConfirmation,function(){return n.removeFile(e)}):n.options.dictRemoveFileConfirmation?t.confirm(n.options.dictRemoveFileConfirmation,function(){return n.removeFile(e)}):n.removeFile(e)}}(this),d=e.previewElement.querySelectorAll("[data-dz-remove]"),h=[],u=0,c=d.length;u'+this.options.dictDefaultMessage+" ")),this.clickableElements.length&&(r=function(e){return function(){return e.hiddenFileInput&&e.hiddenFileInput.parentNode.removeChild(e.hiddenFileInput),e.hiddenFileInput=document.createElement("input"),e.hiddenFileInput.setAttribute("type","file"),(null==e.options.maxFiles||e.options.maxFiles>1)&&e.hiddenFileInput.setAttribute("multiple","multiple"),e.hiddenFileInput.className="dz-hidden-input",null!=e.options.acceptedFiles&&e.hiddenFileInput.setAttribute("accept",e.options.acceptedFiles),null!=e.options.capture&&e.hiddenFileInput.setAttribute("capture",e.options.capture),e.hiddenFileInput.style.visibility="hidden",e.hiddenFileInput.style.position="absolute",e.hiddenFileInput.style.top="0",e.hiddenFileInput.style.left="0",e.hiddenFileInput.style.height="0",e.hiddenFileInput.style.width="0",document.querySelector(e.options.hiddenInputContainer).appendChild(e.hiddenFileInput),e.hiddenFileInput.addEventListener("change",function(){var t,n,i,o;if(n=e.hiddenFileInput.files,n.length)for(i=0,o=n.length;i',this.options.dictFallbackText&&(r+=""+this.options.dictFallbackText+"
"),r+=' ',n=t.createElement(r),"FORM"!==this.element.tagName?(i=t.createElement(''),i.appendChild(n)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=i?i:n)},t.prototype.getExistingFallback=function(){var e,t,n,r,i,o;for(t=function(e){var t,n,r;for(n=0,r=e.length;n0){for(s=["TB","GB","MB","KB","b"],n=u=0,a=s.length;u=t){r=e/Math.pow(this.options.filesizeBase,4-n),i=o;break}r=Math.round(10*r)/10}return""+r+" "+i},t.prototype._updateMaxFilesReachedClass=function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit("maxfilesreached",this.files),this.element.classList.add("dz-max-files-reached")):this.element.classList.remove("dz-max-files-reached")},t.prototype.drop=function(e){var t,n;e.dataTransfer&&(this.emit("drop",e),t=e.dataTransfer.files,this.emit("addedfiles",t),t.length&&(n=e.dataTransfer.items,n&&n.length&&null!=n[0].webkitGetAsEntry?this._addFilesFromItems(n):this.handleFiles(t)))},t.prototype.paste=function(e){var t,n;if(null!=(null!=e&&null!=(n=e.clipboardData)?n.items:void 0))return this.emit("paste",e),t=e.clipboardData.items,t.length?this._addFilesFromItems(t):void 0},t.prototype.handleFiles=function(e){var t,n,r,i;for(i=[],n=0,r=e.length;n0){for(o=0,s=n.length;o1024*this.options.maxFilesize*1024?n(this.options.dictFileTooBig.replace("{{filesize}}",Math.round(e.size/1024/10.24)/100).replace("{{maxFilesize}}",this.options.maxFilesize)):t.isValidFile(e,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(n(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}",this.options.maxFiles)),this.emit("maxfilesexceeded",e)):this.options.accept.call(this,e,n):n(this.options.dictInvalidFileType)},t.prototype.addFile=function(e){return e.upload={progress:0,total:e.size,bytesSent:0},this.files.push(e),e.status=t.ADDED,this.emit("addedfile",e),this._enqueueThumbnail(e),this.accept(e,function(t){return function(n){return n?(e.accepted=!1,t._errorProcessing([e],n)):(e.accepted=!0,t.options.autoQueue&&t.enqueueFile(e)),t._updateMaxFilesReachedClass()}}(this))},t.prototype.enqueueFiles=function(e){var t,n,r;for(n=0,r=e.length;n=t)&&(r=this.getQueuedFiles(),r.length>0)){if(this.options.uploadMultiple)return this.processFiles(r.slice(0,t-n));for(;e=M;c=0<=M?++T:--T)o.append(this._getParamName(c),e[c],this._renameFilename(e[c].name));return this.submitRequest(_,o,e)},t.prototype.submitRequest=function(e,t,n){return e.send(t)},t.prototype._finished=function(e,n,r){var i,o,s;for(o=0,s=e.length;oc;)t=i[4*(a-1)+3],0===t?o=a:c=a,a=o+c>>1;return l=a/s,0===l?1:l},s=function(e,t,n,r,i,s,u,a,l,c){var p;return p=o(t),e.drawImage(t,n,r,i,s,u,a,l,c/p)},i=function(e,t){var n,r,i,o,s,u,a,l,c;if(i=!1,c=!0,r=e.document,l=r.documentElement,n=r.addEventListener?"addEventListener":"attachEvent",a=r.addEventListener?"removeEventListener":"detachEvent",u=r.addEventListener?"":"on",o=function(n){if("readystatechange"!==n.type||"complete"===r.readyState)return("load"===n.type?e:r)[a](u+n.type,o,!1),!i&&(i=!0)?t.call(e,n.type||n):void 0},s=function(){var e;try{l.doScroll("left")}catch(t){return e=t,void setTimeout(s,50)}return o("poll")},"complete"!==r.readyState){if(r.createEventObject&&l.doScroll){try{c=!e.frameElement}catch(e){}c&&s()}return r[n](u+"DOMContentLoaded",o,!1),r[n](u+"readystatechange",o,!1),e[n](u+"load",o,!1)}},t._autoDiscoverFunction=function(){if(t.autoDiscover)return t.discover()},i(window,t._autoDiscoverFunction)}).call(this)}).call(t,n(30)(e))},function(e,t,n){(function(e,n){function r(e,t){return e.set(t[0],t[1]),e}function i(e,t){return e.add(t),e}function o(e,t){for(var n=-1,r=e?e.length:0;++n-1}function S(e,t){var n=this.__data__,r=R(n,e);return r<0?n.push([e,t]):n[r][1]=t,this}function O(e){var t=-1,n=e?e.length:0;for(this.clear();++t-1&&e%1==0&&e-1&&e%1==0&&e<=Ce}function Ee(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function xe(e){return!!e&&"object"==typeof e}function Se(e){return be(e)?N(e):$(e)}function Oe(){return[]}function ke(){return!1}var Ae=200,je="__lodash_hash_undefined__",Ce=9007199254740991,Te="[object Arguments]",Le="[object Array]",ze="[object Boolean]",De="[object Date]",Ie="[object Error]",Pe="[object Function]",Me="[object GeneratorFunction]",Ne="[object Map]",Ue="[object Number]",Re="[object Object]",We="[object Promise]",He="[object RegExp]",qe="[object Set]",Qe="[object String]",Be="[object Symbol]",Ge="[object WeakMap]",$e="[object ArrayBuffer]",Xe="[object DataView]",Ye="[object Float32Array]",Ve="[object Float64Array]",Je="[object Int8Array]",Ke="[object Int16Array]",Ze="[object Int32Array]",et="[object Uint8Array]",tt="[object Uint8ClampedArray]",nt="[object Uint16Array]",rt="[object Uint32Array]",it=/[\\^$.*+?()[\]{}|]/g,ot=/\w*$/,st=/^\[object .+?Constructor\]$/,ut=/^(?:0|[1-9]\d*)$/,at={};at[Te]=at[Le]=at[$e]=at[Xe]=at[ze]=at[De]=at[Ye]=at[Ve]=at[Je]=at[Ke]=at[Ze]=at[Ne]=at[Ue]=at[Re]=at[He]=at[qe]=at[Qe]=at[Be]=at[et]=at[tt]=at[nt]=at[rt]=!0,at[Ie]=at[Pe]=at[Ge]=!1;var lt="object"==typeof e&&e&&e.Object===Object&&e,ct="object"==typeof self&&self&&self.Object===Object&&self,pt=lt||ct||Function("return this")(),ft="object"==typeof t&&t&&!t.nodeType&&t,dt=ft&&"object"==typeof n&&n&&!n.nodeType&&n,ht=dt&&dt.exports===ft,mt=Array.prototype,vt=Function.prototype,gt=Object.prototype,yt=pt["__core-js_shared__"],bt=function(){var e=/[^.]+$/.exec(yt&&yt.keys&&yt.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),Ft=vt.toString,wt=gt.hasOwnProperty,_t=gt.toString,Et=RegExp("^"+Ft.call(wt).replace(it,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),xt=ht?pt.Buffer:void 0,St=pt.Symbol,Ot=pt.Uint8Array,kt=f(Object.getPrototypeOf,Object),At=Object.create,jt=gt.propertyIsEnumerable,Ct=mt.splice,Tt=Object.getOwnPropertySymbols,Lt=xt?xt.isBuffer:void 0,zt=f(Object.keys,Object),Dt=ue(pt,"DataView"),It=ue(pt,"Map"),Pt=ue(pt,"Promise"),Mt=ue(pt,"Set"),Nt=ue(pt,"WeakMap"),Ut=ue(Object,"create"),Rt=me(Dt),Wt=me(It),Ht=me(Pt),qt=me(Mt),Qt=me(Nt),Bt=St?St.prototype:void 0,Gt=Bt?Bt.valueOf:void 0;h.prototype.clear=m,h.prototype.delete=v,h.prototype.get=g,h.prototype.has=y,h.prototype.set=b,F.prototype.clear=w,F.prototype.delete=_,F.prototype.get=E,F.prototype.has=x,F.prototype.set=S,O.prototype.clear=k,O.prototype.delete=A,O.prototype.get=j,O.prototype.has=C,O.prototype.set=T,L.prototype.clear=z,L.prototype.delete=D,L.prototype.get=I,L.prototype.has=P,L.prototype.set=M;var $t=Tt?f(Tt,Object):Oe,Xt=B;(Dt&&Xt(new Dt(new ArrayBuffer(1)))!=Xe||It&&Xt(new It)!=Ne||Pt&&Xt(Pt.resolve())!=We||Mt&&Xt(new Mt)!=qe||Nt&&Xt(new Nt)!=Ge)&&(Xt=function(e){var t=_t.call(e),n=t==Re?e.constructor:void 0,r=n?me(n):void 0;if(r)switch(r){case Rt:return Xe;case Wt:return Ne;case Ht:return We;case qt:return qe;case Qt:return Ge}return t});var Yt=Array.isArray,Vt=Lt||ke;n.exports=ve}).call(t,function(){return this}(),n(30)(e))}])});
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Clip from './src/components/Clip/index.js'
4 |
5 | const VueClip = {
6 | install (Vue) {
7 | Vue.component('vue-clip', Clip)
8 | }
9 | }
10 |
11 | /**
12 | * When required globally
13 | */
14 | if (typeof (window) !== 'undefined' && typeof (window.Vue) !== 'undefined') {
15 | window.Vue.use(VueClip)
16 | }
17 |
18 | export default VueClip
19 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const webpackConfig = require('./webpack.config.js')
4 | delete webpackConfig.entry
5 |
6 | const testType = process.argv.indexOf('--local') > -1 ? 'local' : 'remote'
7 | const browsers = testType === 'local' ? ['Chrome'] : ['PhantomJS']
8 |
9 | module.exports = function (config) {
10 | config.set({
11 | browsers: browsers,
12 | frameworks: ['mocha'],
13 | files: ['test/index.js'],
14 | preprocessors: {
15 | 'test/index.js': ['webpack']
16 | },
17 | webpack: webpackConfig,
18 | webpackMiddleware: {
19 | noInfo: true
20 | },
21 | singleRun: true
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-clip",
3 | "version": "1.0.0",
4 | "description": "file uploader for vuejs with magical powers",
5 | "main": "dist/vue-clip.js",
6 | "scripts": {
7 | "test": "karma start karma.conf.js",
8 | "build": "webpack -p --optimize-minimize --optimize-occurrence-order"
9 | },
10 | "keywords": [
11 | "vuejs",
12 | "vue",
13 | "file-uploader",
14 | "file-upload",
15 | "vue-upload",
16 | "dropzone"
17 | ],
18 | "author": "amanvirk",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "babel-core": "^6.21.0",
22 | "babel-eslint": "^7.1.1",
23 | "babel-loader": "^6.2.10",
24 | "babel-preset-latest": "^6.16.0",
25 | "chai": "^3.5.0",
26 | "css-loader": "^0.26.1",
27 | "eslint": "^3.12.2",
28 | "eslint-config-standard": "^6.2.1",
29 | "eslint-loader": "^1.6.1",
30 | "eslint-plugin-html": "^1.7.0",
31 | "eslint-plugin-promise": "^3.4.0",
32 | "eslint-plugin-standard": "^2.0.1",
33 | "karma": "^1.3.0",
34 | "karma-chrome-launcher": "^2.0.0",
35 | "karma-mocha": "^1.3.0",
36 | "karma-phantomjs-launcher": "^1.0.2",
37 | "karma-webpack": "^1.8.1",
38 | "mocha": "^3.2.0",
39 | "phantomjs": "^2.1.7",
40 | "unminified-webpack-plugin": "^1.2.0",
41 | "vue": "^2.1.8",
42 | "webpack": "^1.14.0"
43 | },
44 | "dependencies": {
45 | "dropzone": "^4.3.0",
46 | "lodash.clone": "^4.5.0"
47 | },
48 | "directories": {
49 | "test": "test"
50 | },
51 | "repository": {
52 | "type": "git",
53 | "url": "git+https://github.com/thetutlage/vue-clip.git"
54 | },
55 | "bugs": {
56 | "url": "https://github.com/thetutlage/vue-clip/issues"
57 | },
58 | "homepage": "https://github.com/thetutlage/vue-clip#readme"
59 | }
60 |
--------------------------------------------------------------------------------
/src/File.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class File {
4 |
5 | constructor (file) {
6 | this._file = file
7 | this.status = file.status
8 | this.name = file.name
9 | this.width = file.width
10 | this.height = file.height
11 | this.bytesSent = file.upload.bytesSent || 0
12 | this.progress = file.upload.progress || 0
13 | this.total = file.upload.total
14 | this.type = file.type
15 | this.size = file.size
16 | this.dataUrl = ''
17 | this.xhrResponse = {}
18 | this.customAttributes = {}
19 | this.errorMessage = ''
20 | }
21 |
22 | updateDataUrl (dataUrl) {
23 | this.dataUrl = dataUrl
24 | }
25 |
26 | updateStatus (status) {
27 | this.status = status
28 | }
29 |
30 | updateProgress (progress) {
31 | this.progress = progress
32 | }
33 |
34 | updateBytesSent (bytesSent) {
35 | this.bytesSent = bytesSent
36 | }
37 |
38 | updateXhrResponse (response) {
39 | this.xhrResponse = response
40 | }
41 |
42 | updateErrorMessage (errorMessage) {
43 | this.errorMessage = errorMessage
44 | }
45 |
46 | addAttribute (key, value) {
47 | this.customAttributes[key] = value
48 | }
49 |
50 | }
51 |
52 | export default File
53 |
--------------------------------------------------------------------------------
/src/Uploader.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Dropzone from 'dropzone'
4 |
5 | Dropzone.autoDiscover = false
6 |
7 | class Uploader {
8 |
9 | constructor (options) {
10 | this._options = options || {}
11 | this._existingInit = this._options.init || function () {}
12 | this._hooks = []
13 | this._uploader = null
14 | }
15 |
16 | /**
17 | * Bind hooks to the uploader instance. Also
18 | * makes sure to call the options init if
19 | * defined.
20 | *
21 | * @private
22 | */
23 | _bindHooks (self) {
24 | self._existingInit.bind(this)()
25 | self._hooks.forEach((hook) => {
26 | this.on(hook.event, hook.callback)
27 | })
28 | self._hooks = []
29 | }
30 |
31 | /**
32 | * Mounts uploader to the DOM
33 | *
34 | * @param {DOMElement} domElem
35 | */
36 | mount (domElem) {
37 | const self = this
38 | this._options.init = function () {
39 | self._bindHooks.bind(this)(self)
40 | }
41 | this._uploader = new Dropzone(domElem, this._options)
42 | }
43 |
44 | /**
45 | * Binds a hook listener
46 | *
47 | * @param {String} event
48 | * @param {Function} callback
49 | */
50 | on (event, callback) {
51 | this._hooks.push({ event, callback })
52 | }
53 |
54 | /**
55 | * Removes all listeners and dom bindings.
56 | */
57 | destroy () {
58 | this._uploader.disable()
59 | }
60 |
61 | /**
62 | * Remove file from the uploader
63 | *
64 | * @param {Object} file
65 | */
66 | removeFile (file) {
67 | this._uploader.removeFile(file)
68 | }
69 |
70 | /**
71 | * Add native file object to dropzone
72 | *
73 | * @param {Object} file
74 | */
75 | addFile (file) {
76 | this._uploader.addFile(file)
77 | }
78 |
79 | /**
80 | * Remove all files
81 | *
82 | * @param {Boolean} [cancelQueued]
83 | */
84 | removeAllFiles (cancelQueued) {
85 | this._uploader.removeAllFiles(cancelQueued)
86 | }
87 |
88 | }
89 |
90 | export default Uploader
91 |
--------------------------------------------------------------------------------
/src/components/Clip/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 | * vue-clip
5 | *
6 | * (c) Harminder Virk
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | import Uploader from '../../Uploader'
13 | import File from '../../File'
14 | import clone from 'lodash.clone'
15 | import SymbolFallback from 'babel-runtime/core-js/symbol'
16 |
17 | const component = {}
18 |
19 | component.template = ``
24 |
25 | // DEFINING PROPS
26 | component.props = {}
27 |
28 | /**
29 | * css class to be placed on action button parent div.
30 | * clip component sets its display to inline-block,
31 | * which may cause issues and this class can be
32 | * used to override it's styles.
33 | *
34 | * @type {Object}
35 | */
36 | component.props.uploaderClass = {
37 | type: String
38 | }
39 |
40 | /**
41 | * Uploader options, majority of options
42 | * are based of dropzone. Check docs
43 | * for more insights.
44 | *
45 | * @type {Object}
46 | */
47 | component.props.options = {
48 | type: Object,
49 | default: function () {
50 | return {}
51 | }
52 | }
53 |
54 | component.props.onAddedFile = {
55 | type: Function,
56 | default: function () {
57 | return function () {}
58 | }
59 | }
60 |
61 | component.props.onRemovedFile = {
62 | type: Function,
63 | default: function () {
64 | return function () {}
65 | }
66 | }
67 |
68 | component.props.onSending = {
69 | type: Function,
70 | default: function () {
71 | return function () {}
72 | }
73 | }
74 |
75 | component.props.onDragEnter = {
76 | type: Function,
77 | default: function () {
78 | return function () {}
79 | }
80 | }
81 |
82 | component.props.onDragLeave = {
83 | type: Function,
84 | default: function () {
85 | return function () {}
86 | }
87 | }
88 |
89 | component.props.onDrop = {
90 | type: Function,
91 | default: function () {
92 | return function () {}
93 | }
94 | }
95 |
96 | component.props.onTotalProgress = {
97 | type: Function,
98 | default: function () {
99 | return function () {}
100 | }
101 | }
102 |
103 | component.props.onQueueComplete = {
104 | type: Function,
105 | default: function () {
106 | return function () {}
107 | }
108 | }
109 |
110 | component.props.onMaxFiles = {
111 | type: Function,
112 | default: function () {
113 | return function () {}
114 | }
115 | }
116 |
117 | component.props.onInit = {
118 | type: Function,
119 | default: function () {
120 | return function () {}
121 | }
122 | }
123 |
124 | component.props.onComplete = {
125 | type: Function,
126 | default: function () {
127 | return function () {}
128 | }
129 | }
130 |
131 | // COMPONENT DATA
132 | component.data = function () {
133 | return {
134 | files: [],
135 | dragCounter: 0,
136 | uploader: null
137 | }
138 | }
139 |
140 | // LIFECYCLE HOOKS
141 | component.mounted = function () {
142 | const options = clone(this.options)
143 | const accept = options.accept || function (file, done) { done() }
144 |
145 | /**
146 | * Overriding properties of the options object
147 | */
148 | options.previewTemplate = this.$refs['clip-preview-template'].innerHTML
149 | options.accept = ({ blobId }, done) => {
150 | accept(this.getFile(blobId), done)
151 | }
152 |
153 | if (typeof (options.maxFiles) !== 'undefined' && options.maxFiles instanceof Object === true) {
154 | const {limit, message} = options.maxFiles
155 | options.maxFiles = limit
156 | options.dictMaxFilesExceeded = this.cleanupMessage(message)
157 | }
158 |
159 | if (typeof (options.maxFilesize) !== 'undefined' && options.maxFilesize instanceof Object === true) {
160 | const {limit, message} = options.maxFilesize
161 | options.maxFilesize = limit
162 | options.dictFileTooBig = this.cleanupMessage(message)
163 | }
164 |
165 | if (typeof options.acceptedFiles !== 'undefined' &&
166 | options.acceptedFiles !== null) {
167 | switch (Object.prototype.toString.call(options.acceptedFiles)) {
168 | case '[object String]':
169 | // already formatted for Dropzone
170 | break
171 | case '[object Array]':
172 | options.acceptedFiles = options.acceptedFiles.join(',')
173 | break
174 | case '[object Object]':
175 | const {extensions, message} = options.acceptedFiles
176 | options.acceptedFiles = extensions.join(',')
177 | options.dictInvalidFileType = this.cleanupMessage(message)
178 | break
179 | default:
180 | // improperly formatted, revert to Dropzone default value
181 | options.acceptedFiles = null
182 | }
183 | }
184 |
185 | /**
186 | * Instantiating uploader
187 | */
188 | this.uploader = new Uploader(options)
189 | this.bindEvents()
190 | this.uploader.mount(this.$el.firstElementChild)
191 | this.onInit(this)
192 | }
193 |
194 | // DEFINING METHODS
195 | component.methods = {}
196 |
197 | /**
198 | * Listening for uploader events.
199 | *
200 | * @param {Object}
201 | */
202 | component.methods.bindEvents = function () {
203 | this.uploader.on('addedfile', this.addedFile.bind(this))
204 | this.uploader.on('removedfile', this.removedFile.bind(this))
205 | this.uploader.on('sending', this.sending.bind(this))
206 | this.uploader.on('complete', this.complete.bind(this))
207 | this.uploader.on('error', this.error.bind(this))
208 | this.uploader.on('uploadprogress', this.uploadProgress.bind(this))
209 | this.uploader.on('thumbnail', this.thumbnail.bind(this))
210 | this.uploader.on('drop', this.drop.bind(this))
211 | this.uploader.on('dragenter', this.dragEnter.bind(this))
212 | this.uploader.on('dragleave', this.dragLeave.bind(this))
213 | this.uploader.on('totaluploadprogress', this.totalUploadProgress.bind(this))
214 | this.uploader.on('maxfilesexceeded', this.maxFilesExceeded.bind(this))
215 | this.uploader.on('queuecomplete', this.queueComplete.bind(this))
216 | }
217 |
218 | /**
219 | * Returns file instance of a unique file id or
220 | * an empty object
221 | *
222 | * @param {Symbol} blobId
223 | *
224 | * @return {Object}
225 | */
226 | component.methods.getFile = function (blobId) {
227 | let matchedFile = {}
228 | this.files.forEach((file) => {
229 | if (file._file.blobId === blobId) {
230 | matchedFile = file
231 | }
232 | })
233 | return matchedFile
234 | }
235 |
236 | /**
237 | * Adds file to the list of local files object
238 | * with a unique symbol key. Same is required
239 | * for update the file object to keep it
240 | * reactive.
241 | *
242 | * Also invokes the onAddedFile prop.
243 | *
244 | * @param {Object} file
245 | */
246 | component.methods.addedFile = function (file) {
247 | const fileId = SymbolFallback()
248 | file.blobId = fileId
249 | this.files.push(new File(file))
250 | this.onAddedFile(this.getFile(fileId))
251 | }
252 |
253 | /**
254 | * Removes the file from the files list and invokes
255 | * the onRemovedFile prop
256 | *
257 | * @param {Symbol} options.blobId
258 | */
259 | component.methods.removedFile = function ({ blobId }) {
260 | const fileInstance = this.getFile(blobId)
261 | fileInstance.updateStatus('removed')
262 | this.onRemovedFile(fileInstance)
263 | }
264 |
265 | /**
266 | * Listens for sending event and calls onSending
267 | * prop
268 | *
269 | * @param {Symbol} options.blobId
270 | * @param {Object} xhr
271 | * @param {Object} formData
272 | */
273 | component.methods.sending = function ({ blobId }, xhr, formData) {
274 | const fileInstance = this.getFile(blobId)
275 | this.onSending(fileInstance, xhr, formData)
276 | }
277 |
278 | /**
279 | * Updates the file status on completion
280 | *
281 | * @param {Symbol} options.blobId
282 | * @param {String} options.status
283 | */
284 | component.methods.complete = function ({ blobId, status, xhr = {} }) {
285 | const fileInstance = this.getFile(blobId)
286 | fileInstance.updateStatus(status)
287 | fileInstance.updateXhrResponse({
288 | response: xhr.response,
289 | responseText: xhr.responseText,
290 | statusCode: xhr.status
291 | })
292 | this.onComplete(fileInstance, status, xhr)
293 | }
294 |
295 | /**
296 | * Update the file error message to be used for
297 | * displaying the error
298 | *
299 | * @param {Symbol} options.blobId
300 | * @param {String} options.status
301 | * @param {String} errorMessage
302 | */
303 | component.methods.error = function ({ blobId, status }, errorMessage) {
304 | const fileInstance = this.getFile(blobId)
305 | fileInstance.updateStatus(status)
306 | fileInstance.updateErrorMessage(errorMessage)
307 | }
308 |
309 | /**
310 | * Updates file progress and bytes sent
311 | *
312 | * @param {Symbol} options.blobId
313 | * @param {Number} progress
314 | * @param {Number} bytesSent
315 | */
316 | component.methods.uploadProgress = function ({ blobId }, progress, bytesSent) {
317 | const fileInstance = this.getFile(blobId)
318 | fileInstance.updateProgress(progress)
319 | fileInstance.updateBytesSent(bytesSent)
320 | }
321 |
322 | /**
323 | * Updates file thumbnail, only in case of image uploads
324 | *
325 | * @param {Symbol} options.blobId
326 | * @param {String} dataUrl
327 | */
328 | component.methods.thumbnail = function ({ blobId }, dataUrl) {
329 | const fileInstance = this.getFile(blobId)
330 | fileInstance.updateDataUrl(dataUrl)
331 | }
332 |
333 | /**
334 | * Listen for drop event and call onDrop
335 | * and onDragLeave prop.
336 | */
337 | component.methods.drop = function () {
338 | this.dragCounter = 0
339 | this.onDrop()
340 | this.onDragLeave()
341 | }
342 |
343 | /**
344 | * Listen for dragenter event and call onDragEnter
345 | * prop. Also increment the drag counter, required
346 | * for handling browser flickering issues.
347 | *
348 | * @param {Object} event
349 | */
350 | component.methods.dragEnter = function (event) {
351 | event.preventDefault()
352 | this.dragCounter++
353 | this.onDragEnter()
354 | }
355 |
356 | /**
357 | * Listen for dragleave event and call onDragLeave
358 | * prop. Also decrement the drag counter, required
359 | * for handling browser flickering issues.
360 | */
361 | component.methods.dragLeave = function () {
362 | this.dragCounter--
363 | if (this.dragCounter === 0) {
364 | this.onDragLeave()
365 | }
366 | }
367 |
368 | /**
369 | * Listen for totaluploadprogress event and call
370 | * onTotalProgress prop.
371 | *
372 | * @param {Spread} args
373 | */
374 | component.methods.totalUploadProgress = function (...args) {
375 | this.onTotalProgress(...args)
376 | }
377 |
378 | /**
379 | * Listen for queuecomplete event and call
380 | * onQueueComplete prop.
381 | *
382 | */
383 | component.methods.queueComplete = function () {
384 | this.onQueueComplete()
385 | }
386 |
387 | /**
388 | * Listen for maxfilesreached event and call
389 | * onMaxFiles prop.
390 | *
391 | * @param {Symbol} blobId
392 | *
393 | */
394 | component.methods.maxFilesExceeded = function ({ blobId }) {
395 | const fileInstance = this.getFile(blobId)
396 | this.onMaxFiles(fileInstance)
397 | }
398 |
399 | /**
400 | * Removes file from the uploader. This file will
401 | * not receive any more events.
402 | *
403 | * @param {Object} file
404 | */
405 | component.methods.removeFile = function (file) {
406 | this.uploader.removeFile(file._file)
407 | }
408 |
409 | /**
410 | * Add a native file object directly to dropzone.
411 | *
412 | * @param {Object} file
413 | */
414 | component.methods.addFile = function (file) {
415 | this.uploader.addFile(file)
416 | }
417 |
418 | /**
419 | * Remove all files.
420 | *
421 | * @param {Boolean} cancelQueued
422 | */
423 | component.methods.removeAllFiles = function (cancelQueued) {
424 | this.uploader.removeAllFiles(cancelQueued)
425 | }
426 |
427 | /**
428 | * Cleans up message by removing spaces within the curly braces
429 | *
430 | * @param {String} message
431 | * @return {String}
432 | */
433 | component.methods.cleanupMessage = function (message) {
434 | return message.replace(/{{\s*?(\w+)\s*?}}/g, (match, group) => `{{${group}}}`)
435 | }
436 |
437 | export default component
438 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var testsContext = require.context('./', true, /\.spec$/)
2 | testsContext.keys().forEach(testsContext)
3 |
--------------------------------------------------------------------------------
/test/integration/clip.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Vue from 'vue'
4 | import { assert } from 'chai'
5 | import Clip from '../../src/components/Clip/index'
6 | import Uploader from '../../src/Uploader'
7 |
8 | describe('Clip', function () {
9 | before(function () {
10 | Vue.component('vue-clip', Clip)
11 | this.Component = Vue.component('vue-clip')
12 | })
13 |
14 | it('should have uploader property on the data object', function () {
15 | assert.isNull(Clip.data().uploader)
16 | })
17 |
18 | it('should have files property on the data object', function () {
19 | assert.deepEqual(Clip.data().files, [])
20 | })
21 |
22 | it('should have dragCounter property on the data object', function () {
23 | assert.equal(Clip.data().dragCounter, 0)
24 | })
25 |
26 | it('should instantiate uploader when component is mounted', function () {
27 | const component = new this.Component({
28 | propsData: {
29 | options: {
30 | url: '/upload'
31 | }
32 | }
33 | })
34 | component.$mount()
35 | assert.instanceOf(component.uploader, Uploader)
36 | })
37 |
38 | it('should set uploader preview template to component preview template element', function () {
39 | const component = new this.Component({
40 | propsData: {
41 | options: {
42 | url: '/upload'
43 | }
44 | }
45 | })
46 | component.$mount()
47 | assert.equal(
48 | component.uploader._options.previewTemplate,
49 | component.$refs['clip-preview-template'].innerHTML
50 | )
51 | })
52 |
53 | it('should set maxFiles property on options object when defined', function () {
54 | const component = new this.Component({
55 | propsData: {
56 | options: {
57 | url: '/upload',
58 | maxFiles: 2
59 | }
60 | }
61 | })
62 | component.$mount()
63 | assert.equal(component.uploader._options.maxFiles, 2)
64 | })
65 |
66 | it('should set maxFiles property and dictMaxFilesExceeded message on options object when defined', function () {
67 | const component = new this.Component({
68 | propsData: {
69 | options: {
70 | url: '/upload',
71 | maxFiles: {
72 | limit: 2,
73 | message: 'Sorry too many files!'
74 | }
75 | }
76 | }
77 | })
78 | component.$mount()
79 | assert.equal(component.uploader._options.maxFiles, 2)
80 | assert.equal(component.uploader._options.dictMaxFilesExceeded, 'Sorry too many files!')
81 | })
82 |
83 | it('should set maxFilesize property on options object when defined', function () {
84 | const component = new this.Component({
85 | propsData: {
86 | options: {
87 | url: '/upload',
88 | maxFilesize: 4
89 | }
90 | }
91 | })
92 | component.$mount()
93 | assert.equal(component.uploader._options.maxFilesize, 4)
94 | })
95 |
96 | it('should set maxFilesize property and dictFileTooBig message on options object when defined', function () {
97 | const component = new this.Component({
98 | propsData: {
99 | options: {
100 | url: '/upload',
101 | maxFilesize: {
102 | limit: 4,
103 | message: '{{filesize}} is greater than {{ maxFilesize }}'
104 | }
105 | }
106 | }
107 | })
108 | component.$mount()
109 | assert.equal(component.uploader._options.maxFilesize, 4)
110 | assert.equal(component.uploader._options.dictFileTooBig, '{{filesize}} is greater than {{maxFilesize}}')
111 | })
112 |
113 | it('should set acceptedFiles property on options object when defined as array', function () {
114 | const component = new this.Component({
115 | propsData: {
116 | options: {
117 | url: '/upload',
118 | acceptedFiles: ['image/*', 'application/pdf']
119 | }
120 | }
121 | })
122 | component.$mount()
123 | assert.equal(component.uploader._options.acceptedFiles, 'image/*,application/pdf')
124 | })
125 |
126 | it('should set acceptedFiles property and dictInvalidFileType message on options object when defined as object', function () {
127 | const component = new this.Component({
128 | propsData: {
129 | options: {
130 | url: '/upload',
131 | acceptedFiles: {
132 | extensions: ['image/*', 'application/pdf'],
133 | message: 'That is a different file type'
134 | }
135 | }
136 | }
137 | })
138 | component.$mount()
139 | assert.equal(component.uploader._options.acceptedFiles, 'image/*,application/pdf')
140 | assert.equal(component.uploader._options.dictInvalidFileType, 'That is a different file type')
141 | })
142 |
143 | it('should set acceptedFiles property on options object when defined as string', function () {
144 | const component = new this.Component({
145 | propsData: {
146 | options: {
147 | url: '/upload',
148 | acceptedFiles: 'image/*,application/pdf'
149 | }
150 | }
151 | })
152 | component.$mount()
153 | assert.equal(component.uploader._options.acceptedFiles, 'image/*,application/pdf')
154 | })
155 |
156 | it('should set acceptedFiles property to null on options object when defined with invalid type', function () {
157 | const component = new this.Component({
158 | propsData: {
159 | options: {
160 | url: '/upload',
161 | acceptedFiles: /^image\/.*/
162 | }
163 | }
164 | })
165 | component.$mount()
166 | assert.isNull(component.uploader._options.acceptedFiles)
167 | })
168 |
169 | it('should assign file a blobId when file has been added', function () {
170 | const component = new this.Component({
171 | propsData: {
172 | options: {
173 | url: '/upload'
174 | }
175 | }
176 | })
177 | component.$mount()
178 | const file = {
179 | name: 'foo.jpg',
180 | upload: {}
181 | }
182 | component.addedFile(file)
183 | assert.isDefined(file.blobId)
184 | })
185 |
186 | it('should push newly added file to the list of files', function () {
187 | const component = new this.Component({
188 | propsData: {
189 | options: {
190 | url: '/upload'
191 | }
192 | }
193 | })
194 | component.$mount()
195 | const file = {
196 | name: 'foo.jpg',
197 | upload: {}
198 | }
199 | component.addedFile(file)
200 | assert.deepEqual(component.files[0]._file, file)
201 | })
202 |
203 | it('should set file status to removed and call onRemovedFile prop when file is removed', function () {
204 | let onRemovedFileCalled = false
205 | const component = new this.Component({
206 | propsData: {
207 | options: {
208 | url: '/upload'
209 | },
210 | onRemovedFile: function () {
211 | onRemovedFileCalled = true
212 | }
213 | }
214 | })
215 | component.$mount()
216 | const file = {
217 | name: 'foo.jpg',
218 | upload: {}
219 | }
220 | component.addedFile(file)
221 | component.removedFile(file)
222 | assert.equal(component.files[0].status, 'removed')
223 | assert.equal(onRemovedFileCalled, true)
224 | })
225 |
226 | it('should update the file status when complete event is received', function () {
227 | const component = new this.Component({
228 | propsData: {
229 | options: {
230 | url: '/upload'
231 | }
232 | }
233 | })
234 | component.$mount()
235 | const file = {
236 | name: 'foo.jpg',
237 | upload: {}
238 | }
239 | component.addedFile(file)
240 | component.complete({blobId: file.blobId, status: 'success'})
241 | assert.equal(component.files[0].status, 'success')
242 | })
243 |
244 | it('should update the file status and errorMessage when error event is received', function () {
245 | const component = new this.Component({
246 | propsData: {
247 | options: {
248 | url: '/upload'
249 | }
250 | }
251 | })
252 | component.$mount()
253 | const file = {
254 | name: 'foo.jpg',
255 | upload: {}
256 | }
257 | component.addedFile(file)
258 | component.error({blobId: file.blobId, status: 'error'}, 'Something bad happened')
259 | assert.equal(component.files[0].status, 'error')
260 | assert.equal(component.files[0].errorMessage, 'Something bad happened')
261 | })
262 |
263 | it('should cleanup the message by removing spaces within the double curly braces', function () {
264 | const component = new this.Component({
265 | propsData: {
266 | options: {
267 | url: '/upload'
268 | }
269 | }
270 | })
271 | assert.equal(component.cleanupMessage('The {{ filesize }} is too big'), 'The {{filesize}} is too big')
272 | })
273 |
274 | it('should cleanup the message by removing spaces within the double curly braces for multiple instances', function () {
275 | const component = new this.Component({
276 | propsData: {
277 | options: {
278 | url: '/upload'
279 | }
280 | }
281 | })
282 | assert.equal(component.cleanupMessage('The {{ filesize }} is greater than {{ maxFilesize }}'), 'The {{filesize}} is greater than {{maxFilesize}}')
283 | })
284 | })
285 |
--------------------------------------------------------------------------------
/test/unit/file.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import File from '../../src/File'
4 | import { assert } from 'chai'
5 |
6 | describe('File', function () {
7 | it('should be able to instantiate a file instance', function () {
8 | const file = new File({
9 | upload: {}
10 | })
11 | assert.instanceOf(file, File)
12 | })
13 |
14 | it('should set instance properties from file properties', function () {
15 | const file = new File({
16 | status: 'queued',
17 | name: 'foo.jpg',
18 | width: 0,
19 | height: 0,
20 | type: 'image/jpeg',
21 | size: 10,
22 | upload: {}
23 | })
24 | assert.equal(file.status, 'queued')
25 | assert.equal(file.name, 'foo.jpg')
26 | assert.equal(file.width, 0)
27 | assert.equal(file.height, 0)
28 | assert.equal(file.type, 'image/jpeg')
29 | assert.equal(file.size, 10)
30 | })
31 |
32 | it('should be able to update file status', function () {
33 | const file = new File({
34 | status: 'queued',
35 | upload: {}
36 | })
37 | assert.equal(file.status, 'queued')
38 | file.updateStatus('success')
39 | assert.equal(file.status, 'success')
40 | })
41 |
42 | it('should be able to update file progress', function () {
43 | const file = new File({
44 | status: 'queued',
45 | upload: {}
46 | })
47 | file.updateProgress(10)
48 | assert.equal(file.progress, 10)
49 | })
50 |
51 | it('should be able to update bytesSent', function () {
52 | const file = new File({
53 | status: 'queued',
54 | upload: {}
55 | })
56 | file.updateBytesSent(10)
57 | assert.equal(file.bytesSent, 10)
58 | })
59 |
60 | it('should be able to update xhrResponse', function () {
61 | const file = new File({
62 | status: 'queued',
63 | upload: {}
64 | })
65 | file.updateXhrResponse({
66 | responseText: 'foo'
67 | })
68 | assert.equal(file.xhrResponse.responseText, 'foo')
69 | })
70 |
71 | it('should be able to update error message', function () {
72 | const file = new File({
73 | status: 'queued',
74 | upload: {}
75 | })
76 | file.updateErrorMessage('There was an error')
77 | assert.equal(file.errorMessage, 'There was an error')
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/test/unit/uploader.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Uploader from '../../src/Uploader'
4 | import Dropzone from 'dropzone'
5 | import { assert } from 'chai'
6 |
7 | function getUploaderElem () {
8 | const uploaderElem = document.createElement('div')
9 | document.querySelector('body').appendChild(uploaderElem)
10 | return uploaderElem
11 | }
12 |
13 | describe('Uploader', function () {
14 | beforeEach(function () {
15 | document.querySelector('body').innerHTML = ''
16 | })
17 |
18 | it('should throw exception when calling mount method without dom element', function () {
19 | try {
20 | const uploader = new Uploader()
21 | uploader.mount()
22 | assert.notExists(uploader._uploader)
23 | } catch (e) {
24 | assert.equal(e.message, 'Invalid dropzone element.')
25 | }
26 | })
27 |
28 | it('should throw exception exception when calling mount without upload url', function () {
29 | try {
30 | const uploader = new Uploader({})
31 | uploader.mount(getUploaderElem())
32 | assert.notExists(uploader._uploader)
33 | } catch (e) {
34 | assert.equal(e.message, 'No URL provided.')
35 | }
36 | })
37 |
38 | it('should initate the dropzone instance', function () {
39 | const uploader = new Uploader({
40 | url: '/upload'
41 | })
42 | uploader.mount(getUploaderElem())
43 | assert.instanceOf(uploader, Uploader)
44 | assert.instanceOf(uploader._uploader, Dropzone)
45 | })
46 |
47 | it('should call the init method when uploader is mounted', function () {
48 | let initCalled = false
49 | const uploader = new Uploader({
50 | url: '/upload',
51 | init: function () {
52 | initCalled = true
53 | }
54 | })
55 | uploader.mount(getUploaderElem())
56 | assert.equal(initCalled, true)
57 | })
58 |
59 | it('should bind hooks to the dropzone events', function () {
60 | let addedFile = function () {}
61 | const uploader = new Uploader({
62 | url: '/upload'
63 | })
64 | uploader.on('addedfile', addedFile)
65 | uploader.mount(getUploaderElem())
66 | assert.deepEqual(uploader._uploader._callbacks.addedfile[1], addedFile)
67 | })
68 |
69 | it('should clear hooks internal array when hooks have been binded', function () {
70 | let addedFile = function () {}
71 | const uploader = new Uploader({
72 | url: '/upload'
73 | })
74 | uploader.on('addedfile', addedFile)
75 | uploader.mount(getUploaderElem())
76 | assert.deepEqual(uploader._uploader._callbacks.addedfile[1], addedFile)
77 | assert.deepEqual(uploader._hooks, [])
78 | })
79 |
80 | it('should clear all hooks on cleaning up the uploader instance', function () {
81 | const uploader = new Uploader({
82 | url: '/upload'
83 | })
84 | uploader.on('addedfile', function () {})
85 | uploader.mount(getUploaderElem())
86 | uploader.destroy()
87 | setTimeout(function () {
88 | assert.deepEqual(uploader._uploader._callbacks.addedfile, [])
89 | })
90 | })
91 |
92 | it('should remove dropzone property from domElem on calling destroy', function () {
93 | const domElem = getUploaderElem()
94 | const uploader = new Uploader({
95 | url: '/upload'
96 | })
97 | uploader.on('addedfile', function () {})
98 | uploader.mount(domElem)
99 | assert.isDefined(domElem.dropzone)
100 | uploader.destroy()
101 | setTimeout(function () {
102 | assert.deepEqual(uploader._uploader._callbacks.addedfile, [])
103 | assert.equal(domElem.dropzone, undefined)
104 | })
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const UnminifiedWebpackPlugin = require('unminified-webpack-plugin')
4 |
5 | module.exports = {
6 | entry: './index.js',
7 | output: {
8 | path: './dist',
9 | filename: 'vue-clip.min.js',
10 | libraryTarget: 'umd'
11 | },
12 | module: {
13 | preLoaders: [
14 | {
15 | test: /\.js$/,
16 | loader: 'eslint',
17 | exclude: /node_modules/
18 | }
19 | ],
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | exclude: /node_modules/,
24 | loader: 'babel'
25 | }
26 | ]
27 | },
28 | plugins: [
29 | new UnminifiedWebpackPlugin()
30 | ],
31 | resolve: {
32 | alias: {
33 | vue: 'vue/dist/vue.js'
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------