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