├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── renovate.json ├── LICENSE ├── package.json ├── demo └── index.html ├── test.js ├── README.md ├── index.js └── vendor └── zip-fs.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [donmccurdy] 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | {"extends": ["github>donmccurdy/renovate-config"]} 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v5 16 | - run: yarn install 17 | - run: yarn dist 18 | - run: yarn test 19 | env: 20 | CI: true 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 Don McCurdy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-dropzone", 3 | "version": "0.8.3", 4 | "description": "A simple multi-file drag-and-drop input using vanilla JavaScript.", 5 | "source": "index.js", 6 | "main": "dist/simple-dropzone.js", 7 | "module": "dist/simple-dropzone.module.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/donmccurdy/simple-dropzone.git" 11 | }, 12 | "scripts": { 13 | "dev": "concurrently \"microbundle watch --name SimpleDropzone --external none\" \"serve .\"", 14 | "dist": "microbundle --name SimpleDropzone --external none", 15 | "test": "browserify test.js | tape-run | tap-spec", 16 | "version": "npm run dist", 17 | "postversion": "git push && git push --tags && npm publish" 18 | }, 19 | "keywords": [ 20 | "files", 21 | "input", 22 | "dropzone", 23 | "drag", 24 | "drop", 25 | "upload" 26 | ], 27 | "author": "Don McCurdy ", 28 | "license": "MIT", 29 | "dependencies": { 30 | "zip-js-esm": "^1.1.1" 31 | }, 32 | "devDependencies": { 33 | "browserify": "17.0.1", 34 | "concurrently": "9.2.1", 35 | "microbundle": "0.15.1", 36 | "serve": "14.2.5", 37 | "tap-spec": "5.0.0", 38 | "tape": "5.9.0", 39 | "tape-run": "11.0.0" 40 | }, 41 | "files": [ 42 | "dist/", 43 | "index.js", 44 | "README.md", 45 | "LICENSE", 46 | "package.json", 47 | "package-lock.json" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SimpleDropzone • Demo 5 | 22 | 23 | 24 |

SimpleDropzone

25 |
26 | Drag in or select some files. 27 | 28 |
29 |
30 | 31 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const { SimpleDropzone } = require('./'); 3 | 4 | const INPUT_FILES = [ 5 | {name: 'a.png', webkitRelativePath: 'path/to/a.png'}, 6 | {name: 'b.png', webkitRelativePath: 'path/to/b.png'}, 7 | ] 8 | 9 | test('construct', (t) => { 10 | const inputEl = new MockHTMLElement(); 11 | const dropEl = new MockHTMLElement(); 12 | const ctrl = new SimpleDropzone(dropEl, inputEl); 13 | t.ok(ctrl, 'creates dropzone'); 14 | t.equal(inputEl.listeners.length, 1, 'adds input listeners'); 15 | t.equal(dropEl.listeners.length, 2, 'adds dropzone listeners'); 16 | t.end(); 17 | }); 18 | 19 | test('select', async (t) => { 20 | const inputEl = new MockHTMLElement(); 21 | const dropEl = new MockHTMLElement(); 22 | const ctrl = new SimpleDropzone(dropEl, inputEl); 23 | 24 | inputEl.files = INPUT_FILES; 25 | 26 | let receivedDropstart = false; 27 | let receivedDroperror = false; 28 | let receivedDrop = false; 29 | 30 | ctrl.on('dropstart', () => (receivedDropstart = true)); 31 | ctrl.on('droperror', () => (receivedDroperror = true)); 32 | ctrl.on('drop', () => (receivedDrop = true)); 33 | 34 | const dropEvent = await new Promise((resolve, reject) => { 35 | ctrl.on('drop', resolve); 36 | ctrl.on('droperror', reject); 37 | inputEl.dispatchEvent(new CustomEvent('change', {})); 38 | }); 39 | 40 | t.ok(receivedDropstart, 'dropstart'); 41 | t.notOk(receivedDroperror, 'droperror'); 42 | t.ok(receivedDrop, 'drop'); 43 | t.deepEqual(Array.from(dropEvent.files), [ 44 | [INPUT_FILES[0].webkitRelativePath, INPUT_FILES[0]], 45 | [INPUT_FILES[1].webkitRelativePath, INPUT_FILES[1]], 46 | ], 'content'); 47 | t.end(); 48 | }); 49 | 50 | test('destroy', (t) => { 51 | const inputEl = new MockHTMLElement(); 52 | const dropEl = new MockHTMLElement(); 53 | const ctrl = new SimpleDropzone(dropEl, inputEl); 54 | 55 | t.equal(inputEl.listeners.length, 1, 'adds input listeners'); 56 | t.equal(dropEl.listeners.length, 2, 'adds dropzone listeners'); 57 | 58 | ctrl.destroy(); 59 | 60 | t.equal(inputEl.listeners.length, 0, 'removes input listeners'); 61 | t.equal(dropEl.listeners.length, 0, 'removes dropzone listeners'); 62 | 63 | t.end(); 64 | }); 65 | 66 | /** 67 | * Mock HTMLElement. 68 | * 69 | * Using an actual HTMLInputElement, it's difficult to modify the .files property 70 | * without user interaction (https://stackoverflow.com/q/1696877/1314762). So 71 | * instead, we use a simple mock element here. 72 | */ 73 | class MockHTMLElement { 74 | constructor () { 75 | this.listeners = []; 76 | } 77 | addEventListener (type, fn, options) { 78 | this.listeners.push([type, fn, options]); 79 | } 80 | removeEventListener (type, fn) { 81 | this.listeners = this.listeners.filter((listener) => { 82 | return !(listener[0] === type && listener[1] === fn); 83 | }); 84 | } 85 | dispatchEvent (event) { 86 | for (const listener of this.listeners) { 87 | if (listener[0] === event.type) listener[1](event); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-dropzone 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/simple-dropzone.svg)](https://www.npmjs.com/package/simple-dropzone) 4 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/simple-dropzone)](https://bundlephobia.com/package/simple-dropzone) 5 | [![License](https://img.shields.io/npm/l/simple-dropzone.svg)](https://github.com/donmccurdy/simple-dropzone/blob/master/LICENSE) 6 | [![CI](https://github.com/donmccurdy/simple-dropzone/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/donmccurdy/simple-dropzone/actions?query=workflow%3ACI) 7 | 8 | A simple drag-and-drop input using vanilla JavaScript. 9 | 10 | The library supports supports selection of multiple files, ZIP decoding, and fallback to `` on older browsers. 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install --save simple-dropzone 16 | ``` 17 | 18 | ## Usage 19 | 20 | Create DOM elements for the dropzone and a file input (for compatibility with older browsers). Both may be styled in CSS however you choose. 21 | 22 | ```html 23 |
24 | 25 | ``` 26 | 27 | Create a `SimpleDropzone` controller. When files are added, by drag-and-drop or selection with the input, a `drop` event is emitted. This event contains a map of filenames to HTML5 [File](https://developer.mozilla.org/en-US/docs/Web/API/File) objects. The file list is flat, although directory structure is shown in the filenames. 28 | 29 | ```js 30 | 31 | dropzone.on('drop', ({files}) => { 32 | console.log(files); 33 | }); 34 | ``` 35 | 36 | Optionally, you may want to set [additional attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#additional_attributes) to configure the file input element, e.g. to allow selection of multiple files. 37 | 38 | ## API 39 | 40 | ### SimpleDropzone( `dropEl`, `inputEl` ) 41 | 42 | Constructor takes two DOM elements, for the dropzone and file input. 43 | 44 | ```js 45 | const dropEl = document.querySelector('#dropzone'); 46 | const inputEl = document.querySelector('#input'); 47 | const dropCtrl = new SimpleDropzone(dropEl, inputEl); 48 | ``` 49 | 50 | ```html 51 |
52 | 53 | ``` 54 | 55 | ### .on( `event`, `callback` ) : `this` 56 | 57 | Listens for `event` and invokes the callback. 58 | 59 | ```js 60 | dropCtrl.on('drop', ({files}) => { 61 | console.log(files); 62 | }); 63 | ``` 64 | 65 | ### .destroy() 66 | 67 | Destroys the instance and unbinds all events. 68 | 69 | ## Events 70 | 71 | | Event | Properties | Description | 72 | |---|---|---| 73 | | `drop` | `files : Map, archive?: File` | New files added, from either drag-and-drop or selection. `archive` is provided if the files were extracted from a ZIP archive. | 74 | | `dropstart` | — | Selection is in progress. Decompressing ZIP archives may take several seconds. | 75 | | `droperror` | `message : string` | Selection has failed. | 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import zip from 'zip-js-esm'; 2 | import { fs } from './vendor/zip-fs.js'; 3 | 4 | zip.useWebWorkers = false; 5 | 6 | /** 7 | * Watches an element for file drops, parses to create a filemap hierarchy, 8 | * and emits the result. 9 | */ 10 | class SimpleDropzone { 11 | 12 | /** 13 | * @param {Element} el 14 | * @param {Element} inputEl 15 | */ 16 | constructor (el, inputEl) { 17 | this.el = el; 18 | this.inputEl = inputEl; 19 | 20 | this.listeners = { 21 | drop: [], 22 | dropstart: [], 23 | droperror: [] 24 | }; 25 | 26 | this._onDragover = this._onDragover.bind(this); 27 | this._onDrop = this._onDrop.bind(this); 28 | this._onSelect = this._onSelect.bind(this); 29 | 30 | el.addEventListener('dragover', this._onDragover, false); 31 | el.addEventListener('drop', this._onDrop, false); 32 | inputEl.addEventListener('change', this._onSelect); 33 | } 34 | 35 | /** 36 | * @param {string} type 37 | * @param {Function} callback 38 | * @return {SimpleDropzone} 39 | */ 40 | on (type, callback) { 41 | this.listeners[type].push(callback); 42 | return this; 43 | } 44 | 45 | /** 46 | * @param {string} type 47 | * @param {Object} data 48 | * @return {SimpleDropzone} 49 | */ 50 | _emit (type, data) { 51 | this.listeners[type] 52 | .forEach((callback) => callback(data)); 53 | return this; 54 | } 55 | 56 | /** 57 | * Destroys the instance. 58 | */ 59 | destroy () { 60 | const el = this.el; 61 | const inputEl = this.inputEl; 62 | 63 | el.removeEventListener('dragover', this._onDragover, false); 64 | el.removeEventListener('drop', this._onDrop, false); 65 | inputEl.removeEventListener('change', this._onSelect); 66 | 67 | delete this.el; 68 | delete this.inputEl; 69 | delete this.listeners; 70 | } 71 | 72 | /** 73 | * References (and horror): 74 | * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items 75 | * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files 76 | * - https://code.flickr.net/2012/12/10/drag-n-drop/ 77 | * - https://stackoverflow.com/q/44842247/1314762 78 | * 79 | * @param {Event} e 80 | */ 81 | _onDrop (e) { 82 | e.stopPropagation(); 83 | e.preventDefault(); 84 | 85 | this._emit('dropstart'); 86 | 87 | const files = Array.from(e.dataTransfer.files || []); 88 | const items = Array.from(e.dataTransfer.items || []); 89 | 90 | if (files.length === 0 && items.length === 0) { 91 | this._fail('Required drag-and-drop APIs are not supported in this browser.'); 92 | return; 93 | } 94 | 95 | // Prefer .items, which allow folder traversal if necessary. 96 | if (items.length > 0) { 97 | const entries = items.map((item) => item.webkitGetAsEntry()).filter(entry => entry !== null); 98 | 99 | // donmccurdy/simple-dropzone#69 100 | if (entries.length > 0) { 101 | if (entries[0].name.match(/\.zip$/)) { 102 | this._loadZip(items[0].getAsFile()); 103 | } else { 104 | this._loadNextEntry(new Map(), entries); 105 | } 106 | } 107 | 108 | return; 109 | } 110 | 111 | // Fall back to .files, since folders can't be traversed. 112 | if (files.length === 1 && files[0].name.match(/\.zip$/)) { 113 | this._loadZip(files[0]); 114 | } 115 | this._emit('drop', {files: new Map(files.map((file) => [file.name, file]))}); 116 | } 117 | 118 | /** 119 | * @param {Event} e 120 | */ 121 | _onDragover (e) { 122 | e.stopPropagation(); 123 | e.preventDefault(); 124 | e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. 125 | } 126 | 127 | /** 128 | * @param {Event} e 129 | */ 130 | _onSelect (e) { 131 | this._emit('dropstart'); 132 | 133 | // HTML file inputs do not seem to support folders, so assume this is a flat file list. 134 | const files = [].slice.call(this.inputEl.files); 135 | 136 | // Automatically decompress a zip archive if it is the only file given. 137 | if (files.length === 1 && this._isZip(files[0])) { 138 | this._loadZip(files[0]); 139 | return; 140 | } 141 | 142 | const fileMap = new Map(); 143 | files.forEach((file) => fileMap.set(file.webkitRelativePath || file.name, file)); 144 | this._emit('drop', {files: fileMap}); 145 | } 146 | 147 | /** 148 | * Iterates through a list of FileSystemEntry objects, creates the fileMap 149 | * tree, and emits the result. 150 | * @param {Map} fileMap 151 | * @param {Array} entries 152 | */ 153 | _loadNextEntry (fileMap, entries) { 154 | const entry = entries.pop(); 155 | 156 | if (!entry) { 157 | this._emit('drop', {files: fileMap}); 158 | return; 159 | } 160 | 161 | if (entry.isFile) { 162 | entry.file((file) => { 163 | fileMap.set(entry.fullPath, file); 164 | this._loadNextEntry(fileMap, entries); 165 | }, () => console.error('Could not load file: %s', entry.fullPath)); 166 | } else if (entry.isDirectory) { 167 | // readEntries() must be called repeatedly until it stops returning results. 168 | // https://www.w3.org/TR/2012/WD-file-system-api-20120417/#the-directoryreader-interface 169 | // https://bugs.chromium.org/p/chromium/issues/detail?id=378883 170 | const reader = entry.createReader(); 171 | const readerCallback = (newEntries) => { 172 | if (newEntries.length) { 173 | entries = entries.concat(newEntries); 174 | reader.readEntries(readerCallback); 175 | } else { 176 | this._loadNextEntry(fileMap, entries); 177 | } 178 | }; 179 | reader.readEntries(readerCallback); 180 | } else { 181 | console.warn('Unknown asset type: ' + entry.fullPath); 182 | this._loadNextEntry(fileMap, entries); 183 | } 184 | } 185 | 186 | /** 187 | * Inflates a File in .ZIP format, creates the fileMap tree, and emits the 188 | * result. 189 | * @param {File} file 190 | */ 191 | _loadZip (file) { 192 | const pending = []; 193 | const fileMap = new Map(); 194 | const archive = new fs.FS(); 195 | 196 | const traverse = (node) => { 197 | if (node.directory) { 198 | node.children.forEach(traverse); 199 | } else if (node.name[0] !== '.') { 200 | pending.push(new Promise((resolve) => { 201 | node.getData(new zip.BlobWriter(), (blob) => { 202 | blob.name = node.name; 203 | fileMap.set(node.getFullname(), blob); 204 | resolve(); 205 | }); 206 | })); 207 | } 208 | }; 209 | 210 | archive.importBlob(file, () => { 211 | traverse(archive.root); 212 | Promise.all(pending).then(() => { 213 | this._emit('drop', {files: fileMap, archive: file}); 214 | }); 215 | }); 216 | } 217 | 218 | /** 219 | * @param {File} file 220 | * @return {Boolean} 221 | */ 222 | _isZip (file) { 223 | return file.type === 'application/zip' || file.name.match(/\.zip$/); 224 | } 225 | 226 | /** 227 | * @param {string} message 228 | * @throws 229 | */ 230 | _fail (message) { 231 | this._emit('droperror', {message: message}); 232 | } 233 | } 234 | 235 | export { SimpleDropzone }; 236 | -------------------------------------------------------------------------------- /vendor/zip-fs.js: -------------------------------------------------------------------------------- 1 | import zip from 'zip-js-esm'; 2 | 3 | /* 4 | Copyright (c) 2013 Gildas Lormeau. All rights reserved. 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in 11 | the documentation and/or other materials provided with the distribution. 12 | 3. The names of the authors may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, 15 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 16 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, 17 | INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 20 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | var CHUNK_SIZE = 512 * 1024; 27 | 28 | var TextWriter = zip.TextWriter, // 29 | BlobWriter = zip.BlobWriter, // 30 | Data64URIWriter = zip.Data64URIWriter, // 31 | Reader = zip.Reader, // 32 | TextReader = zip.TextReader, // 33 | BlobReader = zip.BlobReader, // 34 | Data64URIReader = zip.Data64URIReader, // 35 | createReader = zip.createReader, // 36 | createWriter = zip.createWriter; 37 | 38 | function ZipBlobReader(entry) { 39 | var that = this, blobReader; 40 | 41 | function init(callback) { 42 | that.size = entry.uncompressedSize; 43 | callback(); 44 | } 45 | 46 | function getData(callback) { 47 | if (that.data) 48 | callback(); 49 | else 50 | entry.getData(new BlobWriter(), function(data) { 51 | that.data = data; 52 | blobReader = new BlobReader(data); 53 | callback(); 54 | }, null, that.checkCrc32); 55 | } 56 | 57 | function readUint8Array(index, length, callback, onerror) { 58 | getData(function() { 59 | blobReader.readUint8Array(index, length, callback, onerror); 60 | }, onerror); 61 | } 62 | 63 | that.size = 0; 64 | that.init = init; 65 | that.readUint8Array = readUint8Array; 66 | } 67 | ZipBlobReader.prototype = new Reader(); 68 | ZipBlobReader.prototype.constructor = ZipBlobReader; 69 | ZipBlobReader.prototype.checkCrc32 = false; 70 | 71 | function getTotalSize(entry) { 72 | var size = 0; 73 | 74 | function process(entry) { 75 | size += entry.uncompressedSize || 0; 76 | entry.children.forEach(process); 77 | } 78 | 79 | process(entry); 80 | return size; 81 | } 82 | 83 | function initReaders(entry, onend, onerror) { 84 | var index = 0; 85 | 86 | function next() { 87 | index++; 88 | if (index < entry.children.length) 89 | process(entry.children[index]); 90 | else 91 | onend(); 92 | } 93 | 94 | function process(child) { 95 | if (child.directory) 96 | initReaders(child, next, onerror); 97 | else { 98 | child.reader = new child.Reader(child.data, onerror); 99 | child.reader.init(function() { 100 | child.uncompressedSize = child.reader.size; 101 | next(); 102 | }); 103 | } 104 | } 105 | 106 | if (entry.children.length) 107 | process(entry.children[index]); 108 | else 109 | onend(); 110 | } 111 | 112 | function detach(entry) { 113 | var children = entry.parent.children; 114 | children.forEach(function(child, index) { 115 | if (child.id == entry.id) 116 | children.splice(index, 1); 117 | }); 118 | } 119 | 120 | function exportZip(zipWriter, entry, onend, onprogress, totalSize) { 121 | var currentIndex = 0; 122 | 123 | function process(zipWriter, entry, onend, onprogress, totalSize) { 124 | var childIndex = 0; 125 | 126 | function exportChild() { 127 | var child = entry.children[childIndex]; 128 | if (child) 129 | zipWriter.add(child.getFullname(), child.reader, function() { 130 | currentIndex += child.uncompressedSize || 0; 131 | process(zipWriter, child, function() { 132 | childIndex++; 133 | exportChild(); 134 | }, onprogress, totalSize); 135 | }, function(index) { 136 | if (onprogress) 137 | onprogress(currentIndex + index, totalSize); 138 | }, { 139 | directory : child.directory, 140 | version : child.zipVersion 141 | }); 142 | else 143 | onend(); 144 | } 145 | 146 | exportChild(); 147 | } 148 | 149 | process(zipWriter, entry, onend, onprogress, totalSize); 150 | } 151 | 152 | function addFileEntry(zipEntry, fileEntry, onend, onerror) { 153 | function getChildren(fileEntry, callback) { 154 | if (fileEntry.isDirectory) 155 | fileEntry.createReader().readEntries(callback); 156 | if (fileEntry.isFile) 157 | callback([]); 158 | } 159 | 160 | function process(zipEntry, fileEntry, onend) { 161 | getChildren(fileEntry, function(children) { 162 | var childIndex = 0; 163 | 164 | function addChild(child) { 165 | function nextChild(childFileEntry) { 166 | process(childFileEntry, child, function() { 167 | childIndex++; 168 | processChild(); 169 | }); 170 | } 171 | 172 | if (child.isDirectory) 173 | nextChild(zipEntry.addDirectory(child.name)); 174 | if (child.isFile) 175 | child.file(function(file) { 176 | var childZipEntry = zipEntry.addBlob(child.name, file); 177 | childZipEntry.uncompressedSize = file.size; 178 | nextChild(childZipEntry); 179 | }, onerror); 180 | } 181 | 182 | function processChild() { 183 | var child = children[childIndex]; 184 | if (child) 185 | addChild(child); 186 | else 187 | onend(); 188 | } 189 | 190 | processChild(); 191 | }); 192 | } 193 | 194 | if (fileEntry.isDirectory) 195 | process(zipEntry, fileEntry, onend); 196 | else 197 | fileEntry.file(function(file) { 198 | zipEntry.addBlob(fileEntry.name, file); 199 | onend(); 200 | }, onerror); 201 | } 202 | 203 | function getFileEntry(fileEntry, entry, onend, onprogress, onerror, totalSize, checkCrc32) { 204 | var currentIndex = 0; 205 | 206 | function process(fileEntry, entry, onend, onprogress, onerror, totalSize) { 207 | var childIndex = 0; 208 | 209 | function addChild(child) { 210 | function nextChild(childFileEntry) { 211 | currentIndex += child.uncompressedSize || 0; 212 | process(childFileEntry, child, function() { 213 | childIndex++; 214 | processChild(); 215 | }, onprogress, onerror, totalSize); 216 | } 217 | 218 | if (child.directory) 219 | fileEntry.getDirectory(child.name, { 220 | create : true 221 | }, nextChild, onerror); 222 | else 223 | fileEntry.getFile(child.name, { 224 | create : true 225 | }, function(file) { 226 | child.getData(new zip.FileWriter(file, zip.getMimeType(child.name)), nextChild, function(index) { 227 | if (onprogress) 228 | onprogress(currentIndex + index, totalSize); 229 | }, checkCrc32); 230 | }, onerror); 231 | } 232 | 233 | function processChild() { 234 | var child = entry.children[childIndex]; 235 | if (child) 236 | addChild(child); 237 | else 238 | onend(); 239 | } 240 | 241 | processChild(); 242 | } 243 | 244 | if (entry.directory) 245 | process(fileEntry, entry, onend, onprogress, onerror, totalSize); 246 | else 247 | entry.getData(new zip.FileWriter(fileEntry, zip.getMimeType(entry.name)), onend, onprogress, checkCrc32); 248 | } 249 | 250 | function resetFS(fs) { 251 | fs.entries = []; 252 | fs.root = new ZipDirectoryEntry(fs); 253 | } 254 | 255 | function bufferedCopy(reader, writer, onend, onprogress, onerror) { 256 | var chunkIndex = 0; 257 | 258 | function stepCopy() { 259 | var index = chunkIndex * CHUNK_SIZE; 260 | if (onprogress) 261 | onprogress(index, reader.size); 262 | if (index < reader.size) 263 | reader.readUint8Array(index, Math.min(CHUNK_SIZE, reader.size - index), function(array) { 264 | writer.writeUint8Array(new Uint8Array(array), function() { 265 | chunkIndex++; 266 | stepCopy(); 267 | }); 268 | }, onerror); 269 | else 270 | writer.getData(onend); 271 | } 272 | 273 | stepCopy(); 274 | } 275 | 276 | function addChild(parent, name, params, directory) { 277 | if (parent.directory) 278 | return directory ? new ZipDirectoryEntry(parent.fs, name, params, parent) : new ZipFileEntry(parent.fs, name, params, parent); 279 | else 280 | throw "Parent entry is not a directory."; 281 | } 282 | 283 | function ZipEntry() { 284 | } 285 | 286 | ZipEntry.prototype = { 287 | init : function(fs, name, params, parent) { 288 | var that = this; 289 | if (fs.root && parent && parent.getChildByName(name)) 290 | throw "Entry filename already exists."; 291 | if (!params) 292 | params = {}; 293 | that.fs = fs; 294 | that.name = name; 295 | that.id = fs.entries.length; 296 | that.parent = parent; 297 | that.children = []; 298 | that.zipVersion = params.zipVersion || 0x14; 299 | that.uncompressedSize = 0; 300 | fs.entries.push(that); 301 | if (parent) 302 | that.parent.children.push(that); 303 | }, 304 | getFileEntry : function(fileEntry, onend, onprogress, onerror, checkCrc32) { 305 | var that = this; 306 | initReaders(that, function() { 307 | getFileEntry(fileEntry, that, onend, onprogress, onerror, getTotalSize(that), checkCrc32); 308 | }, onerror); 309 | }, 310 | moveTo : function(target) { 311 | var that = this; 312 | if (target.directory) { 313 | if (!target.isDescendantOf(that)) { 314 | if (that != target) { 315 | if (target.getChildByName(that.name)) 316 | throw "Entry filename already exists."; 317 | detach(that); 318 | that.parent = target; 319 | target.children.push(that); 320 | } 321 | } else 322 | throw "Entry is a ancestor of target entry."; 323 | } else 324 | throw "Target entry is not a directory."; 325 | }, 326 | getFullname : function() { 327 | var that = this, fullname = that.name, entry = that.parent; 328 | while (entry) { 329 | fullname = (entry.name ? entry.name + "/" : "") + fullname; 330 | entry = entry.parent; 331 | } 332 | return fullname; 333 | }, 334 | isDescendantOf : function(ancestor) { 335 | var entry = this.parent; 336 | while (entry && entry.id != ancestor.id) 337 | entry = entry.parent; 338 | return !!entry; 339 | } 340 | }; 341 | ZipEntry.prototype.constructor = ZipEntry; 342 | 343 | var ZipFileEntryProto; 344 | 345 | function ZipFileEntry(fs, name, params, parent) { 346 | var that = this; 347 | ZipEntry.prototype.init.call(that, fs, name, params, parent); 348 | that.Reader = params.Reader; 349 | that.Writer = params.Writer; 350 | that.data = params.data; 351 | if (params.getData) { 352 | that.getData = params.getData; 353 | } 354 | } 355 | 356 | ZipFileEntry.prototype = ZipFileEntryProto = new ZipEntry(); 357 | ZipFileEntryProto.constructor = ZipFileEntry; 358 | ZipFileEntryProto.getData = function(writer, onend, onprogress, onerror) { 359 | var that = this; 360 | if (!writer || (writer.constructor == that.Writer && that.data)) 361 | onend(that.data); 362 | else { 363 | if (!that.reader) 364 | that.reader = new that.Reader(that.data, onerror); 365 | that.reader.init(function() { 366 | writer.init(function() { 367 | bufferedCopy(that.reader, writer, onend, onprogress, onerror); 368 | }, onerror); 369 | }); 370 | } 371 | }; 372 | 373 | ZipFileEntryProto.getText = function(onend, onprogress, checkCrc32, encoding) { 374 | this.getData(new TextWriter(encoding), onend, onprogress, checkCrc32); 375 | }; 376 | ZipFileEntryProto.getBlob = function(mimeType, onend, onprogress, checkCrc32) { 377 | this.getData(new BlobWriter(mimeType), onend, onprogress, checkCrc32); 378 | }; 379 | ZipFileEntryProto.getData64URI = function(mimeType, onend, onprogress, checkCrc32) { 380 | this.getData(new Data64URIWriter(mimeType), onend, onprogress, checkCrc32); 381 | }; 382 | 383 | var ZipDirectoryEntryProto; 384 | 385 | function ZipDirectoryEntry(fs, name, params, parent) { 386 | var that = this; 387 | ZipEntry.prototype.init.call(that, fs, name, params, parent); 388 | that.directory = true; 389 | } 390 | 391 | ZipDirectoryEntry.prototype = ZipDirectoryEntryProto = new ZipEntry(); 392 | ZipDirectoryEntryProto.constructor = ZipDirectoryEntry; 393 | ZipDirectoryEntryProto.addDirectory = function(name) { 394 | return addChild(this, name, null, true); 395 | }; 396 | ZipDirectoryEntryProto.addText = function(name, text) { 397 | return addChild(this, name, { 398 | data : text, 399 | Reader : TextReader, 400 | Writer : TextWriter 401 | }); 402 | }; 403 | ZipDirectoryEntryProto.addBlob = function(name, blob) { 404 | return addChild(this, name, { 405 | data : blob, 406 | Reader : BlobReader, 407 | Writer : BlobWriter 408 | }); 409 | }; 410 | ZipDirectoryEntryProto.addData64URI = function(name, dataURI) { 411 | return addChild(this, name, { 412 | data : dataURI, 413 | Reader : Data64URIReader, 414 | Writer : Data64URIWriter 415 | }); 416 | }; 417 | ZipDirectoryEntryProto.addFileEntry = function(fileEntry, onend, onerror) { 418 | addFileEntry(this, fileEntry, onend, onerror); 419 | }; 420 | ZipDirectoryEntryProto.addData = function(name, params) { 421 | return addChild(this, name, params); 422 | }; 423 | ZipDirectoryEntryProto.importBlob = function(blob, onend, onerror) { 424 | this.importZip(new BlobReader(blob), onend, onerror); 425 | }; 426 | ZipDirectoryEntryProto.importText = function(text, onend, onerror) { 427 | this.importZip(new TextReader(text), onend, onerror); 428 | }; 429 | ZipDirectoryEntryProto.importData64URI = function(dataURI, onend, onerror) { 430 | this.importZip(new Data64URIReader(dataURI), onend, onerror); 431 | }; 432 | ZipDirectoryEntryProto.exportBlob = function(onend, onprogress, onerror) { 433 | this.exportZip(new BlobWriter("application/zip"), onend, onprogress, onerror); 434 | }; 435 | ZipDirectoryEntryProto.exportText = function(onend, onprogress, onerror) { 436 | this.exportZip(new TextWriter(), onend, onprogress, onerror); 437 | }; 438 | ZipDirectoryEntryProto.exportFileEntry = function(fileEntry, onend, onprogress, onerror) { 439 | this.exportZip(new zip.FileWriter(fileEntry, "application/zip"), onend, onprogress, onerror); 440 | }; 441 | ZipDirectoryEntryProto.exportData64URI = function(onend, onprogress, onerror) { 442 | this.exportZip(new Data64URIWriter("application/zip"), onend, onprogress, onerror); 443 | }; 444 | ZipDirectoryEntryProto.importZip = function(reader, onend, onerror) { 445 | var that = this; 446 | createReader(reader, function(zipReader) { 447 | zipReader.getEntries(function(entries) { 448 | entries.forEach(function(entry) { 449 | var parent = that, path = entry.filename.split("/"), name = path.pop(); 450 | path.forEach(function(pathPart) { 451 | parent = parent.getChildByName(pathPart) || new ZipDirectoryEntry(that.fs, pathPart, null, parent); 452 | }); 453 | if (!entry.directory) 454 | addChild(parent, name, { 455 | data : entry, 456 | Reader : ZipBlobReader 457 | }); 458 | }); 459 | onend(); 460 | }); 461 | }, onerror); 462 | }; 463 | ZipDirectoryEntryProto.exportZip = function(writer, onend, onprogress, onerror) { 464 | var that = this; 465 | initReaders(that, function() { 466 | createWriter(writer, function(zipWriter) { 467 | exportZip(zipWriter, that, function() { 468 | zipWriter.close(onend); 469 | }, onprogress, getTotalSize(that)); 470 | }, onerror); 471 | }, onerror); 472 | }; 473 | ZipDirectoryEntryProto.getChildByName = function(name) { 474 | var childIndex, child, that = this; 475 | for (childIndex = 0; childIndex < that.children.length; childIndex++) { 476 | child = that.children[childIndex]; 477 | if (child.name == name) 478 | return child; 479 | } 480 | }; 481 | 482 | function FS() { 483 | resetFS(this); 484 | } 485 | FS.prototype = { 486 | remove : function(entry) { 487 | detach(entry); 488 | this.entries[entry.id] = null; 489 | }, 490 | find : function(fullname) { 491 | var index, path = fullname.split("/"), node = this.root; 492 | for (index = 0; node && index < path.length; index++) 493 | node = node.getChildByName(path[index]); 494 | return node; 495 | }, 496 | getById : function(id) { 497 | return this.entries[id]; 498 | }, 499 | importBlob : function(blob, onend, onerror) { 500 | resetFS(this); 501 | this.root.importBlob(blob, onend, onerror); 502 | }, 503 | importText : function(text, onend, onerror) { 504 | resetFS(this); 505 | this.root.importText(text, onend, onerror); 506 | }, 507 | importData64URI : function(dataURI, onend, onerror) { 508 | resetFS(this); 509 | this.root.importData64URI(dataURI, onend, onerror); 510 | }, 511 | exportBlob : function(onend, onprogress, onerror) { 512 | this.root.exportBlob(onend, onprogress, onerror); 513 | }, 514 | exportText : function(onend, onprogress, onerror) { 515 | this.root.exportText(onend, onprogress, onerror); 516 | }, 517 | exportFileEntry : function(fileEntry, onend, onprogress, onerror) { 518 | this.root.exportFileEntry(fileEntry, onend, onprogress, onerror); 519 | }, 520 | exportData64URI : function(onend, onprogress, onerror) { 521 | this.root.exportData64URI(onend, onprogress, onerror); 522 | } 523 | }; 524 | 525 | zip.getMimeType = function() { 526 | return "application/octet-stream"; 527 | }; 528 | 529 | export const fs = { 530 | FS : FS, 531 | ZipDirectoryEntry : ZipDirectoryEntry, 532 | ZipFileEntry : ZipFileEntry 533 | }; 534 | --------------------------------------------------------------------------------