Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(35)

Side by Side Diff: chrome/browser/resources/history/history.js

Issue 11975053: History: Add option to group visits by domain (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix collapse/toogle Created 7 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 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 <include src="../uber/uber_utils.js"> 5 <include src="../uber/uber_utils.js">
6 6
7 /////////////////////////////////////////////////////////////////////////////// 7 ///////////////////////////////////////////////////////////////////////////////
8 // Globals: 8 // Globals:
9 /** @const */ var RESULTS_PER_PAGE = 150; 9 /** @const */ var RESULTS_PER_PAGE = 150;
10 10
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
63 this.dateShort = result.dateShort || ''; 63 this.dateShort = result.dateShort || '';
64 64
65 // Whether this is the continuation of a previous day. 65 // Whether this is the continuation of a previous day.
66 this.continued = continued; 66 this.continued = continued;
67 } 67 }
68 68
69 // Visit, public: ------------------------------------------------------------- 69 // Visit, public: -------------------------------------------------------------
70 70
71 /** 71 /**
72 * Returns a dom structure for a browse page result or a search page result. 72 * Returns a dom structure for a browse page result or a search page result.
73 * @param {boolean} searchResultFlag Indicates whether the result is a search 73 * @param {Object} propertyBag A bag of configuration properties, false by
74 * result or not. 74 * default:
75 * <ul>
76 * <li>isSearchResult: Whether or not the result is a search result.</li>
77 * <li>addTitleFavicon: Whether or not the favicon should be added.</li>
78 * </ul>
75 * @return {Node} A DOM node to represent the history entry or search result. 79 * @return {Node} A DOM node to represent the history entry or search result.
76 */ 80 */
77 Visit.prototype.getResultDOM = function(searchResultFlag) { 81 Visit.prototype.getResultDOM = function(propertyBag) {
82 var isSearchResult = propertyBag.isSearchResult || false;
83 var addTitleFavicon = propertyBag.addTitleFavicon || false;
78 var node = createElementWithClassName('li', 'entry'); 84 var node = createElementWithClassName('li', 'entry');
79 var time = createElementWithClassName('div', 'time'); 85 var time = createElementWithClassName('div', 'time');
80 var entryBox = createElementWithClassName('label', 'entry-box'); 86 var entryBox = createElementWithClassName('label', 'entry-box');
81 var domain = createElementWithClassName('div', 'domain'); 87 var domain = createElementWithClassName('div', 'domain');
82 88
83 var dropDown = createElementWithClassName('button', 'drop-down'); 89 var dropDown = createElementWithClassName('button', 'drop-down');
84 dropDown.value = 'Open action menu'; 90 dropDown.value = 'Open action menu';
85 dropDown.title = loadTimeData.getString('actionMenuDescription'); 91 dropDown.title = loadTimeData.getString('actionMenuDescription');
86 dropDown.setAttribute('menu', '#action-menu'); 92 dropDown.setAttribute('menu', '#action-menu');
87 cr.ui.decorate(dropDown, MenuButton); 93 cr.ui.decorate(dropDown, MenuButton);
(...skipping 18 matching lines...) Expand all
106 112
107 domain.textContent = this.getDomainFromURL_(this.url_); 113 domain.textContent = this.getDomainFromURL_(this.url_);
108 114
109 // Clicking anywhere in the entryBox will check/uncheck the checkbox. 115 // Clicking anywhere in the entryBox will check/uncheck the checkbox.
110 entryBox.setAttribute('for', checkbox.id); 116 entryBox.setAttribute('for', checkbox.id);
111 entryBox.addEventListener('mousedown', entryBoxMousedown); 117 entryBox.addEventListener('mousedown', entryBoxMousedown);
112 118
113 // Prevent clicks on the drop down from affecting the checkbox. 119 // Prevent clicks on the drop down from affecting the checkbox.
114 dropDown.addEventListener('click', function(e) { e.preventDefault(); }); 120 dropDown.addEventListener('click', function(e) { e.preventDefault(); });
115 121
116 // We use a wrapper div so that the entry contents will be shinkwrapped. 122 // We use a wrapper div so that the entry contents will be shrinkwrapped.
117 entryBox.appendChild(time); 123 entryBox.appendChild(time);
118 entryBox.appendChild(this.getTitleDOM_()); 124 entryBox.appendChild(this.getTitleDOM_(addTitleFavicon));
119 entryBox.appendChild(domain); 125 entryBox.appendChild(domain);
120 entryBox.appendChild(dropDown); 126 entryBox.appendChild(dropDown);
121 127
122 // Let the entryBox be styled appropriately when it contains keyboard focus. 128 // Let the entryBox be styled appropriately when it contains keyboard focus.
123 entryBox.addEventListener('focus', function() { 129 entryBox.addEventListener('focus', function() {
124 this.classList.add('contains-focus'); 130 this.classList.add('contains-focus');
125 }, true); 131 }, true);
126 entryBox.addEventListener('blur', function() { 132 entryBox.addEventListener('blur', function() {
127 this.classList.remove('contains-focus'); 133 this.classList.remove('contains-focus');
128 }, true); 134 }, true);
129 135
130 node.appendChild(entryBox); 136 node.appendChild(entryBox);
131 137
132 if (searchResultFlag) { 138 if (isSearchResult) {
133 time.appendChild(document.createTextNode(this.dateShort)); 139 time.appendChild(document.createTextNode(this.dateShort));
134 var snippet = createElementWithClassName('div', 'snippet'); 140 var snippet = createElementWithClassName('div', 'snippet');
135 this.addHighlightedText_(snippet, 141 this.addHighlightedText_(snippet,
136 this.snippet_, 142 this.snippet_,
137 this.model_.getSearchText()); 143 this.model_.getSearchText());
138 node.appendChild(snippet); 144 node.appendChild(snippet);
139 } else { 145 } else {
140 time.appendChild(document.createTextNode(this.dateTimeOfDay)); 146 time.appendChild(document.createTextNode(this.dateTimeOfDay));
141 } 147 }
142 148
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
184 var b = document.createElement('b'); 190 var b = document.createElement('b');
185 b.textContent = content.substring(match.index, i); 191 b.textContent = content.substring(match.index, i);
186 node.appendChild(b); 192 node.appendChild(b);
187 } 193 }
188 } 194 }
189 if (i < content.length) 195 if (i < content.length)
190 node.appendChild(document.createTextNode(content.slice(i))); 196 node.appendChild(document.createTextNode(content.slice(i)));
191 }; 197 };
192 198
193 /** 199 /**
194 * @return {DOMObject} DOM representation for the title block. 200 * Returns the DOM element containing a link on the title of the URL for the
201 * current visit. Optionally sets the favicon as well.
202 * @param {boolean} addFavicon Whether to add a favicon or not.
203 * @return {Element} DOM representation for the title block.
195 * @private 204 * @private
196 */ 205 */
197 Visit.prototype.getTitleDOM_ = function() { 206 Visit.prototype.getTitleDOM_ = function(addFavicon) {
198 var node = createElementWithClassName('div', 'title'); 207 var node = createElementWithClassName('div', 'title');
199 node.style.backgroundImage = getFaviconImageSet(this.url_); 208 if (addFavicon) {
200 node.style.backgroundSize = '16px'; 209 node.style.backgroundImage = getFaviconImageSet(this.url_);
210 node.style.backgroundSize = '16px';
211 }
201 212
202 var link = document.createElement('a'); 213 var link = document.createElement('a');
203 link.href = this.url_; 214 link.href = this.url_;
204 link.id = 'id-' + this.id_; 215 link.id = 'id-' + this.id_;
205 link.target = '_top'; 216 link.target = '_top';
206 217
207 // Add a tooltip, since it might be ellipsized. 218 // Add a tooltip, since it might be ellipsized.
208 // TODO(dubroy): Find a way to show the tooltip only when necessary. 219 // TODO(dubroy): Find a way to show the tooltip only when necessary.
209 link.title = this.title_; 220 link.title = this.title_;
210 221
211 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); 222 this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
212 node.appendChild(link); 223 node.appendChild(link);
213 224
214 if (this.starred_) { 225 if (this.starred_) {
215 var star = createElementWithClassName('div', 'starred'); 226 var star = createElementWithClassName('div', 'starred');
216 node.appendChild(star); 227 node.appendChild(star);
217 star.addEventListener('click', this.starClicked_.bind(this)); 228 star.addEventListener('click', this.starClicked_.bind(this));
218 } 229 }
219 230
220 return node; 231 return node;
221 }; 232 };
222 233
223 /** 234 /**
235 * Set the favicon for an element.
236 * @param {Element} el The DOM element to which to add the icon.
237 * @private
238 */
239 Visit.prototype.addFaviconToElement_ = function(el) {
240 el.style.backgroundImage = getFaviconImageSet(this.url_);
241 };
242
243 /**
224 * Launch a search for more history entries from the same domain. 244 * Launch a search for more history entries from the same domain.
225 * @private 245 * @private
226 */ 246 */
227 Visit.prototype.showMoreFromSite_ = function() { 247 Visit.prototype.showMoreFromSite_ = function() {
228 setSearch(this.getDomainFromURL_(this.url_)); 248 setSearch(this.getDomainFromURL_(this.url_));
229 }; 249 };
230 250
231 /** 251 /**
232 * Remove a single entry from the history. 252 * Remove a single entry from the history.
233 * @private 253 * @private
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
301 * up an initial view, use #requestPage otherwise. 321 * up an initial view, use #requestPage otherwise.
302 */ 322 */
303 HistoryModel.prototype.setSearchText = function(searchText, opt_page) { 323 HistoryModel.prototype.setSearchText = function(searchText, opt_page) {
304 this.clearModel_(); 324 this.clearModel_();
305 this.searchText_ = searchText; 325 this.searchText_ = searchText;
306 this.requestedPage_ = opt_page ? opt_page : 0; 326 this.requestedPage_ = opt_page ? opt_page : 0;
307 this.queryHistory_(); 327 this.queryHistory_();
308 }; 328 };
309 329
310 /** 330 /**
331 * Clear the search text.
332 */
333 HistoryModel.prototype.clearSearchText = function() {
334 this.searchText_ = '';
335 };
336
337 /**
311 * Reload our model with the current parameters. 338 * Reload our model with the current parameters.
312 */ 339 */
313 HistoryModel.prototype.reload = function() { 340 HistoryModel.prototype.reload = function() {
341 // Save user-visible state, clear the model, and restore the state.
314 var search = this.searchText_; 342 var search = this.searchText_;
315 var page = this.requestedPage_; 343 var page = this.requestedPage_;
344 var groupByDomain = this.groupByDomain_;
345
316 this.clearModel_(); 346 this.clearModel_();
317 this.searchText_ = search; 347 this.searchText_ = search;
318 this.requestedPage_ = page; 348 this.requestedPage_ = page;
349 this.groupByDomain_ = groupByDomain;
319 this.queryHistory_(); 350 this.queryHistory_();
320 }; 351 };
321 352
322 /** 353 /**
323 * @return {string} The current search text. 354 * @return {string} The current search text.
324 */ 355 */
325 HistoryModel.prototype.getSearchText = function() { 356 HistoryModel.prototype.getSearchText = function() {
326 return this.searchText_; 357 return this.searchText_;
327 }; 358 };
328 359
(...skipping 12 matching lines...) Expand all
341 * Receiver for history query. 372 * Receiver for history query.
342 * @param {Object} info An object containing information about the query. 373 * @param {Object} info An object containing information about the query.
343 * @param {Array} results A list of results. 374 * @param {Array} results A list of results.
344 */ 375 */
345 HistoryModel.prototype.addResults = function(info, results) { 376 HistoryModel.prototype.addResults = function(info, results) {
346 $('loading-spinner').hidden = true; 377 $('loading-spinner').hidden = true;
347 this.inFlight_ = false; 378 this.inFlight_ = false;
348 this.isQueryFinished_ = info.finished; 379 this.isQueryFinished_ = info.finished;
349 this.queryCursor_ = info.cursor; 380 this.queryCursor_ = info.cursor;
350 381
351 // If there are no results, or they're not for the current search term, 382 // If the results are not for the current search term there's nothing more
352 // there's nothing more to do. 383 // to do.
353 if (!results || !results.length || info.term != this.searchText_) 384 if (info.term != this.searchText_)
354 return; 385 return;
355 386
356 // If necessary, sort the results from newest to oldest. 387 // If necessary, sort the results from newest to oldest.
357 if (!results.sorted) 388 if (!results.sorted)
358 results.sort(function(a, b) { return b.time - a.time; }); 389 results.sort(function(a, b) { return b.time - a.time; });
359 390
360 var lastVisit = this.visits_.slice(-1)[0]; 391 var lastVisit = this.visits_.slice(-1)[0];
361 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null; 392 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
362 393
363 for (var i = 0, thisResult; thisResult = results[i]; i++) { 394 for (var i = 0, thisResult; thisResult = results[i]; i++) {
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
408 439
409 // HistoryModel, Private: ----------------------------------------------------- 440 // HistoryModel, Private: -----------------------------------------------------
410 441
411 /** 442 /**
412 * Clear the history model. 443 * Clear the history model.
413 * @private 444 * @private
414 */ 445 */
415 HistoryModel.prototype.clearModel_ = function() { 446 HistoryModel.prototype.clearModel_ = function() {
416 this.inFlight_ = false; // Whether a query is inflight. 447 this.inFlight_ = false; // Whether a query is inflight.
417 this.searchText_ = ''; 448 this.searchText_ = '';
449 // Flag to show that the results are grouped by domain or not.
450 this.groupByDomain_ = false;
418 451
419 this.visits_ = []; // Date-sorted list of visits (most recent first). 452 this.visits_ = []; // Date-sorted list of visits (most recent first).
420 this.last_id_ = 0; 453 this.last_id_ = 0;
421 selectionAnchor = -1; 454 selectionAnchor = -1;
422 455
423 // The page that the view wants to see - we only fetch slightly past this 456 // The page that the view wants to see - we only fetch slightly past this
424 // point. If the view requests a page that we don't have data for, we try 457 // point. If the view requests a page that we don't have data for, we try
425 // to fetch it and call back when we're done. 458 // to fetch it and call back when we're done.
426 this.requestedPage_ = 0; 459 this.requestedPage_ = 0;
427 460
(...skipping 10 matching lines...) Expand all
438 // visit to a URL on any day. 471 // visit to a URL on any day.
439 this.urlsFromLastSeenDay_ = {}; 472 this.urlsFromLastSeenDay_ = {};
440 473
441 if (this.view_) 474 if (this.view_)
442 this.view_.clear_(); 475 this.view_.clear_();
443 }; 476 };
444 477
445 /** 478 /**
446 * Figure out if we need to do more queries to fill the currently requested 479 * Figure out if we need to do more queries to fill the currently requested
447 * page. If we think we can fill the page, call the view and let it know 480 * page. If we think we can fill the page, call the view and let it know
448 * we're ready to show something. 481 * we're ready to show something. This only applies to the daily time-based
482 * view.
449 * @private 483 * @private
450 */ 484 */
451 HistoryModel.prototype.updateSearch_ = function() { 485 HistoryModel.prototype.updateSearch_ = function() {
452 var doneLoading = 486 var doneLoading = this.isQueryFinished_ ||
453 this.canFillPage_(this.requestedPage_) || this.isQueryFinished_; 487 this.canFillPage_(this.requestedPage_);
454 488
455 // Try to fetch more results if the current page isn't full. 489 // Try to fetch more results if the results are not grouped by domain and
456 if (!doneLoading && !this.inFlight_) 490 // the current page isn't full.
491 if (!this.groupByDomain_ && !doneLoading && !this.inFlight_)
457 this.queryHistory_(); 492 this.queryHistory_();
458 493
459 // If we have any data for the requested page, show it. 494 // Show the result or a message if no results were returned.
460 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { 495 this.view_.onModelReady();
461 this.view_.onModelReady();
462 this.changed = false;
463 }
464 }; 496 };
465 497
466 /** 498 /**
467 * Query for history, either for a search or time-based browsing. 499 * Query for history, either for a search or time-based browsing.
468 * @private 500 * @private
469 */ 501 */
470 HistoryModel.prototype.queryHistory_ = function() { 502 HistoryModel.prototype.queryHistory_ = function() {
471 var endTime = 0; 503 var endTime = 0;
472 504 if (!this.getGroupByDomain()) {
473 // If there are already some visits, pick up the previous query where it 505 // Do the time-based search.
474 // left off. 506 // If there are already some visits, pick up the previous query where it
475 if (this.visits_.length > 0) { 507 // left off.
476 var lastVisit = this.visits_.slice(-1)[0]; 508 if (this.visits_.length > 0) {
477 endTime = lastVisit.date.getTime(); 509 var lastVisit = this.visits_.slice(-1)[0];
478 cursor = this.queryCursor_; 510 endTime = lastVisit.date.getTime();
511 cursor = this.queryCursor_;
512 }
479 } 513 }
480
481 $('loading-spinner').hidden = false; 514 $('loading-spinner').hidden = false;
482 this.inFlight_ = true; 515 this.inFlight_ = true;
483 chrome.send('queryHistory', 516 chrome.send('queryHistory',
484 [this.searchText_, endTime, this.queryCursor_, RESULTS_PER_PAGE]); 517 [this.searchText_, endTime, this.queryCursor_,
518 RESULTS_PER_PAGE]);
485 }; 519 };
486 520
487 /** 521 /**
488 * Check to see if we have data for the given page. 522 * Check to see if we have data for the given page.
489 * @param {number} page The page number. 523 * @param {number} page The page number.
490 * @return {boolean} Whether we have any data for the given page. 524 * @return {boolean} Whether we have any data for the given page.
491 * @private 525 * @private
492 */ 526 */
493 HistoryModel.prototype.haveDataForPage_ = function(page) { 527 HistoryModel.prototype.haveDataForPage_ = function(page) {
494 return (page * RESULTS_PER_PAGE < this.getSize()); 528 return (page * RESULTS_PER_PAGE < this.getSize());
495 }; 529 };
496 530
497 /** 531 /**
498 * Check to see if we have data to fill the given page. 532 * Check to see if we have data to fill the given page.
499 * @param {number} page The page number. 533 * @param {number} page The page number.
500 * @return {boolean} Whether we have data to fill the page. 534 * @return {boolean} Whether we have data to fill the page.
501 * @private 535 * @private
502 */ 536 */
503 HistoryModel.prototype.canFillPage_ = function(page) { 537 HistoryModel.prototype.canFillPage_ = function(page) {
504 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); 538 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
505 }; 539 };
506 540
541 /**
542 * Enables or disables grouping by domain.
543 * @param {boolean} groupByDomain New groupByDomain_ value.
544 */
545 HistoryModel.prototype.setGroupByDomain = function(groupByDomain) {
546 this.groupByDomain_ = groupByDomain;
547 };
548
549 /**
550 * Gets whether we are grouped by domain.
551 * @return {boolean} Whether the results are grouped by domain.
552 */
553 HistoryModel.prototype.getGroupByDomain = function() {
554 return this.groupByDomain_;
555 };
556
507 /////////////////////////////////////////////////////////////////////////////// 557 ///////////////////////////////////////////////////////////////////////////////
508 // HistoryView: 558 // HistoryView:
509 559
510 /** 560 /**
511 * Functions and state for populating the page with HTML. This should one-day 561 * Functions and state for populating the page with HTML. This should one-day
512 * contain the view and use event handlers, rather than pushing HTML out and 562 * contain the view and use event handlers, rather than pushing HTML out and
513 * getting called externally. 563 * getting called externally.
514 * @param {HistoryModel} model The model backing this view. 564 * @param {HistoryModel} model The model backing this view.
515 * @constructor 565 * @constructor
516 */ 566 */
(...skipping 18 matching lines...) Expand all
535 // Add handlers for the page navigation buttons at the bottom. 585 // Add handlers for the page navigation buttons at the bottom.
536 $('newest-button').addEventListener('click', function() { 586 $('newest-button').addEventListener('click', function() {
537 self.setPage(0); 587 self.setPage(0);
538 }); 588 });
539 $('newer-button').addEventListener('click', function() { 589 $('newer-button').addEventListener('click', function() {
540 self.setPage(self.pageIndex_ - 1); 590 self.setPage(self.pageIndex_ - 1);
541 }); 591 });
542 $('older-button').addEventListener('click', function() { 592 $('older-button').addEventListener('click', function() {
543 self.setPage(self.pageIndex_ + 1); 593 self.setPage(self.pageIndex_ + 1);
544 }); 594 });
595
596 $('display-filter-sites').addEventListener('click', function(e) {
597 self.setGroupByDomain($('display-filter-sites').checked);
598 });
545 } 599 }
546 600
547 // HistoryView, public: ------------------------------------------------------- 601 // HistoryView, public: -------------------------------------------------------
548 /** 602 /**
549 * Do a search and optionally view a certain page. 603 * Do a search and optionally view a certain page.
550 * @param {string} term The string to search for. 604 * @param {string} term The string to search for.
551 * @param {number} opt_page The page we wish to view, only use this for 605 * @param {number} opt_page The page we wish to view, only use this for
552 * setting up initial views, as this triggers a search. 606 * setting up initial views, as this triggers a search.
553 */ 607 */
554 HistoryView.prototype.setSearch = function(term, opt_page) { 608 HistoryView.prototype.setSearch = function(term, opt_page) {
555 this.pageIndex_ = parseInt(opt_page || 0, 10); 609 this.pageIndex_ = parseInt(opt_page || 0, 10);
556 window.scrollTo(0, 0); 610 window.scrollTo(0, 0);
557 this.model_.setSearchText(term, this.pageIndex_); 611 this.model_.setSearchText(term, this.pageIndex_);
558 pageState.setUIState(term, this.pageIndex_); 612 pageState.setUIState(term, this.pageIndex_, this.model_.getGroupByDomain());
559 }; 613 };
560 614
561 /** 615 /**
616 * Enable or disable results as being grouped by domain.
617 * @param {boolean} groupedByDomain Whether to group by domain or not.
618 */
619 HistoryView.prototype.setGroupByDomain = function(groupedByDomain) {
620 // Group by domain is not currently supported for search results, so reset
621 // the search term if there was one.
622 this.model_.clearSearchText();
623 this.model_.setGroupByDomain(groupedByDomain);
624 this.model_.reload();
625 pageState.setUIState(this.model_.getSearchText(),
626 this.pageIndex_,
627 this.model_.getGroupByDomain());
628 };
629
630 /**
562 * Reload the current view. 631 * Reload the current view.
563 */ 632 */
564 HistoryView.prototype.reload = function() { 633 HistoryView.prototype.reload = function() {
565 this.model_.reload(); 634 this.model_.reload();
566 this.updateRemoveButton(); 635 this.updateRemoveButton();
567 }; 636 };
568 637
569 /** 638 /**
570 * Switch to a specified page. 639 * Switch to a specified page.
571 * @param {number} page The page we wish to view. 640 * @param {number} page The page we wish to view.
572 */ 641 */
573 HistoryView.prototype.setPage = function(page) { 642 HistoryView.prototype.setPage = function(page) {
574 this.clear_(); 643 this.clear_();
575 this.pageIndex_ = parseInt(page, 10); 644 this.pageIndex_ = parseInt(page, 10);
576 window.scrollTo(0, 0); 645 window.scrollTo(0, 0);
577 this.model_.requestPage(page); 646 this.model_.requestPage(page);
578 pageState.setUIState(this.model_.getSearchText(), this.pageIndex_); 647 pageState.setUIState(this.model_.getSearchText(),
648 this.pageIndex_,
649 this.model_.getGroupByDomain());
579 }; 650 };
580 651
581 /** 652 /**
582 * @return {number} The page number being viewed. 653 * @return {number} The page number being viewed.
583 */ 654 */
584 HistoryView.prototype.getPage = function() { 655 HistoryView.prototype.getPage = function() {
585 return this.pageIndex_; 656 return this.pageIndex_;
586 }; 657 };
587 658
588 /** 659 /**
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
622 * Record that the given visit has been rendered. 693 * Record that the given visit has been rendered.
623 * @param {Visit} visit The visit that was rendered. 694 * @param {Visit} visit The visit that was rendered.
624 * @private 695 * @private
625 */ 696 */
626 HistoryView.prototype.setVisitRendered_ = function(visit) { 697 HistoryView.prototype.setVisitRendered_ = function(visit) {
627 visit.isRendered = true; 698 visit.isRendered = true;
628 this.currentVisits_.push(visit); 699 this.currentVisits_.push(visit);
629 }; 700 };
630 701
631 /** 702 /**
703 * This function generates and adds the grouped visits DOM for a certain
704 * domain. This includes the clickable arrow and domain name and the visit
705 * entries for that domain.
706 * @param {Element} results DOM object to which to add the elements.
707 * @param {string} domain Current domain name.
708 * @param {Array} domainVisits Array of visits for this domain.
709 * @private
710 */
711 HistoryView.prototype.getGroupedVisitsDOM_ = function(
712 results, domain, domainVisits) {
713 // Add a new domain entry.
714 var siteResults = results.appendChild(
715 createElementWithClassName('li', 'site-name'));
716 // Make a wrapper that will contain the arrow, the favicon and the domain.
717 var siteDomainWrapper = siteResults.appendChild(
718 createElementWithClassName('div', 'site-domain-wrapper'));
719 var siteArrow = siteDomainWrapper.appendChild(
720 createElementWithClassName('div', 'site-domain-arrow collapse'));
721 var siteDomain = siteDomainWrapper.appendChild(
722 createElementWithClassName('div', 'site-domain'));
723 var numberOfVisits = createElementWithClassName('span', 'number-visits');
724 numberOfVisits.textContent = loadTimeData.getStringF('numbervisits',
725 domainVisits.length);
726 siteDomain.textContent = domain;
727 siteDomain.appendChild(numberOfVisits);
728 siteResults.appendChild(siteDomainWrapper);
729 var resultsList = siteResults.appendChild(
730 createElementWithClassName('ol', 'site-results'));
731
732 domainVisits[0].addFaviconToElement_(siteDomain);
733
734 var toggleHandler = function(e) {
735 // |this| is the parent of the element which was clicked on.
736 var innerResultList = this.querySelector('.site-results');
737 var innerArrow = this.querySelector('.site-domain-arrow');
738 if (innerArrow.classList.contains('collapse')) {
739 innerResultList.style.height = 'auto';
740 // -webkit-transition does not work on height:auto elements so first set
741 // the height to auto so that it is computed and then set it to the
742 // computed value in pixels so the transition works properly.
743 var height = innerResultList.clientHeight;
744 innerResultList.style.height = height + 'px';
745 innerArrow.className = 'site-domain-arrow expand';
746 } else {
747 innerResultList.style.height = 0;
748 innerArrow.className = 'site-domain-arrow collapse';
749 }
750 };
751 // |siteResults| is the parent of the arrow and the results so use bind to
752 // make it easily accessible from the handler.
753 siteDomainWrapper.addEventListener('click', toggleHandler.bind(siteResults));
754 // Collapse until it gets toggled.
755 resultsList.style.height = 0;
756
757 // Add the results for each of the domain.
758 for (var j = 0, visit; visit = domainVisits[j]; j++) {
759 resultsList.appendChild(visit.getResultDOM({}));
760 this.setVisitRendered_(visit);
761 }
762 };
763
764 /**
765 * Groups visits by domain, sorting them by the number of visits.
766 * @param {Array} visits Visits received from the query results.
767 * @param {Element} results Object where the results are added to.
768 * @private
769 */
770 HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) {
771 var visitsByDomain = {};
772 var domains = [];
773
774 // Group the visits into a dictionary and generate a list of domains.
775 for (var i = 0, visit; visit = visits[i]; i++) {
776 var domain = visit.getDomainFromURL_(visit.url_);
777 if (!visitsByDomain[domain]) {
778 visitsByDomain[domain] = [];
779 domains.push(domain);
780 }
781 visitsByDomain[domain].push(visit);
782 }
783 var sortByVisits = function(a, b) {
784 return visitsByDomain[b].length - visitsByDomain[a].length;
785 };
786 domains.sort(sortByVisits);
787
788 for (var i = 0, domain; domain = domains[i]; i++) {
789 this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]);
790 }
791 };
792
793 /**
794 * Adds the results grouped by days, grouping them if needed.
795 * @param {Array} visits Visits returned by the query.
796 * @param {Element} parentElement Element to which to add the results to.
797 * @private
798 */
799 HistoryView.prototype.addDayResults_ = function(visits, parentElement) {
800 if (visits.length == 0)
801 return;
802
803 var firstVisit = visits[0];
804 var day = parentElement.appendChild(createElementWithClassName('h3', 'day'));
805 day.appendChild(document.createTextNode(firstVisit.dateRelativeDay));
806 if (firstVisit.continued) {
807 day.appendChild(document.createTextNode(' ' +
808 loadTimeData.getString('cont')));
809 }
810 var dayResults = parentElement.appendChild(
811 createElementWithClassName('ol', 'day-results'));
812
813 if (this.model_.getGroupByDomain()) {
814 this.groupVisitsByDomain_(visits, dayResults);
815 } else {
816 var lastTime;
817
818 for (var i = 0, visit; visit = visits[i]; i++) {
819 // If enough time has passed between visits, indicate a gap in browsing.
820 var thisTime = visit.date.getTime();
821 if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME)
822 dayResults.appendChild(createElementWithClassName('li', 'gap'));
823
824 // Insert the visit into the DOM.
825 dayResults.appendChild(visit.getResultDOM({
826 addTitleFavicon: true
827 }));
828 this.setVisitRendered_(visit);
829
830 lastTime = thisTime;
831 }
832 }
833 };
834
835 /**
632 * Update the page with results. 836 * Update the page with results.
633 * @private 837 * @private
634 */ 838 */
635 HistoryView.prototype.displayResults_ = function() { 839 HistoryView.prototype.displayResults_ = function() {
636 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE; 840 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
637 var rangeEnd = rangeStart + RESULTS_PER_PAGE; 841 var rangeEnd = rangeStart + RESULTS_PER_PAGE;
638 var results = this.model_.getNumberedRange(rangeStart, rangeEnd); 842 var results = this.model_.getNumberedRange(rangeStart, rangeEnd);
639 843
640 var searchText = this.model_.getSearchText(); 844 var searchText = this.model_.getSearchText();
845 var groupByDomain = this.model_.getGroupByDomain();
846
641 if (searchText) { 847 if (searchText) {
642 // Add a header for the search results, if there isn't already one. 848 // Add a header for the search results, if there isn't already one.
643 if (!this.resultDiv_.querySelector('h3')) { 849 if (!this.resultDiv_.querySelector('h3')) {
644 var header = document.createElement('h3'); 850 var header = document.createElement('h3');
645 header.textContent = loadTimeData.getStringF('searchresultsfor', 851 header.textContent = loadTimeData.getStringF('searchresultsfor',
646 searchText); 852 searchText);
647 this.resultDiv_.appendChild(header); 853 this.resultDiv_.appendChild(header);
648 } 854 }
649 855
650 var searchResults = createElementWithClassName('ol', 'search-results'); 856 var searchResults = createElementWithClassName('ol', 'search-results');
651 if (results.length == 0) { 857 if (results.length == 0) {
652 var noResults = document.createElement('div'); 858 var noResults = document.createElement('div');
653 noResults.textContent = loadTimeData.getString('noresults'); 859 noResults.textContent = loadTimeData.getString('noresults');
654 searchResults.appendChild(noResults); 860 searchResults.appendChild(noResults);
655 } else { 861 } else {
656 for (var i = 0, visit; visit = results[i]; i++) { 862 for (var i = 0, visit; visit = results[i]; i++) {
657 if (!visit.isRendered) { 863 if (!visit.isRendered) {
658 searchResults.appendChild(visit.getResultDOM(true)); 864 searchResults.appendChild(visit.getResultDOM({
865 isSearchResult: true,
866 addTitleFavicon: true
867 }));
659 this.setVisitRendered_(visit); 868 this.setVisitRendered_(visit);
660 } 869 }
661 } 870 }
662 } 871 }
663 this.resultDiv_.appendChild(searchResults); 872 this.resultDiv_.appendChild(searchResults);
664 } else { 873 } else {
665 var resultsFragment = document.createDocumentFragment(); 874 var resultsFragment = document.createDocumentFragment();
666 var lastTime = Math.infinity;
667 var dayResults;
668 875
876 if (this.model_.getGroupByDomain()) {
877 if (results.length == 0) {
878 var noResults = document.createElement('div');
879 noResults.textContent = loadTimeData.getString('noresultsinterval');
880 resultsFragment.appendChild(noResults);
881 }
882 }
883
884 var dayStartIndex = 0;
885
886 // Go through all of the visits and process them in chunks of one day.
669 for (var i = 0, visit; visit = results[i]; i++) { 887 for (var i = 0, visit; visit = results[i]; i++) {
670 if (visit.isRendered) 888 if (visit.isRendered) {
889 dayStartIndex = i;
671 continue; 890 continue;
672 891 }
673 var thisTime = visit.date.getTime();
674 892
675 // Break across day boundaries and insert gaps for browsing pauses. 893 // Break across day boundaries and insert gaps for browsing pauses.
676 // Create a dayResults element to contain results for each day. 894 // Create a dayResults element to contain results for each day.
677 if ((i == 0 && visit.continued) || !visit.continued) { 895 if ((i == 0 && visit.continued) || (i != 0 && !visit.continued)) {
678 // It's the first visit of the day, or the day is continued from 896 // Process the visits from the previous day.
679 // the previous page. Create a header for the day on the current page. 897 this.addDayResults_(
680 var day = createElementWithClassName('h3', 'day'); 898 results.slice(dayStartIndex, i), resultsFragment, groupByDomain);
681 day.appendChild(document.createTextNode(visit.dateRelativeDay)); 899 dayStartIndex = i;
682 if (visit.continued) { 900 }
683 day.appendChild(document.createTextNode(' ' + 901 }
684 loadTimeData.getString('cont'))); 902 // Process the final day.
685 } 903 this.addDayResults_(results.slice(dayStartIndex), resultsFragment);
686 904
687 resultsFragment.appendChild(day); 905 // Add all the days and their visits to the page.
688 dayResults = createElementWithClassName('ol', 'day-results');
689 resultsFragment.appendChild(dayResults);
690 } else if (dayResults && lastTime - thisTime > BROWSING_GAP_TIME) {
691 dayResults.appendChild(createElementWithClassName('li', 'gap'));
692 }
693 lastTime = thisTime;
694
695 // Add the entry to the appropriate day.
696 dayResults.appendChild(visit.getResultDOM(false));
697 this.setVisitRendered_(visit);
698 }
699 this.resultDiv_.appendChild(resultsFragment); 906 this.resultDiv_.appendChild(resultsFragment);
700 } 907 }
908 this.updateNavBar_();
701 }; 909 };
702 910
703 /** 911 /**
704 * Update the visibility of the page navigation buttons. 912 * Update the visibility of the page navigation buttons.
705 * @private 913 * @private
706 */ 914 */
707 HistoryView.prototype.updateNavBar_ = function() { 915 HistoryView.prototype.updateNavBar_ = function() {
708 $('newest-button').hidden = this.pageIndex_ == 0; 916 $('newest-button').hidden = this.pageIndex_ == 0;
709 $('newer-button').hidden = this.pageIndex_ == 0; 917 $('newer-button').hidden = this.pageIndex_ == 0;
710 $('older-button').hidden = !this.model_.hasMoreResults(); 918 $('older-button').hidden = !this.model_.hasMoreResults();
(...skipping 21 matching lines...) Expand all
732 } 940 }
733 941
734 // TODO(glen): Replace this with a bound method so we don't need 942 // TODO(glen): Replace this with a bound method so we don't need
735 // public model and view. 943 // public model and view.
736 this.checker_ = setInterval((function(state_obj) { 944 this.checker_ = setInterval((function(state_obj) {
737 var hashData = state_obj.getHashData(); 945 var hashData = state_obj.getHashData();
738 if (hashData.q != state_obj.model.getSearchText()) { 946 if (hashData.q != state_obj.model.getSearchText()) {
739 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); 947 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10));
740 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { 948 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) {
741 state_obj.view.setPage(hashData.p); 949 state_obj.view.setPage(hashData.p);
950 } else if ((hashData.g == 'true') !=
951 state_obj.view.model_.getGroupByDomain()) {
952 state_obj.view.setGroupByDomain(hashData.g);
742 } 953 }
743 }), 50, this); 954 }), 50, this);
744 } 955 }
745 956
746 /** 957 /**
747 * Holds the singleton instance. 958 * Holds the singleton instance.
748 */ 959 */
749 PageState.instance = null; 960 PageState.instance = null;
750 961
751 /** 962 /**
752 * @return {Object} An object containing parameters from our window hash. 963 * @return {Object} An object containing parameters from our window hash.
753 */ 964 */
754 PageState.prototype.getHashData = function() { 965 PageState.prototype.getHashData = function() {
755 var result = { 966 var result = {
756 e: 0, 967 e: 0,
757 q: '', 968 q: '',
758 p: 0 969 p: 0,
970 g: false
759 }; 971 };
760 972
761 if (!window.location.hash) { 973 if (!window.location.hash)
762 return result; 974 return result;
763 }
764 975
765 var hashSplit = window.location.hash.substr(1).split('&'); 976 var hashSplit = window.location.hash.substr(1).split('&');
766 for (var i = 0; i < hashSplit.length; i++) { 977 for (var i = 0; i < hashSplit.length; i++) {
767 var pair = hashSplit[i].split('='); 978 var pair = hashSplit[i].split('=');
768 if (pair.length > 1) { 979 if (pair.length > 1) {
769 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); 980 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
770 } 981 }
771 } 982 }
772 983
773 return result; 984 return result;
774 }; 985 };
775 986
776 /** 987 /**
777 * Set the hash to a specified state, this will create an entry in the 988 * Set the hash to a specified state, this will create an entry in the
778 * session history so the back button cycles through hash states, which 989 * session history so the back button cycles through hash states, which
779 * are then picked up by our listener. 990 * are then picked up by our listener.
780 * @param {string} term The current search string. 991 * @param {string} term The current search string.
781 * @param {string} page The page currently being viewed. 992 * @param {number} page The page currently being viewed.
993 * @param {boolean} grouped Whether the results are grouped or not.
782 */ 994 */
783 PageState.prototype.setUIState = function(term, page) { 995 PageState.prototype.setUIState = function(term, page, grouped) {
784 // Make sure the form looks pretty. 996 // Make sure the form looks pretty.
785 $('search-field').value = term; 997 $('search-field').value = term;
786 var currentHash = this.getHashData(); 998 if (grouped) {
787 if (currentHash.q != term || currentHash.p != page) { 999 $('display-filter-sites').checked = true;
788 window.location.hash = PageState.getHashString(term, page); 1000 } else {
1001 $('display-filter-sites').checked = false;
1002 }
1003 var hash = this.getHashData();
1004 if (hash.q != term || hash.p != page || hash.g != grouped) {
1005 window.location.hash = PageState.getHashString(
1006 term, page, grouped);
789 } 1007 }
790 }; 1008 };
791 1009
792 /** 1010 /**
793 * Static method to get the hash string for a specified state 1011 * Static method to get the hash string for a specified state
794 * @param {string} term The current search string. 1012 * @param {string} term The current search string.
795 * @param {string} page The page currently being viewed. 1013 * @param {number} page The page currently being viewed.
1014 * @param {boolean} grouped Whether the results are grouped or not.
796 * @return {string} The string to be used in a hash. 1015 * @return {string} The string to be used in a hash.
797 */ 1016 */
798 PageState.getHashString = function(term, page) { 1017 PageState.getHashString = function(term, page, grouped) {
1018 // Omit elements that are empty.
799 var newHash = []; 1019 var newHash = [];
800 if (term) { 1020
1021 if (term)
801 newHash.push('q=' + encodeURIComponent(term)); 1022 newHash.push('q=' + encodeURIComponent(term));
802 } 1023
803 if (page != undefined) { 1024 if (page)
804 newHash.push('p=' + page); 1025 newHash.push('p=' + page);
805 } 1026
1027 if (grouped)
1028 newHash.push('g=' + grouped);
806 1029
807 return newHash.join('&'); 1030 return newHash.join('&');
808 }; 1031 };
809 1032
810 /////////////////////////////////////////////////////////////////////////////// 1033 ///////////////////////////////////////////////////////////////////////////////
811 // Document Functions: 1034 // Document Functions:
812 /** 1035 /**
813 * Window onload handler, sets up the page. 1036 * Window onload handler, sets up the page.
814 */ 1037 */
815 function load() { 1038 function load() {
(...skipping 17 matching lines...) Expand all
833 1056
834 $('remove-visit').addEventListener('activate', function(e) { 1057 $('remove-visit').addEventListener('activate', function(e) {
835 activeVisit.removeFromHistory_(); 1058 activeVisit.removeFromHistory_();
836 activeVisit = null; 1059 activeVisit = null;
837 }); 1060 });
838 $('more-from-site').addEventListener('activate', function(e) { 1061 $('more-from-site').addEventListener('activate', function(e) {
839 activeVisit.showMoreFromSite_(); 1062 activeVisit.showMoreFromSite_();
840 activeVisit = null; 1063 activeVisit = null;
841 }); 1064 });
842 1065
1066 // Only show the controls if the command line switch is activated.
1067 if (loadTimeData.getBoolean('historyGroupEnabled')) {
1068 $('filter-controls').hidden = false;
1069 }
1070
843 var title = loadTimeData.getString('title'); 1071 var title = loadTimeData.getString('title');
844 uber.invokeMethodOnParent('setTitle', {title: title}); 1072 uber.invokeMethodOnParent('setTitle', {title: title});
845 1073
846 window.addEventListener('message', function(e) { 1074 window.addEventListener('message', function(e) {
847 if (e.data.method == 'frameSelected') 1075 if (e.data.method == 'frameSelected')
848 searchField.focus(); 1076 searchField.focus();
849 }); 1077 });
850 } 1078 }
851 1079
852 /** 1080 /**
(...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after
1079 historyView.reload(); 1307 historyView.reload();
1080 } 1308 }
1081 1309
1082 // Add handlers to HTML elements. 1310 // Add handlers to HTML elements.
1083 document.addEventListener('DOMContentLoaded', load); 1311 document.addEventListener('DOMContentLoaded', load);
1084 1312
1085 // This event lets us enable and disable menu items before the menu is shown. 1313 // This event lets us enable and disable menu items before the menu is shown.
1086 document.addEventListener('canExecute', function(e) { 1314 document.addEventListener('canExecute', function(e) {
1087 e.canExecute = true; 1315 e.canExecute = true;
1088 }); 1316 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698