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.
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,
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.
40 function createActionLink(onclick, opt_text) {
41 var link = new ActionLink;
42 link.onclick = onclick;
43 if (opt_text) link.textContent = opt_text;
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.
53 function createButton(onclick, value) {
54 var button = document.createElement('input');
55 button.type = 'button';
57 button.onclick = onclick;
61 ///////////////////////////////////////////////////////////////////////////////
65 * Provides an implementation for a single column grid.
67 * @extends {cr.ui.FocusRow}
69 function DownloadFocusRow() {}
72 * Decorates |focusRow| so that it can be treated as a DownloadFocusRow.
73 * @param {Element} focusRow The element that has all the columns represented
75 * @param {Download} download The Download representing this row.
76 * @param {Node} boundary Focus events are ignored outside of this node.
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_,
99 DownloadFocusRow.prototype = {
100 __proto__: cr.ui.FocusRow.prototype,
103 getEquivalentElement: function(element) {
104 if (this.focusableElements.indexOf(element) > -1)
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])';
118 equivalent = this.querySelector(allTypes);
122 // Return the first focusable element if no equivalent element is found.
123 return equivalent || this.focusableElements[0];
127 * @param {Element} element The element that should be added.
128 * @param {string} type The column type to use for the element.
131 addElementIfFocusable_: function(element, type) {
132 if (this.shouldFocus_(element)) {
133 this.addFocusableElement(element);
134 element.setAttribute('column-type', type);
139 * Determines if element should be focusable.
140 * @param {Element} element
144 shouldFocus_: function(element) {
148 // Hidden elements are not focusable.
149 var style = window.getComputedStyle(element);
150 if (style.visibility == 'hidden' || style.display == 'none')
153 // Verify all ancestors are focusable.
154 return !element.parentElement || this.shouldFocus_(element.parentElement);
158 ///////////////////////////////////////////////////////////////////////////////
161 * Class to hold all the information about the visible downloads.
164 function Downloads() {
166 * @type {!Object<string, Download>}
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.
199 Downloads.prototype.updated = function(download) {
200 var id = download.id;
201 if (this.downloads_[id]) {
202 this.downloads_[id].update(download);
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
209 if (download.started > this.newestTime_) {
210 this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
211 this.newestTime_ = download.started;
213 this.node_.appendChild(this.downloads_[id].node);
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.
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',
238 this.summary_.textContent = '';
243 * Called when either a search or load completes to update whether there are
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.
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_);
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();
290 * Returns the number of downloads in the model. Used by tests.
291 * @return {number} Returns the number of downloads shown on the page.
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).
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');
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';
311 displayed[dateString] = true;
312 container.style.display = 'block';
316 this.updateResults();
321 * @param {string} id The id of the download to remove.
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();
338 * Schedule icon load.
339 * @param {HTMLImageElement} elem Image element that should contain the icon.
340 * @param {string} iconURL URL to the icon.
342 Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
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)
358 self.isIconLoading_ = false;
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_)
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.
375 Downloads.prototype.isUpdateNeeded = function(downloads) {
377 for (var i in this.downloads_)
379 if (size != downloads.length)
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])
396 Downloads.prototype.onCanExecute_ = function(e) {
397 e = /** @type {cr.ui.CanExecuteEvent} */(e);
398 e.canExecute = document.activeElement != $('term');
405 Downloads.prototype.onCommand_ = function(e) {
406 if (e.command.id == 'undo-command')
408 else if (e.command.id == 'clear-all-command')
412 ///////////////////////////////////////////////////////////////////////////////
415 * A download and the DOM representation for that download.
416 * @param {DownloadItem} download Info about the download.
419 function Download(download) {
421 this.node = createElementWithClassName(
422 'div', 'download' + (download.otr ? ' otr' : ''));
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_);
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_);
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_);
488 this.controlShow_ = null;
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_);
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
563 IN_PROGRESS: 'IN_PROGRESS',
564 CANCELLED: 'CANCELLED',
565 COMPLETE: 'COMPLETE',
567 DANGEROUS: 'DANGEROUS',
568 INTERRUPTED: 'INTERRUPTED',
572 * Explains why a download is in DANGEROUS state.
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.
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,
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.
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.
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();
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_;
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');
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(
692 foregroundImage.width,
693 foregroundImage.height,
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_),
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';
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.
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)));
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_;
756 this.nodeStatus_.textContent = this.getStatusText_();
758 this.danger_.style.display = 'none';
759 this.safe_.style.display = 'block';
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.
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_);
774 case Download.DangerType.DANGEROUS_URL: {
775 this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
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_);
784 case Download.DangerType.UNCOMMON_CONTENT: {
785 this.dangerDesc_.textContent = loadTimeData.getStringF(
786 'danger_uncommon_desc', this.fileName_);
789 case Download.DangerType.POTENTIALLY_UNWANTED: {
790 this.dangerDesc_.textContent = loadTimeData.getStringF(
791 'danger_settings_desc', this.fileName_);
796 if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
797 downloads.scheduleIconLoad(
799 'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
801 downloads.scheduleIconLoad(
803 'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
804 window.devicePixelRatio + 'x');
805 this.dangerDesc_.className = 'malware-description';
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';
816 this.malwareNodeControls_.style.display = 'none';
817 this.dangerDiscard_.style.display = 'inline';
818 this.dangerSave_.style.display = 'inline';
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;
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 = '';
845 * @return {string} User-visible status update text.
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') : '';
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
874 * @return {boolean} Returns false to prevent the default action.
877 Download.prototype.drag_ = function() {
878 chrome.send('drag', [this.id_]);
883 * Tells the backend to open this file.
884 * @return {boolean} Returns false to prevent the default action.
887 Download.prototype.openFile_ = function() {
888 chrome.send('openFile', [this.id_]);
893 * Tells the backend that the user chose to save a dangerous file.
894 * @return {boolean} Returns false to prevent the default action.
897 Download.prototype.saveDangerous_ = function() {
898 chrome.send('saveDangerous', [this.id_]);
903 * Tells the backend that the user chose to discard a dangerous file.
904 * @return {boolean} Returns false to prevent the default action.
907 Download.prototype.discardDangerous_ = function() {
908 chrome.send('discardDangerous', [this.id_]);
909 downloads.remove(this.id_);
914 * Tells the backend to show the file in explorer.
915 * @return {boolean} Returns false to prevent the default action.
918 Download.prototype.show_ = function() {
919 chrome.send('show', [this.id_]);
924 * Tells the backend to pause this download.
925 * @return {boolean} Returns false to prevent the default action.
928 Download.prototype.pause_ = function() {
929 chrome.send('pause', [this.id_]);
934 * Tells the backend to resume this download.
935 * @return {boolean} Returns false to prevent the default action.
938 Download.prototype.resume_ = function() {
939 chrome.send('resume', [this.id_]);
944 * Tells the backend to remove this download from history and download shelf.
945 * @return {boolean} Returns false to prevent the default action.
948 Download.prototype.remove_ = function() {
949 assert(loadTimeData.getBoolean('allow_deleting_history'));
950 chrome.send('remove', [this.id_]);
955 * Tells the backend to cancel this download.
956 * @return {boolean} Returns false to prevent the default action.
959 Download.prototype.cancel_ = function() {
960 chrome.send('cancel', [this.id_]);
964 ///////////////////////////////////////////////////////////////////////////////
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.
980 chrome.send('onPageLoaded');
982 downloads = new Downloads();
986 $('clear-all').onclick = function() {
987 chrome.send('clearAll');
990 $('open-downloads-folder').onclick = function() {
991 chrome.send('openDownloadsFolder');
994 $('term').onsearch = function(e) {
995 setSearch($('term').value);
999 function setSearch(searchText) {
1000 fifoResults.length = 0;
1001 downloads.setSearchText(searchText);
1002 searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
1004 searchText = searchText.map(function(term) {
1006 return (term.match(/\s/) &&
1007 term[0].match(/["']/) &&
1008 term[term.length - 1] == term[0]) ?
1009 term.substr(1, term.length - 2) : term;
1014 chrome.send('getDownloads', searchText);
1017 function clearAll() {
1018 if (!loadTimeData.getBoolean('allow_deleting_history'))
1021 fifoResults.length = 0;
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.
1034 function downloadsList(results) {
1035 if (downloads && downloads.isUpdateNeeded(results)) {
1037 clearTimeout(resultsTimeout);
1038 fifoResults.length = 0;
1040 downloadUpdated(results);
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.
1050 function downloadUpdated(results) {
1051 // Sometimes this can get called too early.
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.
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);
1077 // Add handlers to HTML elements.
1078 window.addEventListener('DOMContentLoaded', load);