Chromium Code Reviews| Index: chrome/browser/resources/md_history/history_list.js |
| diff --git a/chrome/browser/resources/md_history/history_list.js b/chrome/browser/resources/md_history/history_list.js |
| index 38a5bd0299b42fecf80b005df7df27f1a4fe2b0d..ec071516b40b79819e47173f79c2c8ee26dc0aed 100644 |
| --- a/chrome/browser/resources/md_history/history_list.js |
| +++ b/chrome/browser/resources/md_history/history_list.js |
| @@ -5,8 +5,6 @@ |
| Polymer({ |
| is: 'history-list', |
| - behaviors: [HistoryListBehavior], |
| - |
| properties: { |
| // The search term for the current query. Set when the query returns. |
| searchedTerm: { |
| @@ -19,17 +17,42 @@ Polymer({ |
| value: false, |
| }, |
| + /** |
| + * Indexes into historyData_ of selected items. |
| + * @type {!Set<number>} |
| + */ |
| + selectedItems: { |
| + type: Object, |
| + value: /** @return {!Set<string>} */ function() { |
| + return new Set(); |
| + }, |
| + }, |
| + |
| // An array of history entries in reverse chronological order. |
| historyData_: Array, |
| lastFocused_: Object, |
| - querying: Boolean, |
| + lastSelectedIndex: Number, |
| + |
| + /** @type {!QueryState} */ |
| + queryState: Object, |
| + |
| + /** |
| + * @private {?{ |
| + * index: number, |
| + * item: !HistoryEntry, |
| + * path: string, |
| + * target: !HTMLElement |
| + * }} |
| + */ |
| + actionMenuModel_: Object, |
| }, |
| listeners: { |
| - 'remove-bookmark-stars': 'removeBookmarkStars_', |
| + 'history-checkbox-select': 'onItemSelected_', |
| 'open-menu': 'onOpenMenu_', |
| + 'remove-bookmark-stars': 'removeBookmarkStars_', |
| }, |
| /** @override */ |
| @@ -42,21 +65,27 @@ Polymer({ |
| this.$['scroll-threshold'].scrollTarget = this; |
| }, |
| + ///////////////////////////////////////////////////////////////////////////// |
| + // Public methods: |
| + |
| /** |
| - * Remove bookmark star for history items with matching URLs. |
| - * @param {{detail: !string}} e |
| - * @private |
| + * @param {HistoryQuery} info An object containing information about the |
| + * query. |
| + * @param {!Array<!HistoryEntry>} results A list of results. |
| */ |
| - removeBookmarkStars_: function(e) { |
| - var url = e.detail; |
| + historyResult: function(info, results) { |
| + this.initializeResults_(info, results); |
| + this.closeMenu_(); |
| - if (this.historyData_ === undefined) |
| - return; |
| - |
| - for (var i = 0; i < this.historyData_.length; i++) { |
| - if (this.historyData_[i].url == url) |
| - this.set('historyData_.' + i + '.starred', false); |
| + if (info.term && !this.queryState.incremental) { |
| + Polymer.IronA11yAnnouncer.requestAvailability(); |
| + this.fire('iron-announce', { |
| + text: |
| + md_history.HistoryItem.searchResultsTitle(results.length, info.term) |
| + }); |
| } |
| + |
| + this.addNewResults(results, this.queryState.incremental, info.finished); |
| }, |
| /** |
| @@ -94,21 +123,166 @@ Polymer({ |
| this.resultLoadingDisabled_ = finished; |
| }, |
| + historyDeleted: function() { |
| + // Do not reload the list when there are items checked. |
| + if (this.getSelectedItemCount() > 0) |
| + return; |
| + |
| + // Reload the list with current search state. |
| + this.fire('query-history', false); |
| + }, |
| + |
| + /** |
| + * Set the selection status for an item at a particular index. |
| + * @param {number} index |
| + * @param {boolean} selected |
| + */ |
| + changeSelection: function(index, selected) { |
| + this.set('historyData_.' + index + '.selected', selected); |
|
calamity
2017/02/21 02:40:06
nit: Can be private.
tsergeant
2017/02/21 23:24:13
Done.
|
| + if (selected) |
| + this.selectedItems.add(index); |
| + else |
| + this.selectedItems.delete(index); |
| + }, |
| + |
| + /** |
| + * Deselect each item in |selectedItems|. |
| + */ |
| + unselectAllItems: function() { |
| + this.selectedItems.forEach(function(index) { |
| + this.changeSelection(index, false); |
| + }.bind(this)); |
| + |
| + assert(this.selectedItems.size == 0); |
| + }, |
| + |
| + /** @return {number} */ |
| + getSelectedItemCount: function() { |
| + return this.selectedItems.size; |
| + }, |
| + |
| + /** |
| + * Delete all the currently selected history items. Will prompt the user with |
| + * a dialog to confirm that the deletion should be performed. |
| + */ |
| + deleteSelectedWithPrompt: function() { |
| + if (!this.canDeleteHistory_()) |
| + return; |
| + |
| + var browserService = md_history.BrowserService.getInstance(); |
| + browserService.recordAction('RemoveSelected'); |
| + if (this.queryState.searchTerm != '') |
| + browserService.recordAction('SearchResultRemove'); |
| + this.$.dialog.get().showModal(); |
| + |
| + // TODO(dbeam): remove focus flicker caused by showModal() + focus(). |
| + this.$$('.action-button').focus(); |
| + }, |
| + |
| + /** |
| + * Performs a request to the backend to delete all selected items. If |
| + * successful, removes them from the view. Does not prompt the user before |
| + * deleting -- see deleteSelectedWithPrompt for a version of this method which |
| + * does prompt. |
| + */ |
| + deleteSelected: function() { |
|
calamity
2017/02/21 02:40:06
nit: private-able.
tsergeant
2017/02/21 23:24:13
Done.
|
| + var toBeRemoved = |
| + Array.from(this.selectedItems.values()).map(function(index) { |
| + return this.get('historyData_.' + index); |
| + }.bind(this)); |
| + |
| + md_history.BrowserService.getInstance() |
| + .deleteItems(toBeRemoved) |
| + .then(function(items) { |
| + this.removeItemsByIndex(Array.from(this.selectedItems)); |
| + this.fire('unselect-all'); |
| + }.bind(this)); |
| + }, |
| + |
| + /** |
| + * Remove all |indices| from the history list. Uses notifySplices to send a |
| + * single large notification to Polymer, rather than many small notifications, |
| + * which greatly improves performance. |
| + * @param {!Array<number>} indices |
| + * @private |
| + */ |
| + removeItemsByIndex: function(indices) { |
|
calamity
2017/02/21 02:40:06
nit: private-able.
tsergeant
2017/02/21 23:24:12
Done.
|
| + var splices = []; |
| + indices.sort(function(a, b) { |
| + // Sort in reverse numerical order. |
| + return b - a; |
| + }); |
| + indices.forEach(function(index) { |
| + var item = this.historyData_.splice(index, 1); |
| + splices.push({ |
| + index: index, |
| + removed: [item], |
| + addedCount: 0, |
| + object: this.historyData_, |
| + type: 'splice' |
| + }); |
| + }.bind(this)); |
| + this.notifySplices('historyData_', splices); |
| + }, |
| + |
| + ///////////////////////////////////////////////////////////////////////////// |
| + // Event listeners: |
| + |
| + /** @private */ |
| + onDialogConfirmTap_: function() { |
| + md_history.BrowserService.getInstance().recordAction( |
| + 'ConfirmRemoveSelected'); |
| + |
| + this.deleteSelected(); |
| + var dialog = assert(this.$.dialog.getIfExists()); |
| + dialog.close(); |
| + }, |
| + |
| + /** @private */ |
| + onDialogCancelTap_: function() { |
| + md_history.BrowserService.getInstance().recordAction( |
| + 'CancelRemoveSelected'); |
| + |
| + var dialog = assert(this.$.dialog.getIfExists()); |
| + dialog.close(); |
| + }, |
| + |
| + /** |
| + * Remove bookmark star for history items with matching URLs. |
| + * @param {{detail: !string}} e |
| + * @private |
| + */ |
| + removeBookmarkStars_: function(e) { |
| + var url = e.detail; |
| + |
| + if (this.historyData_ === undefined) |
| + return; |
| + |
| + for (var i = 0; i < this.historyData_.length; i++) { |
| + if (this.historyData_[i].url == url) |
| + this.set('historyData_.' + i + '.starred', false); |
| + } |
| + }, |
| + |
| /** |
| * Called when the page is scrolled to near the bottom of the list. |
| * @private |
| */ |
| loadMoreData_: function() { |
|
calamity
2017/02/21 02:40:06
While you're here, you may as well make this and t
tsergeant
2017/02/22 01:54:27
Done, oops
|
| - if (this.resultLoadingDisabled_ || this.querying) |
| + if (this.resultLoadingDisabled_ || this.queryState.querying) |
| return; |
| this.fire('query-history', true); |
| }, |
| /** |
| - * Ensure that the item is visible in the scroll pane when its menu is |
| - * opened (it is possible to open off-screen items using keyboard shortcuts). |
| - * @param {Event} e |
| + * Open the overflow menu and ensure that the item is visible in the scroll |
| + * pane when its menu is opened (it is possible to open off-screen items using |
| + * keyboard shortcuts). |
| + * @param {{detail: { |
| + * index: number, item: !HistoryEntry, |
| + * path: string, target: !HTMLElement |
| + * }}} e |
| * @private |
| */ |
| onOpenMenu_: function(e) { |
| @@ -116,9 +290,101 @@ Polymer({ |
| var list = /** @type {IronListElement} */ (this.$['infinite-list']); |
| if (index < list.firstVisibleIndex || index > list.lastVisibleIndex) |
| list.scrollToIndex(index); |
| + |
| + var target = e.detail.target; |
| + this.actionMenuModel_ = e.detail; |
| + var menu = /** @type {CrSharedMenuElement} */ this.$.sharedMenu.get(); |
| + menu.showAt(target); |
| }, |
| /** |
| + * Closes the overflow menu. |
| + * @private |
| + */ |
| + closeMenu_: function() { |
|
calamity
2017/02/21 02:40:06
I don't think this is hooked to any events.
tsergeant
2017/02/21 23:24:12
Moved up into the new Private methods section
|
| + var menu = this.$.sharedMenu.getIfExists(); |
| + if (menu && menu.open) { |
| + this.actionMenuModel_ = null; |
| + menu.close(); |
| + } |
| + }, |
| + |
| + /** @private */ |
| + onMoreFromSiteTap_: function() { |
| + md_history.BrowserService.getInstance().recordAction( |
| + 'EntryMenuShowMoreFromSite'); |
| + |
| + var menu = assert(this.$.sharedMenu.getIfExists()); |
| + this.fire('change-query', {search: this.actionMenuModel_.item.domain}); |
| + this.actionMenuModel_ = null; |
| + this.closeMenu_(); |
| + }, |
| + |
| + /** @private */ |
| + onRemoveFromHistoryTap_: function() { |
| + var browserService = md_history.BrowserService.getInstance(); |
| + browserService.recordAction('EntryMenuRemoveFromHistory'); |
| + var menu = assert(this.$.sharedMenu.getIfExists()); |
| + var itemData = this.actionMenuModel_; |
| + browserService.deleteItems([itemData.item]).then(function(items) { |
| + // This unselect-all resets the toolbar when deleting a selected item |
| + // and clears selection state which can be invalid if items move |
| + // around during deletion. |
| + // TODO(tsergeant): Make this automatic based on observing list |
| + // modifications. |
| + this.fire('unselect-all'); |
| + this.removeItemsByIndex([itemData.index]); |
| + |
| + var index = itemData.index; |
| + if (index == undefined) |
| + return; |
| + |
| + var browserService = md_history.BrowserService.getInstance(); |
| + browserService.recordHistogram( |
| + 'HistoryPage.RemoveEntryPosition', |
| + Math.min(index, UMA_MAX_BUCKET_VALUE), UMA_MAX_BUCKET_VALUE); |
| + if (index <= UMA_MAX_SUBSET_BUCKET_VALUE) { |
| + browserService.recordHistogram( |
| + 'HistoryPage.RemoveEntryPositionSubset', index, |
| + UMA_MAX_SUBSET_BUCKET_VALUE); |
| + } |
| + }.bind(this)); |
| + this.closeMenu_(); |
| + }, |
| + |
| + /** |
| + * @param {Event} e |
| + * @private |
| + */ |
| + onItemSelected_: function(e) { |
| + var index = e.detail.index; |
| + var indices = []; |
| + |
| + // Handle shift selection. Change the selection state of all items between |
| + // |path| and |lastSelected| to the selection state of |item|. |
| + if (e.detail.shiftKey && this.lastSelectedIndex != undefined) { |
| + for (var i = Math.min(index, this.lastSelectedIndex); |
| + i <= Math.max(index, this.lastSelectedIndex); i++) { |
| + indices.push(i); |
| + } |
| + } |
| + |
| + if (indices.length == 0) |
| + indices.push(index); |
| + |
| + var selected = !this.selectedItems.has(index); |
| + |
| + indices.forEach(function(index) { |
| + this.changeSelection(index, selected); |
| + }.bind(this)); |
| + |
| + this.lastSelectedIndex = index; |
| + }, |
| + |
| + ///////////////////////////////////////////////////////////////////////////// |
| + // Template helpers: |
| + |
| + /** |
| * Check whether the time difference between the given history item and the |
| * next one is large enough for a spacer to be required. |
| * @param {HistoryEntry} item |
| @@ -128,8 +394,17 @@ Polymer({ |
| * @private |
| */ |
| needsTimeGap_: function(item, index, length) { |
| - return md_history.HistoryItem.needsTimeGap( |
| - this.historyData_, index, this.searchedTerm); |
| + if (index >= length - 1 || length == 0) |
| + return false; |
| + |
| + var currentItem = this.historyData_[index]; |
| + var nextItem = this.historyData_[index + 1]; |
| + |
| + if (this.searchedTerm) |
| + return currentItem.dateShort != nextItem.dateShort; |
| + |
| + return currentItem.time - nextItem.time > BROWSING_GAP_TIME && |
| + currentItem.dateRelativeDay == nextItem.dateRelativeDay; |
| }, |
| /** |
| @@ -165,11 +440,53 @@ Polymer({ |
| }, |
| /** |
| - * @param {number} index |
| + * @param {number} historyDataLength |
| + * @return {boolean} |
| + * @private |
| + */ |
| + hasResults_: function(historyDataLength) { |
| + return historyDataLength > 0; |
| + }, |
| + |
| + /** |
| + * @param {string} searchedTerm |
| + * @param {boolean} isLoading |
| * @return {string} |
| * @private |
| */ |
| - pathForItem_: function(index) { |
| - return 'historyData_.' + index; |
| + noResultsMessage_: function(searchedTerm, isLoading) { |
| + if (isLoading) |
| + return ''; |
| + |
| + var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
| + return loadTimeData.getString(messageId); |
| + }, |
| + |
| + /** @private */ |
| + canDeleteHistory_: function() { |
| + return loadTimeData.getBoolean('allowDeletingHistory'); |
| + }, |
| + |
| + /** |
| + * @param {HistoryQuery} info |
| + * @param {!Array<HistoryEntry>} results |
| + * @private |
| + */ |
| + initializeResults_: function(info, results) { |
| + if (results.length == 0) |
| + return; |
| + |
| + var currentDate = results[0].dateRelativeDay; |
| + |
| + for (var i = 0; i < results.length; i++) { |
| + // Sets the default values for these fields to prevent undefined types. |
| + results[i].selected = false; |
| + results[i].readableTimestamp = |
| + info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; |
| + |
| + if (results[i].dateRelativeDay != currentDate) { |
| + currentDate = results[i].dateRelativeDay; |
| + } |
| + } |
| }, |
| }); |