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