file-drops
Version:
A simple in-browser file drop utility
244 lines (178 loc) • 4.87 kB
JavaScript
;
var minDom = require('min-dom');
var OVERLAY_HTML = '<div class="drop-overlay">' +
'<div class="box">' +
'<div class="label">{label}</div>' +
'</div>' +
'</div>';
/**
* @typedef { {
* name: string,
* path: string | undefined,
* contents: string
* } } File
*/
/**
* Add file drop functionality to the given element,
* calling fn(files...) on drop.
*
* @example
*
* var node = document.querySelector('#container');
*
* var dragOverHandler = fileDrop(handleFiles);
*
* node.addEventListener('dragover', dragOverHandler);
*
* @param { string } [label='Drop files here']
* @param { (files: File[], dropEvent: DragEvent) => void } fn
*
* @return { (event: DragEvent) => any } drag over handler
*/
function fileDrop(label, fn) {
if (typeof label === 'function') {
fn = label;
label = 'Drop files here';
}
var self;
var extraArgs;
// we are bound, if overlay exists
var overlay;
/**
* @param { DragEvent } event
*/
function onDrop(event) {
event.preventDefault();
asyncMap(event.dataTransfer.files, readFile, function(err, files) {
if (err) {
console.warn('file drop failed', err);
} else {
var args = extraArgs.concat([ files, event ]);
// cleanup on drop
// onEnd(event);
// call provided fn with extraArgs..., files, event
fn.apply(self, args);
}
});
}
function isDragAllowed(dataTransfer) {
// Universal check for all browsers
return dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files');
}
/**
* Drag over event to be registered by clients in respective contexts
*
* @param { DragEvent } _event
*/
function onDragover(_event) {
// (0) extract extra arguments (extraArgs..., event)
var args = slice(arguments);
/**
* @type {DragEvent}
*/
var event = args.pop();
var dataTransfer = event.dataTransfer,
target = event.currentTarget || event.target;
if (!isDragAllowed(dataTransfer)) {
return;
}
// make us a drop zone
event.preventDefault();
dataTransfer.dropEffect = 'copy';
// only register if we do not drag and drop already
if (overlay) {
return;
}
overlay = createOverlay(label);
target.appendChild(overlay);
self = this;
extraArgs = args;
// do not register events during testing
if (!target) {
return;
}
// (2) setup drag listeners
function onLeave(event) {
var relatedTarget = event.relatedTarget;
if (target.contains(relatedTarget)) {
return;
}
onEnd();
}
// (2.1) detach on end
function onEnd(event) {
document.removeEventListener('drop', onDrop);
document.removeEventListener('drop', onEnd);
document.removeEventListener('dragleave', onLeave);
document.removeEventListener('dragend', onEnd);
document.removeEventListener('dragover', preventDrop);
if (overlay) {
target.removeChild(overlay);
overlay = null;
}
}
// (2.0) attach drag + cleanup event
document.addEventListener('drop', onDrop);
document.addEventListener('drop', onEnd);
document.addEventListener('dragleave', onLeave);
document.addEventListener('dragend', onEnd);
document.addEventListener('dragover', preventDrop);
}
onDragover.onDrop = onDrop;
return onDragover;
}
// helpers ////////////////////////////////////
function readFile(dropFile, done) {
if (!window.FileReader) {
return done();
}
var reader = new FileReader();
// Closure to capture the file information.
reader.onload = function(e) {
done(null, {
name: dropFile.name,
path: dropFile.path,
contents: e.target.result
});
};
reader.onerror = function(event) {
done(event.target.error);
};
// Read in the image file as a data URL.
reader.readAsText(dropFile);
}
function asyncMap(elements, iterator, done) {
var idx = 0,
results = [];
function next() {
if (idx === elements.length) {
done(null, results);
} else {
iterator(elements[idx], function(err, result) {
if (err) {
return done(err);
} else {
results[idx] = result;
idx++;
next();
}
});
}
}
next();
}
function slice(arr) {
return Array.prototype.slice.call(arr);
}
function createOverlay(label) {
var markup = OVERLAY_HTML.replace('{label}', label);
const overlay = minDom.domify(markup);
// remove blinking on safari
overlay.style.pointerEvents = 'none';
return overlay;
}
function preventDrop(event) {
event.preventDefault();
}
module.exports = fileDrop;
//# sourceMappingURL=index.cjs.map