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(jhawkins): Use hidden instead of showInline* and display:none.
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,
19 * last_reason_text: (string|undefined),
21 * percent: (number|undefined),
22 * progress_status_text: (string|undefined),
23 * received: (number|undefined),
26 * since_string: string,
32 var BackendDownloadObject;
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.
39 function showInline(node, isShow) {
40 node.style.display = isShow ? 'inline' : 'none';
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.
48 function showInlineBlock(node, isShow) {
49 node.style.display = isShow ? 'inline-block' : 'none';
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.
58 function createActionLink(onclick, opt_text) {
59 var link = new ActionLink;
60 link.onclick = onclick;
61 if (opt_text) link.textContent = opt_text;
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.
71 function createButton(onclick, value) {
72 var button = document.createElement('input');
73 button.type = 'button';
75 button.onclick = onclick;
79 ///////////////////////////////////////////////////////////////////////////////
82 * Class to hold all the information about the visible downloads.
85 function Downloads() {
87 * @type {!Object.<string, Download>}
91 this.node_ = $('downloads-display');
92 this.noDownloadsOrResults_ = $('no-downloads-or-results');
93 this.summary_ = $('downloads-summary-text');
94 this.searchText_ = '';
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;
100 // Icon load request queue.
101 this.iconLoadQueue_ = [];
102 this.isIconLoading_ = false;
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';
111 window.addEventListener('keydown', this.onKeyDown_.bind(this));
113 this.onDownloadListChanged_();
117 * Called when a download has been updated or added.
118 * @param {BackendDownloadObject} download A backend download object
120 Downloads.prototype.updated = function(download) {
121 var id = download.id;
122 if (!!this.downloads_[id]) {
123 this.downloads_[id].update(download);
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
130 if (download.started > this.newestTime_) {
131 this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
132 this.newestTime_ = download.started;
134 this.node_.appendChild(this.downloads_[id].node);
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_();
146 * Set our display search text.
147 * @param {string} searchText The string we're searching for.
149 Downloads.prototype.setSearchText = function(searchText) {
150 this.searchText_ = searchText;
154 * Update the summary block above the results
156 Downloads.prototype.updateSummary = function() {
157 if (this.searchText_) {
158 this.summary_.textContent = loadTimeData.getStringF('searchresultsfor',
161 this.summary_.textContent = '';
166 * Returns the number of downloads in the model. Used by tests.
167 * @return {number} Returns the number of downloads shown on the page.
169 Downloads.prototype.size = function() {
170 return Object.keys(this.downloads_).length;
174 * Called whenever the downloads lists items have changed (either by being
175 * updated, added, or removed).
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');
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';
187 displayed[dateString] = true;
188 container.style.display = 'block';
192 this.noDownloadsOrResults_.textContent = loadTimeData.getString(
193 this.searchText_ ? 'no_search_results' : 'no_downloads');
195 var hasDownloads = this.size() > 0;
196 this.node_.hidden = !hasDownloads;
197 this.noDownloadsOrResults_.hidden = hasDownloads;
202 * @param {string} id The id of the download to remove.
204 Downloads.prototype.remove = function(id) {
205 this.node_.removeChild(this.downloads_[id].node);
206 delete this.downloads_[id];
207 this.onDownloadListChanged_();
211 * Clear all downloads and reset us back to a null state.
213 Downloads.prototype.clear = function() {
214 for (var id in this.downloads_) {
215 this.downloads_[id].clear();
221 * Schedule icon load.
222 * @param {HTMLImageElement} elem Image element that should contain the icon.
223 * @param {string} iconURL URL to the icon.
225 Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
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)
241 self.isIconLoading_ = false;
244 // Create new request
245 var loadRequest = {element: elem, url: iconURL};
246 this.iconLoadQueue_.push(loadRequest);
248 // Start loading if none scheduled yet
249 if (!this.isIconLoading_)
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.
258 Downloads.prototype.isUpdateNeeded = function(downloads) {
260 for (var i in this.downloads_)
262 if (size != downloads.length)
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])
276 * Handles shortcut keys.
277 * @param {Event} evt The keyboard event.
280 Downloads.prototype.onKeyDown_ = function(evt) {
281 var keyEvt = /** @type {KeyboardEvent} */(evt);
282 if (keyEvt.keyCode == 67 && keyEvt.altKey) { // alt + c.
284 keyEvt.preventDefault();
288 ///////////////////////////////////////////////////////////////////////////////
291 * A download and the DOM representation for that download.
292 * @param {BackendDownloadObject} download A backend download object
295 function Download(download) {
297 this.node = createElementWithClassName(
298 'div', 'download' + (download.otr ? ' otr' : ''));
301 this.dateContainer_ = createElementWithClassName('div', 'date-container');
302 this.node.appendChild(this.dateContainer_);
304 this.nodeSince_ = createElementWithClassName('div', 'since');
305 this.nodeDate_ = createElementWithClassName('div', 'date');
306 this.dateContainer_.appendChild(this.nodeSince_);
307 this.dateContainer_.appendChild(this.nodeDate_);
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_);
314 if (download.state != Download.States.COMPLETE) {
315 this.nodeProgressBackground_ =
316 createElementWithClassName('div', 'progress background');
317 this.safe_.appendChild(this.nodeProgressBackground_);
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');
325 this.safe_.appendChild(this.nodeProgressForeground_);
328 this.nodeImg_ = createElementWithClassName('img', 'icon');
329 this.nodeImg_.alt = '';
330 this.safe_.appendChild(this.nodeImg_);
332 // FileLink is used for completed downloads, otherwise we show FileName.
333 this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
334 this.safe_.appendChild(this.nodeTitleArea_);
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_);
341 this.nodeFileName_ = createElementWithClassName('span', 'name');
342 this.nodeFileName_.style.display = 'none';
343 this.nodeTitleArea_.appendChild(this.nodeFileName_);
345 this.nodeStatus_ = createElementWithClassName('span', 'status');
346 this.nodeTitleArea_.appendChild(this.nodeStatus_);
348 var nodeURLDiv = createElementWithClassName('div', 'url-container');
349 this.safe_.appendChild(nodeURLDiv);
351 this.nodeURL_ = createElementWithClassName('a', 'src-url');
352 this.nodeURL_.target = '_blank';
353 nodeURLDiv.appendChild(this.nodeURL_);
356 this.nodeControls_ = createElementWithClassName('div', 'controls');
357 this.safe_.appendChild(this.nodeControls_);
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_);
366 this.controlShow_ = null;
369 this.controlRetry_ = document.createElement('a');
370 this.controlRetry_.download = '';
371 this.controlRetry_.textContent = loadTimeData.getString('control_retry');
372 this.nodeControls_.appendChild(this.controlRetry_);
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_);
379 this.controlResume_ = createActionLink(this.resume_.bind(this),
380 loadTimeData.getString('control_resume'));
381 this.nodeControls_.appendChild(this.controlResume_);
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');
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);
395 if (!loadTimeData.getBoolean('show_delete_history'))
396 this.controlRemove_.hidden = true;
398 this.nodeControls_.appendChild(this.controlRemove_);
400 this.controlCancel_ = createActionLink(this.cancel_.bind(this),
401 loadTimeData.getString('control_cancel'));
402 this.nodeControls_.appendChild(this.controlCancel_);
404 this.controlByExtension_ = document.createElement('span');
405 this.nodeControls_.appendChild(this.controlByExtension_);
407 // Container for 'unsafe download' UI.
408 this.danger_ = createElementWithClassName('div', 'show-dangerous');
409 this.node.appendChild(this.danger_);
411 this.dangerNodeImg_ = createElementWithClassName('img', 'icon');
412 this.dangerNodeImg_.alt = '';
413 this.danger_.appendChild(this.dangerNodeImg_);
415 this.dangerDesc_ = document.createElement('div');
416 this.danger_.appendChild(this.dangerDesc_);
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_);
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_);
436 this.dangerDiscard_ = createButton(
437 this.discardDangerous_.bind(this),
438 loadTimeData.getString('danger_discard'));
439 this.danger_.appendChild(this.dangerDiscard_);
441 // Update member vars.
442 this.update(download);
446 * The states a download can be in. These correspond to states defined in
447 * DownloadsDOMHandler::CreateDownloadItemValue
451 IN_PROGRESS: 'IN_PROGRESS',
452 CANCELLED: 'CANCELLED',
453 COMPLETE: 'COMPLETE',
455 DANGEROUS: 'DANGEROUS',
456 INTERRUPTED: 'INTERRUPTED',
460 * Explains why a download is in DANGEROUS state.
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',
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.
479 function floatEq(a, b, opt_pct) {
480 return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
484 * Constants and "constants" for the progress meter.
486 Download.Progress = {
487 START_ANGLE: -0.5 * Math.PI,
492 Download.Progress.HALF = Download.Progress.SIDE / 2;
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.
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;
507 computeDownloadProgress();
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);
517 * Updates the download to reflect new data.
518 * @param {BackendDownloadObject} download A backend download object
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;
533 this.since_ = download.since_string;
534 this.date_ = download.date_string;
536 // See DownloadItem::PercentComplete
537 this.percent_ = Math.max(download.percent, 0);
538 this.progressStatusText_ = download.progress_status_text;
539 this.received_ = download.received;
541 if (this.state_ == Download.States.DANGEROUS) {
542 this.updateDangerousFile();
544 downloads.scheduleIconLoad(this.nodeImg_,
545 'chrome://fileicon/' +
546 encodeURIComponent(this.filePath_) +
547 '?scale=' + window.devicePixelRatio + 'x');
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_;
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');
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_);
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;
580 var foregroundImage = (window.devicePixelRatio < 2) ?
581 downloads.progressForeground1_ : downloads.progressForeground2_;
583 // Draw a pie-slice for the progress.
584 this.canvasProgress_.globalCompositeOperation = 'copy';
585 this.canvasProgress_.drawImage(
588 foregroundImage.width,
589 foregroundImage.height,
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);
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_),
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';
615 if (this.controlShow_) {
616 showInline(this.controlShow_,
617 this.state_ == Download.States.COMPLETE &&
618 !this.fileExternallyRemoved_);
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);
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.
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)));
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_;
654 this.nodeStatus_.textContent = this.getStatusText_();
656 this.danger_.style.display = 'none';
657 this.safe_.style.display = 'block';
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.
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_);
672 case Download.DangerType.DANGEROUS_URL: {
673 this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
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_);
682 case Download.DangerType.UNCOMMON_CONTENT: {
683 this.dangerDesc_.textContent = loadTimeData.getStringF(
684 'danger_uncommon_desc', this.fileName_);
687 case Download.DangerType.POTENTIALLY_UNWANTED: {
688 this.dangerDesc_.textContent = loadTimeData.getStringF(
689 'danger_settings_desc', this.fileName_);
694 if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
695 downloads.scheduleIconLoad(
697 'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
699 downloads.scheduleIconLoad(
701 'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
702 window.devicePixelRatio + 'x');
703 this.dangerDesc_.className = 'malware-description';
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';
714 this.malwareNodeControls_.style.display = 'none';
715 this.dangerDiscard_.style.display = 'inline';
716 this.dangerSave_.style.display = 'inline';
719 this.danger_.style.display = 'block';
720 this.safe_.style.display = 'none';
724 * Removes applicable bits from the DOM in preparation for deletion.
726 Download.prototype.clear = function() {
727 this.safe_.ondragstart = null;
728 this.nodeFileLink_.onclick = null;
729 if (this.controlShow_) {
730 this.controlShow_.onclick = null;
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;
740 this.node.innerHTML = '';
745 * @return {string} User-visible status update text.
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') : '';
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
774 * @return {boolean} Returns false to prevent the default action.
777 Download.prototype.drag_ = function() {
778 chrome.send('drag', [this.id_.toString()]);
783 * Tells the backend to open this file.
784 * @return {boolean} Returns false to prevent the default action.
787 Download.prototype.openFile_ = function() {
788 chrome.send('openFile', [this.id_.toString()]);
793 * Tells the backend that the user chose to save a dangerous file.
794 * @return {boolean} Returns false to prevent the default action.
797 Download.prototype.saveDangerous_ = function() {
798 chrome.send('saveDangerous', [this.id_.toString()]);
803 * Tells the backend that the user chose to discard a dangerous file.
804 * @return {boolean} Returns false to prevent the default action.
807 Download.prototype.discardDangerous_ = function() {
808 chrome.send('discardDangerous', [this.id_.toString()]);
809 downloads.remove(this.id_);
814 * Tells the backend to show the file in explorer.
815 * @return {boolean} Returns false to prevent the default action.
818 Download.prototype.show_ = function() {
819 chrome.send('show', [this.id_.toString()]);
824 * Tells the backend to pause this download.
825 * @return {boolean} Returns false to prevent the default action.
828 Download.prototype.pause_ = function() {
829 chrome.send('pause', [this.id_.toString()]);
834 * Tells the backend to resume this download.
835 * @return {boolean} Returns false to prevent the default action.
838 Download.prototype.resume_ = function() {
839 chrome.send('resume', [this.id_.toString()]);
844 * Tells the backend to remove this download from history and download shelf.
845 * @return {boolean} Returns false to prevent the default action.
848 Download.prototype.remove_ = function() {
849 if (loadTimeData.getBoolean('allow_deleting_history')) {
850 chrome.send('remove', [this.id_.toString()]);
856 * Tells the backend to cancel this download.
857 * @return {boolean} Returns false to prevent the default action.
860 Download.prototype.cancel_ = function() {
861 chrome.send('cancel', [this.id_.toString()]);
865 ///////////////////////////////////////////////////////////////////////////////
867 var downloads, resultsTimeout;
869 // TODO(benjhayden): Rename Downloads to DownloadManager, downloads to
870 // downloadManager or theDownloadManager or DownloadManager.get() to prevent
871 // confusing Downloads with Download.
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.
881 chrome.send('onPageLoaded');
883 downloads = new Downloads();
887 var clearAllHolder = $('clear-all-holder');
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');
895 clearAllElement = document.createTextNode(
896 loadTimeData.getString('clear_all'));
897 clearAllHolder.classList.add('disabled-link');
899 if (!loadTimeData.getBoolean('show_delete_history'))
900 clearAllHolder.hidden = true;
902 clearAllHolder.appendChild(clearAllElement);
904 $('open-downloads-folder').onclick = function() {
905 chrome.send('openDownloadsFolder');
908 $('term').onsearch = function(e) {
909 setSearch($('term').value);
913 function setSearch(searchText) {
914 fifoResults.length = 0;
915 downloads.setSearchText(searchText);
916 searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
918 searchText = searchText.map(function(term) {
920 return (term.match(/\s/) &&
921 term[0].match(/["']/) &&
922 term[term.length - 1] == term[0]) ?
923 term.substr(1, term.length - 2) : term;
928 chrome.send('getDownloads', searchText);
931 function clearAll() {
932 if (!loadTimeData.getBoolean('allow_deleting_history'))
935 fifoResults.length = 0;
937 downloads.setSearchText('');
938 chrome.send('clearAll');
941 ///////////////////////////////////////////////////////////////////////////////
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.
948 function downloadsList(results) {
949 if (downloads && downloads.isUpdateNeeded(results)) {
951 clearTimeout(resultsTimeout);
952 fifoResults.length = 0;
954 downloadUpdated(results);
956 downloads.updateSummary();
960 * When a download is updated (progress, state change), this is called.
961 * @param {Array.<Object>} results List of updates for the download process.
963 function downloadUpdated(results) {
964 // Sometimes this can get called too early.
968 fifoResults = fifoResults.concat(results);
969 tryDownloadUpdatedPeriodically();
973 * Try to reflect as much updates as possible within 50ms.
974 * This function is scheduled again and again until all updates are reflected.
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);
990 // Add handlers to HTML elements.
991 window.addEventListener('DOMContentLoaded', load);