| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 Polymer({ | 5 Polymer({ |
| 6 is: 'history-list', | 6 is: 'history-list', |
| 7 | 7 |
| 8 behaviors: [HistoryListBehavior], | |
| 9 | |
| 10 properties: { | 8 properties: { |
| 11 // The search term for the current query. Set when the query returns. | 9 // The search term for the current query. Set when the query returns. |
| 12 searchedTerm: { | 10 searchedTerm: { |
| 13 type: String, | 11 type: String, |
| 14 value: '', | 12 value: '', |
| 15 }, | 13 }, |
| 16 | 14 |
| 17 resultLoadingDisabled_: { | 15 resultLoadingDisabled_: { |
| 18 type: Boolean, | 16 type: Boolean, |
| 19 value: false, | 17 value: false, |
| 20 }, | 18 }, |
| 21 | 19 |
| 20 /** |
| 21 * Indexes into historyData_ of selected items. |
| 22 * @type {!Set<number>} |
| 23 */ |
| 24 selectedItems: { |
| 25 type: Object, |
| 26 value: /** @return {!Set<string>} */ function() { |
| 27 return new Set(); |
| 28 }, |
| 29 }, |
| 30 |
| 31 canDeleteHistory_: { |
| 32 type: Boolean, |
| 33 value: loadTimeData.getBoolean('allowDeletingHistory'), |
| 34 }, |
| 35 |
| 22 // An array of history entries in reverse chronological order. | 36 // An array of history entries in reverse chronological order. |
| 23 historyData_: Array, | 37 historyData_: Array, |
| 24 | 38 |
| 25 lastFocused_: Object, | 39 lastFocused_: Object, |
| 26 | 40 |
| 27 querying: Boolean, | 41 lastSelectedIndex: Number, |
| 42 |
| 43 /** @type {!QueryState} */ |
| 44 queryState: Object, |
| 45 |
| 46 /** |
| 47 * @private {?{ |
| 48 * index: number, |
| 49 * item: !HistoryEntry, |
| 50 * path: string, |
| 51 * target: !HTMLElement |
| 52 * }} |
| 53 */ |
| 54 actionMenuModel_: Object, |
| 28 }, | 55 }, |
| 29 | 56 |
| 30 listeners: { | 57 listeners: { |
| 31 'remove-bookmark-stars': 'removeBookmarkStars_', | 58 'history-checkbox-select': 'onItemSelected_', |
| 32 'open-menu': 'onOpenMenu_', | 59 'open-menu': 'onOpenMenu_', |
| 60 'remove-bookmark-stars': 'onRemoveBookmarkStars_', |
| 33 }, | 61 }, |
| 34 | 62 |
| 35 /** @override */ | 63 /** @override */ |
| 36 attached: function() { | 64 attached: function() { |
| 37 // It is possible (eg, when middle clicking the reload button) for all other | 65 // It is possible (eg, when middle clicking the reload button) for all other |
| 38 // resize events to fire before the list is attached and can be measured. | 66 // resize events to fire before the list is attached and can be measured. |
| 39 // Adding another resize here ensures it will get sized correctly. | 67 // Adding another resize here ensures it will get sized correctly. |
| 40 /** @type {IronListElement} */ (this.$['infinite-list']).notifyResize(); | 68 /** @type {IronListElement} */ (this.$['infinite-list']).notifyResize(); |
| 41 this.$['infinite-list'].scrollTarget = this; | 69 this.$['infinite-list'].scrollTarget = this; |
| 42 this.$['scroll-threshold'].scrollTarget = this; | 70 this.$['scroll-threshold'].scrollTarget = this; |
| 43 }, | 71 }, |
| 44 | 72 |
| 73 ///////////////////////////////////////////////////////////////////////////// |
| 74 // Public methods: |
| 75 |
| 45 /** | 76 /** |
| 46 * Remove bookmark star for history items with matching URLs. | 77 * @param {HistoryQuery} info An object containing information about the |
| 47 * @param {{detail: !string}} e | 78 * query. |
| 48 * @private | 79 * @param {!Array<!HistoryEntry>} results A list of results. |
| 49 */ | 80 */ |
| 50 removeBookmarkStars_: function(e) { | 81 historyResult: function(info, results) { |
| 51 var url = e.detail; | 82 this.initializeResults_(info, results); |
| 83 this.closeMenu_(); |
| 52 | 84 |
| 53 if (this.historyData_ === undefined) | 85 if (info.term && !this.queryState.incremental) { |
| 54 return; | 86 Polymer.IronA11yAnnouncer.requestAvailability(); |
| 87 this.fire('iron-announce', { |
| 88 text: |
| 89 md_history.HistoryItem.searchResultsTitle(results.length, info.term) |
| 90 }); |
| 91 } |
| 55 | 92 |
| 56 for (var i = 0; i < this.historyData_.length; i++) { | 93 this.addNewResults(results, this.queryState.incremental, info.finished); |
| 57 if (this.historyData_[i].url == url) | |
| 58 this.set('historyData_.' + i + '.starred', false); | |
| 59 } | |
| 60 }, | 94 }, |
| 61 | 95 |
| 62 /** | 96 /** |
| 63 * Adds the newly updated history results into historyData_. Adds new fields | 97 * Adds the newly updated history results into historyData_. Adds new fields |
| 64 * for each result. | 98 * for each result. |
| 65 * @param {!Array<!HistoryEntry>} historyResults The new history results. | 99 * @param {!Array<!HistoryEntry>} historyResults The new history results. |
| 66 * @param {boolean} incremental Whether the result is from loading more | 100 * @param {boolean} incremental Whether the result is from loading more |
| 67 * history, or a new search/list reload. | 101 * history, or a new search/list reload. |
| 68 * @param {boolean} finished True if there are no more results available and | 102 * @param {boolean} finished True if there are no more results available and |
| 69 * result loading should be disabled. | 103 * result loading should be disabled. |
| (...skipping 17 matching lines...) Expand all Loading... |
| 87 this.push.apply(this, results); | 121 this.push.apply(this, results); |
| 88 } else { | 122 } else { |
| 89 // The first time we receive data, use set() to ensure the iron-list is | 123 // The first time we receive data, use set() to ensure the iron-list is |
| 90 // initialized correctly. | 124 // initialized correctly. |
| 91 this.set('historyData_', results); | 125 this.set('historyData_', results); |
| 92 } | 126 } |
| 93 | 127 |
| 94 this.resultLoadingDisabled_ = finished; | 128 this.resultLoadingDisabled_ = finished; |
| 95 }, | 129 }, |
| 96 | 130 |
| 131 historyDeleted: function() { |
| 132 // Do not reload the list when there are items checked. |
| 133 if (this.getSelectedItemCount() > 0) |
| 134 return; |
| 135 |
| 136 // Reload the list with current search state. |
| 137 this.fire('query-history', false); |
| 138 }, |
| 139 |
| 140 /** |
| 141 * Deselect each item in |selectedItems|. |
| 142 */ |
| 143 unselectAllItems: function() { |
| 144 this.selectedItems.forEach(function(index) { |
| 145 this.changeSelection_(index, false); |
| 146 }.bind(this)); |
| 147 |
| 148 assert(this.selectedItems.size == 0); |
| 149 }, |
| 150 |
| 151 /** @return {number} */ |
| 152 getSelectedItemCount: function() { |
| 153 return this.selectedItems.size; |
| 154 }, |
| 155 |
| 156 /** |
| 157 * Delete all the currently selected history items. Will prompt the user with |
| 158 * a dialog to confirm that the deletion should be performed. |
| 159 */ |
| 160 deleteSelectedWithPrompt: function() { |
| 161 if (!this.canDeleteHistory_) |
| 162 return; |
| 163 |
| 164 var browserService = md_history.BrowserService.getInstance(); |
| 165 browserService.recordAction('RemoveSelected'); |
| 166 if (this.queryState.searchTerm != '') |
| 167 browserService.recordAction('SearchResultRemove'); |
| 168 this.$.dialog.get().showModal(); |
| 169 |
| 170 // TODO(dbeam): remove focus flicker caused by showModal() + focus(). |
| 171 this.$$('.action-button').focus(); |
| 172 }, |
| 173 |
| 174 ///////////////////////////////////////////////////////////////////////////// |
| 175 // Private methods: |
| 176 |
| 177 /** |
| 178 * Set the selection status for an item at a particular index. |
| 179 * @param {number} index |
| 180 * @param {boolean} selected |
| 181 * @private |
| 182 */ |
| 183 changeSelection_: function(index, selected) { |
| 184 this.set('historyData_.' + index + '.selected', selected); |
| 185 if (selected) |
| 186 this.selectedItems.add(index); |
| 187 else |
| 188 this.selectedItems.delete(index); |
| 189 }, |
| 190 |
| 191 /** |
| 192 * Performs a request to the backend to delete all selected items. If |
| 193 * successful, removes them from the view. Does not prompt the user before |
| 194 * deleting -- see deleteSelectedWithPrompt for a version of this method which |
| 195 * does prompt. |
| 196 * @private |
| 197 */ |
| 198 deleteSelected_: function() { |
| 199 var toBeRemoved = |
| 200 Array.from(this.selectedItems.values()).map(function(index) { |
| 201 return this.get('historyData_.' + index); |
| 202 }.bind(this)); |
| 203 |
| 204 md_history.BrowserService.getInstance() |
| 205 .deleteItems(toBeRemoved) |
| 206 .then(function(items) { |
| 207 this.removeItemsByIndex_(Array.from(this.selectedItems)); |
| 208 this.fire('unselect-all'); |
| 209 }.bind(this)); |
| 210 }, |
| 211 |
| 212 /** |
| 213 * Remove all |indices| from the history list. Uses notifySplices to send a |
| 214 * single large notification to Polymer, rather than many small notifications, |
| 215 * which greatly improves performance. |
| 216 * @param {!Array<number>} indices |
| 217 * @private |
| 218 */ |
| 219 removeItemsByIndex_: function(indices) { |
| 220 var splices = []; |
| 221 indices.sort(function(a, b) { |
| 222 // Sort in reverse numerical order. |
| 223 return b - a; |
| 224 }); |
| 225 indices.forEach(function(index) { |
| 226 var item = this.historyData_.splice(index, 1); |
| 227 splices.push({ |
| 228 index: index, |
| 229 removed: [item], |
| 230 addedCount: 0, |
| 231 object: this.historyData_, |
| 232 type: 'splice' |
| 233 }); |
| 234 }.bind(this)); |
| 235 this.notifySplices('historyData_', splices); |
| 236 }, |
| 237 |
| 238 /** |
| 239 * Closes the overflow menu. |
| 240 * @private |
| 241 */ |
| 242 closeMenu_: function() { |
| 243 var menu = this.$.sharedMenu.getIfExists(); |
| 244 if (menu && menu.open) { |
| 245 this.actionMenuModel_ = null; |
| 246 menu.close(); |
| 247 } |
| 248 }, |
| 249 |
| 250 ///////////////////////////////////////////////////////////////////////////// |
| 251 // Event listeners: |
| 252 |
| 253 /** @private */ |
| 254 onDialogConfirmTap_: function() { |
| 255 md_history.BrowserService.getInstance().recordAction( |
| 256 'ConfirmRemoveSelected'); |
| 257 |
| 258 this.deleteSelected_(); |
| 259 var dialog = assert(this.$.dialog.getIfExists()); |
| 260 dialog.close(); |
| 261 }, |
| 262 |
| 263 /** @private */ |
| 264 onDialogCancelTap_: function() { |
| 265 md_history.BrowserService.getInstance().recordAction( |
| 266 'CancelRemoveSelected'); |
| 267 |
| 268 var dialog = assert(this.$.dialog.getIfExists()); |
| 269 dialog.close(); |
| 270 }, |
| 271 |
| 272 /** |
| 273 * Remove bookmark star for history items with matching URLs. |
| 274 * @param {{detail: !string}} e |
| 275 * @private |
| 276 */ |
| 277 onRemoveBookmarkStars_: function(e) { |
| 278 var url = e.detail; |
| 279 |
| 280 if (this.historyData_ === undefined) |
| 281 return; |
| 282 |
| 283 for (var i = 0; i < this.historyData_.length; i++) { |
| 284 if (this.historyData_[i].url == url) |
| 285 this.set('historyData_.' + i + '.starred', false); |
| 286 } |
| 287 }, |
| 288 |
| 97 /** | 289 /** |
| 98 * Called when the page is scrolled to near the bottom of the list. | 290 * Called when the page is scrolled to near the bottom of the list. |
| 99 * @private | 291 * @private |
| 100 */ | 292 */ |
| 101 loadMoreData_: function() { | 293 onScrollToBottom_: function() { |
| 102 if (this.resultLoadingDisabled_ || this.querying) | 294 if (this.resultLoadingDisabled_ || this.queryState.querying) |
| 103 return; | 295 return; |
| 104 | 296 |
| 105 this.fire('query-history', true); | 297 this.fire('query-history', true); |
| 106 }, | 298 }, |
| 107 | 299 |
| 108 /** | 300 /** |
| 109 * Ensure that the item is visible in the scroll pane when its menu is | 301 * Open the overflow menu and ensure that the item is visible in the scroll |
| 110 * opened (it is possible to open off-screen items using keyboard shortcuts). | 302 * pane when its menu is opened (it is possible to open off-screen items using |
| 111 * @param {Event} e | 303 * keyboard shortcuts). |
| 304 * @param {{detail: { |
| 305 * index: number, item: !HistoryEntry, |
| 306 * path: string, target: !HTMLElement |
| 307 * }}} e |
| 112 * @private | 308 * @private |
| 113 */ | 309 */ |
| 114 onOpenMenu_: function(e) { | 310 onOpenMenu_: function(e) { |
| 115 var index = e.detail.index; | 311 var index = e.detail.index; |
| 116 var list = /** @type {IronListElement} */ (this.$['infinite-list']); | 312 var list = /** @type {IronListElement} */ (this.$['infinite-list']); |
| 117 if (index < list.firstVisibleIndex || index > list.lastVisibleIndex) | 313 if (index < list.firstVisibleIndex || index > list.lastVisibleIndex) |
| 118 list.scrollToIndex(index); | 314 list.scrollToIndex(index); |
| 315 |
| 316 var target = e.detail.target; |
| 317 this.actionMenuModel_ = e.detail; |
| 318 var menu = /** @type {CrSharedMenuElement} */ this.$.sharedMenu.get(); |
| 319 menu.showAt(target); |
| 119 }, | 320 }, |
| 120 | 321 |
| 322 /** @private */ |
| 323 onMoreFromSiteTap_: function() { |
| 324 md_history.BrowserService.getInstance().recordAction( |
| 325 'EntryMenuShowMoreFromSite'); |
| 326 |
| 327 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 328 this.fire('change-query', {search: this.actionMenuModel_.item.domain}); |
| 329 this.actionMenuModel_ = null; |
| 330 this.closeMenu_(); |
| 331 }, |
| 332 |
| 333 /** @private */ |
| 334 onRemoveFromHistoryTap_: function() { |
| 335 var browserService = md_history.BrowserService.getInstance(); |
| 336 browserService.recordAction('EntryMenuRemoveFromHistory'); |
| 337 var menu = assert(this.$.sharedMenu.getIfExists()); |
| 338 var itemData = this.actionMenuModel_; |
| 339 browserService.deleteItems([itemData.item]).then(function(items) { |
| 340 // This unselect-all resets the toolbar when deleting a selected item |
| 341 // and clears selection state which can be invalid if items move |
| 342 // around during deletion. |
| 343 // TODO(tsergeant): Make this automatic based on observing list |
| 344 // modifications. |
| 345 this.fire('unselect-all'); |
| 346 this.removeItemsByIndex_([itemData.index]); |
| 347 |
| 348 var index = itemData.index; |
| 349 if (index == undefined) |
| 350 return; |
| 351 |
| 352 var browserService = md_history.BrowserService.getInstance(); |
| 353 browserService.recordHistogram( |
| 354 'HistoryPage.RemoveEntryPosition', |
| 355 Math.min(index, UMA_MAX_BUCKET_VALUE), UMA_MAX_BUCKET_VALUE); |
| 356 if (index <= UMA_MAX_SUBSET_BUCKET_VALUE) { |
| 357 browserService.recordHistogram( |
| 358 'HistoryPage.RemoveEntryPositionSubset', index, |
| 359 UMA_MAX_SUBSET_BUCKET_VALUE); |
| 360 } |
| 361 }.bind(this)); |
| 362 this.closeMenu_(); |
| 363 }, |
| 364 |
| 365 /** |
| 366 * @param {Event} e |
| 367 * @private |
| 368 */ |
| 369 onItemSelected_: function(e) { |
| 370 var index = e.detail.index; |
| 371 var indices = []; |
| 372 |
| 373 // Handle shift selection. Change the selection state of all items between |
| 374 // |path| and |lastSelected| to the selection state of |item|. |
| 375 if (e.detail.shiftKey && this.lastSelectedIndex != undefined) { |
| 376 for (var i = Math.min(index, this.lastSelectedIndex); |
| 377 i <= Math.max(index, this.lastSelectedIndex); i++) { |
| 378 indices.push(i); |
| 379 } |
| 380 } |
| 381 |
| 382 if (indices.length == 0) |
| 383 indices.push(index); |
| 384 |
| 385 var selected = !this.selectedItems.has(index); |
| 386 |
| 387 indices.forEach(function(index) { |
| 388 this.changeSelection_(index, selected); |
| 389 }.bind(this)); |
| 390 |
| 391 this.lastSelectedIndex = index; |
| 392 }, |
| 393 |
| 394 ///////////////////////////////////////////////////////////////////////////// |
| 395 // Template helpers: |
| 396 |
| 121 /** | 397 /** |
| 122 * Check whether the time difference between the given history item and the | 398 * Check whether the time difference between the given history item and the |
| 123 * next one is large enough for a spacer to be required. | 399 * next one is large enough for a spacer to be required. |
| 124 * @param {HistoryEntry} item | 400 * @param {HistoryEntry} item |
| 125 * @param {number} index The index of |item| in |historyData_|. | 401 * @param {number} index The index of |item| in |historyData_|. |
| 126 * @param {number} length The length of |historyData_|. | 402 * @param {number} length The length of |historyData_|. |
| 127 * @return {boolean} Whether or not time gap separator is required. | 403 * @return {boolean} Whether or not time gap separator is required. |
| 128 * @private | 404 * @private |
| 129 */ | 405 */ |
| 130 needsTimeGap_: function(item, index, length) { | 406 needsTimeGap_: function(item, index, length) { |
| 131 return md_history.HistoryItem.needsTimeGap( | 407 if (index >= length - 1 || length == 0) |
| 132 this.historyData_, index, this.searchedTerm); | 408 return false; |
| 409 |
| 410 var currentItem = this.historyData_[index]; |
| 411 var nextItem = this.historyData_[index + 1]; |
| 412 |
| 413 if (this.searchedTerm) |
| 414 return currentItem.dateShort != nextItem.dateShort; |
| 415 |
| 416 return currentItem.time - nextItem.time > BROWSING_GAP_TIME && |
| 417 currentItem.dateRelativeDay == nextItem.dateRelativeDay; |
| 133 }, | 418 }, |
| 134 | 419 |
| 135 /** | 420 /** |
| 136 * True if the given item is the beginning of a new card. | 421 * True if the given item is the beginning of a new card. |
| 137 * @param {HistoryEntry} item | 422 * @param {HistoryEntry} item |
| 138 * @param {number} i Index of |item| within |historyData_|. | 423 * @param {number} i Index of |item| within |historyData_|. |
| 139 * @param {number} length | 424 * @param {number} length |
| 140 * @return {boolean} | 425 * @return {boolean} |
| 141 * @private | 426 * @private |
| 142 */ | 427 */ |
| (...skipping 15 matching lines...) Expand all Loading... |
| 158 */ | 443 */ |
| 159 isCardEnd_: function(item, i, length) { | 444 isCardEnd_: function(item, i, length) { |
| 160 if (length == 0 || i > length - 1) | 445 if (length == 0 || i > length - 1) |
| 161 return false; | 446 return false; |
| 162 return i == length - 1 || | 447 return i == length - 1 || |
| 163 this.historyData_[i].dateRelativeDay != | 448 this.historyData_[i].dateRelativeDay != |
| 164 this.historyData_[i + 1].dateRelativeDay; | 449 this.historyData_[i + 1].dateRelativeDay; |
| 165 }, | 450 }, |
| 166 | 451 |
| 167 /** | 452 /** |
| 168 * @param {number} index | 453 * @param {number} historyDataLength |
| 454 * @return {boolean} |
| 455 * @private |
| 456 */ |
| 457 hasResults_: function(historyDataLength) { |
| 458 return historyDataLength > 0; |
| 459 }, |
| 460 |
| 461 /** |
| 462 * @param {string} searchedTerm |
| 463 * @param {boolean} isLoading |
| 169 * @return {string} | 464 * @return {string} |
| 170 * @private | 465 * @private |
| 171 */ | 466 */ |
| 172 pathForItem_: function(index) { | 467 noResultsMessage_: function(searchedTerm, isLoading) { |
| 173 return 'historyData_.' + index; | 468 if (isLoading) |
| 469 return ''; |
| 470 |
| 471 var messageId = searchedTerm !== '' ? 'noSearchResults' : 'noResults'; |
| 472 return loadTimeData.getString(messageId); |
| 473 }, |
| 474 |
| 475 /** |
| 476 * @param {HistoryQuery} info |
| 477 * @param {!Array<HistoryEntry>} results |
| 478 * @private |
| 479 */ |
| 480 initializeResults_: function(info, results) { |
| 481 if (results.length == 0) |
| 482 return; |
| 483 |
| 484 var currentDate = results[0].dateRelativeDay; |
| 485 |
| 486 for (var i = 0; i < results.length; i++) { |
| 487 // Sets the default values for these fields to prevent undefined types. |
| 488 results[i].selected = false; |
| 489 results[i].readableTimestamp = |
| 490 info.term == '' ? results[i].dateTimeOfDay : results[i].dateShort; |
| 491 |
| 492 if (results[i].dateRelativeDay != currentDate) { |
| 493 currentDate = results[i].dateRelativeDay; |
| 494 } |
| 495 } |
| 174 }, | 496 }, |
| 175 }); | 497 }); |
| OLD | NEW |