Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/js/core/DragAndDropFileUpload.js
12241 views
1
/**
2
* @requires javelin-install
3
* javelin-util
4
* javelin-request
5
* javelin-dom
6
* javelin-uri
7
* phabricator-file-upload
8
* @provides phabricator-drag-and-drop-file-upload
9
* @javelin
10
*/
11
12
JX.install('PhabricatorDragAndDropFileUpload', {
13
14
construct : function(target) {
15
if (JX.DOM.isNode(target)) {
16
this._node = target;
17
} else {
18
this._sigil = target;
19
}
20
},
21
22
events : [
23
'didBeginDrag',
24
'didEndDrag',
25
'willUpload',
26
'progress',
27
'didUpload',
28
'didError'],
29
30
statics : {
31
isSupported : function() {
32
// TODO: Is there a better capability test for this? This seems okay in
33
// Safari, Firefox and Chrome.
34
35
return !!window.FileList;
36
},
37
isPasteSupported : function() {
38
// TODO: Needs to check if event.clipboardData is available.
39
// Works in Chrome, doesn't work in Firefox 10.
40
return !!window.FileList;
41
}
42
},
43
44
members : {
45
_node : null,
46
_sigil: null,
47
_depth : 0,
48
_isEnabled: false,
49
50
setIsEnabled: function(bool) {
51
this._isEnabled = bool;
52
return this;
53
},
54
55
getIsEnabled: function() {
56
return this._isEnabled;
57
},
58
59
_updateDepth : function(delta) {
60
if (this._depth === 0 && delta > 0) {
61
this.invoke('didBeginDrag', this._getTarget());
62
}
63
64
this._depth += delta;
65
66
if (this._depth === 0 && delta < 0) {
67
this.invoke('didEndDrag', this._getTarget());
68
}
69
},
70
71
_getTarget: function() {
72
return this._target || this._node;
73
},
74
75
start : function() {
76
77
// TODO: move this to JX.DOM.contains()?
78
function contains(container, child) {
79
do {
80
if (child === container) {
81
return true;
82
}
83
child = child.parentNode;
84
} while (child);
85
86
return false;
87
}
88
89
// Firefox has some issues sometimes; implement this click handler so
90
// the user can recover. See T5188.
91
var on_click = JX.bind(this, function (e) {
92
if (!this.getIsEnabled()) {
93
return;
94
}
95
96
if (this._depth) {
97
e.kill();
98
// Force depth to 0.
99
this._updateDepth(-this._depth);
100
}
101
});
102
103
// We track depth so that the _node may have children inside of it and
104
// not become unselected when they are dragged over.
105
var on_dragenter = JX.bind(this, function(e) {
106
if (!this.getIsEnabled()) {
107
return;
108
}
109
110
if (!this._node) {
111
var target = e.getNode(this._sigil);
112
if (target !== this._target) {
113
this._updateDepth(-this._depth);
114
this._target = target;
115
}
116
}
117
118
if (contains(this._getTarget(), e.getTarget())) {
119
this._updateDepth(1);
120
}
121
122
});
123
124
var on_dragleave = JX.bind(this, function(e) {
125
if (!this.getIsEnabled()) {
126
return;
127
}
128
129
if (!this._getTarget()) {
130
return;
131
}
132
133
if (contains(this._getTarget(), e.getTarget())) {
134
this._updateDepth(-1);
135
}
136
});
137
138
var on_dragover = JX.bind(this, function(e) {
139
if (!this.getIsEnabled()) {
140
return;
141
}
142
143
// NOTE: We must set this, or Chrome refuses to drop files from the
144
// download shelf.
145
e.getRawEvent().dataTransfer.dropEffect = 'copy';
146
e.kill();
147
});
148
149
var on_drop = JX.bind(this, function(e) {
150
if (!this.getIsEnabled()) {
151
return;
152
}
153
154
e.kill();
155
156
var files = e.getRawEvent().dataTransfer.files;
157
for (var ii = 0; ii < files.length; ii++) {
158
this.sendRequest(files[ii]);
159
}
160
161
// Force depth to 0.
162
this._updateDepth(-this._depth);
163
});
164
165
if (this._node) {
166
JX.DOM.listen(this._node, 'click', null, on_click);
167
JX.DOM.listen(this._node, 'dragenter', null, on_dragenter);
168
JX.DOM.listen(this._node, 'dragleave', null, on_dragleave);
169
JX.DOM.listen(this._node, 'dragover', null, on_dragover);
170
JX.DOM.listen(this._node, 'drop', null, on_drop);
171
} else {
172
JX.Stratcom.listen('click', this._sigil, on_click);
173
JX.Stratcom.listen('dragenter', this._sigil, on_dragenter);
174
JX.Stratcom.listen('dragleave', this._sigil, on_dragleave);
175
JX.Stratcom.listen('dragover', this._sigil, on_dragover);
176
JX.Stratcom.listen('drop', this._sigil, on_drop);
177
}
178
179
if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported() &&
180
this._node) {
181
JX.DOM.listen(
182
this._node,
183
'paste',
184
null,
185
JX.bind(this, function(e) {
186
if (!this.getIsEnabled()) {
187
return;
188
}
189
190
var clipboard = e.getRawEvent().clipboardData;
191
if (!clipboard) {
192
return;
193
}
194
195
// If there's any text on the clipboard, just let the event fire
196
// normally, choosing the text over any images. See T5437 / D9647.
197
var text = clipboard.getData('text/plain').toString();
198
if (text.length) {
199
return;
200
}
201
202
// Safari and Firefox have clipboardData, but no items. They
203
// don't seem to provide a way to get image data directly yet.
204
if (!clipboard.items) {
205
return;
206
}
207
208
for (var ii = 0; ii < clipboard.items.length; ii++) {
209
var item = clipboard.items[ii];
210
if (!/^image\//.test(item.type)) {
211
continue;
212
}
213
var spec = item.getAsFile();
214
// pasted files don't have a name; see
215
// https://code.google.com/p/chromium/issues/detail?id=361145
216
if (!spec.name) {
217
spec.name = 'pasted_file';
218
}
219
this.sendRequest(spec);
220
}
221
}));
222
}
223
224
this.setIsEnabled(true);
225
},
226
227
sendRequest : function(spec) {
228
var file = new JX.PhabricatorFileUpload()
229
.setRawFileObject(spec)
230
.setName(spec.name)
231
.setTotalBytes(spec.size);
232
233
var threshold = this.getChunkThreshold();
234
if (threshold && (file.getTotalBytes() > threshold)) {
235
// This is a large file, so we'll go through allocation so we can
236
// pick up support for resume and chunking.
237
this._allocateFile(file);
238
} else {
239
// If this file is smaller than the chunk threshold, skip the round
240
// trip for allocation and just upload it directly.
241
this._sendDataRequest(file);
242
}
243
},
244
245
_allocateFile: function(file) {
246
file
247
.setStatus('allocate')
248
.update();
249
250
this.invoke('willUpload', file);
251
252
var alloc_uri = this._getUploadURI(file)
253
.setQueryParam('allocate', 1);
254
255
new JX.Workflow(alloc_uri)
256
.setHandler(JX.bind(this, this._didAllocateFile, file))
257
.start();
258
},
259
260
_getUploadURI: function(file) {
261
var uri = JX.$U(this.getURI())
262
.setQueryParam('name', file.getName())
263
.setQueryParam('length', file.getTotalBytes());
264
265
if (this.getViewPolicy()) {
266
uri.setQueryParam('viewPolicy', this.getViewPolicy());
267
}
268
269
if (file.getAllocatedPHID()) {
270
uri.setQueryParam('phid', file.getAllocatedPHID());
271
}
272
273
return uri;
274
},
275
276
_didAllocateFile: function(file, r) {
277
var phid = r.phid;
278
var upload = r.upload;
279
280
if (!upload) {
281
if (phid) {
282
this._completeUpload(file, r);
283
} else {
284
this._failUpload(file, r);
285
}
286
return;
287
} else {
288
if (phid) {
289
// Start or resume a chunked upload.
290
file.setAllocatedPHID(phid);
291
this._loadChunks(file);
292
} else {
293
// Proceed with non-chunked upload.
294
this._sendDataRequest(file);
295
}
296
}
297
},
298
299
_loadChunks: function(file) {
300
file
301
.setStatus('chunks')
302
.update();
303
304
var chunks_uri = this._getUploadURI(file)
305
.setQueryParam('querychunks', 1);
306
307
new JX.Workflow(chunks_uri)
308
.setHandler(JX.bind(this, this._didLoadChunks, file))
309
.start();
310
},
311
312
_didLoadChunks: function(file, r) {
313
file.setChunks(r);
314
this._uploadNextChunk(file);
315
},
316
317
_uploadNextChunk: function(file) {
318
var chunks = file.getChunks();
319
var chunk;
320
for (var ii = 0; ii < chunks.length; ii++) {
321
chunk = chunks[ii];
322
if (!chunk.complete) {
323
this._uploadChunk(file, chunk);
324
break;
325
}
326
}
327
},
328
329
_uploadChunk: function(file, chunk, callback) {
330
file
331
.setStatus('upload')
332
.update();
333
334
var chunkup_uri = this._getUploadURI(file)
335
.setQueryParam('uploadchunk', 1)
336
.setQueryParam('__upload__', 1)
337
.setQueryParam('byteStart', chunk.byteStart)
338
.toString();
339
340
var callback = JX.bind(this, this._didUploadChunk, file, chunk);
341
342
var req = new JX.Request(chunkup_uri, callback);
343
344
var seen_bytes = 0;
345
var onprogress = JX.bind(this, function(progress) {
346
file
347
.addUploadedBytes(progress.loaded - seen_bytes)
348
.update();
349
350
seen_bytes = progress.loaded;
351
this.invoke('progress', file);
352
});
353
354
req.listen('error', JX.bind(this, this._onUploadError, req, file));
355
req.listen('uploadprogress', onprogress);
356
357
var blob = file.getRawFileObject().slice(chunk.byteStart, chunk.byteEnd);
358
359
req
360
.setRawData(blob)
361
.send();
362
},
363
364
_didUploadChunk: function(file, chunk, r) {
365
file.didCompleteChunk(chunk);
366
367
if (r.complete) {
368
this._completeUpload(file, r);
369
} else {
370
this._uploadNextChunk(file);
371
}
372
},
373
374
_sendDataRequest: function(file) {
375
file
376
.setStatus('uploading')
377
.update();
378
379
this.invoke('willUpload', file);
380
381
var up_uri = this._getUploadURI(file)
382
.setQueryParam('__upload__', 1)
383
.toString();
384
385
var onupload = JX.bind(this, function(r) {
386
if (r.error) {
387
this._failUpload(file, r);
388
} else {
389
this._completeUpload(file, r);
390
}
391
});
392
393
var req = new JX.Request(up_uri, onupload);
394
395
var onprogress = JX.bind(this, function(progress) {
396
file
397
.setTotalBytes(progress.total)
398
.setUploadedBytes(progress.loaded)
399
.update();
400
401
this.invoke('progress', file);
402
});
403
404
req.listen('error', JX.bind(this, this._onUploadError, req, file));
405
req.listen('uploadprogress', onprogress);
406
407
req
408
.setRawData(file.getRawFileObject())
409
.send();
410
},
411
412
_completeUpload: function(file, r) {
413
file
414
.setID(r.id)
415
.setPHID(r.phid)
416
.setURI(r.uri)
417
.setMarkup(r.html)
418
.setStatus('done')
419
.setTargetNode(this._getTarget())
420
.update();
421
422
this.invoke('didUpload', file);
423
},
424
425
_failUpload: function(file, r) {
426
file
427
.setStatus('error')
428
.setError(r.error)
429
.update();
430
431
this.invoke('didError', file);
432
},
433
434
_onUploadError: function(req, file, error) {
435
file.setStatus('error');
436
437
if (error) {
438
file.setError(error.code + ': ' + error.info);
439
} else {
440
var xhr = req.getTransport();
441
if (xhr.responseText) {
442
file.setError('Server responded: ' + xhr.responseText);
443
}
444
}
445
446
file.update();
447
this.invoke('didError', file);
448
}
449
450
},
451
properties: {
452
URI: null,
453
activatedClass: null,
454
viewPolicy: null,
455
chunkThreshold: null
456
}
457
});
458
459