More cr.ui.FocusRow simplifications
[chromium-blink-merge.git] / chrome / browser / resources / downloads / manager.js
blob7f91b6153324a3e9f18aab6e4a9988a7e5c4d291
1 // Copyright 2015 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 cr.define('downloads', function() {
6   /**
7    * Class to own and manage download items.
8    * @constructor
9    */
10   function Manager() {}
12   cr.addSingletonGetter(Manager);
14   Manager.prototype = {
15     /** @private {string} */
16     searchText_: '',
18     /**
19      * Sets the search text, updates related UIs, and tells the browser.
20      * @param {string} searchText Text we're searching for.
21      * @private
22      */
23     setSearchText_: function(searchText) {
24       this.searchText_ = searchText;
26       $('downloads-summary-text').textContent = this.searchText_ ?
27           loadTimeData.getStringF('searchResultsFor', this.searchText_) : '';
29       // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
30       function trim(s) { return s.trim(); }
31       chrome.send('getDownloads', searchText.split(/"([^"]*)"/).map(trim));
32     },
34     /**
35      * @return {number} A guess at how many items could be visible at once.
36      * @private
37      */
38     guesstimateNumberOfVisibleItems_: function() {
39       var headerHeight = document.querySelector('header').offsetHeight;
40       var summaryHeight = $('downloads-summary').offsetHeight;
41       var nonItemSpace = headerHeight + summaryHeight;
42       return Math.floor((window.innerHeight - nonItemSpace) / 46) + 1;
43     },
45     /**
46      * Called when all items need to be updated.
47      * @param {!Array<!downloads.Data>} list A list of new download data.
48      * @private
49      */
50     updateAll_: function(list) {
51       var oldIdMap = this.idMap_ || {};
53       /** @private {!Object<!downloads.ItemView>} */
54       this.idMap_ = {};
56       /** @private {!Array<!downloads.ItemView>} */
57       this.items_ = [];
59       if (!this.iconLoader_) {
60         var guesstimate = Math.max(this.guesstimateNumberOfVisibleItems_(), 1);
61         /** @private {downloads.ThrottledIconLoader} */
62         this.iconLoader_ = new downloads.ThrottledIconLoader(guesstimate);
63       }
65       for (var i = 0; i < list.length; ++i) {
66         var data = list[i];
67         var id = data.id;
69         // Re-use old items when possible (saves work, preserves focus).
70         var item = oldIdMap[id] || new downloads.ItemView(this.iconLoader_);
72         this.idMap_[id] = item;  // Associated by ID for fast lookup.
73         this.items_.push(item);  // Add to sorted list for order.
75         // Render |item| but don't actually add to the DOM yet. |this.items_|
76         // must be fully created to be able to find the right spot to insert.
77         item.update(data);
79         // Collapse redundant dates.
80         var prev = list[i - 1];
81         item.dateContainer.hidden =
82             prev && prev.date_string == data.date_string;
84         delete oldIdMap[id];
85       }
87       // Remove stale, previously rendered items from the DOM.
88       for (var id in oldIdMap) {
89         var oldNode = oldIdMap[id].node;
90         if (oldNode.parentNode)
91           oldNode.parentNode.removeChild(oldNode);
92         delete oldIdMap[id];
93       }
95       for (var i = 0; i < this.items_.length; ++i) {
96         var item = this.items_[i];
97         if (item.node.parentNode)  // Already in the DOM; skip.
98           continue;
100         var before = null;
101         // Find the next rendered item after this one, and insert before it.
102         for (var j = i + 1; !before && j < this.items_.length; ++j) {
103           if (this.items_[j].node.parentNode)
104             before = this.items_[j].node;
105         }
106         // If |before| is null, |item| will just get added at the end.
107         this.node_.insertBefore(item.node, before);
108       }
110       var noDownloadsOrResults = $('no-downloads-or-results');
111       noDownloadsOrResults.textContent = loadTimeData.getString(
112           this.searchText_ ? 'noSearchResults' : 'noDownloads');
114       var hasDownloads = this.size_() > 0;
115       this.node_.hidden = !hasDownloads;
116       noDownloadsOrResults.hidden = hasDownloads;
118       if (loadTimeData.getBoolean('allowDeletingHistory'))
119         $('clear-all').hidden = !hasDownloads || this.searchText_.length > 0;
121       this.rebuildFocusGrid_();
122     },
124     /**
125      * @param {!downloads.Data} data Info about the item to update.
126      * @private
127      */
128     updateItem_: function(data) {
129       var activeElement = document.activeElement;
131       var item = this.idMap_[data.id];
132       item.update(data);
134       if (item.node.contains(activeElement) &&
135           !cr.ui.FocusRow.isFocusable(activeElement)) {
136         var focusRow = this.focusGrid_.getRowForRoot(item.node);
137         focusRow.getEquivalentElement(activeElement).focus();
138       }
139     },
141     /**
142      * Rebuild the focusGrid_ using the elements that each download will have.
143      * @private
144      */
145     rebuildFocusGrid_: function() {
146       var activeElement = document.activeElement;
148       /** @private {!cr.ui.FocusGrid} */
149       this.focusGrid_ = this.focusGrid_ || new cr.ui.FocusGrid();
150       this.focusGrid_.destroy();
152       this.items_.forEach(function(item) {
153         var focusRow = new downloads.FocusRow(item.node, this.node_);
155         this.focusGrid_.addRow(focusRow);
157         if (item.node.contains(activeElement) &&
158             !cr.ui.FocusRow.isFocusable(activeElement)) {
159           focusRow.getEquivalentElement(activeElement).focus();
160         }
161       }, this);
163       this.focusGrid_.ensureRowActive();
164     },
166     /**
167      * @return {number} The number of downloads shown on the page.
168      * @private
169      */
170     size_: function() {
171       return this.items_.length;
172     },
174     /** @private */
175     clearAll_: function() {
176       if (loadTimeData.getBoolean('allowDeletingHistory')) {
177         chrome.send('clearAll');
178         this.setSearchText_('');
179       }
180     },
182     /** @private */
183     onLoad_: function() {
184       this.node_ = $('downloads-display');
186       $('clear-all').onclick = function() {
187         this.clearAll_();
188       }.bind(this);
190       $('open-downloads-folder').onclick = function() {
191         chrome.send('openDownloadsFolder');
192       };
194       $('term').onsearch = function(e) {
195         this.setSearchText_($('term').value);
196       }.bind(this);
198       cr.ui.decorate('command', cr.ui.Command);
199       document.addEventListener('canExecute', this.onCanExecute_.bind(this));
200       document.addEventListener('command', this.onCommand_.bind(this));
202       this.setSearchText_('');
203     },
205     /**
206      * @param {Event} e
207      * @private
208      */
209     onCanExecute_: function(e) {
210       e = /** @type {cr.ui.CanExecuteEvent} */(e);
211       switch (e.command.id) {
212         case 'undo-command':
213           e.canExecute = document.activeElement != $('term');
214           break;
215         case 'clear-all-command':
216           e.canExecute = true;
217           break;
218       }
219     },
221     /**
222      * @param {Event} e
223      * @private
224      */
225     onCommand_: function(e) {
226       if (e.command.id == 'undo-command')
227         chrome.send('undo');
228       else if (e.command.id == 'clear-all-command')
229         this.clearAll_();
230     },
231   };
233   Manager.updateAll = function(list) {
234     Manager.getInstance().updateAll_(list);
235   };
237   Manager.updateItem = function(item) {
238     Manager.getInstance().updateItem_(item);
239   };
241   Manager.setSearchText = function(searchText) {
242     Manager.getInstance().setSearchText_(searchText);
243   };
245   Manager.onLoad = function() {
246     Manager.getInstance().onLoad_();
247   };
249   Manager.size = function() {
250     return Manager.getInstance().size_();
251   };
253   return {Manager: Manager};
256 window.addEventListener('DOMContentLoaded', downloads.Manager.onLoad);