├── .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 | Version 7 | Build Status 8 | Downloads 9 | License 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 310 | 311 | 324 | ``` 325 | 326 | #### onDragEnter 327 | This event is invoked as soon as the user starts dragging the file. 328 | 329 | ```html 330 | 334 | 335 | 346 | ``` 347 | 348 | #### onDragLeave 349 | This event is invoked when the user stops dragging the file. 350 | 351 | ```html 352 | 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 | 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 | 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 | 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 | 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 | 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 = `
20 | 21 | 22 | 23 |
` 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 | --------------------------------------------------------------------------------