UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

255 lines (223 loc) 8.7 kB
/** * Fork of: https://github.com/donmccurdy/simple-dropzone updated: Mar 2021 * The MIT License (MIT) * Copyright (c) 2018 Don McCurdy * * Changes: * Convert to typescript. * webkitRelativePath for file input select. * Removed unzip and dependency(done in importer). * * Watches an element for file drops, parses to create a filemap hierarchy, * and emits the result. */ export class Dropzone { get inputEl(): HTMLInputElement|undefined { return this._inputEl } get el(): HTMLElement|undefined { return this._el } private _el?: HTMLElement private _inputEl?: HTMLInputElement private _listeners: Record<DropEventType, ListenerCallback[]> constructor(el?: HTMLElement, inputEl?: HTMLInputElement, listeners?: Partial<Record<DropEventType, ListenerCallback>>) { this._el = el this._inputEl = inputEl this._listeners = { drop: [], dropstart: [], droperror: [], } this._onDragover = this._onDragover.bind(this) this._onDrop = this._onDrop.bind(this) this._onSelect = this._onSelect.bind(this) el?.addEventListener('dragover', this._onDragover, false) el?.addEventListener('drop', this._onDrop, false) inputEl?.addEventListener('change', this._onSelect) listeners && Object.entries(listeners).forEach(([e, c])=> c && this.on(e as DropEventType, c)) } on(type: DropEventType, callback: ListenerCallback): Dropzone { this._listeners[type].push(callback) return this } private _emit(type: DropEventType, data?: {[id:string]: any}) { this._listeners[type] .forEach((callback) => callback(data)) return this } /** * Destroys the instance. */ destroy(): void { const el = this._el const inputEl = this._inputEl el?.removeEventListener('dragover', this._onDragover) el?.removeEventListener('drop', this._onDrop) inputEl?.removeEventListener('change', this._onSelect) } /** * Use dataTransfer.items when available instead of dataTransfer.files (when files are dropped) * * Set to false to use dataTransfer.files only. * This is useful for environments where files cannot be read from FileSystemEntry like in figma plugins/widgets. */ static USE_DATA_TRANSFER_ITEMS = true /** * References (and horror): * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items * - https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files * - https://code.flickr.net/2012/12/10/drag-n-drop/ * - https://stackoverflow.com/q/44842247/1314762 * */ private _onDrop(e: DragEvent) { e.stopPropagation() e.preventDefault() this._emit('dropstart') const files = Array.from(e.dataTransfer?.files || []) as DropFile[] const items = Array.from(e.dataTransfer?.items || []) if (files.length === 0 && items.length === 0) { this._fail('Required drag-and-drop APIs are not supported in this browser.') return } // Prefer .items, which allow folder traversal if necessary. if (Dropzone.USE_DATA_TRANSFER_ITEMS && items.length > 0) { const entries = items.map((item) => item.webkitGetAsEntry()) // if (entries[0].name.match(/\.zip$/)) { // this._loadZip(items[0].getAsFile()) // } else { this._loadNextEntry(new Map(), entries, e) // } return } // Fall back to .files, since folders can't be traversed. // if (files.length === 1 && files[0].name.match(/\.zip$/)) { // this._loadZip(files[0]) // } this._emit('drop', { nativeEvent: e, files: new Map(files.map((file) => { file.filePath = file.name return [file.filePath, file] })), }) } /** * @param {Event} e */ private _onDragover(e: DragEvent) { e.stopPropagation() e.preventDefault() e.dataTransfer && (e.dataTransfer.dropEffect = 'copy') // Explicitly show this is a copy. } private _onSelect(e: Event) { if (!this._inputEl) { console.warn('Invalid Dropzone event ', e) return } this._emit('dropstart') // HTML file inputs do not seem to support folders, so assume this is a flat file list. const files: DropFile[] = [].slice.call(this._inputEl.files ?? new FileList()) // Automatically decompress a zip archive if it is the only file given. // if (files.length === 1 && this._isZip(files[0])) { // this._loadZip(files[0]) // return // } const fileMap = new Map() files.forEach((file) => { file.filePath = (file as any).webkitRelativePath || file.name fileMap.set(file.filePath, file) }) this._emit('drop', {files: fileMap, nativeEvent: e}) } /** * Iterates through a list of FileSystemEntry objects, creates the fileMap * tree, and emits the result. * @param fileMap * @param {Array<FileSystemEntry>} entries * @param e */ private _loadNextEntry(fileMap: Map<string, DropFile>, entries: (FileSystemEntry|null)[], e: DragEvent) { const entry = entries.pop() if (!entry) { this._emit('drop', {files: fileMap, nativeEvent: e}) return } if (entry.isFile) { (entry as FileSystemFileEntry).file((file: DropFile) => { file.filePath = entry.fullPath fileMap.set(entry.fullPath, file) this._loadNextEntry(fileMap, entries, e) }, (err) => console.error('Could not load file: %s', entry.fullPath, err, 'Try setting Dropzone.USE_DATA_TRANSFER_ITEMS to false.')) } else if (entry.isDirectory) { // readEntries() must be called repeatedly until it stops returning results. // https://www.w3.org/TR/2012/WD-file-system-api-20120417/#the-directoryreader-interface // https://bugs.chromium.org/p/chromium/issues/detail?id=378883 const reader = (entry as FileSystemDirectoryEntry).createReader() const readerCallback = (newEntries: any[]) => { if (newEntries.length) { entries = entries.concat(newEntries) // eslint-disable-next-line @typescript-eslint/no-unsafe-call reader.readEntries(readerCallback) } else { this._loadNextEntry(fileMap, entries, e) } } // eslint-disable-next-line @typescript-eslint/no-unsafe-call reader.readEntries(readerCallback) } else { console.warn('Unknown asset type: ' + entry.fullPath) this._loadNextEntry(fileMap, entries, e) } } // /** // * Inflates a File in .ZIP format, creates the fileMap tree, and emits the // * result. // * @param {File} file // */ // _loadZip(file) { // const pending = [] // const fileMap = new Map() // const archive = new fs.FS() // // const traverse = (node) => { // if (node.directory) { // node.children.forEach(traverse) // } else if (node.name[0] !== '.') { // pending.push(new Promise((resolve) => { // node.getData(new zip.BlobWriter(), (blob) => { // blob.name = node.name // fileMap.set(node.getFullname(), blob) // resolve() // }) // })) // } // } // // archive.importBlob(file, () => { // traverse(archive.root) // Promise.all(pending).then(() => { // this._emit('drop', {files: fileMap, archive: file}) // }) // }) // } // /** // * @param {File} file // * @return {Boolean} // */ // _isZip(file) { // return file.type === 'application/zip' || file.name.match(/\.zip$/) // } /** * @throws */ private _fail(message: string) { this._emit('droperror', {message: message}) } } export type DropEventType = 'drop'|'dropstart'|'droperror' export type ListenerCallback = ((data?:{files?:Map<string, DropFile>, message?:string})=>void) export interface DropFile extends File{ filePath: string }