├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── storybook.yml ├── .gitignore ├── .storybook ├── main.js ├── manager.js └── preview.js ├── .travis.yml ├── README.md ├── babel.config.js ├── docs-build ├── 0.0a0da810.iframe.bundle.js ├── 0.0a0da810.iframe.bundle.js.LICENSE.txt ├── 0.0a0da810.iframe.bundle.js.map ├── 0.c481ebebccf6cf7791d4.manager.bundle.js ├── 1.d9bcdec1.iframe.bundle.js ├── 1.d9bcdec1.iframe.bundle.js.LICENSE.txt ├── 1.d9bcdec1.iframe.bundle.js.map ├── 10.96f61faf.iframe.bundle.js ├── 2.592440bb.iframe.bundle.js ├── 2.592440bb.iframe.bundle.js.LICENSE.txt ├── 2.592440bb.iframe.bundle.js.map ├── 3.9ddf69ae.iframe.bundle.js ├── 4.68ba4924.iframe.bundle.js ├── 4.ee13f2332116a21a043c.manager.bundle.js ├── 4.ee13f2332116a21a043c.manager.bundle.js.LICENSE.txt ├── 5.5ea178aba7a79cc1f80c.manager.bundle.js ├── 6.b5c9485ad58686e8e5c7.manager.bundle.js ├── 6.b5c9485ad58686e8e5c7.manager.bundle.js.LICENSE.txt ├── 7.5fe523497e461d9dafc1.manager.bundle.js ├── 8.41a2ce18.iframe.bundle.js ├── 8.b265724e4f55ef44d453.manager.bundle.js ├── 9.9e894c1e.iframe.bundle.js ├── 9.9e894c1e.iframe.bundle.js.LICENSE.txt ├── 9.9e894c1e.iframe.bundle.js.map ├── abduction.svg ├── apocalypse.svg ├── custom.scss ├── demo.gif ├── favicon.ico ├── iframe.html ├── index.html ├── main.db713139302bc99ecb92.manager.bundle.js ├── main.fbbd2114.iframe.bundle.js ├── mockServiceWorker.js ├── runtime~main.6b50e652.iframe.bundle.js ├── runtime~main.8b7f9aa4bea89820a038.manager.bundle.js ├── vendors~main.76607514.iframe.bundle.js ├── vendors~main.76607514.iframe.bundle.js.LICENSE.txt ├── vendors~main.76607514.iframe.bundle.js.map ├── vendors~main.7e0cd44f0827ad19d8a5.manager.bundle.js └── vendors~main.7e0cd44f0827ad19d8a5.manager.bundle.js.LICENSE.txt ├── docs ├── 0.53d8b394ac94e493ab11.bundle.js ├── 0.ca6dfbc962cbceae684e.bundle.js ├── 4.53d8b394ac94e493ab11.bundle.js ├── 4.53d8b394ac94e493ab11.bundle.js.LICENSE.txt ├── 4.53d8b394ac94e493ab11.bundle.js.map ├── 4.b0f882987c69531acc56.bundle.js ├── 4.b0f882987c69531acc56.bundle.js.LICENSE.txt ├── 5.53d8b394ac94e493ab11.bundle.js ├── 5.c9e01036a8c89a844715.bundle.js ├── 6.1119ede1b201480f9a79.bundle.js ├── 6.1119ede1b201480f9a79.bundle.js.LICENSE.txt ├── 6.53d8b394ac94e493ab11.bundle.js ├── 6.53d8b394ac94e493ab11.bundle.js.LICENSE.txt ├── 6.53d8b394ac94e493ab11.bundle.js.map ├── 7.53d8b394ac94e493ab11.bundle.js ├── 7.6dcce8c646381a526e8b.bundle.js ├── 8.f1e1296e6801446b1927.bundle.js ├── abduction.svg ├── apocalypse.svg ├── custom.scss ├── demo.gif ├── favicon.ico ├── iframe.html ├── index.html ├── main.515ae4e5df417ac473a0.bundle.js ├── main.53d8b394ac94e493ab11.bundle.js ├── mockServiceWorker.js ├── runtime~main.53d8b394ac94e493ab11.bundle.js ├── runtime~main.674894373b36b299f021.bundle.js ├── vendors~main.53d8b394ac94e493ab11.bundle.js ├── vendors~main.53d8b394ac94e493ab11.bundle.js.LICENSE.txt ├── vendors~main.53d8b394ac94e493ab11.bundle.js.map ├── vendors~main.f97bf6338e2cf12e2f2e.bundle.js └── vendors~main.f97bf6338e2cf12e2f2e.bundle.js.LICENSE.txt ├── jest.config.js ├── mocks └── browser.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── rollup.config.js ├── server ├── app.js ├── package-lock.json ├── package.json └── routes │ └── web.js ├── src ├── DropZone.vue ├── assets │ ├── animations.scss │ ├── basic.scss │ ├── icons.scss │ └── main.scss ├── hooks │ ├── config.js │ ├── drag.js │ ├── hiddenIpuntFile.js │ ├── itemManager.js │ ├── thumbnail.js │ ├── uploadQueue.js │ └── uploadXHR.js ├── index.js ├── props.js └── utils │ ├── index.js │ ├── minetypes.js │ └── status.js ├── stories ├── DropZone.emits.stories.js ├── DropZone.slots.stories.js ├── DropZone.styles.stories.js ├── DropZone.upload.stories.js ├── DropZone.xhrOptions.stories.js ├── assets │ ├── abduction.svg │ ├── apocalypse.svg │ ├── custom.scss │ ├── demo.gif │ └── mockServiceWorker.js └── controls.js └── tests └── unit ├── DropZone.spec.js └── __snapshots__ └── DropZone.spec.js.snap /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/airbnb', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020, 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | '**/__tests__/*.{j,t}s?(x)', 21 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 22 | ], 23 | env: { 24 | jest: true, 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | paths: ["stories/**", "src/**"] # Trigger the action only when files change in the folders defined here 5 | jobs: 6 | build-and-deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 🛎️ 10 | uses: actions/checkout@v2.3.1 11 | with: 12 | persist-credentials: false 13 | - name: Install and Build 🔧 14 | run: | # Install npm packages and build the Storybook files 15 | npm install 16 | npm run build-storybook 17 | - name: Deploy 🚀 18 | uses: JamesIves/github-pages-deploy-action@3.6.2 19 | with: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | BRANCH: main # The branch the action should deploy to. 22 | FOLDER: docs-build # The folder that the build-storybook script generates files. 23 | CLEAN: true # Automatically remove deleted files from the deploy branch 24 | TARGET_FOLDER: docs # The folder that we serve our Storybook files from 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | server/node_modules/ 5 | server/upload 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | "stories": [ 5 | "../stories/**/*.stories.mdx", 6 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 7 | ], 8 | "addons": [ 9 | "@storybook/addon-links", 10 | "@storybook/addon-essentials", 11 | ], 12 | webpackFinal: async (config) => { 13 | config.resolve.alias = { 14 | ...config.resolve.alias, 15 | "@": path.resolve(__dirname, "../src/"), 16 | }; 17 | // Make whatever fine-grained changes you need 18 | config.module.rules.push({ 19 | test: /\.scss$/, 20 | use: ['style-loader', 'css-loader', 'sass-loader'], 21 | include: path.resolve(__dirname, '../'), 22 | }); 23 | // keep this if you're doing typescript 24 | // config.resolve.extensions.push(".ts", ".tsx"); 25 | return config; 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | import { themes } from '@storybook/theming'; 3 | 4 | addons.setConfig({ 5 | theme: themes.dark, 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | } 4 | import Dropdown from '@/index' 5 | import { app } from '@storybook/vue3'; 6 | 7 | app.use(Dropdown); 8 | 9 | if (typeof global.process === 'undefined') { 10 | const { worker } = require('../mocks/browser') 11 | worker.start({ 12 | serviceWorker: { 13 | // Points to the custom location of the Service Worker file. 14 | url: 'https://darknessnerd.github.io/drop-zone/mockServiceWorker.js' 15 | } 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - lts/* 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm run lint 10 | - npm run test:unit 11 | before_deploy: 12 | - npm run build 13 | deploy: 14 | provider: npm 15 | email: "$NPM_EMAIL" 16 | api_key: "$NPM_TOKEN" 17 | skip_cleanup: true 18 | on: 19 | tags: true 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dropzone-vue 2 | 3 | [![Build Status](https://www.travis-ci.com/darknessnerd/drop-zone.svg?branch=main)](https://www.travis-ci.com/darknessnerd/drop-zone) 4 | ![NPM Downloads](https://img.shields.io/npm/dw/dropzone-vue) 5 | ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/dropzone-vue) 6 | ![NPM License](https://img.shields.io/npm/l/dropzone-vue) 7 | ![NPM Version](https://img.shields.io/npm/v/dropzone-vue) 8 | ![npm collaborators](https://img.shields.io/npm/collaborators/dropzone-vue) 9 | 10 | :bomb:
11 | [Features Live Demo Link: Click here !! ](https://darknessnerd.github.io/drop-zone/index.html) 12 | 13 | > Vue3 Library Component for drag’n’drop file uploads with image previews. 14 | 15 | ![demo](https://github.com/darknessnerd/drop-zone/blob/main/stories/assets/demo.gif?raw=true) 16 | 17 | ### :rocket: Features 18 | 19 | * No dependencies 20 | * Drag and drop file uploads 21 | * Custom accepted file types 22 | * XHR custom: Header, url, method and form data. 23 | * Parallel upload with different request 24 | * Multiple upload files in a single request 25 | * Chunking 26 | * Custom styling 27 | * Events 28 | * Provide your own markup for drop, error and success message 29 | 30 | ## Install and basic usage 31 | 32 | ```bash 33 | $ npm install --save dropzone-vue 34 | ``` 35 | Register the component 36 | 37 | ```js 38 | import DropZone from 'dropzone-vue'; 39 | 40 | // optionally import default styles 41 | import 'dropzone-vue/dist/dropzone-vue.common.css'; 42 | 43 | createApp(App) 44 | .use(DropZone) 45 | .mount('#app'); 46 | ``` 47 | 48 | Now your component inside a code: 49 | 50 | ```vue 51 | 61 | 62 | 77 | 78 | ``` 79 | 80 | 81 | ### Props 82 | 83 | #### url 84 | Type: `String`
85 | Required: `false`
86 | Default: `window.localtion` 87 | 88 | Upload url 89 | 90 | ```html 91 | 92 | ``` 93 | 94 | #### method 95 | Type: `String`
96 | Required: `false`
97 | Default: `POST` 98 | 99 | Upload method can be POST or PUT 100 | 101 | ```html 102 | 103 | ``` 104 | 105 | #### headers 106 | Type: `Object`
107 | Required: `false`
108 | Default: `{}` 109 | 110 | Send additional headers to the server. 111 | 112 | ```html 113 | 114 | ``` 115 | #### paramName 116 | Type: `String`
117 | Required: `false`
118 | Default: `file` 119 | 120 | Formdata key for file upload request 121 | 122 | ```html 123 | 124 | ``` 125 | 126 | #### xhrTimeout 127 | Type: `number`
128 | Required: `false`
129 | Default: `60000` 130 | 131 | The timeout for the XHR requests in milliseconds 132 | 133 | ```html 134 | 135 | ``` 136 | 137 | #### withCredentials 138 | Type: `boolean`
139 | Required: `false`
140 | Default: `false` 141 | 142 | withCredentials option for XHR requests 143 | 144 | ```html 145 | 146 | ``` 147 | #### uploadOnDrop 148 | Type: `boolean`
149 | Required: `false`
150 | Default: `true` 151 | 152 | Process the upload automatically on drop or on file selection 153 | if it's set to true 154 | 155 | ```html 156 | 157 | ``` 158 | 159 | if it's set to false, the upload can be triggered with: 160 | ```html 161 | 162 | ``` 163 | 164 | ```js 165 | dropzone.value.processQueue(); 166 | ``` 167 | 168 | #### retryOnError 169 | Type: `boolean`
170 | Required: `false`
171 | Default: `false` 172 | 173 | Retry an upload if it fail. 174 | 175 | ```html 176 | 177 | ``` 178 | 179 | #### multipleUpload 180 | Type: `boolean`
181 | Required: `false`
182 | Default: `false` 183 | 184 | Send more items in one request, this is disabled in case of the prop chunking is true. 185 | 186 | ```html 187 | 188 | ``` 189 | 190 | #### parallelUpload 191 | Type: `number`
192 | Required: `false`
193 | Default: `3` 194 | 195 | Parallel request upload to be processed 196 | 197 | ```html 198 | 199 | ``` 200 | 201 | #### maxFiles 202 | Type: `number`
203 | Required: `false`
204 | Default: `null` 205 | 206 | Max files number accepted by the Dropzone, if it not set 207 | there is no limit. 208 | 209 | ```html 210 | 211 | ``` 212 | 213 | #### maxFileSize 214 | Type: `number`
215 | Required: `false`
216 | Default: `1000000` 217 | 218 | Bytes value for the max upload size allowed, default 1mb 219 | 220 | ```html 221 | 222 | ``` 223 | 224 | #### hiddenInputContainer 225 | Type: `string | Element`
226 | Required: `false`
227 | Default: `body` 228 | 229 | Element or query selector where the hidden Input it's placed 230 | 231 | ```html 232 | 233 | ``` 234 | 235 | #### clickable 236 | Type: `boolean`
237 | Required: `false`
238 | Default: `true` 239 | 240 | If active enable the dropzone to be clickable and show the files selection 241 | ```html 242 | 243 | ``` 244 | 245 | #### acceptedFiles 246 | Type: `array`
247 | Required: `false`
248 | Default: `null` 249 | 250 | Array that contain the accepted files, possible values: 251 | ['image', 'doc', 'video', 'png', ... , 'audio' ] 252 | 253 | ```html 254 | 255 | ``` 256 | #### chunking 257 | Type: `boolean`
258 | Required: `false`
259 | Default: `false` 260 | 261 | Enable the upload chunking feature, 262 | if this is active the multipleUpload for request will be set to false. 263 | 264 | ```html 265 | 266 | ``` 267 | 268 | #### numberOfChunks 269 | Type: `number`
270 | Required: `false`
271 | Default: `10` 272 | 273 | If the chunking mode is active this property represents the number of 274 | chunks with which the file will be split 275 | 276 | ```html 277 | 278 | ``` 279 | 280 | #### dropzoneClassName 281 | Type: `string`
282 | Required: `false`
283 | Default: `dropzone__box` 284 | 285 | custom class for the dropzone 286 | 287 | ```html 288 | 289 | ``` 290 | #### dropzoneMessageClassName 291 | Type: `string`
292 | Required: `false`
293 | Default: `dropzone__message--style` 294 | 295 | custom class for the dropzone message 296 | 297 | ```html 298 | 299 | ``` 300 | 301 | #### dropzoneItemClassName 302 | Type: `string`
303 | Required: `false`
304 | Default: `dropzone__item--style` 305 | 306 | custom class for the dropzone item 307 | 308 | ```html 309 | 310 | ``` 311 | #### dropzoneDetailsClassName 312 | Type: `string`
313 | Required: `false`
314 | Default: `dropzone__details--style` 315 | 316 | custom class for the dropzone details 317 | 318 | ```html 319 | 320 | ``` 321 | 322 | 323 | 324 | 325 | ### Events 326 | 327 | #### config-update 328 | 329 | Parameters: 330 | * `config` config object with the new values 331 | 332 | Called when a props is changed 333 | 334 | ```html 335 | 336 | ``` 337 | 338 | 339 | #### added-file 340 | 341 | Parameters: 342 | * `item` {id: 'fileid', file: File} 343 | 344 | Called when a file is valid ( type and size ) and added to the queue. 345 | 346 | ```html 347 | 348 | ``` 349 | #### removed-file 350 | 351 | Parameters: 352 | * `item` {id: 'fileid', status: "DONE|ERROR|QUEUE", file: File} 353 | 354 | Called when a file is removed. 355 | 356 | ```html 357 | 358 | ``` 359 | #### uploaded 360 | 361 | Parameters: 362 | * `items` [{file: File}] 363 | 364 | Called when a file or files are uploaded. 365 | 366 | ```html 367 | 368 | ``` 369 | 370 | #### error-upload 371 | 372 | Parameters: 373 | * `error` {ids: Array(['fileid']), errorType: "error type"} 374 | 375 | Called when a file or files uploads fail. 376 | 377 | ```html 378 | 379 | ``` 380 | 381 | 382 | 383 | #### sending 384 | 385 | Parameters: 386 | * `files` Array(File) 387 | * `xhr` XMLHttpRequest 388 | * `formData` FormData 389 | 390 | Called when a file is going to be uploaded. 391 | 392 | ```html 393 | 394 | ``` 395 | 396 | #### error-add 397 | 398 | Parameters: 399 | * `files` Array(File) 400 | * `error` String {'INVALID_TYPE'|'MAX_FILE'|'MAX_FILE_SIZE'} 401 | 402 | Called when a file is not added for one of this reason 403 | * invalid type 404 | * max file size 405 | * max file number inside the dropzone 406 | 407 | ```html 408 | 409 | ``` 410 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /docs-build/0.0a0da810.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** @license React v0.19.1 2 | * scheduler.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | /** @license React v16.14.0 11 | * react-dom.production.min.js 12 | * 13 | * Copyright (c) Facebook, Inc. and its affiliates. 14 | * 15 | * This source code is licensed under the MIT license found in the 16 | * LICENSE file in the root directory of this source tree. 17 | */ 18 | -------------------------------------------------------------------------------- /docs-build/0.0a0da810.iframe.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"0.0a0da810.iframe.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs-build/1.d9bcdec1.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** @license React v16.13.1 2 | * react-is.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | -------------------------------------------------------------------------------- /docs-build/1.d9bcdec1.iframe.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"1.d9bcdec1.iframe.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs-build/2.592440bb.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /docs-build/2.592440bb.iframe.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"2.592440bb.iframe.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs-build/4.68ba4924.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[4],{"./node_modules/@storybook/preview-web/dist/esm/renderDocs.js":function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"renderDocs",(function(){return renderDocs})),__webpack_require__.d(__webpack_exports__,"unmountDocs",(function(){return unmountDocs}));__webpack_require__("./node_modules/@storybook/preview-web/node_modules/regenerator-runtime/runtime.js"),__webpack_require__("./node_modules/core-js/modules/es.promise.js"),__webpack_require__("./node_modules/core-js/modules/es.object.to-string.js");var react=__webpack_require__("./node_modules/react/index.js"),react_default=__webpack_require__.n(react),react_dom=__webpack_require__("./node_modules/react-dom/index.js"),react_dom_default=__webpack_require__.n(react_dom),wrapper={fontSize:"14px",letterSpacing:"0.2px",margin:"10px 0"},main={margin:"auto",padding:30,borderRadius:10,background:"rgba(0,0,0,0.03)"},heading={textAlign:"center"},NoDocs_NoDocs=function NoDocs(){return react_default.a.createElement("div",{style:wrapper,className:"sb-nodocs sb-wrapper"},react_default.a.createElement("div",{style:main},react_default.a.createElement("h1",{style:heading},"No Docs"),react_default.a.createElement("p",null,"Sorry, but there are no docs for the selected story. To add them, set the story's ",react_default.a.createElement("code",null,"docs")," parameter. If you think this is an error:"),react_default.a.createElement("ul",null,react_default.a.createElement("li",null,"Please check the story definition."),react_default.a.createElement("li",null,"Please check the Storybook config."),react_default.a.createElement("li",null,"Try reloading the page.")),react_default.a.createElement("p",null,"If the problem persists, check the browser console, or the terminal you've run Storybook from.")))};function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}function _asyncToGenerator(fn){return function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))}}function renderDocs(story,docsContext,element,callback){return function renderDocsAsync(_x,_x2,_x3){return _renderDocsAsync.apply(this,arguments)}(story,docsContext,element).then(callback)}function _renderDocsAsync(){return(_renderDocsAsync=_asyncToGenerator(regeneratorRuntime.mark((function _callee(story,docsContext,element){var _docs$getContainer,_docs$getPage,docs,DocsContainer,Page,docsElement;return regeneratorRuntime.wrap((function _callee$(_context){for(;;)switch(_context.prev=_context.next){case 0:if(!(null!=(docs=story.parameters.docs)&&docs.getPage||null!=docs&&docs.page)||(null!=docs&&docs.getContainer||null!=docs&&docs.container)){_context.next=3;break}throw new Error("No `docs.container` set, did you run `addon-docs/preset`?");case 3:if(_context.t1=docs.container,_context.t1){_context.next=8;break}return _context.next=7,null===(_docs$getContainer=docs.getContainer)||void 0===_docs$getContainer?void 0:_docs$getContainer.call(docs);case 7:_context.t1=_context.sent;case 8:if(_context.t0=_context.t1,_context.t0){_context.next=11;break}_context.t0=function(_ref){var children=_ref.children;return react_default.a.createElement(react_default.a.Fragment,null,children)};case 11:if(DocsContainer=_context.t0,_context.t3=docs.page,_context.t3){_context.next=17;break}return _context.next=16,null===(_docs$getPage=docs.getPage)||void 0===_docs$getPage?void 0:_docs$getPage.call(docs);case 16:_context.t3=_context.sent;case 17:if(_context.t2=_context.t3,_context.t2){_context.next=20;break}_context.t2=NoDocs_NoDocs;case 20:return Page=_context.t2,docsElement=react_default.a.createElement(DocsContainer,{key:story.componentId,context:docsContext},react_default.a.createElement(Page,null)),_context.next=24,new Promise((function(resolve){react_dom_default.a.render(docsElement,element,resolve)}));case 24:case"end":return _context.stop()}}),_callee)})))).apply(this,arguments)}function unmountDocs(element){react_dom_default.a.unmountComponentAtNode(element)}NoDocs_NoDocs.displayName="NoDocs"}}]); -------------------------------------------------------------------------------- /docs-build/4.ee13f2332116a21a043c.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /docs-build/6.b5c9485ad58686e8e5c7.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /docs-build/8.b265724e4f55ef44d453.manager.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{1204:function(module,exports){module.exports=function(e,n){return n=n||{},new Promise((function(t,r){var s=new XMLHttpRequest,o=[],u=[],i={},a=function(){return{ok:2==(s.status/100|0),statusText:s.statusText,status:s.status,url:s.responseURL,text:function(){return Promise.resolve(s.responseText)},json:function(){return Promise.resolve(s.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([s.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var l in s.open(n.method||"get",e,!0),s.onload=function(){s.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,(function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t})),t(a())},s.onerror=r,s.withCredentials="include"==n.credentials,n.headers)s.setRequestHeader(l,n.headers[l]);s.send(n.body||null)}))}}}]); -------------------------------------------------------------------------------- /docs-build/9.9e894c1e.iframe.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /docs-build/9.9e894c1e.iframe.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"9.9e894c1e.iframe.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs-build/abduction.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-build/apocalypse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-build/custom.scss: -------------------------------------------------------------------------------- 1 | .dropzoneCustomClass { 2 | background-color: whitesmoke; 3 | position: relative; 4 | display: flex; 5 | flex-flow: row nowrap; 6 | border: 4px solid greenyellow; 7 | } 8 | .dropzoneMessageClassName{ 9 | background-color: greenyellow; 10 | } 11 | .dropzoneItemClassName { 12 | 13 | } 14 | .dropzoneItemClassName:not(.dropzone--has-thumbnail) > .dropzone__item-thumbnail { 15 | background-color: darkgrey; 16 | border: 4px solid greenyellow; 17 | } 18 | .dropzoneItemClassName > .dropzone__item-thumbnail { 19 | background-color: whitesmoke; 20 | border: 4px solid black; 21 | } 22 | 23 | 24 | .dropzoneDetailsClassName { 25 | top: 18px; 26 | background-color: black; 27 | border: 3px solid whitesmoke; 28 | margin: 18px; 29 | color: greenyellow; 30 | width: 80%; 31 | } 32 | -------------------------------------------------------------------------------- /docs-build/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/docs-build/demo.gif -------------------------------------------------------------------------------- /docs-build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/docs-build/favicon.ico -------------------------------------------------------------------------------- /docs-build/index.html: -------------------------------------------------------------------------------- 1 | Webpack App
-------------------------------------------------------------------------------- /docs-build/main.db713139302bc99ecb92.manager.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[1],{530:function(module,exports,__webpack_require__){__webpack_require__(531),__webpack_require__(879),__webpack_require__(1119),__webpack_require__(1127),__webpack_require__(1128),__webpack_require__(1120),__webpack_require__(1123),__webpack_require__(1122),__webpack_require__(1124),__webpack_require__(1121),__webpack_require__(1125),module.exports=__webpack_require__(1126)},646:function(module,exports){},671:function(module,exports){},716:function(module,exports){},762:function(module,exports){},831:function(module,exports){},879:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__);var _storybook_addons__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(80),_storybook_theming__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(164);_storybook_addons__WEBPACK_IMPORTED_MODULE_0__.a.setConfig({theme:_storybook_theming__WEBPACK_IMPORTED_MODULE_1__.a.dark})}},[[530,2,3]]]); -------------------------------------------------------------------------------- /docs-build/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = 'f7d0ed371e596d181f62c6f68c4b7baf' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (activeClientIds.has(client.id)) { 118 | const clonedResponse = response.clone() 119 | 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: clonedResponse.body === null ? null : await clonedResponse.text(), 129 | headers: serializeHeaders(clonedResponse.headers), 130 | redirected: clonedResponse.redirected, 131 | }, 132 | }) 133 | } 134 | 135 | return response 136 | } 137 | 138 | async function getResponse(event, client, requestId) { 139 | const { request } = event 140 | const requestClone = request.clone() 141 | const getOriginalResponse = () => fetch(requestClone) 142 | 143 | // Bypass mocking when the request client is not active. 144 | if (!client) { 145 | return getOriginalResponse() 146 | } 147 | 148 | // Bypass initial page load requests (i.e. static assets). 149 | // The absence of the immediate/parent client in the map of the active clients 150 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 151 | // and is not ready to handle requests. 152 | if (!activeClientIds.has(client.id)) { 153 | return await getOriginalResponse() 154 | } 155 | 156 | // Bypass requests with the explicit bypass header 157 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 158 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 159 | 160 | // Remove the bypass header to comply with the CORS preflight check. 161 | delete cleanRequestHeaders[bypassHeaderName] 162 | 163 | const originalRequest = new Request(requestClone, { 164 | headers: new Headers(cleanRequestHeaders), 165 | }) 166 | 167 | return fetch(originalRequest) 168 | } 169 | 170 | // Send the request to the client-side MSW. 171 | const reqHeaders = serializeHeaders(request.headers) 172 | const body = await request.text() 173 | 174 | const clientMessage = await sendToClient(client, { 175 | type: 'REQUEST', 176 | payload: { 177 | id: requestId, 178 | url: request.url, 179 | method: request.method, 180 | headers: reqHeaders, 181 | cache: request.cache, 182 | mode: request.mode, 183 | credentials: request.credentials, 184 | destination: request.destination, 185 | integrity: request.integrity, 186 | redirect: request.redirect, 187 | referrer: request.referrer, 188 | referrerPolicy: request.referrerPolicy, 189 | body, 190 | bodyUsed: request.bodyUsed, 191 | keepalive: request.keepalive, 192 | }, 193 | }) 194 | 195 | switch (clientMessage.type) { 196 | case 'MOCK_SUCCESS': { 197 | return delayPromise( 198 | () => respondWithMock(clientMessage), 199 | clientMessage.payload.delay, 200 | ) 201 | } 202 | 203 | case 'MOCK_NOT_FOUND': { 204 | return getOriginalResponse() 205 | } 206 | 207 | case 'NETWORK_ERROR': { 208 | const { name, message } = clientMessage.payload 209 | const networkError = new Error(message) 210 | networkError.name = name 211 | 212 | // Rejecting a request Promise emulates a network error. 213 | throw networkError 214 | } 215 | 216 | case 'INTERNAL_ERROR': { 217 | const parsedBody = JSON.parse(clientMessage.payload.body) 218 | 219 | console.error( 220 | `\ 221 | [MSW] Request handler function for "%s %s" has thrown the following exception: 222 | 223 | ${parsedBody.errorType}: ${parsedBody.message} 224 | (see more detailed error stack trace in the mocked response body) 225 | 226 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 227 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 228 | `, 229 | request.method, 230 | request.url, 231 | ) 232 | 233 | return respondWithMock(clientMessage) 234 | } 235 | } 236 | 237 | return getOriginalResponse() 238 | } 239 | 240 | self.addEventListener('fetch', function (event) { 241 | const { request } = event 242 | 243 | // Bypass navigation requests. 244 | if (request.mode === 'navigate') { 245 | return 246 | } 247 | 248 | // Opening the DevTools triggers the "only-if-cached" request 249 | // that cannot be handled by the worker. Bypass such requests. 250 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 251 | return 252 | } 253 | 254 | // Bypass all requests when there are no active clients. 255 | // Prevents the self-unregistered worked from handling requests 256 | // after it's been deleted (still remains active until the next reload). 257 | if (activeClientIds.size === 0) { 258 | return 259 | } 260 | 261 | const requestId = uuidv4() 262 | 263 | return event.respondWith( 264 | handleRequest(event, requestId).catch((error) => { 265 | console.error( 266 | '[MSW] Failed to mock a "%s" request to "%s": %s', 267 | request.method, 268 | request.url, 269 | error, 270 | ) 271 | }), 272 | ) 273 | }) 274 | 275 | function serializeHeaders(headers) { 276 | const reqHeaders = {} 277 | headers.forEach((value, name) => { 278 | reqHeaders[name] = reqHeaders[name] 279 | ? [].concat(reqHeaders[name]).concat(value) 280 | : value 281 | }) 282 | return reqHeaders 283 | } 284 | 285 | function sendToClient(client, message) { 286 | return new Promise((resolve, reject) => { 287 | const channel = new MessageChannel() 288 | 289 | channel.port1.onmessage = (event) => { 290 | if (event.data && event.data.error) { 291 | return reject(event.data.error) 292 | } 293 | 294 | resolve(event.data) 295 | } 296 | 297 | client.postMessage(JSON.stringify(message), [channel.port2]) 298 | }) 299 | } 300 | 301 | function delayPromise(cb, duration) { 302 | return new Promise((resolve) => { 303 | setTimeout(() => resolve(cb()), duration) 304 | }) 305 | } 306 | 307 | function respondWithMock(clientMessage) { 308 | return new Response(clientMessage.payload.body, { 309 | ...clientMessage.payload, 310 | headers: clientMessage.payload.headers, 311 | }) 312 | } 313 | 314 | function uuidv4() { 315 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 316 | const r = (Math.random() * 16) | 0 317 | const v = c == 'x' ? r : (r & 0x3) | 0x8 318 | return v.toString(16) 319 | }) 320 | } 321 | -------------------------------------------------------------------------------- /docs-build/runtime~main.6b50e652.iframe.bundle.js: -------------------------------------------------------------------------------- 1 | !function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i 11 | * @license MIT 12 | */ 13 | 14 | /*! 15 | * cookie 16 | * Copyright(c) 2012-2014 Roman Shtylman 17 | * Copyright(c) 2015 Douglas Christopher Wilson 18 | * MIT Licensed 19 | */ 20 | 21 | /*! 22 | * https://github.com/es-shims/es5-shim 23 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 24 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 25 | */ 26 | 27 | /*! 28 | * https://github.com/paulmillr/es6-shim 29 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 30 | * and contributors, MIT License 31 | * es6-shim: v0.35.4 32 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 33 | * Details and documentation: 34 | * https://github.com/paulmillr/es6-shim/ 35 | */ 36 | 37 | /*! 38 | * isobject 39 | * 40 | * Copyright (c) 2014-2017, Jon Schlinkert. 41 | * Released under the MIT License. 42 | */ 43 | 44 | /*! ***************************************************************************** 45 | Copyright (c) Microsoft Corporation. 46 | 47 | Permission to use, copy, modify, and/or distribute this software for any 48 | purpose with or without fee is hereby granted. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 51 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 52 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 53 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 54 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 55 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 56 | PERFORMANCE OF THIS SOFTWARE. 57 | ***************************************************************************** */ 58 | 59 | /*! https://mths.be/punycode v1.3.2 by @mathias */ 60 | 61 | /** @license React v16.14.0 62 | * react.production.min.js 63 | * 64 | * Copyright (c) Facebook, Inc. and its affiliates. 65 | * 66 | * This source code is licensed under the MIT license found in the 67 | * LICENSE file in the root directory of this source tree. 68 | */ 69 | 70 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 71 | 72 | //! © 2018 Angry Bytes and contributors. MIT licensed. 73 | -------------------------------------------------------------------------------- /docs-build/vendors~main.76607514.iframe.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"vendors~main.76607514.iframe.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs-build/vendors~main.7e0cd44f0827ad19d8a5.manager.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * Fuse.js v3.6.1 - Lightweight fuzzy-search (http://fusejs.io) 9 | * 10 | * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me) 11 | * All Rights Reserved. Apache Software License 2.0 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | */ 15 | 16 | /*! 17 | * https://github.com/es-shims/es5-shim 18 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 19 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 20 | */ 21 | 22 | /*! 23 | * https://github.com/paulmillr/es6-shim 24 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 25 | * and contributors, MIT License 26 | * es6-shim: v0.35.4 27 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 28 | * Details and documentation: 29 | * https://github.com/paulmillr/es6-shim/ 30 | */ 31 | 32 | /*! 33 | * isobject 34 | * 35 | * Copyright (c) 2014-2017, Jon Schlinkert. 36 | * Released under the MIT License. 37 | */ 38 | 39 | /** 40 | * React Router DOM v6.0.2 41 | * 42 | * Copyright (c) Remix Software Inc. 43 | * 44 | * This source code is licensed under the MIT license found in the 45 | * LICENSE.md file in the root directory of this source tree. 46 | * 47 | * @license MIT 48 | */ 49 | 50 | /** 51 | * React Router v6.0.2 52 | * 53 | * Copyright (c) Remix Software Inc. 54 | * 55 | * This source code is licensed under the MIT license found in the 56 | * LICENSE.md file in the root directory of this source tree. 57 | * 58 | * @license MIT 59 | */ 60 | 61 | /** @license React v0.19.1 62 | * scheduler.production.min.js 63 | * 64 | * Copyright (c) Facebook, Inc. and its affiliates. 65 | * 66 | * This source code is licensed under the MIT license found in the 67 | * LICENSE file in the root directory of this source tree. 68 | */ 69 | 70 | /** @license React v16.13.1 71 | * react-is.production.min.js 72 | * 73 | * Copyright (c) Facebook, Inc. and its affiliates. 74 | * 75 | * This source code is licensed under the MIT license found in the 76 | * LICENSE file in the root directory of this source tree. 77 | */ 78 | 79 | /** @license React v16.14.0 80 | * react-dom.production.min.js 81 | * 82 | * Copyright (c) Facebook, Inc. and its affiliates. 83 | * 84 | * This source code is licensed under the MIT license found in the 85 | * LICENSE file in the root directory of this source tree. 86 | */ 87 | 88 | /** @license React v16.14.0 89 | * react.production.min.js 90 | * 91 | * Copyright (c) Facebook, Inc. and its affiliates. 92 | * 93 | * This source code is licensed under the MIT license found in the 94 | * LICENSE file in the root directory of this source tree. 95 | */ 96 | 97 | /** @license React v17.0.2 98 | * react-is.production.min.js 99 | * 100 | * Copyright (c) Facebook, Inc. and its affiliates. 101 | * 102 | * This source code is licensed under the MIT license found in the 103 | * LICENSE file in the root directory of this source tree. 104 | */ 105 | -------------------------------------------------------------------------------- /docs/4.53d8b394ac94e493ab11.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /docs/4.53d8b394ac94e493ab11.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"4.53d8b394ac94e493ab11.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs/4.b0f882987c69531acc56.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * 4 | * @license MIT 5 | * @author Lea Verou 6 | * @namespace 7 | * @public 8 | */ 9 | -------------------------------------------------------------------------------- /docs/6.1119ede1b201480f9a79.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /docs/6.53d8b394ac94e493ab11.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * OverlayScrollbars 3 | * https://github.com/KingSora/OverlayScrollbars 4 | * 5 | * Version: 1.13.0 6 | * 7 | * Copyright KingSora | Rene Haas. 8 | * https://github.com/KingSora 9 | * 10 | * Released under the MIT license. 11 | * Date: 02.08.2020 12 | */ 13 | -------------------------------------------------------------------------------- /docs/6.53d8b394ac94e493ab11.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"6.53d8b394ac94e493ab11.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs/8.f1e1296e6801446b1927.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{2611:function(module,exports){module.exports=function(e,n){return n=n||{},new Promise((function(t,r){var s=new XMLHttpRequest,o=[],u=[],i={},a=function(){return{ok:2==(s.status/100|0),statusText:s.statusText,status:s.status,url:s.responseURL,text:function(){return Promise.resolve(s.responseText)},json:function(){return Promise.resolve(s.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([s.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var l in s.open(n.method||"get",e,!0),s.onload=function(){s.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,(function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t})),t(a())},s.onerror=r,s.withCredentials="include"==n.credentials,n.headers)s.setRequestHeader(l,n.headers[l]);s.send(n.body||null)}))}}}]); -------------------------------------------------------------------------------- /docs/abduction.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/apocalypse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/custom.scss: -------------------------------------------------------------------------------- 1 | .dropzoneCustomClass { 2 | background-color: whitesmoke; 3 | position: relative; 4 | display: flex; 5 | flex-flow: row nowrap; 6 | border: 4px solid greenyellow; 7 | } 8 | .dropzoneMessageClassName{ 9 | background-color: greenyellow; 10 | } 11 | .dropzoneItemClassName { 12 | 13 | } 14 | .dropzoneItemClassName:not(.dropzone--has-thumbnail) > .dropzone__item-thumbnail { 15 | background-color: darkgrey; 16 | border: 4px solid greenyellow; 17 | } 18 | .dropzoneItemClassName > .dropzone__item-thumbnail { 19 | background-color: whitesmoke; 20 | border: 4px solid black; 21 | } 22 | 23 | 24 | .dropzoneDetailsClassName { 25 | top: 18px; 26 | background-color: black; 27 | border: 3px solid whitesmoke; 28 | margin: 18px; 29 | color: greenyellow; 30 | width: 80%; 31 | } 32 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/docs/demo.gif -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/docs/favicon.ico -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | Storybook

No Preview

Sorry, but you either have no stories or none are selected somehow.

  • Please check the Storybook config.
  • Try reloading the page.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Storybook
-------------------------------------------------------------------------------- /docs/main.515ae4e5df417ac473a0.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[1],{1510:function(module,exports,__webpack_require__){__webpack_require__(1511),__webpack_require__(1695),__webpack_require__(2494),__webpack_require__(2500),__webpack_require__(2501),__webpack_require__(2495),__webpack_require__(2497),__webpack_require__(2496),__webpack_require__(2498),__webpack_require__(2499),module.exports=__webpack_require__(2493)},1585:function(module,exports){},1695:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__);var _storybook_addons__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(116),_storybook_theming__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(422);_storybook_addons__WEBPACK_IMPORTED_MODULE_0__.c.setConfig({theme:_storybook_theming__WEBPACK_IMPORTED_MODULE_1__.a.dark})},2493:function(module,exports,__webpack_require__){"use strict";__webpack_require__(19).addons.setConfig({refs:{}})}},[[1510,2,3]]]); -------------------------------------------------------------------------------- /docs/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = 'f7d0ed371e596d181f62c6f68c4b7baf' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (activeClientIds.has(client.id)) { 118 | const clonedResponse = response.clone() 119 | 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: clonedResponse.body === null ? null : await clonedResponse.text(), 129 | headers: serializeHeaders(clonedResponse.headers), 130 | redirected: clonedResponse.redirected, 131 | }, 132 | }) 133 | } 134 | 135 | return response 136 | } 137 | 138 | async function getResponse(event, client, requestId) { 139 | const { request } = event 140 | const requestClone = request.clone() 141 | const getOriginalResponse = () => fetch(requestClone) 142 | 143 | // Bypass mocking when the request client is not active. 144 | if (!client) { 145 | return getOriginalResponse() 146 | } 147 | 148 | // Bypass initial page load requests (i.e. static assets). 149 | // The absence of the immediate/parent client in the map of the active clients 150 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 151 | // and is not ready to handle requests. 152 | if (!activeClientIds.has(client.id)) { 153 | return await getOriginalResponse() 154 | } 155 | 156 | // Bypass requests with the explicit bypass header 157 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 158 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 159 | 160 | // Remove the bypass header to comply with the CORS preflight check. 161 | delete cleanRequestHeaders[bypassHeaderName] 162 | 163 | const originalRequest = new Request(requestClone, { 164 | headers: new Headers(cleanRequestHeaders), 165 | }) 166 | 167 | return fetch(originalRequest) 168 | } 169 | 170 | // Send the request to the client-side MSW. 171 | const reqHeaders = serializeHeaders(request.headers) 172 | const body = await request.text() 173 | 174 | const clientMessage = await sendToClient(client, { 175 | type: 'REQUEST', 176 | payload: { 177 | id: requestId, 178 | url: request.url, 179 | method: request.method, 180 | headers: reqHeaders, 181 | cache: request.cache, 182 | mode: request.mode, 183 | credentials: request.credentials, 184 | destination: request.destination, 185 | integrity: request.integrity, 186 | redirect: request.redirect, 187 | referrer: request.referrer, 188 | referrerPolicy: request.referrerPolicy, 189 | body, 190 | bodyUsed: request.bodyUsed, 191 | keepalive: request.keepalive, 192 | }, 193 | }) 194 | 195 | switch (clientMessage.type) { 196 | case 'MOCK_SUCCESS': { 197 | return delayPromise( 198 | () => respondWithMock(clientMessage), 199 | clientMessage.payload.delay, 200 | ) 201 | } 202 | 203 | case 'MOCK_NOT_FOUND': { 204 | return getOriginalResponse() 205 | } 206 | 207 | case 'NETWORK_ERROR': { 208 | const { name, message } = clientMessage.payload 209 | const networkError = new Error(message) 210 | networkError.name = name 211 | 212 | // Rejecting a request Promise emulates a network error. 213 | throw networkError 214 | } 215 | 216 | case 'INTERNAL_ERROR': { 217 | const parsedBody = JSON.parse(clientMessage.payload.body) 218 | 219 | console.error( 220 | `\ 221 | [MSW] Request handler function for "%s %s" has thrown the following exception: 222 | 223 | ${parsedBody.errorType}: ${parsedBody.message} 224 | (see more detailed error stack trace in the mocked response body) 225 | 226 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 227 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 228 | `, 229 | request.method, 230 | request.url, 231 | ) 232 | 233 | return respondWithMock(clientMessage) 234 | } 235 | } 236 | 237 | return getOriginalResponse() 238 | } 239 | 240 | self.addEventListener('fetch', function (event) { 241 | const { request } = event 242 | 243 | // Bypass navigation requests. 244 | if (request.mode === 'navigate') { 245 | return 246 | } 247 | 248 | // Opening the DevTools triggers the "only-if-cached" request 249 | // that cannot be handled by the worker. Bypass such requests. 250 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 251 | return 252 | } 253 | 254 | // Bypass all requests when there are no active clients. 255 | // Prevents the self-unregistered worked from handling requests 256 | // after it's been deleted (still remains active until the next reload). 257 | if (activeClientIds.size === 0) { 258 | return 259 | } 260 | 261 | const requestId = uuidv4() 262 | 263 | return event.respondWith( 264 | handleRequest(event, requestId).catch((error) => { 265 | console.error( 266 | '[MSW] Failed to mock a "%s" request to "%s": %s', 267 | request.method, 268 | request.url, 269 | error, 270 | ) 271 | }), 272 | ) 273 | }) 274 | 275 | function serializeHeaders(headers) { 276 | const reqHeaders = {} 277 | headers.forEach((value, name) => { 278 | reqHeaders[name] = reqHeaders[name] 279 | ? [].concat(reqHeaders[name]).concat(value) 280 | : value 281 | }) 282 | return reqHeaders 283 | } 284 | 285 | function sendToClient(client, message) { 286 | return new Promise((resolve, reject) => { 287 | const channel = new MessageChannel() 288 | 289 | channel.port1.onmessage = (event) => { 290 | if (event.data && event.data.error) { 291 | return reject(event.data.error) 292 | } 293 | 294 | resolve(event.data) 295 | } 296 | 297 | client.postMessage(JSON.stringify(message), [channel.port2]) 298 | }) 299 | } 300 | 301 | function delayPromise(cb, duration) { 302 | return new Promise((resolve) => { 303 | setTimeout(() => resolve(cb()), duration) 304 | }) 305 | } 306 | 307 | function respondWithMock(clientMessage) { 308 | return new Response(clientMessage.payload.body, { 309 | ...clientMessage.payload, 310 | headers: clientMessage.payload.headers, 311 | }) 312 | } 313 | 314 | function uuidv4() { 315 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 316 | const r = (Math.random() * 16) | 0 317 | const v = c == 'x' ? r : (r & 0x3) | 0x8 318 | return v.toString(16) 319 | }) 320 | } 321 | -------------------------------------------------------------------------------- /docs/runtime~main.53d8b394ac94e493ab11.bundle.js: -------------------------------------------------------------------------------- 1 | !function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i 11 | * @license MIT 12 | */ 13 | 14 | /*! 15 | * cookie 16 | * Copyright(c) 2012-2014 Roman Shtylman 17 | * Copyright(c) 2015 Douglas Christopher Wilson 18 | * MIT Licensed 19 | */ 20 | 21 | /*! 22 | * https://github.com/es-shims/es5-shim 23 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 24 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 25 | */ 26 | 27 | /*! 28 | * https://github.com/paulmillr/es6-shim 29 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 30 | * and contributors, MIT License 31 | * es6-shim: v0.35.4 32 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 33 | * Details and documentation: 34 | * https://github.com/paulmillr/es6-shim/ 35 | */ 36 | 37 | /*! 38 | * isobject 39 | * 40 | * Copyright (c) 2014-2017, Jon Schlinkert. 41 | * Released under the MIT License. 42 | */ 43 | 44 | /*! ***************************************************************************** 45 | Copyright (c) Microsoft Corporation. 46 | 47 | Permission to use, copy, modify, and/or distribute this software for any 48 | purpose with or without fee is hereby granted. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 51 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 52 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 53 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 54 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 55 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 56 | PERFORMANCE OF THIS SOFTWARE. 57 | ***************************************************************************** */ 58 | 59 | /*! https://mths.be/punycode v1.3.2 by @mathias */ 60 | 61 | /** 62 | * @license 63 | * Lodash 64 | * Copyright OpenJS Foundation and other contributors 65 | * Released under MIT license 66 | * Based on Underscore.js 1.8.3 67 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 68 | */ 69 | 70 | /** @license React v0.19.1 71 | * scheduler.production.min.js 72 | * 73 | * Copyright (c) Facebook, Inc. and its affiliates. 74 | * 75 | * This source code is licensed under the MIT license found in the 76 | * LICENSE file in the root directory of this source tree. 77 | */ 78 | 79 | /** @license React v16.13.1 80 | * react-is.production.min.js 81 | * 82 | * Copyright (c) Facebook, Inc. and its affiliates. 83 | * 84 | * This source code is licensed under the MIT license found in the 85 | * LICENSE file in the root directory of this source tree. 86 | */ 87 | 88 | /** @license React v16.14.0 89 | * react-dom.production.min.js 90 | * 91 | * Copyright (c) Facebook, Inc. and its affiliates. 92 | * 93 | * This source code is licensed under the MIT license found in the 94 | * LICENSE file in the root directory of this source tree. 95 | */ 96 | 97 | /** @license React v16.14.0 98 | * react.production.min.js 99 | * 100 | * Copyright (c) Facebook, Inc. and its affiliates. 101 | * 102 | * This source code is licensed under the MIT license found in the 103 | * LICENSE file in the root directory of this source tree. 104 | */ 105 | 106 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 107 | 108 | //! © 2018 Angry Bytes and contributors. MIT licensed. 109 | -------------------------------------------------------------------------------- /docs/vendors~main.53d8b394ac94e493ab11.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"vendors~main.53d8b394ac94e493ab11.bundle.js","sources":[],"mappings":";A","sourceRoot":""} -------------------------------------------------------------------------------- /docs/vendors~main.f97bf6338e2cf12e2f2e.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * Fuse.js v3.6.1 - Lightweight fuzzy-search (http://fusejs.io) 15 | * 16 | * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me) 17 | * All Rights Reserved. Apache Software License 2.0 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | */ 21 | 22 | /*! 23 | * https://github.com/es-shims/es5-shim 24 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 25 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 26 | */ 27 | 28 | /*! 29 | * https://github.com/paulmillr/es6-shim 30 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 31 | * and contributors, MIT License 32 | * es6-shim: v0.35.4 33 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 34 | * Details and documentation: 35 | * https://github.com/paulmillr/es6-shim/ 36 | */ 37 | 38 | /*! 39 | * isobject 40 | * 41 | * Copyright (c) 2014-2017, Jon Schlinkert. 42 | * Released under the MIT License. 43 | */ 44 | 45 | /** @license React v0.19.1 46 | * scheduler.production.min.js 47 | * 48 | * Copyright (c) Facebook, Inc. and its affiliates. 49 | * 50 | * This source code is licensed under the MIT license found in the 51 | * LICENSE file in the root directory of this source tree. 52 | */ 53 | 54 | /** @license React v16.13.1 55 | * react-is.production.min.js 56 | * 57 | * Copyright (c) Facebook, Inc. and its affiliates. 58 | * 59 | * This source code is licensed under the MIT license found in the 60 | * LICENSE file in the root directory of this source tree. 61 | */ 62 | 63 | /** @license React v16.14.0 64 | * react-dom.production.min.js 65 | * 66 | * Copyright (c) Facebook, Inc. and its affiliates. 67 | * 68 | * This source code is licensed under the MIT license found in the 69 | * LICENSE file in the root directory of this source tree. 70 | */ 71 | 72 | /** @license React v16.14.0 73 | * react.production.min.js 74 | * 75 | * Copyright (c) Facebook, Inc. and its affiliates. 76 | * 77 | * This source code is licensed under the MIT license found in the 78 | * LICENSE file in the root directory of this source tree. 79 | */ 80 | 81 | /** @license React v17.0.1 82 | * react-is.production.min.js 83 | * 84 | * Copyright (c) Facebook, Inc. and its affiliates. 85 | * 86 | * This source code is licensed under the MIT license found in the 87 | * LICENSE file in the root directory of this source tree. 88 | */ 89 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/no-babel', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /mocks/browser.js: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | 3 | export const worker = setupWorker(); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropzone-vue", 3 | "version": "0.1.11", 4 | "private": false, 5 | "author": "Brunino Criniti ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/darknessnerd/drop-zone.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/darknessnerd/drop-zone/issues" 13 | }, 14 | "homepage": "https://github.com/darknessnerd/drop-zone", 15 | "keywords": [ 16 | "vue3", 17 | "upload", 18 | "chunking", 19 | "drag'n'drop", 20 | "file" 21 | ], 22 | "description": "Vue3 Library Component for drag’n’drop file uploads with image previews..", 23 | "main": "dist/dropzone-vue.common.js", 24 | "unpkg": "dist/dropzone-vue.umd.min.js", 25 | "files": [ 26 | "dist/" 27 | ], 28 | "scripts": { 29 | "serve": "vue-cli-service serve --open src/components/DropZone.vue", 30 | "build": "rollup -c rollup.config.js", 31 | "test:unit": "vue-cli-service test:unit --runInBand", 32 | "test:e2e": "vue-cli-service test:e2e", 33 | "lint": "vue-cli-service lint", 34 | "storybook": "start-storybook -s ./stories/assets -p 6006", 35 | "build-storybook": "build-storybook -o docs-build -s ./stories/assets", 36 | "test:generate-output": "jest --json --outputFile=.jest-test-results.json" 37 | }, 38 | "peerDependencies": { 39 | "vue": "^3.0.5", 40 | "core-js": "^3.9.1" 41 | }, 42 | "engines": { 43 | "node": ">=12" 44 | }, 45 | "devDependencies": { 46 | "@rollup/plugin-alias": "^3.1.2", 47 | "@rollup/plugin-babel": "^5.3.0", 48 | "@rollup/plugin-commonjs": "^17.1.0", 49 | "@rollup/plugin-node-resolve": "^11.2.0", 50 | "@rollup/plugin-replace": "^2.3.4", 51 | "@storybook/addon-actions": "^6.2.0-alpha.32", 52 | "@storybook/addon-essentials": "^6.4.4", 53 | "@storybook/addon-jest": "^6.2.0-alpha.33", 54 | "@storybook/addon-links": "^6.2.0-alpha.32", 55 | "@storybook/addons": "^6.2.0-alpha.32", 56 | "@storybook/theming": "^6.2.0-alpha.32", 57 | "@storybook/vue3": "^6.4.4", 58 | "@vue/cli-plugin-babel": "^4.5.11", 59 | "@vue/cli-plugin-eslint": "~4.5.0", 60 | "@vue/cli-plugin-unit-jest": "~4.5.0", 61 | "@vue/cli-service": "~4.5.0", 62 | "@vue/compiler-sfc": "^3.0.0", 63 | "@vue/eslint-config-airbnb": "^5.0.2", 64 | "@vue/test-utils": "^2.0.0-0", 65 | "core-js": "^3.9.1", 66 | "eslint": "^6.7.2", 67 | "eslint-plugin-import": "^2.20.2", 68 | "eslint-plugin-vue": "^7.0.0-0", 69 | "msw": "^0.27.1", 70 | "node-sass": "^4.12.0", 71 | "rollup": "^2.39.0", 72 | "rollup-plugin-scss": "^2.6.1", 73 | "rollup-plugin-terser": "^7.0.2", 74 | "rollup-plugin-vue": "^6.0.0", 75 | "sass-loader": "^8.0.2", 76 | "storybook-addon-mock": "0.0.4", 77 | "typescript": "~3.9.3", 78 | "vue": "^3.0.5", 79 | "vue-jest": "^5.0.0-0", 80 | "vue-loader": "^16.1.2" 81 | }, 82 | "msw": { 83 | "workerDirectory": "docs" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; // Convert CommonJS modules to ES6 2 | import scss from 'rollup-plugin-scss'; 3 | import vuePlugin from 'rollup-plugin-vue'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import { babel } from '@rollup/plugin-babel'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import replace from '@rollup/plugin-replace'; // Replace strings in files while bundling them. 8 | import alias from '@rollup/plugin-alias'; 9 | import path from 'path'; 10 | import pkg from './package.json'; 11 | 12 | const projectRoot = path.resolve(__dirname, '.'); 13 | 14 | const banner = `/** 15 | * ${pkg.name} ${pkg.version} 16 | * (c) ${new Date().getFullYear()} 17 | * @license MIT 18 | */`; 19 | 20 | export default { 21 | // this is the file containing all our exported components/functions 22 | input: 'src/index.js', 23 | // this is an array of outputed formats 24 | output: [ 25 | { 26 | file: pkg.main, 27 | format: 'cjs', 28 | sourcemap: true, 29 | banner, 30 | exports: 'default', 31 | }, 32 | { 33 | file: pkg.unpkg, 34 | format: 'umd', 35 | name: 'drop-zone', 36 | sourcemap: true, 37 | exports: 'default', 38 | globals: { 39 | vue: 'Vue', 40 | }, 41 | banner, 42 | }, 43 | ], 44 | // this is an array of the plugins that we are including 45 | plugins: [ 46 | replace({ 47 | preventAssignment: true, 48 | 'process.env.NODE_ENV': JSON.stringify('production'), 49 | __VUE_PROD_DEVTOOLS__: JSON.stringify(false), 50 | }), 51 | alias({ 52 | entries: [ 53 | { 54 | find: '@', 55 | replacement: `${path.resolve(projectRoot, 'src')}`, 56 | }, 57 | ], 58 | customResolver: nodeResolve({ 59 | extensions: ['.js', '.jsx', '.vue'], 60 | }), 61 | }), 62 | scss(), 63 | vuePlugin({ 64 | target: 'browser', 65 | template: { 66 | optimizeSSR: true, 67 | }, 68 | }), 69 | babel({ 70 | exclude: ['node_modules/**', 'core-js/*'], 71 | extensions: ['.js', '.jsx', '.vue'], 72 | babelHelpers: 'runtime', 73 | presets: [ 74 | '@babel/preset-env', 75 | ], 76 | }), 77 | commonjs(), // Convert CommonJS modules to ES6, so they can be included in a Rollup bundle 78 | nodeResolve(), 79 | terser({ 80 | output: { 81 | ecma: 5, 82 | }, 83 | compress: { 84 | drop_console: true, 85 | drop_debugger: true, 86 | }, 87 | }), 88 | ], 89 | // ask rollup to not bundle Vue in the library 90 | external: [ 91 | ...Object.keys(pkg.peerDependencies || {}), 92 | ...Object.keys(pkg.dependencies || {}), 93 | ], 94 | }; 95 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | 4 | const app = express(); 5 | const webRoutes = require('./routes/web'); 6 | 7 | app.use(cors()); 8 | 9 | // Front Routes handling 10 | app.use(webRoutes); 11 | 12 | app.listen(5000); 13 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon app.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "express": "^4.17.1", 15 | "multer": "^1.4.2", 16 | "nodemon": "^2.0.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/routes/web.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const multer = require('multer'); 4 | 5 | const storage = multer.diskStorage({ 6 | destination(req, file, cb) { 7 | cb(null, 'upload'); 8 | }, 9 | filename(req, file, cb) { 10 | if (req.body.chunkIndex) { 11 | const fileName = path.basename(file.originalname, path.extname(file.originalname)); 12 | cb(null, `${fileName}.${path.extname(file.originalname)}-${req.body.chunkIndex}`); 13 | } else { 14 | // You could use the original name 15 | cb(null, file.originalname); 16 | } 17 | }, 18 | }); 19 | 20 | const upload = multer({ storage }); 21 | 22 | const router = express.Router(); 23 | 24 | // Upload item 25 | // eslint-disable-next-line no-unused-vars 26 | router.post('/item', upload.array('file'), (req, res, next) => res.json({ 27 | files: req.files, 28 | })); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /src/DropZone.vue: -------------------------------------------------------------------------------- 1 | 47 | 170 | -------------------------------------------------------------------------------- /src/assets/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes passing-through { 2 | 0% { 3 | opacity: 0; 4 | transform: translateY(40px); 5 | } 6 | 7 | 30%, 70% { 8 | opacity: 1; 9 | transform: translateY(0px); 10 | } 11 | 12 | 100% { 13 | opacity: 1; 14 | transform: translateY(0px); 15 | } 16 | } 17 | @keyframes slide-in { 18 | 19 | 0% { 20 | opacity: 0; 21 | transform: translateY(40px); 22 | } 23 | 24 | 30% { 25 | opacity: 1; 26 | transform: translateY(0px); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/basic.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --preview-size: 128px; 3 | --dropzone-min-height: 64px; 4 | } 5 | .dropzone, .dropzone * { 6 | box-sizing: border-box; 7 | } 8 | .dropzone__box { 9 | position: relative; 10 | display: flex; 11 | flex-flow: row wrap; 12 | min-height: var(--dropzone-min-height); 13 | border: 2px dashed #03A062; 14 | border-radius: 4px; 15 | padding: 4px; 16 | } 17 | // Class for the div that 18 | // contain the message when 19 | // the items array is empty 20 | .dropzone__message--style { 21 | flex-grow: 1; 22 | text-align: center; 23 | } 24 | .dropzone__details--style { 25 | padding: 2em 1em; 26 | text-align: center; 27 | color: rgba(0, 0, 0, 0.9); 28 | font-size: 13px; 29 | line-height: 150%; 30 | width: 64px; 31 | min-width: 100%; 32 | max-width: 100%; 33 | } 34 | .dropzone__item--style:not(.dropzone--has-thumbnail) > .dropzone__item-thumbnail { 35 | border-radius: 12px; 36 | border: solid 1px black; 37 | } 38 | .dropzone__item--style { 39 | margin: auto auto; 40 | padding: 4px; 41 | } 42 | .dropzone { 43 | // Item element 44 | &__item:hover { 45 | z-index: 1000; 46 | } 47 | &__item { 48 | position: relative; 49 | display: inline-block; 50 | } 51 | // Error and success mark 52 | &__error-mark, &__success-mark{ 53 | pointer-events: none; 54 | opacity: 0; 55 | z-index: 500; 56 | position: absolute; 57 | display: block; 58 | top: 8px; 59 | left: 8px; 60 | } 61 | &--has-error > .dropzone__error-mark > * { 62 | display: block; 63 | } 64 | &--has-error .dropzone__progress { 65 | opacity: 0; 66 | transition: opacity 0.4s ease-in; 67 | } 68 | &--has-error { 69 | .dropzone__error-mark { 70 | opacity: 1; 71 | animation: slide-in 3s cubic-bezier(0.770, 0.000, 0.175, 1.000); 72 | } 73 | } 74 | 75 | &--success > .dropzone__success-mark > * { 76 | display: block; 77 | } 78 | &--success .dropzone__progress { 79 | opacity: 0; 80 | transition: opacity 0.4s ease-in; 81 | } 82 | 83 | .dropzone--success > .dropzone__success-mark { 84 | opacity: 1; 85 | -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 86 | -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 87 | -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 88 | -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 89 | animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); 90 | } 91 | 92 | // Item thumbnail 93 | &__item-thumbnail { 94 | border-radius: 24px; 95 | overflow: hidden; 96 | width: var(--preview-size); 97 | height: var(--preview-size); 98 | position: relative; 99 | display: block; 100 | } 101 | &__item:not(.dropzone--has-thumbnail) > .dropzone__details { 102 | opacity: 1; 103 | } 104 | &--has-thumbnail:hover > .dropzone__details { 105 | opacity: 1; 106 | } 107 | &--has-thumbnail > .dropzone__item-thumbnail > img { 108 | display: block; 109 | width: 100%; 110 | height: auto; 111 | } 112 | &--has-thumbnail > .dropzone__details { 113 | -webkit-transition: opacity 0.2s linear; 114 | -moz-transition: opacity 0.2s linear; 115 | -ms-transition: opacity 0.2s linear; 116 | -o-transition: opacity 0.2s linear; 117 | transition: opacity 0.2s linear; 118 | transition-property: opacity; 119 | transition-duration: 0.2s; 120 | transition-timing-function: ease-in; 121 | transition-delay: 100ms; 122 | } 123 | &--has-thumbnail:hover > .dropzone__item-thumbnail > img { 124 | -webkit-transform: scale(1.01, 1.01); 125 | -moz-transform: scale(1.01, 1.01); 126 | -ms-transform: scale(1.01, 1.01); 127 | -o-transform: scale(1.01, 1.01); 128 | transform: scale(1.01, 1.01); 129 | -webkit-filter: blur(1px); 130 | filter: blur(1px); 131 | } 132 | // Controls 133 | &__item-controls { 134 | display: inline-block; 135 | position: absolute; 136 | z-index: 10000; 137 | border-radius: 4px; 138 | top: 8px; 139 | right: 8px; 140 | .dropzone__item-control { 141 | cursor: pointer; 142 | } 143 | } 144 | &-clickable { 145 | cursor: pointer; 146 | * { 147 | cursor: default; 148 | } 149 | .dropzone__message * { 150 | box-sizing: border-box; 151 | cursor: pointer; 152 | } 153 | } 154 | } 155 | 156 | .dropzone__file-size { 157 | margin-bottom: 1em; 158 | font-size: 16px; 159 | } 160 | .dropzone__file-size > span { 161 | background-color: rgba(255, 255, 255, 0.4); 162 | padding: 0 0.4em; 163 | border-radius: 3px; 164 | } 165 | 166 | .dropzone__filename { 167 | white-space: nowrap; 168 | } 169 | .dropzone__filename > span { 170 | background-color: rgba(255, 255, 255, 0.4); 171 | padding: 0 0.4em; 172 | border-radius: 3px; 173 | } 174 | .dropzone__filename:not(:hover) { 175 | overflow: hidden; 176 | text-overflow: ellipsis; 177 | } 178 | .dropzone__filename:not(:hover) > span { 179 | border: 1px solid transparent; 180 | } 181 | 182 | .dropzone__details { 183 | z-index: 20; 184 | position: absolute; 185 | top: 0; 186 | left: 0; 187 | opacity: 0; 188 | } 189 | 190 | .dropzone__progress { 191 | opacity: 1; 192 | z-index: 1000; 193 | 194 | position: absolute; 195 | left: 50%; 196 | top: 50%; 197 | margin-left: -40px; 198 | 199 | -webkit-transform: scale(1); 200 | border-radius: 8px; 201 | overflow: hidden; 202 | .dropzone__progress-bar { 203 | position: relative; 204 | pointer-events: none; 205 | // border: 2px solid #333; 206 | background: rgba(255, 255, 255, 0.9); 207 | height: 16px; 208 | margin-top: -8px; 209 | width: 80px; 210 | transition: width 300ms ease-in-out; 211 | } 212 | } 213 | 214 | 215 | 216 | .dropzone--added .dropzone__progress { 217 | opacity: 0; 218 | transition: opacity 0.4s ease-in; 219 | } 220 | 221 | 222 | .dropzone-processing .dropzone__progress { 223 | opacity: 1; 224 | transition: all 0.2s linear; 225 | } 226 | 227 | 228 | .dropzone__item:not(.dropzone-processing) { 229 | .dropzone__progress { 230 | animation: pulse 6s ease infinite; 231 | } 232 | } 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/assets/icons.scss: -------------------------------------------------------------------------------- 1 | 2 | .gg-close { 3 | box-sizing: border-box; 4 | position: relative; 5 | display: block; 6 | transform: scale(var(--ggs,1)); 7 | width: 22px; 8 | height: 22px; 9 | border: 2px solid transparent; 10 | border-radius: 40px 11 | } 12 | 13 | .gg-close::after, 14 | .gg-close::before { 15 | cursor: pointer; 16 | content: ""; 17 | display: block; 18 | box-sizing: border-box; 19 | position: absolute; 20 | width: 16px; 21 | height: 2px; 22 | background: currentColor; 23 | transform: rotate(45deg); 24 | border-radius: 5px; 25 | top: 8px; 26 | left: 1px 27 | } 28 | 29 | .gg-close::after { 30 | transform: rotate(-45deg) 31 | } 32 | 33 | .gg-check-o { 34 | box-sizing: border-box; 35 | position: relative; 36 | display: block; 37 | transform: scale(var(--ggs,2)); 38 | width: 22px; 39 | height: 22px; 40 | border: 2px solid; 41 | border-radius: 100px; 42 | color: #747474; 43 | opacity: 0.98; 44 | background-color: #FFFFFF; 45 | } 46 | .gg-check-o::after { 47 | content: ""; 48 | display: block; 49 | box-sizing: border-box; 50 | position: absolute; 51 | left: 3px; 52 | top: -1px; 53 | width: 6px; 54 | height: 10px; 55 | border-color: currentColor; 56 | border-width: 0 2px 2px 0; 57 | border-style: solid; 58 | transform-origin: bottom left; 59 | transform: rotate(45deg) 60 | } 61 | .gg-check-o { 62 | box-sizing: border-box; 63 | position: relative; 64 | display: block; 65 | transform: scale(var(--ggs,1)); 66 | width: 22px; 67 | height: 22px; 68 | border: 2px solid; 69 | border-radius: 100px 70 | } 71 | .gg-danger { 72 | box-sizing: border-box; 73 | position: relative; 74 | display: block; 75 | transform: scale(var(--ggs,1)); 76 | width: 20px; 77 | height: 20px; 78 | border: 2px solid; 79 | border-radius: 40px 80 | } 81 | .gg-danger::after, 82 | .gg-danger::before { 83 | content: ""; 84 | display: block; 85 | box-sizing: border-box; 86 | position: absolute; 87 | border-radius: 3px; 88 | width: 2px; 89 | background: currentColor; 90 | left: 7px 91 | } 92 | 93 | .gg-danger::after { 94 | top: 2px; 95 | height: 8px 96 | } 97 | 98 | .gg-danger::before { 99 | height: 2px; 100 | bottom: 2px 101 | } 102 | .gg-check-o::after { 103 | content: ""; 104 | display: block; 105 | box-sizing: border-box; 106 | position: absolute; 107 | left: 3px; 108 | top: -1px; 109 | width: 6px; 110 | height: 10px; 111 | border-color: currentColor; 112 | border-width: 0 2px 2px 0; 113 | border-style: solid; 114 | transform-origin: bottom left; 115 | transform: rotate(45deg) 116 | } 117 | -------------------------------------------------------------------------------- /src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @import "animations"; 2 | @import "icons"; 3 | @import "basic"; 4 | -------------------------------------------------------------------------------- /src/hooks/config.js: -------------------------------------------------------------------------------- 1 | import { getWindowUrl } from '@/utils'; 2 | import { 3 | reactive, watch, 4 | } from 'vue'; 5 | import mineTypes from '@/utils/minetypes'; 6 | 7 | export default function useConfig({ props, context, setMultiple }) { 8 | // Dropbox reactive config 9 | const config = reactive({ 10 | paramName: props.paramName, 11 | headers: props.headers, 12 | xhrTimeout: props.xhrTimeout, 13 | withCredentials: props.withCredentials, 14 | url: props.url ? props.url : getWindowUrl(), 15 | method: props.method, 16 | maxFiles: props.maxFiles, 17 | maxFileSize: props.maxFileSize, 18 | autoUpload: props.uploadOnDrop, 19 | parallelUpload: props.parallelUpload, 20 | hiddenInputContainer: props.hiddenInputContainer, 21 | clickable: props.clickable, 22 | acceptedFiles: props.acceptedFiles, 23 | retryOnError: props.retryOnError, 24 | maxRetryError: 3, 25 | chunking: props.chunking, 26 | numberOfChunks: props.numberOfChunks, 27 | multipleUpload: props.chunking ? false : props.multipleUpload, 28 | dropzoneMessageClassName: props.dropzoneMessageClassName, 29 | accepts: [], 30 | }); 31 | const createAcceptsArray = () => { 32 | const acceptsTmp = []; 33 | // CREATE THE ACCEPTS ARRAY 34 | if (props.acceptedFiles !== null) { 35 | props.acceptedFiles.forEach((accept) => { 36 | mineTypes.data 37 | .filter((mt) => ( 38 | mt.ext && !!mt.ext.split(/\s/).find((extension) => accept === extension) 39 | ) || mt.mime_type.startsWith(accept)) 40 | .forEach((mdType) => { 41 | acceptsTmp.push(mdType.mime_type); 42 | if (mdType.ext) { 43 | acceptsTmp.push(...mdType.ext.split(/\s/).map((e) => `.${e}`)); 44 | } 45 | }); 46 | }); 47 | } 48 | config.accepts = [...acceptsTmp]; 49 | }; 50 | 51 | const emitConfigUpdate = () => { 52 | context.emit('config-update', { ...config }); 53 | }; 54 | 55 | createAcceptsArray(); 56 | emitConfigUpdate(); 57 | // Watch on props changes 58 | watch(() => props.paramName, (val) => { 59 | if (config.paramName !== val) { 60 | config.paramName = val; 61 | emitConfigUpdate(); 62 | } 63 | }); 64 | watch(() => props.acceptedFiles, (val) => { 65 | if (!config.acceptedFiles.every((accept) => val.includes(accept))) { 66 | config.acceptedFiles = [...val]; 67 | createAcceptsArray(); 68 | emitConfigUpdate(); 69 | } 70 | }); 71 | watch(() => props.headers, (val) => { 72 | if (config.headers !== val) { 73 | config.headers = val; 74 | emitConfigUpdate(); 75 | } 76 | }); 77 | watch(() => props.xhrTimeout, (val) => { 78 | if (config.xhrTimeout !== val) { 79 | config.xhrTimeout = val; 80 | emitConfigUpdate(); 81 | } 82 | }); 83 | watch(() => props.withCredentials, (val) => { 84 | if (config.withCredentials !== val) { 85 | config.withCredentials = val; 86 | emitConfigUpdate(); 87 | } 88 | }); 89 | watch(() => props.method, (val) => { 90 | if (config.method !== val) { 91 | config.method = val; 92 | emitConfigUpdate(); 93 | } 94 | }); 95 | watch(() => props.maxFiles, (val) => { 96 | if (config.maxFiles !== val) { 97 | config.maxFiles = val; 98 | emitConfigUpdate(); 99 | } 100 | }); 101 | watch(() => props.maxFileSize, (val) => { 102 | if (config.maxFileSize !== val) { 103 | config.maxFileSize = val; 104 | emitConfigUpdate(); 105 | if (config.maxFileSize > 1) { 106 | setMultiple(true); 107 | } else { 108 | setMultiple(false); 109 | } 110 | } 111 | }); 112 | watch(() => props.hiddenInputContainer, (val) => { 113 | if (config.hiddenInputContainer !== val) { 114 | config.hiddenInputContainer = val; 115 | emitConfigUpdate(); 116 | } 117 | }); 118 | watch(() => props.clickable, (val) => { 119 | if (config.hiddenInputContainer !== val) { 120 | config.clickable = val; 121 | emitConfigUpdate(); 122 | } 123 | }); 124 | watch(() => props.uploadOnDrop, (val) => { 125 | if (config.uploadOnDrop !== val) { 126 | config.autoUpload = val; 127 | emitConfigUpdate(); 128 | } 129 | }); 130 | watch(() => props.parallelUpload, (val) => { 131 | if (config.parallelUpload !== val) { 132 | config.parallelUpload = val; 133 | emitConfigUpdate(); 134 | } 135 | }); 136 | watch(() => props.retryOnError, (val) => { 137 | if (config.retryOnError !== val) { 138 | config.retryOnError = val; 139 | emitConfigUpdate(); 140 | } 141 | }); 142 | watch(() => props.maxRetryError, (val) => { 143 | if (config.maxRetryError !== val) { 144 | config.maxRetryError = val; 145 | emitConfigUpdate(); 146 | } 147 | }); 148 | watch(() => props.multipleUpload, (val) => { 149 | if (config.multipleUpload !== val) { 150 | config.multipleUpload = config.chunking ? false : val; 151 | emitConfigUpdate(); 152 | } 153 | }); 154 | watch(() => props.numberOfChunks, (val) => { 155 | if (config.numberOfChunks !== val) { 156 | config.numberOfChunks = val; 157 | emitConfigUpdate(); 158 | } 159 | }); 160 | watch(() => props.chunking, (val) => { 161 | if (config.chunking !== val) { 162 | config.chunking = val; 163 | config.multipleUpload = val ? false : config.multipleUpload; 164 | emitConfigUpdate(); 165 | } 166 | }); 167 | return { config }; 168 | } 169 | -------------------------------------------------------------------------------- /src/hooks/drag.js: -------------------------------------------------------------------------------- 1 | import { noPropagation, uuidv4 } from '@/utils'; 2 | 3 | export default function useDragAndDrop({ 4 | itemManager, 5 | }) { 6 | // TODO - Make config 7 | const ignoreHiddenFiles = false; 8 | 9 | const handleDragOver = ($event) => { 10 | let effect; 11 | try { 12 | effect = $event.dataTransfer.effectAllowed; 13 | } catch (error) { 14 | // Do nothing due to ie bg 15 | } 16 | noPropagation($event); 17 | // eslint-disable-next-line no-param-reassign 18 | $event.dataTransfer.dropEffect = effect === 'move' || effect === 'linkMove' ? 'move' : 'copy'; 19 | }; 20 | // Goes through the directory, and adds each file it finds recursively 21 | const addFilesFromDirectory = (directory, path, result) => { 22 | /* 23 | Returns an array containing some number of the directory's entries. 24 | Each item in the array is an object based on FileSystemEntry—typically 25 | either FileSystemFileEntry or FileSystemDirectoryEntry. 26 | */ 27 | const dirReader = directory.createReader(); 28 | const errorHandler = (error) => { console.log(error); }; 29 | const readEntries = () => dirReader.readEntries((entries) => { 30 | if (entries.length > 0) { 31 | for (let i = 0; i < entries.length; i += 1) { 32 | const entry = entries[i]; 33 | if (entry.isFile) { 34 | entry.file((file) => { 35 | if ( 36 | ignoreHiddenFiles 37 | && file.name.substring(0, 1) === '.' 38 | ) { 39 | return; 40 | } 41 | console.warn(entry); 42 | // eslint-disable-next-line no-param-reassign 43 | file.fullPath = `${path}/${file.name}`; 44 | itemManager.addFile(uuidv4(), file); 45 | }); 46 | } else if (entry.isDirectory) { 47 | addFilesFromDirectory(entry, `${path}/${entry.name}`, result); 48 | } 49 | } 50 | // Recursively call readEntries() again, since browser only handle 51 | // the first 100 entries. 52 | // See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries 53 | readEntries(); 54 | } 55 | return null; 56 | }, errorHandler); 57 | return readEntries(); 58 | }; 59 | // When a folder is dropped (or files are pasted), items must be handled 60 | // instead of files. 61 | const addFilesFromItems = (dataTransferItems, includeFirstLvl) => { 62 | console.warn(dataTransferItems); 63 | dataTransferItems.forEach((dataTransferItem) => { 64 | let entry = null; 65 | console.warn(dataTransferItem); 66 | if (dataTransferItem.webkitGetAsEntry != null) { 67 | entry = dataTransferItem.webkitGetAsEntry(); 68 | if (entry.isFile && includeFirstLvl) { 69 | itemManager.addFile(uuidv4(), dataTransferItem.getAsFile()); 70 | } else if (entry.isDirectory) { 71 | addFilesFromDirectory(entry, entry.name); 72 | } else { 73 | console.warn('addFilesFromItems.webkitGetAsEntry: unknown entry'); 74 | } 75 | } else if (dataTransferItem.getAsFile != null) { 76 | if (dataTransferItem.kind == null || dataTransferItem.kind === 'file') { 77 | itemManager.addFile(uuidv4(), dataTransferItem.getAsFile()); 78 | } else { 79 | console.warn('dataTransferItem.getAsFile: unknown entry'); 80 | } 81 | } 82 | }); 83 | }; 84 | const onDrop = ($event) => { 85 | noPropagation($event); 86 | 87 | if ($event.dataTransfer.items) { 88 | const dataTransferItems = $event.dataTransfer.items; 89 | if (dataTransferItems 90 | && dataTransferItems.length 91 | && dataTransferItems[0].webkitGetAsEntry != null) { 92 | addFilesFromItems(dataTransferItems, true); 93 | } 94 | } else { 95 | for (let i = 0; i < $event.dataTransfer.files.length; i += 1) { 96 | const itemId = uuidv4(); 97 | const file = $event.dataTransfer.files[i]; 98 | itemManager.addFile(itemId, file); 99 | } 100 | } 101 | }; 102 | return { 103 | handleDragOver, 104 | onDrop, 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /src/hooks/hiddenIpuntFile.js: -------------------------------------------------------------------------------- 1 | import { getAllDescendants, getElement, uuidv4 } from '@/utils'; 2 | 3 | /** 4 | * Create and hidden input file in order to click on the dropzone clickable area 5 | * and trigger the file selection 6 | * 7 | * @returns {{ 8 | * initHiddenFileInput: initHiddenFileInput, 9 | * destroyHiddenFileInput: destroyHiddenFileInput 10 | * }} 11 | */ 12 | export default function useHiddenInputFile() { 13 | let hiddenFileInput; 14 | let clickableElements = []; 15 | const triggerClickOnHiddenFileInput = (evt) => { 16 | if (!evt.detail || evt.detail === 1) { 17 | if (clickableElements.findIndex((el) => el === evt.target) !== -1) { 18 | evt.stopPropagation(); 19 | hiddenFileInput.click(); 20 | } 21 | return true; 22 | } 23 | return false; 24 | }; 25 | const setMultiple = (multiple) => { 26 | if (hiddenFileInput && multiple) { 27 | hiddenFileInput.setAttribute('multiple', 'multiple'); 28 | } else if (hiddenFileInput && !multiple) { 29 | hiddenFileInput.removeAttribute('multiple'); 30 | } 31 | }; 32 | const setupHiddenFileInput = ({ config, itemManager }) => { 33 | if (hiddenFileInput) { 34 | hiddenFileInput.parentNode.removeChild(hiddenFileInput); 35 | } 36 | hiddenFileInput = document.createElement('input'); 37 | hiddenFileInput.setAttribute('type', 'file'); 38 | if (config.maxFiles === null || config.maxFiles > 1) { 39 | setMultiple(true); 40 | } 41 | if (config.accepts.length > 0) { 42 | hiddenFileInput.setAttribute('accept', config.accepts.join(',')); 43 | } 44 | // Make sure that no one can tab in this input field. 45 | hiddenFileInput.setAttribute('tabindex', '-1'); 46 | hiddenFileInput.style.visibility = 'hidden'; 47 | hiddenFileInput.style.position = 'absolute'; 48 | hiddenFileInput.style.top = '0'; 49 | hiddenFileInput.style.left = '0'; 50 | hiddenFileInput.style.height = '0'; 51 | hiddenFileInput.style.width = '0'; 52 | getElement(config.hiddenInputContainer, 'hiddenInputContainer') 53 | .appendChild(hiddenFileInput); 54 | hiddenFileInput.addEventListener('change', () => { 55 | const { files } = hiddenFileInput; 56 | files.forEach((file) => { 57 | itemManager.addFile(uuidv4(), file); 58 | }); 59 | setupHiddenFileInput({ config, itemManager }); 60 | }); 61 | }; 62 | /** 63 | * 64 | * Create the hidden input file 65 | * 66 | * @param config - dropzone props 67 | * @param dropzone - element ref 68 | * @param addFile - callback that add a new file 69 | * @param accepts - mine-types array, can be null or empty 70 | */ 71 | const initHiddenFileInput = ({ 72 | config, dropzone, itemManager, 73 | }) => { 74 | if (config.clickable) { 75 | const element = getElement(`.${config.dropzoneMessageClassName}`, config.dropzoneMessageClassName); 76 | const elements = getAllDescendants(element); 77 | clickableElements = [dropzone.value]; 78 | clickableElements.push(element); 79 | if (elements.length > 0) { 80 | clickableElements.push(...elements); 81 | } 82 | setupHiddenFileInput({ config, itemManager }); 83 | clickableElements.forEach((el) => { 84 | el.classList.add('dropzone-clickable'); 85 | el.addEventListener('click', triggerClickOnHiddenFileInput); 86 | }); 87 | } 88 | }; 89 | const destroyHiddenFileInput = () => { 90 | // Remove all events 91 | clickableElements.forEach((el) => { 92 | el.removeEventListener('click', triggerClickOnHiddenFileInput); 93 | }); 94 | // Delete the hidden input file 95 | if (hiddenFileInput) { 96 | hiddenFileInput.parentElement.removeChild(hiddenFileInput); 97 | hiddenFileInput = null; 98 | } 99 | }; 100 | return { initHiddenFileInput, destroyHiddenFileInput, setMultiple }; 101 | } 102 | -------------------------------------------------------------------------------- /src/hooks/itemManager.js: -------------------------------------------------------------------------------- 1 | import { 2 | reactive, 3 | } from 'vue'; 4 | import mineTypes from '@/utils/minetypes'; 5 | import STATUS from '@/utils/status'; 6 | import useUploadQueue from '@/hooks/uploadQueue'; 7 | import useThumbnail from '@/hooks/thumbnail'; 8 | 9 | export default function useItemManager({ config, context }) { 10 | const items = reactive({ 11 | ids: [], 12 | all: {}, 13 | }); 14 | /** 15 | * callback for thumbnail generation 16 | * 17 | * @param itemId - item id 18 | * @param thumbnail - generated thumbnail 19 | */ 20 | const thumbnailGenerated = (itemId, thumbnail) => { 21 | items.all[itemId].thumbnail = thumbnail; 22 | }; 23 | const { 24 | enqueueThumbnail, 25 | } = useThumbnail(thumbnailGenerated); 26 | // File upload queue feature 27 | const { 28 | enqueueFile, 29 | processQueue, 30 | } = useUploadQueue({ 31 | config, 32 | items, 33 | context, 34 | }); 35 | /** 36 | * Remove an uploaded file 37 | * 38 | * @param id - file id 39 | */ 40 | const removeFile = (id) => { 41 | const index = items.ids.findIndex((idItem) => idItem === id); 42 | if (index < 0) { 43 | return; 44 | } 45 | items.ids.splice(index, 1); 46 | const deletedFile = items.all[id]; 47 | delete items.all[id]; 48 | context.emit('removed-file', { file: deletedFile.file, status: deletedFile.status, id }); 49 | }; 50 | /** 51 | * 52 | * Check if the file is an accepted 53 | * 54 | * @param file 55 | * @returns {boolean} 56 | */ 57 | const isValidFile = (file) => { 58 | if (config.accepts === null || config.accepts.length === 0) { 59 | return true; 60 | } 61 | const fileMineType = file.type; 62 | let isValid = config.accepts 63 | .findIndex((validType) => validType.trim() === fileMineType.trim()) !== -1; 64 | if (!isValid) { 65 | // Retrieve the extension from the file if it exist 66 | const splitFileName = file.name.split('.'); 67 | if (splitFileName.length > 1) { 68 | const ext = splitFileName[splitFileName.length - 1]; 69 | // Retrieve mine-type from extension 70 | const extMineTypes = mineTypes.data 71 | .filter((mt) => ( 72 | mt.ext && !!mt.ext.split(/\s/).find((extension) => ext === extension) 73 | )) 74 | .map((mediaType) => mediaType.mime_type); 75 | isValid = !!config.accepts.find((mt) => extMineTypes.find((extMt) => extMt === mt)); 76 | } 77 | } 78 | return isValid; 79 | }; 80 | const addFile = (id, file) => { 81 | console.debug('addFile', file); 82 | // Check the size 83 | console.debug(config.accepts); 84 | if (config.maxFileSize && file.size > config.maxFileSize) { 85 | console 86 | .warn(`ignored file: ${file.name} with size ~= ${(file.size / 1024 / 1024) 87 | .toPrecision(3)} mb`); 88 | context.emit('error-add', { files: [file], error: 'MAX_FILE_SIZE' }); 89 | return; 90 | } 91 | // Filter invalid type 92 | if (!isValidFile(file)) { 93 | context.emit('error-add', { files: [file], error: 'INVALID_TYPE' }); 94 | console.warn(`ignored file: ${file.name}`); 95 | return; 96 | } 97 | if (config.maxFiles && (items.ids.length + 1 > config.maxFiles)) { 98 | context.emit('error-add', { files: [file], error: 'MAX_FILE' }); 99 | console.warn('Max file reached'); 100 | return; 101 | } 102 | // Add the id 103 | items.ids.push(id); 104 | // eslint-disable-next-line no-param-reassign 105 | items.all[id] = { 106 | file, 107 | thumbnail: null, 108 | upload: { progress: 0 }, 109 | status: STATUS.ADDED, 110 | accepted: true, 111 | id, 112 | }; 113 | enqueueThumbnail(id, file); 114 | enqueueFile(id); 115 | context.emit('added-file', { file: items.all[id].file, id }); 116 | }; 117 | 118 | const itemManager = { 119 | getItems: () => items, 120 | removeFile, 121 | addFile, 122 | processQueue, 123 | }; 124 | return { itemManager }; 125 | } 126 | -------------------------------------------------------------------------------- /src/hooks/thumbnail.js: -------------------------------------------------------------------------------- 1 | const thumbnailQueue = []; 2 | let isProcessing = false; 3 | export default function useThumbnail(done) { 4 | const generateThumbnail = (item) => { 5 | const reader = new FileReader(); 6 | /* 7 | Add an event listener for when the file has been loaded 8 | to update the src on the file preview. 9 | */ 10 | reader.addEventListener('load', () => { 11 | done(item.id, reader.result); 12 | }, false); 13 | /* 14 | Read the data for the file in through the reader. When it has 15 | been loaded, we listen to the event propagated and set the image 16 | src to what was loaded from the reader. 17 | */ 18 | reader.readAsDataURL(item.file); 19 | }; 20 | const shiftQueue = () => { 21 | if (isProcessing || thumbnailQueue.length === 0) { 22 | return; 23 | } 24 | isProcessing = true; 25 | const item = thumbnailQueue.shift(); 26 | generateThumbnail(item); 27 | isProcessing = false; 28 | }; 29 | const enqueueThumbnail = (id, file) => { 30 | // Check if it's an image 31 | if (/\.(jpe?g|png|gif)$/i.test(file.name) || file.type.match('image.*')) { 32 | thumbnailQueue.push({ id, file }); 33 | setTimeout(() => shiftQueue(), 0); 34 | } 35 | }; 36 | return { enqueueThumbnail }; 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/uploadQueue.js: -------------------------------------------------------------------------------- 1 | import { readonly } from 'vue'; 2 | import useUploadXHR from '@/hooks/uploadXHR'; 3 | import STATUS from '@/utils/status'; 4 | 5 | /** 6 | * @param retryOnError - retry when an upload call is failed 7 | * @param autoUpload.value - autoUpload.valueProcess 8 | * @param items - items state 9 | * @returns {{enqueueFile: enqueueFile, processQueue: (function(): (undefined))}} 10 | */ 11 | export default function useUploadQueue({ 12 | config, 13 | items, 14 | context, 15 | }) { 16 | const processQueue = () => { 17 | const { 18 | upload, 19 | uploadWithChunking, 20 | } = useUploadXHR({ config, items }); 21 | const currentProcessing = Object 22 | .values(items.all) 23 | .filter(((item) => item.status === STATUS.UPLOADING)).length; 24 | if (currentProcessing >= config.parallelUpload) { 25 | console.debug('max limit for parallel uploads reached'); 26 | return; 27 | } 28 | 29 | const queuedFiles = Object 30 | .values(items.all) 31 | .filter(((item) => item.status === STATUS.QUEUED || ( 32 | item.status === STATUS.ERROR && config.retryOnError && item.upload.retryErrorCounter > 0))); 33 | 34 | if (queuedFiles.length <= 0) { 35 | console.debug('processQueue is empty'); 36 | return; 37 | } 38 | const sending = (uploadedItems, xhr, formData) => { 39 | context.emit('sending', readonly(uploadedItems.map((item) => item.file)), xhr, formData); 40 | }; 41 | const triggerProcessQueue = (uploadedItems) => { 42 | if (config.autoUpload) { 43 | processQueue(); 44 | } 45 | context.emit('uploaded', readonly(uploadedItems.map((item) => item.file))); 46 | }; 47 | const onError = (ids, error) => { 48 | if (config.autoUpload) { 49 | processQueue(); 50 | } 51 | context.emit('error-upload', { ids: [...ids], ...error }); 52 | }; 53 | let i = currentProcessing; 54 | console.debug(`start to processQueue for ${config.parallelUpload - i} items`); 55 | if (config.chunking) { 56 | uploadWithChunking(queuedFiles.shift(), triggerProcessQueue, onError, sending); 57 | } else if (config.multipleUpload) { 58 | upload(queuedFiles.slice(0, config.parallelUpload - currentProcessing), 59 | triggerProcessQueue, 60 | onError, 61 | sending); 62 | } else { 63 | while (i <= config.parallelUpload) { 64 | if (queuedFiles.length <= 0) { 65 | return; 66 | } 67 | upload([queuedFiles.shift()], triggerProcessQueue, onError, sending); 68 | i += 1; 69 | } 70 | } 71 | }; 72 | 73 | const enqueueFile = (id) => { 74 | console.debug(`enqueueFile id=${id}`); 75 | if (items.ids.findIndex((value) => value === id) === -1) { 76 | throw new Error(`File with ${id} does not exist `); 77 | } 78 | const file = items.all[id]; 79 | if (file.status === STATUS.ADDED && file.accepted === true) { 80 | file.status = STATUS.QUEUED; 81 | if (config.autoUpload) { 82 | setTimeout(() => processQueue(), 0); 83 | } 84 | } else { 85 | throw new Error(`File ${id} already in ${id.status} state`); 86 | } 87 | }; 88 | return { enqueueFile, processQueue }; 89 | } 90 | -------------------------------------------------------------------------------- /src/hooks/uploadXHR.js: -------------------------------------------------------------------------------- 1 | import { uuidv4 } from '@/utils'; 2 | import STATUS from '@/utils/status'; 3 | 4 | export default function useUploadXHR({ config, items }) { 5 | const error = (uploadId, e, onError) => { 6 | const idsWithError = []; 7 | Object.values(items.all) 8 | .filter((item) => item.upload && item.upload.id === uploadId) 9 | .forEach((item) => { 10 | // eslint-disable-next-line no-param-reassign 11 | item.status = STATUS.ERROR; 12 | if (!item.upload.retryErrorCounter) { 13 | // eslint-disable-next-line no-param-reassign 14 | item.upload.retryErrorCounter = config.maxRetryError; 15 | } else { 16 | // eslint-disable-next-line no-param-reassign 17 | item.upload.retryErrorCounter -= 1; 18 | } 19 | idsWithError.push(item.id); 20 | }); 21 | onError(idsWithError, { errorType: e.type }); 22 | }; 23 | const uploadChunck = ( 24 | chunkStart, 25 | sliceSize, 26 | item, 27 | reader, 28 | onFinish, 29 | onError, 30 | onSending, 31 | uploadId, 32 | ) => { 33 | const nextSlice = chunkStart + sliceSize + 1; 34 | const blob = item.file.webkitSlice 35 | ? item.file.webkitSlice(chunkStart, nextSlice) 36 | : item.file.slice(chunkStart, nextSlice); 37 | // eslint-disable-next-line no-param-reassign 38 | reader.onloadend = (event) => { 39 | if (event.target.readyState !== FileReader.DONE) { 40 | return; 41 | } 42 | // eslint-disable-next-line no-param-reassign 43 | item.upload.blob = blob; 44 | // eslint-disable-next-line no-param-reassign 45 | item.upload.chunkIndex = Math.floor(nextSlice / sliceSize); 46 | // eslint-disable-next-line no-param-reassign 47 | item.upload.nextSlice = nextSlice; 48 | // eslint-disable-next-line no-param-reassign 49 | item.upload.chunkStart = chunkStart; 50 | // eslint-disable-next-line no-param-reassign 51 | item.upload.sliceSize = sliceSize; 52 | // eslint-disable-next-line no-param-reassign 53 | item.upload.reader = reader; 54 | // eslint-disable-next-line no-use-before-define 55 | makeRequest(uploadId, [item], onFinish, onError, onSending); 56 | }; 57 | reader.readAsDataURL(blob); 58 | }; 59 | // Invoked when there is new progress information about given files. 60 | // If e is not provided, it is assumed that the upload is finished. 61 | const updateFilesUploadProgress = (uploadId, e) => { 62 | let progress; 63 | if (typeof e !== 'undefined') { 64 | Object.values(items.all) 65 | .filter((item) => item.upload.id === uploadId) 66 | .forEach((item) => { 67 | if (!item.upload.chunking) { 68 | progress = (100 * e.loaded) / e.total; 69 | // eslint-disable-next-line no-param-reassign 70 | item.upload.progress = progress; 71 | } else { 72 | // eslint-disable-next-line no-param-reassign 73 | item.upload.loadedBytes += e.loaded; 74 | // eslint-disable-next-line no-param-reassign 75 | item.upload.progress = (100 * item.upload.loadedBytes) / item.file.size; 76 | } 77 | }); 78 | } 79 | }; 80 | const makeRequest = async (uploadId, files, onFinish, onError, onSending) => { 81 | const xhr = new XMLHttpRequest(); 82 | xhr.open(config.method, config.url, true); 83 | xhr.timeout = config.xhrTimeout; 84 | xhr.withCredentials = config.withCredentials; 85 | // Async callbacks 86 | xhr.ontimeout = (e) => { error(uploadId, e, onError); }; 87 | xhr.onerror = (e) => { error(uploadId, e, onError); }; 88 | // Defaults headers 89 | const headers = { 90 | Accept: 'application/json', 91 | 'Cache-Control': 'no-cache', 92 | 'X-Requested-With': 'XMLHttpRequest', 93 | }; 94 | // Custom headers from config 95 | let headersName = Object.keys(headers); 96 | for (let i = 0; i < headersName.length; i += 1) { 97 | const headerName = headersName[i]; 98 | xhr.setRequestHeader(headerName, headers[headerName]); 99 | } 100 | if (config.headers) { 101 | headersName = Object.keys(config.headers); 102 | for (let i = 0; i < headersName.length; i += 1) { 103 | const headerName = headersName[i]; 104 | const val = config.headers[headerName]; 105 | if (typeof val === 'string' || typeof val === 'number') { 106 | xhr.setRequestHeader(headerName, val); 107 | } 108 | } 109 | } 110 | // Some browsers do not have the .upload property 111 | const progressObj = xhr.upload != null ? xhr.upload : xhr; 112 | progressObj.onprogress = (e) => updateFilesUploadProgress(uploadId, e); 113 | 114 | // Append to formData 115 | const formData = new FormData(); 116 | await onSending(Object.values(files) 117 | .filter((item) => item.upload.id === uploadId), xhr, formData); 118 | for (let i = 0; i < files.length; i += 1) { 119 | const item = files[i]; 120 | if (item.upload.id === uploadId) { 121 | if (!item.upload.chunking) { 122 | formData.append(config.paramName, item.file, item.file.name); 123 | } else { 124 | formData.set('fileName', item.file.name); 125 | // 1 based chunk order index 126 | formData.set('chunkIndex', item.upload.chunkIndex); 127 | formData.set(config.paramName, item.upload.blob, item.file.name); 128 | } 129 | // eslint-disable-next-line no-param-reassign 130 | item.status = STATUS.UPLOADING; 131 | } 132 | } 133 | 134 | xhr.onload = (e) => { 135 | let response; 136 | if (xhr.readyState !== 4) { 137 | return; 138 | } 139 | if (xhr.responseType !== 'arraybuffer' && xhr.responseType !== 'blob') { 140 | response = xhr.responseText; 141 | if ( 142 | xhr.getResponseHeader('content-type') 143 | // eslint-disable-next-line no-bitwise 144 | && ~xhr.getResponseHeader('content-type').indexOf('application/json') 145 | ) { 146 | try { 147 | response = JSON.parse(response); 148 | } catch (errorCatch) { 149 | console.error(errorCatch); 150 | response = 'Invalid JSON response from server.'; 151 | } 152 | } 153 | console.debug(response); 154 | } 155 | if (!(xhr.status >= 200 && xhr.status < 300)) { 156 | error(uploadId, e, onError); 157 | } else { 158 | let allDone = true; 159 | Object.values(files) 160 | .filter((item) => item.upload.id === uploadId) 161 | .forEach((item) => { 162 | if (item.upload.chunking) { 163 | if (item.upload.nextSlice < item.file.size) { 164 | allDone = false; 165 | uploadChunck( 166 | item.upload.nextSlice, 167 | item.upload.sliceSize, 168 | item, 169 | item.upload.reader, onFinish, onError, onSending, 170 | item.upload.id, 171 | ); 172 | } else { 173 | console.debug(`file upload finished ${item.file.name}`); 174 | // eslint-disable-next-line no-param-reassign 175 | item.status = STATUS.DONE; 176 | // eslint-disable-next-line no-param-reassign 177 | item.upload.progress = 100.00; 178 | } 179 | } else { 180 | // eslint-disable-next-line no-param-reassign 181 | item.status = STATUS.DONE; 182 | console.debug(`file upload finished ${item.file.name}`); 183 | } 184 | }); 185 | if (allDone === true) { 186 | onFinish(files); 187 | } 188 | } 189 | }; 190 | xhr.send(formData); 191 | }; 192 | const uploadWithChunking = (item, onFinish, onError, onSending) => { 193 | const uploadId = uuidv4(); 194 | console.log(`try to uploadWithChunking ${uploadId}`); 195 | // eslint-disable-next-line no-param-reassign 196 | item.upload.id = uploadId; 197 | // eslint-disable-next-line no-param-reassign 198 | item.upload.chunking = true; 199 | // eslint-disable-next-line no-param-reassign 200 | item.upload.loadedBytes = 0; 201 | const reader = new FileReader(); 202 | // TODO - define the size of the chunks 203 | const sliceSize = (item.file.size / config.numberOfChunks); 204 | uploadChunck(0, sliceSize, item, reader, onFinish, onError, onSending, uploadId); 205 | }; 206 | 207 | const upload = (files, onFinish, onError, onSending) => { 208 | const uploadId = uuidv4(); 209 | console.debug(`start an upload with id: ${uploadId}`); 210 | let needUpload = false; 211 | for (let i = 0; i < files.length; i += 1) { 212 | const item = files[i]; 213 | if (item.status === STATUS.QUEUED 214 | || (config.retryOnError 215 | && item.status === STATUS.ERROR 216 | && item.upload.retryErrorCounter > 0)) { 217 | item.upload.id = uploadId; 218 | needUpload = true; 219 | } 220 | } 221 | if (!needUpload) { 222 | console.debug('Nothing to upload !'); 223 | return; 224 | } 225 | makeRequest(uploadId, files, onFinish, onError, onSending); 226 | }; 227 | return { upload, uploadWithChunking }; 228 | } 229 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import '@/assets/main.scss'; 2 | import DropZone from '@/DropZone.vue'; 3 | 4 | const install = (Vue) => { 5 | Vue.component(DropZone.name, DropZone); 6 | }; 7 | 8 | if (typeof window !== 'undefined' && window.Vue) { 9 | install(window.Vue); 10 | } 11 | 12 | DropZone.install = install; 13 | 14 | export default DropZone; 15 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * Upload url, if null it will set the window location 4 | */ 5 | url: { 6 | type: String, 7 | default: null, 8 | validator: (val) => { 9 | if (val === null) return true; 10 | try { 11 | return new URL(val) !== undefined; 12 | } catch { 13 | return false; 14 | } 15 | }, 16 | }, 17 | /** 18 | * Upload xhr method can be post or put 19 | */ 20 | method: { 21 | type: String, 22 | default: 'POST', 23 | validator: (val) => val.toUpperCase() === 'POST' || val.toUpperCase() === 'PUT', 24 | }, 25 | paramName: { 26 | type: String, 27 | default: 'file', 28 | }, 29 | headers: { 30 | type: Object, 31 | default: null, 32 | validator: (val) => { 33 | const foundNonValid = Object 34 | .values(val) 35 | .find((k) => typeof k !== 'string' && typeof k !== 'number'); 36 | return !foundNonValid; 37 | }, 38 | }, 39 | xhrTimeout: { 40 | type: Number, 41 | default: 60000, 42 | validator: (val) => val >= 0, 43 | }, 44 | withCredentials: { 45 | type: Boolean, 46 | default: false, 47 | }, 48 | /** 49 | * Auto upload on drop item or select items form hiddent input 50 | */ 51 | uploadOnDrop: { 52 | type: Boolean, 53 | default: true, 54 | }, 55 | /** 56 | * Retry an upload if it fail 57 | */ 58 | retryOnError: { 59 | type: Boolean, 60 | default: false, 61 | }, 62 | /** 63 | * Send more items in one request, this is disabled in case of the prop chunking is 64 | * active 65 | */ 66 | multipleUpload: { 67 | type: Boolean, 68 | default: false, 69 | }, 70 | /** 71 | * Parallel files upload to be process 72 | */ 73 | parallelUpload: { 74 | type: Number, 75 | default: 3, 76 | }, 77 | /** 78 | * Max files number accepted by the Dropzone 79 | */ 80 | maxFiles: { 81 | type: Number, 82 | default: null, 83 | }, 84 | /** 85 | * Bytes value for the max upload size allowed 86 | */ 87 | maxFileSize: { 88 | type: Number, 89 | default: 1000000, 90 | }, 91 | /** 92 | * Element or query selector where the hidden Input it's placed 93 | */ 94 | hiddenInputContainer: { 95 | default: 'body', 96 | }, 97 | /** 98 | * If active enable the dropzone to be clickable and show the files selection 99 | */ 100 | clickable: { 101 | type: Boolean, 102 | default: true, 103 | }, 104 | /** 105 | * Array that contain the accepted files, possibile values: 106 | * ['image', 'doc', 'video', 'png', ... , 'audio' ] 107 | * for a full list see: TODO - place a link with the full accepted 108 | */ 109 | acceptedFiles: { 110 | type: Array, 111 | default: null, 112 | }, 113 | /** 114 | * Enable the upload chunking feature, if this is active the multipleUpload for request is 115 | * disabled. 116 | */ 117 | chunking: { 118 | type: Boolean, 119 | default: false, 120 | }, 121 | /** 122 | * If the chunking mode is active this property represents the number of 123 | * chunks with which the file will be split 124 | */ 125 | numberOfChunks: { 126 | type: Number, 127 | default: 10, 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-bitwise 2 | const uuidv4 = () => ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, 3 | // eslint-disable-next-line no-mixed-operators,no-bitwise 4 | (c) => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); 5 | 6 | const getWindowUrl = () => (window.URL !== null ? window.URL : window.webkitURL); 7 | /** 8 | * 9 | * Return element if it's provided a query string or an Element 10 | * 11 | * @param el 12 | * @param name 13 | * @returns HTMLElement 14 | * @throws Error - if the element was not found 15 | */ 16 | const getElement = (el, name) => { 17 | let element; 18 | if (typeof el === 'string') { 19 | element = document.querySelector(el); 20 | } else if (el.nodeType != null) { 21 | element = el; 22 | } 23 | if (element == null) { 24 | throw new Error( 25 | `Invalid \`${name}\` option provided. Please provide a CSS selector or a plain HTML element.`, 26 | ); 27 | } 28 | return element; 29 | }; 30 | const getAllDescendants = (el) => { 31 | const elements = []; 32 | if (el.nodeType !== null) { 33 | Object.values(el.children).forEach((node) => { 34 | elements.push(node); 35 | const children = getAllDescendants(node); 36 | if (children.length > 0) { 37 | elements.push(...children); 38 | } 39 | }); 40 | } 41 | return elements; 42 | }; 43 | const determineDragAndDropCapable = () => { 44 | /* 45 | Create a test element to see if certain events 46 | are present that let us do drag and drop. 47 | */ 48 | const div = document.createElement('div'); 49 | 50 | /* 51 | Check to see if the `draggable` event is in the element 52 | or the `ondragstart` and `ondrop` events are in the element. If 53 | they are, then we have what we need for dragging and dropping files. 54 | 55 | We also check to see if the window has `FormData` and `FileReader` objects 56 | present so we can do our AJAX uploading 57 | */ 58 | return (('draggable' in div) 59 | || ('ondragstart' in div && 'ondrop' in div)) 60 | && 'FormData' in window 61 | && 'FileReader' in window; 62 | }; 63 | const containsFiles = (e) => { 64 | if (e.dataTransfer.types) { 65 | // Because e.dataTransfer.types is an Object in 66 | // IE, we need to iterate like this instead of 67 | // using e.dataTransfer.types.some() 68 | // eslint-disable-next-line no-plusplus 69 | for (let i = 0; i < e.dataTransfer.types.length; i++) { 70 | if (e.dataTransfer.types[i] === 'Files') return true; 71 | } 72 | } 73 | return false; 74 | }; 75 | 76 | /** 77 | * 78 | * @param e 79 | */ 80 | const noPropagation = (e) => { 81 | // If there are no files, we don't want to stop 82 | // propagation so we don't interfere with other 83 | // drag and drop behaviour. 84 | if (!containsFiles(e)) { 85 | return; 86 | } 87 | e.stopPropagation(); 88 | if (e.preventDefault) { 89 | e.preventDefault(); 90 | return; 91 | } 92 | e.returnValue = false; 93 | }; 94 | 95 | /** 96 | * 97 | * Convert bytes to string 98 | * 99 | * @param size 100 | * @returns {string} 101 | */ 102 | const fileSizeBase = 1024; 103 | const dictFileSizeUnits = { 104 | tb: 'TB', gb: 'GB', mb: 'MB', kb: 'KB', b: 'b', 105 | }; 106 | 107 | const filesize = (size) => { 108 | let selectedSize = 0; 109 | let selectedUnit = 'b'; 110 | if (size > 0) { 111 | const units = ['tb', 'gb', 'mb', 'kb', 'b']; 112 | for (let i = 0; i < units.length; i += 1) { 113 | const unit = units[i]; 114 | const cutoff = (fileSizeBase ** (4 - i)) / 10; 115 | if (size >= cutoff) { 116 | selectedSize = size / (fileSizeBase ** (4 - i)); 117 | selectedUnit = unit; 118 | break; 119 | } 120 | } 121 | selectedSize = Math.round(10 * selectedSize) / 10; // Cutting of digits 122 | } 123 | return `${selectedSize} ${dictFileSizeUnits[selectedUnit]}`; 124 | }; 125 | export { 126 | getWindowUrl, 127 | uuidv4, 128 | filesize, 129 | noPropagation, 130 | determineDragAndDropCapable, 131 | getElement, 132 | getAllDescendants, 133 | }; 134 | -------------------------------------------------------------------------------- /src/utils/status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for item status. 3 | * 4 | * @readonly 5 | * @enum string 6 | */ 7 | const STATUS = Object.freeze({ 8 | ADDED: 'ADDED', 9 | QUEUED: 'QUEUED', 10 | UPLOADING: 'UPLOADING', 11 | ERROR: 'ERROR', 12 | DONE: 'DONE', 13 | }); 14 | 15 | export default STATUS; 16 | -------------------------------------------------------------------------------- /stories/DropZone.slots.stories.js: -------------------------------------------------------------------------------- 1 | import DropZone from '@/index'; 2 | import './assets/custom.scss'; 3 | import { rest } from 'msw'; 4 | import { ref } from 'vue'; 5 | import controls from './controls'; 6 | import { worker } from '../mocks/browser'; 7 | import { SuccessEmitsFlow } from './DropZone.emits.stories'; 8 | 9 | export default { 10 | title: 'DropZone/slots', 11 | component: DropZone, 12 | decorators: [() => { 13 | worker.use( 14 | rest.post('http://localhost:5000/item', (req, res, ctx) => 15 | // Mock an infinite loading state. 16 | res( 17 | ctx.status(200), 18 | ctx.delay(100), 19 | )), 20 | ); 21 | return { template: '
' }; 22 | }], 23 | }; 24 | 25 | const Template = (args) => ({ 26 | // Components used in your story `template` are defined in the `components` object 27 | components: { DropZone }, 28 | // The story's `args` need to be mapped into the template through the `setup()` method 29 | setup() { 30 | const dropZoneRef = ref(); 31 | const uploadAction = () => { 32 | dropZoneRef.value.processQueue(); 33 | }; 34 | return { args, uploadAction, dropZoneRef }; 35 | }, 36 | // And then the `args` are bound to your component with `v-bind="args"` 37 | template: args.template, 38 | }); 39 | 40 | export const DropMessage = Template.bind({}); 41 | DropMessage.argTypes = { 42 | ...controls, 43 | }; 44 | DropMessage.args = { 45 | template: '' 50 | + '' 54 | + '', 55 | }; 56 | 57 | export const RemoveIcon = Template.bind({}); 58 | RemoveIcon.argTypes = { 59 | ...controls, 60 | }; 61 | RemoveIcon.args = { 62 | template: '' 67 | + '' 70 | + '' 73 | + '', 74 | }; 75 | 76 | 77 | export const SuccessIcon = Template.bind({}); 78 | SuccessIcon.argTypes = { 79 | ...controls, 80 | }; 81 | SuccessIcon.args = { 82 | template: '' 87 | + '' 90 | + '' 93 | + '', 94 | }; 95 | 96 | export const ErrorIcon = Template.bind({}); 97 | ErrorIcon.argTypes = { 98 | ...controls, 99 | }; 100 | ErrorIcon.decorators = [ 101 | () => { 102 | worker.use( 103 | rest.post('http://localhost:5000/item', (req, res, ctx) => 104 | // Mock an infinite loading state. 105 | res( 106 | ctx.status(500), 107 | ctx.delay(0), 108 | )), 109 | ); 110 | return { 111 | template: '
' 112 | + '' 113 | + '
', 114 | }; 115 | }, 116 | ]; 117 | ErrorIcon.args = { 118 | template: '' 123 | + '' 126 | + '' 129 | + '', 130 | }; 131 | -------------------------------------------------------------------------------- /stories/DropZone.styles.stories.js: -------------------------------------------------------------------------------- 1 | import DropZone from '@/index'; 2 | import './assets/custom.scss'; 3 | import { rest } from 'msw'; 4 | import { ref } from 'vue'; 5 | import controls from './controls'; 6 | import { worker } from '../mocks/browser'; 7 | 8 | export default { 9 | title: 'DropZone/styles', 10 | component: DropZone, 11 | decorators: [() => { 12 | worker.use( 13 | rest.post('http://localhost:5000/item', (req, res, ctx) => 14 | // Mock an infinite loading state. 15 | res( 16 | ctx.status(200), 17 | ctx.delay(100), 18 | )), 19 | ); 20 | return { template: '
' }; 21 | }], 22 | }; 23 | 24 | const Template = (args) => ({ 25 | // Components used in your story `template` are defined in the `components` object 26 | components: { DropZone }, 27 | // The story's `args` need to be mapped into the template through the `setup()` method 28 | setup() { 29 | const dropZoneRef = ref(); 30 | const uploadAction = () => { 31 | dropZoneRef.value.processQueue(); 32 | }; 33 | return { args, uploadAction, dropZoneRef }; 34 | }, 35 | // And then the `args` are bound to your component with `v-bind="args"` 36 | template: args.template, 37 | }); 38 | export const DropzoneMessageClassName = Template.bind({}); 39 | DropzoneMessageClassName.argTypes = { 40 | ...controls, 41 | }; 42 | DropzoneMessageClassName.args = { 43 | template: '' 49 | + '' 61 | + '', 62 | }; 63 | export const DropzoneClass = Template.bind({}); 64 | DropzoneClass.argTypes = { 65 | ...controls, 66 | }; 67 | DropzoneClass.args = { 68 | template: '' 74 | + '' 90 | + '', 91 | }; 92 | 93 | export const DropzoneItemClassName = Template.bind({}); 94 | DropzoneItemClassName.argTypes = { 95 | ...controls, 96 | }; 97 | DropzoneItemClassName.args = { 98 | template: '' 104 | + '' 124 | + '', 125 | }; 126 | 127 | export const DropzoneDetailsClassName = Template.bind({}); 128 | DropzoneDetailsClassName.argTypes = { 129 | ...controls, 130 | }; 131 | DropzoneDetailsClassName.args = { 132 | template: '' 138 | + '' 156 | + '', 157 | }; 158 | -------------------------------------------------------------------------------- /stories/DropZone.upload.stories.js: -------------------------------------------------------------------------------- 1 | import DropZone from '@/index'; 2 | import './assets/custom.scss'; 3 | import { rest } from 'msw'; 4 | import { ref } from 'vue'; 5 | import controls from './controls'; 6 | import { worker } from '../mocks/browser'; 7 | 8 | export default { 9 | title: 'DropZone/upload', 10 | component: DropZone, 11 | decorators: [() => { 12 | worker.use( 13 | rest.post('http://localhost:5000/item', (req, res, ctx) => 14 | // Mock an infinite loading state. 15 | res( 16 | ctx.status(200), 17 | ctx.delay(100), 18 | )), 19 | ); 20 | return { template: '
' }; 21 | }], 22 | }; 23 | 24 | const Template = (args) => ({ 25 | // Components used in your story `template` are defined in the `components` object 26 | components: { DropZone }, 27 | // The story's `args` need to be mapped into the template through the `setup()` method 28 | setup() { 29 | const dropZoneRef = ref(); 30 | const uploadAction = () => { 31 | dropZoneRef.value.processQueue(); 32 | }; 33 | return { args, uploadAction, dropZoneRef }; 34 | }, 35 | // And then the `args` are bound to your component with `v-bind="args"` 36 | template: args.template, 37 | }); 38 | 39 | export const BasicUpload = Template.bind({}); 40 | BasicUpload.argTypes = { 41 | ...controls, 42 | }; 43 | BasicUpload.args = { 44 | template: '' 49 | + '' 51 | + '', 52 | }; 53 | export const ParallelUpload = Template.bind({}); 54 | 55 | ParallelUpload.argTypes = { 56 | ...controls, 57 | ...{ parallelUpload: { control: { type: 'number' } } }, 58 | }; 59 | 60 | ParallelUpload.args = { 61 | parallelUpload: 3, 62 | template: '' 67 | + '' 69 | + '', 70 | }; 71 | 72 | export const MultiUpload = Template.bind({}); 73 | MultiUpload.argTypes = { 74 | ...controls, 75 | }; 76 | MultiUpload.args = { 77 | template: '' 83 | + '' 85 | + '', 86 | }; 87 | 88 | export const Accepts = Template.bind({}); 89 | Accepts.argTypes = { 90 | ...controls, 91 | accepts: { 92 | control: { 93 | type: 'multi-select', 94 | multiple: true, 95 | options: ['image', 'video', 'exe', 'gif', 'png', 'doc', 'pdf'], 96 | }, 97 | }, 98 | }; 99 | Accepts.args = { 100 | accepts: [], 101 | template: '' 105 | + '' 107 | + '', 108 | }; 109 | export const AutoRetryOnError = Template.bind({}); 110 | AutoRetryOnError.argTypes = { 111 | ...controls, 112 | ...{ retryOnError: { control: { type: 'boolean' } } }, 113 | }; 114 | AutoRetryOnError.args = { 115 | retryOnError: true, 116 | template: '' 121 | + '' 123 | + '', 124 | }; 125 | 126 | export const AutoUploadOnDrop = Template.bind({}); 127 | AutoUploadOnDrop.argTypes = { 128 | ...controls, 129 | ...{ uploadOnDrop: { control: { type: 'boolean' } } }, 130 | }; 131 | AutoUploadOnDrop.args = { 132 | uploadOnDrop: true, 133 | template: '' 137 | + '' 139 | + '', 140 | }; 141 | 142 | export const Clickable = Template.bind({}); 143 | Clickable.argTypes = { 144 | ...controls, 145 | ...{ clickable: { control: { type: 'boolean' } } }, 146 | }; 147 | Clickable.args = { 148 | clickable: true, 149 | template: '' 153 | + '' 155 | + '', 156 | }; 157 | 158 | export const MaxFileNumberAndSize = Template.bind({}); 159 | MaxFileNumberAndSize.argTypes = { 160 | ...controls, 161 | ...{ maxFileSize: { control: { type: 'number' } } }, 162 | ...{ maxFiles: { control: { type: 'number' } } }, 163 | }; 164 | MaxFileNumberAndSize.args = { 165 | maxFileSize: 60000000, 166 | maxFiles: 2, 167 | template: '' 172 | + '' 174 | + '', 175 | }; 176 | 177 | export const Chuncking = Template.bind({}); 178 | Chuncking.argTypes = { 179 | ...controls, 180 | ...{ chunking: { control: { type: 'boolean' } } }, 181 | ...{ numberOfChunks: { control: { type: 'number' } } }, 182 | }; 183 | Chuncking.args = { 184 | chunking: true, 185 | numberOfChunks: 2, 186 | template: '' 191 | + '' 193 | + '', 194 | }; 195 | 196 | export const TriggerUploadManually = Template.bind({}); 197 | TriggerUploadManually.argTypes = { 198 | ...controls, 199 | }; 200 | TriggerUploadManually.args = { 201 | template: '' 207 | + '' 209 | + '' 210 | + '', 211 | }; 212 | -------------------------------------------------------------------------------- /stories/DropZone.xhrOptions.stories.js: -------------------------------------------------------------------------------- 1 | import DropZone from '@/index'; 2 | import './assets/custom.scss'; 3 | import { rest } from 'msw'; 4 | import controls from './controls'; 5 | import { worker } from '../mocks/browser'; 6 | 7 | export default { 8 | title: 'DropZone/xhr', 9 | component: DropZone, 10 | decorators: [() => { 11 | worker.use( 12 | rest.post('http://localhost:5000/item', (req, res, ctx) => 13 | // Mock an infinite loading state. 14 | res( 15 | ctx.status(200), 16 | ctx.delay(100), 17 | )), 18 | ); 19 | worker.use( 20 | rest.put('http://localhost:5000/item', (req, res, ctx) => 21 | // Mock an infinite loading state. 22 | res( 23 | ctx.status(200), 24 | ctx.delay(100), 25 | )), 26 | ); 27 | return { template: '
' }; 28 | }], 29 | }; 30 | 31 | const Template = (args) => ({ 32 | // Components used in your story `template` are defined in the `components` object 33 | components: { DropZone }, 34 | // The story's `args` need to be mapped into the template through the `setup()` method 35 | setup() { 36 | const sending = (files, xhr, formData) => { 37 | if (args.formData) { 38 | Object.entries(args.formData).forEach((k, v) => { 39 | formData.set(k, v); 40 | }); 41 | } 42 | }; 43 | return { args, sending }; 44 | }, 45 | // And then the `args` are bound to your component with `v-bind="args"` 46 | template: args.template, 47 | }); 48 | 49 | export const XhrMethodUrl = Template.bind({}); 50 | XhrMethodUrl.argTypes = { 51 | ...controls, 52 | method: { 53 | control: { 54 | type: 'select', 55 | multiple: true, 56 | options: ['POST', 'PUT'], 57 | }, 58 | }, 59 | url: { control: { type: 'text' } }, 60 | }; 61 | XhrMethodUrl.args = { 62 | url: 'http://localhost:5000/item', 63 | method: 'POST', 64 | template: '' 71 | + '' 73 | + '', 74 | }; 75 | 76 | export const AdditionalFormData = Template.bind({}); 77 | AdditionalFormData.argTypes = { 78 | ...controls, 79 | formData: { control: { type: 'object' } }, 80 | }; 81 | AdditionalFormData.args = { 82 | formData: { formData1: 'test', formData2: 'test', formData3: 'test' }, 83 | template: '' 89 | + '' 91 | + '', 92 | }; 93 | export const CustomHeaders = Template.bind({}); 94 | CustomHeaders.argTypes = { 95 | ...controls, 96 | headers: { control: { type: 'object' } }, 97 | }; 98 | CustomHeaders.args = { 99 | headers: { headerName1: 'test', headerName2: 'test', headerName3: 'test' }, 100 | template: '' 105 | + '' 107 | + '', 108 | }; 109 | export const WithCredentials = Template.bind({}); 110 | WithCredentials.argTypes = { 111 | ...controls, 112 | withCredentials: { control: { type: 'boolean' } }, 113 | }; 114 | WithCredentials.args = { 115 | withCredentials: true, 116 | template: '' 122 | + '' 124 | + '', 125 | }; 126 | 127 | export const ParamName = Template.bind({}); 128 | ParamName.argTypes = { 129 | ...controls, 130 | paramName: { control: { type: 'text' } }, 131 | }; 132 | ParamName.args = { 133 | paramName: 'file', 134 | template: '' 140 | + '' 142 | + '', 143 | }; 144 | 145 | export const XhrTimeout = Template.bind({}); 146 | XhrTimeout.argTypes = { 147 | ...controls, 148 | xhrTimeout: { control: { type: 'number' } }, 149 | }; 150 | XhrTimeout.args = { 151 | xhrTimeout: 500, 152 | template: '' 158 | + '' 160 | + '', 161 | }; 162 | -------------------------------------------------------------------------------- /stories/assets/abduction.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/assets/apocalypse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/assets/custom.scss: -------------------------------------------------------------------------------- 1 | .dropzoneCustomClass { 2 | background-color: whitesmoke; 3 | position: relative; 4 | display: flex; 5 | flex-flow: row nowrap; 6 | border: 4px solid greenyellow; 7 | } 8 | .dropzoneMessageClassName{ 9 | background-color: greenyellow; 10 | } 11 | .dropzoneItemClassName { 12 | 13 | } 14 | .dropzoneItemClassName:not(.dropzone--has-thumbnail) > .dropzone__item-thumbnail { 15 | background-color: darkgrey; 16 | border: 4px solid greenyellow; 17 | } 18 | .dropzoneItemClassName > .dropzone__item-thumbnail { 19 | background-color: whitesmoke; 20 | border: 4px solid black; 21 | } 22 | 23 | 24 | .dropzoneDetailsClassName { 25 | top: 18px; 26 | background-color: black; 27 | border: 3px solid whitesmoke; 28 | margin: 18px; 29 | color: greenyellow; 30 | width: 80%; 31 | } 32 | -------------------------------------------------------------------------------- /stories/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darknessnerd/drop-zone/ccf7944b0bca9c1d3a3b08dcf124db4a784d546b/stories/assets/demo.gif -------------------------------------------------------------------------------- /stories/assets/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = 'f7d0ed371e596d181f62c6f68c4b7baf' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (activeClientIds.has(client.id)) { 118 | const clonedResponse = response.clone() 119 | 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: clonedResponse.body === null ? null : await clonedResponse.text(), 129 | headers: serializeHeaders(clonedResponse.headers), 130 | redirected: clonedResponse.redirected, 131 | }, 132 | }) 133 | } 134 | 135 | return response 136 | } 137 | 138 | async function getResponse(event, client, requestId) { 139 | const { request } = event 140 | const requestClone = request.clone() 141 | const getOriginalResponse = () => fetch(requestClone) 142 | 143 | // Bypass mocking when the request client is not active. 144 | if (!client) { 145 | return getOriginalResponse() 146 | } 147 | 148 | // Bypass initial page load requests (i.e. static assets). 149 | // The absence of the immediate/parent client in the map of the active clients 150 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 151 | // and is not ready to handle requests. 152 | if (!activeClientIds.has(client.id)) { 153 | return await getOriginalResponse() 154 | } 155 | 156 | // Bypass requests with the explicit bypass header 157 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 158 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 159 | 160 | // Remove the bypass header to comply with the CORS preflight check. 161 | delete cleanRequestHeaders[bypassHeaderName] 162 | 163 | const originalRequest = new Request(requestClone, { 164 | headers: new Headers(cleanRequestHeaders), 165 | }) 166 | 167 | return fetch(originalRequest) 168 | } 169 | 170 | // Send the request to the client-side MSW. 171 | const reqHeaders = serializeHeaders(request.headers) 172 | const body = await request.text() 173 | 174 | const clientMessage = await sendToClient(client, { 175 | type: 'REQUEST', 176 | payload: { 177 | id: requestId, 178 | url: request.url, 179 | method: request.method, 180 | headers: reqHeaders, 181 | cache: request.cache, 182 | mode: request.mode, 183 | credentials: request.credentials, 184 | destination: request.destination, 185 | integrity: request.integrity, 186 | redirect: request.redirect, 187 | referrer: request.referrer, 188 | referrerPolicy: request.referrerPolicy, 189 | body, 190 | bodyUsed: request.bodyUsed, 191 | keepalive: request.keepalive, 192 | }, 193 | }) 194 | 195 | switch (clientMessage.type) { 196 | case 'MOCK_SUCCESS': { 197 | return delayPromise( 198 | () => respondWithMock(clientMessage), 199 | clientMessage.payload.delay, 200 | ) 201 | } 202 | 203 | case 'MOCK_NOT_FOUND': { 204 | return getOriginalResponse() 205 | } 206 | 207 | case 'NETWORK_ERROR': { 208 | const { name, message } = clientMessage.payload 209 | const networkError = new Error(message) 210 | networkError.name = name 211 | 212 | // Rejecting a request Promise emulates a network error. 213 | throw networkError 214 | } 215 | 216 | case 'INTERNAL_ERROR': { 217 | const parsedBody = JSON.parse(clientMessage.payload.body) 218 | 219 | console.error( 220 | `\ 221 | [MSW] Request handler function for "%s %s" has thrown the following exception: 222 | 223 | ${parsedBody.errorType}: ${parsedBody.message} 224 | (see more detailed error stack trace in the mocked response body) 225 | 226 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 227 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 228 | `, 229 | request.method, 230 | request.url, 231 | ) 232 | 233 | return respondWithMock(clientMessage) 234 | } 235 | } 236 | 237 | return getOriginalResponse() 238 | } 239 | 240 | self.addEventListener('fetch', function (event) { 241 | const { request } = event 242 | 243 | // Bypass navigation requests. 244 | if (request.mode === 'navigate') { 245 | return 246 | } 247 | 248 | // Opening the DevTools triggers the "only-if-cached" request 249 | // that cannot be handled by the worker. Bypass such requests. 250 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 251 | return 252 | } 253 | 254 | // Bypass all requests when there are no active clients. 255 | // Prevents the self-unregistered worked from handling requests 256 | // after it's been deleted (still remains active until the next reload). 257 | if (activeClientIds.size === 0) { 258 | return 259 | } 260 | 261 | const requestId = uuidv4() 262 | 263 | return event.respondWith( 264 | handleRequest(event, requestId).catch((error) => { 265 | console.error( 266 | '[MSW] Failed to mock a "%s" request to "%s": %s', 267 | request.method, 268 | request.url, 269 | error, 270 | ) 271 | }), 272 | ) 273 | }) 274 | 275 | function serializeHeaders(headers) { 276 | const reqHeaders = {} 277 | headers.forEach((value, name) => { 278 | reqHeaders[name] = reqHeaders[name] 279 | ? [].concat(reqHeaders[name]).concat(value) 280 | : value 281 | }) 282 | return reqHeaders 283 | } 284 | 285 | function sendToClient(client, message) { 286 | return new Promise((resolve, reject) => { 287 | const channel = new MessageChannel() 288 | 289 | channel.port1.onmessage = (event) => { 290 | if (event.data && event.data.error) { 291 | return reject(event.data.error) 292 | } 293 | 294 | resolve(event.data) 295 | } 296 | 297 | client.postMessage(JSON.stringify(message), [channel.port2]) 298 | }) 299 | } 300 | 301 | function delayPromise(cb, duration) { 302 | return new Promise((resolve) => { 303 | setTimeout(() => resolve(cb()), duration) 304 | }) 305 | } 306 | 307 | function respondWithMock(clientMessage) { 308 | return new Response(clientMessage.payload.body, { 309 | ...clientMessage.payload, 310 | headers: clientMessage.payload.headers, 311 | }) 312 | } 313 | 314 | function uuidv4() { 315 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 316 | const r = (Math.random() * 16) | 0 317 | const v = c == 'x' ? r : (r & 0x3) | 0x8 318 | return v.toString(16) 319 | }) 320 | } 321 | -------------------------------------------------------------------------------- /stories/controls.js: -------------------------------------------------------------------------------- 1 | export default { 2 | url: { 3 | table: { disable: true }, 4 | }, 5 | uploadOnDrop: { 6 | table: { disable: true }, 7 | }, 8 | retryOnError: { 9 | table: { disable: true }, 10 | }, 11 | parallelUpload: { 12 | table: { disable: true }, 13 | }, 14 | multipleUpload: { 15 | table: { disable: true }, 16 | }, 17 | headers: { 18 | table: { disable: true }, 19 | }, 20 | xhrTimeout: { 21 | table: { disable: true }, 22 | }, 23 | withCredentials: { 24 | table: { disable: true }, 25 | }, 26 | method: { 27 | table: { disable: true }, 28 | }, 29 | maxFileSize: { 30 | table: { disable: true }, 31 | }, 32 | hiddenInputContainer: { 33 | table: { disable: true }, 34 | }, 35 | clickable: { 36 | table: { disable: true }, 37 | }, 38 | acceptedFiles: { 39 | table: { disable: true }, 40 | }, 41 | chunking: { 42 | table: { disable: true }, 43 | }, 44 | numberOfChunks: { 45 | table: { disable: true }, 46 | }, 47 | template: { 48 | table: { disable: true }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /tests/unit/DropZone.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import DropZone from '@/DropZone.vue'; 3 | 4 | describe('DropZone.vue', () => { 5 | beforeEach(() => { 6 | jest.resetModules(); 7 | jest.clearAllMocks(); 8 | }); 9 | it('renders correctly', () => { 10 | const wrapper = shallowMount(DropZone, 11 | { 12 | attachTo: document.body, 13 | }); 14 | expect(wrapper).toMatchSnapshot(); 15 | wrapper.unmount(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/DropZone.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DropZone.vue renders correctly 1`] = ` 4 | VueWrapper { 5 | "__app": Object { 6 | "_component": Object { 7 | "__emits": Object {}, 8 | "__props": Array [ 9 | Object {}, 10 | Array [], 11 | ], 12 | "name": "VTU_ROOT", 13 | "render": [Function], 14 | }, 15 | "_container":
18 |
21 |
24 | 25 | Drop here 26 | 27 |
28 | 29 | 30 |
31 |
, 32 | "_context": Object { 33 | "app": [Circular], 34 | "components": Object { 35 | "transition": Object { 36 | "name": "transition", 37 | "props": undefined, 38 | "render": [Function], 39 | }, 40 | "transition-group": Object { 41 | "name": "transition-group", 42 | "props": undefined, 43 | "render": [Function], 44 | }, 45 | }, 46 | "config": Object { 47 | "errorHandler": undefined, 48 | "globalProperties": Object {}, 49 | "isCustomElement": [Function], 50 | "isNativeTag": [Function], 51 | "optionMergeStrategies": Object {}, 52 | "performance": false, 53 | "warnHandler": undefined, 54 | }, 55 | "directives": Object {}, 56 | "mixins": Array [ 57 | Object { 58 | "__emits": null, 59 | "__props": Array [], 60 | "beforeCreate": [Function], 61 | }, 62 | ], 63 | "provides": Object {}, 64 | "reload": [Function], 65 | }, 66 | "_props": null, 67 | "_uid": 0, 68 | "component": [Function], 69 | "config": Object { 70 | "errorHandler": undefined, 71 | "globalProperties": Object {}, 72 | "isCustomElement": [Function], 73 | "isNativeTag": [Function], 74 | "optionMergeStrategies": Object {}, 75 | "performance": false, 76 | "warnHandler": undefined, 77 | }, 78 | "directive": [Function], 79 | "mixin": [Function], 80 | "mount": [Function], 81 | "provide": [Function], 82 | "unmount": [Function], 83 | "use": [Function], 84 | "version": "3.0.6", 85 | }, 86 | "__setProps": [Function], 87 | "componentVM": Object { 88 | "acceptedFiles": null, 89 | "accepts": Array [], 90 | "all": Object {}, 91 | "chunking": false, 92 | "clickable": true, 93 | "dropzone":
96 |
99 | 100 | Drop here 101 | 102 |
103 | 104 | 105 |
, 106 | "dropzoneClassName": "dropzone__box", 107 | "dropzoneDetailsClassName": "dropzone__details--style", 108 | "dropzoneItemClassName": "dropzone__item--style", 109 | "dropzoneMessageClassName": "dropzone__message--style", 110 | "dropzoneRef": "dropzone", 111 | "filesize": [Function], 112 | "handleDragOver": [Function], 113 | "headers": null, 114 | "hiddenInputContainer": "body", 115 | "ids": Array [], 116 | "maxFileSize": 1000000, 117 | "maxFiles": null, 118 | "method": "POST", 119 | "multipleUpload": false, 120 | "numberOfChunks": 10, 121 | "onDrop": [Function], 122 | "parallelUpload": 3, 123 | "paramName": "file", 124 | "processQueue": [Function], 125 | "removeFile": [Function], 126 | "retryOnError": false, 127 | "uploadOnDrop": true, 128 | "url": null, 129 | "withCredentials": false, 130 | "xhrTimeout": 60000, 131 | }, 132 | "rootVM": Object {}, 133 | } 134 | `; 135 | --------------------------------------------------------------------------------