325 lines
9.3 KiB
JavaScript
325 lines
9.3 KiB
JavaScript
/**********************************
|
|
Directory Upload Proposal Polyfill
|
|
Author: Ali Alabbas (Microsoft)
|
|
**********************************/
|
|
(function() {
|
|
// Do not proceed with the polyfill if Directory interface is already natively available,
|
|
// or if webkitdirectory is not supported (i.e. not Chrome, since the polyfill only works in Chrome)
|
|
if (window.Directory || !('webkitdirectory' in document.createElement('input') && 'webkitGetAsEntry' in DataTransferItem.prototype)) {
|
|
return;
|
|
}
|
|
|
|
var allowdirsAttr = 'allowdirs',
|
|
getFilesMethod = 'getFilesAndDirectories',
|
|
isSupportedProp = 'isFilesAndDirectoriesSupported',
|
|
chooseDirMethod = 'chooseDirectory';
|
|
|
|
var separator = '/';
|
|
|
|
var Directory = function() {
|
|
this.name = '';
|
|
this.path = separator;
|
|
this._children = {};
|
|
this._items = false;
|
|
};
|
|
|
|
Directory.prototype[getFilesMethod] = function() {
|
|
var that = this;
|
|
|
|
// from drag and drop and file input drag and drop (webkitEntries)
|
|
if (this._items) {
|
|
var getItem = function(entry) {
|
|
if (entry.isDirectory) {
|
|
var dir = new Directory();
|
|
dir.name = entry.name;
|
|
dir.path = entry.fullPath;
|
|
dir._items = entry;
|
|
|
|
return dir;
|
|
} else {
|
|
return new Promise(function(resolve, reject) {
|
|
entry.file(function(file) {
|
|
resolve(file);
|
|
}, reject);
|
|
});
|
|
}
|
|
};
|
|
|
|
if (this.path === separator) {
|
|
var promises = [];
|
|
|
|
for (var i = 0; i < this._items.length; i++) {
|
|
var entry;
|
|
|
|
// from file input drag and drop (webkitEntries)
|
|
if (this._items[i].isDirectory || this._items[i].isFile) {
|
|
entry = this._items[i];
|
|
} else {
|
|
entry = this._items[i].webkitGetAsEntry();
|
|
}
|
|
|
|
promises.push(getItem(entry));
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
} else {
|
|
return new Promise(function(resolve, reject) {
|
|
var dirReader = that._items.createReader();
|
|
var promises = [];
|
|
|
|
var readEntries = function() {
|
|
dirReader.readEntries(function(entries) {
|
|
if (!entries.length) {
|
|
resolve(Promise.all(promises));
|
|
} else {
|
|
for (var i = 0; i < entries.length; i++) {
|
|
promises.push(getItem(entries[i]));
|
|
}
|
|
|
|
readEntries();
|
|
}
|
|
}, reject);
|
|
};
|
|
|
|
readEntries();
|
|
});
|
|
}
|
|
// from file input manual selection
|
|
} else {
|
|
var arr = [];
|
|
|
|
for (var child in this._children) {
|
|
arr.push(this._children[child]);
|
|
}
|
|
|
|
return Promise.resolve(arr);
|
|
}
|
|
};
|
|
|
|
// set blank as default for all inputs
|
|
HTMLInputElement.prototype[getFilesMethod] = function() {
|
|
return Promise.resolve([]);
|
|
};
|
|
|
|
// if OS is Mac, the combined directory and file picker is supported
|
|
HTMLInputElement.prototype[isSupportedProp] = navigator.appVersion.indexOf("Mac") !== -1;
|
|
|
|
HTMLInputElement.prototype[allowdirsAttr] = undefined;
|
|
HTMLInputElement.prototype[chooseDirMethod] = undefined;
|
|
|
|
// expose Directory interface to window
|
|
window.Directory = Directory;
|
|
|
|
/********************
|
|
**** File Input ****
|
|
********************/
|
|
var convertInputs = function(nodes) {
|
|
var recurse = function(dir, path, fullPath, file) {
|
|
var pathPieces = path.split(separator);
|
|
var dirName = pathPieces.shift();
|
|
|
|
if (pathPieces.length > 0) {
|
|
var subDir = new Directory();
|
|
subDir.name = dirName;
|
|
subDir.path = separator + fullPath;
|
|
|
|
if (!dir._children[subDir.name]) {
|
|
dir._children[subDir.name] = subDir;
|
|
}
|
|
|
|
recurse(dir._children[subDir.name], pathPieces.join(separator), fullPath, file);
|
|
} else {
|
|
dir._children[file.name] = file;
|
|
}
|
|
};
|
|
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var node = nodes[i];
|
|
|
|
if (node.tagName === 'INPUT' && node.type === 'file') {
|
|
var getFiles = function() {
|
|
var files = node.files;
|
|
|
|
if (draggedAndDropped) {
|
|
files = node.webkitEntries;
|
|
draggedAndDropped = false;
|
|
} else {
|
|
if (files.length === 0) {
|
|
files = node.shadowRoot.querySelector('#input1').files;
|
|
|
|
if (files.length === 0) {
|
|
files = node.shadowRoot.querySelector('#input2').files;
|
|
|
|
if (files.length === 0) {
|
|
files = node.webkitEntries;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return files;
|
|
};
|
|
|
|
var draggedAndDropped = false;
|
|
|
|
node.addEventListener('drop', function(e) {
|
|
draggedAndDropped = true;
|
|
}, false);
|
|
|
|
if (node.hasAttribute(allowdirsAttr)) {
|
|
// force multiple selection for default behavior
|
|
if (!node.hasAttribute('multiple')) {
|
|
node.setAttribute('multiple', '');
|
|
}
|
|
|
|
var shadow = node.createShadowRoot();
|
|
|
|
node[chooseDirMethod] = function() {
|
|
// can't do this without an actual click
|
|
console.log('This is unsupported. For security reasons the dialog cannot be triggered unless it is a response to some user triggered event such as a click on some other element.');
|
|
};
|
|
|
|
shadow.innerHTML = '<div style="border: 1px solid #999; padding: 3px; width: 235px; box-sizing: content-box; font-size: 14px; height: 21px;">'
|
|
+ '<div id="fileButtons" style="box-sizing: content-box;">'
|
|
+ '<button id="button1" style="width: 100px; box-sizing: content-box;">Choose file(s)...</button>'
|
|
+ '<button id="button2" style="width: 100px; box-sizing: content-box; margin-left: 3px;">Choose folder...</button>'
|
|
+ '</div>'
|
|
+ '<div id="filesChosen" style="padding: 3px; display: none; box-sizing: content-box;"><span id="filesChosenText">files selected...</span>'
|
|
+ '<a id="clear" title="Clear selection" href="javascript:;" style="text-decoration: none; float: right; margin: -3px -1px 0 0; padding: 3px; font-weight: bold; font-size: 16px; color:#999; box-sizing: content-box;">×</a>'
|
|
+ '</div>'
|
|
+ '</div>'
|
|
+ '<input id="input1" type="file" multiple style="display: none;">'
|
|
+ '<input id="input2" type="file" webkitdirectory style="display: none;">'
|
|
+ '</div>';
|
|
|
|
shadow.querySelector('#button1').onclick = function(e) {
|
|
e.preventDefault();
|
|
|
|
shadow.querySelector('#input1').click();
|
|
};
|
|
|
|
shadow.querySelector('#button2').onclick = function(e) {
|
|
e.preventDefault();
|
|
|
|
shadow.querySelector('#input2').click();
|
|
};
|
|
|
|
var toggleView = function(defaultView, filesLength) {
|
|
shadow.querySelector('#fileButtons').style.display = defaultView ? 'block' : 'none';
|
|
shadow.querySelector('#filesChosen').style.display = defaultView ? 'none' : 'block';
|
|
|
|
if (!defaultView) {
|
|
shadow.querySelector('#filesChosenText').innerText = filesLength + ' file' + (filesLength > 1 ? 's' : '') + ' selected...';
|
|
}
|
|
};
|
|
|
|
var changeHandler = function(e) {
|
|
node.dispatchEvent(new Event('change'));
|
|
|
|
toggleView(false, getFiles().length);
|
|
};
|
|
|
|
shadow.querySelector('#input1').onchange = shadow.querySelector('#input2').onchange = changeHandler;
|
|
|
|
var clear = function (e) {
|
|
toggleView(true);
|
|
|
|
var form = document.createElement('form');
|
|
node.parentNode.insertBefore(form, node);
|
|
node.parentNode.removeChild(node);
|
|
form.appendChild(node);
|
|
form.reset();
|
|
|
|
form.parentNode.insertBefore(node, form);
|
|
form.parentNode.removeChild(form);
|
|
|
|
// reset does not instantly occur, need to give it some time
|
|
setTimeout(function() {
|
|
node.dispatchEvent(new Event('change'));
|
|
}, 1);
|
|
};
|
|
|
|
shadow.querySelector('#clear').onclick = clear;
|
|
}
|
|
|
|
node.addEventListener('change', function() {
|
|
var dir = new Directory();
|
|
|
|
var files = getFiles();
|
|
|
|
if (files.length > 0) {
|
|
if (node.hasAttribute(allowdirsAttr)) {
|
|
toggleView(false, files.length);
|
|
}
|
|
|
|
// from file input drag and drop (webkitEntries)
|
|
if (files[0].isFile || files[0].isDirectory) {
|
|
dir._items = files;
|
|
} else {
|
|
for (var j = 0; j < files.length; j++) {
|
|
var file = files[j];
|
|
var path = file.webkitRelativePath;
|
|
var fullPath = path.substring(0, path.lastIndexOf(separator));
|
|
|
|
recurse(dir, path, fullPath, file);
|
|
}
|
|
}
|
|
} else if (node.hasAttribute(allowdirsAttr)) {
|
|
toggleView(true, files.length);
|
|
}
|
|
|
|
this[getFilesMethod] = function() {
|
|
return dir[getFilesMethod]();
|
|
};
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// polyfill file inputs when the DOM loads
|
|
document.addEventListener('DOMContentLoaded', function(event) {
|
|
convertInputs(document.getElementsByTagName('input'));
|
|
});
|
|
|
|
// polyfill file inputs that are created dynamically and inserted into the body
|
|
var observer = new MutationObserver(function(mutations, observer) {
|
|
for (var i = 0; i < mutations.length; i++) {
|
|
if (mutations[i].addedNodes.length > 0) {
|
|
convertInputs(mutations[i].addedNodes);
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {childList: true, subtree: true});
|
|
|
|
/***********************
|
|
**** Drag and drop ****
|
|
***********************/
|
|
// keep a reference to the original method
|
|
var _addEventListener = EventTarget.prototype.addEventListener;
|
|
|
|
DataTransfer.prototype[getFilesMethod] = function() {
|
|
return Promise.resolve([]);
|
|
};
|
|
|
|
EventTarget.prototype.addEventListener = function(type, listener, useCapture) {
|
|
if (type === 'drop') {
|
|
var _listener = listener;
|
|
|
|
listener = function(e) {
|
|
var dir = new Directory();
|
|
dir._items = e.dataTransfer.items;
|
|
|
|
e.dataTransfer[getFilesMethod] = function() {
|
|
return dir[getFilesMethod]();
|
|
};
|
|
|
|
_listener(e);
|
|
};
|
|
}
|
|
|
|
// call the original method
|
|
return _addEventListener.apply(this, arguments);
|
|
};
|
|
}());
|