Path: blob/master/webroot/rsrc/js/core/DragAndDropFileUpload.js
12241 views
/**1* @requires javelin-install2* javelin-util3* javelin-request4* javelin-dom5* javelin-uri6* phabricator-file-upload7* @provides phabricator-drag-and-drop-file-upload8* @javelin9*/1011JX.install('PhabricatorDragAndDropFileUpload', {1213construct : function(target) {14if (JX.DOM.isNode(target)) {15this._node = target;16} else {17this._sigil = target;18}19},2021events : [22'didBeginDrag',23'didEndDrag',24'willUpload',25'progress',26'didUpload',27'didError'],2829statics : {30isSupported : function() {31// TODO: Is there a better capability test for this? This seems okay in32// Safari, Firefox and Chrome.3334return !!window.FileList;35},36isPasteSupported : function() {37// TODO: Needs to check if event.clipboardData is available.38// Works in Chrome, doesn't work in Firefox 10.39return !!window.FileList;40}41},4243members : {44_node : null,45_sigil: null,46_depth : 0,47_isEnabled: false,4849setIsEnabled: function(bool) {50this._isEnabled = bool;51return this;52},5354getIsEnabled: function() {55return this._isEnabled;56},5758_updateDepth : function(delta) {59if (this._depth === 0 && delta > 0) {60this.invoke('didBeginDrag', this._getTarget());61}6263this._depth += delta;6465if (this._depth === 0 && delta < 0) {66this.invoke('didEndDrag', this._getTarget());67}68},6970_getTarget: function() {71return this._target || this._node;72},7374start : function() {7576// TODO: move this to JX.DOM.contains()?77function contains(container, child) {78do {79if (child === container) {80return true;81}82child = child.parentNode;83} while (child);8485return false;86}8788// Firefox has some issues sometimes; implement this click handler so89// the user can recover. See T5188.90var on_click = JX.bind(this, function (e) {91if (!this.getIsEnabled()) {92return;93}9495if (this._depth) {96e.kill();97// Force depth to 0.98this._updateDepth(-this._depth);99}100});101102// We track depth so that the _node may have children inside of it and103// not become unselected when they are dragged over.104var on_dragenter = JX.bind(this, function(e) {105if (!this.getIsEnabled()) {106return;107}108109if (!this._node) {110var target = e.getNode(this._sigil);111if (target !== this._target) {112this._updateDepth(-this._depth);113this._target = target;114}115}116117if (contains(this._getTarget(), e.getTarget())) {118this._updateDepth(1);119}120121});122123var on_dragleave = JX.bind(this, function(e) {124if (!this.getIsEnabled()) {125return;126}127128if (!this._getTarget()) {129return;130}131132if (contains(this._getTarget(), e.getTarget())) {133this._updateDepth(-1);134}135});136137var on_dragover = JX.bind(this, function(e) {138if (!this.getIsEnabled()) {139return;140}141142// NOTE: We must set this, or Chrome refuses to drop files from the143// download shelf.144e.getRawEvent().dataTransfer.dropEffect = 'copy';145e.kill();146});147148var on_drop = JX.bind(this, function(e) {149if (!this.getIsEnabled()) {150return;151}152153e.kill();154155var files = e.getRawEvent().dataTransfer.files;156for (var ii = 0; ii < files.length; ii++) {157this.sendRequest(files[ii]);158}159160// Force depth to 0.161this._updateDepth(-this._depth);162});163164if (this._node) {165JX.DOM.listen(this._node, 'click', null, on_click);166JX.DOM.listen(this._node, 'dragenter', null, on_dragenter);167JX.DOM.listen(this._node, 'dragleave', null, on_dragleave);168JX.DOM.listen(this._node, 'dragover', null, on_dragover);169JX.DOM.listen(this._node, 'drop', null, on_drop);170} else {171JX.Stratcom.listen('click', this._sigil, on_click);172JX.Stratcom.listen('dragenter', this._sigil, on_dragenter);173JX.Stratcom.listen('dragleave', this._sigil, on_dragleave);174JX.Stratcom.listen('dragover', this._sigil, on_dragover);175JX.Stratcom.listen('drop', this._sigil, on_drop);176}177178if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported() &&179this._node) {180JX.DOM.listen(181this._node,182'paste',183null,184JX.bind(this, function(e) {185if (!this.getIsEnabled()) {186return;187}188189var clipboard = e.getRawEvent().clipboardData;190if (!clipboard) {191return;192}193194// If there's any text on the clipboard, just let the event fire195// normally, choosing the text over any images. See T5437 / D9647.196var text = clipboard.getData('text/plain').toString();197if (text.length) {198return;199}200201// Safari and Firefox have clipboardData, but no items. They202// don't seem to provide a way to get image data directly yet.203if (!clipboard.items) {204return;205}206207for (var ii = 0; ii < clipboard.items.length; ii++) {208var item = clipboard.items[ii];209if (!/^image\//.test(item.type)) {210continue;211}212var spec = item.getAsFile();213// pasted files don't have a name; see214// https://code.google.com/p/chromium/issues/detail?id=361145215if (!spec.name) {216spec.name = 'pasted_file';217}218this.sendRequest(spec);219}220}));221}222223this.setIsEnabled(true);224},225226sendRequest : function(spec) {227var file = new JX.PhabricatorFileUpload()228.setRawFileObject(spec)229.setName(spec.name)230.setTotalBytes(spec.size);231232var threshold = this.getChunkThreshold();233if (threshold && (file.getTotalBytes() > threshold)) {234// This is a large file, so we'll go through allocation so we can235// pick up support for resume and chunking.236this._allocateFile(file);237} else {238// If this file is smaller than the chunk threshold, skip the round239// trip for allocation and just upload it directly.240this._sendDataRequest(file);241}242},243244_allocateFile: function(file) {245file246.setStatus('allocate')247.update();248249this.invoke('willUpload', file);250251var alloc_uri = this._getUploadURI(file)252.setQueryParam('allocate', 1);253254new JX.Workflow(alloc_uri)255.setHandler(JX.bind(this, this._didAllocateFile, file))256.start();257},258259_getUploadURI: function(file) {260var uri = JX.$U(this.getURI())261.setQueryParam('name', file.getName())262.setQueryParam('length', file.getTotalBytes());263264if (this.getViewPolicy()) {265uri.setQueryParam('viewPolicy', this.getViewPolicy());266}267268if (file.getAllocatedPHID()) {269uri.setQueryParam('phid', file.getAllocatedPHID());270}271272return uri;273},274275_didAllocateFile: function(file, r) {276var phid = r.phid;277var upload = r.upload;278279if (!upload) {280if (phid) {281this._completeUpload(file, r);282} else {283this._failUpload(file, r);284}285return;286} else {287if (phid) {288// Start or resume a chunked upload.289file.setAllocatedPHID(phid);290this._loadChunks(file);291} else {292// Proceed with non-chunked upload.293this._sendDataRequest(file);294}295}296},297298_loadChunks: function(file) {299file300.setStatus('chunks')301.update();302303var chunks_uri = this._getUploadURI(file)304.setQueryParam('querychunks', 1);305306new JX.Workflow(chunks_uri)307.setHandler(JX.bind(this, this._didLoadChunks, file))308.start();309},310311_didLoadChunks: function(file, r) {312file.setChunks(r);313this._uploadNextChunk(file);314},315316_uploadNextChunk: function(file) {317var chunks = file.getChunks();318var chunk;319for (var ii = 0; ii < chunks.length; ii++) {320chunk = chunks[ii];321if (!chunk.complete) {322this._uploadChunk(file, chunk);323break;324}325}326},327328_uploadChunk: function(file, chunk, callback) {329file330.setStatus('upload')331.update();332333var chunkup_uri = this._getUploadURI(file)334.setQueryParam('uploadchunk', 1)335.setQueryParam('__upload__', 1)336.setQueryParam('byteStart', chunk.byteStart)337.toString();338339var callback = JX.bind(this, this._didUploadChunk, file, chunk);340341var req = new JX.Request(chunkup_uri, callback);342343var seen_bytes = 0;344var onprogress = JX.bind(this, function(progress) {345file346.addUploadedBytes(progress.loaded - seen_bytes)347.update();348349seen_bytes = progress.loaded;350this.invoke('progress', file);351});352353req.listen('error', JX.bind(this, this._onUploadError, req, file));354req.listen('uploadprogress', onprogress);355356var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd);357358req359.setRawData(blob)360.send();361},362363_didUploadChunk: function(file, chunk, r) {364file.didCompleteChunk(chunk);365366if (r.complete) {367this._completeUpload(file, r);368} else {369this._uploadNextChunk(file);370}371},372373_sendDataRequest: function(file) {374file375.setStatus('uploading')376.update();377378this.invoke('willUpload', file);379380var up_uri = this._getUploadURI(file)381.setQueryParam('__upload__', 1)382.toString();383384var onupload = JX.bind(this, function(r) {385if (r.error) {386this._failUpload(file, r);387} else {388this._completeUpload(file, r);389}390});391392var req = new JX.Request(up_uri, onupload);393394var onprogress = JX.bind(this, function(progress) {395file396.setTotalBytes(progress.total)397.setUploadedBytes(progress.loaded)398.update();399400this.invoke('progress', file);401});402403req.listen('error', JX.bind(this, this._onUploadError, req, file));404req.listen('uploadprogress', onprogress);405406req407.setRawData(file.getRawFileObject())408.send();409},410411_completeUpload: function(file, r) {412file413.setID(r.id)414.setPHID(r.phid)415.setURI(r.uri)416.setMarkup(r.html)417.setStatus('done')418.setTargetNode(this._getTarget())419.update();420421this.invoke('didUpload', file);422},423424_failUpload: function(file, r) {425file426.setStatus('error')427.setError(r.error)428.update();429430this.invoke('didError', file);431},432433_onUploadError: function(req, file, error) {434file.setStatus('error');435436if (error) {437file.setError(error.code + ': ' + error.info);438} else {439var xhr = req.getTransport();440if (xhr.responseText) {441file.setError('Server responded: ' + xhr.responseText);442}443}444445file.update();446this.invoke('didError', file);447}448449},450properties: {451URI: null,452activatedClass: null,453viewPolicy: null,454chunkThreshold: null455}456});457458459