├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ ├── publishing.yml │ └── validating.yml ├── .gitignore ├── .npmignore ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cypress.config.js ├── cypress ├── e2e │ ├── 1-basic │ │ └── zero_configuration.cy.js │ └── 2-integrations │ │ └── aws-s3-multipart.cy.js ├── fixtures │ ├── example.json │ ├── image.jpg │ └── image.tiff ├── plugins │ └── index.js └── support │ └── e2e.js ├── package.json ├── src ├── .gitignore ├── basic.scss ├── dropzone.js ├── dropzone.scss ├── emitter.js ├── options.js └── preview-template.html ├── test ├── .gitignore ├── built │ └── .gitignore ├── karma.conf.js ├── test-server.js ├── test-sites │ ├── 1-basic │ │ └── zero_configuration.html │ ├── 2-integrations │ │ └── aws-s3-multipart.html │ ├── README.md │ ├── dist │ └── index.html ├── unit-tests.js └── unit-tests │ ├── README.md │ ├── all.js │ ├── amazon-s3.js │ ├── emitter.js │ ├── static-functions.js │ └── utils.js ├── tool └── dropzone-global.js ├── types └── dropzone.d.ts └── yarn.lock /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Don't be a jerk! 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Browser / OS:** 27 | - OS & Device: [e.g. iOS 14 on iPhone X] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please submit feature requests in the Discussion tab! I will close issues that are feature requests. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Question / Help 4 | about: I have a question or I need help 5 | 6 | --- 7 | 8 | # Question / free support 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Make sure to run the test suite. 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ dev ] 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: [ 'javascript' ] 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v2 30 | with: 31 | languages: ${{ matrix.language }} 32 | paths: src 33 | 34 | - name: Enable Corepack 35 | run: corepack enable 36 | 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Build 41 | run: yarn run build 42 | 43 | - name: Perform CodeQL Analysis 44 | uses: github/codeql-action/analyze@v2 45 | -------------------------------------------------------------------------------- /.github/workflows/publishing.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | name: Publish latest release 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup node 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: '20.x' 15 | cache: 'npm' 16 | registry-url: 'https://registry.npmjs.org' 17 | - name: Enable Corepack 18 | run: corepack enable 19 | - name: Install dependencies 20 | run: yarn install 21 | - name: Build dist files 22 | run: yarn run build 23 | - name: Publish package 24 | uses: JS-DevTools/npm-publish@v3 25 | with: 26 | token: ${{ secrets.NPM_TOKEN }} 27 | access: 'public' 28 | -------------------------------------------------------------------------------- /.github/workflows/validating.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: [push, pull_request] 3 | jobs: 4 | run-test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node: [ '18', '20' ] 9 | name: Node ${{ matrix.node }} test 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup node 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ matrix.node }} 16 | cache: 'npm' 17 | 18 | - name: Enable Corepack 19 | run: corepack enable 20 | 21 | - name: Install dependencies 22 | run: yarn install 23 | 24 | - name: Build 25 | run: yarn run build 26 | 27 | - name: Run test 28 | run: yarn run test 29 | 30 | - uses: cypress-io/github-action@v6 31 | with: 32 | install-command : yarn install --immutable --immutable-cache 33 | # fix issue with "Cannot find module 'cypress'" 34 | # https://github.com/cypress-io/github-action/issues/430#issuecomment-949936528 35 | command: yarn test:e2e 36 | start: yarn start-test-server 37 | wait-on: "http://localhost:8888" 38 | wait-on-timeout: 5 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | components 3 | node_modules 4 | .DS_Store 5 | .sass-cache 6 | _site 7 | _config.yaml 8 | .idea 9 | dist 10 | .parcel-cache 11 | 12 | # The GitLab pages artifacts 13 | public 14 | 15 | # yarn stuff 16 | .pnp.* 17 | .yarn/* 18 | !.yarn/patches 19 | !.yarn/plugins 20 | !.yarn/releases 21 | !.yarn/sdks 22 | !.yarn/versions 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .parcel-cache 3 | .sass-cache 4 | .idea 5 | .pnp* 6 | .yarn 7 | .yarnrc.yml 8 | cypress 9 | cypress.json 10 | tool 11 | test 12 | babel.config.js 13 | webpack.config.js 14 | CHANGELOG.md 15 | CODE_OF_CONDUCT.md 16 | CONTRIBUTING.md 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | # yarnrc for dropzone 2 | enableTelemetry: 0 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for dropzone 2 | 3 | ## 7.3.1 4 | 5 | * bug/minor: ux: avoid displaying empty confirm on cancel action (by spatarel in #42) 6 | * doc: readme: fix broken link by AalbatrossGuy in #41 7 | 8 | ## 7.3.0 9 | 10 | * Emit emptyfolder event when receiving an empty folder by @CyBot in #35 11 | * Fix for Issue #36 by @alloylab in #37 12 | 13 | ## 7.2.1 14 | 15 | * bug/minor: add null check to prevent uncaught type error on parent node (#34 by Christina Toegl) 16 | This fix prevents uncaught type errors thrown if hiddenFileInput has no parentNode. 17 | 18 | ## 7.2.0 19 | 20 | * feat: uuid: add fallback uuid generator for insecure contexts (PR #24 fix #22 by Dieter Oberkofler) 21 | * dev: minor: improve fallback uuid-generator (PR #27 by Erik Axel Nielsen) 22 | * dev: chore: bump yarnpkg from 4.2.2 to 4.5.3 23 | 24 | ## 7.1.6 25 | 26 | * Add support for MacOS 14+ filenames (PR #21 by @brentmartinmiller, fix #20) 27 | * Add custom type definition (PR #19 by @doberkofler, fix #7) 28 | * Readme improvements (PR #15 by @jules-w2) 29 | * Use setAttributes() for setting the aria-label 30 | 31 | ## 7.1.5 32 | 33 | * Fix SASS deprecation warnings with version 1.77.7 (PR #14 by Jules) 34 | * Fix import statements in README.md (fix #16) 35 | * Mention the need for secure context in README (fix #13) 36 | * Upgrade `@swc/helpers` from 0.5.11 to 0.5.12 37 | 38 | ## 7.1.4 39 | 40 | Fix bug where using an ID for selecting the dropzone element would make the lib crash. fix #12 41 | 42 | ## 7.1.3 43 | 44 | A very small patch release with a fix regarding the `form` attribute of the hidden `input` field. See dropzone/dropzone#2300. PR #11 by @bancer. 45 | 46 | ## 7.1.2 47 | 48 | A small patch release with an important bugfix that was asked by the community for a long time: 49 | 50 | * fix: parallelChunkUploads should respect parallelUploads. fix dropzone/dropzone#2030 (based on PR dropzone/dropzone#2249 by @labor4) 51 | 52 | ## 7.1.1 53 | 54 | A small patch release with an important bugfix that was asked by the community for a long time: 55 | 56 | * Fix issue with exif resizing of thumbnail that messed up the files. fix dropzone/dropzone#1967 57 | PR dropzone/dropzone#2001 by @kaymes 58 | 59 | ## 7.1.0 60 | 61 | With this release, we drop deprecated and old things, and bring back the test suite. 62 | 63 | ### Breaking change 64 | 65 | If you have `acceptedMimeTypes` in your options, replace it with `acceptedFiles`. This option was deprecated 3 years ago, and has now be completely removed. 66 | 67 | ### Changes 68 | 69 | * All tests now run: a previous change made it run only one single test! (by using `describe.only()` instead of `describe()`) 70 | * The test suite now runs on each commit in a GH Action 71 | * Remove a dependency on `just-extend`: use `Object.assign()` instead 72 | * Simplify greatly the browser detection and remove blockedBrowser code that only targeted opera 12 from 12 years ago 73 | * Removed some IE related code and simplified some code paths as we now expect a decent enough browser 74 | * Some code cleanup: remove unused variables 75 | * Use crypto.randomUUID instead of a custom function 76 | * Run CodeQL only in src/ 77 | * Update cypress config 78 | 79 | ## 7.0.4 80 | 81 | * Upgrade dependencies 82 | * Add SECURITY.md file (and enable security settings on repository) 83 | 84 | ## 7.0.3 85 | 86 | * Add more files to .npmignore: no need to distribute parcel-cache or documentation files 87 | * Remove composer.json 88 | 89 | ## 7.0.2 90 | 91 | * Try and fix the publish on npm action (it worked). 92 | 93 | ## 7.0.1 94 | 95 | * Upgrade publish action version. Try and fix the publish on npm action. 96 | 97 | ## 7.0.0 98 | 99 | * No issues were reported with alpha versions, so this is the (maintained) stable version now. There are no changes from alpha2. 100 | 101 | ## 7.0.0-alpha2 102 | 103 | * README changes 104 | 105 | ## 7.0.0-alpha 106 | 107 | This is the first release of this fork. Mainly to test if publish action runs fine. 108 | 109 | * upgrade some dependencies 110 | * add github actions 111 | * fix improper grammer PR dropzone/dropzone#2211 by @Offlein 112 | * Send at least one chunk if filesize is 0. Fix for dropzone/dropzone#1982. PR dropzone/dropzone#2153 by @Forceu 113 | * fix typos. fix dropzone/dropzone#2140 114 | * add arialabel to hidden input 115 | 116 | ## 6.0.0-beta.2 117 | 118 | - Add `binaryBody` support (thanks to @patrickbussmann and @meg1502). 119 | - This adds full support for AWS S3 Multipart Upload. 120 | - There is an example setup for this now in `test/test-sites/2-integrations`. 121 | 122 | ## 6.0.0-beta.1 123 | 124 | ### Breaking 125 | 126 | - Dropzone is dropping IE support! If you still need to support IE, please use 127 | `5.9.3`. You can download it here: 128 | https://github.com/dropzone/dropzone/releases/download/v5.9.3/dist.zip 129 | - `Dropzone.autoDiscover` has been removed! If you want to auto discover your 130 | elements, invoke `Dropzone.discover()` after your HTML has loaded and it will 131 | do the same. 132 | - The `dropzone-amd-module` files have been removed. There is now a 133 | `dropzone.js` and a `dropzone.mjs` in the dist folder. 134 | - The `min/` folder has been removed. `dropzone.min.js` is now the only 135 | file that is minimized. 136 | - Remove `Dropzone.extend` and replace by the `just-extend` package. 137 | - There is no more `Dropzone.version`. 138 | 139 | ## 5.9.3 140 | 141 | - Fix incorrect resize method used for creating thumbnails of existing files 142 | (thanks to @gplwhite) 143 | 144 | ## 5.9.2 145 | 146 | - Handle `xhr.readyState` in the `submitRequest` function and don't attempt to 147 | send if it's not `1` (OPENED). (thanks to @bobbysmith007) 148 | 149 | ## 5.9.1 150 | 151 | - Fix the way upload progress is calculated when using chunked uploads. (thanks 152 | to @ckovey) 153 | 154 | ## 5.9.0 155 | 156 | - Properly handle when timeout is null or 0 157 | - Make the default of timeout null 158 | 159 | ## 5.8.1 160 | 161 | - Fix custom event polyfill for IE11 162 | - Fix build to use ES5 instead of ES6, which was broken due to webpack upgrade. 163 | (thanks to @fukayatsu) 164 | 165 | ## 5.8.0 166 | 167 | - Dropzone now also triggers custom events on the DOM element. The custom events 168 | are the same as the events you can listen on with Dropzone but start with 169 | `dropzone:`. (thanks to @1cg) 170 | - Moved the `./src/options.js` previewTemplate in its own 171 | `preview-template.html` file. 172 | - Switched to yarn as the primary package manager (shouldn't affect anybody that 173 | is not working Dropzone itself). 174 | 175 | ## 5.7.6 176 | 177 | - Revert `dist/min/*.css` files to be named `dist/min/*.min.css`. 178 | - Setup bower releases. 179 | 180 | ## 5.7.5 181 | 182 | - Rename `blacklistedBrowsers` to `blockedBrowsers` (but still accept 183 | `blacklistedBrowsers` for legacy). 184 | - Add automatic trigger for packagist deployment. 185 | - Fix links in `package.json`. 186 | 187 | ## 5.7.4 188 | 189 | - Prevent hidden input field from getting focus (thanks to @sinedied) 190 | - Fix documentation of `maxFilesize` (thanks to @alxndr-w) 191 | - Fix build issues so the UMD module can be imported properly 192 | 193 | ## 5.7.3 (retracted) 194 | 195 | - Add `disablePreviews` option. 196 | - Fix IE problems with Symbols. 197 | - **WARNING**: This release had issues because the .js files couldn't be 198 | imported as AMD/CommonJS packages properly. The standalone version worked fine 199 | though. I have retracted this version from npm but have left the release on 200 | GitHub. 201 | 202 | ## 5.7.2 203 | 204 | - Base the calculation of the chunks to send on the transformed files 205 | - Properly display seconds (instead of ms) in error message when timeout is 206 | reached 207 | - Properly handle it when `options.method` is a function (there was a bug, which 208 | always assumed that it was a String) (thanks to @almdac) 209 | - Fix orientation on devices that already handle it properly (thanks to @nosegrind) 210 | - Handle additionalParams when they are an Array the way it's expected (thanks to @wiz78) 211 | - Check for `string` in error message type instead of `String` (thanks to @RuQuentin) 212 | 213 | ## 5.7.1 214 | 215 | - Fix issue with IE (thanks to @Bjego) 216 | 217 | ## 5.7.0 218 | 219 | - Cleanup the SVGs used to remove IDs and sketch attributes 220 | Since SVGs are duplicated this resulted in duplicate IDs being used. 221 | - Add a dedicated `displayExistingFile` method to make it easier to display 222 | server files. 223 | - Fix an error where chunked uploads don't work as expected when transforming 224 | files before uploading. 225 | - Make the default text a button so it's discoverable by keyboard. 226 | 227 | ## 5.6.1 228 | 229 | - Re-released due to missing javascript files 230 | - Removes `npm` dependency that got added by mistake 231 | 232 | ## 5.6.0 233 | 234 | - Timeout now generates an error (thanks to @mmollick) 235 | - Fix duplicate iteration of error processing (#159 thanks @darkland) 236 | - Fixed bootstrap example (@thanks to @polosatus) 237 | - The `addedfiles` event now triggers _after_ each individual `addedfile` event 238 | when dragging files into the dropzone, which is the same behavior as when 239 | clicking it. 240 | 241 | ## 5.5.0 242 | 243 | - Correct photo orientation before uploading (if enabled) (thanks to @nosegrind) 244 | - Remove a potential memory leak in some browsers by keeping a reference to `xhr` inside the individual 245 | chunk objects (thanks to @clayton2) 246 | - Allow HTML in the remove links (thanks to @christianklemp) 247 | - `hiddenInputContainer` can now be an `HtmlElement` in addition to a selector String (thanks to @WAmeling) 248 | - Fix default values on website (since the last deployment, the default values all stated `null`) 249 | 250 | ## 5.4.0 251 | 252 | - Fix IE11 issue when dropping files 253 | 254 | ## 5.3.1 255 | 256 | - Fix broken npm release of 5.3.0 257 | 258 | ## 5.3.0 259 | 260 | - Add `dictUploadCanceled` option (thanks to @Fohlen) 261 | - Fix issue with drag'n'drop on Safari and IE10 (thanks to @taylorryan) 262 | - Fix issues with resizing if SVG files are dropped (thanks to @saschagros) 263 | 264 | ## 5.2.0 265 | 266 | - **Migrated from coffeescript to ES6!** 267 | - **Added chunked file uploading!** The highly requested chunked uploads are now available. Checkout the 268 | `chunking` option documentation for more information. 269 | - Fixed a faulty `console.warning` (should be `console.warn`) 270 | - If an input field doesn't have a name, don't include it when sending the form (thanks to @remyj38) 271 | - Opera on Windows Phone is now also blacklisted (thanks to @dracos1) 272 | - If a custom preview element is used, it is now properly handled when it doesn't have a parent (thanks to @uNmAnNeR) 273 | 274 | ## 5.1.1 275 | 276 | - Fix issue where showing files already on the server fails, due to the missing `file.upload.filename` 277 | - Fix issue where `file.upload.filename` gets removed after the file uploaded completed 278 | - Properly handle `arraybuffer` and `blob` responses 279 | 280 | ## 5.1.0 281 | 282 | - Add possibility to translate file sizes. (#16 thanks to @lerarybak for that) 283 | - Fix duplicate filenames in multiple file uploads (#15) 284 | - The `renameFilename` option has been **deprecated**. Use `renameFile` instead 285 | (which also has a slightly different function signature) 286 | - The `renameFile` option now stores the new name in `file.upload.filename` (#1) 287 | 288 | ## 5.0.1 289 | 290 | - Add missing dist/ folder to npm. 291 | 292 | ## 5.0.0 293 | 294 | - **Add support for browser image resizing!** Yes, really. The new options are: `resizeWidth`, `resizeHeight`, `resizeMimeType` and `resizeQuality`. 295 | Thanks a lot to [MD Systems](https://www.md-systems.ch/) for donating the money to make this a reality. 296 | - Fix IE11 issue with `options.timeout` 297 | - Resolve an issue that occurs in the iOS squashed image fix, where some transparent PNGs are stretched inaccurately 298 | 299 | ## 4.4.0 300 | 301 | - Add `options.timeout` 302 | 303 | ## 4.3.0 304 | 305 | Added Changelog. Sorry that this didn't happen sooner. 306 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at m@tias.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | ## Communicate 4 | 5 | Before you start implementing new features, please create an issue about it 6 | first and discuss your intent. 7 | 8 | It might be something that someone else is already implementing or that goes 9 | against the concepts of Dropzone, and I really hate rejecting pull requests 10 | others spent hours writing on. 11 | 12 | ## Developer Dependencies 13 | 14 | The first thing you need to do, is to install the developer dependencies: 15 | 16 | ```bash 17 | $ yarn install 18 | ``` 19 | 20 | This will install all the tools you need to compile the source files and to test 21 | the library. 22 | 23 | ## Testing 24 | 25 | Testing is done on the compiled files. So either run `yarn build` or 26 | `yarn watch` first, and then `yarn test`. 27 | 28 | ### Cypress 29 | 30 | In order to run the cypress tests (e2e tests), you need to first start the 31 | test server (`yarn start-test-server`) and then cypress `yarn cypress open`. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | 3 | (The MIT License) 4 | 5 | Copyright (c) 2021 Matias Meno 6 | Logo (c) 2015 by "Weare1910" 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dropzone.js 2 | 3 | [![npm version](https://badge.fury.io/js/@deltablot%2Fdropzone.svg)](https://badge.fury.io/js/@deltablot%2Fdropzone) 4 | [![Validate](https://github.com/NicolasCARPi/dropzone/actions/workflows/validating.yml/badge.svg)](https://github.com/NicolasCARPi/dropzone/actions/workflows/validating.yml) 5 | [![CodeQL](https://github.com/NicolasCARPi/dropzone/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/NicolasCARPi/dropzone/actions/workflows/codeql-analysis.yml) 6 | 7 | # Fork 8 | 9 | This fork exists because upstream isn't maintained anymore. Its goal is to maintain this library in a good state, with mainly bugfixes and possibly minor improvements. 10 | 11 | ## the new package name is `@deltablot/dropzone` 12 | 13 | # Description 14 | 15 | Dropzone is a JavaScript library that turns any HTML element into a dropzone. 16 | This means that a user can drag and drop a file onto it, and Dropzone will 17 | display file previews and upload progress, and handle the upload for you via 18 | XHR. 19 | 20 | It is fully configurable, can be styled according to your needs and is trusted by 21 | thousands. 22 | 23 |
24 | Dropzone Screenshot 25 |
26 | 27 | ## Quickstart 28 | 29 | Install: 30 | 31 | ```bash 32 | $ npm install --save @deltablot/dropzone 33 | # or with yarn: 34 | $ yarn add @deltablot/dropzone 35 | # or with bun: 36 | $ bun add @deltablot/dropzone 37 | ``` 38 | 39 | First argument is an element to bind to, second argument is the [`options`](./src/options.js) object. 40 | 41 | Use as **ES6 module** (recommended): 42 | 43 | ```js 44 | import { Dropzone } from "@deltablot/dropzone"; 45 | const dropzone = new Dropzone("#elementId", { url: "/file/post" }); 46 | ``` 47 | 48 | or use as **CommonJS module**: 49 | 50 | ```js 51 | const { Dropzone } = require("@deltablot/dropzone"); 52 | const dropzone = new Dropzone("#elementId", { url: "/file/post" }); 53 | ``` 54 | 55 | [👉 Checkout our example implementations for different 56 | bundlers](https://github.com/dropzone/dropzone-examples) 57 | 58 |
59 | 60 | **CSS** 61 | 62 | ```css 63 | @import "@deltablot/dropzone/src/dropzone"; 64 | ``` 65 | 66 | ## Not using a package manager or bundler? 67 | 68 | Use the standalone files like this: 69 | 70 | ```html 71 | 72 | 77 | 78 |
79 | 80 | 84 | ``` 85 | 86 | --- 87 | 88 | - [📚 Full documentation](https://docs.dropzone.dev) 89 | - [⚙️ `src/options.js`](https://github.com/NicolasCARPi/dropzone/blob/master/src/options.js) 90 | for all available options 91 | 92 | --- 93 | 94 | ## Community 95 | 96 | If you need support please open an [issue](https://github.com/NicolasCARPi/dropzone/issues). 97 | 98 | If you have a feature request or want to discuss something, please use the 99 | [issues](https://github.com/NicolasCARPi/dropzone/issues) as well. 100 | 101 | ## Main features ✅ 102 | 103 | - Beautiful by default 104 | - Image thumbnail previews. Simply register the callback `thumbnail(file, data)` 105 | and display the image wherever you like 106 | - High-DPI screen support 107 | - Multiple files and synchronous uploads 108 | - Progress updates 109 | - Support for large files 110 | - Chunked uploads (upload large files in smaller chunks) 111 | - Support for Amazon S3 Multipart upload 112 | - Complete theming. The look and feel of Dropzone is just the default theme. You 113 | can define everything yourself by overwriting the default event listeners. 114 | - Browser image resizing (resize the images before you upload them to your 115 | server) 116 | - Well tested 117 | 118 | # MIT License 119 | 120 | See the [LICENSE](./LICENSE) file 121 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for **@deltablot/dropzone**. 4 | 5 | ## Reporting a security issue 6 | 7 | See our [Responsible Disclosure Policy](https://www.deltablot.com/security/). 8 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // We've imported your old cypress plugins here. 6 | // You may want to clean this up later by importing these. 7 | setupNodeEvents(on, config) { 8 | return require('./cypress/plugins/index.js')(on, config) 9 | }, 10 | baseUrl: 'http://localhost:8888', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/1-basic/zero_configuration.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Dropzone with zero configuration", () => { 4 | beforeEach(() => { 5 | cy.visit("/1-basic/zero_configuration.html"); 6 | }); 7 | 8 | it("uploads single file", () => { 9 | cy.intercept("POST", "/").as("upload"); 10 | 11 | cy.get(".dropzone").selectFile("cypress/fixtures/image.jpg", { 12 | action: "drag-drop", 13 | }); 14 | 15 | cy.wait("@upload").then((interception) => { 16 | expect(interception.response.statusCode).to.eq(200); 17 | expect(interception.response.body).to.deep.eq({ 18 | success: true, 19 | }); 20 | }); 21 | }); 22 | 23 | it("uploads two files", () => { 24 | cy.intercept("POST", "/").as("upload"); 25 | 26 | cy.get(".dropzone") 27 | .selectFile("cypress/fixtures/image.jpg", { action: "drag-drop" }) 28 | .selectFile("cypress/fixtures/image.tiff", { action: "drag-drop" }) 29 | 30 | cy.wait("@upload").then((interception) => { 31 | expect(interception.response.statusCode).to.eq(200); 32 | expect(interception.response.body).to.deep.eq({ 33 | success: true, 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /cypress/e2e/2-integrations/aws-s3-multipart.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Dropzone with zero configuration", () => { 4 | const imageSize = 373282; 5 | const chunkSize = 100 * 1024; 6 | 7 | beforeEach(() => { 8 | cy.visit("/2-integrations/aws-s3-multipart.html"); 9 | }); 10 | 11 | it("uploads single file", () => { 12 | cy.intercept( 13 | "PUT", 14 | "/amazon-multipart-upload?uploadId=demo-id&partNumber=*" 15 | ).as("upload"); 16 | cy.intercept("POST", "/amazon-complete").as("complete"); 17 | 18 | cy.get(".dropzone").selectFile("cypress/fixtures/image.jpg", { 19 | action: "drag-drop", 20 | }); 21 | let remainingSize = imageSize; 22 | let partEtags = []; 23 | for (let i = 0; i < 4; i++) { 24 | cy.wait("@upload").then((interception) => { 25 | partEtags.push( 26 | interception.response.headers["etag"].replaceAll('"', "") 27 | ); 28 | expect(interception.request.headers["content-type"]).to.eq( 29 | "image/jpeg" 30 | ); 31 | expect(interception.request.headers["content-length"]).to.eq( 32 | `${remainingSize > chunkSize ? chunkSize : remainingSize}` 33 | ); 34 | expect(interception.response.body).to.deep.eq({ 35 | success: true, 36 | }); 37 | remainingSize -= chunkSize; 38 | }); 39 | } 40 | 41 | cy.wait("@complete").then((interception) => { 42 | // Now making sure that the finalise request is valid as well. 43 | expect(interception.request.body).to.eq( 44 | JSON.stringify({ 45 | // This is the demo id that we defined in the html. 46 | UploadId: "demo-id", 47 | MultipartUpload: { 48 | Parts: [ 49 | // The individual etags have been returned by the server, and 50 | // stored in the `@upload` intercept handler. 51 | { PartNumber: 1, ETag: partEtags[0] }, 52 | { PartNumber: 2, ETag: partEtags[1] }, 53 | { PartNumber: 3, ETag: partEtags[2] }, 54 | { PartNumber: 4, ETag: partEtags[3] }, 55 | ], 56 | }, 57 | }) 58 | ); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasCARPi/dropzone/e02fdb74099ef401c1025b205fd6d21694eb63c8/cypress/fixtures/image.jpg -------------------------------------------------------------------------------- /cypress/fixtures/image.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicolasCARPi/dropzone/e02fdb74099ef401c1025b205fd6d21694eb63c8/cypress/fixtures/image.tiff -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deltablot/dropzone", 3 | "version": "7.3.1", 4 | "description": "Handles drag and drop of files for you.", 5 | "keywords": [ 6 | "dragndrop", 7 | "drag and drop", 8 | "file upload", 9 | "upload" 10 | ], 11 | "homepage": "https://www.dropzone.dev/js", 12 | "source": "src/dropzone.js", 13 | "main": "dist/dropzone.js", 14 | "module": "dist/dropzone.mjs", 15 | "standalone": "dist/dropzone-min.js", 16 | "types": "dist/dropzone.d.ts", 17 | "browserslist": "defaults, > 0.25%", 18 | "targets": { 19 | "main": { 20 | "source": "src/dropzone.js" 21 | }, 22 | "module": { 23 | "source": "src/dropzone.js" 24 | }, 25 | "standalone": { 26 | "source": "tool/dropzone-global.js", 27 | "outputFormat": "global" 28 | }, 29 | "built-test": { 30 | "source": "test/unit-tests.js", 31 | "distDir": "test/built/" 32 | } 33 | }, 34 | "maintainers": [ 35 | { 36 | "name": "Nicolas CARPi", 37 | "web": "https://www.deltablot.com" 38 | } 39 | ], 40 | "scripts": { 41 | "watch": "parcel watch", 42 | "build": "parcel build && yarn run css && cp types/dropzone.d.ts dist", 43 | "css": "yarn sass src/:dist/ --style compressed", 44 | "watch-css": "yarn sass src/:dist/ --watch --style compressed", 45 | "test": "karma start test/karma.conf.js", 46 | "test:e2e": "cypress run", 47 | "start-test-server": "yarn node test/test-server.js" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/NicolasCARPi/dropzone/issues" 51 | }, 52 | "license": "MIT", 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/NicolasCARPi/dropzone.git" 56 | }, 57 | "dependencies": { 58 | "@swc/helpers": "^0.5.17" 59 | }, 60 | "resolutions": { 61 | "body-parser": "^1.20.3" 62 | }, 63 | "devDependencies": { 64 | "@parcel/core": "^2.15.2", 65 | "@parcel/transformer-inline-string": "^2.15.2", 66 | "@parcel/transformer-sass": "^2.15.2", 67 | "chai": "^4.5.0", 68 | "cypress": "^13.15.0", 69 | "express": "^4.21.2", 70 | "karma": "^6.4.4", 71 | "karma-chrome-launcher": "^3.2.0", 72 | "karma-mocha": "^2.0.1", 73 | "karma-sinon-chai": "^2.0.2", 74 | "karma-spec-reporter": "^0.0.36", 75 | "mocha": "^10.7.3", 76 | "mocha-headless-chrome": "^4.0.0", 77 | "parcel": "^2.12.0", 78 | "sass": "^1.79.3", 79 | "sinon": "^18.0.1", 80 | "sinon-chai": "^3.7.0" 81 | }, 82 | "packageManager": "yarn@4.5.3" 83 | } 84 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.css 2 | *.map -------------------------------------------------------------------------------- /src/basic.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | .dropzone, .dropzone * { 4 | box-sizing: border-box; 5 | } 6 | .dropzone { 7 | 8 | position: relative; 9 | 10 | .dz-preview { 11 | position: relative; 12 | display: inline-block; 13 | width: 120px; 14 | margin: 0.5em; 15 | 16 | .dz-progress { 17 | display: block; 18 | height: 15px; 19 | border: 1px solid #aaa; 20 | .dz-upload { 21 | display: block; 22 | height: 100%; 23 | width: 0; 24 | background: green; 25 | } 26 | } 27 | 28 | .dz-error-message { 29 | color: red; 30 | display: none; 31 | } 32 | &.dz-error { 33 | .dz-error-message, .dz-error-mark { 34 | display: block; 35 | } 36 | } 37 | &.dz-success { 38 | .dz-success-mark { 39 | display: block; 40 | } 41 | } 42 | 43 | .dz-error-mark, .dz-success-mark { 44 | position: absolute; 45 | display: none; 46 | left: 30px; 47 | top: 30px; 48 | width: 54px; 49 | height: 58px; 50 | left: 50%; 51 | margin-left: -(math.div(54px, 2)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/dropzone.js: -------------------------------------------------------------------------------- 1 | import Emitter from "./emitter"; 2 | import defaultOptions from "./options"; 3 | 4 | export default class Dropzone extends Emitter { 5 | static initClass() { 6 | // Exposing the emitter class, mainly for tests 7 | this.prototype.Emitter = Emitter; 8 | 9 | /* 10 | This is a list of all available events you can register on a dropzone object. 11 | 12 | You can register an event handler like this: 13 | 14 | dropzone.on("dragEnter", function() { }); 15 | 16 | */ 17 | this.prototype.events = [ 18 | "drop", 19 | "dragstart", 20 | "dragend", 21 | "dragenter", 22 | "dragover", 23 | "dragleave", 24 | "addedfile", 25 | "addedfiles", 26 | "removedfile", 27 | "thumbnail", 28 | "error", 29 | "errormultiple", 30 | "processing", 31 | "processingmultiple", 32 | "uploadprogress", 33 | "totaluploadprogress", 34 | "sending", 35 | "sendingmultiple", 36 | "success", 37 | "successmultiple", 38 | "canceled", 39 | "canceledmultiple", 40 | "complete", 41 | "completemultiple", 42 | "reset", 43 | "maxfilesexceeded", 44 | "maxfilesreached", 45 | "queuecomplete", 46 | "emptyfolder", 47 | ]; 48 | 49 | this.prototype._thumbnailQueue = []; 50 | this.prototype._processingThumbnail = false; 51 | } 52 | 53 | constructor(el, options) { 54 | super(); 55 | let fallback, left; 56 | this.element = el; 57 | 58 | this.clickableElements = []; 59 | this.listeners = []; 60 | this.files = []; // All files 61 | 62 | if (typeof this.element === "string") { 63 | this.element = document.querySelector(this.element); 64 | } 65 | 66 | // make sure we actually have an HTML Element 67 | if (this.element === null || !this.element instanceof HTMLElement) { 68 | throw new Error("Invalid dropzone element: not an instance of HTMLElement."); 69 | } 70 | 71 | if (this.element.dropzone) { 72 | throw new Error("Dropzone already attached."); 73 | } 74 | 75 | // Now add this dropzone to the instances. 76 | Dropzone.instances.push(this); 77 | 78 | // Put the dropzone inside the element itself. 79 | this.element.dropzone = this; 80 | 81 | let elementOptions = 82 | (left = Dropzone.optionsForElement(this.element)) != null ? left : {}; 83 | 84 | this.options = Object.assign( 85 | {}, 86 | defaultOptions, 87 | elementOptions, 88 | options != null ? options : {} 89 | ); 90 | 91 | this.options.previewTemplate = this.options.previewTemplate.replace( 92 | /\n*/g, 93 | "" 94 | ); 95 | 96 | // If the browser failed, just call the fallback and leave 97 | if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { 98 | return this.options.fallback.call(this); 99 | } 100 | 101 | // @options.url = @element.getAttribute "action" unless @options.url? 102 | if (this.options.url == null) { 103 | this.options.url = this.element.getAttribute("action"); 104 | } 105 | 106 | if (!this.options.url) { 107 | throw new Error("No URL provided."); 108 | } 109 | 110 | if (this.options.uploadMultiple && this.options.chunking) { 111 | throw new Error("You cannot set both: uploadMultiple and chunking."); 112 | } 113 | 114 | if (this.options.binaryBody && this.options.uploadMultiple) { 115 | throw new Error("You cannot set both: binaryBody and uploadMultiple."); 116 | } 117 | 118 | if (typeof this.options.method === "string") { 119 | this.options.method = this.options.method.toUpperCase(); 120 | } 121 | 122 | if ((fallback = this.getExistingFallback()) && fallback.parentNode) { 123 | // Remove the fallback 124 | fallback.parentNode.removeChild(fallback); 125 | } 126 | 127 | // Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false 128 | if (this.options.previewsContainer !== false) { 129 | if (this.options.previewsContainer) { 130 | this.previewsContainer = Dropzone.getElement( 131 | this.options.previewsContainer, 132 | "previewsContainer" 133 | ); 134 | } else { 135 | this.previewsContainer = this.element; 136 | } 137 | } 138 | 139 | if (this.options.clickable) { 140 | if (this.options.clickable === true) { 141 | this.clickableElements = [this.element]; 142 | } else { 143 | this.clickableElements = Dropzone.getElements( 144 | this.options.clickable, 145 | "clickable" 146 | ); 147 | } 148 | } 149 | 150 | this.init(); 151 | } 152 | 153 | // Returns all files that have been accepted 154 | getAcceptedFiles() { 155 | return this.files.filter((file) => file.accepted).map((file) => file); 156 | } 157 | 158 | // Returns all files that have been rejected 159 | // Not sure when that's going to be useful, but added for completeness. 160 | getRejectedFiles() { 161 | return this.files.filter((file) => !file.accepted).map((file) => file); 162 | } 163 | 164 | getFilesWithStatus(status) { 165 | return this.files 166 | .filter((file) => file.status === status) 167 | .map((file) => file); 168 | } 169 | 170 | // Returns all files that are in the queue 171 | getQueuedFiles() { 172 | return this.getFilesWithStatus(Dropzone.QUEUED); 173 | } 174 | 175 | getUploadingFiles() { 176 | return this.getFilesWithStatus(Dropzone.UPLOADING); 177 | } 178 | 179 | getAddedFiles() { 180 | return this.getFilesWithStatus(Dropzone.ADDED); 181 | } 182 | 183 | // Files that are either queued or uploading 184 | getActiveFiles() { 185 | return this.files 186 | .filter( 187 | (file) => 188 | file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED 189 | ) 190 | .map((file) => file); 191 | } 192 | 193 | // The function that gets called when Dropzone is initialized. You 194 | // can (and should) setup event listeners inside this function. 195 | init() { 196 | // In case it isn't set already 197 | if (this.element.tagName === "form") { 198 | this.element.setAttribute("enctype", "multipart/form-data"); 199 | } 200 | 201 | if ( 202 | this.element.classList.contains("dropzone") && 203 | !this.element.querySelector(".dz-message") 204 | ) { 205 | this.element.appendChild( 206 | Dropzone.createElement( 207 | `
` 208 | ) 209 | ); 210 | } 211 | 212 | if (this.clickableElements.length) { 213 | let setupHiddenFileInput = () => { 214 | if (this.hiddenFileInput) { 215 | this.hiddenFileInput.parentNode?.removeChild(this.hiddenFileInput); 216 | } 217 | this.hiddenFileInput = document.createElement("input"); 218 | this.hiddenFileInput.setAttribute("type", "file"); 219 | this.hiddenFileInput.setAttribute("form", this.element.id); 220 | if (this.options.maxFiles === null || this.options.maxFiles > 1) { 221 | this.hiddenFileInput.setAttribute("multiple", "multiple"); 222 | } 223 | this.hiddenFileInput.className = "dz-hidden-input"; 224 | 225 | if (this.options.acceptedFiles !== null) { 226 | this.hiddenFileInput.setAttribute( 227 | "accept", 228 | this.options.acceptedFiles 229 | ); 230 | } 231 | if (this.options.capture !== null) { 232 | this.hiddenFileInput.setAttribute("capture", this.options.capture); 233 | } 234 | 235 | // Making sure that no one can "tab" into this field. 236 | this.hiddenFileInput.setAttribute("tabindex", "-1"); 237 | // Add arialabel for a11y 238 | this.hiddenFileInput.setAttribute("aria-label", "dropzone hidden input"); 239 | 240 | // Not setting `display="none"` because some browsers don't accept clicks 241 | // on elements that aren't displayed. 242 | this.hiddenFileInput.style.visibility = "hidden"; 243 | this.hiddenFileInput.style.position = "absolute"; 244 | this.hiddenFileInput.style.top = "0"; 245 | this.hiddenFileInput.style.left = "0"; 246 | this.hiddenFileInput.style.height = "0"; 247 | this.hiddenFileInput.style.width = "0"; 248 | Dropzone.getElement( 249 | this.options.hiddenInputContainer, 250 | "hiddenInputContainer" 251 | ).appendChild(this.hiddenFileInput); 252 | this.hiddenFileInput.addEventListener("change", () => { 253 | let { files } = this.hiddenFileInput; 254 | if (files.length) { 255 | for (let file of files) { 256 | this.addFile(file); 257 | } 258 | } 259 | this.emit("addedfiles", files); 260 | setupHiddenFileInput(); 261 | }); 262 | }; 263 | setupHiddenFileInput(); 264 | } 265 | 266 | this.URL = window.URL !== null ? window.URL : window.webkitURL; 267 | 268 | // Setup all event listeners on the Dropzone object itself. 269 | // They're not in @setupEventListeners() because they shouldn't be removed 270 | // again when the dropzone gets disabled. 271 | for (let eventName of this.events) { 272 | this.on(eventName, this.options[eventName]); 273 | } 274 | 275 | this.on("uploadprogress", () => this.updateTotalUploadProgress()); 276 | 277 | this.on("removedfile", () => this.updateTotalUploadProgress()); 278 | 279 | this.on("canceled", (file) => this.emit("complete", file)); 280 | 281 | // Emit a `queuecomplete` event if all files finished uploading. 282 | this.on("complete", (file) => { 283 | if ( 284 | this.getAddedFiles().length === 0 && 285 | this.getUploadingFiles().length === 0 && 286 | this.getQueuedFiles().length === 0 287 | ) { 288 | // This needs to be deferred so that `queuecomplete` really triggers after `complete` 289 | return setTimeout(() => this.emit("queuecomplete"), 0); 290 | } 291 | }); 292 | 293 | const containsFiles = function (e) { 294 | return e.dataTransfer.types && e.dataTransfer.types.includes("Files"); 295 | }; 296 | 297 | let noPropagation = function (e) { 298 | // If there are no files, we don't want to stop 299 | // propagation so we don't interfere with other 300 | // drag and drop behaviour. 301 | if (!containsFiles(e)) return; 302 | e.stopPropagation(); 303 | return e.preventDefault(); 304 | }; 305 | 306 | // Create the listeners 307 | this.listeners = [ 308 | { 309 | element: this.element, 310 | events: { 311 | dragstart: (e) => { 312 | return this.emit("dragstart", e); 313 | }, 314 | dragenter: (e) => { 315 | noPropagation(e); 316 | return this.emit("dragenter", e); 317 | }, 318 | dragover: (e) => { 319 | // Makes it possible to drag files from chrome's download bar 320 | // http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar 321 | const efct = e.dataTransfer.effectAllowed; 322 | e.dataTransfer.dropEffect = 323 | "move" === efct || "linkMove" === efct ? "move" : "copy"; 324 | 325 | noPropagation(e); 326 | return this.emit("dragover", e); 327 | }, 328 | dragleave: (e) => { 329 | return this.emit("dragleave", e); 330 | }, 331 | drop: (e) => { 332 | noPropagation(e); 333 | return this.drop(e); 334 | }, 335 | dragend: (e) => { 336 | return this.emit("dragend", e); 337 | }, 338 | }, 339 | 340 | // This is disabled right now, because the browsers don't implement it properly. 341 | // "paste": (e) => 342 | // noPropagation e 343 | // @paste e 344 | }, 345 | ]; 346 | 347 | this.clickableElements.forEach((clickableElement) => { 348 | return this.listeners.push({ 349 | element: clickableElement, 350 | events: { 351 | click: (evt) => { 352 | // Only the actual dropzone or the message element should trigger file selection 353 | if ( 354 | clickableElement !== this.element || 355 | evt.target === this.element || 356 | Dropzone.elementInside( 357 | evt.target, 358 | this.element.querySelector(".dz-message") 359 | ) 360 | ) { 361 | this.hiddenFileInput.click(); // Forward the click 362 | } 363 | return true; 364 | }, 365 | }, 366 | }); 367 | }); 368 | 369 | this.enable(); 370 | 371 | return this.options.init.call(this); 372 | } 373 | 374 | // Not fully tested yet 375 | destroy() { 376 | this.disable(); 377 | this.removeAllFiles(true); 378 | if ( 379 | this.hiddenFileInput != null ? this.hiddenFileInput.parentNode : undefined 380 | ) { 381 | this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); 382 | this.hiddenFileInput = null; 383 | } 384 | delete this.element.dropzone; 385 | return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); 386 | } 387 | 388 | updateTotalUploadProgress() { 389 | let totalUploadProgress; 390 | let totalBytesSent = 0; 391 | let totalBytes = 0; 392 | 393 | let activeFiles = this.getActiveFiles(); 394 | 395 | if (activeFiles.length) { 396 | for (let file of this.getActiveFiles()) { 397 | totalBytesSent += file.upload.bytesSent; 398 | totalBytes += file.upload.total; 399 | } 400 | totalUploadProgress = (100 * totalBytesSent) / totalBytes; 401 | } else { 402 | totalUploadProgress = 100; 403 | } 404 | 405 | return this.emit( 406 | "totaluploadprogress", 407 | totalUploadProgress, 408 | totalBytes, 409 | totalBytesSent 410 | ); 411 | } 412 | 413 | // @options.paramName can be a function taking one parameter rather than a string. 414 | // A parameter name for a file is obtained simply by calling this with an index number. 415 | _getParamName(n) { 416 | if (typeof this.options.paramName === "function") { 417 | return this.options.paramName(n); 418 | } else { 419 | return `${this.options.paramName}${ 420 | this.options.uploadMultiple ? `[${n}]` : "" 421 | }`; 422 | } 423 | } 424 | 425 | // If @options.renameFile is a function, 426 | // the function will be used to rename the file.name before appending it to the formData. 427 | // MacOS 14+ screenshots contain narrow non-breaking space (U+202F) characters in filenames 428 | // (e.g., "Screenshot 2024-01-30 at 10.32.07 AM.png" where the space after "07" and before "AM" is U+202F). 429 | // This function now replaces these with regular spaces to prevent upload issues and maintain compatibility with MacOS 430 | _renameFile(file) { 431 | const cleanFile = { 432 | ...file, 433 | name: file.name.replace(/\u202F/g, ' ') 434 | }; 435 | 436 | if (typeof this.options.renameFile !== "function") { 437 | return cleanFile.name; 438 | } 439 | return this.options.renameFile(cleanFile); 440 | } 441 | 442 | // Returns a form that can be used as fallback if the browser does not support DragnDrop 443 | // 444 | // If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided. 445 | // This code has to pass in IE7 :( 446 | getFallbackForm() { 447 | let existingFallback, form; 448 | if ((existingFallback = this.getExistingFallback())) { 449 | return existingFallback; 450 | } 451 | 452 | let fieldsString = '
'; 453 | if (this.options.dictFallbackText) { 454 | fieldsString += `

