ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / downloads / downloads.js
blob0a074b7bf28f290c4615c3195488864ef9ee92f1
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.
5 // TODO(hcarmona): This file is big: it may be good to split it up.
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 DownloadItem;
34 /**
35  * Creates a link with a specified onclick handler and content.
36  * @param {function()} onclick The onclick handler.
37  * @param {string=} opt_text The link text.
38  * @return {!Element} The created link element.
39  */
40 function createActionLink(onclick, opt_text) {
41   var link = new ActionLink;
42   link.onclick = onclick;
43   if (opt_text) link.textContent = opt_text;
44   return link;
47 /**
48  * Creates a button with a specified onclick handler and content.
49  * @param {function()} onclick The onclick handler.
50  * @param {string} value The button text.
51  * @return {Element} The created button.
52  */
53 function createButton(onclick, value) {
54   var button = document.createElement('input');
55   button.type = 'button';
56   button.value = value;
57   button.onclick = onclick;
58   return button;
61 ///////////////////////////////////////////////////////////////////////////////
62 // DownloadFocusRow:
64 /**
65  * Provides an implementation for a single column grid.
66  * @constructor
67  * @extends {cr.ui.FocusRow}
68  */
69 function DownloadFocusRow() {}
71 /**
72  * Decorates |focusRow| so that it can be treated as a DownloadFocusRow.
73  * @param {Element} focusRow The element that has all the columns represented
74  *     by |download|.
75  * @param {Download} download The Download representing this row.
76  * @param {Node} boundary Focus events are ignored outside of this node.
77  */
78 DownloadFocusRow.decorate = function(focusRow, download, boundary) {
79   focusRow.__proto__ = DownloadFocusRow.prototype;
80   focusRow.decorate(boundary);
82   // Add all clickable elements as a row into the grid.
83   focusRow.addElementIfFocusable_(download.nodeFileLink_, 'name');
84   focusRow.addElementIfFocusable_(download.nodeURL_, 'url');
85   focusRow.addElementIfFocusable_(download.controlShow_, 'show');
86   focusRow.addElementIfFocusable_(download.controlRetry_, 'retry');
87   focusRow.addElementIfFocusable_(download.controlPause_, 'pause');
88   focusRow.addElementIfFocusable_(download.controlResume_, 'resume');
89   focusRow.addElementIfFocusable_(download.controlRemove_, 'remove');
90   focusRow.addElementIfFocusable_(download.controlCancel_, 'cancel');
91   focusRow.addElementIfFocusable_(download.malwareSave_, 'save');
92   focusRow.addElementIfFocusable_(download.dangerSave_, 'save');
93   focusRow.addElementIfFocusable_(download.malwareDiscard_, 'discard');
94   focusRow.addElementIfFocusable_(download.dangerDiscard_, 'discard');
95   focusRow.addElementIfFocusable_(download.controlByExtensionLink_,
96                                   'extension');
99 DownloadFocusRow.prototype = {
100   __proto__: cr.ui.FocusRow.prototype,
102   /** @override */
103   getEquivalentElement: function(element) {
104     if (this.focusableElements.indexOf(element) > -1)
105       return element;
107     // All elements default to another element with the same type.
108     var columnType = element.getAttribute('column-type');
109     var equivalent = this.querySelector('[column-type=' + columnType + ']');
111     if (this.focusableElements.indexOf(equivalent) < 0) {
112       var equivalentTypes =
113           ['show', 'retry', 'pause', 'resume', 'remove', 'cancel'];
114       if (equivalentTypes.indexOf(columnType) != -1) {
115         var allTypes = equivalentTypes.map(function(type) {
116           return '[column-type=' + type + ']:not([hidden])';
117         }).join(', ');
118         equivalent = this.querySelector(allTypes);
119       }
120     }
122     // Return the first focusable element if no equivalent element is found.
123     return equivalent || this.focusableElements[0];
124   },
126   /**
127    * @param {Element} element The element that should be added.
128    * @param {string} type The column type to use for the element.
129    * @private
130    */
131   addElementIfFocusable_: function(element, type) {
132     if (this.shouldFocus_(element)) {
133       this.addFocusableElement(element);
134       element.setAttribute('column-type', type);
135     }
136   },
138   /**
139    * Determines if element should be focusable.
140    * @param {Element} element
141    * @return {boolean}
142    * @private
143    */
144   shouldFocus_: function(element) {
145     if (!element)
146       return false;
148     // Hidden elements are not focusable.
149     var style = window.getComputedStyle(element);
150     if (style.visibility == 'hidden' || style.display == 'none')
151       return false;
153     // Verify all ancestors are focusable.
154     return !element.parentElement || this.shouldFocus_(element.parentElement);
155   },
158 ///////////////////////////////////////////////////////////////////////////////
159 // Downloads
161  * Class to hold all the information about the visible downloads.
162  * @constructor
163  */
164 function Downloads() {
165   /**
166    * @type {!Object<string, Download>}
167    * @private
168    */
169   this.downloads_ = {};
170   this.node_ = $('downloads-display');
171   this.summary_ = $('downloads-summary-text');
172   this.searchText_ = '';
173   this.focusGrid_ = new cr.ui.FocusGrid();
175   // Keep track of the dates of the newest and oldest downloads so that we
176   // know where to insert them.
177   this.newestTime_ = -1;
179   // Icon load request queue.
180   this.iconLoadQueue_ = [];
181   this.isIconLoading_ = false;
183   this.progressForeground1_ = new Image();
184   this.progressForeground1_.src =
185     'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@1x';
186   this.progressForeground2_ = new Image();
187   this.progressForeground2_.src =
188     'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@2x';
190   cr.ui.decorate('command', cr.ui.Command);
191   document.addEventListener('canExecute', this.onCanExecute_.bind(this));
192   document.addEventListener('command', this.onCommand_.bind(this));
196  * Called when a download has been updated or added.
197  * @param {DownloadItem} download Information about a download.
198  */
199 Downloads.prototype.updated = function(download) {
200   var id = download.id;
201   if (this.downloads_[id]) {
202     this.downloads_[id].update(download);
203   } else {
204     this.downloads_[id] = new Download(download);
205     // We get downloads in display order, so we don't have to worry about
206     // maintaining correct order - we can assume that any downloads not in
207     // display order are new ones and so we can add them to the top of the
208     // list.
209     if (download.started > this.newestTime_) {
210       this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
211       this.newestTime_ = download.started;
212     } else {
213       this.node_.appendChild(this.downloads_[id].node);
214     }
215   }
216   // Download.prototype.update may change its nodeSince_ and nodeDate_, so
217   // update all the date displays.
218   // TODO(benjhayden) Only do this if its nodeSince_ or nodeDate_ actually did
219   // change since this may touch 150 elements and Downloads.prototype.updated
220   // may be called 150 times.
221   this.onDownloadListChanged_();
225  * Set our display search text.
226  * @param {string} searchText The string we're searching for.
227  */
228 Downloads.prototype.setSearchText = function(searchText) {
229   this.searchText_ = searchText;
232 /** Update the summary block above the results. */
233 Downloads.prototype.updateSummary = function() {
234   if (this.searchText_) {
235     this.summary_.textContent = loadTimeData.getStringF('searchresultsfor',
236                                                         this.searchText_);
237   } else {
238     this.summary_.textContent = '';
239   }
243  * Called when either a search or load completes to update whether there are
244  * results or not.
245  */
246 Downloads.prototype.updateResults = function() {
247   var noDownloadsOrResults = $('no-downloads-or-results');
248   noDownloadsOrResults.textContent = loadTimeData.getString(
249       this.searchText_ ? 'no_search_results' : 'no_downloads');
251   var hasDownloads = this.size() > 0;
252   this.node_.hidden = !hasDownloads;
253   noDownloadsOrResults.hidden = hasDownloads;
255   if (loadTimeData.getBoolean('allow_deleting_history'))
256     $('clear-all').hidden = !hasDownloads || this.searchText_.length > 0;
258   this.rebuildFocusGrid_();
262  * Rebuild the focusGrid_ using the elements that each download will have.
263  * @private
264  */
265 Downloads.prototype.rebuildFocusGrid_ = function() {
266   var activeElement = document.activeElement;
267   this.focusGrid_.destroy();
269   var keys = Object.keys(this.downloads_);
270   for (var i = 0; i < keys.length; ++i) {
271     var download = this.downloads_[keys[i]];
272     DownloadFocusRow.decorate(download.node, download, this.node_);
273   }
275   // The ordering of the keys is not guaranteed, and downloads should be added
276   // to the FocusGrid in the order they will be in the UI.
277   var downloads = document.querySelectorAll('.download');
278   for (var i = 0; i < downloads.length; ++i) {
279     var focusRow = downloads[i];
280     this.focusGrid_.addRow(focusRow);
282     // Focus the equivalent element in the focusRow because the active element
283     // may no longer be visible.
284     if (focusRow.contains(activeElement))
285       focusRow.getEquivalentElement(activeElement).focus();
286   }
290  * Returns the number of downloads in the model. Used by tests.
291  * @return {number} Returns the number of downloads shown on the page.
292  */
293 Downloads.prototype.size = function() {
294   return Object.keys(this.downloads_).length;
298  * Called whenever the downloads lists items have changed (either by being
299  * updated, added, or removed).
300  * @private
301  */
302 Downloads.prototype.onDownloadListChanged_ = function() {
303   // Update the date visibility in our nodes so that no date is repeated.
304   var dateContainers = document.getElementsByClassName('date-container');
305   var displayed = {};
306   for (var i = 0, container; container = dateContainers[i]; i++) {
307     var dateString = container.getElementsByClassName('date')[0].innerHTML;
308     if (displayed[dateString]) {
309       container.style.display = 'none';
310     } else {
311       displayed[dateString] = true;
312       container.style.display = 'block';
313     }
314   }
316   this.updateResults();
320  * Remove a download.
321  * @param {string} id The id of the download to remove.
322  */
323 Downloads.prototype.remove = function(id) {
324   this.node_.removeChild(this.downloads_[id].node);
325   delete this.downloads_[id];
326   this.onDownloadListChanged_();
329 /** Clear all downloads and reset us back to a null state. */
330 Downloads.prototype.clear = function() {
331   for (var id in this.downloads_) {
332     this.downloads_[id].clear();
333     this.remove(id);
334   }
338  * Schedule icon load.
339  * @param {HTMLImageElement} elem Image element that should contain the icon.
340  * @param {string} iconURL URL to the icon.
341  */
342 Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
343   var self = this;
345   // Sends request to the next icon in the queue and schedules
346   // call to itself when the icon is loaded.
347   function loadNext() {
348     self.isIconLoading_ = true;
349     while (self.iconLoadQueue_.length > 0) {
350       var request = self.iconLoadQueue_.shift();
351       var oldSrc = request.element.src;
352       request.element.onabort = request.element.onerror =
353           request.element.onload = loadNext;
354       request.element.src = request.url;
355       if (oldSrc != request.element.src)
356         return;
357     }
358     self.isIconLoading_ = false;
359   }
361   // Create new request
362   var loadRequest = {element: elem, url: iconURL};
363   this.iconLoadQueue_.push(loadRequest);
365   // Start loading if none scheduled yet
366   if (!this.isIconLoading_)
367     loadNext();
371  * Returns whether the displayed list needs to be updated or not.
372  * @param {Array} downloads Array of download nodes.
373  * @return {boolean} Returns true if the displayed list is to be updated.
374  */
375 Downloads.prototype.isUpdateNeeded = function(downloads) {
376   var size = 0;
377   for (var i in this.downloads_)
378     size++;
379   if (size != downloads.length)
380     return true;
381   // Since there are the same number of items in the incoming list as
382   // |this.downloads_|, there won't be any removed downloads without some
383   // downloads having been inserted.  So check only for new downloads in
384   // deciding whether to update.
385   for (var i = 0; i < downloads.length; i++) {
386     if (!this.downloads_[downloads[i].id])
387       return true;
388   }
389   return false;
393  * @param {Event} e
394  * @private
395  */
396 Downloads.prototype.onCanExecute_ = function(e) {
397   e = /** @type {cr.ui.CanExecuteEvent} */(e);
398   e.canExecute = document.activeElement != $('term');
402  * @param {Event} e
403  * @private
404  */
405 Downloads.prototype.onCommand_ = function(e) {
406   if (e.command.id == 'undo-command')
407     chrome.send('undo');
408   else if (e.command.id == 'clear-all-command')
409     clearAll();
412 ///////////////////////////////////////////////////////////////////////////////
413 // Download
415  * A download and the DOM representation for that download.
416  * @param {DownloadItem} download Info about the download.
417  * @constructor
418  */
419 function Download(download) {
420   // Create DOM
421   this.node = createElementWithClassName(
422       'div', 'download' + (download.otr ? ' otr' : ''));
424   // Dates
425   this.dateContainer_ = createElementWithClassName('div', 'date-container');
426   this.node.appendChild(this.dateContainer_);
428   this.nodeSince_ = createElementWithClassName('div', 'since');
429   this.nodeDate_ = createElementWithClassName('div', 'date');
430   this.dateContainer_.appendChild(this.nodeSince_);
431   this.dateContainer_.appendChild(this.nodeDate_);
433   // Container for all 'safe download' UI.
434   this.safe_ = createElementWithClassName('div', 'safe');
435   this.safe_.ondragstart = this.drag_.bind(this);
436   this.node.appendChild(this.safe_);
438   if (download.state != Download.States.COMPLETE) {
439     this.nodeProgressBackground_ =
440         createElementWithClassName('div', 'progress background');
441     this.safe_.appendChild(this.nodeProgressBackground_);
443     this.nodeProgressForeground_ =
444         createElementWithClassName('canvas', 'progress');
445     this.nodeProgressForeground_.width = Download.Progress.width;
446     this.nodeProgressForeground_.height = Download.Progress.height;
447     this.canvasProgress_ = this.nodeProgressForeground_.getContext('2d');
449     this.safe_.appendChild(this.nodeProgressForeground_);
450   }
452   this.nodeImg_ = createElementWithClassName('img', 'icon');
453   this.nodeImg_.alt = '';
454   this.safe_.appendChild(this.nodeImg_);
456   // FileLink is used for completed downloads, otherwise we show FileName.
457   this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
458   this.safe_.appendChild(this.nodeTitleArea_);
460   this.nodeFileLink_ = createActionLink(this.openFile_.bind(this));
461   this.nodeFileLink_.className = 'name';
462   this.nodeTitleArea_.appendChild(this.nodeFileLink_);
464   this.nodeFileName_ = createElementWithClassName('span', 'name');
465   this.nodeTitleArea_.appendChild(this.nodeFileName_);
467   this.nodeStatus_ = createElementWithClassName('span', 'status');
468   this.nodeTitleArea_.appendChild(this.nodeStatus_);
470   var nodeURLDiv = createElementWithClassName('div', 'url-container');
471   this.safe_.appendChild(nodeURLDiv);
473   this.nodeURL_ = createElementWithClassName('a', 'src-url');
474   this.nodeURL_.target = '_blank';
475   nodeURLDiv.appendChild(this.nodeURL_);
477   // Controls.
478   this.nodeControls_ = createElementWithClassName('div', 'controls');
479   this.safe_.appendChild(this.nodeControls_);
481   // We don't need 'show in folder' in chromium os. See download_ui.cc and
482   // http://code.google.com/p/chromium-os/issues/detail?id=916.
483   if (loadTimeData.valueExists('control_showinfolder')) {
484     this.controlShow_ = createActionLink(this.show_.bind(this),
485         loadTimeData.getString('control_showinfolder'));
486     this.nodeControls_.appendChild(this.controlShow_);
487   } else {
488     this.controlShow_ = null;
489   }
491   this.controlRetry_ = document.createElement('a');
492   this.controlRetry_.download = '';
493   this.controlRetry_.textContent = loadTimeData.getString('control_retry');
494   this.nodeControls_.appendChild(this.controlRetry_);
496   // Pause/Resume are a toggle.
497   this.controlPause_ = createActionLink(this.pause_.bind(this),
498       loadTimeData.getString('control_pause'));
499   this.nodeControls_.appendChild(this.controlPause_);
501   this.controlResume_ = createActionLink(this.resume_.bind(this),
502       loadTimeData.getString('control_resume'));
503   this.nodeControls_.appendChild(this.controlResume_);
505   if (loadTimeData.getBoolean('allow_deleting_history')) {
506     this.controlRemove_ = createActionLink(this.remove_.bind(this),
507         loadTimeData.getString('control_removefromlist'));
508     this.controlRemove_.classList.add('control-remove-link');
509     this.nodeControls_.appendChild(this.controlRemove_);
510   }
512   this.controlCancel_ = createActionLink(this.cancel_.bind(this),
513       loadTimeData.getString('control_cancel'));
514   this.nodeControls_.appendChild(this.controlCancel_);
516   this.controlByExtension_ = document.createElement('span');
517   this.nodeControls_.appendChild(this.controlByExtension_);
519   // Container for 'unsafe download' UI.
520   this.danger_ = createElementWithClassName('div', 'show-dangerous');
521   this.node.appendChild(this.danger_);
523   this.dangerNodeImg_ = createElementWithClassName('img', 'icon');
524   this.dangerNodeImg_.alt = '';
525   this.danger_.appendChild(this.dangerNodeImg_);
527   this.dangerDesc_ = document.createElement('div');
528   this.danger_.appendChild(this.dangerDesc_);
530   // Buttons for the malicious case.
531   this.malwareNodeControls_ = createElementWithClassName('div', 'controls');
532   this.malwareSave_ = createActionLink(
533       this.saveDangerous_.bind(this),
534       loadTimeData.getString('danger_restore'));
535   this.malwareNodeControls_.appendChild(this.malwareSave_);
536   this.malwareDiscard_ = createActionLink(
537       this.discardDangerous_.bind(this),
538       loadTimeData.getString('control_removefromlist'));
539   this.malwareNodeControls_.appendChild(this.malwareDiscard_);
540   this.danger_.appendChild(this.malwareNodeControls_);
542   // Buttons for the dangerous but not malicious case.
543   this.dangerSave_ = createButton(
544       this.saveDangerous_.bind(this),
545       loadTimeData.getString('danger_save'));
546   this.danger_.appendChild(this.dangerSave_);
548   this.dangerDiscard_ = createButton(
549       this.discardDangerous_.bind(this),
550       loadTimeData.getString('danger_discard'));
551   this.danger_.appendChild(this.dangerDiscard_);
553   // Update member vars.
554   this.update(download);
558  * The states a download can be in. These correspond to states defined in
559  * DownloadsDOMHandler::CreateDownloadItemValue
560  * @enum {string}
561  */
562 Download.States = {
563   IN_PROGRESS: 'IN_PROGRESS',
564   CANCELLED: 'CANCELLED',
565   COMPLETE: 'COMPLETE',
566   PAUSED: 'PAUSED',
567   DANGEROUS: 'DANGEROUS',
568   INTERRUPTED: 'INTERRUPTED',
572  * Explains why a download is in DANGEROUS state.
573  * @enum {string}
574  */
575 Download.DangerType = {
576   NOT_DANGEROUS: 'NOT_DANGEROUS',
577   DANGEROUS_FILE: 'DANGEROUS_FILE',
578   DANGEROUS_URL: 'DANGEROUS_URL',
579   DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
580   UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
581   DANGEROUS_HOST: 'DANGEROUS_HOST',
582   POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
586  * @param {number} a Some float.
587  * @param {number} b Some float.
588  * @param {number=} opt_pct Percent of min(a,b).
589  * @return {boolean} true if a is within opt_pct percent of b.
590  */
591 function floatEq(a, b, opt_pct) {
592   return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
595 /** Constants and "constants" for the progress meter. */
596 Download.Progress = {
597   START_ANGLE: -0.5 * Math.PI,
598   SIDE: 48,
601 /***/
602 Download.Progress.HALF = Download.Progress.SIDE / 2;
604 function computeDownloadProgress() {
605   if (floatEq(Download.Progress.scale, window.devicePixelRatio)) {
606     // Zooming in or out multiple times then typing Ctrl+0 resets the zoom level
607     // directly to 1x, which fires the matchMedia event multiple times.
608     return;
609   }
610   Download.Progress.scale = window.devicePixelRatio;
611   Download.Progress.width = Download.Progress.SIDE * Download.Progress.scale;
612   Download.Progress.height = Download.Progress.SIDE * Download.Progress.scale;
613   Download.Progress.radius = Download.Progress.HALF * Download.Progress.scale;
614   Download.Progress.centerX = Download.Progress.HALF * Download.Progress.scale;
615   Download.Progress.centerY = Download.Progress.HALF * Download.Progress.scale;
617 computeDownloadProgress();
619 // Listens for when device-pixel-ratio changes between any zoom level.
620 [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
621 ].forEach(function(scale) {
622   var media = '(-webkit-min-device-pixel-ratio:' + scale + ')';
623   window.matchMedia(media).addListener(computeDownloadProgress);
627  * Updates the download to reflect new data.
628  * @param {DownloadItem} download Updated info about this download.
629  */
630 Download.prototype.update = function(download) {
631   this.id_ = download.id;
632   this.filePath_ = download.file_path;
633   this.fileUrl_ = download.file_url;
634   this.fileName_ = download.file_name;
635   this.url_ = download.url;
636   this.state_ = download.state;
637   this.fileExternallyRemoved_ = download.file_externally_removed;
638   this.dangerType_ = download.danger_type;
639   this.lastReasonDescription_ = download.last_reason_text;
640   this.byExtensionId_ = download.by_ext_id;
641   this.byExtensionName_ = download.by_ext_name;
643   this.since_ = download.since_string;
644   this.date_ = download.date_string;
646   // See DownloadItem::PercentComplete
647   this.percent_ = Math.max(download.percent, 0);
648   this.progressStatusText_ = download.progress_status_text;
649   this.received_ = download.received;
651   if (this.state_ == Download.States.DANGEROUS) {
652     this.updateDangerousFile();
653   } else {
654     downloads.scheduleIconLoad(this.nodeImg_,
655                                'chrome://fileicon/' +
656                                    encodeURIComponent(this.filePath_) +
657                                    '?scale=' + window.devicePixelRatio + 'x');
659     if (this.state_ == Download.States.COMPLETE &&
660         !this.fileExternallyRemoved_) {
661       this.nodeFileLink_.textContent = this.fileName_;
662       this.nodeFileLink_.href = this.fileUrl_;
663       this.nodeFileLink_.oncontextmenu = null;
664     } else if (this.nodeFileName_.textContent != this.fileName_) {
665       this.nodeFileName_.textContent = this.fileName_;
666     }
667     if (this.state_ == Download.States.INTERRUPTED) {
668       this.nodeFileName_.classList.add('interrupted');
669     } else if (this.nodeFileName_.classList.contains('interrupted')) {
670       this.nodeFileName_.classList.remove('interrupted');
671     }
673     var completelyOnDisk = this.state_ == Download.States.COMPLETE &&
674                            !this.fileExternallyRemoved_;
675     this.nodeFileName_.hidden = completelyOnDisk;
676     this.nodeFileLink_.hidden = !completelyOnDisk;
678     if (this.state_ == Download.States.IN_PROGRESS) {
679       this.nodeProgressForeground_.style.display = 'block';
680       this.nodeProgressBackground_.style.display = 'block';
681       this.nodeProgressForeground_.width = Download.Progress.width;
682       this.nodeProgressForeground_.height = Download.Progress.height;
684       var foregroundImage = (window.devicePixelRatio < 2) ?
685         downloads.progressForeground1_ : downloads.progressForeground2_;
687       // Draw a pie-slice for the progress.
688       this.canvasProgress_.globalCompositeOperation = 'copy';
689       this.canvasProgress_.drawImage(
690           foregroundImage,
691           0, 0,  // sx, sy
692           foregroundImage.width,
693           foregroundImage.height,
694           0, 0,  // x, y
695           Download.Progress.width, Download.Progress.height);
696       this.canvasProgress_.globalCompositeOperation = 'destination-in';
697       this.canvasProgress_.beginPath();
698       this.canvasProgress_.moveTo(Download.Progress.centerX,
699                                   Download.Progress.centerY);
701       // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
702       this.canvasProgress_.arc(Download.Progress.centerX,
703                                Download.Progress.centerY,
704                                Download.Progress.radius,
705                                Download.Progress.START_ANGLE,
706                                Download.Progress.START_ANGLE + Math.PI * 0.02 *
707                                Number(this.percent_),
708                                false);
710       this.canvasProgress_.lineTo(Download.Progress.centerX,
711                                   Download.Progress.centerY);
712       this.canvasProgress_.fill();
713       this.canvasProgress_.closePath();
714     } else if (this.nodeProgressBackground_) {
715       this.nodeProgressForeground_.style.display = 'none';
716       this.nodeProgressBackground_.style.display = 'none';
717     }
719     if (this.controlShow_)
720       this.controlShow_.hidden = !completelyOnDisk;
721     this.controlRetry_.hidden = !download.retry;
722     this.controlRetry_.href = this.url_;
723     this.controlPause_.hidden = this.state_ != Download.States.IN_PROGRESS;
724     this.controlResume_.hidden = !download.resume;
725     var showCancel = this.state_ == Download.States.IN_PROGRESS ||
726                      this.state_ == Download.States.PAUSED;
727     this.controlCancel_.hidden = !showCancel;
728     if (this.controlRemove_)
729       this.controlRemove_.hidden = showCancel;
731     if (this.byExtensionId_ && this.byExtensionName_) {
732       // Format 'control_by_extension' with a link instead of plain text by
733       // splitting the formatted string into pieces.
734       var slug = 'XXXXX';
735       var formatted = loadTimeData.getStringF('control_by_extension', slug);
736       var slugIndex = formatted.indexOf(slug);
737       this.controlByExtension_.textContent = formatted.substr(0, slugIndex);
738       this.controlByExtensionLink_ = document.createElement('a');
739       this.controlByExtensionLink_.href =
740           'chrome://extensions#' + this.byExtensionId_;
741       this.controlByExtensionLink_.textContent = this.byExtensionName_;
742       this.controlByExtension_.appendChild(this.controlByExtensionLink_);
743       if (slugIndex < (formatted.length - slug.length))
744         this.controlByExtension_.appendChild(document.createTextNode(
745             formatted.substr(slugIndex + 1)));
746     }
748     this.nodeSince_.textContent = this.since_;
749     this.nodeDate_.textContent = this.date_;
750     // Don't unnecessarily update the url, as doing so will remove any
751     // text selection the user has started (http://crbug.com/44982).
752     if (this.nodeURL_.textContent != this.url_) {
753       this.nodeURL_.textContent = this.url_;
754       this.nodeURL_.href = this.url_;
755     }
756     this.nodeStatus_.textContent = this.getStatusText_();
758     this.danger_.style.display = 'none';
759     this.safe_.style.display = 'block';
760   }
764  * Decorates the icons, strings, and buttons for a download to reflect the
765  * danger level of a file. Dangerous & malicious files are treated differently.
766  */
767 Download.prototype.updateDangerousFile = function() {
768   switch (this.dangerType_) {
769     case Download.DangerType.DANGEROUS_FILE: {
770       this.dangerDesc_.textContent = loadTimeData.getStringF(
771           'danger_file_desc', this.fileName_);
772       break;
773     }
774     case Download.DangerType.DANGEROUS_URL: {
775       this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
776       break;
777     }
778     case Download.DangerType.DANGEROUS_CONTENT:  // Fall through.
779     case Download.DangerType.DANGEROUS_HOST: {
780       this.dangerDesc_.textContent = loadTimeData.getStringF(
781           'danger_content_desc', this.fileName_);
782       break;
783     }
784     case Download.DangerType.UNCOMMON_CONTENT: {
785       this.dangerDesc_.textContent = loadTimeData.getStringF(
786           'danger_uncommon_desc', this.fileName_);
787       break;
788     }
789     case Download.DangerType.POTENTIALLY_UNWANTED: {
790       this.dangerDesc_.textContent = loadTimeData.getStringF(
791           'danger_settings_desc', this.fileName_);
792       break;
793     }
794   }
796   if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
797     downloads.scheduleIconLoad(
798         this.dangerNodeImg_,
799         'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
800   } else {
801     downloads.scheduleIconLoad(
802         this.dangerNodeImg_,
803         'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
804             window.devicePixelRatio + 'x');
805     this.dangerDesc_.className = 'malware-description';
806   }
808   if (this.dangerType_ == Download.DangerType.DANGEROUS_CONTENT ||
809       this.dangerType_ == Download.DangerType.DANGEROUS_HOST ||
810       this.dangerType_ == Download.DangerType.DANGEROUS_URL ||
811       this.dangerType_ == Download.DangerType.POTENTIALLY_UNWANTED) {
812     this.malwareNodeControls_.style.display = 'block';
813     this.dangerDiscard_.style.display = 'none';
814     this.dangerSave_.style.display = 'none';
815   } else {
816     this.malwareNodeControls_.style.display = 'none';
817     this.dangerDiscard_.style.display = 'inline';
818     this.dangerSave_.style.display = 'inline';
819   }
821   this.danger_.style.display = 'block';
822   this.safe_.style.display = 'none';
825 /** Removes applicable bits from the DOM in preparation for deletion. */
826 Download.prototype.clear = function() {
827   this.safe_.ondragstart = null;
828   this.nodeFileLink_.onclick = null;
829   if (this.controlShow_) {
830     this.controlShow_.onclick = null;
831   }
832   this.controlCancel_.onclick = null;
833   this.controlPause_.onclick = null;
834   this.controlResume_.onclick = null;
835   this.dangerDiscard_.onclick = null;
836   this.dangerSave_.onclick = null;
837   this.malwareDiscard_.onclick = null;
838   this.malwareSave_.onclick = null;
840   this.node.innerHTML = '';
844  * @private
845  * @return {string} User-visible status update text.
846  */
847 Download.prototype.getStatusText_ = function() {
848   switch (this.state_) {
849     case Download.States.IN_PROGRESS:
850       return this.progressStatusText_;
851     case Download.States.CANCELLED:
852       return loadTimeData.getString('status_cancelled');
853     case Download.States.PAUSED:
854       return loadTimeData.getString('status_paused');
855     case Download.States.DANGEROUS:
856       // danger_url_desc is also used by DANGEROUS_CONTENT.
857       var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ?
858           'danger_file_desc' : 'danger_url_desc';
859       return loadTimeData.getString(desc);
860     case Download.States.INTERRUPTED:
861       return this.lastReasonDescription_;
862     case Download.States.COMPLETE:
863       return this.fileExternallyRemoved_ ?
864           loadTimeData.getString('status_removed') : '';
865   }
866   assertNotReached();
867   return '';
871  * Tells the backend to initiate a drag, allowing users to drag
872  * files from the download page and have them appear as native file
873  * drags.
874  * @return {boolean} Returns false to prevent the default action.
875  * @private
876  */
877 Download.prototype.drag_ = function() {
878   chrome.send('drag', [this.id_]);
879   return false;
883  * Tells the backend to open this file.
884  * @return {boolean} Returns false to prevent the default action.
885  * @private
886  */
887 Download.prototype.openFile_ = function() {
888   chrome.send('openFile', [this.id_]);
889   return false;
893  * Tells the backend that the user chose to save a dangerous file.
894  * @return {boolean} Returns false to prevent the default action.
895  * @private
896  */
897 Download.prototype.saveDangerous_ = function() {
898   chrome.send('saveDangerous', [this.id_]);
899   return false;
903  * Tells the backend that the user chose to discard a dangerous file.
904  * @return {boolean} Returns false to prevent the default action.
905  * @private
906  */
907 Download.prototype.discardDangerous_ = function() {
908   chrome.send('discardDangerous', [this.id_]);
909   downloads.remove(this.id_);
910   return false;
914  * Tells the backend to show the file in explorer.
915  * @return {boolean} Returns false to prevent the default action.
916  * @private
917  */
918 Download.prototype.show_ = function() {
919   chrome.send('show', [this.id_]);
920   return false;
924  * Tells the backend to pause this download.
925  * @return {boolean} Returns false to prevent the default action.
926  * @private
927  */
928 Download.prototype.pause_ = function() {
929   chrome.send('pause', [this.id_]);
930   return false;
934  * Tells the backend to resume this download.
935  * @return {boolean} Returns false to prevent the default action.
936  * @private
937  */
938 Download.prototype.resume_ = function() {
939   chrome.send('resume', [this.id_]);
940   return false;
944  * Tells the backend to remove this download from history and download shelf.
945  * @return {boolean} Returns false to prevent the default action.
946  * @private
947  */
948 Download.prototype.remove_ = function() {
949   assert(loadTimeData.getBoolean('allow_deleting_history'));
950   chrome.send('remove', [this.id_]);
951   return false;
955  * Tells the backend to cancel this download.
956  * @return {boolean} Returns false to prevent the default action.
957  * @private
958  */
959 Download.prototype.cancel_ = function() {
960   chrome.send('cancel', [this.id_]);
961   return false;
964 ///////////////////////////////////////////////////////////////////////////////
965 // Page:
966 var downloads, resultsTimeout;
968 // TODO(benjhayden): Rename Downloads to DownloadManager, downloads to
969 // downloadManager or theDownloadManager or DownloadManager.get() to prevent
970 // confusing Downloads with Download.
973  * The FIFO array that stores updates of download files to be appeared
974  * on the download page. It is guaranteed that the updates in this array
975  * are reflected to the download page in a FIFO order.
977 var fifoResults;
979 function load() {
980   chrome.send('onPageLoaded');
981   fifoResults = [];
982   downloads = new Downloads();
983   $('term').focus();
984   setSearch('');
986   $('clear-all').onclick = function() {
987     chrome.send('clearAll');
988   };
990   $('open-downloads-folder').onclick = function() {
991     chrome.send('openDownloadsFolder');
992   };
994   $('term').onsearch = function(e) {
995     setSearch($('term').value);
996   };
999 function setSearch(searchText) {
1000   fifoResults.length = 0;
1001   downloads.setSearchText(searchText);
1002   searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
1003   if (searchText) {
1004     searchText = searchText.map(function(term) {
1005       // strip quotes
1006       return (term.match(/\s/) &&
1007               term[0].match(/["']/) &&
1008               term[term.length - 1] == term[0]) ?
1009         term.substr(1, term.length - 2) : term;
1010     });
1011   } else {
1012     searchText = [];
1013   }
1014   chrome.send('getDownloads', searchText);
1017 function clearAll() {
1018   if (!loadTimeData.getBoolean('allow_deleting_history'))
1019     return;
1021   fifoResults.length = 0;
1022   downloads.clear();
1023   downloads.setSearchText('');
1024   chrome.send('clearAll');
1027 ///////////////////////////////////////////////////////////////////////////////
1028 // Chrome callbacks:
1030  * Our history system calls this function with results from searches or when
1031  * downloads are added or removed.
1032  * @param {Array<Object>} results List of updates.
1033  */
1034 function downloadsList(results) {
1035   if (downloads && downloads.isUpdateNeeded(results)) {
1036     if (resultsTimeout)
1037       clearTimeout(resultsTimeout);
1038     fifoResults.length = 0;
1039     downloads.clear();
1040     downloadUpdated(results);
1041   }
1042   downloads.updateResults();
1043   downloads.updateSummary();
1047  * When a download is updated (progress, state change), this is called.
1048  * @param {Array<Object>} results List of updates for the download process.
1049  */
1050 function downloadUpdated(results) {
1051   // Sometimes this can get called too early.
1052   if (!downloads)
1053     return;
1055   fifoResults = fifoResults.concat(results);
1056   tryDownloadUpdatedPeriodically();
1060  * Try to reflect as much updates as possible within 50ms.
1061  * This function is scheduled again and again until all updates are reflected.
1062  */
1063 function tryDownloadUpdatedPeriodically() {
1064   var start = Date.now();
1065   while (fifoResults.length) {
1066     var result = fifoResults.shift();
1067     downloads.updated(result);
1068     // Do as much as we can in 50ms.
1069     if (Date.now() - start > 50) {
1070       clearTimeout(resultsTimeout);
1071       resultsTimeout = setTimeout(tryDownloadUpdatedPeriodically, 5);
1072       break;
1073     }
1074   }
1077 // Add handlers to HTML elements.
1078 window.addEventListener('DOMContentLoaded', load);