Index: chrome/browser/resources/history/history.js |
diff --git a/chrome/browser/resources/history/history.js b/chrome/browser/resources/history/history.js |
index e595f6cfea8fc5586f687b5bfd31b0dd202ffd9f..71cd0b0593da4ba54bdd8d9080f419e292da6660 100644 |
--- a/chrome/browser/resources/history/history.js |
+++ b/chrome/browser/resources/history/history.js |
@@ -70,11 +70,17 @@ function Visit(result, continued, model, id) { |
/** |
* Returns a dom structure for a browse page result or a search page result. |
- * @param {boolean} searchResultFlag Indicates whether the result is a search |
- * result or not. |
+ * @param {Object} propertyBag A bag of configuration properties, false by |
+ * default: |
+ * <ul> |
+ * <li>isSearchResult: Whether or not the result is a search result.</li> |
+ * <li>addTitleFavicon: Whether or not the favicon should be added.</li> |
+ * </ul> |
* @return {Node} A DOM node to represent the history entry or search result. |
*/ |
-Visit.prototype.getResultDOM = function(searchResultFlag) { |
+Visit.prototype.getResultDOM = function(propertyBag) { |
+ var isSearchResult = propertyBag.isSearchResult || false; |
+ var addTitleFavicon = propertyBag.addTitleFavicon || false; |
var node = createElementWithClassName('li', 'entry'); |
var time = createElementWithClassName('div', 'time'); |
var entryBox = createElementWithClassName('label', 'entry-box'); |
@@ -113,9 +119,9 @@ Visit.prototype.getResultDOM = function(searchResultFlag) { |
// Prevent clicks on the drop down from affecting the checkbox. |
dropDown.addEventListener('click', function(e) { e.preventDefault(); }); |
- // We use a wrapper div so that the entry contents will be shinkwrapped. |
+ // We use a wrapper div so that the entry contents will be shrinkwrapped. |
entryBox.appendChild(time); |
- entryBox.appendChild(this.getTitleDOM_()); |
+ entryBox.appendChild(this.getTitleDOM_(addTitleFavicon)); |
entryBox.appendChild(domain); |
entryBox.appendChild(dropDown); |
@@ -129,7 +135,7 @@ Visit.prototype.getResultDOM = function(searchResultFlag) { |
node.appendChild(entryBox); |
- if (searchResultFlag) { |
+ if (isSearchResult) { |
time.appendChild(document.createTextNode(this.dateShort)); |
var snippet = createElementWithClassName('div', 'snippet'); |
this.addHighlightedText_(snippet, |
@@ -191,13 +197,18 @@ Visit.prototype.addHighlightedText_ = function(node, content, highlightText) { |
}; |
/** |
- * @return {DOMObject} DOM representation for the title block. |
+ * Returns the DOM element containing a link on the title of the URL for the |
+ * current visit. Optionally sets the favicon as well. |
+ * @param {boolean} addFavicon Whether to add a favicon or not. |
+ * @return {Element} DOM representation for the title block. |
* @private |
*/ |
-Visit.prototype.getTitleDOM_ = function() { |
+Visit.prototype.getTitleDOM_ = function(addFavicon) { |
var node = createElementWithClassName('div', 'title'); |
- node.style.backgroundImage = getFaviconImageSet(this.url_); |
- node.style.backgroundSize = '16px'; |
+ if (addFavicon) { |
+ node.style.backgroundImage = getFaviconImageSet(this.url_); |
+ node.style.backgroundSize = '16px'; |
+ } |
var link = document.createElement('a'); |
link.href = this.url_; |
@@ -221,6 +232,15 @@ Visit.prototype.getTitleDOM_ = function() { |
}; |
/** |
+ * Set the favicon for an element. |
+ * @param {Element} el The DOM element to which to add the icon. |
+ * @private |
+ */ |
+Visit.prototype.addFaviconToElement_ = function(el) { |
+ el.style.backgroundImage = getFaviconImageSet(this.url_); |
+}; |
+ |
+/** |
* Launch a search for more history entries from the same domain. |
* @private |
*/ |
@@ -308,14 +328,25 @@ HistoryModel.prototype.setSearchText = function(searchText, opt_page) { |
}; |
/** |
+ * Clear the search text. |
+ */ |
+HistoryModel.prototype.clearSearchText = function() { |
+ this.searchText_ = ''; |
+}; |
+ |
+/** |
* Reload our model with the current parameters. |
*/ |
HistoryModel.prototype.reload = function() { |
+ // Save user-visible state, clear the model, and restore the state. |
var search = this.searchText_; |
var page = this.requestedPage_; |
+ var groupByDomain = this.groupByDomain_; |
+ |
this.clearModel_(); |
this.searchText_ = search; |
this.requestedPage_ = page; |
+ this.groupByDomain_ = groupByDomain; |
this.queryHistory_(); |
}; |
@@ -348,9 +379,9 @@ HistoryModel.prototype.addResults = function(info, results) { |
this.isQueryFinished_ = info.finished; |
this.queryCursor_ = info.cursor; |
- // If there are no results, or they're not for the current search term, |
- // there's nothing more to do. |
- if (!results || !results.length || info.term != this.searchText_) |
+ // If the results are not for the current search term there's nothing more |
+ // to do. |
+ if (info.term != this.searchText_) |
return; |
// If necessary, sort the results from newest to oldest. |
@@ -415,6 +446,8 @@ HistoryModel.prototype.hasMoreResults = function() { |
HistoryModel.prototype.clearModel_ = function() { |
this.inFlight_ = false; // Whether a query is inflight. |
this.searchText_ = ''; |
+ // Flag to show that the results are grouped by domain or not. |
+ this.groupByDomain_ = false; |
this.visits_ = []; // Date-sorted list of visits (most recent first). |
this.last_id_ = 0; |
@@ -445,22 +478,21 @@ HistoryModel.prototype.clearModel_ = function() { |
/** |
* Figure out if we need to do more queries to fill the currently requested |
* page. If we think we can fill the page, call the view and let it know |
- * we're ready to show something. |
+ * we're ready to show something. This only applies to the daily time-based |
+ * view. |
* @private |
*/ |
HistoryModel.prototype.updateSearch_ = function() { |
- var doneLoading = |
- this.canFillPage_(this.requestedPage_) || this.isQueryFinished_; |
+ var doneLoading = this.isQueryFinished_ || |
+ this.canFillPage_(this.requestedPage_); |
- // Try to fetch more results if the current page isn't full. |
- if (!doneLoading && !this.inFlight_) |
+ // Try to fetch more results if the results are not grouped by domain and |
+ // the current page isn't full. |
+ if (!this.groupByDomain_ && !doneLoading && !this.inFlight_) |
this.queryHistory_(); |
- // If we have any data for the requested page, show it. |
- if (this.changed && this.haveDataForPage_(this.requestedPage_)) { |
- this.view_.onModelReady(); |
- this.changed = false; |
- } |
+ // Show the result or a message if no results were returned. |
+ this.view_.onModelReady(); |
}; |
/** |
@@ -469,19 +501,21 @@ HistoryModel.prototype.updateSearch_ = function() { |
*/ |
HistoryModel.prototype.queryHistory_ = function() { |
var endTime = 0; |
- |
- // If there are already some visits, pick up the previous query where it |
- // left off. |
- if (this.visits_.length > 0) { |
- var lastVisit = this.visits_.slice(-1)[0]; |
- endTime = lastVisit.date.getTime(); |
- cursor = this.queryCursor_; |
+ if (!this.getGroupByDomain()) { |
+ // Do the time-based search. |
+ // If there are already some visits, pick up the previous query where it |
+ // left off. |
+ if (this.visits_.length > 0) { |
+ var lastVisit = this.visits_.slice(-1)[0]; |
+ endTime = lastVisit.date.getTime(); |
+ cursor = this.queryCursor_; |
+ } |
} |
- |
$('loading-spinner').hidden = false; |
this.inFlight_ = true; |
chrome.send('queryHistory', |
- [this.searchText_, endTime, this.queryCursor_, RESULTS_PER_PAGE]); |
+ [this.searchText_, endTime, this.queryCursor_, |
+ RESULTS_PER_PAGE]); |
}; |
/** |
@@ -504,6 +538,22 @@ HistoryModel.prototype.canFillPage_ = function(page) { |
return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); |
}; |
+/** |
+ * Enables or disables grouping by domain. |
+ * @param {boolean} groupByDomain New groupByDomain_ value. |
+ */ |
+HistoryModel.prototype.setGroupByDomain = function(groupByDomain) { |
+ this.groupByDomain_ = groupByDomain; |
+}; |
+ |
+/** |
+ * Gets whether we are grouped by domain. |
+ * @return {boolean} Whether the results are grouped by domain. |
+ */ |
+HistoryModel.prototype.getGroupByDomain = function() { |
+ return this.groupByDomain_; |
+}; |
+ |
/////////////////////////////////////////////////////////////////////////////// |
// HistoryView: |
@@ -542,6 +592,10 @@ function HistoryView(model) { |
$('older-button').addEventListener('click', function() { |
self.setPage(self.pageIndex_ + 1); |
}); |
+ |
+ $('display-filter-sites').addEventListener('click', function(e) { |
+ self.setGroupByDomain($('display-filter-sites').checked); |
+ }); |
} |
// HistoryView, public: ------------------------------------------------------- |
@@ -555,7 +609,22 @@ HistoryView.prototype.setSearch = function(term, opt_page) { |
this.pageIndex_ = parseInt(opt_page || 0, 10); |
window.scrollTo(0, 0); |
this.model_.setSearchText(term, this.pageIndex_); |
- pageState.setUIState(term, this.pageIndex_); |
+ pageState.setUIState(term, this.pageIndex_, this.model_.getGroupByDomain()); |
+}; |
+ |
+/** |
+ * Enable or disable results as being grouped by domain. |
+ * @param {boolean} groupedByDomain Whether to group by domain or not. |
+ */ |
+HistoryView.prototype.setGroupByDomain = function(groupedByDomain) { |
+ // Group by domain is not currently supported for search results, so reset |
+ // the search term if there was one. |
+ this.model_.clearSearchText(); |
+ this.model_.setGroupByDomain(groupedByDomain); |
+ this.model_.reload(); |
+ pageState.setUIState(this.model_.getSearchText(), |
+ this.pageIndex_, |
+ this.model_.getGroupByDomain()); |
}; |
/** |
@@ -575,7 +644,9 @@ HistoryView.prototype.setPage = function(page) { |
this.pageIndex_ = parseInt(page, 10); |
window.scrollTo(0, 0); |
this.model_.requestPage(page); |
- pageState.setUIState(this.model_.getSearchText(), this.pageIndex_); |
+ pageState.setUIState(this.model_.getSearchText(), |
+ this.pageIndex_, |
+ this.model_.getGroupByDomain()); |
}; |
/** |
@@ -629,6 +700,139 @@ HistoryView.prototype.setVisitRendered_ = function(visit) { |
}; |
/** |
+ * This function generates and adds the grouped visits DOM for a certain |
+ * domain. This includes the clickable arrow and domain name and the visit |
+ * entries for that domain. |
+ * @param {Element} results DOM object to which to add the elements. |
+ * @param {string} domain Current domain name. |
+ * @param {Array} domainVisits Array of visits for this domain. |
+ * @private |
+ */ |
+HistoryView.prototype.getGroupedVisitsDOM_ = function( |
+ results, domain, domainVisits) { |
+ // Add a new domain entry. |
+ var siteResults = results.appendChild( |
+ createElementWithClassName('li', 'site-name')); |
+ // Make a wrapper that will contain the arrow, the favicon and the domain. |
+ var siteDomainWrapper = siteResults.appendChild( |
+ createElementWithClassName('div', 'site-domain-wrapper')); |
+ var siteArrow = siteDomainWrapper.appendChild( |
+ createElementWithClassName('div', 'site-domain-arrow')); |
+ var siteDomain = siteDomainWrapper.appendChild( |
+ createElementWithClassName('div', 'site-domain')); |
+ var numberOfVisits = createElementWithClassName('span', 'number-visits'); |
+ numberOfVisits.textContent = loadTimeData.getStringF('numbervisits', |
+ domainVisits.length); |
+ siteDomain.textContent = domain; |
+ siteDomain.appendChild(numberOfVisits); |
+ siteResults.appendChild(siteDomainWrapper); |
+ var resultsList = siteResults.appendChild( |
+ createElementWithClassName('ol', 'site-results collapse')); |
+ |
+ domainVisits[0].addFaviconToElement_(siteDomain); |
+ |
+ var toggleHandler = function(e) { |
+ // |this| is the parent of the element which was clicked on. |
+ var innerResultList = this.querySelector('.site-results'); |
+ var innerArrow = this.querySelector('.site-domain-arrow'); |
+ if (innerResultList.classList.contains('collapse')) { |
+ innerResultList.style.height = 'auto'; |
+ // -webkit-transition does not work on height:auto elements so first set |
+ // the height to auto so that it is computed and then set it to the |
+ // computed value in pixels so the transition works properly. |
+ var height = innerResultList.clientHeight; |
+ innerResultList.style.height = height + 'px'; |
+ innerArrow.className = 'site-domain-arrow expand'; |
+ } else { |
+ innerResultList.style.height = 0; |
+ innerArrow.className = 'site-domain-arrow collapse'; |
+ } |
+ }; |
+ // |siteResults| is the parent of the arrow and the results so use bind to |
+ // make it easily accessible from the handler. |
+ siteDomainWrapper.addEventListener('click', toggleHandler.bind(siteResults)); |
+ // Collapse until it gets toggled. |
+ resultsList.style.height = 0; |
+ |
+ // Add the results for each of the domain. |
+ for (var j = 0, visit; visit = domainVisits[j]; j++) { |
+ resultsList.appendChild(visit.getResultDOM({})); |
+ this.setVisitRendered_(visit); |
+ } |
+}; |
+ |
+/** |
+ * Groups visits by domain, sorting them by the number of visits. |
+ * @param {Array} visits Visits received from the query results. |
+ * @param {Element} results Object where the results are added to. |
+ * @private |
+ */ |
+HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) { |
+ var visitsByDomain = {}; |
+ var domains = []; |
+ |
+ // Group the visits into a dictionary and generate a list of domains. |
+ for (var i = 0, visit; visit = visits[i]; i++) { |
+ var domain = visit.getDomainFromURL_(visit.url_); |
+ if (!visitsByDomain[domain]) { |
+ visitsByDomain[domain] = []; |
+ domains.push(domain); |
+ } |
+ visitsByDomain[domain].push(visit); |
+ } |
+ var sortByVisits = function(a, b) { |
+ return visitsByDomain[b].length - visitsByDomain[a].length; |
+ }; |
+ domains.sort(sortByVisits); |
+ |
+ for (var i = 0, domain; domain = domains[i]; i++) { |
+ this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]); |
+ } |
+}; |
+ |
+/** |
+ * Adds the results grouped by days, grouping them if needed. |
+ * @param {Array} visits Visits returned by the query. |
+ * @param {Element} parentElement Element to which to add the results to. |
+ * @private |
+ */ |
+HistoryView.prototype.addDayResults_ = function(visits, parentElement) { |
+ if (visits.length == 0) |
+ return; |
+ |
+ var firstVisit = visits[0]; |
+ var day = parentElement.appendChild(createElementWithClassName('h3', 'day')); |
+ day.appendChild(document.createTextNode(firstVisit.dateRelativeDay)); |
+ if (firstVisit.continued) { |
+ day.appendChild(document.createTextNode(' ' + |
+ loadTimeData.getString('cont'))); |
+ } |
+ var dayResults = parentElement.appendChild( |
+ createElementWithClassName('ol', 'day-results')); |
+ |
+ if (this.model_.getGroupByDomain()) { |
+ this.groupVisitsByDomain_(visits, dayResults); |
+ } else { |
+ var lastTime; |
+ |
+ for (var i = 0, visit; visit = visits[i]; i++) { |
+ // If enough time has passed between visits, indicate a gap in browsing. |
+ var thisTime = visit.date.getTime(); |
+ if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME) |
+ dayResults.appendChild(createElementWithClassName('li', 'gap')); |
+ |
+ // Insert the visit into the DOM. |
+ dayResults.appendChild(visit.getResultDOM({ |
+ addTitleFavicon: true |
+ })); |
+ this.setVisitRendered_(visit); |
+ |
+ lastTime = thisTime; |
+ } |
+ } |
+}; |
+ |
+/** |
* Update the page with results. |
* @private |
*/ |
@@ -638,6 +842,8 @@ HistoryView.prototype.displayResults_ = function() { |
var results = this.model_.getNumberedRange(rangeStart, rangeEnd); |
var searchText = this.model_.getSearchText(); |
+ var groupByDomain = this.model_.getGroupByDomain(); |
+ |
if (searchText) { |
// Add a header for the search results, if there isn't already one. |
if (!this.resultDiv_.querySelector('h3')) { |
@@ -655,7 +861,10 @@ HistoryView.prototype.displayResults_ = function() { |
} else { |
for (var i = 0, visit; visit = results[i]; i++) { |
if (!visit.isRendered) { |
- searchResults.appendChild(visit.getResultDOM(true)); |
+ searchResults.appendChild(visit.getResultDOM({ |
+ isSearchResult: true, |
+ addTitleFavicon: true |
+ })); |
this.setVisitRendered_(visit); |
} |
} |
@@ -663,41 +872,40 @@ HistoryView.prototype.displayResults_ = function() { |
this.resultDiv_.appendChild(searchResults); |
} else { |
var resultsFragment = document.createDocumentFragment(); |
- var lastTime = Math.infinity; |
- var dayResults; |
+ if (this.model_.getGroupByDomain()) { |
+ if (results.length == 0) { |
+ var noResults = document.createElement('div'); |
+ noResults.textContent = loadTimeData.getString('noresultsinterval'); |
+ resultsFragment.appendChild(noResults); |
+ } |
+ } |
+ |
+ var dayStartIndex = 0; |
+ |
+ // Go through all of the visits and process them in chunks of one day. |
for (var i = 0, visit; visit = results[i]; i++) { |
- if (visit.isRendered) |
+ if (visit.isRendered) { |
+ dayStartIndex = i; |
continue; |
- |
- var thisTime = visit.date.getTime(); |
+ } |
// Break across day boundaries and insert gaps for browsing pauses. |
// Create a dayResults element to contain results for each day. |
- if ((i == 0 && visit.continued) || !visit.continued) { |
- // It's the first visit of the day, or the day is continued from |
- // the previous page. Create a header for the day on the current page. |
- var day = createElementWithClassName('h3', 'day'); |
- day.appendChild(document.createTextNode(visit.dateRelativeDay)); |
- if (visit.continued) { |
- day.appendChild(document.createTextNode(' ' + |
- loadTimeData.getString('cont'))); |
- } |
- |
- resultsFragment.appendChild(day); |
- dayResults = createElementWithClassName('ol', 'day-results'); |
- resultsFragment.appendChild(dayResults); |
- } else if (dayResults && lastTime - thisTime > BROWSING_GAP_TIME) { |
- dayResults.appendChild(createElementWithClassName('li', 'gap')); |
+ if ((i == 0 && visit.continued) || (i != 0 && !visit.continued)) { |
+ // Process the visits from the previous day. |
+ this.addDayResults_( |
+ results.slice(dayStartIndex, i), resultsFragment, groupByDomain); |
+ dayStartIndex = i; |
} |
- lastTime = thisTime; |
- |
- // Add the entry to the appropriate day. |
- dayResults.appendChild(visit.getResultDOM(false)); |
- this.setVisitRendered_(visit); |
} |
+ // Process the final day. |
+ this.addDayResults_(results.slice(dayStartIndex), resultsFragment); |
+ |
+ // Add all the days and their visits to the page. |
this.resultDiv_.appendChild(resultsFragment); |
} |
+ this.updateNavBar_(); |
}; |
/** |
@@ -739,6 +947,9 @@ function PageState(model, view) { |
state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); |
} else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { |
state_obj.view.setPage(hashData.p); |
+ } else if ((hashData.g == 'true') != |
+ state_obj.view.model_.getGroupByDomain()) { |
+ state_obj.view.setGroupByDomain(hashData.g); |
} |
}), 50, this); |
} |
@@ -755,12 +966,12 @@ PageState.prototype.getHashData = function() { |
var result = { |
e: 0, |
q: '', |
- p: 0 |
+ p: 0, |
+ g: false |
}; |
- if (!window.location.hash) { |
+ if (!window.location.hash) |
return result; |
- } |
var hashSplit = window.location.hash.substr(1).split('&'); |
for (var i = 0; i < hashSplit.length; i++) { |
@@ -778,31 +989,43 @@ PageState.prototype.getHashData = function() { |
* session history so the back button cycles through hash states, which |
* are then picked up by our listener. |
* @param {string} term The current search string. |
- * @param {string} page The page currently being viewed. |
+ * @param {number} page The page currently being viewed. |
+ * @param {boolean} grouped Whether the results are grouped or not. |
*/ |
-PageState.prototype.setUIState = function(term, page) { |
+PageState.prototype.setUIState = function(term, page, grouped) { |
// Make sure the form looks pretty. |
$('search-field').value = term; |
- var currentHash = this.getHashData(); |
- if (currentHash.q != term || currentHash.p != page) { |
- window.location.hash = PageState.getHashString(term, page); |
+ if (grouped) { |
+ $('display-filter-sites').checked = true; |
+ } else { |
+ $('display-filter-sites').checked = false; |
+ } |
+ var hash = this.getHashData(); |
+ if (hash.q != term || hash.p != page || hash.g != grouped) { |
+ window.location.hash = PageState.getHashString( |
+ term, page, grouped); |
} |
}; |
/** |
* Static method to get the hash string for a specified state |
* @param {string} term The current search string. |
- * @param {string} page The page currently being viewed. |
+ * @param {number} page The page currently being viewed. |
+ * @param {boolean} grouped Whether the results are grouped or not. |
* @return {string} The string to be used in a hash. |
*/ |
-PageState.getHashString = function(term, page) { |
+PageState.getHashString = function(term, page, grouped) { |
+ // Omit elements that are empty. |
var newHash = []; |
- if (term) { |
+ |
+ if (term) |
newHash.push('q=' + encodeURIComponent(term)); |
- } |
- if (page != undefined) { |
+ |
+ if (page) |
newHash.push('p=' + page); |
- } |
+ |
+ if (grouped) |
+ newHash.push('g=' + grouped); |
return newHash.join('&'); |
}; |
@@ -840,6 +1063,11 @@ function load() { |
activeVisit = null; |
}); |
+ // Only show the controls if the command line switch is activated. |
+ if (loadTimeData.getBoolean('historyGroupEnabled')) { |
+ $('filter-controls').hidden = false; |
+ } |
+ |
var title = loadTimeData.getString('title'); |
uber.invokeMethodOnParent('setTitle', {title: title}); |