${this.options.dictFallbackText}

`; 455 | } 456 | fieldsString += `
`; 459 | 460 | let fields = Dropzone.createElement(fieldsString); 461 | if (this.element.tagName !== "FORM") { 462 | form = Dropzone.createElement( 463 | `
` 464 | ); 465 | form.appendChild(fields); 466 | } else { 467 | // Make sure that the enctype and method attributes are set properly 468 | this.element.setAttribute("enctype", "multipart/form-data"); 469 | this.element.setAttribute("method", this.options.method); 470 | } 471 | return form != null ? form : fields; 472 | } 473 | 474 | // Returns the fallback elements if they exist already 475 | // 476 | // This code has to pass in IE7 :( 477 | getExistingFallback() { 478 | let getFallback = function (elements) { 479 | for (let el of elements) { 480 | if (/(^| )fallback($| )/.test(el.className)) { 481 | return el; 482 | } 483 | } 484 | }; 485 | 486 | for (let tagName of ["div", "form"]) { 487 | var fallback; 488 | if ( 489 | (fallback = getFallback(this.element.getElementsByTagName(tagName))) 490 | ) { 491 | return fallback; 492 | } 493 | } 494 | } 495 | 496 | // Activates all listeners stored in @listeners 497 | setupEventListeners() { 498 | return this.listeners.map((elementListeners) => 499 | (() => { 500 | let result = []; 501 | for (let event in elementListeners.events) { 502 | let listener = elementListeners.events[event]; 503 | result.push( 504 | elementListeners.element.addEventListener(event, listener, false) 505 | ); 506 | } 507 | return result; 508 | })() 509 | ); 510 | } 511 | 512 | // Deactivates all listeners stored in @listeners 513 | removeEventListeners() { 514 | return this.listeners.map((elementListeners) => 515 | (() => { 516 | let result = []; 517 | for (let event in elementListeners.events) { 518 | let listener = elementListeners.events[event]; 519 | result.push( 520 | elementListeners.element.removeEventListener(event, listener, false) 521 | ); 522 | } 523 | return result; 524 | })() 525 | ); 526 | } 527 | 528 | // Removes all event listeners and cancels all files in the queue or being processed. 529 | disable() { 530 | this.clickableElements.forEach((element) => 531 | element.classList.remove("dz-clickable") 532 | ); 533 | this.removeEventListeners(); 534 | this.disabled = true; 535 | 536 | return this.files.map((file) => this.cancelUpload(file)); 537 | } 538 | 539 | enable() { 540 | delete this.disabled; 541 | this.clickableElements.forEach((element) => 542 | element.classList.add("dz-clickable") 543 | ); 544 | return this.setupEventListeners(); 545 | } 546 | 547 | // Returns a nicely formatted filesize 548 | filesize(size) { 549 | let selectedSize = 0; 550 | let selectedUnit = "b"; 551 | 552 | if (size > 0) { 553 | let units = ["tb", "gb", "mb", "kb", "b"]; 554 | 555 | for (let i = 0; i < units.length; i++) { 556 | let unit = units[i]; 557 | let cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10; 558 | 559 | if (size >= cutoff) { 560 | selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i); 561 | selectedUnit = unit; 562 | break; 563 | } 564 | } 565 | 566 | selectedSize = Math.round(10 * selectedSize) / 10; // Cutting of digits 567 | } 568 | 569 | return `${selectedSize} ${this.options.dictFileSizeUnits[selectedUnit]}`; 570 | } 571 | 572 | // Adds or removes the `dz-max-files-reached` class from the form. 573 | _updateMaxFilesReachedClass() { 574 | if ( 575 | this.options.maxFiles != null && 576 | this.getAcceptedFiles().length >= this.options.maxFiles 577 | ) { 578 | if (this.getAcceptedFiles().length === this.options.maxFiles) { 579 | this.emit("maxfilesreached", this.files); 580 | } 581 | return this.element.classList.add("dz-max-files-reached"); 582 | } else { 583 | return this.element.classList.remove("dz-max-files-reached"); 584 | } 585 | } 586 | 587 | drop(e) { 588 | if (!e.dataTransfer) { 589 | return; 590 | } 591 | this.emit("drop", e); 592 | 593 | // Convert the FileList to an Array 594 | // This is necessary for IE11 595 | let files = []; 596 | for (let i = 0; i < e.dataTransfer.files.length; i++) { 597 | files[i] = e.dataTransfer.files[i]; 598 | } 599 | 600 | // Even if it's a folder, files.length will contain the folders. 601 | if (files.length) { 602 | let { items } = e.dataTransfer; 603 | if (items && items.length && items[0].webkitGetAsEntry != null) { 604 | // The browser supports dropping of folders, so handle items instead of files 605 | this._addFilesFromItems(items); 606 | } else { 607 | this.handleFiles(files); 608 | } 609 | } 610 | 611 | this.emit("addedfiles", files); 612 | } 613 | 614 | paste(e) { 615 | if ( 616 | __guard__(e != null ? e.clipboardData : undefined, (x) => x.items) == null 617 | ) { 618 | return; 619 | } 620 | 621 | this.emit("paste", e); 622 | let { items } = e.clipboardData; 623 | 624 | if (items.length) { 625 | return this._addFilesFromItems(items); 626 | } 627 | } 628 | 629 | handleFiles(files) { 630 | for (let file of files) { 631 | this.addFile(file); 632 | } 633 | } 634 | 635 | // When a folder is dropped (or files are pasted), items must be handled 636 | // instead of files. 637 | _addFilesFromItems(items) { 638 | return (() => { 639 | let result = []; 640 | for (let item of items) { 641 | var entry; 642 | if ( 643 | item.webkitGetAsEntry != null && 644 | (entry = item.webkitGetAsEntry()) 645 | ) { 646 | if (entry.isFile) { 647 | result.push(this.addFile(item.getAsFile())); 648 | } else if (entry.isDirectory) { 649 | // Append all files from that directory to files 650 | result.push(this._addFilesFromDirectory(entry, entry.name)); 651 | } else { 652 | result.push(undefined); 653 | } 654 | } else if (item.getAsFile != null) { 655 | if (item.kind == null || item.kind === "file") { 656 | result.push(this.addFile(item.getAsFile())); 657 | } else { 658 | result.push(undefined); 659 | } 660 | } else { 661 | result.push(undefined); 662 | } 663 | } 664 | return result; 665 | })(); 666 | } 667 | 668 | // Goes through the directory, and adds each file it finds recursively 669 | _addFilesFromDirectory(directory, path) { 670 | let dirReader = directory.createReader(); 671 | 672 | let errorHandler = (error) => 673 | __guardMethod__(console, "log", (o) => o.log(error)); 674 | 675 | let entryCount = 0; 676 | var readEntries = () => { 677 | return dirReader.readEntries((entries) => { 678 | if (entries.length > 0) { 679 | for (let entry of entries) { 680 | if (entry.isFile) { 681 | ++entryCount; 682 | entry.file((file) => { 683 | if ( 684 | this.options.ignoreHiddenFiles && 685 | file.name.substring(0, 1) === "." 686 | ) { 687 | return; 688 | } 689 | file.fullPath = `${path}/${file.name}`; 690 | return this.addFile(file); 691 | }); 692 | } else if (entry.isDirectory) { 693 | this._addFilesFromDirectory(entry, `${path}/${entry.name}`); 694 | } 695 | } 696 | 697 | // Recursively call readEntries() again, since browser only handle 698 | // the first 100 entries. 699 | // See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries 700 | readEntries(); 701 | } else if (entryCount === 0) { 702 | this.emit("emptyfolder", path); 703 | } 704 | return null; 705 | }, errorHandler); 706 | }; 707 | 708 | return readEntries(); 709 | } 710 | 711 | // If `done()` is called without argument the file is accepted 712 | // If you call it with an error message, the file is rejected 713 | // (This allows for asynchronous validation) 714 | // 715 | // This function checks the filesize, and if the file.type passes the 716 | // `acceptedFiles` check. 717 | accept(file, done) { 718 | if ( 719 | this.options.maxFilesize && 720 | file.size > this.options.maxFilesize * 1024 * 1024 721 | ) { 722 | done( 723 | this.options.dictFileTooBig 724 | .replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100) 725 | .replace("{{maxFilesize}}", this.options.maxFilesize) 726 | ); 727 | } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) { 728 | done(this.options.dictInvalidFileType); 729 | } else if ( 730 | this.options.maxFiles != null && 731 | this.getAcceptedFiles().length >= this.options.maxFiles 732 | ) { 733 | done( 734 | this.options.dictMaxFilesExceeded.replace( 735 | "{{maxFiles}}", 736 | this.options.maxFiles 737 | ) 738 | ); 739 | this.emit("maxfilesexceeded", file); 740 | } else { 741 | this.options.accept.call(this, file, done); 742 | } 743 | } 744 | 745 | addFile(file) { 746 | file.upload = { 747 | // note: this only works if window.isSecureContext is true, which includes localhost in http 748 | uuid: window.isSecureContext ? self.crypto.randomUUID() : Dropzone.uuidv4(), 749 | progress: 0, 750 | // Setting the total upload size to file.size for the beginning 751 | // It's actual different than the size to be transmitted. 752 | total: file.size, 753 | bytesSent: 0, 754 | filename: this._renameFile(file), 755 | // Not setting chunking information here, because the actual data — and 756 | // thus the chunks — might change if `options.transformFile` is set 757 | // and does something to the data. 758 | }; 759 | this.files.push(file); 760 | 761 | file.status = Dropzone.ADDED; 762 | 763 | this.emit("addedfile", file); 764 | 765 | this._enqueueThumbnail(file); 766 | 767 | this.accept(file, (error) => { 768 | if (error) { 769 | file.accepted = false; 770 | this._errorProcessing([file], error); // Will set the file.status 771 | } else { 772 | file.accepted = true; 773 | if (this.options.autoQueue) { 774 | this.enqueueFile(file); 775 | } // Will set .accepted = true 776 | } 777 | this._updateMaxFilesReachedClass(); 778 | }); 779 | } 780 | 781 | // Wrapper for enqueueFile 782 | enqueueFiles(files) { 783 | for (let file of files) { 784 | this.enqueueFile(file); 785 | } 786 | return null; 787 | } 788 | 789 | enqueueFile(file) { 790 | if (file.status === Dropzone.ADDED && file.accepted === true) { 791 | file.status = Dropzone.QUEUED; 792 | if (this.options.autoProcessQueue) { 793 | return setTimeout(() => this.processQueue(), 0); // Deferring the call 794 | } 795 | } else { 796 | throw new Error( 797 | "This file can't be queued because it has already been processed or was rejected." 798 | ); 799 | } 800 | } 801 | 802 | _enqueueThumbnail(file) { 803 | if ( 804 | this.options.createImageThumbnails && 805 | file.type.match(/image.*/) && 806 | file.size <= this.options.maxThumbnailFilesize * 1024 * 1024 807 | ) { 808 | this._thumbnailQueue.push(file); 809 | return setTimeout(() => this._processThumbnailQueue(), 0); // Deferring the call 810 | } 811 | } 812 | 813 | _processThumbnailQueue() { 814 | if (this._processingThumbnail || this._thumbnailQueue.length === 0) { 815 | return; 816 | } 817 | 818 | this._processingThumbnail = true; 819 | let file = this._thumbnailQueue.shift(); 820 | return this.createThumbnail( 821 | file, 822 | this.options.thumbnailWidth, 823 | this.options.thumbnailHeight, 824 | this.options.thumbnailMethod, 825 | true, 826 | (dataUrl) => { 827 | this.emit("thumbnail", file, dataUrl); 828 | this._processingThumbnail = false; 829 | return this._processThumbnailQueue(); 830 | } 831 | ); 832 | } 833 | 834 | // Can be called by the user to remove a file 835 | removeFile(file) { 836 | if (file.status === Dropzone.UPLOADING) { 837 | this.cancelUpload(file); 838 | } 839 | this.files = without(this.files, file); 840 | 841 | this.emit("removedfile", file); 842 | if (this.files.length === 0) { 843 | return this.emit("reset"); 844 | } 845 | } 846 | 847 | // Removes all files that aren't currently processed from the list 848 | removeAllFiles(cancelIfNecessary) { 849 | // Create a copy of files since removeFile() changes the @files array. 850 | if (cancelIfNecessary == null) { 851 | cancelIfNecessary = false; 852 | } 853 | for (let file of this.files.slice()) { 854 | if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) { 855 | this.removeFile(file); 856 | } 857 | } 858 | return null; 859 | } 860 | 861 | // Resizes an image before it gets sent to the server. This function is the default behavior of 862 | // `options.transformFile` if `resizeWidth` or `resizeHeight` are set. The callback is invoked with 863 | // the resized blob. 864 | resizeImage(file, width, height, resizeMethod, callback) { 865 | return this.createThumbnail( 866 | file, 867 | width, 868 | height, 869 | resizeMethod, 870 | true, 871 | (dataUrl, canvas) => { 872 | if (canvas == null) { 873 | // The image has not been resized 874 | return callback(file); 875 | } else { 876 | let { resizeMimeType } = this.options; 877 | if (resizeMimeType == null) { 878 | resizeMimeType = file.type; 879 | } 880 | let resizedDataURL = canvas.toDataURL( 881 | resizeMimeType, 882 | this.options.resizeQuality 883 | ); 884 | if ( 885 | resizeMimeType === "image/jpeg" || 886 | resizeMimeType === "image/jpg" 887 | ) { 888 | // Now add the original EXIF information 889 | resizedDataURL = restoreExif(file.dataURL, resizedDataURL); 890 | } 891 | return callback(Dropzone.dataURItoBlob(resizedDataURL)); 892 | } 893 | }, 894 | true 895 | ); 896 | } 897 | 898 | createThumbnail(file, width, height, resizeMethod, fixOrientation, callback, ignoreExif = false) { 899 | let fileReader = new FileReader(); 900 | 901 | fileReader.onload = () => { 902 | file.dataURL = fileReader.result; 903 | 904 | // Don't bother creating a thumbnail for SVG images since they're vector 905 | if (file.type === "image/svg+xml") { 906 | if (callback != null) { 907 | callback(fileReader.result); 908 | } 909 | return; 910 | } 911 | 912 | this.createThumbnailFromUrl( 913 | file, 914 | width, 915 | height, 916 | resizeMethod, 917 | fixOrientation, 918 | callback, 919 | undefined, 920 | ignoreExif, 921 | ); 922 | }; 923 | 924 | fileReader.readAsDataURL(file); 925 | } 926 | 927 | // `mockFile` needs to have these attributes: 928 | // 929 | // { name: 'name', size: 12345, imageUrl: '' } 930 | // 931 | // `callback` will be invoked when the image has been downloaded and displayed. 932 | // `crossOrigin` will be added to the `img` tag when accessing the file. 933 | displayExistingFile( 934 | mockFile, 935 | imageUrl, 936 | callback, 937 | crossOrigin, 938 | resizeThumbnail = true 939 | ) { 940 | this.emit("addedfile", mockFile); 941 | this.emit("complete", mockFile); 942 | 943 | if (!resizeThumbnail) { 944 | this.emit("thumbnail", mockFile, imageUrl); 945 | if (callback) callback(); 946 | } else { 947 | let onDone = (thumbnail) => { 948 | this.emit("thumbnail", mockFile, thumbnail); 949 | if (callback) callback(); 950 | }; 951 | mockFile.dataURL = imageUrl; 952 | 953 | this.createThumbnailFromUrl( 954 | mockFile, 955 | this.options.thumbnailWidth, 956 | this.options.thumbnailHeight, 957 | this.options.thumbnailMethod, 958 | this.options.fixOrientation, 959 | onDone, 960 | crossOrigin 961 | ); 962 | } 963 | } 964 | 965 | createThumbnailFromUrl( 966 | file, 967 | width, 968 | height, 969 | resizeMethod, 970 | fixOrientation, 971 | callback, 972 | crossOrigin, 973 | ignoreExif = false, 974 | ) { 975 | // Not using `new Image` here because of a bug in latest Chrome versions. 976 | // See https://github.com/enyo/dropzone/pull/226 977 | let img = document.createElement("img"); 978 | 979 | if (crossOrigin) { 980 | img.crossOrigin = crossOrigin; 981 | } 982 | 983 | // fixOrientation is not needed anymore with browsers handling imageOrientation 984 | fixOrientation = 985 | getComputedStyle(document.body)["imageOrientation"] == "from-image" 986 | ? false 987 | : fixOrientation; 988 | 989 | img.onload = () => { 990 | let loadExif = (callback) => callback(1); 991 | if (typeof EXIF !== "undefined" && EXIF !== null && fixOrientation) { 992 | loadExif = (callback) => 993 | EXIF.getData(img, function () { 994 | return callback(EXIF.getTag(this, "Orientation")); 995 | }); 996 | } 997 | 998 | return loadExif((orientation) => { 999 | file.width = img.width; 1000 | file.height = img.height; 1001 | 1002 | let resizeInfo = this.options.resize.call( 1003 | this, 1004 | file, 1005 | width, 1006 | height, 1007 | resizeMethod 1008 | ); 1009 | 1010 | let canvas = document.createElement("canvas"); 1011 | let ctx = canvas.getContext("2d"); 1012 | 1013 | canvas.width = resizeInfo.trgWidth; 1014 | canvas.height = resizeInfo.trgHeight; 1015 | 1016 | if (orientation > 4) { 1017 | canvas.width = resizeInfo.trgHeight; 1018 | canvas.height = resizeInfo.trgWidth; 1019 | } 1020 | 1021 | switch (orientation) { 1022 | case 2: 1023 | // horizontal flip 1024 | ctx.translate(canvas.width, 0); 1025 | ctx.scale(-1, 1); 1026 | break; 1027 | case 3: 1028 | // 180° rotate left 1029 | ctx.translate(canvas.width, canvas.height); 1030 | ctx.rotate(Math.PI); 1031 | break; 1032 | case 4: 1033 | // vertical flip 1034 | ctx.translate(0, canvas.height); 1035 | ctx.scale(1, -1); 1036 | break; 1037 | case 5: 1038 | // vertical flip + 90 rotate right 1039 | ctx.rotate(0.5 * Math.PI); 1040 | ctx.scale(1, -1); 1041 | break; 1042 | case 6: 1043 | // 90° rotate right 1044 | ctx.rotate(0.5 * Math.PI); 1045 | ctx.translate(0, -canvas.width); 1046 | break; 1047 | case 7: 1048 | // horizontal flip + 90 rotate right 1049 | ctx.rotate(0.5 * Math.PI); 1050 | ctx.translate(canvas.height, -canvas.width); 1051 | ctx.scale(-1, 1); 1052 | break; 1053 | case 8: 1054 | // 90° rotate left 1055 | ctx.rotate(-0.5 * Math.PI); 1056 | ctx.translate(-canvas.height, 0); 1057 | break; 1058 | } 1059 | 1060 | // This is a bugfix for iOS' scaling bug. 1061 | drawImageIOSFix( 1062 | ctx, 1063 | img, 1064 | resizeInfo.srcX != null ? resizeInfo.srcX : 0, 1065 | resizeInfo.srcY != null ? resizeInfo.srcY : 0, 1066 | resizeInfo.srcWidth, 1067 | resizeInfo.srcHeight, 1068 | resizeInfo.trgX != null ? resizeInfo.trgX : 0, 1069 | resizeInfo.trgY != null ? resizeInfo.trgY : 0, 1070 | resizeInfo.trgWidth, 1071 | resizeInfo.trgHeight 1072 | ); 1073 | 1074 | let thumbnail = canvas.toDataURL("image/png"); 1075 | 1076 | if (callback != null) { 1077 | return callback(thumbnail, canvas); 1078 | } 1079 | }); 1080 | }; 1081 | 1082 | if (callback != null) { 1083 | img.onerror = callback; 1084 | } 1085 | 1086 | var dataURL = file.dataURL; 1087 | if (ignoreExif) { 1088 | dataURL = removeExif(dataURL); 1089 | } 1090 | 1091 | return (img.src = dataURL); 1092 | } 1093 | 1094 | // Goes through the queue and processes files if there aren't too many already. 1095 | processQueue() { 1096 | let { parallelUploads } = this.options; 1097 | let processingLength = this.getUploadingFiles().length; 1098 | let i = processingLength; 1099 | 1100 | // There are already at least as many files uploading than should be 1101 | if (processingLength >= parallelUploads) { 1102 | return; 1103 | } 1104 | 1105 | let queuedFiles = this.getQueuedFiles(); 1106 | 1107 | if (!(queuedFiles.length > 0)) { 1108 | return; 1109 | } 1110 | 1111 | if (this.options.uploadMultiple) { 1112 | // The files should be uploaded in one request 1113 | return this.processFiles( 1114 | queuedFiles.slice(0, parallelUploads - processingLength) 1115 | ); 1116 | } else { 1117 | while (i < parallelUploads) { 1118 | if (!queuedFiles.length) { 1119 | return; 1120 | } // Nothing left to process 1121 | this.processFile(queuedFiles.shift()); 1122 | i++; 1123 | } 1124 | } 1125 | } 1126 | 1127 | // Wrapper for `processFiles` 1128 | processFile(file) { 1129 | return this.processFiles([file]); 1130 | } 1131 | 1132 | // Loads the file, then calls finishedLoading() 1133 | processFiles(files) { 1134 | for (let file of files) { 1135 | file.processing = true; // Backwards compatibility 1136 | file.status = Dropzone.UPLOADING; 1137 | 1138 | this.emit("processing", file); 1139 | } 1140 | 1141 | if (this.options.uploadMultiple) { 1142 | this.emit("processingmultiple", files); 1143 | } 1144 | 1145 | return this.uploadFiles(files); 1146 | } 1147 | 1148 | _getFilesWithXhr(xhr) { 1149 | let files; 1150 | return (files = this.files 1151 | .filter((file) => file.xhr === xhr) 1152 | .map((file) => file)); 1153 | } 1154 | 1155 | // Cancels the file upload and sets the status to CANCELED 1156 | // **if** the file is actually being uploaded. 1157 | // If it's still in the queue, the file is being removed from it and the status 1158 | // set to CANCELED. 1159 | cancelUpload(file) { 1160 | if (file.status === Dropzone.UPLOADING) { 1161 | let groupedFiles = this._getFilesWithXhr(file.xhr); 1162 | for (let groupedFile of groupedFiles) { 1163 | groupedFile.status = Dropzone.CANCELED; 1164 | } 1165 | if (typeof file.xhr !== "undefined") { 1166 | file.xhr.abort(); 1167 | } 1168 | for (let groupedFile of groupedFiles) { 1169 | this.emit("canceled", groupedFile); 1170 | } 1171 | if (this.options.uploadMultiple) { 1172 | this.emit("canceledmultiple", groupedFiles); 1173 | } 1174 | } else if ( 1175 | file.status === Dropzone.ADDED || 1176 | file.status === Dropzone.QUEUED 1177 | ) { 1178 | file.status = Dropzone.CANCELED; 1179 | this.emit("canceled", file); 1180 | if (this.options.uploadMultiple) { 1181 | this.emit("canceledmultiple", [file]); 1182 | } 1183 | } 1184 | 1185 | if (this.options.autoProcessQueue) { 1186 | return this.processQueue(); 1187 | } 1188 | } 1189 | 1190 | resolveOption(option, ...args) { 1191 | if (typeof option === "function") { 1192 | return option.apply(this, args); 1193 | } 1194 | return option; 1195 | } 1196 | 1197 | uploadFile(file) { 1198 | return this.uploadFiles([file]); 1199 | } 1200 | 1201 | uploadFiles(files) { 1202 | this._transformFiles(files, (transformedFiles) => { 1203 | if (this.options.chunking) { 1204 | // Chunking is not allowed to be used with `uploadMultiple` so we know 1205 | // that there is only __one__file. 1206 | let transformedFile = transformedFiles[0]; 1207 | files[0].upload.chunked = 1208 | this.options.chunking && 1209 | (this.options.forceChunking || 1210 | transformedFile.size > this.options.chunkSize); 1211 | files[0].upload.totalChunkCount = Math.ceil( 1212 | transformedFile.size / this.options.chunkSize 1213 | ); 1214 | if (transformedFile.size === 0) { 1215 | files[0].upload.totalChunkCount = 1; 1216 | } 1217 | } 1218 | 1219 | if (files[0].upload.chunked) { 1220 | // This file should be sent in chunks! 1221 | 1222 | // If the chunking option is set, we **know** that there can only be **one** file, since 1223 | // uploadMultiple is not allowed with this option. 1224 | let file = files[0]; 1225 | let transformedFile = transformedFiles[0]; 1226 | 1227 | file.upload.chunks = []; 1228 | 1229 | let handleNextChunk = () => { 1230 | let chunkIndex = 0; 1231 | 1232 | // Find the next item in file.upload.chunks that is not defined yet. 1233 | while (file.upload.chunks[chunkIndex] !== undefined) { 1234 | chunkIndex++; 1235 | } 1236 | 1237 | // This means, that all chunks have already been started. 1238 | if (chunkIndex >= file.upload.totalChunkCount) return; 1239 | 1240 | let start = chunkIndex * this.options.chunkSize; 1241 | let end = Math.min( 1242 | start + this.options.chunkSize, 1243 | transformedFile.size 1244 | ); 1245 | 1246 | let dataBlock = { 1247 | name: this._getParamName(0), 1248 | data: transformedFile.webkitSlice 1249 | ? transformedFile.webkitSlice(start, end) 1250 | : transformedFile.slice(start, end), 1251 | filename: file.upload.filename, 1252 | chunkIndex: chunkIndex, 1253 | }; 1254 | 1255 | file.upload.chunks[chunkIndex] = { 1256 | file: file, 1257 | index: chunkIndex, 1258 | dataBlock: dataBlock, // In case we want to retry. 1259 | status: Dropzone.UPLOADING, 1260 | progress: 0, 1261 | retries: 0, // The number of times this block has been retried. 1262 | }; 1263 | 1264 | this._uploadData(files, [dataBlock]); 1265 | }; 1266 | 1267 | file.upload.finishedChunkUpload = (chunk, response) => { 1268 | let allFinished = true; 1269 | chunk.status = Dropzone.SUCCESS; 1270 | 1271 | // Clear the data from the chunk 1272 | chunk.dataBlock = null; 1273 | chunk.response = chunk.xhr.responseText; 1274 | chunk.responseHeaders = chunk.xhr.getAllResponseHeaders(); 1275 | // Leaving this reference to xhr will cause memory leaks. 1276 | chunk.xhr = null; 1277 | 1278 | for (let i = 0; i < file.upload.totalChunkCount; i++) { 1279 | if (file.upload.chunks[i] === undefined) { 1280 | return handleNextChunk(); 1281 | } 1282 | if (file.upload.chunks[i].status !== Dropzone.SUCCESS) { 1283 | allFinished = false; 1284 | } 1285 | } 1286 | 1287 | if (allFinished) { 1288 | this.options.chunksUploaded(file, () => { 1289 | this._finished(files, response, null); 1290 | }); 1291 | } 1292 | }; 1293 | 1294 | if (this.options.parallelChunkUploads) { 1295 | // we want to limit parallelChunkUploads to the same value as parallelUploads option 1296 | const parallelCount = Math.min( 1297 | this.options.parallelChunkUploads === true ? this.options.parallelUploads : this.options.parallelChunkUploads, 1298 | file.upload.totalChunkCount 1299 | ); 1300 | for (let i = 0; i < parallelCount; i++) { 1301 | handleNextChunk(); 1302 | } 1303 | } else { 1304 | handleNextChunk(); 1305 | } 1306 | } else { 1307 | let dataBlocks = []; 1308 | for (let i = 0; i < files.length; i++) { 1309 | dataBlocks[i] = { 1310 | name: this._getParamName(i), 1311 | data: transformedFiles[i], 1312 | filename: files[i].upload.filename, 1313 | }; 1314 | } 1315 | this._uploadData(files, dataBlocks); 1316 | } 1317 | }); 1318 | } 1319 | 1320 | /// Returns the right chunk for given file and xhr 1321 | _getChunk(file, xhr) { 1322 | for (let i = 0; i < file.upload.totalChunkCount; i++) { 1323 | if ( 1324 | file.upload.chunks[i] !== undefined && 1325 | file.upload.chunks[i].xhr === xhr 1326 | ) { 1327 | return file.upload.chunks[i]; 1328 | } 1329 | } 1330 | } 1331 | 1332 | // This function actually uploads the file(s) to the server. 1333 | // 1334 | // If dataBlocks contains the actual data to upload (meaning, that this could 1335 | // either be transformed files, or individual chunks for chunked upload) then 1336 | // they will be used for the actual data to upload. 1337 | _uploadData(files, dataBlocks) { 1338 | let xhr = new XMLHttpRequest(); 1339 | 1340 | // Put the xhr object in the file objects to be able to reference it later. 1341 | for (let file of files) { 1342 | file.xhr = xhr; 1343 | } 1344 | if (files[0].upload.chunked) { 1345 | // Put the xhr object in the right chunk object, so it can be associated 1346 | // later, and found with _getChunk. 1347 | files[0].upload.chunks[dataBlocks[0].chunkIndex].xhr = xhr; 1348 | } 1349 | 1350 | let method = this.resolveOption(this.options.method, files, dataBlocks); 1351 | let url = this.resolveOption(this.options.url, files, dataBlocks); 1352 | xhr.open(method, url, true); 1353 | 1354 | // Setting the timeout after open because of IE11 issue: https://gitlab.com/meno/dropzone/issues/8 1355 | let timeout = this.resolveOption(this.options.timeout, files); 1356 | if (timeout) xhr.timeout = this.resolveOption(this.options.timeout, files); 1357 | 1358 | // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179 1359 | xhr.withCredentials = !!this.options.withCredentials; 1360 | 1361 | xhr.onload = (e) => { 1362 | this._finishedUploading(files, xhr, e); 1363 | }; 1364 | 1365 | xhr.ontimeout = () => { 1366 | this._handleUploadError( 1367 | files, 1368 | xhr, 1369 | `Request timedout after ${this.options.timeout / 1000} seconds` 1370 | ); 1371 | }; 1372 | 1373 | xhr.onerror = () => { 1374 | this._handleUploadError(files, xhr); 1375 | }; 1376 | 1377 | // Some browsers do not have the .upload property 1378 | let progressObj = xhr.upload != null ? xhr.upload : xhr; 1379 | progressObj.onprogress = (e) => 1380 | this._updateFilesUploadProgress(files, xhr, e); 1381 | 1382 | let headers = this.options.defaultHeaders 1383 | ? { 1384 | Accept: "application/json", 1385 | "Cache-Control": "no-cache", 1386 | "X-Requested-With": "XMLHttpRequest", 1387 | } 1388 | : {}; 1389 | 1390 | if (this.options.binaryBody) { 1391 | headers["Content-Type"] = files[0].type; 1392 | } 1393 | 1394 | if (this.options.headers) { 1395 | Object.assign(headers, this.options.headers); 1396 | } 1397 | 1398 | for (let headerName in headers) { 1399 | let headerValue = headers[headerName]; 1400 | if (headerValue) { 1401 | xhr.setRequestHeader(headerName, headerValue); 1402 | } 1403 | } 1404 | 1405 | if (this.options.binaryBody) { 1406 | // Since the file is going to be sent as binary body, it doesn't make 1407 | // any sense to generate `FormData` for it. 1408 | for (let file of files) { 1409 | this.emit("sending", file, xhr); 1410 | } 1411 | if (this.options.uploadMultiple) { 1412 | this.emit("sendingmultiple", files, xhr); 1413 | } 1414 | this.submitRequest(xhr, null, files); 1415 | } else { 1416 | let formData = new FormData(); 1417 | 1418 | // Adding all @options parameters 1419 | if (this.options.params) { 1420 | let additionalParams = this.options.params; 1421 | if (typeof additionalParams === "function") { 1422 | additionalParams = additionalParams.call( 1423 | this, 1424 | files, 1425 | xhr, 1426 | files[0].upload.chunked ? this._getChunk(files[0], xhr) : null 1427 | ); 1428 | } 1429 | 1430 | for (let key in additionalParams) { 1431 | let value = additionalParams[key]; 1432 | if (Array.isArray(value)) { 1433 | // The additional parameter contains an array, 1434 | // so lets iterate over it to attach each value 1435 | // individually. 1436 | for (let i = 0; i < value.length; i++) { 1437 | formData.append(key, value[i]); 1438 | } 1439 | } else { 1440 | formData.append(key, value); 1441 | } 1442 | } 1443 | } 1444 | 1445 | // Let the user add additional data if necessary 1446 | for (let file of files) { 1447 | this.emit("sending", file, xhr, formData); 1448 | } 1449 | if (this.options.uploadMultiple) { 1450 | this.emit("sendingmultiple", files, xhr, formData); 1451 | } 1452 | 1453 | this._addFormElementData(formData); 1454 | 1455 | // Finally add the files 1456 | // Has to be last because some servers (eg: S3) expect the file to be the last parameter 1457 | for (let i = 0; i < dataBlocks.length; i++) { 1458 | let dataBlock = dataBlocks[i]; 1459 | formData.append(dataBlock.name, dataBlock.data, dataBlock.filename); 1460 | } 1461 | 1462 | this.submitRequest(xhr, formData, files); 1463 | } 1464 | } 1465 | 1466 | // Transforms all files with this.options.transformFile and invokes done with the transformed files when done. 1467 | _transformFiles(files, done) { 1468 | let transformedFiles = []; 1469 | // Clumsy way of handling asynchronous calls, until I get to add a proper Future library. 1470 | let doneCounter = 0; 1471 | for (let i = 0; i < files.length; i++) { 1472 | this.options.transformFile.call(this, files[i], (transformedFile) => { 1473 | transformedFiles[i] = transformedFile; 1474 | if (++doneCounter === files.length) { 1475 | done(transformedFiles); 1476 | } 1477 | }); 1478 | } 1479 | } 1480 | 1481 | // Takes care of adding other input elements of the form to the AJAX request 1482 | _addFormElementData(formData) { 1483 | // Take care of other input elements 1484 | if (this.element.tagName === "FORM") { 1485 | for (let input of this.element.querySelectorAll( 1486 | "input, textarea, select, button" 1487 | )) { 1488 | let inputName = input.getAttribute("name"); 1489 | let inputType = input.getAttribute("type"); 1490 | if (inputType) inputType = inputType.toLowerCase(); 1491 | 1492 | // If the input doesn't have a name, we can't use it. 1493 | if (typeof inputName === "undefined" || inputName === null) continue; 1494 | 1495 | if (input.tagName === "SELECT" && input.hasAttribute("multiple")) { 1496 | // Possibly multiple values 1497 | for (let option of input.options) { 1498 | if (option.selected) { 1499 | formData.append(inputName, option.value); 1500 | } 1501 | } 1502 | } else if ( 1503 | !inputType || 1504 | (inputType !== "checkbox" && inputType !== "radio") || 1505 | input.checked 1506 | ) { 1507 | formData.append(inputName, input.value); 1508 | } 1509 | } 1510 | } 1511 | } 1512 | 1513 | // Invoked when there is new progress information about given files. 1514 | // If e is not provided, it is assumed that the upload is finished. 1515 | _updateFilesUploadProgress(files, xhr, e) { 1516 | if (!files[0].upload.chunked) { 1517 | // Handle file uploads without chunking 1518 | for (let file of files) { 1519 | if ( 1520 | file.upload.total && 1521 | file.upload.bytesSent && 1522 | file.upload.bytesSent == file.upload.total 1523 | ) { 1524 | // If both, the `total` and `bytesSent` have already been set, and 1525 | // they are equal (meaning progress is at 100%), we can skip this 1526 | // file, since an upload progress shouldn't go down. 1527 | continue; 1528 | } 1529 | 1530 | if (e) { 1531 | file.upload.progress = (100 * e.loaded) / e.total; 1532 | file.upload.total = e.total; 1533 | file.upload.bytesSent = e.loaded; 1534 | } else { 1535 | // No event, so we're at 100% 1536 | file.upload.progress = 100; 1537 | file.upload.bytesSent = file.upload.total; 1538 | } 1539 | 1540 | this.emit( 1541 | "uploadprogress", 1542 | file, 1543 | file.upload.progress, 1544 | file.upload.bytesSent 1545 | ); 1546 | } 1547 | } else { 1548 | // Handle chunked file uploads 1549 | 1550 | // Chunked upload is not compatible with uploading multiple files in one 1551 | // request, so we know there's only one file. 1552 | let file = files[0]; 1553 | 1554 | // Since this is a chunked upload, we need to update the appropriate chunk 1555 | // progress. 1556 | let chunk = this._getChunk(file, xhr); 1557 | 1558 | if (e) { 1559 | chunk.progress = (100 * e.loaded) / e.total; 1560 | chunk.total = e.total; 1561 | chunk.bytesSent = e.loaded; 1562 | } else { 1563 | // No event, so we're at 100% 1564 | chunk.progress = 100; 1565 | chunk.bytesSent = chunk.total; 1566 | } 1567 | 1568 | // Now tally the *file* upload progress from its individual chunks 1569 | file.upload.progress = 0; 1570 | file.upload.total = 0; 1571 | file.upload.bytesSent = 0; 1572 | for (let i = 0; i < file.upload.totalChunkCount; i++) { 1573 | if ( 1574 | file.upload.chunks[i] && 1575 | typeof file.upload.chunks[i].progress !== "undefined" 1576 | ) { 1577 | file.upload.progress += file.upload.chunks[i].progress; 1578 | file.upload.total += file.upload.chunks[i].total; 1579 | file.upload.bytesSent += file.upload.chunks[i].bytesSent; 1580 | } 1581 | } 1582 | // Since the process is a percentage, we need to divide by the amount of 1583 | // chunks we've used. 1584 | file.upload.progress = file.upload.progress / file.upload.totalChunkCount; 1585 | 1586 | this.emit( 1587 | "uploadprogress", 1588 | file, 1589 | file.upload.progress, 1590 | file.upload.bytesSent 1591 | ); 1592 | } 1593 | } 1594 | 1595 | _finishedUploading(files, xhr, e) { 1596 | let response; 1597 | 1598 | if (files[0].status === Dropzone.CANCELED) { 1599 | return; 1600 | } 1601 | 1602 | if (xhr.readyState !== 4) { 1603 | return; 1604 | } 1605 | 1606 | if (xhr.responseType !== "arraybuffer" && xhr.responseType !== "blob") { 1607 | response = xhr.responseText; 1608 | 1609 | if ( 1610 | xhr.getResponseHeader("content-type") && 1611 | ~xhr.getResponseHeader("content-type").indexOf("application/json") 1612 | ) { 1613 | try { 1614 | response = JSON.parse(response); 1615 | } catch (error) { 1616 | e = error; 1617 | response = "Invalid JSON response from server."; 1618 | } 1619 | } 1620 | } 1621 | 1622 | this._updateFilesUploadProgress(files, xhr); 1623 | 1624 | if (!(200 <= xhr.status && xhr.status < 300)) { 1625 | this._handleUploadError(files, xhr, response); 1626 | } else { 1627 | if (files[0].upload.chunked) { 1628 | files[0].upload.finishedChunkUpload( 1629 | this._getChunk(files[0], xhr), 1630 | response 1631 | ); 1632 | } else { 1633 | this._finished(files, response, e); 1634 | } 1635 | } 1636 | } 1637 | 1638 | _handleUploadError(files, xhr, response) { 1639 | if (files[0].status === Dropzone.CANCELED) { 1640 | return; 1641 | } 1642 | 1643 | if (files[0].upload.chunked && this.options.retryChunks) { 1644 | let chunk = this._getChunk(files[0], xhr); 1645 | if (chunk.retries++ < this.options.retryChunksLimit) { 1646 | this._uploadData(files, [chunk.dataBlock]); 1647 | return; 1648 | } else { 1649 | console.warn("Retried this chunk too often. Giving up."); 1650 | } 1651 | } 1652 | 1653 | this._errorProcessing( 1654 | files, 1655 | response || 1656 | this.options.dictResponseError.replace("{{statusCode}}", xhr.status), 1657 | xhr 1658 | ); 1659 | } 1660 | 1661 | submitRequest(xhr, formData, files) { 1662 | if (xhr.readyState != 1) { 1663 | console.warn( 1664 | "Cannot send this request because the XMLHttpRequest.readyState is not OPENED." 1665 | ); 1666 | return; 1667 | } 1668 | if (this.options.binaryBody) { 1669 | if (files[0].upload.chunked) { 1670 | const chunk = this._getChunk(files[0], xhr); 1671 | xhr.send(chunk.dataBlock.data); 1672 | } else { 1673 | xhr.send(files[0]); 1674 | } 1675 | } else { 1676 | xhr.send(formData); 1677 | } 1678 | } 1679 | 1680 | // Called internally when processing is finished. 1681 | // Individual callbacks have to be called in the appropriate sections. 1682 | _finished(files, responseText, e) { 1683 | for (let file of files) { 1684 | file.status = Dropzone.SUCCESS; 1685 | this.emit("success", file, responseText, e); 1686 | this.emit("complete", file); 1687 | } 1688 | if (this.options.uploadMultiple) { 1689 | this.emit("successmultiple", files, responseText, e); 1690 | this.emit("completemultiple", files); 1691 | } 1692 | 1693 | if (this.options.autoProcessQueue) { 1694 | return this.processQueue(); 1695 | } 1696 | } 1697 | 1698 | // Called internally when processing is finished. 1699 | // Individual callbacks have to be called in the appropriate sections. 1700 | _errorProcessing(files, message, xhr) { 1701 | for (let file of files) { 1702 | file.status = Dropzone.ERROR; 1703 | this.emit("error", file, message, xhr); 1704 | this.emit("complete", file); 1705 | } 1706 | if (this.options.uploadMultiple) { 1707 | this.emit("errormultiple", files, message, xhr); 1708 | this.emit("completemultiple", files); 1709 | } 1710 | 1711 | if (this.options.autoProcessQueue) { 1712 | return this.processQueue(); 1713 | } 1714 | } 1715 | 1716 | static uuidv4() { 1717 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => 1718 | (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) 1719 | ); 1720 | } 1721 | } 1722 | Dropzone.initClass(); 1723 | 1724 | // This is a map of options for your different dropzones. Add configurations 1725 | // to this object for your different dropzone elements. 1726 | // 1727 | // Example: 1728 | // 1729 | // Dropzone.options.myDropzoneElementId = { maxFilesize: 1 }; 1730 | // 1731 | // And in html: 1732 | // 1733 | //
1734 | Dropzone.options = {}; 1735 | 1736 | // Returns the options for an element or undefined if none available. 1737 | Dropzone.optionsForElement = function (element) { 1738 | // Get the `Dropzone.options.elementId` for this element if it exists 1739 | if (element.getAttribute("id") && typeof Dropzone.options !== 'undefined') { 1740 | return Dropzone.options[camelize(element.getAttribute("id"))]; 1741 | } else { 1742 | return undefined; 1743 | } 1744 | }; 1745 | 1746 | // Holds a list of all dropzone instances 1747 | Dropzone.instances = []; 1748 | 1749 | // Returns the dropzone for given element if any 1750 | Dropzone.forElement = function (element) { 1751 | if (typeof element === "string") { 1752 | element = document.querySelector(element); 1753 | } 1754 | if ((element != null ? element.dropzone : undefined) == null) { 1755 | throw new Error( 1756 | "No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone." 1757 | ); 1758 | } 1759 | return element.dropzone; 1760 | }; 1761 | 1762 | // Looks for all .dropzone elements and creates a dropzone for them 1763 | Dropzone.discover = function () { 1764 | let dropzones; 1765 | if (document.querySelectorAll) { 1766 | dropzones = document.querySelectorAll(".dropzone"); 1767 | } else { 1768 | dropzones = []; 1769 | // IE :( 1770 | let checkElements = (elements) => 1771 | (() => { 1772 | let result = []; 1773 | for (let el of elements) { 1774 | if (/(^| )dropzone($| )/.test(el.className)) { 1775 | result.push(dropzones.push(el)); 1776 | } else { 1777 | result.push(undefined); 1778 | } 1779 | } 1780 | return result; 1781 | })(); 1782 | checkElements(document.getElementsByTagName("div")); 1783 | checkElements(document.getElementsByTagName("form")); 1784 | } 1785 | 1786 | return (() => { 1787 | let result = []; 1788 | for (let dropzone of dropzones) { 1789 | // Create a dropzone unless auto discover has been disabled for specific element 1790 | if (Dropzone.optionsForElement(dropzone) !== false) { 1791 | result.push(new Dropzone(dropzone)); 1792 | } else { 1793 | result.push(undefined); 1794 | } 1795 | } 1796 | return result; 1797 | })(); 1798 | }; 1799 | 1800 | // Checks if the browser is supported by simply checking if Promise is here: a good cutoff 1801 | Dropzone.isBrowserSupported = function () { 1802 | return typeof Promise !== "undefined"; 1803 | }; 1804 | 1805 | Dropzone.dataURItoBlob = function (dataURI) { 1806 | // convert base64 to raw binary data held in a string 1807 | // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this 1808 | let byteString = atob(dataURI.split(",")[1]); 1809 | 1810 | // separate out the mime component 1811 | let mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; 1812 | 1813 | // write the bytes of the string to an ArrayBuffer 1814 | let ab = new ArrayBuffer(byteString.length); 1815 | let ia = new Uint8Array(ab); 1816 | for ( 1817 | let i = 0, end = byteString.length, asc = 0 <= end; 1818 | asc ? i <= end : i >= end; 1819 | asc ? i++ : i-- 1820 | ) { 1821 | ia[i] = byteString.charCodeAt(i); 1822 | } 1823 | 1824 | // write the ArrayBuffer to a blob 1825 | return new Blob([ab], { type: mimeString }); 1826 | }; 1827 | 1828 | // Returns an array without the rejected item 1829 | const without = (list, rejectedItem) => 1830 | list.filter((item) => item !== rejectedItem).map((item) => item); 1831 | 1832 | // abc-def_ghi -> abcDefGhi 1833 | const camelize = (str) => 1834 | str.replace(/[\-_](\w)/g, (match) => match.charAt(1).toUpperCase()); 1835 | 1836 | // Creates an element from string 1837 | Dropzone.createElement = function (string) { 1838 | let div = document.createElement("div"); 1839 | div.innerHTML = string; 1840 | return div.childNodes[0]; 1841 | }; 1842 | 1843 | // Tests if given element is inside (or simply is) the container 1844 | Dropzone.elementInside = function (element, container) { 1845 | if (element === container) { 1846 | return true; 1847 | } // Coffeescript doesn't support do/while loops 1848 | while ((element = element.parentNode)) { 1849 | if (element === container) { 1850 | return true; 1851 | } 1852 | } 1853 | return false; 1854 | }; 1855 | 1856 | Dropzone.getElement = function (el, name) { 1857 | let element; 1858 | if (typeof el === "string") { 1859 | element = document.querySelector(el); 1860 | } else if (el.nodeType != null) { 1861 | element = el; 1862 | } 1863 | if (element == null) { 1864 | throw new Error( 1865 | `Invalid \`${name}\` option provided. Please provide a CSS selector or a plain HTML element.` 1866 | ); 1867 | } 1868 | return element; 1869 | }; 1870 | 1871 | Dropzone.getElements = function (els, name) { 1872 | let el, elements; 1873 | if (els instanceof Array) { 1874 | elements = []; 1875 | try { 1876 | for (el of els) { 1877 | elements.push(this.getElement(el, name)); 1878 | } 1879 | } catch (e) { 1880 | elements = null; 1881 | } 1882 | } else if (typeof els === "string") { 1883 | elements = []; 1884 | for (el of document.querySelectorAll(els)) { 1885 | elements.push(el); 1886 | } 1887 | } else if (els.nodeType != null) { 1888 | elements = [els]; 1889 | } 1890 | 1891 | if (elements == null || !elements.length) { 1892 | throw new Error( 1893 | `Invalid \`${name}\` option provided. Please provide a CSS selector, a plain HTML element or a list of those.` 1894 | ); 1895 | } 1896 | 1897 | return elements; 1898 | }; 1899 | 1900 | // Asks the user the question and calls accepted or rejected accordingly 1901 | // 1902 | // The default implementation just uses `window.confirm` and then calls the 1903 | // appropriate callback. 1904 | Dropzone.confirm = function (question, accepted, rejected) { 1905 | if (window.confirm(question)) { 1906 | return accepted(); 1907 | } else if (rejected != null) { 1908 | return rejected(); 1909 | } 1910 | }; 1911 | 1912 | // Validates the mime type like this: 1913 | // 1914 | // https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept 1915 | Dropzone.isValidFile = function (file, acceptedFiles) { 1916 | if (!acceptedFiles) { 1917 | return true; 1918 | } // If there are no accepted mime types, it's OK 1919 | acceptedFiles = acceptedFiles.split(","); 1920 | 1921 | let mimeType = file.type; 1922 | let baseMimeType = mimeType.replace(/\/.*$/, ""); 1923 | 1924 | for (let validType of acceptedFiles) { 1925 | validType = validType.trim(); 1926 | if (validType.charAt(0) === ".") { 1927 | if ( 1928 | file.name 1929 | .toLowerCase() 1930 | .indexOf( 1931 | validType.toLowerCase(), 1932 | file.name.length - validType.length 1933 | ) !== -1 1934 | ) { 1935 | return true; 1936 | } 1937 | } else if (/\/\*$/.test(validType)) { 1938 | // This is something like a image/* mime type 1939 | if (baseMimeType === validType.replace(/\/.*$/, "")) { 1940 | return true; 1941 | } 1942 | } else { 1943 | if (mimeType === validType) { 1944 | return true; 1945 | } 1946 | } 1947 | } 1948 | 1949 | return false; 1950 | }; 1951 | 1952 | // Augment jQuery 1953 | if (typeof jQuery !== "undefined" && jQuery !== null) { 1954 | jQuery.fn.dropzone = function (options) { 1955 | return this.each(function () { 1956 | return new Dropzone(this, options); 1957 | }); 1958 | }; 1959 | } 1960 | 1961 | // Dropzone file status codes 1962 | Dropzone.ADDED = "added"; 1963 | 1964 | Dropzone.QUEUED = "queued"; 1965 | // For backwards compatibility. Now, if a file is accepted, it's either queued 1966 | // or uploading. 1967 | Dropzone.ACCEPTED = Dropzone.QUEUED; 1968 | 1969 | Dropzone.UPLOADING = "uploading"; 1970 | Dropzone.PROCESSING = Dropzone.UPLOADING; // alias 1971 | 1972 | Dropzone.CANCELED = "canceled"; 1973 | Dropzone.ERROR = "error"; 1974 | Dropzone.SUCCESS = "success"; 1975 | 1976 | /* 1977 | 1978 | Bugfix for iOS 6 and 7 1979 | Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios 1980 | based on the work of https://github.com/stomita/ios-imagefile-megapixel 1981 | 1982 | */ 1983 | 1984 | // Detecting vertical squash in loaded image. 1985 | // Fixes a bug which squash image vertically while drawing into canvas for some images. 1986 | // This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel 1987 | let detectVerticalSquash = function (img) { 1988 | let ih = img.naturalHeight; 1989 | let canvas = document.createElement("canvas"); 1990 | canvas.width = 1; 1991 | canvas.height = ih; 1992 | let ctx = canvas.getContext("2d"); 1993 | ctx.drawImage(img, 0, 0); 1994 | let { data } = ctx.getImageData(1, 0, 1, ih); 1995 | 1996 | // search image edge pixel position in case it is squashed vertically. 1997 | let sy = 0; 1998 | let ey = ih; 1999 | let py = ih; 2000 | while (py > sy) { 2001 | let alpha = data[(py - 1) * 4 + 3]; 2002 | 2003 | if (alpha === 0) { 2004 | ey = py; 2005 | } else { 2006 | sy = py; 2007 | } 2008 | 2009 | py = (ey + sy) >> 1; 2010 | } 2011 | let ratio = py / ih; 2012 | 2013 | if (ratio === 0) { 2014 | return 1; 2015 | } else { 2016 | return ratio; 2017 | } 2018 | }; 2019 | 2020 | // A replacement for context.drawImage 2021 | // (args are for source and destination). 2022 | var drawImageIOSFix = function (ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { 2023 | let vertSquashRatio = detectVerticalSquash(img); 2024 | return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); 2025 | }; 2026 | 2027 | // Inspired by MinifyJpeg 2028 | // Source: http://www.perry.cz/files/ExifRestorer.js 2029 | // http://elicon.blog57.fc2.com/blog-entry-206.html 2030 | function removeExif(origFileBase64) { 2031 | 2032 | var marker = 'data:image/jpeg;base64,'; 2033 | 2034 | if (!origFileBase64.startsWith(marker)) { 2035 | return origFileBase64; 2036 | } 2037 | 2038 | var origFile = window.atob(origFileBase64.slice(marker.length)); 2039 | 2040 | if (!origFile.startsWith("\xFF\xD8\xFF")) { 2041 | return origFileBase64; 2042 | } 2043 | 2044 | // loop through the JPEG file segments and copy all but Exif segments into the filtered file. 2045 | var head = 0; 2046 | var filteredFile = ""; 2047 | while (head < origFile.length) { 2048 | 2049 | if (origFile.slice(head, head+2) == "\xFF\xDA") { 2050 | // this is the start of the image data, we don't expect exif data after that. 2051 | filteredFile += origFile.slice(head); 2052 | break; 2053 | } else if (origFile.slice(head, head+2) == "\xFF\xD8") { 2054 | // this is the global start marker. 2055 | filteredFile += origFile.slice(head, head+2); 2056 | head += 2; 2057 | } else { 2058 | // we have a segment of variable size. 2059 | var length = origFile.charCodeAt(head + 2) * 256 + origFile.charCodeAt(head + 3); 2060 | var endPoint = head + length + 2; 2061 | var segment = origFile.slice(head, endPoint); 2062 | if (!segment.startsWith("\xFF\xE1")) { 2063 | filteredFile += segment; 2064 | } 2065 | head = endPoint; 2066 | } 2067 | } 2068 | 2069 | return marker + window.btoa(filteredFile); 2070 | } 2071 | 2072 | function restoreExif(origFileBase64, resizedFileBase64) { 2073 | 2074 | var marker = 'data:image/jpeg;base64,'; 2075 | 2076 | if (!(origFileBase64.startsWith(marker) && resizedFileBase64.startsWith(marker))) { 2077 | return resizedFileBase64; 2078 | } 2079 | 2080 | var origFile = window.atob(origFileBase64.slice(marker.length)); 2081 | 2082 | if (!origFile.startsWith("\xFF\xD8\xFF")) { 2083 | return resizedFileBase64; 2084 | } 2085 | 2086 | // Go through the JPEG file segments one by one and collect any Exif segments we find. 2087 | var head = 0; 2088 | var exifData = ""; 2089 | while (head < origFile.length) { 2090 | 2091 | if (origFile.slice(head, head+2) == "\xFF\xDA") { 2092 | // this is the start of the image data, we don't expect exif data after that. 2093 | break; 2094 | } else if (origFile.slice(head, head+2) == "\xFF\xD8") { 2095 | // this is the global start marker. 2096 | head += 2; 2097 | } else { 2098 | // we have a segment of variable size. 2099 | var length = origFile.charCodeAt(head + 2) * 256 + origFile.charCodeAt(head + 3); 2100 | var endPoint = head + length + 2; 2101 | var segment = origFile.slice(head, endPoint); 2102 | if (segment.startsWith("\xFF\xE1")) { 2103 | exifData += segment; 2104 | } 2105 | head = endPoint; 2106 | } 2107 | 2108 | } 2109 | 2110 | if (exifData == "") { 2111 | return resizedFileBase64; 2112 | } 2113 | 2114 | var resizedFile = window.atob(resizedFileBase64.slice(marker.length)); 2115 | 2116 | if (!resizedFile.startsWith("\xFF\xD8\xFF")) { 2117 | return resizedFileBase64; 2118 | } 2119 | 2120 | // The first file segment is always header information so insert the Exif data as second segment. 2121 | var splitPoint = 4 + resizedFile.charCodeAt(4) * 256 + resizedFile.charCodeAt(5) 2122 | resizedFile = resizedFile.slice(0, splitPoint) + exifData + resizedFile.slice(splitPoint); 2123 | 2124 | return marker + window.btoa(resizedFile); 2125 | } 2126 | 2127 | function __guard__(value, transform) { 2128 | return typeof value !== "undefined" && value !== null 2129 | ? transform(value) 2130 | : undefined; 2131 | } 2132 | function __guardMethod__(obj, methodName, transform) { 2133 | if ( 2134 | typeof obj !== "undefined" && 2135 | obj !== null && 2136 | typeof obj[methodName] === "function" 2137 | ) { 2138 | return transform(obj, methodName); 2139 | } else { 2140 | return undefined; 2141 | } 2142 | } 2143 | 2144 | export { Dropzone }; 2145 | -------------------------------------------------------------------------------- /src/dropzone.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | 4 | @keyframes passing-through { 5 | 0% { 6 | opacity: 0; 7 | transform: translateY(40px); 8 | } 9 | 10 | 30%, 70% { 11 | opacity: 1; 12 | transform: translateY(0px); 13 | } 14 | 15 | 100% { 16 | opacity: 0; 17 | transform: translateY(-40px); 18 | } 19 | } 20 | 21 | 22 | @keyframes slide-in { 23 | 0% { 24 | opacity: 0; 25 | transform: translateY(40px); 26 | } 27 | 30% { 28 | opacity: 1; 29 | transform: translateY(0px); 30 | } 31 | } 32 | 33 | 34 | 35 | @keyframes pulse { 36 | 0% { transform: scale(1); } 37 | 10% { transform: scale(1.1); } 38 | 20% { transform: scale(1); } 39 | } 40 | 41 | 42 | 43 | .dropzone, .dropzone * { 44 | box-sizing: border-box; 45 | } 46 | .dropzone { 47 | 48 | $image-size: 120px; 49 | 50 | $image-border-radius: 20px; 51 | 52 | min-height: 150px; 53 | border: 1px solid rgba(0, 0, 0, 0.8); 54 | border-radius: 5px; 55 | padding: 20px 20px; 56 | 57 | &.dz-clickable { 58 | cursor: pointer; 59 | 60 | * { 61 | cursor: default; 62 | } 63 | .dz-message { 64 | &, * { 65 | cursor: pointer; 66 | } 67 | } 68 | } 69 | 70 | 71 | 72 | &.dz-started { 73 | .dz-message { 74 | display: none; 75 | } 76 | } 77 | 78 | &.dz-drag-hover { 79 | border-style: solid; 80 | .dz-message { 81 | opacity: 0.5; 82 | } 83 | } 84 | .dz-message { 85 | text-align: center; 86 | margin: 3em 0; 87 | 88 | .dz-button { 89 | background: none; 90 | color: inherit; 91 | border: none; 92 | padding: 0; 93 | font: inherit; 94 | cursor: pointer; 95 | outline: inherit; 96 | } 97 | } 98 | 99 | 100 | 101 | .dz-preview { 102 | position: relative; 103 | display: inline-block; 104 | 105 | vertical-align: top; 106 | 107 | margin: 16px; 108 | min-height: 100px; 109 | 110 | &:hover { 111 | // Making sure that always the hovered preview element is on top 112 | z-index: 1000; 113 | .dz-details { 114 | opacity: 1; 115 | } 116 | } 117 | 118 | &.dz-file-preview { 119 | 120 | .dz-image { 121 | border-radius: $image-border-radius; 122 | background: #999; 123 | background: linear-gradient(to bottom, #eee, #ddd); 124 | } 125 | 126 | .dz-details { 127 | opacity: 1; 128 | } 129 | } 130 | 131 | &.dz-image-preview { 132 | background: white; 133 | .dz-details { 134 | transition: opacity 0.2s linear; 135 | } 136 | } 137 | 138 | .dz-remove { 139 | font-size: 14px; 140 | text-align: center; 141 | display: block; 142 | cursor: pointer; 143 | border: none; 144 | &:hover { 145 | text-decoration: underline; 146 | } 147 | } 148 | 149 | &:hover .dz-details { 150 | opacity: 1; 151 | } 152 | .dz-details { 153 | $background-color: #444; 154 | 155 | z-index: 20; 156 | 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | 161 | opacity: 0; 162 | 163 | font-size: 13px; 164 | min-width: 100%; 165 | max-width: 100%; 166 | padding: 2em 1em; 167 | text-align: center; 168 | color: rgba(0, 0, 0, 0.9); 169 | 170 | $width: 120px; 171 | 172 | line-height: 150%; 173 | 174 | .dz-size { 175 | margin-bottom: 1em; 176 | font-size: 16px; 177 | } 178 | 179 | .dz-filename { 180 | 181 | white-space: nowrap; 182 | 183 | &:hover { 184 | span { 185 | border: 1px solid rgba(200, 200, 200, 0.8); 186 | background-color: rgba(255, 255, 255, 0.8); 187 | } 188 | } 189 | &:not(:hover) { 190 | overflow: hidden; 191 | text-overflow: ellipsis; 192 | span { 193 | border: 1px solid transparent; 194 | } 195 | } 196 | 197 | } 198 | 199 | .dz-filename, .dz-size { 200 | span { 201 | background-color: rgba(255, 255, 255, 0.4); 202 | padding: 0 0.4em; 203 | border-radius: 3px; 204 | } 205 | } 206 | 207 | } 208 | 209 | &:hover { 210 | .dz-image { 211 | img { 212 | // Getting rid of that white bleed-in 213 | transform: scale(1.05, 1.05); 214 | filter: blur(8px); 215 | } 216 | } 217 | } 218 | .dz-image { 219 | border-radius: $image-border-radius; 220 | overflow: hidden; 221 | width: $image-size; 222 | height: $image-size; 223 | position: relative; 224 | display: block; 225 | z-index: 10; 226 | 227 | img { 228 | display: block; 229 | } 230 | } 231 | 232 | 233 | &.dz-success { 234 | .dz-success-mark { 235 | animation: passing-through 3s cubic-bezier(0.770, 0.000, 0.175, 1.000); 236 | } 237 | } 238 | &.dz-error { 239 | .dz-error-mark { 240 | opacity: 1; 241 | animation: slide-in 3s cubic-bezier(0.770, 0.000, 0.175, 1.000); 242 | } 243 | } 244 | 245 | 246 | $overlay-color: white; 247 | $overlay-bg-color: rgba(0, 0, 0, 0.8); 248 | 249 | 250 | .dz-success-mark, .dz-error-mark { 251 | 252 | $image-height: 54px; 253 | $image-width: 54px; 254 | 255 | pointer-events: none; 256 | 257 | opacity: 0; 258 | z-index: 500; 259 | 260 | position: absolute; 261 | display: block; 262 | top: 50%; 263 | left: 50%; 264 | margin-left: -(math.div($image-width, 2)); 265 | margin-top: -(math.div($image-height, 2)); 266 | 267 | background: $overlay-bg-color; 268 | border-radius: 50%; 269 | 270 | svg { 271 | display: block; 272 | width: $image-width; 273 | height: $image-height; 274 | fill: $overlay-color; 275 | } 276 | } 277 | 278 | &.dz-processing .dz-progress { 279 | opacity: 1; 280 | transition: all 0.2s linear; 281 | } 282 | &.dz-complete .dz-progress { 283 | opacity: 0; 284 | transition: opacity 0.4s ease-in; 285 | } 286 | 287 | &:not(.dz-processing) { 288 | .dz-progress { 289 | animation: pulse 6s ease infinite; 290 | } 291 | } 292 | .dz-progress { 293 | $progress-height: 20px; 294 | $progress-border-width: 3px; 295 | 296 | opacity: 1; 297 | z-index: 1000; 298 | 299 | pointer-events: none; 300 | position: absolute; 301 | height: 20px; 302 | top: 50%; 303 | margin-top: -10px; 304 | left: 15%; 305 | right: 15%; 306 | 307 | border: $progress-border-width solid $overlay-bg-color; 308 | background: $overlay-bg-color; 309 | 310 | border-radius: 10px; 311 | 312 | overflow: hidden; 313 | 314 | .dz-upload { 315 | background: $overlay-color; 316 | 317 | display: block; 318 | position: relative; 319 | height: 100%; 320 | width: 0; 321 | transition: width 300ms ease-in-out; 322 | 323 | border-radius: $progress-height - $progress-border-width; 324 | } 325 | 326 | } 327 | 328 | &.dz-error { 329 | .dz-error-message { 330 | display: block; 331 | } 332 | &:hover .dz-error-message { 333 | opacity: 1; 334 | pointer-events: auto; 335 | } 336 | } 337 | 338 | .dz-error-message { 339 | $width: $image-size + 20px; 340 | $color: rgb(177, 6, 6); 341 | 342 | pointer-events: none; 343 | z-index: 1000; 344 | position: absolute; 345 | display: block; 346 | display: none; 347 | opacity: 0; 348 | transition: opacity 0.3s ease; 349 | border-radius: 8px; 350 | font-size: 13px; 351 | top: $image-size + 10px; 352 | left: -10px; 353 | width: $width; 354 | background: $color; 355 | padding: 0.5em 1em; 356 | color: white; 357 | 358 | // The triangle pointing up 359 | &:after { 360 | content: ''; 361 | position: absolute; 362 | top: -6px; 363 | left: math.div($width, 2) - 6px; 364 | width: 0; 365 | height: 0; 366 | border-left: 6px solid transparent; 367 | border-right: 6px solid transparent; 368 | border-bottom: 6px solid $color; 369 | } 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/emitter.js: -------------------------------------------------------------------------------- 1 | // The Emitter class provides the ability to call `.on()` on Dropzone to listen 2 | // to events. 3 | // It is strongly based on component's emitter class, and I removed the 4 | // functionality because of the dependency hell with different frameworks. 5 | export default class Emitter { 6 | // Add an event listener for given event 7 | on(event, fn) { 8 | this._callbacks = this._callbacks || {}; 9 | // Create namespace for this event 10 | if (!this._callbacks[event]) { 11 | this._callbacks[event] = []; 12 | } 13 | this._callbacks[event].push(fn); 14 | return this; 15 | } 16 | 17 | emit(event, ...args) { 18 | this._callbacks = this._callbacks || {}; 19 | let callbacks = this._callbacks[event]; 20 | 21 | if (callbacks) { 22 | for (let callback of callbacks) { 23 | callback.apply(this, args); 24 | } 25 | } 26 | // trigger a corresponding DOM event 27 | if (this.element) { 28 | this.element.dispatchEvent( 29 | this.makeEvent("dropzone:" + event, { args: args }) 30 | ); 31 | } 32 | return this; 33 | } 34 | 35 | makeEvent(eventName, detail) { 36 | let params = { bubbles: true, cancelable: true, detail: detail }; 37 | 38 | if (typeof window.CustomEvent === "function") { 39 | return new CustomEvent(eventName, params); 40 | } else { 41 | // IE 11 support 42 | // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent 43 | var evt = document.createEvent("CustomEvent"); 44 | evt.initCustomEvent( 45 | eventName, 46 | params.bubbles, 47 | params.cancelable, 48 | params.detail 49 | ); 50 | return evt; 51 | } 52 | } 53 | 54 | // Remove event listener for given event. If fn is not provided, all event 55 | // listeners for that event will be removed. If neither is provided, all 56 | // event listeners will be removed. 57 | off(event, fn) { 58 | if (!this._callbacks || arguments.length === 0) { 59 | this._callbacks = {}; 60 | return this; 61 | } 62 | 63 | // specific event 64 | let callbacks = this._callbacks[event]; 65 | if (!callbacks) { 66 | return this; 67 | } 68 | 69 | // remove all handlers 70 | if (arguments.length === 1) { 71 | delete this._callbacks[event]; 72 | return this; 73 | } 74 | 75 | // remove specific handler 76 | for (let i = 0; i < callbacks.length; i++) { 77 | let callback = callbacks[i]; 78 | if (callback === fn) { 79 | callbacks.splice(i, 1); 80 | break; 81 | } 82 | } 83 | 84 | return this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import Dropzone from "./dropzone"; 2 | import defaultPreviewTemplate from "bundle-text:./preview-template.html"; 3 | 4 | let defaultOptions = { 5 | /** 6 | * Has to be specified on elements other than form (or when the form doesn't 7 | * have an `action` attribute). 8 | * 9 | * You can also provide a function that will be called with `files` and 10 | * `dataBlocks` and must return the url as string. 11 | */ 12 | url: null, 13 | 14 | /** 15 | * Can be changed to `"put"` if necessary. You can also provide a function 16 | * that will be called with `files` and must return the method (since `v3.12.0`). 17 | */ 18 | method: "post", 19 | 20 | /** 21 | * Will be set on the XHRequest. 22 | */ 23 | withCredentials: false, 24 | 25 | /** 26 | * The timeout for the XHR requests in milliseconds (since `v4.4.0`). 27 | * If set to null or 0, no timeout is going to be set. 28 | */ 29 | timeout: null, 30 | 31 | /** 32 | * How many file uploads to process in parallel (See the 33 | * Enqueuing file uploads documentation section for more info) 34 | */ 35 | parallelUploads: 2, 36 | 37 | /** 38 | * Whether to send multiple files in one request. If 39 | * this it set to true, then the fallback file input element will 40 | * have the `multiple` attribute as well. This option will 41 | * also trigger additional events (like `processingmultiple`). See the events 42 | * documentation section for more information. 43 | */ 44 | uploadMultiple: false, 45 | 46 | /** 47 | * Whether you want files to be uploaded in chunks to your server. This can't be 48 | * used in combination with `uploadMultiple`. 49 | * 50 | * See [chunksUploaded](#config-chunksUploaded) for the callback to finalise an upload. 51 | */ 52 | chunking: false, 53 | 54 | /** 55 | * If `chunking` is enabled, this defines whether **every** file should be chunked, 56 | * even if the file size is below chunkSize. This means, that the additional chunk 57 | * form data will be submitted and the `chunksUploaded` callback will be invoked. 58 | */ 59 | forceChunking: false, 60 | 61 | /** 62 | * If `chunking` is `true`, then this defines the chunk size in bytes. 63 | */ 64 | chunkSize: 2 * 1024 * 1024, 65 | 66 | /** 67 | * If `true`, the individual chunks of a file are being uploaded simultaneously. 68 | * The limit of concurrent connections is governed by `parallelUploads`. 69 | */ 70 | parallelChunkUploads: false, 71 | 72 | /** 73 | * Whether a chunk should be retried if it fails. 74 | */ 75 | retryChunks: false, 76 | 77 | /** 78 | * If `retryChunks` is true, how many times should it be retried. 79 | */ 80 | retryChunksLimit: 3, 81 | 82 | /** 83 | * The maximum filesize (in MiB) that is allowed to be uploaded. 84 | */ 85 | maxFilesize: 256, 86 | 87 | /** 88 | * The name of the file param that gets transferred. 89 | * **NOTE**: If you have the option `uploadMultiple` set to `true`, then 90 | * Dropzone will append `[]` to the name. 91 | */ 92 | paramName: "file", 93 | 94 | /** 95 | * Whether thumbnails for images should be generated 96 | */ 97 | createImageThumbnails: true, 98 | 99 | /** 100 | * In MB. When the filename exceeds this limit, the thumbnail will not be generated. 101 | */ 102 | maxThumbnailFilesize: 10, 103 | 104 | /** 105 | * If `null`, the ratio of the image will be used to calculate it. 106 | */ 107 | thumbnailWidth: 120, 108 | 109 | /** 110 | * The same as `thumbnailWidth`. If both are null, images will not be resized. 111 | */ 112 | thumbnailHeight: 120, 113 | 114 | /** 115 | * How the images should be scaled down in case both, `thumbnailWidth` and `thumbnailHeight` are provided. 116 | * Can be either `contain` or `crop`. 117 | */ 118 | thumbnailMethod: "crop", 119 | 120 | /** 121 | * If set, images will be resized to these dimensions before being **uploaded**. 122 | * If only one, `resizeWidth` **or** `resizeHeight` is provided, the original aspect 123 | * ratio of the file will be preserved. 124 | * 125 | * The `options.transformFile` function uses these options, so if the `transformFile` function 126 | * is overridden, these options don't do anything. 127 | */ 128 | resizeWidth: null, 129 | 130 | /** 131 | * See `resizeWidth`. 132 | */ 133 | resizeHeight: null, 134 | 135 | /** 136 | * The mime type of the resized image (before it gets uploaded to the server). 137 | * If `null` the original mime type will be used. To force jpeg, for example, use `image/jpeg`. 138 | * See `resizeWidth` for more information. 139 | */ 140 | resizeMimeType: null, 141 | 142 | /** 143 | * The quality of the resized images. See `resizeWidth`. 144 | */ 145 | resizeQuality: 0.8, 146 | 147 | /** 148 | * How the images should be scaled down in case both, `resizeWidth` and `resizeHeight` are provided. 149 | * Can be either `contain` or `crop`. 150 | */ 151 | resizeMethod: "contain", 152 | 153 | /** 154 | * The base that is used to calculate the **displayed** filesize. You can 155 | * change this to 1024 if you would rather display kibibytes, mebibytes, 156 | * etc... 1024 is technically incorrect, because `1024 bytes` are `1 kibibyte` 157 | * not `1 kilobyte`. You can change this to `1024` if you don't care about 158 | * validity. 159 | */ 160 | filesizeBase: 1000, 161 | 162 | /** 163 | * If not `null` defines how many files this Dropzone handles. If it exceeds, 164 | * the event `maxfilesexceeded` will be called. The dropzone element gets the 165 | * class `dz-max-files-reached` accordingly so you can provide visual 166 | * feedback. 167 | */ 168 | maxFiles: null, 169 | 170 | /** 171 | * An optional object to send additional headers to the server. Eg: 172 | * `{ "My-Awesome-Header": "header value" }` 173 | */ 174 | headers: null, 175 | 176 | /** 177 | * Should the default headers be set or not? 178 | * Accept: application/json <- for requesting json response 179 | * Cache-Control: no-cache <- Request shouldn't be cached 180 | * X-Requested-With: XMLHttpRequest <- We sent the request via XMLHttpRequest 181 | */ 182 | defaultHeaders: true, 183 | 184 | /** 185 | * If `true`, the dropzone element itself will be clickable, if `false` 186 | * nothing will be clickable. 187 | * 188 | * You can also pass an HTML element, a CSS selector (for multiple elements) 189 | * or an array of those. In that case, all of those elements will trigger an 190 | * upload when clicked. 191 | */ 192 | clickable: true, 193 | 194 | /** 195 | * Whether hidden files in directories should be ignored. 196 | */ 197 | ignoreHiddenFiles: true, 198 | 199 | /** 200 | * The default implementation of `accept` checks the file's mime type or 201 | * extension against this list. This is a comma separated list of mime 202 | * types or file extensions. 203 | * 204 | * Eg.: `image/*,application/pdf,.psd` 205 | * 206 | * If the Dropzone is `clickable` this option will also be used as 207 | * [`accept`](https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept) 208 | * parameter on the hidden file input as well. 209 | */ 210 | acceptedFiles: null, 211 | 212 | /** 213 | * If false, files will be added to the queue but the queue will not be 214 | * processed automatically. 215 | * This can be useful if you need some additional user input before sending 216 | * files (or if you want want all files sent at once). 217 | * If you're ready to send the file simply call `myDropzone.processQueue()`. 218 | * 219 | * See the [enqueuing file uploads](#enqueuing-file-uploads) documentation 220 | * section for more information. 221 | */ 222 | autoProcessQueue: true, 223 | 224 | /** 225 | * If false, files added to the dropzone will not be queued by default. 226 | * You'll have to call `enqueueFile(file)` manually. 227 | */ 228 | autoQueue: true, 229 | 230 | /** 231 | * If `true`, this will add a link to every file preview to remove or cancel (if 232 | * already uploading) the file. The `dictCancelUpload`, `dictCancelUploadConfirmation` 233 | * and `dictRemoveFile` options are used for the wording. 234 | */ 235 | addRemoveLinks: false, 236 | 237 | /** 238 | * Defines where to display the file previews – if `null` the 239 | * Dropzone element itself is used. Can be a plain `HTMLElement` or a CSS 240 | * selector. The element should have the `dropzone-previews` class so 241 | * the previews are displayed properly. 242 | */ 243 | previewsContainer: null, 244 | 245 | /** 246 | * Set this to `true` if you don't want previews to be shown. 247 | */ 248 | disablePreviews: false, 249 | 250 | /** 251 | * This is the element the hidden input field (which is used when clicking on the 252 | * dropzone to trigger file selection) will be appended to. This might 253 | * be important in case you use frameworks to switch the content of your page. 254 | * 255 | * Can be a selector string, or an element directly. 256 | */ 257 | hiddenInputContainer: "body", 258 | 259 | /** 260 | * If null, no capture type will be specified 261 | * If camera, mobile devices will skip the file selection and choose camera 262 | * If microphone, mobile devices will skip the file selection and choose the microphone 263 | * If camcorder, mobile devices will skip the file selection and choose the camera in video mode 264 | * On apple devices multiple must be set to false. AcceptedFiles may need to 265 | * be set to an appropriate mime type (e.g. "image/*", "audio/*", or "video/*"). 266 | */ 267 | capture: null, 268 | 269 | /** 270 | * **Deprecated**. Use `renameFile` instead. 271 | */ 272 | renameFilename: null, 273 | 274 | /** 275 | * A function that is invoked before the file is uploaded to the server and renames the file. 276 | * This function gets the `File` as argument and can use the `file.name`. The actual name of the 277 | * file that gets used during the upload can be accessed through `file.upload.filename`. 278 | */ 279 | renameFile: null, 280 | 281 | /** 282 | * If `true` the fallback will be forced. This is very useful to test your server 283 | * implementations first and make sure that everything works as 284 | * expected without dropzone if you experience problems, and to test 285 | * how your fallbacks will look. 286 | */ 287 | forceFallback: false, 288 | 289 | /** 290 | * The text used before any files are dropped. 291 | */ 292 | dictDefaultMessage: "Drop files here to upload", 293 | 294 | /** 295 | * The text that replaces the default message text it the browser is not supported. 296 | */ 297 | dictFallbackMessage: 298 | "Your browser does not support drag'n'drop file uploads.", 299 | 300 | /** 301 | * The text that will be added before the fallback form. 302 | * If you provide a fallback element yourself, or if this option is `null` this will 303 | * be ignored. 304 | */ 305 | dictFallbackText: 306 | "Please use the fallback form below to upload your files like in the olden days.", 307 | 308 | /** 309 | * If the filesize is too big. 310 | * `{{filesize}}` and `{{maxFilesize}}` will be replaced with the respective configuration values. 311 | */ 312 | dictFileTooBig: 313 | "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", 314 | 315 | /** 316 | * If the file doesn't match the file type. 317 | */ 318 | dictInvalidFileType: "You can't upload files of this type.", 319 | 320 | /** 321 | * If the server response was invalid. 322 | * `{{statusCode}}` will be replaced with the servers status code. 323 | */ 324 | dictResponseError: "Server responded with {{statusCode}} code.", 325 | 326 | /** 327 | * If `addRemoveLinks` is true, the text to be used for the cancel upload link. 328 | */ 329 | dictCancelUpload: "Cancel upload", 330 | 331 | /** 332 | * The text that is displayed if an upload was manually canceled 333 | */ 334 | dictUploadCanceled: "Upload canceled.", 335 | 336 | /** 337 | * If `addRemoveLinks` is true, the text to be used for confirmation when cancelling upload. 338 | */ 339 | dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", 340 | 341 | /** 342 | * If `addRemoveLinks` is true, the text to be used to remove a file. 343 | */ 344 | dictRemoveFile: "Remove file", 345 | 346 | /** 347 | * If this is not null, then the user will be prompted before removing a file. 348 | */ 349 | dictRemoveFileConfirmation: null, 350 | 351 | /** 352 | * Displayed if `maxFiles` is st and exceeded. 353 | * The string `{{maxFiles}}` will be replaced by the configuration value. 354 | */ 355 | dictMaxFilesExceeded: "You cannot upload any more files.", 356 | 357 | /** 358 | * Allows you to translate the different units. Starting with `tb` for terabytes and going down to 359 | * `b` for bytes. 360 | */ 361 | dictFileSizeUnits: { tb: "TB", gb: "GB", mb: "MB", kb: "KB", b: "b" }, 362 | /** 363 | * Called when dropzone initialized 364 | * You can add event listeners here 365 | */ 366 | init() {}, 367 | 368 | /** 369 | * Can be an **object** of additional parameters to transfer to the server, **or** a `Function` 370 | * that gets invoked with the `files`, `xhr` and, if it's a chunked upload, `chunk` arguments. In case 371 | * of a function, this needs to return a map. 372 | * 373 | * The default implementation does nothing for normal uploads, but adds relevant information for 374 | * chunked uploads. 375 | * 376 | * This is the same as adding hidden input fields in the form element. 377 | */ 378 | params(files, xhr, chunk) { 379 | if (chunk) { 380 | return { 381 | dzuuid: chunk.file.upload.uuid, 382 | dzchunkindex: chunk.index, 383 | dztotalfilesize: chunk.file.size, 384 | dzchunksize: this.options.chunkSize, 385 | dztotalchunkcount: chunk.file.upload.totalChunkCount, 386 | dzchunkbyteoffset: chunk.index * this.options.chunkSize, 387 | }; 388 | } 389 | }, 390 | 391 | /** 392 | * A function that gets a [file](https://developer.mozilla.org/en-US/docs/DOM/File) 393 | * and a `done` function as parameters. 394 | * 395 | * If the done function is invoked without arguments, the file is "accepted" and will 396 | * be processed. If you pass an error message, the file is rejected, and the error 397 | * message will be displayed. 398 | * This function will not be called if the file is too big or doesn't match the mime types. 399 | */ 400 | accept(file, done) { 401 | return done(); 402 | }, 403 | 404 | /** 405 | * The callback that will be invoked when all chunks have been uploaded for a file. 406 | * It gets the file for which the chunks have been uploaded as the first parameter, 407 | * and the `done` function as second. `done()` needs to be invoked when everything 408 | * needed to finish the upload process is done. 409 | */ 410 | chunksUploaded: function (file, done) { 411 | done(); 412 | }, 413 | 414 | /** 415 | * Sends the file as binary blob in body instead of form data. 416 | * If this is set, the `params` option will be ignored. 417 | * It's an error to set this to `true` along with `uploadMultiple` since 418 | * multiple files cannot be in a single binary body. 419 | */ 420 | binaryBody: false, 421 | 422 | /** 423 | * Gets called when the browser is not supported. 424 | * The default implementation shows the fallback input field and adds 425 | * a text. 426 | */ 427 | fallback() { 428 | // This code should pass in IE7... :( 429 | let messageElement; 430 | this.element.className = `${this.element.className} dz-browser-not-supported`; 431 | 432 | for (let child of this.element.getElementsByTagName("div")) { 433 | if (/(^| )dz-message($| )/.test(child.className)) { 434 | messageElement = child; 435 | child.className = "dz-message"; // Removes the 'dz-default' class 436 | break; 437 | } 438 | } 439 | if (!messageElement) { 440 | messageElement = Dropzone.createElement( 441 | '
' 442 | ); 443 | this.element.appendChild(messageElement); 444 | } 445 | 446 | let span = messageElement.getElementsByTagName("span")[0]; 447 | if (span) { 448 | if (span.textContent != null) { 449 | span.textContent = this.options.dictFallbackMessage; 450 | } else if (span.innerText != null) { 451 | span.innerText = this.options.dictFallbackMessage; 452 | } 453 | } 454 | 455 | return this.element.appendChild(this.getFallbackForm()); 456 | }, 457 | 458 | /** 459 | * Gets called to calculate the thumbnail dimensions. 460 | * 461 | * It gets `file`, `width` and `height` (both may be `null`) as parameters and must return an object containing: 462 | * 463 | * - `srcWidth` & `srcHeight` (required) 464 | * - `trgWidth` & `trgHeight` (required) 465 | * - `srcX` & `srcY` (optional, default `0`) 466 | * - `trgX` & `trgY` (optional, default `0`) 467 | * 468 | * Those values are going to be used by `ctx.drawImage()`. 469 | */ 470 | resize(file, width, height, resizeMethod) { 471 | let info = { 472 | srcX: 0, 473 | srcY: 0, 474 | srcWidth: file.width, 475 | srcHeight: file.height, 476 | }; 477 | 478 | let srcRatio = file.width / file.height; 479 | 480 | // Automatically calculate dimensions if not specified 481 | if (width == null && height == null) { 482 | width = info.srcWidth; 483 | height = info.srcHeight; 484 | } else if (width == null) { 485 | width = height * srcRatio; 486 | } else if (height == null) { 487 | height = width / srcRatio; 488 | } 489 | 490 | // Make sure images aren't upscaled 491 | width = Math.min(width, info.srcWidth); 492 | height = Math.min(height, info.srcHeight); 493 | 494 | let trgRatio = width / height; 495 | 496 | if (info.srcWidth > width || info.srcHeight > height) { 497 | // Image is bigger and needs rescaling 498 | if (resizeMethod === "crop") { 499 | if (srcRatio > trgRatio) { 500 | info.srcHeight = file.height; 501 | info.srcWidth = info.srcHeight * trgRatio; 502 | } else { 503 | info.srcWidth = file.width; 504 | info.srcHeight = info.srcWidth / trgRatio; 505 | } 506 | } else if (resizeMethod === "contain") { 507 | // Method 'contain' 508 | if (srcRatio > trgRatio) { 509 | height = width / srcRatio; 510 | } else { 511 | width = height * srcRatio; 512 | } 513 | } else { 514 | throw new Error(`Unknown resizeMethod '${resizeMethod}'`); 515 | } 516 | } 517 | 518 | info.srcX = (file.width - info.srcWidth) / 2; 519 | info.srcY = (file.height - info.srcHeight) / 2; 520 | 521 | info.trgWidth = width; 522 | info.trgHeight = height; 523 | 524 | return info; 525 | }, 526 | 527 | /** 528 | * Can be used to transform the file (for example, resize an image if necessary). 529 | * 530 | * The default implementation uses `resizeWidth` and `resizeHeight` (if provided) and resizes 531 | * images according to those dimensions. 532 | * 533 | * Gets the `file` as the first parameter, and a `done()` function as the second, that needs 534 | * to be invoked with the file when the transformation is done. 535 | */ 536 | transformFile(file, done) { 537 | if ( 538 | (this.options.resizeWidth || this.options.resizeHeight) && 539 | file.type.match(/image.*/) 540 | ) { 541 | return this.resizeImage( 542 | file, 543 | this.options.resizeWidth, 544 | this.options.resizeHeight, 545 | this.options.resizeMethod, 546 | done 547 | ); 548 | } else { 549 | return done(file); 550 | } 551 | }, 552 | 553 | /** 554 | * A string that contains the template used for each dropped 555 | * file. Change it to fulfill your needs but make sure to properly 556 | * provide all elements. 557 | * 558 | * If you want to use an actual HTML element instead of providing a String 559 | * as a config option, you could create a div with the id `tpl`, 560 | * put the template inside it and provide the element like this: 561 | * 562 | * document 563 | * .querySelector('#tpl') 564 | * .innerHTML 565 | * 566 | */ 567 | previewTemplate: defaultPreviewTemplate, 568 | 569 | /* 570 | Those functions register themselves to the events on init and handle all 571 | the user interface specific stuff. Overwriting them won't break the upload 572 | but can break the way it's displayed. 573 | You can overwrite them if you don't like the default behavior. If you just 574 | want to add an additional event handler, register it on the dropzone object 575 | and don't overwrite those options. 576 | */ 577 | 578 | // Those are self explanatory and simply concern the DragnDrop. 579 | drop(e) { 580 | return this.element.classList.remove("dz-drag-hover"); 581 | }, 582 | dragstart(e) {}, 583 | dragend(e) { 584 | return this.element.classList.remove("dz-drag-hover"); 585 | }, 586 | dragenter(e) { 587 | return this.element.classList.add("dz-drag-hover"); 588 | }, 589 | dragover(e) { 590 | return this.element.classList.add("dz-drag-hover"); 591 | }, 592 | dragleave(e) { 593 | return this.element.classList.remove("dz-drag-hover"); 594 | }, 595 | 596 | paste(e) {}, 597 | 598 | // Called whenever there are no files left in the dropzone anymore, and the 599 | // dropzone should be displayed as if in the initial state. 600 | reset() { 601 | return this.element.classList.remove("dz-started"); 602 | }, 603 | 604 | // Called when a file is added to the queue 605 | // Receives `file` 606 | addedfile(file) { 607 | if (this.element === this.previewsContainer) { 608 | this.element.classList.add("dz-started"); 609 | } 610 | 611 | if (this.previewsContainer && !this.options.disablePreviews) { 612 | file.previewElement = Dropzone.createElement( 613 | this.options.previewTemplate.trim() 614 | ); 615 | file.previewTemplate = file.previewElement; // Backwards compatibility 616 | 617 | this.previewsContainer.appendChild(file.previewElement); 618 | for (var node of file.previewElement.querySelectorAll("[data-dz-name]")) { 619 | node.textContent = file.name; 620 | } 621 | for (node of file.previewElement.querySelectorAll("[data-dz-size]")) { 622 | node.innerHTML = this.filesize(file.size); 623 | } 624 | 625 | if (this.options.addRemoveLinks) { 626 | file._removeLink = Dropzone.createElement( 627 | `${this.options.dictRemoveFile}` 628 | ); 629 | file.previewElement.appendChild(file._removeLink); 630 | } 631 | 632 | let removeFileEvent = (e) => { 633 | e.preventDefault(); 634 | e.stopPropagation(); 635 | if (file.status === Dropzone.UPLOADING) { 636 | if (this.options.dictCancelUploadConfirmation) { 637 | return Dropzone.confirm( 638 | this.options.dictCancelUploadConfirmation, 639 | () => this.removeFile(file) 640 | ); 641 | } else { 642 | return this.removeFile(file); 643 | } 644 | } else { 645 | if (this.options.dictRemoveFileConfirmation) { 646 | return Dropzone.confirm( 647 | this.options.dictRemoveFileConfirmation, 648 | () => this.removeFile(file) 649 | ); 650 | } else { 651 | return this.removeFile(file); 652 | } 653 | } 654 | }; 655 | 656 | for (let removeLink of file.previewElement.querySelectorAll( 657 | "[data-dz-remove]" 658 | )) { 659 | removeLink.addEventListener("click", removeFileEvent); 660 | } 661 | } 662 | }, 663 | 664 | // Called whenever a file is removed. 665 | removedfile(file) { 666 | if (file.previewElement != null && file.previewElement.parentNode != null) { 667 | file.previewElement.parentNode.removeChild(file.previewElement); 668 | } 669 | return this._updateMaxFilesReachedClass(); 670 | }, 671 | 672 | // Called when a thumbnail has been generated 673 | // Receives `file` and `dataUrl` 674 | thumbnail(file, dataUrl) { 675 | if (file.previewElement) { 676 | file.previewElement.classList.remove("dz-file-preview"); 677 | for (let thumbnailElement of file.previewElement.querySelectorAll( 678 | "[data-dz-thumbnail]" 679 | )) { 680 | thumbnailElement.alt = file.name; 681 | thumbnailElement.src = dataUrl; 682 | } 683 | 684 | return setTimeout( 685 | () => file.previewElement.classList.add("dz-image-preview"), 686 | 1 687 | ); 688 | } 689 | }, 690 | 691 | // Called whenever an error occurs 692 | // Receives `file` and `message` 693 | error(file, message) { 694 | if (file.previewElement) { 695 | file.previewElement.classList.add("dz-error"); 696 | if (typeof message !== "string" && message.error) { 697 | message = message.error; 698 | } 699 | for (let node of file.previewElement.querySelectorAll( 700 | "[data-dz-errormessage]" 701 | )) { 702 | node.textContent = message; 703 | } 704 | } 705 | }, 706 | 707 | errormultiple() {}, 708 | 709 | // Called when a file gets processed. Since there is a queue, not all added 710 | // files are processed immediately. 711 | // Receives `file` 712 | processing(file) { 713 | if (file.previewElement) { 714 | file.previewElement.classList.add("dz-processing"); 715 | if (file._removeLink) { 716 | return (file._removeLink.innerHTML = this.options.dictCancelUpload); 717 | } 718 | } 719 | }, 720 | 721 | processingmultiple() {}, 722 | 723 | // Called whenever the upload progress gets updated. 724 | // Receives `file`, `progress` (percentage 0-100) and `bytesSent`. 725 | // To get the total number of bytes of the file, use `file.size` 726 | uploadprogress(file, progress, bytesSent) { 727 | if (file.previewElement) { 728 | for (let node of file.previewElement.querySelectorAll( 729 | "[data-dz-uploadprogress]" 730 | )) { 731 | node.nodeName === "PROGRESS" 732 | ? (node.value = progress) 733 | : (node.style.width = `${progress}%`); 734 | } 735 | } 736 | }, 737 | 738 | // Called whenever the total upload progress gets updated. 739 | // Called with totalUploadProgress (0-100), totalBytes and totalBytesSent 740 | totaluploadprogress() {}, 741 | 742 | // Called just before the file is sent. Gets the `xhr` object as second 743 | // parameter, so you can modify it (for example to add a CSRF token) and a 744 | // `formData` object to add additional information. 745 | sending() {}, 746 | 747 | sendingmultiple() {}, 748 | 749 | // When the complete upload is finished and successful 750 | // Receives `file` 751 | success(file) { 752 | if (file.previewElement) { 753 | return file.previewElement.classList.add("dz-success"); 754 | } 755 | }, 756 | 757 | successmultiple() {}, 758 | 759 | // When the upload is canceled. 760 | canceled(file) { 761 | return this.emit("error", file, this.options.dictUploadCanceled); 762 | }, 763 | 764 | canceledmultiple() {}, 765 | 766 | // When the upload is finished, either with success or an error. 767 | // Receives `file` 768 | complete(file) { 769 | if (file._removeLink) { 770 | file._removeLink.innerHTML = this.options.dictRemoveFile; 771 | } 772 | if (file.previewElement) { 773 | return file.previewElement.classList.add("dz-complete"); 774 | } 775 | }, 776 | 777 | completemultiple() {}, 778 | 779 | maxfilesexceeded() {}, 780 | 781 | maxfilesreached() {}, 782 | 783 | queuecomplete() {}, 784 | 785 | addedfiles() {}, 786 | 787 | emptyfolder() {}, 788 | }; 789 | 790 | export default defaultOptions; 791 | -------------------------------------------------------------------------------- /src/preview-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | 19 | 22 | 23 |
24 |
25 | 32 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | test-prebuilt.js -------------------------------------------------------------------------------- /test/built/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ["mocha", "sinon-chai"], 4 | files: ["built/unit-tests.js"], 5 | reporters: ["spec"], 6 | specReporter: { 7 | maxLogLines: 5, // limit number of lines logged per test 8 | suppressErrorSummary: false, // do not print error summary 9 | suppressFailed: false, // do not print information about failed tests 10 | suppressPassed: false, // do not print information about passed tests 11 | suppressSkipped: false, // do not print information about skipped tests 12 | showSpecTiming: true, // print the time elapsed for each spec 13 | failFast: false, // test would finish with error when a first fail occurs. 14 | }, 15 | logLevel: config.LOG_INFO, 16 | browsers: ["ChromeHeadless"], 17 | autoWatch: false, 18 | singleRun: true, // Karma captures browsers, runs the tests and exits 19 | concurrency: Infinity, 20 | plugins: ["karma-mocha", "karma-spec-reporter", "karma-chrome-launcher", "karma-sinon-chai"], 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/test-server.js: -------------------------------------------------------------------------------- 1 | // A simple test server that serves all files in `test-sites/` and accepts POST 2 | // and PUT requests. 3 | const express = require('express'); 4 | const path = require('path'); 5 | 6 | const app = express(); 7 | const port = 8888; 8 | 9 | // serve static files from this dir 10 | app.use(express.static(path.join(__dirname, 'test-sites'))); 11 | // middleware to parse incoming requests body 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: true })); 14 | // Handle all POST requests 15 | app.post('*', (_, res) => { 16 | // Send a success response 17 | res.json({ success: true }); 18 | }); 19 | 20 | // Handle all PUT requests 21 | app.put('*', (req, res) => { 22 | // Special handling for URLs that start with `/amazon-multipart-upload` 23 | if (req.path.startsWith('/amazon-multipart-upload')) { 24 | const etag = `"${Math.round(Math.random() * 10000)}"`; 25 | res.set('ETag', etag); 26 | } 27 | // Send a success response 28 | res.json({ success: true }); 29 | }); 30 | app.listen(port, () => { 31 | console.log(`Test server running on http://localhost:${port}`); 32 | }) 33 | -------------------------------------------------------------------------------- /test/test-sites/1-basic/zero_configuration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dropzone Test 5 | 6 | 7 | 8 | 9 | 14 | 15 |
16 | 17 | 20 | -------------------------------------------------------------------------------- /test/test-sites/2-integrations/aws-s3-multipart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dropzone Test 5 | 6 | 7 | 8 | 9 | 14 | 15 |
16 | 17 | 93 | -------------------------------------------------------------------------------- /test/test-sites/README.md: -------------------------------------------------------------------------------- 1 | These sites serve as examples, and are tested with Cypress. 2 | 3 | The tests are in `/cypress/e2e`. 4 | -------------------------------------------------------------------------------- /test/test-sites/dist: -------------------------------------------------------------------------------- 1 | ../../dist -------------------------------------------------------------------------------- /test/test-sites/index.html: -------------------------------------------------------------------------------- 1 | Server running. 2 | -------------------------------------------------------------------------------- /test/unit-tests.js: -------------------------------------------------------------------------------- 1 | import "./unit-tests/all"; 2 | import "./unit-tests/amazon-s3"; 3 | import "./unit-tests/emitter"; 4 | import "./unit-tests/static-functions"; 5 | -------------------------------------------------------------------------------- /test/unit-tests/README.md: -------------------------------------------------------------------------------- 1 | Initially all tests were just written in `all.js` but this file has become 2 | quite difficult to maintain. 3 | 4 | The idea is to have one file per feature now, which makes it a lot easier to 5 | manage. Not all groups have been extracted from `all.js` yet though, but please 6 | don't add any more to that file. 7 | -------------------------------------------------------------------------------- /test/unit-tests/amazon-s3.js: -------------------------------------------------------------------------------- 1 | import { Dropzone } from "../../src/dropzone.js"; 2 | import { sleep } from "./utils"; 3 | 4 | describe("Amazon S3 Support", function () { 5 | let getMockFile = ( 6 | type = "text/html", 7 | filename = "test file name", 8 | contents = ["file contents"] 9 | ) => { 10 | let file = new File(contents, filename, { type: type }); 11 | file.status = Dropzone.ADDED; 12 | file.accepted = true; 13 | file.upload = { 14 | filename: filename, 15 | }; 16 | return file; 17 | }; 18 | 19 | let xhr = null; 20 | let dropzone = null; 21 | beforeEach(() => (xhr = sinon.useFakeXMLHttpRequest())); 22 | 23 | afterEach(function () { 24 | if (dropzone != null) { 25 | dropzone.destroy(); 26 | } 27 | }); 28 | describe("constructor()", () => { 29 | it("should throw an exception if binaryBody and uploadMultiple", () => { 30 | let element = document.createElement("div"); 31 | expect( 32 | () => 33 | (dropzone = new Dropzone(element, { 34 | url: "/", 35 | binaryBody: true, 36 | uploadMultiple: true, 37 | })) 38 | ).to.throw("You cannot set both: binaryBody and uploadMultiple."); 39 | }); 40 | }); 41 | 42 | describe("upload", () => { 43 | let element = null; 44 | let dropzone = null; 45 | let requests = null; 46 | beforeEach(function () { 47 | requests = []; 48 | xhr.onCreate = (xhr) => requests.push(xhr); 49 | 50 | element = Dropzone.createElement("
"); 51 | document.body.appendChild(element); 52 | return (dropzone = new Dropzone(element, { 53 | url: "url", 54 | binaryBody: true, 55 | uploadprogress() {}, 56 | })); 57 | }); 58 | afterEach(function () { 59 | document.body.removeChild(element); 60 | dropzone.destroy(); 61 | return xhr.restore(); 62 | }); 63 | it("should add proper Content-Type", async () => { 64 | dropzone.addFile(getMockFile()); 65 | dropzone.addFile(getMockFile("image/jpeg", "some-file.jpg", [[1, 2, 3]])); 66 | await sleep(10); 67 | 68 | console.log(requests[0].requestHeaders); 69 | console.log(requests[1].requestHeaders); 70 | 71 | expect(requests[0].requestHeaders["Content-Type"]).eq( 72 | "text/html;charset=utf-8" 73 | ); 74 | 75 | expect(requests[1].requestHeaders["Content-Type"]).eq( 76 | "image/jpeg;charset=utf-8" 77 | ); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/unit-tests/emitter.js: -------------------------------------------------------------------------------- 1 | import { Dropzone } from "../../src/dropzone.js"; 2 | 3 | describe("Emitter", function () { 4 | let emitter = null; 5 | beforeEach(() => (emitter = new Dropzone.prototype.Emitter())); 6 | 7 | it(".on() should return the object itself", () => 8 | emitter.on("test", function () {}).should.equal(emitter)); 9 | 10 | it(".on() should properly register listeners", function () { 11 | (emitter._callbacks === undefined).should.be.true; 12 | let callback = function () {}; 13 | let callback2 = function () {}; 14 | emitter.on("test", callback); 15 | emitter.on("test", callback2); 16 | emitter.on("test2", callback); 17 | emitter._callbacks.test.length.should.equal(2); 18 | emitter._callbacks.test[0].should.equal(callback); 19 | emitter._callbacks.test[1].should.equal(callback2); 20 | emitter._callbacks.test2.length.should.equal(1); 21 | return emitter._callbacks.test2[0].should.equal(callback); 22 | }); 23 | 24 | it(".emit() should return the object itself", () => 25 | emitter.emit("test").should.equal(emitter)); 26 | 27 | it(".emit() should properly invoke all registered callbacks with arguments", function () { 28 | let callCount1 = 0; 29 | let callCount12 = 0; 30 | let callCount2 = 0; 31 | let callback1 = function (var1, var2) { 32 | callCount1++; 33 | var1.should.equal("callback1 var1"); 34 | return var2.should.equal("callback1 var2"); 35 | }; 36 | let callback12 = function (var1, var2) { 37 | callCount12++; 38 | var1.should.equal("callback1 var1"); 39 | return var2.should.equal("callback1 var2"); 40 | }; 41 | let callback2 = function (var1, var2) { 42 | callCount2++; 43 | var1.should.equal("callback2 var1"); 44 | return var2.should.equal("callback2 var2"); 45 | }; 46 | 47 | emitter.on("test1", callback1); 48 | emitter.on("test1", callback12); 49 | emitter.on("test2", callback2); 50 | 51 | callCount1.should.equal(0); 52 | callCount12.should.equal(0); 53 | callCount2.should.equal(0); 54 | 55 | emitter.emit("test1", "callback1 var1", "callback1 var2"); 56 | 57 | callCount1.should.equal(1); 58 | callCount12.should.equal(1); 59 | callCount2.should.equal(0); 60 | 61 | emitter.emit("test2", "callback2 var1", "callback2 var2"); 62 | 63 | callCount1.should.equal(1); 64 | callCount12.should.equal(1); 65 | callCount2.should.equal(1); 66 | 67 | emitter.emit("test1", "callback1 var1", "callback1 var2"); 68 | 69 | callCount1.should.equal(2); 70 | callCount12.should.equal(2); 71 | return callCount2.should.equal(1); 72 | }); 73 | 74 | return describe(".off()", function () { 75 | let callback1 = function () {}; 76 | let callback2 = function () {}; 77 | let callback3 = function () {}; 78 | let callback4 = function () {}; 79 | 80 | beforeEach( 81 | () => 82 | (emitter._callbacks = { 83 | test1: [callback1, callback2], 84 | test2: [callback3], 85 | test3: [callback1, callback4], 86 | test4: [], 87 | }) 88 | ); 89 | 90 | it("should work without any listeners", function () { 91 | emitter._callbacks = undefined; 92 | let emt = emitter.off(); 93 | emitter._callbacks.should.eql({}); 94 | return emt.should.equal(emitter); 95 | }); 96 | 97 | it("should properly remove all event listeners", function () { 98 | let emt = emitter.off(); 99 | emitter._callbacks.should.eql({}); 100 | return emt.should.equal(emitter); 101 | }); 102 | 103 | it("should properly remove all event listeners for specific event", function () { 104 | emitter.off("test1"); 105 | (emitter._callbacks["test1"] === undefined).should.be.true; 106 | emitter._callbacks["test2"].length.should.equal(1); 107 | emitter._callbacks["test3"].length.should.equal(2); 108 | let emt = emitter.off("test2"); 109 | (emitter._callbacks["test2"] === undefined).should.be.true; 110 | return emt.should.equal(emitter); 111 | }); 112 | 113 | it("should properly remove specific event listener", function () { 114 | emitter.off("test1", callback1); 115 | emitter._callbacks["test1"].length.should.equal(1); 116 | emitter._callbacks["test1"][0].should.equal(callback2); 117 | emitter._callbacks["test3"].length.should.equal(2); 118 | let emt = emitter.off("test3", callback4); 119 | emitter._callbacks["test3"].length.should.equal(1); 120 | emitter._callbacks["test3"][0].should.equal(callback1); 121 | return emt.should.equal(emitter); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /test/unit-tests/static-functions.js: -------------------------------------------------------------------------------- 1 | import { Dropzone } from "../../src/dropzone.js"; 2 | 3 | describe("Static functions", function () { 4 | describe("Dropzone.isBrowserSupported()", function () { 5 | it("should be supported browser", () => { 6 | Dropzone.isBrowserSupported().should.be.true; 7 | }); 8 | }); 9 | 10 | describe("Dropzone.createElement()", function () { 11 | let element = Dropzone.createElement( 12 | '
Hallo
' 13 | ); 14 | 15 | it("should properly create an element from a string", () => 16 | element.tagName.should.equal("DIV")); 17 | it("should properly add the correct class", () => 18 | element.classList.contains("test").should.be.ok); 19 | it("should properly create child elements", () => 20 | element.querySelector("span").tagName.should.equal("SPAN")); 21 | it("should always return only one element", function () { 22 | element = Dropzone.createElement("
"); 23 | return element.tagName.should.equal("DIV"); 24 | }); 25 | }); 26 | 27 | describe("Dropzone.elementInside()", function () { 28 | let element = Dropzone.createElement( 29 | '
' 30 | ); 31 | document.body.appendChild(element); 32 | 33 | let child1 = element.querySelector(".child1"); 34 | let child2 = element.querySelector(".child2"); 35 | 36 | after(() => document.body.removeChild(element)); 37 | 38 | it("should return yes if elements are the same", () => 39 | Dropzone.elementInside(element, element).should.be.ok); 40 | it("should return yes if element is direct child", () => 41 | Dropzone.elementInside(child1, element).should.be.ok); 42 | it("should return yes if element is some child", function () { 43 | Dropzone.elementInside(child2, element).should.be.ok; 44 | return Dropzone.elementInside(child2, document.body).should.be.ok; 45 | }); 46 | it("should return no unless element is some child", function () { 47 | Dropzone.elementInside(element, child1).should.not.be.ok; 48 | return Dropzone.elementInside(document.body, child1).should.not.be.ok; 49 | }); 50 | }); 51 | 52 | describe("Dropzone.optionsForElement()", function () { 53 | let testOptions = { 54 | url: "/some/url", 55 | method: "put", 56 | }; 57 | 58 | before(() => (Dropzone.options.testElement = testOptions)); 59 | after(() => delete Dropzone.options.testElement); 60 | 61 | let element = document.createElement("div"); 62 | 63 | it("should take options set in Dropzone.options from camelized id", function () { 64 | element.id = "test-element"; 65 | return Dropzone.optionsForElement(element).should.equal(testOptions); 66 | }); 67 | 68 | it("should return undefined if no options set", function () { 69 | element.id = "test-element2"; 70 | return expect(Dropzone.optionsForElement(element)).to.equal(undefined); 71 | }); 72 | 73 | it("should return undefined and not throw if it's a form with an input element of the name 'id'", function () { 74 | element = Dropzone.createElement('
'); 75 | return expect(Dropzone.optionsForElement(element)).to.equal(undefined); 76 | }); 77 | 78 | it("should ignore input fields with the name='id'", function () { 79 | element = Dropzone.createElement( 80 | '
' 81 | ); 82 | return Dropzone.optionsForElement(element).should.equal(testOptions); 83 | }); 84 | }); 85 | 86 | describe("Dropzone.forElement()", function () { 87 | let element = document.createElement("div"); 88 | element.id = "some-test-element"; 89 | let dropzone = null; 90 | before(function () { 91 | document.body.appendChild(element); 92 | return (dropzone = new Dropzone(element, { url: "/test" })); 93 | }); 94 | after(function () { 95 | dropzone.disable(); 96 | return document.body.removeChild(element); 97 | }); 98 | 99 | it("should throw an exception if no dropzone attached", () => 100 | expect(() => Dropzone.forElement(document.createElement("div"))).to.throw( 101 | "No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone." 102 | )); 103 | 104 | it("should accept css selectors", () => 105 | expect(Dropzone.forElement("#some-test-element")).to.equal(dropzone)); 106 | 107 | it("should accept native elements", () => 108 | expect(Dropzone.forElement(element)).to.equal(dropzone)); 109 | }); 110 | 111 | describe("Dropzone.discover()", function () { 112 | let element1 = document.createElement("div"); 113 | element1.className = "dropzone"; 114 | let element2 = element1.cloneNode(); 115 | let element3 = element1.cloneNode(); 116 | 117 | element1.id = "test-element-1"; 118 | element2.id = "test-element-2"; 119 | element3.id = "test-element-3"; 120 | 121 | describe("specific options", function () { 122 | before(function () { 123 | Dropzone.options.testElement1 = { url: "test-url" }; 124 | Dropzone.options.testElement2 = false; // Disabled 125 | document.body.appendChild(element1); 126 | document.body.appendChild(element2); 127 | return Dropzone.discover(); 128 | }); 129 | after(function () { 130 | document.body.removeChild(element1); 131 | return document.body.removeChild(element2); 132 | }); 133 | 134 | it("should find elements with a .dropzone class", () => 135 | element1.dropzone.should.be.ok); 136 | 137 | it("should not create dropzones with disabled options", () => 138 | expect(element2.dropzone).to.not.be.ok); 139 | }); 140 | }); 141 | 142 | describe("Dropzone.isValidFile()", function () { 143 | it("should return true if called without acceptedFiles", () => 144 | Dropzone.isValidFile({ type: "some/type" }, null).should.be.ok); 145 | 146 | it("should properly validate if called with concrete mime types", function () { 147 | let acceptedMimeTypes = "text/html,image/jpeg,application/json"; 148 | 149 | Dropzone.isValidFile({ type: "text/html" }, acceptedMimeTypes).should.be 150 | .ok; 151 | Dropzone.isValidFile({ type: "image/jpeg" }, acceptedMimeTypes).should.be 152 | .ok; 153 | Dropzone.isValidFile({ type: "application/json" }, acceptedMimeTypes) 154 | .should.be.ok; 155 | return Dropzone.isValidFile({ type: "image/bmp" }, acceptedMimeTypes) 156 | .should.not.be.ok; 157 | }); 158 | 159 | it("should properly validate if called with base mime types", function () { 160 | let acceptedMimeTypes = "text/*,image/*,application/*"; 161 | 162 | Dropzone.isValidFile({ type: "text/html" }, acceptedMimeTypes).should.be 163 | .ok; 164 | Dropzone.isValidFile({ type: "image/jpeg" }, acceptedMimeTypes).should.be 165 | .ok; 166 | Dropzone.isValidFile({ type: "application/json" }, acceptedMimeTypes) 167 | .should.be.ok; 168 | Dropzone.isValidFile({ type: "image/bmp" }, acceptedMimeTypes).should.be 169 | .ok; 170 | return Dropzone.isValidFile({ type: "some/type" }, acceptedMimeTypes) 171 | .should.not.be.ok; 172 | }); 173 | 174 | it("should properly validate if called with mixed mime types", function () { 175 | let acceptedMimeTypes = "text/*,image/jpeg,application/*"; 176 | 177 | Dropzone.isValidFile({ type: "text/html" }, acceptedMimeTypes).should.be 178 | .ok; 179 | Dropzone.isValidFile({ type: "image/jpeg" }, acceptedMimeTypes).should.be 180 | .ok; 181 | Dropzone.isValidFile({ type: "image/bmp" }, acceptedMimeTypes).should.not 182 | .be.ok; 183 | Dropzone.isValidFile({ type: "application/json" }, acceptedMimeTypes) 184 | .should.be.ok; 185 | return Dropzone.isValidFile({ type: "some/type" }, acceptedMimeTypes) 186 | .should.not.be.ok; 187 | }); 188 | 189 | it("should properly validate even with spaces in between", function () { 190 | let acceptedMimeTypes = "text/html , image/jpeg, application/json"; 191 | 192 | Dropzone.isValidFile({ type: "text/html" }, acceptedMimeTypes).should.be 193 | .ok; 194 | return Dropzone.isValidFile({ type: "image/jpeg" }, acceptedMimeTypes) 195 | .should.be.ok; 196 | }); 197 | 198 | it("should properly validate extensions", function () { 199 | let acceptedMimeTypes = "text/html , image/jpeg, .pdf ,.png"; 200 | 201 | Dropzone.isValidFile( 202 | { name: "somxsfsd", type: "text/html" }, 203 | acceptedMimeTypes 204 | ).should.be.ok; 205 | Dropzone.isValidFile( 206 | { name: "somesdfsdf", type: "image/jpeg" }, 207 | acceptedMimeTypes 208 | ).should.be.ok; 209 | Dropzone.isValidFile( 210 | { name: "somesdfadfadf", type: "application/json" }, 211 | acceptedMimeTypes 212 | ).should.not.be.ok; 213 | Dropzone.isValidFile( 214 | { name: "some-file file.pdf", type: "random/type" }, 215 | acceptedMimeTypes 216 | ).should.be.ok; 217 | // .pdf has to be in the end 218 | Dropzone.isValidFile( 219 | { name: "some-file.pdf file.gif", type: "random/type" }, 220 | acceptedMimeTypes 221 | ).should.not.be.ok; 222 | return Dropzone.isValidFile( 223 | { name: "some-file file.png", type: "random/type" }, 224 | acceptedMimeTypes 225 | ).should.be.ok; 226 | }); 227 | }); 228 | 229 | describe("Dropzone.confirm", function () { 230 | beforeEach(() => sinon.stub(window, "confirm")); 231 | afterEach(() => window.confirm.restore()); 232 | it("should forward to window.confirm and call the callbacks accordingly", function () { 233 | let rejected; 234 | let accepted = (rejected = false); 235 | window.confirm.returns(true); 236 | Dropzone.confirm( 237 | "test question", 238 | () => (accepted = true), 239 | () => (rejected = true) 240 | ); 241 | window.confirm.args[0][0].should.equal("test question"); 242 | accepted.should.equal(true); 243 | rejected.should.equal(false); 244 | 245 | accepted = rejected = false; 246 | window.confirm.returns(false); 247 | Dropzone.confirm( 248 | "test question 2", 249 | () => (accepted = true), 250 | () => (rejected = true) 251 | ); 252 | window.confirm.args[1][0].should.equal("test question 2"); 253 | accepted.should.equal(false); 254 | return rejected.should.equal(true); 255 | }); 256 | 257 | it("should not error if rejected is not provided", function () { 258 | let rejected; 259 | let accepted = (rejected = false); 260 | window.confirm.returns(false); 261 | Dropzone.confirm("test question", () => (accepted = true)); 262 | window.confirm.args[0][0].should.equal("test question"); 263 | // Nothing should have changed since there is no rejected function. 264 | accepted.should.equal(false); 265 | return rejected.should.equal(false); 266 | }); 267 | }); 268 | 269 | describe("Dropzone.getElement() / getElements()", function () { 270 | let tmpElements = []; 271 | 272 | beforeEach(function () { 273 | tmpElements = []; 274 | tmpElements.push(Dropzone.createElement('
')); 275 | tmpElements.push( 276 | Dropzone.createElement('
') 277 | ); 278 | tmpElements.push( 279 | Dropzone.createElement('
') 280 | ); 281 | return tmpElements.forEach((el) => document.body.appendChild(el)); 282 | }); 283 | 284 | afterEach(() => tmpElements.forEach((el) => document.body.removeChild(el))); 285 | 286 | describe(".getElement()", function () { 287 | it("should accept a string", function () { 288 | let el = Dropzone.getElement(".tmptest"); 289 | el.should.equal(tmpElements[0]); 290 | el = Dropzone.getElement("#tmptest1"); 291 | return el.should.equal(tmpElements[1]); 292 | }); 293 | it("should accept a node", function () { 294 | let el = Dropzone.getElement(tmpElements[2]); 295 | return el.should.equal(tmpElements[2]); 296 | }); 297 | it("should fail if invalid selector", function () { 298 | let errorMessage = 299 | "Invalid `clickable` option provided. Please provide a CSS selector or a plain HTML element."; 300 | expect(() => Dropzone.getElement("lblasdlfsfl", "clickable")).to.throw( 301 | errorMessage 302 | ); 303 | expect(() => 304 | Dropzone.getElement({ lblasdlfsfl: "lblasdlfsfl" }, "clickable") 305 | ).to.throw(errorMessage); 306 | return expect(() => 307 | Dropzone.getElement(["lblasdlfsfl"], "clickable") 308 | ).to.throw(errorMessage); 309 | }); 310 | }); 311 | 312 | describe(".getElements()", function () { 313 | it("should accept a list of strings", function () { 314 | let els = Dropzone.getElements([".tmptest", "#tmptest1"]); 315 | return els.should.eql([tmpElements[0], tmpElements[1]]); 316 | }); 317 | it("should accept a list of nodes", function () { 318 | let els = Dropzone.getElements([tmpElements[0], tmpElements[2]]); 319 | return els.should.eql([tmpElements[0], tmpElements[2]]); 320 | }); 321 | it("should accept a mixed list", function () { 322 | let els = Dropzone.getElements(["#tmptest1", tmpElements[2]]); 323 | return els.should.eql([tmpElements[1], tmpElements[2]]); 324 | }); 325 | it("should accept a string selector", function () { 326 | let els = Dropzone.getElements(".random"); 327 | return els.should.eql([tmpElements[1], tmpElements[2]]); 328 | }); 329 | it("should accept a single node", function () { 330 | let els = Dropzone.getElements(tmpElements[1]); 331 | return els.should.eql([tmpElements[1]]); 332 | }); 333 | it("should fail if invalid selector", function () { 334 | let errorMessage = 335 | "Invalid `clickable` option provided. Please provide a CSS selector, a plain HTML element or a list of those."; 336 | expect(() => Dropzone.getElements("lblasdlfsfl", "clickable")).to.throw( 337 | errorMessage 338 | ); 339 | return expect(() => 340 | Dropzone.getElements(["lblasdlfsfl"], "clickable") 341 | ).to.throw(errorMessage); 342 | }); 343 | }); 344 | }); 345 | }); 346 | -------------------------------------------------------------------------------- /test/unit-tests/utils.js: -------------------------------------------------------------------------------- 1 | export async function sleep(delay) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, delay); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /tool/dropzone-global.js: -------------------------------------------------------------------------------- 1 | import Dropzone from "../src/dropzone"; 2 | 3 | window.Dropzone = Dropzone; 4 | 5 | export default Dropzone; 6 | -------------------------------------------------------------------------------- /types/dropzone.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Dropzone { 4 | export interface DropzoneResizeInfo { 5 | srcX?: number | undefined; 6 | srcY?: number | undefined; 7 | trgX?: number | undefined; 8 | trgY?: number | undefined; 9 | srcWidth?: number | undefined; 10 | srcHeight?: number | undefined; 11 | trgWidth?: number | undefined; 12 | trgHeight?: number | undefined; 13 | } 14 | 15 | export interface DropzoneFileUpload { 16 | progress: number; 17 | total: number; 18 | bytesSent: number; 19 | uuid: string; 20 | totalChunkCount?: number | undefined; 21 | } 22 | 23 | export interface DropzoneFile extends File { 24 | dataURL?: string | undefined; 25 | previewElement: HTMLElement; 26 | previewTemplate: HTMLElement; 27 | previewsContainer: HTMLElement; 28 | status: string; 29 | accepted: boolean; 30 | xhr?: XMLHttpRequest | undefined; 31 | upload?: DropzoneFileUpload | undefined; 32 | } 33 | 34 | export interface DropzoneMockFile { 35 | name: string; 36 | size: number; 37 | [index: string]: any; 38 | } 39 | 40 | export interface DropzoneDictFileSizeUnits { 41 | tb?: string | undefined; 42 | gb?: string | undefined; 43 | mb?: string | undefined; 44 | kb?: string | undefined; 45 | b?: string | undefined; 46 | } 47 | 48 | export interface DropzoneOptions { 49 | url?: ((files: readonly DropzoneFile[]) => string) | string | undefined; 50 | method?: ((files: readonly DropzoneFile[]) => string) | string | undefined; 51 | withCredentials?: boolean | undefined; 52 | timeout?: number | undefined; 53 | parallelUploads?: number | undefined; 54 | uploadMultiple?: boolean | undefined; 55 | chunking?: boolean | undefined; 56 | forceChunking?: boolean | undefined; 57 | chunkSize?: number | undefined; 58 | parallelChunkUploads?: boolean | undefined; 59 | retryChunks?: boolean | undefined; 60 | retryChunksLimit?: number | undefined; 61 | maxFilesize?: number | undefined; 62 | paramName?: string | undefined; 63 | createImageThumbnails?: boolean | undefined; 64 | maxThumbnailFilesize?: number | undefined; 65 | thumbnailWidth?: number | undefined; 66 | thumbnailHeight?: number | undefined; 67 | thumbnailMethod?: 'contain' | 'crop' | undefined; 68 | resizeWidth?: number | undefined; 69 | resizeHeight?: number | undefined; 70 | resizeMimeType?: string | undefined; 71 | resizeQuality?: number | undefined; 72 | resizeMethod?: 'contain' | 'crop' | undefined; 73 | filesizeBase?: number | undefined; 74 | maxFiles?: number | undefined; 75 | params?: {} | undefined; 76 | headers?: {[key: string]: string} | undefined; 77 | clickable?: boolean | string | HTMLElement | Array | undefined; 78 | ignoreHiddenFiles?: boolean | undefined; 79 | acceptedFiles?: string | undefined; 80 | renameFilename?(name: string): string; 81 | autoProcessQueue?: boolean | undefined; 82 | autoQueue?: boolean | undefined; 83 | addRemoveLinks?: boolean | undefined; 84 | previewsContainer?: boolean | string | HTMLElement | undefined; 85 | hiddenInputContainer?: HTMLElement | undefined; 86 | capture?: string | undefined; 87 | 88 | dictDefaultMessage?: string | undefined; 89 | dictFallbackMessage?: string | undefined; 90 | dictFallbackText?: string | undefined; 91 | dictFileTooBig?: string | undefined; 92 | dictInvalidFileType?: string | undefined; 93 | dictResponseError?: string | undefined; 94 | dictCancelUpload?: string | undefined; 95 | dictCancelUploadConfirmation?: string | undefined; 96 | dictRemoveFile?: string | undefined; 97 | dictRemoveFileConfirmation?: string | undefined; 98 | dictMaxFilesExceeded?: string | undefined; 99 | dictFileSizeUnits?: DropzoneDictFileSizeUnits | undefined; 100 | dictUploadCanceled?: string | undefined; 101 | 102 | accept?(file: DropzoneFile, done: (error?: string | Error) => void): void; 103 | chunksUploaded?(file: DropzoneFile, done: (error?: string | Error) => void): void; 104 | init?(this: Dropzone): void; 105 | forceFallback?: boolean | undefined; 106 | fallback?(): void; 107 | resize?(file: DropzoneFile, width?: number, height?: number, resizeMethod?: string): DropzoneResizeInfo; 108 | 109 | drop?(e: DragEvent): void; 110 | dragstart?(e: DragEvent): void; 111 | dragend?(e: DragEvent): void; 112 | dragenter?(e: DragEvent): void; 113 | dragover?(e: DragEvent): void; 114 | dragleave?(e: DragEvent): void; 115 | paste?(e: DragEvent): void; 116 | 117 | reset?(): void; 118 | 119 | addedfile?(file: DropzoneFile): void; 120 | addedfiles?(files: DropzoneFile[]): void; 121 | removedfile?(file: DropzoneFile): void; 122 | thumbnail?(file: DropzoneFile, dataUrl: string): void; 123 | 124 | error?(file: DropzoneFile, message: string | Error, xhr: XMLHttpRequest): void; 125 | errormultiple?(files: DropzoneFile[], message: string | Error, xhr: XMLHttpRequest): void; 126 | 127 | processing?(file: DropzoneFile): void; 128 | processingmultiple?(files: DropzoneFile[]): void; 129 | 130 | uploadprogress?(file: DropzoneFile, progress: number, bytesSent: number): void; 131 | totaluploadprogress?(totalProgress: number, totalBytes: number, totalBytesSent: number): void; 132 | 133 | sending?(file: DropzoneFile, xhr: XMLHttpRequest, formData: FormData): void; 134 | sendingmultiple?(files: DropzoneFile[], xhr: XMLHttpRequest, formData: FormData): void; 135 | 136 | success?(file: DropzoneFile): void; 137 | successmultiple?(files: DropzoneFile[], responseText: string): void; 138 | 139 | canceled?(file: DropzoneFile): void; 140 | canceledmultiple?(file: DropzoneFile[]): void; 141 | 142 | complete?(file: DropzoneFile): void; 143 | completemultiple?(file: DropzoneFile[]): void; 144 | 145 | maxfilesexceeded?(file: DropzoneFile): void; 146 | maxfilesreached?(files: DropzoneFile[]): void; 147 | queuecomplete?(): void; 148 | 149 | transformFile?(file: DropzoneFile, done: (file: string | Blob) => void): void; 150 | 151 | previewTemplate?: string | undefined; 152 | } 153 | 154 | export interface DropzoneListener { 155 | element: HTMLElement; 156 | events: { 157 | [key: string]: (e: Event) => any; 158 | }; 159 | } 160 | } 161 | 162 | declare class Dropzone { 163 | constructor(container: string | HTMLElement, options?: Dropzone.DropzoneOptions); 164 | 165 | //static autoDiscover: boolean; 166 | //static blacklistedBrowsers: RegExp[]; 167 | static confirm: (question: string, accepted: () => void, rejected?: () => void) => void; 168 | static createElement(string: string): HTMLElement; 169 | static dataURItoBlob(dataURI: string): Blob; 170 | static discover(): Dropzone[]; 171 | static elementInside(element: HTMLElement, container: HTMLElement): boolean; 172 | static forElement(element: string | HTMLElement): Dropzone; 173 | static getElement(element: string | HTMLElement, name?: string): HTMLElement; 174 | static getElements(elements: string | HTMLElement | Array): HTMLElement[]; 175 | static instances: Dropzone[]; 176 | static isBrowserSupported(): boolean; 177 | static isValidFile(file: File, acceptedFiles: string): boolean; 178 | static options: {[key: string]: Dropzone.DropzoneOptions | false}; 179 | static optionsForElement(element: HTMLElement): Dropzone.DropzoneOptions | undefined; 180 | static version: string; 181 | 182 | static ADDED: string; 183 | static QUEUED: string; 184 | static ACCEPTED: string; 185 | static UPLOADING: string; 186 | static PROCESSING: string; 187 | static CANCELED: string; 188 | static ERROR: string; 189 | static SUCCESS: string; 190 | 191 | element: HTMLElement; 192 | files: Dropzone.DropzoneFile[]; 193 | hiddenFileInput?: HTMLInputElement | undefined; 194 | listeners: Dropzone.DropzoneListener[]; 195 | defaultOptions: Dropzone.DropzoneOptions; 196 | options: Dropzone.DropzoneOptions; 197 | previewsContainer: HTMLElement; 198 | version: string; 199 | 200 | enable(): void; 201 | 202 | disable(): void; 203 | 204 | destroy(): Dropzone; 205 | 206 | addFile(file: Dropzone.DropzoneFile): void; 207 | 208 | removeFile(file: Dropzone.DropzoneFile): void; 209 | 210 | removeAllFiles(cancelIfNecessary?: boolean): void; 211 | 212 | resizeImage(file: Dropzone.DropzoneFile, width?: number, height?: number, resizeMethod?: string, callback?: (...args: any[]) => void): void; 213 | 214 | processQueue(): void; 215 | 216 | cancelUpload(file: Dropzone.DropzoneFile): void; 217 | 218 | createThumbnail( 219 | file: Dropzone.DropzoneFile, 220 | width?: number, 221 | height?: number, 222 | resizeMethod?: string, 223 | fixOrientation?: boolean, 224 | callback?: (...args: any[]) => void, 225 | ): any; 226 | 227 | displayExistingFile( 228 | mockFile: Dropzone.DropzoneMockFile, 229 | imageUrl: string, 230 | callback?: () => void, 231 | crossOrigin?: 'anonymous' | 'use-credentials', 232 | resizeThumbnail?: boolean, 233 | ): any; 234 | 235 | createThumbnailFromUrl( 236 | file: Dropzone.DropzoneFile, 237 | width?: number, 238 | height?: number, 239 | resizeMethod?: string, 240 | fixOrientation?: boolean, 241 | callback?: (...args: any[]) => void, 242 | crossOrigin?: string, 243 | ): any; 244 | 245 | processFiles(files: Dropzone.DropzoneFile[]): void; 246 | 247 | processFile(file: Dropzone.DropzoneFile): void; 248 | 249 | uploadFile(file: Dropzone.DropzoneFile): void; 250 | 251 | uploadFiles(files: Dropzone.DropzoneFile[]): void; 252 | 253 | getAcceptedFiles(): Dropzone.DropzoneFile[]; 254 | 255 | getActiveFiles(): Dropzone.DropzoneFile[]; 256 | 257 | getAddedFiles(): Dropzone.DropzoneFile[]; 258 | 259 | getRejectedFiles(): Dropzone.DropzoneFile[]; 260 | 261 | getQueuedFiles(): Dropzone.DropzoneFile[]; 262 | 263 | getUploadingFiles(): Dropzone.DropzoneFile[]; 264 | 265 | accept(file: Dropzone.DropzoneFile, done: (error?: string | Error) => void): void; 266 | 267 | getFilesWithStatus(status: string): Dropzone.DropzoneFile[]; 268 | 269 | enqueueFile(file: Dropzone.DropzoneFile): void; 270 | 271 | enqueueFiles(file: Dropzone.DropzoneFile[]): void; 272 | 273 | createThumbnail(file: Dropzone.DropzoneFile, callback?: (...args: any[]) => void): any; 274 | 275 | createThumbnailFromUrl(file: Dropzone.DropzoneFile, url: string, callback?: (...args: any[]) => void): any; 276 | 277 | on(eventName: string, callback: (...args: any[]) => void): Dropzone; 278 | 279 | off(): Dropzone; 280 | off(eventName: string, callback?: (...args: any[]) => void): Dropzone; 281 | 282 | emit(eventName: string, ...args: any[]): Dropzone; 283 | 284 | on(eventName: 'drop', callback: (e: DragEvent) => any): Dropzone; 285 | on(eventName: 'dragstart', callback: (e: DragEvent) => any): Dropzone; 286 | on(eventName: 'dragend', callback: (e: DragEvent) => any): Dropzone; 287 | on(eventName: 'dragenter', callback: (e: DragEvent) => any): Dropzone; 288 | on(eventName: 'dragover', callback: (e: DragEvent) => any): Dropzone; 289 | on(eventName: 'dragleave', callback: (e: DragEvent) => any): Dropzone; 290 | on(eventName: 'paste', callback: (e: DragEvent) => any): Dropzone; 291 | 292 | on(eventName: 'reset'): Dropzone; 293 | 294 | on(eventName: 'addedfile', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 295 | on(eventName: 'addedfiles', callback: (files: Dropzone.DropzoneFile[]) => any): Dropzone; 296 | on(eventName: 'removedfile', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 297 | on(eventName: 'thumbnail', callback: (file: Dropzone.DropzoneFile, dataUrl: string) => any): Dropzone; 298 | 299 | on(eventName: 'error', callback: (file: Dropzone.DropzoneFile, message: string | Error) => any): Dropzone; 300 | on(eventName: 'errormultiple', callback: (files: Dropzone.DropzoneFile[], message: string | Error) => any): Dropzone; 301 | 302 | on(eventName: 'processing', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 303 | on(eventName: 'processingmultiple', callback: (files: Dropzone.DropzoneFile[]) => any): Dropzone; 304 | 305 | on(eventName: 'uploadprogress', callback: (file: Dropzone.DropzoneFile, progress: number, bytesSent: number) => any): Dropzone; 306 | on(eventName: 'totaluploadprogress', callback: (totalProgress: number, totalBytes: number, totalBytesSent: number) => any): Dropzone; 307 | 308 | on(eventName: 'sending', callback: (file: Dropzone.DropzoneFile, xhr: XMLHttpRequest, formData: FormData) => any): Dropzone; 309 | on(eventName: 'sendingmultiple', callback: (files: Dropzone.DropzoneFile[], xhr: XMLHttpRequest, formData: FormData) => any): Dropzone; 310 | 311 | on(eventName: 'success', callback: (file: Dropzone.DropzoneFile, response: Object | string) => any): Dropzone; 312 | on(eventName: 'successmultiple', callback: (files: Dropzone.DropzoneFile[]) => any): Dropzone; 313 | 314 | on(eventName: 'canceled', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 315 | on(eventName: 'canceledmultiple', callback: (file: Dropzone.DropzoneFile[]) => any): Dropzone; 316 | 317 | on(eventName: 'complete', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 318 | on(eventName: 'completemultiple', callback: (file: Dropzone.DropzoneFile[]) => any): Dropzone; 319 | 320 | on(eventName: 'maxfilesexceeded', callback: (file: Dropzone.DropzoneFile) => any): Dropzone; 321 | on(eventName: 'maxfilesreached', callback: (files: Dropzone.DropzoneFile[]) => any): Dropzone; 322 | on(eventName: 'queuecomplete'): Dropzone; 323 | 324 | emit(eventName: 'drop', e: DragEvent): Dropzone; 325 | emit(eventName: 'dragstart', e: DragEvent): Dropzone; 326 | emit(eventName: 'dragend', e: DragEvent): Dropzone; 327 | emit(eventName: 'dragenter', e: DragEvent): Dropzone; 328 | emit(eventName: 'dragover', e: DragEvent): Dropzone; 329 | emit(eventName: 'dragleave', e: DragEvent): Dropzone; 330 | emit(eventName: 'paste', e: DragEvent): Dropzone; 331 | 332 | emit(eventName: 'reset'): Dropzone; 333 | 334 | emit(eventName: 'addedfile', file: Dropzone.DropzoneFile): Dropzone; 335 | emit(eventName: 'addedfiles', files: Dropzone.DropzoneFile[]): Dropzone; 336 | emit(eventName: 'removedfile', file: Dropzone.DropzoneFile): Dropzone; 337 | emit(eventName: 'thumbnail', file: Dropzone.DropzoneFile, dataUrl: string): Dropzone; 338 | 339 | emit(eventName: 'error', file: Dropzone.DropzoneFile, message: string | Error): Dropzone; 340 | emit(eventName: 'errormultiple', files: Dropzone.DropzoneFile[], message: string | Error): Dropzone; 341 | 342 | emit(eventName: 'processing', file: Dropzone.DropzoneFile): Dropzone; 343 | emit(eventName: 'processingmultiple', files: Dropzone.DropzoneFile[]): Dropzone; 344 | 345 | emit(eventName: 'uploadprogress', file: Dropzone.DropzoneFile, progress: number, bytesSent: number): Dropzone; 346 | emit(eventName: 'totaluploadprogress', totalProgress: number, totalBytes: number, totalBytesSent: number): Dropzone; 347 | 348 | emit(eventName: 'sending', file: Dropzone.DropzoneFile, xhr: XMLHttpRequest, formData: FormData): Dropzone; 349 | emit(eventName: 'sendingmultiple', files: Dropzone.DropzoneFile[], xhr: XMLHttpRequest, formData: FormData): Dropzone; 350 | 351 | emit(eventName: 'success', file: Dropzone.DropzoneFile, response: object | string): Dropzone; 352 | emit(eventName: 'successmultiple', files: Dropzone.DropzoneFile[]): Dropzone; 353 | 354 | emit(eventName: 'canceled', file: Dropzone.DropzoneFile): Dropzone; 355 | emit(eventName: 'canceledmultiple', file: Dropzone.DropzoneFile[]): Dropzone; 356 | 357 | emit(eventName: 'complete', file: Dropzone.DropzoneFile): Dropzone; 358 | emit(eventName: 'completemultiple', file: Dropzone.DropzoneFile[]): Dropzone; 359 | 360 | emit(eventName: 'maxfilesexceeded', file: Dropzone.DropzoneFile): Dropzone; 361 | emit(eventName: 'maxfilesreached', files: Dropzone.DropzoneFile[]): Dropzone; 362 | emit(eventName: 'queuecomplete'): Dropzone; 363 | } 364 | 365 | declare global { 366 | interface JQuery { 367 | dropzone(options: Dropzone.DropzoneOptions): Dropzone; 368 | } 369 | 370 | interface HTMLElement { 371 | dropzone: Dropzone; 372 | } 373 | } 374 | 375 | export default Dropzone; 376 | --------------------------------------------------------------------------------