- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / downloads / downloads.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // TODO(jhawkins): Use hidden instead of showInline* and display:none.
6
7 /**
8  * Sets the display style of a node.
9  * @param {!Element} node The target element to show or hide.
10  * @param {boolean} isShow Should the target element be visible.
11  */
12 function showInline(node, isShow) {
13   node.style.display = isShow ? 'inline' : 'none';
14 }
15
16 /**
17  * Sets the display style of a node.
18  * @param {!Element} node The target element to show or hide.
19  * @param {boolean} isShow Should the target element be visible.
20  */
21 function showInlineBlock(node, isShow) {
22   node.style.display = isShow ? 'inline-block' : 'none';
23 }
24
25 /**
26  * Creates a link with a specified onclick handler and content.
27  * @param {function()} onclick The onclick handler.
28  * @param {string} value The link text.
29  * @return {Element} The created link element.
30  */
31 function createLink(onclick, value) {
32   var link = document.createElement('a');
33   link.onclick = onclick;
34   link.href = '#';
35   link.textContent = value;
36   link.oncontextmenu = function() { return false; };
37   return link;
38 }
39
40 /**
41  * Creates a button with a specified onclick handler and content.
42  * @param {function()} onclick The onclick handler.
43  * @param {string} value The button text.
44  * @return {Element} The created button.
45  */
46 function createButton(onclick, value) {
47   var button = document.createElement('input');
48   button.type = 'button';
49   button.value = value;
50   button.onclick = onclick;
51   return button;
52 }
53
54 ///////////////////////////////////////////////////////////////////////////////
55 // Downloads
56 /**
57  * Class to hold all the information about the visible downloads.
58  * @constructor
59  */
60 function Downloads() {
61   this.downloads_ = {};
62   this.node_ = $('downloads-display');
63   this.summary_ = $('downloads-summary-text');
64   this.searchText_ = '';
65
66   // Keep track of the dates of the newest and oldest downloads so that we
67   // know where to insert them.
68   this.newestTime_ = -1;
69
70   // Icon load request queue.
71   this.iconLoadQueue_ = [];
72   this.isIconLoading_ = false;
73 }
74
75 /**
76  * Called when a download has been updated or added.
77  * @param {Object} download A backend download object (see downloads_ui.cc)
78  */
79 Downloads.prototype.updated = function(download) {
80   var id = download.id;
81   if (!!this.downloads_[id]) {
82     this.downloads_[id].update(download);
83   } else {
84     this.downloads_[id] = new Download(download);
85     // We get downloads in display order, so we don't have to worry about
86     // maintaining correct order - we can assume that any downloads not in
87     // display order are new ones and so we can add them to the top of the
88     // list.
89     if (download.started > this.newestTime_) {
90       this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
91       this.newestTime_ = download.started;
92     } else {
93       this.node_.appendChild(this.downloads_[id].node);
94     }
95   }
96   // Download.prototype.update may change its nodeSince_ and nodeDate_, so
97   // update all the date displays.
98   // TODO(benjhayden) Only do this if its nodeSince_ or nodeDate_ actually did
99   // change since this may touch 150 elements and Downloads.prototype.updated
100   // may be called 150 times.
101   this.updateDateDisplay_();
102 };
103
104 /**
105  * Set our display search text.
106  * @param {string} searchText The string we're searching for.
107  */
108 Downloads.prototype.setSearchText = function(searchText) {
109   this.searchText_ = searchText;
110 };
111
112 /**
113  * Update the summary block above the results
114  */
115 Downloads.prototype.updateSummary = function() {
116   if (this.searchText_) {
117     this.summary_.textContent = loadTimeData.getStringF('searchresultsfor',
118                                                         this.searchText_);
119   } else {
120     this.summary_.textContent = loadTimeData.getString('downloads');
121   }
122
123   var hasDownloads = false;
124   for (var i in this.downloads_) {
125     hasDownloads = true;
126     break;
127   }
128 };
129
130 /**
131  * Returns the number of downloads in the model. Used by tests.
132  * @return {integer} Returns the number of downloads shown on the page.
133  */
134 Downloads.prototype.size = function() {
135   return Object.keys(this.downloads_).length;
136 };
137
138 /**
139  * Update the date visibility in our nodes so that no date is
140  * repeated.
141  * @private
142  */
143 Downloads.prototype.updateDateDisplay_ = function() {
144   var dateContainers = document.getElementsByClassName('date-container');
145   var displayed = {};
146   for (var i = 0, container; container = dateContainers[i]; i++) {
147     var dateString = container.getElementsByClassName('date')[0].innerHTML;
148     if (!!displayed[dateString]) {
149       container.style.display = 'none';
150     } else {
151       displayed[dateString] = true;
152       container.style.display = 'block';
153     }
154   }
155 };
156
157 /**
158  * Remove a download.
159  * @param {number} id The id of the download to remove.
160  */
161 Downloads.prototype.remove = function(id) {
162   this.node_.removeChild(this.downloads_[id].node);
163   delete this.downloads_[id];
164   this.updateDateDisplay_();
165 };
166
167 /**
168  * Clear all downloads and reset us back to a null state.
169  */
170 Downloads.prototype.clear = function() {
171   for (var id in this.downloads_) {
172     this.downloads_[id].clear();
173     this.remove(id);
174   }
175 };
176
177 /**
178  * Schedule icon load.
179  * @param {HTMLImageElement} elem Image element that should contain the icon.
180  * @param {string} iconURL URL to the icon.
181  */
182 Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
183   var self = this;
184
185   // Sends request to the next icon in the queue and schedules
186   // call to itself when the icon is loaded.
187   function loadNext() {
188     self.isIconLoading_ = true;
189     while (self.iconLoadQueue_.length > 0) {
190       var request = self.iconLoadQueue_.shift();
191       var oldSrc = request.element.src;
192       request.element.onabort = request.element.onerror =
193           request.element.onload = loadNext;
194       request.element.src = request.url;
195       if (oldSrc != request.element.src)
196         return;
197     }
198     self.isIconLoading_ = false;
199   }
200
201   // Create new request
202   var loadRequest = {element: elem, url: iconURL};
203   this.iconLoadQueue_.push(loadRequest);
204
205   // Start loading if none scheduled yet
206   if (!this.isIconLoading_)
207     loadNext();
208 };
209
210 /**
211  * Returns whether the displayed list needs to be updated or not.
212  * @param {Array} downloads Array of download nodes.
213  * @return {boolean} Returns true if the displayed list is to be updated.
214  */
215 Downloads.prototype.isUpdateNeeded = function(downloads) {
216   var size = 0;
217   for (var i in this.downloads_)
218     size++;
219   if (size != downloads.length)
220     return true;
221   // Since there are the same number of items in the incoming list as
222   // |this.downloads_|, there won't be any removed downloads without some
223   // downloads having been inserted.  So check only for new downloads in
224   // deciding whether to update.
225   for (var i = 0; i < downloads.length; i++) {
226     if (!this.downloads_[downloads[i].id])
227       return true;
228   }
229   return false;
230 };
231
232 ///////////////////////////////////////////////////////////////////////////////
233 // Download
234 /**
235  * A download and the DOM representation for that download.
236  * @param {Object} download A backend download object (see downloads_ui.cc)
237  * @constructor
238  */
239 function Download(download) {
240   // Create DOM
241   this.node = createElementWithClassName(
242       'div', 'download' + (download.otr ? ' otr' : ''));
243
244   // Dates
245   this.dateContainer_ = createElementWithClassName('div', 'date-container');
246   this.node.appendChild(this.dateContainer_);
247
248   this.nodeSince_ = createElementWithClassName('div', 'since');
249   this.nodeDate_ = createElementWithClassName('div', 'date');
250   this.dateContainer_.appendChild(this.nodeSince_);
251   this.dateContainer_.appendChild(this.nodeDate_);
252
253   // Container for all 'safe download' UI.
254   this.safe_ = createElementWithClassName('div', 'safe');
255   this.safe_.ondragstart = this.drag_.bind(this);
256   this.node.appendChild(this.safe_);
257
258   if (download.state != Download.States.COMPLETE) {
259     this.nodeProgressBackground_ =
260         createElementWithClassName('div', 'progress background');
261     this.safe_.appendChild(this.nodeProgressBackground_);
262
263     this.nodeProgressForeground_ =
264         createElementWithClassName('canvas', 'progress');
265     this.nodeProgressForeground_.width = Download.Progress.width;
266     this.nodeProgressForeground_.height = Download.Progress.height;
267     this.canvasProgress_ = this.nodeProgressForeground_.getContext('2d');
268
269     this.canvasProgressForegroundImage_ = new Image();
270     this.canvasProgressForegroundImage_.src =
271         'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@' +
272         window.devicePixelRatio + 'x';
273     this.safe_.appendChild(this.nodeProgressForeground_);
274   }
275
276   this.nodeImg_ = createElementWithClassName('img', 'icon');
277   this.safe_.appendChild(this.nodeImg_);
278
279   // FileLink is used for completed downloads, otherwise we show FileName.
280   this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
281   this.safe_.appendChild(this.nodeTitleArea_);
282
283   this.nodeFileLink_ = createLink(this.openFile_.bind(this), '');
284   this.nodeFileLink_.className = 'name';
285   this.nodeFileLink_.style.display = 'none';
286   this.nodeTitleArea_.appendChild(this.nodeFileLink_);
287
288   this.nodeFileName_ = createElementWithClassName('span', 'name');
289   this.nodeFileName_.style.display = 'none';
290   this.nodeTitleArea_.appendChild(this.nodeFileName_);
291
292   this.nodeStatus_ = createElementWithClassName('span', 'status');
293   this.nodeTitleArea_.appendChild(this.nodeStatus_);
294
295   var nodeURLDiv = createElementWithClassName('div', 'url-container');
296   this.safe_.appendChild(nodeURLDiv);
297
298   this.nodeURL_ = createElementWithClassName('a', 'src-url');
299   this.nodeURL_.target = '_blank';
300   nodeURLDiv.appendChild(this.nodeURL_);
301
302   // Controls.
303   this.nodeControls_ = createElementWithClassName('div', 'controls');
304   this.safe_.appendChild(this.nodeControls_);
305
306   // We don't need 'show in folder' in chromium os. See download_ui.cc and
307   // http://code.google.com/p/chromium-os/issues/detail?id=916.
308   if (loadTimeData.valueExists('control_showinfolder')) {
309     this.controlShow_ = createLink(this.show_.bind(this),
310         loadTimeData.getString('control_showinfolder'));
311     this.nodeControls_.appendChild(this.controlShow_);
312   } else {
313     this.controlShow_ = null;
314   }
315
316   this.controlRetry_ = document.createElement('a');
317   this.controlRetry_.download = '';
318   this.controlRetry_.textContent = loadTimeData.getString('control_retry');
319   this.nodeControls_.appendChild(this.controlRetry_);
320
321   // Pause/Resume are a toggle.
322   this.controlPause_ = createLink(this.pause_.bind(this),
323       loadTimeData.getString('control_pause'));
324   this.nodeControls_.appendChild(this.controlPause_);
325
326   this.controlResume_ = createLink(this.resume_.bind(this),
327       loadTimeData.getString('control_resume'));
328   this.nodeControls_.appendChild(this.controlResume_);
329
330   // Anchors <a> don't support the "disabled" property.
331   if (loadTimeData.getBoolean('allow_deleting_history')) {
332     this.controlRemove_ = createLink(this.remove_.bind(this),
333         loadTimeData.getString('control_removefromlist'));
334     this.controlRemove_.classList.add('control-remove-link');
335   } else {
336     this.controlRemove_ = document.createElement('span');
337     this.controlRemove_.classList.add('disabled-link');
338     var text = document.createTextNode(
339         loadTimeData.getString('control_removefromlist'));
340     this.controlRemove_.appendChild(text);
341   }
342   if (!loadTimeData.getBoolean('show_delete_history'))
343     this.controlRemove_.hidden = true;
344
345   this.nodeControls_.appendChild(this.controlRemove_);
346
347   this.controlCancel_ = createLink(this.cancel_.bind(this),
348       loadTimeData.getString('control_cancel'));
349   this.nodeControls_.appendChild(this.controlCancel_);
350
351   this.controlByExtension_ = document.createElement('span');
352   this.nodeControls_.appendChild(this.controlByExtension_);
353
354   // Container for 'unsafe download' UI.
355   this.danger_ = createElementWithClassName('div', 'show-dangerous');
356   this.node.appendChild(this.danger_);
357
358   this.dangerNodeImg_ = createElementWithClassName('img', 'icon');
359   this.danger_.appendChild(this.dangerNodeImg_);
360
361   this.dangerDesc_ = document.createElement('div');
362   this.danger_.appendChild(this.dangerDesc_);
363
364   // Buttons for the malicious case.
365   this.malwareNodeControls_ = createElementWithClassName('div', 'controls');
366   this.malwareSave_ = createLink(
367       this.saveDangerous_.bind(this),
368       loadTimeData.getString('danger_restore'));
369   this.malwareNodeControls_.appendChild(this.malwareSave_);
370   this.malwareDiscard_ = createLink(
371       this.discardDangerous_.bind(this),
372       loadTimeData.getString('control_removefromlist'));
373   this.malwareNodeControls_.appendChild(this.malwareDiscard_);
374   this.danger_.appendChild(this.malwareNodeControls_);
375
376   // Buttons for the dangerous but not malicious case.
377   this.dangerSave_ = createButton(
378       this.saveDangerous_.bind(this),
379       loadTimeData.getString('danger_save'));
380   this.danger_.appendChild(this.dangerSave_);
381
382   this.dangerDiscard_ = createButton(
383       this.discardDangerous_.bind(this),
384       loadTimeData.getString('danger_discard'));
385   this.danger_.appendChild(this.dangerDiscard_);
386
387   // Update member vars.
388   this.update(download);
389 }
390
391 /**
392  * The states a download can be in. These correspond to states defined in
393  * DownloadsDOMHandler::CreateDownloadItemValue
394  */
395 Download.States = {
396   IN_PROGRESS: 'IN_PROGRESS',
397   CANCELLED: 'CANCELLED',
398   COMPLETE: 'COMPLETE',
399   PAUSED: 'PAUSED',
400   DANGEROUS: 'DANGEROUS',
401   INTERRUPTED: 'INTERRUPTED',
402 };
403
404 /**
405  * Explains why a download is in DANGEROUS state.
406  */
407 Download.DangerType = {
408   NOT_DANGEROUS: 'NOT_DANGEROUS',
409   DANGEROUS_FILE: 'DANGEROUS_FILE',
410   DANGEROUS_URL: 'DANGEROUS_URL',
411   DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
412   UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
413   DANGEROUS_HOST: 'DANGEROUS_HOST',
414   POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
415 };
416
417 /**
418  * Constants for the progress meter.
419  */
420
421 Download.Progress = (function() {
422   var scale = window.devicePixelRatio;
423   return {
424     width: 48 * scale,
425     height: 48 * scale,
426     radius: 24 * scale,
427     centerX: 24 * scale,
428     centerY: 24 * scale,
429     base: -0.5 * Math.PI,
430     dir: false,
431   };
432 })();
433
434 /**
435  * Updates the download to reflect new data.
436  * @param {Object} download A backend download object (see downloads_ui.cc)
437  */
438 Download.prototype.update = function(download) {
439   this.id_ = download.id;
440   this.filePath_ = download.file_path;
441   this.fileUrl_ = download.file_url;
442   this.fileName_ = download.file_name;
443   this.url_ = download.url;
444   this.state_ = download.state;
445   this.fileExternallyRemoved_ = download.file_externally_removed;
446   this.dangerType_ = download.danger_type;
447   this.lastReasonDescription_ = download.last_reason_text;
448   this.byExtensionId_ = download.by_ext_id;
449   this.byExtensionName_ = download.by_ext_name;
450
451   this.since_ = download.since_string;
452   this.date_ = download.date_string;
453
454   // See DownloadItem::PercentComplete
455   this.percent_ = Math.max(download.percent, 0);
456   this.progressStatusText_ = download.progress_status_text;
457   this.received_ = download.received;
458
459   if (this.state_ == Download.States.DANGEROUS) {
460     this.updateDangerousFile();
461   } else {
462     downloads.scheduleIconLoad(this.nodeImg_,
463                                'chrome://fileicon/' +
464                                    encodeURIComponent(this.filePath_) +
465                                    '?scale=' + window.devicePixelRatio + 'x');
466
467     if (this.state_ == Download.States.COMPLETE &&
468         !this.fileExternallyRemoved_) {
469       this.nodeFileLink_.textContent = this.fileName_;
470       this.nodeFileLink_.href = this.fileUrl_;
471       this.nodeFileLink_.oncontextmenu = null;
472     } else if (this.nodeFileName_.textContent != this.fileName_) {
473       this.nodeFileName_.textContent = this.fileName_;
474     }
475     if (this.state_ == Download.States.INTERRUPTED)
476       this.nodeFileName_.classList.add('interrupted');
477
478     showInline(this.nodeFileLink_,
479                this.state_ == Download.States.COMPLETE &&
480                    !this.fileExternallyRemoved_);
481     // nodeFileName_ has to be inline-block to avoid the 'interaction' with
482     // nodeStatus_. If both are inline, it appears that their text contents
483     // are merged before the bidi algorithm is applied leading to an
484     // undesirable reordering. http://crbug.com/13216
485     showInlineBlock(this.nodeFileName_,
486                     this.state_ != Download.States.COMPLETE ||
487                         this.fileExternallyRemoved_);
488
489     if (this.state_ == Download.States.IN_PROGRESS) {
490       this.nodeProgressForeground_.style.display = 'block';
491       this.nodeProgressBackground_.style.display = 'block';
492
493       // Draw a pie-slice for the progress.
494       this.canvasProgress_.globalCompositeOperation = 'copy';
495       this.canvasProgress_.drawImage(this.canvasProgressForegroundImage_, 0, 0);
496       this.canvasProgress_.globalCompositeOperation = 'destination-in';
497       this.canvasProgress_.beginPath();
498       this.canvasProgress_.moveTo(Download.Progress.centerX,
499                                   Download.Progress.centerY);
500
501       // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
502       this.canvasProgress_.arc(Download.Progress.centerX,
503                                Download.Progress.centerY,
504                                Download.Progress.radius,
505                                Download.Progress.base,
506                                Download.Progress.base + Math.PI * 0.02 *
507                                Number(this.percent_),
508                                false);
509
510       this.canvasProgress_.lineTo(Download.Progress.centerX,
511                                   Download.Progress.centerY);
512       this.canvasProgress_.fill();
513       this.canvasProgress_.closePath();
514     } else if (this.nodeProgressBackground_) {
515       this.nodeProgressForeground_.style.display = 'none';
516       this.nodeProgressBackground_.style.display = 'none';
517     }
518
519     if (this.controlShow_) {
520       showInline(this.controlShow_,
521                  this.state_ == Download.States.COMPLETE &&
522                      !this.fileExternallyRemoved_);
523     }
524     showInline(this.controlRetry_, download.retry);
525     this.controlRetry_.href = this.url_;
526     showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS);
527     showInline(this.controlResume_, download.resume);
528     var showCancel = this.state_ == Download.States.IN_PROGRESS ||
529                      this.state_ == Download.States.PAUSED;
530     showInline(this.controlCancel_, showCancel);
531     showInline(this.controlRemove_, !showCancel);
532
533     if (this.byExtensionId_ && this.byExtensionName_) {
534       // Format 'control_by_extension' with a link instead of plain text by
535       // splitting the formatted string into pieces.
536       var slug = 'XXXXX';
537       var formatted = loadTimeData.getStringF('control_by_extension', slug);
538       var slugIndex = formatted.indexOf(slug);
539       this.controlByExtension_.textContent = formatted.substr(0, slugIndex);
540       this.controlByExtensionLink_ = document.createElement('a');
541       this.controlByExtensionLink_.href =
542           'chrome://extensions#' + this.byExtensionId_;
543       this.controlByExtensionLink_.textContent = this.byExtensionName_;
544       this.controlByExtension_.appendChild(this.controlByExtensionLink_);
545       if (slugIndex < (formatted.length - slug.length))
546         this.controlByExtension_.appendChild(document.createTextNode(
547             formatted.substr(slugIndex + 1)));
548     }
549
550     this.nodeSince_.textContent = this.since_;
551     this.nodeDate_.textContent = this.date_;
552     // Don't unnecessarily update the url, as doing so will remove any
553     // text selection the user has started (http://crbug.com/44982).
554     if (this.nodeURL_.textContent != this.url_) {
555       this.nodeURL_.textContent = this.url_;
556       this.nodeURL_.href = this.url_;
557     }
558     this.nodeStatus_.textContent = this.getStatusText_();
559
560     this.danger_.style.display = 'none';
561     this.safe_.style.display = 'block';
562   }
563 };
564
565 /**
566  * Decorates the icons, strings, and buttons for a download to reflect the
567  * danger level of a file. Dangerous & malicious files are treated differently.
568  */
569 Download.prototype.updateDangerousFile = function() {
570   switch (this.dangerType_) {
571     case Download.DangerType.DANGEROUS_FILE: {
572       this.dangerDesc_.textContent = loadTimeData.getStringF(
573           'danger_file_desc', this.fileName_);
574       break;
575     }
576     case Download.DangerType.DANGEROUS_URL: {
577       this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
578       break;
579     }
580     case Download.DangerType.DANGEROUS_CONTENT:  // Fall through.
581     case Download.DangerType.DANGEROUS_HOST: {
582       this.dangerDesc_.textContent = loadTimeData.getStringF(
583           'danger_content_desc', this.fileName_);
584       break;
585     }
586     case Download.DangerType.UNCOMMON_CONTENT: {
587       this.dangerDesc_.textContent = loadTimeData.getStringF(
588           'danger_uncommon_desc', this.fileName_);
589       break;
590     }
591     case Download.DangerType.POTENTIALLY_UNWANTED: {
592       this.dangerDesc_.textContent = loadTimeData.getStringF(
593           'danger_settings_desc', this.fileName_);
594       break;
595     }
596   }
597
598   if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
599     downloads.scheduleIconLoad(
600         this.dangerNodeImg_,
601         'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
602   } else {
603     downloads.scheduleIconLoad(
604         this.dangerNodeImg_,
605         'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
606             window.devicePixelRatio + 'x');
607     this.dangerDesc_.className = 'malware-description';
608   }
609
610   if (this.dangerType_ == Download.DangerType.DANGEROUS_CONTENT ||
611       this.dangerType_ == Download.DangerType.DANGEROUS_HOST ||
612       this.dangerType_ == Download.DangerType.DANGEROUS_URL ||
613       this.dangerType_ == Download.DangerType.POTENTIALLY_UNWANTED) {
614     this.malwareNodeControls_.style.display = 'block';
615     this.dangerDiscard_.style.display = 'none';
616     this.dangerSave_.style.display = 'none';
617   } else {
618     this.malwareNodeControls_.style.display = 'none';
619     this.dangerDiscard_.style.display = 'inline';
620     this.dangerSave_.style.display = 'inline';
621   }
622
623   this.danger_.style.display = 'block';
624   this.safe_.style.display = 'none';
625 };
626
627 /**
628  * Removes applicable bits from the DOM in preparation for deletion.
629  */
630 Download.prototype.clear = function() {
631   this.safe_.ondragstart = null;
632   this.nodeFileLink_.onclick = null;
633   if (this.controlShow_) {
634     this.controlShow_.onclick = null;
635   }
636   this.controlCancel_.onclick = null;
637   this.controlPause_.onclick = null;
638   this.controlResume_.onclick = null;
639   this.dangerDiscard_.onclick = null;
640   this.dangerSave_.onclick = null;
641   this.malwareDiscard_.onclick = null;
642   this.malwareSave_.onclick = null;
643
644   this.node.innerHTML = '';
645 };
646
647 /**
648  * @private
649  * @return {string} User-visible status update text.
650  */
651 Download.prototype.getStatusText_ = function() {
652   switch (this.state_) {
653     case Download.States.IN_PROGRESS:
654       return this.progressStatusText_;
655     case Download.States.CANCELLED:
656       return loadTimeData.getString('status_cancelled');
657     case Download.States.PAUSED:
658       return loadTimeData.getString('status_paused');
659     case Download.States.DANGEROUS:
660       // danger_url_desc is also used by DANGEROUS_CONTENT.
661       var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ?
662           'danger_file_desc' : 'danger_url_desc';
663       return loadTimeData.getString(desc);
664     case Download.States.INTERRUPTED:
665       return this.lastReasonDescription_;
666     case Download.States.COMPLETE:
667       return this.fileExternallyRemoved_ ?
668           loadTimeData.getString('status_removed') : '';
669   }
670 };
671
672 /**
673  * Tells the backend to initiate a drag, allowing users to drag
674  * files from the download page and have them appear as native file
675  * drags.
676  * @return {boolean} Returns false to prevent the default action.
677  * @private
678  */
679 Download.prototype.drag_ = function() {
680   chrome.send('drag', [this.id_.toString()]);
681   return false;
682 };
683
684 /**
685  * Tells the backend to open this file.
686  * @return {boolean} Returns false to prevent the default action.
687  * @private
688  */
689 Download.prototype.openFile_ = function() {
690   chrome.send('openFile', [this.id_.toString()]);
691   return false;
692 };
693
694 /**
695  * Tells the backend that the user chose to save a dangerous file.
696  * @return {boolean} Returns false to prevent the default action.
697  * @private
698  */
699 Download.prototype.saveDangerous_ = function() {
700   chrome.send('saveDangerous', [this.id_.toString()]);
701   return false;
702 };
703
704 /**
705  * Tells the backend that the user chose to discard a dangerous file.
706  * @return {boolean} Returns false to prevent the default action.
707  * @private
708  */
709 Download.prototype.discardDangerous_ = function() {
710   chrome.send('discardDangerous', [this.id_.toString()]);
711   downloads.remove(this.id_);
712   return false;
713 };
714
715 /**
716  * Tells the backend to show the file in explorer.
717  * @return {boolean} Returns false to prevent the default action.
718  * @private
719  */
720 Download.prototype.show_ = function() {
721   chrome.send('show', [this.id_.toString()]);
722   return false;
723 };
724
725 /**
726  * Tells the backend to pause this download.
727  * @return {boolean} Returns false to prevent the default action.
728  * @private
729  */
730 Download.prototype.pause_ = function() {
731   chrome.send('pause', [this.id_.toString()]);
732   return false;
733 };
734
735 /**
736  * Tells the backend to resume this download.
737  * @return {boolean} Returns false to prevent the default action.
738  * @private
739  */
740 Download.prototype.resume_ = function() {
741   chrome.send('resume', [this.id_.toString()]);
742   return false;
743 };
744
745 /**
746  * Tells the backend to remove this download from history and download shelf.
747  * @return {boolean} Returns false to prevent the default action.
748  * @private
749  */
750  Download.prototype.remove_ = function() {
751    if (loadTimeData.getBoolean('allow_deleting_history')) {
752     chrome.send('remove', [this.id_.toString()]);
753   }
754   return false;
755 };
756
757 /**
758  * Tells the backend to cancel this download.
759  * @return {boolean} Returns false to prevent the default action.
760  * @private
761  */
762 Download.prototype.cancel_ = function() {
763   chrome.send('cancel', [this.id_.toString()]);
764   return false;
765 };
766
767 ///////////////////////////////////////////////////////////////////////////////
768 // Page:
769 var downloads, resultsTimeout;
770
771 // TODO(benjhayden): Rename Downloads to DownloadManager, downloads to
772 // downloadManager or theDownloadManager or DownloadManager.get() to prevent
773 // confusing Downloads with Download.
774
775 /**
776  * The FIFO array that stores updates of download files to be appeared
777  * on the download page. It is guaranteed that the updates in this array
778  * are reflected to the download page in a FIFO order.
779 */
780 var fifoResults;
781
782 function load() {
783   chrome.send('onPageLoaded');
784   fifoResults = [];
785   downloads = new Downloads();
786   $('term').focus();
787   setSearch('');
788
789   var clearAllHolder = $('clear-all-holder');
790   var clearAllElement;
791   if (loadTimeData.getBoolean('allow_deleting_history')) {
792     clearAllElement = createLink(clearAll, loadTimeData.getString('clear_all'));
793     clearAllElement.classList.add('clear-all-link');
794     clearAllHolder.classList.remove('disabled-link');
795   } else {
796     clearAllElement = document.createTextNode(
797         loadTimeData.getString('clear_all'));
798     clearAllHolder.classList.add('disabled-link');
799   }
800   if (!loadTimeData.getBoolean('show_delete_history'))
801     clearAllHolder.hidden = true;
802
803   clearAllHolder.appendChild(clearAllElement);
804   clearAllElement.oncontextmenu = function() { return false; };
805
806   // TODO(jhawkins): Use a link-button here.
807   var openDownloadsFolderLink = $('open-downloads-folder');
808   openDownloadsFolderLink.onclick = function() {
809     chrome.send('openDownloadsFolder');
810   };
811   openDownloadsFolderLink.oncontextmenu = function() { return false; };
812
813   $('search-link').onclick = function(e) {
814     setSearch('');
815     e.preventDefault();
816     $('term').value = '';
817     return false;
818   };
819
820   $('term').onsearch = function(e) {
821     setSearch(this.value);
822   };
823 }
824
825 function setSearch(searchText) {
826   fifoResults.length = 0;
827   downloads.setSearchText(searchText);
828   searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
829   if (searchText) {
830     searchText = searchText.map(function(term) {
831       // strip quotes
832       return (term.match(/\s/) &&
833               term[0].match(/["']/) &&
834               term[term.length - 1] == term[0]) ?
835         term.substr(1, term.length - 2) : term;
836     });
837   } else {
838     searchText = [];
839   }
840   chrome.send('getDownloads', searchText);
841 }
842
843 function clearAll() {
844   if (!loadTimeData.getBoolean('allow_deleting_history'))
845     return;
846
847   fifoResults.length = 0;
848   downloads.clear();
849   downloads.setSearchText('');
850   chrome.send('clearAll');
851 }
852
853 ///////////////////////////////////////////////////////////////////////////////
854 // Chrome callbacks:
855 /**
856  * Our history system calls this function with results from searches or when
857  * downloads are added or removed.
858  * @param {Array.<Object>} results List of updates.
859  */
860 function downloadsList(results) {
861   if (downloads && downloads.isUpdateNeeded(results)) {
862     if (resultsTimeout)
863       clearTimeout(resultsTimeout);
864     fifoResults.length = 0;
865     downloads.clear();
866     downloadUpdated(results);
867   }
868   downloads.updateSummary();
869 }
870
871 /**
872  * When a download is updated (progress, state change), this is called.
873  * @param {Array.<Object>} results List of updates for the download process.
874  */
875 function downloadUpdated(results) {
876   // Sometimes this can get called too early.
877   if (!downloads)
878     return;
879
880   fifoResults = fifoResults.concat(results);
881   tryDownloadUpdatedPeriodically();
882 }
883
884 /**
885  * Try to reflect as much updates as possible within 50ms.
886  * This function is scheduled again and again until all updates are reflected.
887  */
888 function tryDownloadUpdatedPeriodically() {
889   var start = Date.now();
890   while (fifoResults.length) {
891     var result = fifoResults.shift();
892     downloads.updated(result);
893     // Do as much as we can in 50ms.
894     if (Date.now() - start > 50) {
895       clearTimeout(resultsTimeout);
896       resultsTimeout = setTimeout(tryDownloadUpdatedPeriodically, 5);
897       break;
898     }
899   }
900 }
901
902 // Add handlers to HTML elements.
903 window.addEventListener('DOMContentLoaded', load);