phorge/webroot/rsrc/js/core/DragAndDropFileUpload.js
epriestley d2374c468f Fix missing willUpload hook for drag-and-drop chunked upload
Summary:
Ref T9324. When you upload a normal file, we call `willUpload` and then `didUpload`.

When you upload a chunked file, we never call `willUpload`. This can get things out of sync. Make sure we invoke this event for both chunked and non-chunked uploads.

Test Plan: Got cleaner behavior (redirect after all uploads finish) in new Phacility file upload area.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T9324

Differential Revision: https://secure.phabricator.com/D14083
2015-09-08 16:20:58 -07:00

440 lines
11 KiB
JavaScript

/**
* @requires javelin-install
* javelin-util
* javelin-request
* javelin-dom
* javelin-uri
* phabricator-file-upload
* @provides phabricator-drag-and-drop-file-upload
* @javelin
*/
JX.install('PhabricatorDragAndDropFileUpload', {
construct : function(node) {
this._node = node;
},
events : [
'didBeginDrag',
'didEndDrag',
'willUpload',
'progress',
'didUpload',
'didError'],
statics : {
isSupported : function() {
// TODO: Is there a better capability test for this? This seems okay in
// Safari, Firefox and Chrome.
return !!window.FileList;
},
isPasteSupported : function() {
// TODO: Needs to check if event.clipboardData is available.
// Works in Chrome, doesn't work in Firefox 10.
return !!window.FileList;
}
},
members : {
_node : null,
_depth : 0,
_isEnabled: false,
setIsEnabled: function(bool) {
this._isEnabled = bool;
return this;
},
getIsEnabled: function() {
return this._isEnabled;
},
_updateDepth : function(delta) {
if (this._depth === 0 && delta > 0) {
this.invoke('didBeginDrag');
}
this._depth += delta;
if (this._depth === 0 && delta < 0) {
this.invoke('didEndDrag');
}
},
start : function() {
// TODO: move this to JX.DOM.contains()?
function contains(container, child) {
do {
if (child === container) {
return true;
}
child = child.parentNode;
} while (child);
return false;
}
// Firefox has some issues sometimes; implement this click handler so
// the user can recover. See T5188.
JX.DOM.listen(
this._node,
'click',
null,
JX.bind(this, function (e) {
if (!this.getIsEnabled()) {
return;
}
if (this._depth) {
e.kill();
// Force depth to 0.
this._updateDepth(-this._depth);
}
}));
// We track depth so that the _node may have children inside of it and
// not become unselected when they are dragged over.
JX.DOM.listen(
this._node,
'dragenter',
null,
JX.bind(this, function(e) {
if (!this.getIsEnabled()) {
return;
}
if (contains(this._node, e.getTarget())) {
this._updateDepth(1);
}
}));
JX.DOM.listen(
this._node,
'dragleave',
null,
JX.bind(this, function(e) {
if (!this.getIsEnabled()) {
return;
}
if (contains(this._node, e.getTarget())) {
this._updateDepth(-1);
}
}));
JX.DOM.listen(
this._node,
'dragover',
null,
JX.bind(this, function(e) {
if (!this.getIsEnabled()) {
return;
}
// NOTE: We must set this, or Chrome refuses to drop files from the
// download shelf.
e.getRawEvent().dataTransfer.dropEffect = 'copy';
e.kill();
}));
JX.DOM.listen(
this._node,
'drop',
null,
JX.bind(this, function(e) {
if (!this.getIsEnabled()) {
return;
}
e.kill();
var files = e.getRawEvent().dataTransfer.files;
for (var ii = 0; ii < files.length; ii++) {
this._sendRequest(files[ii]);
}
// Force depth to 0.
this._updateDepth(-this._depth);
}));
if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported()) {
JX.DOM.listen(
this._node,
'paste',
null,
JX.bind(this, function(e) {
if (!this.getIsEnabled()) {
return;
}
var clipboard = e.getRawEvent().clipboardData;
if (!clipboard) {
return;
}
// If there's any text on the clipboard, just let the event fire
// normally, choosing the text over any images. See T5437 / D9647.
var text = clipboard.getData('text/plain').toString();
if (text.length) {
return;
}
// Safari and Firefox have clipboardData, but no items. They
// don't seem to provide a way to get image data directly yet.
if (!clipboard.items) {
return;
}
for (var ii = 0; ii < clipboard.items.length; ii++) {
var item = clipboard.items[ii];
if (!/^image\//.test(item.type)) {
continue;
}
var spec = item.getAsFile();
// pasted files don't have a name; see
// https://code.google.com/p/chromium/issues/detail?id=361145
if (!spec.name) {
spec.name = 'pasted_file';
}
this._sendRequest(spec);
}
}));
}
this.setIsEnabled(true);
},
_sendRequest : function(spec) {
var file = new JX.PhabricatorFileUpload()
.setRawFileObject(spec)
.setName(spec.name)
.setTotalBytes(spec.size);
var threshold = this.getChunkThreshold();
if (threshold && (file.getTotalBytes() > threshold)) {
// This is a large file, so we'll go through allocation so we can
// pick up support for resume and chunking.
this._allocateFile(file);
} else {
// If this file is smaller than the chunk threshold, skip the round
// trip for allocation and just upload it directly.
this._sendDataRequest(file);
}
},
_allocateFile: function(file) {
file
.setStatus('allocate')
.update();
this.invoke('willUpload', file);
var alloc_uri = this._getUploadURI(file)
.setQueryParam('allocate', 1);
new JX.Workflow(alloc_uri)
.setHandler(JX.bind(this, this._didAllocateFile, file))
.start();
},
_getUploadURI: function(file) {
var uri = JX.$U(this.getURI())
.setQueryParam('name', file.getName())
.setQueryParam('length', file.getTotalBytes());
if (this.getViewPolicy()) {
uri.setQueryParam('viewPolicy', this.getViewPolicy());
}
if (file.getAllocatedPHID()) {
uri.setQueryParam('phid', file.getAllocatedPHID());
}
return uri;
},
_didAllocateFile: function(file, r) {
var phid = r.phid;
var upload = r.upload;
if (!upload) {
if (phid) {
this._completeUpload(file, r);
} else {
this._failUpload(file, r);
}
return;
} else {
if (phid) {
// Start or resume a chunked upload.
file.setAllocatedPHID(phid);
this._loadChunks(file);
} else {
// Proceed with non-chunked upload.
this._sendDataRequest(file);
}
}
},
_loadChunks: function(file) {
file
.setStatus('chunks')
.update();
var chunks_uri = this._getUploadURI(file)
.setQueryParam('querychunks', 1);
new JX.Workflow(chunks_uri)
.setHandler(JX.bind(this, this._didLoadChunks, file))
.start();
},
_didLoadChunks: function(file, r) {
file.setChunks(r);
this._uploadNextChunk(file);
},
_uploadNextChunk: function(file) {
var chunks = file.getChunks();
var chunk;
for (var ii = 0; ii < chunks.length; ii++) {
chunk = chunks[ii];
if (!chunk.complete) {
this._uploadChunk(file, chunk);
break;
}
}
},
_uploadChunk: function(file, chunk, callback) {
file
.setStatus('upload')
.update();
var chunkup_uri = this._getUploadURI(file)
.setQueryParam('uploadchunk', 1)
.setQueryParam('__upload__', 1)
.setQueryParam('byteStart', chunk.byteStart)
.toString();
var callback = JX.bind(this, this._didUploadChunk, file, chunk);
var req = new JX.Request(chunkup_uri, callback);
var seen_bytes = 0;
var onprogress = JX.bind(this, function(progress) {
file
.addUploadedBytes(progress.loaded - seen_bytes)
.update();
seen_bytes = progress.loaded;
this.invoke('progress', file);
});
req.listen('error', JX.bind(this, this._onUploadError, req, file));
req.listen('uploadprogress', onprogress);
var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd);
req
.setRawData(blob)
.send();
},
_didUploadChunk: function(file, chunk, r) {
file.didCompleteChunk(chunk);
if (r.complete) {
this._completeUpload(file, r);
} else {
this._uploadNextChunk(file);
}
},
_sendDataRequest: function(file) {
file
.setStatus('uploading')
.update();
this.invoke('willUpload', file);
var up_uri = this._getUploadURI(file)
.setQueryParam('__upload__', 1)
.toString();
var onupload = JX.bind(this, function(r) {
if (r.error) {
this._failUpload(file, r);
} else {
this._completeUpload(file, r);
}
});
var req = new JX.Request(up_uri, onupload);
var onprogress = JX.bind(this, function(progress) {
file
.setTotalBytes(progress.total)
.setUploadedBytes(progress.loaded)
.update();
this.invoke('progress', file);
});
req.listen('error', JX.bind(this, this._onUploadError, req, file));
req.listen('uploadprogress', onprogress);
req
.setRawData(file.getRawFileObject())
.send();
},
_completeUpload: function(file, r) {
file
.setID(r.id)
.setPHID(r.phid)
.setURI(r.uri)
.setMarkup(r.html)
.setStatus('done')
.update();
this.invoke('didUpload', file);
},
_failUpload: function(file, r) {
file
.setStatus('error')
.setError(r.error)
.update();
this.invoke('didError', file);
},
_onUploadError: function(req, file, error) {
file.setStatus('error');
if (error) {
file.setError(error.code + ': ' + error.info);
} else {
var xhr = req.getTransport();
if (xhr.responseText) {
file.setError('Server responded: ' + xhr.responseText);
}
}
file.update();
this.invoke('didError', file);
}
},
properties: {
URI: null,
activatedClass: null,
viewPolicy: null,
chunkThreshold: null
}
});