Home | History | Annotate | Download | only in history
      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
      3 // found in the LICENSE file.
      4 
      5 <include src="../uber/uber_utils.js">
      6 <include src="history_focus_manager.js">
      7 
      8 ///////////////////////////////////////////////////////////////////////////////
      9 // Globals:
     10 /** @const */ var RESULTS_PER_PAGE = 150;
     11 
     12 // Amount of time between pageviews that we consider a 'break' in browsing,
     13 // measured in milliseconds.
     14 /** @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000;
     15 
     16 // The largest bucket value for UMA histogram, based on entry ID. All entries
     17 // with IDs greater than this will be included in this bucket.
     18 /** @const */ var UMA_MAX_BUCKET_VALUE = 1000;
     19 
     20 // The largest bucket value for a UMA histogram that is a subset of above.
     21 /** @const */ var UMA_MAX_SUBSET_BUCKET_VALUE = 100;
     22 
     23 // TODO(glen): Get rid of these global references, replace with a controller
     24 //     or just make the classes own more of the page.
     25 var historyModel;
     26 var historyView;
     27 var pageState;
     28 var selectionAnchor = -1;
     29 var activeVisit = null;
     30 
     31 /** @const */ var Command = cr.ui.Command;
     32 /** @const */ var Menu = cr.ui.Menu;
     33 /** @const */ var MenuButton = cr.ui.MenuButton;
     34 
     35 /**
     36  * Enum that shows the filtering behavior for a host or URL to a managed user.
     37  * Must behave like the FilteringBehavior enum from managed_mode_url_filter.h.
     38  * @enum {number}
     39  */
     40 ManagedModeFilteringBehavior = {
     41   ALLOW: 0,
     42   WARN: 1,
     43   BLOCK: 2
     44 };
     45 
     46 MenuButton.createDropDownArrows();
     47 
     48 /**
     49  * Returns true if the mobile (non-desktop) version is being shown.
     50  * @return {boolean} true if the mobile version is being shown.
     51  */
     52 function isMobileVersion() {
     53   return !document.body.classList.contains('uber-frame');
     54 }
     55 
     56 /**
     57  * Record an action in UMA.
     58  * @param {string} actionDesc The name of the action to be logged.
     59  */
     60 function recordUmaAction(actionDesc) {
     61   chrome.send('metricsHandler:recordAction', [actionDesc]);
     62 }
     63 
     64 /**
     65  * Record a histogram value in UMA. If specified value is larger than the max
     66  * bucket value, record the value in the largest bucket.
     67  * @param {string} histogram The name of the histogram to be recorded in.
     68  * @param {integer} maxBucketValue The max value for the last histogram bucket.
     69  * @param {integer} value The value to record in the histogram.
     70  */
     71 
     72 function recordUmaHistogram(histogram, maxBucketValue, value) {
     73   chrome.send('metricsHandler:recordInHistogram',
     74               [histogram,
     75               ((value > maxBucketValue) ? maxBucketValue : value),
     76               maxBucketValue]);
     77 }
     78 
     79 ///////////////////////////////////////////////////////////////////////////////
     80 // Visit:
     81 
     82 /**
     83  * Class to hold all the information about an entry in our model.
     84  * @param {Object} result An object containing the visit's data.
     85  * @param {boolean} continued Whether this visit is on the same day as the
     86  *     visit before it.
     87  * @param {HistoryModel} model The model object this entry belongs to.
     88  * @constructor
     89  */
     90 function Visit(result, continued, model) {
     91   this.model_ = model;
     92   this.title_ = result.title;
     93   this.url_ = result.url;
     94   this.starred_ = result.starred;
     95   this.snippet_ = result.snippet || '';
     96 
     97   // These identify the name and type of the device on which this visit
     98   // occurred. They will be empty if the visit occurred on the current device.
     99   this.deviceName = result.deviceName;
    100   this.deviceType = result.deviceType;
    101 
    102   // The ID will be set according to when the visit was displayed, not
    103   // received. Set to -1 to show that it has not been set yet.
    104   this.id_ = -1;
    105 
    106   this.isRendered = false;  // Has the visit already been rendered on the page?
    107 
    108   // All the date information is public so that owners can compare properties of
    109   // two items easily.
    110 
    111   this.date = new Date(result.time);
    112 
    113   // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
    114   // get all of these.
    115   this.dateRelativeDay = result.dateRelativeDay || '';
    116   this.dateTimeOfDay = result.dateTimeOfDay || '';
    117   this.dateShort = result.dateShort || '';
    118 
    119   // Shows the filtering behavior for that host (only used for managed users).
    120   // A value of |ManagedModeFilteringBehavior.ALLOW| is not displayed so it is
    121   // used as the default value.
    122   this.hostFilteringBehavior = ManagedModeFilteringBehavior.ALLOW;
    123   if (typeof result.hostFilteringBehavior != 'undefined')
    124     this.hostFilteringBehavior = result.hostFilteringBehavior;
    125 
    126   this.blockedVisit = result.blockedVisit || false;
    127 
    128   // Whether this is the continuation of a previous day.
    129   this.continued = continued;
    130 
    131   this.allTimestamps = result.allTimestamps;
    132 }
    133 
    134 // Visit, public: -------------------------------------------------------------
    135 
    136 /**
    137  * Returns a dom structure for a browse page result or a search page result.
    138  * @param {Object} propertyBag A bag of configuration properties, false by
    139  * default:
    140  *  - isSearchResult: Whether or not the result is a search result.
    141  *  - addTitleFavicon: Whether or not the favicon should be added.
    142  *  - useMonthDate: Whether or not the full date should be inserted (used for
    143  * monthly view).
    144  * @return {Node} A DOM node to represent the history entry or search result.
    145  */
    146 Visit.prototype.getResultDOM = function(propertyBag) {
    147   var isSearchResult = propertyBag.isSearchResult || false;
    148   var addTitleFavicon = propertyBag.addTitleFavicon || false;
    149   var useMonthDate = propertyBag.useMonthDate || false;
    150   var node = createElementWithClassName('li', 'entry');
    151   var time = createElementWithClassName('div', 'time');
    152   var entryBox = createElementWithClassName('label', 'entry-box');
    153   var domain = createElementWithClassName('div', 'domain');
    154 
    155   this.id_ = this.model_.nextVisitId_++;
    156 
    157   // Only create the checkbox if it can be used either to delete an entry or to
    158   // block/allow it.
    159   if (this.model_.editingEntriesAllowed) {
    160     var checkbox = document.createElement('input');
    161     checkbox.type = 'checkbox';
    162     checkbox.id = 'checkbox-' + this.id_;
    163     checkbox.time = this.date.getTime();
    164     checkbox.addEventListener('click', checkboxClicked);
    165     entryBox.appendChild(checkbox);
    166 
    167     // Clicking anywhere in the entryBox will check/uncheck the checkbox.
    168     entryBox.setAttribute('for', checkbox.id);
    169     entryBox.addEventListener('mousedown', entryBoxMousedown);
    170     entryBox.addEventListener('click', entryBoxClick);
    171   }
    172 
    173   // Keep track of the drop down that triggered the menu, so we know
    174   // which element to apply the command to.
    175   // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
    176   var self = this;
    177   var setActiveVisit = function(e) {
    178     activeVisit = self;
    179     var menu = $('action-menu');
    180     menu.dataset.devicename = self.deviceName;
    181     menu.dataset.devicetype = self.deviceType;
    182   };
    183   domain.textContent = this.getDomainFromURL_(this.url_);
    184 
    185   entryBox.appendChild(time);
    186 
    187   var bookmarkSection = createElementWithClassName('div', 'bookmark-section');
    188   if (this.starred_) {
    189     bookmarkSection.classList.add('starred');
    190     bookmarkSection.addEventListener('click', function f(e) {
    191       recordUmaAction('HistoryPage_BookmarkStarClicked');
    192       bookmarkSection.classList.remove('starred');
    193       chrome.send('removeBookmark', [self.url_]);
    194       bookmarkSection.removeEventListener('click', f);
    195       e.preventDefault();
    196     });
    197   }
    198   entryBox.appendChild(bookmarkSection);
    199 
    200   var visitEntryWrapper = entryBox.appendChild(document.createElement('div'));
    201   if (addTitleFavicon || this.blockedVisit)
    202     visitEntryWrapper.classList.add('visit-entry');
    203   if (this.blockedVisit) {
    204     visitEntryWrapper.classList.add('blocked-indicator');
    205     visitEntryWrapper.appendChild(this.getVisitAttemptDOM_());
    206   } else {
    207     visitEntryWrapper.appendChild(this.getTitleDOM_(isSearchResult));
    208     if (addTitleFavicon)
    209       this.addFaviconToElement_(visitEntryWrapper);
    210     visitEntryWrapper.appendChild(domain);
    211   }
    212 
    213   if (isMobileVersion()) {
    214     var removeButton = createElementWithClassName('button', 'remove-entry');
    215     removeButton.classList.add('custom-appearance');
    216     removeButton.addEventListener('click', function(e) {
    217       self.removeFromHistory();
    218       e.stopPropagation();
    219       e.preventDefault();
    220     });
    221     entryBox.appendChild(removeButton);
    222 
    223     // Support clicking anywhere inside the entry box.
    224     entryBox.addEventListener('click', function(e) {
    225       e.currentTarget.querySelector('a').click();
    226     });
    227   } else {
    228     var dropDown = createElementWithClassName('button', 'drop-down');
    229     dropDown.value = 'Open action menu';
    230     dropDown.title = loadTimeData.getString('actionMenuDescription');
    231     dropDown.setAttribute('menu', '#action-menu');
    232     cr.ui.decorate(dropDown, MenuButton);
    233 
    234     dropDown.addEventListener('mousedown', setActiveVisit);
    235     dropDown.addEventListener('focus', setActiveVisit);
    236 
    237     // Prevent clicks on the drop down from affecting the checkbox.
    238     dropDown.addEventListener('click', function(e) { e.preventDefault(); });
    239     entryBox.appendChild(dropDown);
    240   }
    241 
    242   // Let the entryBox be styled appropriately when it contains keyboard focus.
    243   entryBox.addEventListener('focus', function() {
    244     this.classList.add('contains-focus');
    245   }, true);
    246   entryBox.addEventListener('blur', function() {
    247     this.classList.remove('contains-focus');
    248   }, true);
    249 
    250   var entryBoxContainer =
    251       createElementWithClassName('div', 'entry-box-container');
    252   node.appendChild(entryBoxContainer);
    253   entryBoxContainer.appendChild(entryBox);
    254 
    255   if (isSearchResult) {
    256     time.appendChild(document.createTextNode(this.dateShort));
    257     var snippet = createElementWithClassName('div', 'snippet');
    258     this.addHighlightedText_(snippet,
    259                              this.snippet_,
    260                              this.model_.getSearchText());
    261     node.appendChild(snippet);
    262   } else if (useMonthDate) {
    263     // Show the day instead of the time.
    264     time.appendChild(document.createTextNode(this.dateShort));
    265   } else {
    266     time.appendChild(document.createTextNode(this.dateTimeOfDay));
    267   }
    268 
    269   this.domNode_ = node;
    270   node.visit = this;
    271 
    272   return node;
    273 };
    274 
    275 /**
    276  * Remove this visit from the history.
    277  */
    278 Visit.prototype.removeFromHistory = function() {
    279   recordUmaAction('HistoryPage_EntryMenuRemoveFromHistory');
    280   var self = this;
    281   this.model_.removeVisitsFromHistory([this], function() {
    282     removeEntryFromView(self.domNode_);
    283   });
    284 };
    285 
    286 // Visit, private: ------------------------------------------------------------
    287 
    288 /**
    289  * Extracts and returns the domain (and subdomains) from a URL.
    290  * @param {string} url The url.
    291  * @return {string} The domain. An empty string is returned if no domain can
    292  *     be found.
    293  * @private
    294  */
    295 Visit.prototype.getDomainFromURL_ = function(url) {
    296   // TODO(sergiu): Extract the domain from the C++ side and send it here.
    297   var domain = url.replace(/^.+?:\/\//, '').match(/[^/]+/);
    298   return domain ? domain[0] : '';
    299 };
    300 
    301 /**
    302  * Add child text nodes to a node such that occurrences of the specified text is
    303  * highlighted.
    304  * @param {Node} node The node under which new text nodes will be made as
    305  *     children.
    306  * @param {string} content Text to be added beneath |node| as one or more
    307  *     text nodes.
    308  * @param {string} highlightText Occurences of this text inside |content| will
    309  *     be highlighted.
    310  * @private
    311  */
    312 Visit.prototype.addHighlightedText_ = function(node, content, highlightText) {
    313   var i = 0;
    314   if (highlightText) {
    315     var re = new RegExp(Visit.pregQuote_(highlightText), 'gim');
    316     var match;
    317     while (match = re.exec(content)) {
    318       if (match.index > i)
    319         node.appendChild(document.createTextNode(content.slice(i,
    320                                                                match.index)));
    321       i = re.lastIndex;
    322       // Mark the highlighted text in bold.
    323       var b = document.createElement('b');
    324       b.textContent = content.substring(match.index, i);
    325       node.appendChild(b);
    326     }
    327   }
    328   if (i < content.length)
    329     node.appendChild(document.createTextNode(content.slice(i)));
    330 };
    331 
    332 /**
    333  * Returns the DOM element containing a link on the title of the URL for the
    334  * current visit.
    335  * @param {boolean} isSearchResult Whether or not the entry is a search result.
    336  * @return {Element} DOM representation for the title block.
    337  * @private
    338  */
    339 Visit.prototype.getTitleDOM_ = function(isSearchResult) {
    340   var node = createElementWithClassName('div', 'title');
    341   var link = document.createElement('a');
    342   link.href = this.url_;
    343   link.id = 'id-' + this.id_;
    344   link.target = '_top';
    345   var integerId = parseInt(this.id_, 10);
    346   link.addEventListener('click', function() {
    347     recordUmaAction('HistoryPage_EntryLinkClick');
    348     // Record the ID of the entry to signify how many entries are above this
    349     // link on the page.
    350     recordUmaHistogram('HistoryPage.ClickPosition',
    351                        UMA_MAX_BUCKET_VALUE,
    352                        integerId);
    353     if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
    354       recordUmaHistogram('HistoryPage.ClickPositionSubset',
    355                          UMA_MAX_SUBSET_BUCKET_VALUE,
    356                          integerId);
    357     }
    358   });
    359   link.addEventListener('contextmenu', function() {
    360     recordUmaAction('HistoryPage_EntryLinkRightClick');
    361   });
    362 
    363   if (isSearchResult) {
    364     link.addEventListener('click', function() {
    365       recordUmaAction('HistoryPage_SearchResultClick');
    366     });
    367   }
    368 
    369   // Add a tooltip, since it might be ellipsized.
    370   // TODO(dubroy): Find a way to show the tooltip only when necessary.
    371   link.title = this.title_;
    372 
    373   this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
    374   node.appendChild(link);
    375 
    376   return node;
    377 };
    378 
    379 /**
    380  * Returns the DOM element containing the text for a blocked visit attempt.
    381  * @return {Element} DOM representation of the visit attempt.
    382  * @private
    383  */
    384 Visit.prototype.getVisitAttemptDOM_ = function() {
    385   var node = createElementWithClassName('div', 'title');
    386   node.innerHTML = loadTimeData.getStringF('blockedVisitText',
    387                                            this.url_,
    388                                            this.id_,
    389                                            this.getDomainFromURL_(this.url_));
    390   return node;
    391 };
    392 
    393 /**
    394  * Set the favicon for an element.
    395  * @param {Element} el The DOM element to which to add the icon.
    396  * @private
    397  */
    398 Visit.prototype.addFaviconToElement_ = function(el) {
    399   var url = isMobileVersion() ?
    400       getFaviconImageSet(this.url_, 32, 'touch-icon') :
    401       getFaviconImageSet(this.url_);
    402   el.style.backgroundImage = url;
    403 };
    404 
    405 /**
    406  * Launch a search for more history entries from the same domain.
    407  * @private
    408  */
    409 Visit.prototype.showMoreFromSite_ = function() {
    410   recordUmaAction('HistoryPage_EntryMenuShowMoreFromSite');
    411   historyView.setSearch(this.getDomainFromURL_(this.url_));
    412 };
    413 
    414 // Visit, private, static: ----------------------------------------------------
    415 
    416 /**
    417  * Quote a string so it can be used in a regular expression.
    418  * @param {string} str The source string.
    419  * @return {string} The escaped string.
    420  * @private
    421  */
    422 Visit.pregQuote_ = function(str) {
    423   return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
    424 };
    425 
    426 ///////////////////////////////////////////////////////////////////////////////
    427 // HistoryModel:
    428 
    429 /**
    430  * Global container for history data. Future optimizations might include
    431  * allowing the creation of a HistoryModel for each search string, allowing
    432  * quick flips back and forth between results.
    433  *
    434  * The history model is based around pages, and only fetching the data to
    435  * fill the currently requested page. This is somewhat dependent on the view,
    436  * and so future work may wish to change history model to operate on
    437  * timeframe (day or week) based containers.
    438  *
    439  * @constructor
    440  */
    441 function HistoryModel() {
    442   this.clearModel_();
    443 }
    444 
    445 // HistoryModel, Public: ------------------------------------------------------
    446 
    447 /** @enum {number} */
    448 HistoryModel.Range = {
    449   ALL_TIME: 0,
    450   WEEK: 1,
    451   MONTH: 2
    452 };
    453 
    454 /**
    455  * Sets our current view that is called when the history model changes.
    456  * @param {HistoryView} view The view to set our current view to.
    457  */
    458 HistoryModel.prototype.setView = function(view) {
    459   this.view_ = view;
    460 };
    461 
    462 /**
    463  * Reload our model with the current parameters.
    464  */
    465 HistoryModel.prototype.reload = function() {
    466   // Save user-visible state, clear the model, and restore the state.
    467   var search = this.searchText_;
    468   var page = this.requestedPage_;
    469   var range = this.rangeInDays_;
    470   var offset = this.offset_;
    471   var groupByDomain = this.groupByDomain_;
    472 
    473   this.clearModel_();
    474   this.searchText_ = search;
    475   this.requestedPage_ = page;
    476   this.rangeInDays_ = range;
    477   this.offset_ = offset;
    478   this.groupByDomain_ = groupByDomain;
    479   this.queryHistory_();
    480 };
    481 
    482 /**
    483  * @return {string} The current search text.
    484  */
    485 HistoryModel.prototype.getSearchText = function() {
    486   return this.searchText_;
    487 };
    488 
    489 /**
    490  * Tell the model that the view will want to see the current page. When
    491  * the data becomes available, the model will call the view back.
    492  * @param {number} page The page we want to view.
    493  */
    494 HistoryModel.prototype.requestPage = function(page) {
    495   this.requestedPage_ = page;
    496   this.updateSearch_();
    497 };
    498 
    499 /**
    500  * Receiver for history query.
    501  * @param {Object} info An object containing information about the query.
    502  * @param {Array} results A list of results.
    503  */
    504 HistoryModel.prototype.addResults = function(info, results) {
    505   // If no requests are in flight then this was an old request so we drop the
    506   // results. Double check the search term as well.
    507   if (!this.inFlight_ || info.term != this.searchText_)
    508     return;
    509 
    510   $('loading-spinner').hidden = true;
    511   this.inFlight_ = false;
    512   this.isQueryFinished_ = info.finished;
    513   this.queryStartTime = info.queryStartTime;
    514   this.queryEndTime = info.queryEndTime;
    515 
    516   var lastVisit = this.visits_.slice(-1)[0];
    517   var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
    518 
    519   for (var i = 0, result; result = results[i]; i++) {
    520     var thisDay = result.dateRelativeDay;
    521     var isSameDay = lastDay == thisDay;
    522     this.visits_.push(new Visit(result, isSameDay, this));
    523     lastDay = thisDay;
    524   }
    525 
    526   if (loadTimeData.getBoolean('isUserSignedIn')) {
    527     var message = loadTimeData.getString(
    528         info.hasSyncedResults ? 'hasSyncedResults' : 'noSyncedResults');
    529     this.view_.showNotification(message);
    530   }
    531 
    532   this.updateSearch_();
    533 };
    534 
    535 /**
    536  * @return {number} The number of visits in the model.
    537  */
    538 HistoryModel.prototype.getSize = function() {
    539   return this.visits_.length;
    540 };
    541 
    542 /**
    543  * Get a list of visits between specified index positions.
    544  * @param {number} start The start index.
    545  * @param {number} end The end index.
    546  * @return {Array.<Visit>} A list of visits.
    547  */
    548 HistoryModel.prototype.getNumberedRange = function(start, end) {
    549   return this.visits_.slice(start, end);
    550 };
    551 
    552 /**
    553  * Return true if there are more results beyond the current page.
    554  * @return {boolean} true if the there are more results, otherwise false.
    555  */
    556 HistoryModel.prototype.hasMoreResults = function() {
    557   return this.haveDataForPage_(this.requestedPage_ + 1) ||
    558       !this.isQueryFinished_;
    559 };
    560 
    561 /**
    562  * Removes a list of visits from the history, and calls |callback| when the
    563  * removal has successfully completed.
    564  * @param {Array<Visit>} visits The visits to remove.
    565  * @param {Function} callback The function to call after removal succeeds.
    566  */
    567 HistoryModel.prototype.removeVisitsFromHistory = function(visits, callback) {
    568   var toBeRemoved = [];
    569   for (var i = 0; i < visits.length; i++) {
    570     toBeRemoved.push({
    571       url: visits[i].url_,
    572       timestamps: visits[i].allTimestamps
    573     });
    574   }
    575   chrome.send('removeVisits', toBeRemoved);
    576   this.deleteCompleteCallback_ = callback;
    577 };
    578 
    579 /**
    580  * Called when visits have been succesfully removed from the history.
    581  */
    582 HistoryModel.prototype.deleteComplete = function() {
    583   // Call the callback, with 'this' undefined inside the callback.
    584   this.deleteCompleteCallback_.call();
    585   this.deleteCompleteCallback_ = null;
    586 };
    587 
    588 // Getter and setter for HistoryModel.rangeInDays_.
    589 Object.defineProperty(HistoryModel.prototype, 'rangeInDays', {
    590   get: function() {
    591     return this.rangeInDays_;
    592   },
    593   set: function(range) {
    594     this.rangeInDays_ = range;
    595   }
    596 });
    597 
    598 /**
    599  * Getter and setter for HistoryModel.offset_. The offset moves the current
    600  * query 'window' |range| days behind. As such for range set to WEEK an offset
    601  * of 0 refers to the last 7 days, an offset of 1 refers to the 7 day period
    602  * that ended 7 days ago, etc. For MONTH an offset of 0 refers to the current
    603  * calendar month, 1 to the previous one, etc.
    604  */
    605 Object.defineProperty(HistoryModel.prototype, 'offset', {
    606   get: function() {
    607     return this.offset_;
    608   },
    609   set: function(offset) {
    610     this.offset_ = offset;
    611   }
    612 });
    613 
    614 // Setter for HistoryModel.requestedPage_.
    615 Object.defineProperty(HistoryModel.prototype, 'requestedPage', {
    616   set: function(page) {
    617     this.requestedPage_ = page;
    618   }
    619 });
    620 
    621 // HistoryModel, Private: -----------------------------------------------------
    622 
    623 /**
    624  * Clear the history model.
    625  * @private
    626  */
    627 HistoryModel.prototype.clearModel_ = function() {
    628   this.inFlight_ = false;  // Whether a query is inflight.
    629   this.searchText_ = '';
    630   // Whether this user is a managed user.
    631   this.isManagedProfile = loadTimeData.getBoolean('isManagedProfile');
    632   this.deletingHistoryAllowed = loadTimeData.getBoolean('allowDeletingHistory');
    633 
    634   // Only create checkboxes for editing entries if they can be used either to
    635   // delete an entry or to block/allow it.
    636   this.editingEntriesAllowed = this.deletingHistoryAllowed;
    637 
    638   // Flag to show that the results are grouped by domain or not.
    639   this.groupByDomain_ = false;
    640 
    641   this.visits_ = [];  // Date-sorted list of visits (most recent first).
    642   this.nextVisitId_ = 0;
    643   selectionAnchor = -1;
    644 
    645   // The page that the view wants to see - we only fetch slightly past this
    646   // point. If the view requests a page that we don't have data for, we try
    647   // to fetch it and call back when we're done.
    648   this.requestedPage_ = 0;
    649 
    650   // The range of history to view or search over.
    651   this.rangeInDays_ = HistoryModel.Range.ALL_TIME;
    652 
    653   // Skip |offset_| * weeks/months from the begining.
    654   this.offset_ = 0;
    655 
    656   // Keeps track of whether or not there are more results available than are
    657   // currently held in |this.visits_|.
    658   this.isQueryFinished_ = false;
    659 
    660   if (this.view_)
    661     this.view_.clear_();
    662 };
    663 
    664 /**
    665  * Figure out if we need to do more queries to fill the currently requested
    666  * page. If we think we can fill the page, call the view and let it know
    667  * we're ready to show something. This only applies to the daily time-based
    668  * view.
    669  * @private
    670  */
    671 HistoryModel.prototype.updateSearch_ = function() {
    672   var doneLoading = this.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
    673                     this.isQueryFinished_ ||
    674                     this.canFillPage_(this.requestedPage_);
    675 
    676   // Try to fetch more results if more results can arrive and the page is not
    677   // full.
    678   if (!doneLoading && !this.inFlight_)
    679     this.queryHistory_();
    680 
    681   // Show the result or a message if no results were returned.
    682   this.view_.onModelReady(doneLoading);
    683 };
    684 
    685 /**
    686  * Query for history, either for a search or time-based browsing.
    687  * @private
    688  */
    689 HistoryModel.prototype.queryHistory_ = function() {
    690   var maxResults =
    691       (this.rangeInDays_ == HistoryModel.Range.ALL_TIME) ? RESULTS_PER_PAGE : 0;
    692 
    693   // If there are already some visits, pick up the previous query where it
    694   // left off.
    695   var lastVisit = this.visits_.slice(-1)[0];
    696   var endTime = lastVisit ? lastVisit.date.getTime() : 0;
    697 
    698   $('loading-spinner').hidden = false;
    699   this.inFlight_ = true;
    700   chrome.send('queryHistory',
    701       [this.searchText_, this.offset_, this.rangeInDays_, endTime, maxResults]);
    702 };
    703 
    704 /**
    705  * Check to see if we have data for the given page.
    706  * @param {number} page The page number.
    707  * @return {boolean} Whether we have any data for the given page.
    708  * @private
    709  */
    710 HistoryModel.prototype.haveDataForPage_ = function(page) {
    711   return page * RESULTS_PER_PAGE < this.getSize();
    712 };
    713 
    714 /**
    715  * Check to see if we have data to fill the given page.
    716  * @param {number} page The page number.
    717  * @return {boolean} Whether we have data to fill the page.
    718  * @private
    719  */
    720 HistoryModel.prototype.canFillPage_ = function(page) {
    721   return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
    722 };
    723 
    724 /**
    725  * Enables or disables grouping by domain.
    726  * @param {boolean} groupByDomain New groupByDomain_ value.
    727  */
    728 HistoryModel.prototype.setGroupByDomain = function(groupByDomain) {
    729   this.groupByDomain_ = groupByDomain;
    730   this.offset_ = 0;
    731 };
    732 
    733 /**
    734  * Gets whether we are grouped by domain.
    735  * @return {boolean} Whether the results are grouped by domain.
    736  */
    737 HistoryModel.prototype.getGroupByDomain = function() {
    738   return this.groupByDomain_;
    739 };
    740 
    741 ///////////////////////////////////////////////////////////////////////////////
    742 // HistoryView:
    743 
    744 /**
    745  * Functions and state for populating the page with HTML. This should one-day
    746  * contain the view and use event handlers, rather than pushing HTML out and
    747  * getting called externally.
    748  * @param {HistoryModel} model The model backing this view.
    749  * @constructor
    750  */
    751 function HistoryView(model) {
    752   this.editButtonTd_ = $('edit-button');
    753   this.editingControlsDiv_ = $('editing-controls');
    754   this.resultDiv_ = $('results-display');
    755   this.pageDiv_ = $('results-pagination');
    756   this.model_ = model;
    757   this.pageIndex_ = 0;
    758   this.lastDisplayed_ = [];
    759 
    760   this.model_.setView(this);
    761 
    762   this.currentVisits_ = [];
    763 
    764   // If there is no search button, use the search button label as placeholder
    765   // text in the search field.
    766   if ($('search-button').offsetWidth == 0)
    767     $('search-field').placeholder = $('search-button').value;
    768 
    769   var self = this;
    770 
    771   $('clear-browsing-data').addEventListener('click', openClearBrowsingData);
    772   $('remove-selected').addEventListener('click', removeItems);
    773 
    774   // Add handlers for the page navigation buttons at the bottom.
    775   $('newest-button').addEventListener('click', function() {
    776     recordUmaAction('HistoryPage_NewestHistoryClick');
    777     self.setPage(0);
    778   });
    779   $('newer-button').addEventListener('click', function() {
    780     recordUmaAction('HistoryPage_NewerHistoryClick');
    781     self.setPage(self.pageIndex_ - 1);
    782   });
    783   $('older-button').addEventListener('click', function() {
    784     recordUmaAction('HistoryPage_OlderHistoryClick');
    785     self.setPage(self.pageIndex_ + 1);
    786   });
    787 
    788   var handleRangeChange = function(e) {
    789     // Update the results and save the last state.
    790     self.setRangeInDays(parseInt(e.target.value, 10));
    791   };
    792 
    793   // Add handlers for the range options.
    794   $('timeframe-filter-all').addEventListener('change', handleRangeChange);
    795   $('timeframe-filter-week').addEventListener('change', handleRangeChange);
    796   $('timeframe-filter-month').addEventListener('change', handleRangeChange);
    797 
    798   $('range-previous').addEventListener('click', function(e) {
    799     if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
    800       self.setPage(self.pageIndex_ + 1);
    801     else
    802       self.setOffset(self.getOffset() + 1);
    803   });
    804   $('range-next').addEventListener('click', function(e) {
    805     if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
    806       self.setPage(self.pageIndex_ - 1);
    807     else
    808       self.setOffset(self.getOffset() - 1);
    809   });
    810   $('range-today').addEventListener('click', function(e) {
    811     if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
    812       self.setPage(0);
    813     else
    814       self.setOffset(0);
    815   });
    816 }
    817 
    818 // HistoryView, public: -------------------------------------------------------
    819 /**
    820  * Do a search on a specific term.
    821  * @param {string} term The string to search for.
    822  */
    823 HistoryView.prototype.setSearch = function(term) {
    824   window.scrollTo(0, 0);
    825   this.setPageState(term, 0, this.getRangeInDays(), this.getOffset());
    826 };
    827 
    828 /**
    829  * Reload the current view.
    830  */
    831 HistoryView.prototype.reload = function() {
    832   this.model_.reload();
    833   this.updateSelectionEditButtons();
    834   this.updateRangeButtons_();
    835 };
    836 
    837 /**
    838  * Sets all the parameters for the history page and then reloads the view to
    839  * update the results.
    840  * @param {string} searchText The search string to set.
    841  * @param {number} page The page to be viewed.
    842  * @param {HistoryModel.Range} range The range to view or search over.
    843  * @param {number} offset Set the begining of the query to the specific offset.
    844  */
    845 HistoryView.prototype.setPageState = function(searchText, page, range, offset) {
    846   this.clear_();
    847   this.model_.searchText_ = searchText;
    848   this.pageIndex_ = page;
    849   this.model_.requestedPage_ = page;
    850   this.model_.rangeInDays_ = range;
    851   this.model_.groupByDomain_ = false;
    852   if (range != HistoryModel.Range.ALL_TIME)
    853     this.model_.groupByDomain_ = true;
    854   this.model_.offset_ = offset;
    855   this.reload();
    856   pageState.setUIState(this.model_.getSearchText(),
    857                        this.pageIndex_,
    858                        this.getRangeInDays(),
    859                        this.getOffset());
    860 };
    861 
    862 /**
    863  * Switch to a specified page.
    864  * @param {number} page The page we wish to view.
    865  */
    866 HistoryView.prototype.setPage = function(page) {
    867   // TODO(sergiu): Move this function to setPageState as well and see why one
    868   // of the tests fails when using setPageState.
    869   this.clear_();
    870   this.pageIndex_ = parseInt(page, 10);
    871   window.scrollTo(0, 0);
    872   this.model_.requestPage(page);
    873   pageState.setUIState(this.model_.getSearchText(),
    874                        this.pageIndex_,
    875                        this.getRangeInDays(),
    876                        this.getOffset());
    877 };
    878 
    879 /**
    880  * @return {number} The page number being viewed.
    881  */
    882 HistoryView.prototype.getPage = function() {
    883   return this.pageIndex_;
    884 };
    885 
    886 /**
    887  * Set the current range for grouped results.
    888  * @param {string} range The number of days to which the range should be set.
    889  */
    890 HistoryView.prototype.setRangeInDays = function(range) {
    891   // Set the range, offset and reset the page.
    892   this.setPageState(this.model_.getSearchText(), 0, range, 0);
    893 };
    894 
    895 /**
    896  * Get the current range in days.
    897  * @return {number} Current range in days from the model.
    898  */
    899 HistoryView.prototype.getRangeInDays = function() {
    900   return this.model_.rangeInDays;
    901 };
    902 
    903 /**
    904  * Set the current offset for grouped results.
    905  * @param {number} offset Offset to set.
    906  */
    907 HistoryView.prototype.setOffset = function(offset) {
    908   // If there is another query already in flight wait for that to complete.
    909   if (this.model_.inFlight_)
    910     return;
    911   this.setPageState(this.model_.getSearchText(),
    912                     this.pageIndex_,
    913                     this.getRangeInDays(),
    914                     offset);
    915 };
    916 
    917 /**
    918  * Get the current offset.
    919  * @return {number} Current offset from the model.
    920  */
    921 HistoryView.prototype.getOffset = function() {
    922   return this.model_.offset;
    923 };
    924 
    925 /**
    926  * Callback for the history model to let it know that it has data ready for us
    927  * to view.
    928  * @param {boolean} doneLoading Whether the current request is complete.
    929  */
    930 HistoryView.prototype.onModelReady = function(doneLoading) {
    931   this.displayResults_(doneLoading);
    932 
    933   // Allow custom styling based on whether there are any results on the page.
    934   // To make this easier, add a class to the body if there are any results.
    935   if (this.model_.visits_.length)
    936     document.body.classList.add('has-results');
    937   else
    938     document.body.classList.remove('has-results');
    939 
    940   this.updateNavBar_();
    941 
    942   if (isMobileVersion()) {
    943     // Hide the search field if it is empty and there are no results.
    944     var hasResults = this.model_.visits_.length > 0;
    945     var isSearch = this.model_.getSearchText().length > 0;
    946     $('search-field').hidden = !(hasResults || isSearch);
    947   }
    948 };
    949 
    950 /**
    951  * Enables or disables the buttons that control editing entries depending on
    952  * whether there are any checked boxes.
    953  */
    954 HistoryView.prototype.updateSelectionEditButtons = function() {
    955   if (loadTimeData.getBoolean('allowDeletingHistory')) {
    956     var anyChecked = document.querySelector('.entry input:checked') != null;
    957     $('remove-selected').disabled = !anyChecked;
    958   } else {
    959     $('remove-selected').disabled = true;
    960   }
    961 };
    962 
    963 /**
    964  * Shows the notification bar at the top of the page with |innerHTML| as its
    965  * content.
    966  * @param {string} innerHTML The HTML content of the warning.
    967  * @param {boolean} isWarning If true, style the notification as a warning.
    968  */
    969 HistoryView.prototype.showNotification = function(innerHTML, isWarning) {
    970   var bar = $('notification-bar');
    971   bar.innerHTML = innerHTML;
    972   bar.hidden = false;
    973   if (isWarning)
    974     bar.classList.add('warning');
    975   else
    976     bar.classList.remove('warning');
    977 
    978   // Make sure that any links in the HTML are targeting the top level.
    979   var links = bar.querySelectorAll('a');
    980   for (var i = 0; i < links.length; i++)
    981     links[i].target = '_top';
    982 
    983   this.positionNotificationBar();
    984 };
    985 
    986 /**
    987  * Adjusts the position of the notification bar based on the size of the page.
    988  */
    989 HistoryView.prototype.positionNotificationBar = function() {
    990   var bar = $('notification-bar');
    991 
    992   // If the bar does not fit beside the editing controls, put it into the
    993   // overflow state.
    994   if (bar.getBoundingClientRect().top >=
    995       $('editing-controls').getBoundingClientRect().bottom) {
    996     bar.classList.add('alone');
    997   } else {
    998     bar.classList.remove('alone');
    999   }
   1000 };
   1001 
   1002 // HistoryView, private: ------------------------------------------------------
   1003 
   1004 /**
   1005  * Clear the results in the view.  Since we add results piecemeal, we need
   1006  * to clear them out when we switch to a new page or reload.
   1007  * @private
   1008  */
   1009 HistoryView.prototype.clear_ = function() {
   1010   this.resultDiv_.textContent = '';
   1011 
   1012   this.currentVisits_.forEach(function(visit) {
   1013     visit.isRendered = false;
   1014   });
   1015   this.currentVisits_ = [];
   1016 
   1017   document.body.classList.remove('has-results');
   1018 };
   1019 
   1020 /**
   1021  * Record that the given visit has been rendered.
   1022  * @param {Visit} visit The visit that was rendered.
   1023  * @private
   1024  */
   1025 HistoryView.prototype.setVisitRendered_ = function(visit) {
   1026   visit.isRendered = true;
   1027   this.currentVisits_.push(visit);
   1028 };
   1029 
   1030 /**
   1031  * Generates and adds the grouped visits DOM for a certain domain. This
   1032  * includes the clickable arrow and domain name and the visit entries for
   1033  * that domain.
   1034  * @param {Element} results DOM object to which to add the elements.
   1035  * @param {string} domain Current domain name.
   1036  * @param {Array} domainVisits Array of visits for this domain.
   1037  * @private
   1038  */
   1039 HistoryView.prototype.getGroupedVisitsDOM_ = function(
   1040     results, domain, domainVisits) {
   1041   // Add a new domain entry.
   1042   var siteResults = results.appendChild(
   1043       createElementWithClassName('li', 'site-entry'));
   1044 
   1045   // Make a wrapper that will contain the arrow, the favicon and the domain.
   1046   var siteDomainWrapper = siteResults.appendChild(
   1047       createElementWithClassName('div', 'site-domain-wrapper'));
   1048 
   1049   if (this.model_.editingEntriesAllowed) {
   1050     var siteDomainCheckbox =
   1051         createElementWithClassName('input', 'domain-checkbox');
   1052 
   1053     siteDomainCheckbox.type = 'checkbox';
   1054     siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
   1055     siteDomainCheckbox.domain_ = domain;
   1056 
   1057     siteDomainWrapper.appendChild(siteDomainCheckbox);
   1058   }
   1059 
   1060   var siteArrow = siteDomainWrapper.appendChild(
   1061       createElementWithClassName('div', 'site-domain-arrow collapse'));
   1062   var siteDomain = siteDomainWrapper.appendChild(
   1063       createElementWithClassName('div', 'site-domain'));
   1064   var siteDomainLink = siteDomain.appendChild(
   1065       createElementWithClassName('button', 'link-button'));
   1066   siteDomainLink.addEventListener('click', function(e) { e.preventDefault(); });
   1067   siteDomainLink.textContent = domain;
   1068   var numberOfVisits = createElementWithClassName('span', 'number-visits');
   1069   var domainElement = document.createElement('span');
   1070 
   1071   numberOfVisits.textContent = loadTimeData.getStringF('numberVisits',
   1072                                                        domainVisits.length);
   1073   siteDomain.appendChild(numberOfVisits);
   1074 
   1075   domainVisits[0].addFaviconToElement_(siteDomain);
   1076 
   1077   siteDomainWrapper.addEventListener('click', toggleHandler);
   1078 
   1079   if (this.model_.isManagedProfile) {
   1080     siteDomainWrapper.appendChild(
   1081         getManagedStatusDOM(domainVisits[0].hostFilteringBehavior));
   1082   }
   1083 
   1084   siteResults.appendChild(siteDomainWrapper);
   1085   var resultsList = siteResults.appendChild(
   1086       createElementWithClassName('ol', 'site-results'));
   1087 
   1088   // Collapse until it gets toggled.
   1089   resultsList.style.height = 0;
   1090 
   1091   // Add the results for each of the domain.
   1092   var isMonthGroupedResult = this.getRangeInDays() == HistoryModel.Range.MONTH;
   1093   for (var j = 0, visit; visit = domainVisits[j]; j++) {
   1094     resultsList.appendChild(visit.getResultDOM({
   1095       useMonthDate: isMonthGroupedResult
   1096     }));
   1097     this.setVisitRendered_(visit);
   1098   }
   1099 };
   1100 
   1101 /**
   1102  * Enables or disables the time range buttons.
   1103  * @private
   1104  */
   1105 HistoryView.prototype.updateRangeButtons_ = function() {
   1106   // The enabled state for the previous, today and next buttons.
   1107   var previousState = false;
   1108   var todayState = false;
   1109   var nextState = false;
   1110   var usePage = (this.getRangeInDays() == HistoryModel.Range.ALL_TIME);
   1111 
   1112   // Use pagination for most recent visits, offset otherwise.
   1113   // TODO(sergiu): Maybe send just one variable in the future.
   1114   if (usePage) {
   1115     if (this.getPage() != 0) {
   1116       nextState = true;
   1117       todayState = true;
   1118     }
   1119     previousState = this.model_.hasMoreResults();
   1120   } else {
   1121     if (this.getOffset() != 0) {
   1122       nextState = true;
   1123       todayState = true;
   1124     }
   1125     previousState = !this.model_.isQueryFinished_;
   1126   }
   1127 
   1128   $('range-previous').disabled = !previousState;
   1129   $('range-today').disabled = !todayState;
   1130   $('range-next').disabled = !nextState;
   1131 };
   1132 
   1133 /**
   1134  * Groups visits by domain, sorting them by the number of visits.
   1135  * @param {Array} visits Visits received from the query results.
   1136  * @param {Element} results Object where the results are added to.
   1137  * @private
   1138  */
   1139 HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) {
   1140   var visitsByDomain = {};
   1141   var domains = [];
   1142 
   1143   // Group the visits into a dictionary and generate a list of domains.
   1144   for (var i = 0, visit; visit = visits[i]; i++) {
   1145     var domain = visit.getDomainFromURL_(visit.url_);
   1146     if (!visitsByDomain[domain]) {
   1147       visitsByDomain[domain] = [];
   1148       domains.push(domain);
   1149     }
   1150     visitsByDomain[domain].push(visit);
   1151   }
   1152   var sortByVisits = function(a, b) {
   1153     return visitsByDomain[b].length - visitsByDomain[a].length;
   1154   };
   1155   domains.sort(sortByVisits);
   1156 
   1157   for (var i = 0, domain; domain = domains[i]; i++)
   1158     this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]);
   1159 };
   1160 
   1161 /**
   1162  * Adds the results for a month.
   1163  * @param {Array} visits Visits returned by the query.
   1164  * @param {Element} parentElement Element to which to add the results to.
   1165  * @private
   1166  */
   1167 HistoryView.prototype.addMonthResults_ = function(visits, parentElement) {
   1168   if (visits.length == 0)
   1169     return;
   1170 
   1171   var monthResults = parentElement.appendChild(
   1172       createElementWithClassName('ol', 'month-results'));
   1173   // Don't add checkboxes if entries can not be edited.
   1174   if (!this.model_.editingEntriesAllowed)
   1175     monthResults.classList.add('no-checkboxes');
   1176 
   1177   this.groupVisitsByDomain_(visits, monthResults);
   1178 };
   1179 
   1180 /**
   1181  * Adds the results for a certain day. This includes a title with the day of
   1182  * the results and the results themselves, grouped or not.
   1183  * @param {Array} visits Visits returned by the query.
   1184  * @param {Element} parentElement Element to which to add the results to.
   1185  * @private
   1186  */
   1187 HistoryView.prototype.addDayResults_ = function(visits, parentElement) {
   1188   if (visits.length == 0)
   1189     return;
   1190 
   1191   var firstVisit = visits[0];
   1192   var day = parentElement.appendChild(createElementWithClassName('h3', 'day'));
   1193   day.appendChild(document.createTextNode(firstVisit.dateRelativeDay));
   1194   if (firstVisit.continued) {
   1195     day.appendChild(document.createTextNode(' ' +
   1196                                             loadTimeData.getString('cont')));
   1197   }
   1198   var dayResults = parentElement.appendChild(
   1199       createElementWithClassName('ol', 'day-results'));
   1200 
   1201   // Don't add checkboxes if entries can not be edited.
   1202   if (!this.model_.editingEntriesAllowed)
   1203     dayResults.classList.add('no-checkboxes');
   1204 
   1205   if (this.model_.getGroupByDomain()) {
   1206     this.groupVisitsByDomain_(visits, dayResults);
   1207   } else {
   1208     var lastTime;
   1209 
   1210     for (var i = 0, visit; visit = visits[i]; i++) {
   1211       // If enough time has passed between visits, indicate a gap in browsing.
   1212       var thisTime = visit.date.getTime();
   1213       if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME)
   1214         dayResults.appendChild(createElementWithClassName('li', 'gap'));
   1215 
   1216       // Insert the visit into the DOM.
   1217       dayResults.appendChild(visit.getResultDOM({ addTitleFavicon: true }));
   1218       this.setVisitRendered_(visit);
   1219 
   1220       lastTime = thisTime;
   1221     }
   1222   }
   1223 };
   1224 
   1225 /**
   1226  * Adds the text that shows the current interval, used for week and month
   1227  * results.
   1228  * @param {Element} resultsFragment The element to which the interval will be
   1229  *     added to.
   1230  * @private
   1231  */
   1232 HistoryView.prototype.addTimeframeInterval_ = function(resultsFragment) {
   1233   if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME)
   1234     return;
   1235 
   1236   // If this is a time range result add some text that shows what is the
   1237   // time range for the results the user is viewing.
   1238   var timeFrame = resultsFragment.appendChild(
   1239       createElementWithClassName('h2', 'timeframe'));
   1240   // TODO(sergiu): Figure the best way to show this for the first day of
   1241   // the month.
   1242   timeFrame.appendChild(document.createTextNode(loadTimeData.getStringF(
   1243       'historyInterval',
   1244       this.model_.queryStartTime,
   1245       this.model_.queryEndTime)));
   1246 };
   1247 
   1248 /**
   1249  * Update the page with results.
   1250  * @param {boolean} doneLoading Whether the current request is complete.
   1251  * @private
   1252  */
   1253 HistoryView.prototype.displayResults_ = function(doneLoading) {
   1254   // Either show a page of results received for the all time results or all the
   1255   // received results for the weekly and monthly view.
   1256   var results = this.model_.visits_;
   1257   if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME) {
   1258     var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
   1259     var rangeEnd = rangeStart + RESULTS_PER_PAGE;
   1260     results = this.model_.getNumberedRange(rangeStart, rangeEnd);
   1261   }
   1262   var searchText = this.model_.getSearchText();
   1263   var groupByDomain = this.model_.getGroupByDomain();
   1264 
   1265   if (searchText) {
   1266     // Add a header for the search results, if there isn't already one.
   1267     if (!this.resultDiv_.querySelector('h3')) {
   1268       var header = document.createElement('h3');
   1269       header.textContent = loadTimeData.getStringF('searchResultsFor',
   1270                                                    searchText);
   1271       this.resultDiv_.appendChild(header);
   1272     }
   1273 
   1274     this.addTimeframeInterval_(this.resultDiv_);
   1275 
   1276     var searchResults = createElementWithClassName('ol', 'search-results');
   1277 
   1278     // Don't add checkboxes if entries can not be edited.
   1279     if (!this.model_.editingEntriesAllowed)
   1280       searchResults.classList.add('no-checkboxes');
   1281 
   1282     if (results.length == 0 && doneLoading) {
   1283       var noSearchResults = searchResults.appendChild(
   1284           createElementWithClassName('div', 'no-results-message'));
   1285       noSearchResults.textContent = loadTimeData.getString('noSearchResults');
   1286     } else {
   1287       for (var i = 0, visit; visit = results[i]; i++) {
   1288         if (!visit.isRendered) {
   1289           searchResults.appendChild(visit.getResultDOM({
   1290             isSearchResult: true,
   1291             addTitleFavicon: true
   1292           }));
   1293           this.setVisitRendered_(visit);
   1294         }
   1295       }
   1296     }
   1297     this.resultDiv_.appendChild(searchResults);
   1298   } else {
   1299     var resultsFragment = document.createDocumentFragment();
   1300 
   1301     this.addTimeframeInterval_(resultsFragment);
   1302 
   1303     if (results.length == 0 && doneLoading) {
   1304       var noResults = resultsFragment.appendChild(
   1305           createElementWithClassName('div', 'no-results-message'));
   1306       noResults.textContent = loadTimeData.getString('noResults');
   1307       this.resultDiv_.appendChild(resultsFragment);
   1308       return;
   1309     }
   1310 
   1311     if (this.getRangeInDays() == HistoryModel.Range.MONTH &&
   1312         groupByDomain) {
   1313       // Group everything together in the month view.
   1314       this.addMonthResults_(results, resultsFragment);
   1315     } else {
   1316       var dayStart = 0;
   1317       var dayEnd = 0;
   1318       // Go through all of the visits and process them in chunks of one day.
   1319       while (dayEnd < results.length) {
   1320         // Skip over the ones that are already rendered.
   1321         while (dayStart < results.length && results[dayStart].isRendered)
   1322           ++dayStart;
   1323         var dayEnd = dayStart + 1;
   1324         while (dayEnd < results.length && results[dayEnd].continued)
   1325           ++dayEnd;
   1326 
   1327         this.addDayResults_(
   1328             results.slice(dayStart, dayEnd), resultsFragment, groupByDomain);
   1329       }
   1330     }
   1331 
   1332     // Add all the days and their visits to the page.
   1333     this.resultDiv_.appendChild(resultsFragment);
   1334   }
   1335 };
   1336 
   1337 /**
   1338  * Update the visibility of the page navigation buttons.
   1339  * @private
   1340  */
   1341 HistoryView.prototype.updateNavBar_ = function() {
   1342   this.updateRangeButtons_();
   1343 
   1344   // Managed users have the control bar on top, don't show it on the bottom
   1345   // as well.
   1346   if (!loadTimeData.getBoolean('isManagedProfile')) {
   1347     $('newest-button').hidden = this.pageIndex_ == 0;
   1348     $('newer-button').hidden = this.pageIndex_ == 0;
   1349     $('older-button').hidden =
   1350         this.model_.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
   1351         !this.model_.hasMoreResults();
   1352   }
   1353 };
   1354 
   1355 /**
   1356  * Updates the visibility of the 'Clear browsing data' button.
   1357  * Only used on mobile platforms.
   1358  * @private
   1359  */
   1360 HistoryView.prototype.updateClearBrowsingDataButton_ = function() {
   1361   // Ideally, we should hide the 'Clear browsing data' button whenever the
   1362   // soft keyboard is visible. This is not possible, so instead, hide the
   1363   // button whenever the search field has focus.
   1364   $('clear-browsing-data').hidden =
   1365       (document.activeElement === $('search-field'));
   1366 };
   1367 
   1368 ///////////////////////////////////////////////////////////////////////////////
   1369 // State object:
   1370 /**
   1371  * An 'AJAX-history' implementation.
   1372  * @param {HistoryModel} model The model we're representing.
   1373  * @param {HistoryView} view The view we're representing.
   1374  * @constructor
   1375  */
   1376 function PageState(model, view) {
   1377   // Enforce a singleton.
   1378   if (PageState.instance) {
   1379     return PageState.instance;
   1380   }
   1381 
   1382   this.model = model;
   1383   this.view = view;
   1384 
   1385   if (typeof this.checker_ != 'undefined' && this.checker_) {
   1386     clearInterval(this.checker_);
   1387   }
   1388 
   1389   // TODO(glen): Replace this with a bound method so we don't need
   1390   //     public model and view.
   1391   this.checker_ = window.setInterval(function(stateObj) {
   1392     var hashData = stateObj.getHashData();
   1393     var page = parseInt(hashData.page, 10);
   1394     var range = parseInt(hashData.range, 10);
   1395     var offset = parseInt(hashData.offset, 10);
   1396     if (hashData.q != stateObj.model.getSearchText() ||
   1397         page != stateObj.view.getPage() ||
   1398         range != stateObj.model.rangeInDays ||
   1399         offset != stateObj.model.offset) {
   1400       stateObj.view.setPageState(hashData.q, page, range, offset);
   1401     }
   1402   }, 50, this);
   1403 }
   1404 
   1405 /**
   1406  * Holds the singleton instance.
   1407  */
   1408 PageState.instance = null;
   1409 
   1410 /**
   1411  * @return {Object} An object containing parameters from our window hash.
   1412  */
   1413 PageState.prototype.getHashData = function() {
   1414   var result = {
   1415     q: '',
   1416     page: 0,
   1417     grouped: false,
   1418     range: 0,
   1419     offset: 0
   1420   };
   1421 
   1422   if (!window.location.hash)
   1423     return result;
   1424 
   1425   var hashSplit = window.location.hash.substr(1).split('&');
   1426   for (var i = 0; i < hashSplit.length; i++) {
   1427     var pair = hashSplit[i].split('=');
   1428     if (pair.length > 1) {
   1429       result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
   1430     }
   1431   }
   1432 
   1433   return result;
   1434 };
   1435 
   1436 /**
   1437  * Set the hash to a specified state, this will create an entry in the
   1438  * session history so the back button cycles through hash states, which
   1439  * are then picked up by our listener.
   1440  * @param {string} term The current search string.
   1441  * @param {number} page The page currently being viewed.
   1442  * @param {HistoryModel.Range} range The range to view or search over.
   1443  * @param {number} offset Set the begining of the query to the specific offset.
   1444  */
   1445 PageState.prototype.setUIState = function(term, page, range, offset) {
   1446   // Make sure the form looks pretty.
   1447   $('search-field').value = term;
   1448   var hash = this.getHashData();
   1449   if (hash.q != term || hash.page != page || hash.range != range ||
   1450       hash.offset != offset) {
   1451     window.location.hash = PageState.getHashString(term, page, range, offset);
   1452   }
   1453 };
   1454 
   1455 /**
   1456  * Static method to get the hash string for a specified state
   1457  * @param {string} term The current search string.
   1458  * @param {number} page The page currently being viewed.
   1459  * @param {HistoryModel.Range} range The range to view or search over.
   1460  * @param {number} offset Set the begining of the query to the specific offset.
   1461  * @return {string} The string to be used in a hash.
   1462  */
   1463 PageState.getHashString = function(term, page, range, offset) {
   1464   // Omit elements that are empty.
   1465   var newHash = [];
   1466 
   1467   if (term)
   1468     newHash.push('q=' + encodeURIComponent(term));
   1469 
   1470   if (page)
   1471     newHash.push('page=' + page);
   1472 
   1473   if (range)
   1474     newHash.push('range=' + range);
   1475 
   1476   if (offset)
   1477     newHash.push('offset=' + offset);
   1478 
   1479   return newHash.join('&');
   1480 };
   1481 
   1482 ///////////////////////////////////////////////////////////////////////////////
   1483 // Document Functions:
   1484 /**
   1485  * Window onload handler, sets up the page.
   1486  */
   1487 function load() {
   1488   uber.onContentFrameLoaded();
   1489 
   1490   var searchField = $('search-field');
   1491 
   1492   historyModel = new HistoryModel();
   1493   historyView = new HistoryView(historyModel);
   1494   pageState = new PageState(historyModel, historyView);
   1495 
   1496   // Create default view.
   1497   var hashData = pageState.getHashData();
   1498   var grouped = (hashData.grouped == 'true') || historyModel.getGroupByDomain();
   1499   var page = parseInt(hashData.page, 10) || historyView.getPage();
   1500   var range = parseInt(hashData.range, 10) || historyView.getRangeInDays();
   1501   var offset = parseInt(hashData.offset, 10) || historyView.getOffset();
   1502   historyView.setPageState(hashData.q, page, range, offset);
   1503 
   1504   if ($('overlay')) {
   1505     cr.ui.overlay.setupOverlay($('overlay'));
   1506     cr.ui.overlay.globalInitialization();
   1507   }
   1508   HistoryFocusManager.getInstance().initialize();
   1509 
   1510   var doSearch = function(e) {
   1511     recordUmaAction('HistoryPage_Search');
   1512     historyView.setSearch(searchField.value);
   1513 
   1514     if (isMobileVersion())
   1515       searchField.blur();  // Dismiss the keyboard.
   1516   };
   1517 
   1518   var mayRemoveVisits = loadTimeData.getBoolean('allowDeletingHistory');
   1519   $('remove-visit').disabled = !mayRemoveVisits;
   1520 
   1521   if (mayRemoveVisits) {
   1522     $('remove-visit').addEventListener('activate', function(e) {
   1523       activeVisit.removeFromHistory();
   1524       activeVisit = null;
   1525     });
   1526   }
   1527 
   1528   if (!loadTimeData.getBoolean('showDeleteVisitUI'))
   1529     $('remove-visit').hidden = true;
   1530 
   1531   searchField.addEventListener('search', doSearch);
   1532   $('search-button').addEventListener('click', doSearch);
   1533 
   1534   $('more-from-site').addEventListener('activate', function(e) {
   1535     activeVisit.showMoreFromSite_();
   1536     activeVisit = null;
   1537   });
   1538 
   1539   // Only show the controls if the command line switch is activated.
   1540   if (loadTimeData.getBoolean('groupByDomain') ||
   1541       loadTimeData.getBoolean('isManagedProfile')) {
   1542     // Hide the top container which has the "Clear browsing data" and "Remove
   1543     // selected entries" buttons since they're unavailable in managed mode
   1544     $('top-container').hidden = true;
   1545     $('history-page').classList.add('big-topbar-page');
   1546     $('filter-controls').hidden = false;
   1547   }
   1548 
   1549   var title = loadTimeData.getString('title');
   1550   uber.invokeMethodOnParent('setTitle', {title: title});
   1551 
   1552   // Adjust the position of the notification bar when the window size changes.
   1553   window.addEventListener('resize',
   1554       historyView.positionNotificationBar.bind(historyView));
   1555 
   1556   cr.ui.FocusManager.disableMouseFocusOnButtons();
   1557 
   1558   if (isMobileVersion()) {
   1559     if (searchField) {
   1560       // Move the search box out of the header.
   1561       var resultsDisplay = $('results-display');
   1562       resultsDisplay.parentNode.insertBefore($('search-field'), resultsDisplay);
   1563 
   1564       window.addEventListener(
   1565           'resize', historyView.updateClearBrowsingDataButton_);
   1566 
   1567       // When the search field loses focus, add a delay before updating the
   1568       // visibility, otherwise the button will flash on the screen before the
   1569       // keyboard animates away.
   1570       searchField.addEventListener('blur', function() {
   1571         setTimeout(historyView.updateClearBrowsingDataButton_, 250);
   1572       });
   1573     }
   1574 
   1575     // Move the button to the bottom of the body.
   1576     document.body.appendChild($('clear-browsing-data'));
   1577   } else {
   1578     window.addEventListener('message', function(e) {
   1579       if (e.data.method == 'frameSelected')
   1580         searchField.focus();
   1581     });
   1582     searchField.focus();
   1583   }
   1584 }
   1585 
   1586 /**
   1587  * Updates the managed filter status labels of a host/URL entry to the current
   1588  * value.
   1589  * @param {Element} statusElement The div which contains the status labels.
   1590  * @param {ManagedModeFilteringBehavior} newStatus The filter status of the
   1591  *     current domain/URL.
   1592  */
   1593 function updateHostStatus(statusElement, newStatus) {
   1594   var filteringBehaviorDiv =
   1595       statusElement.querySelector('.filtering-behavior');
   1596   // Reset to the base class first, then add modifier classes if needed.
   1597   filteringBehaviorDiv.className = 'filtering-behavior';
   1598   if (newStatus == ManagedModeFilteringBehavior.BLOCK) {
   1599     filteringBehaviorDiv.textContent =
   1600         loadTimeData.getString('filterBlocked');
   1601     filteringBehaviorDiv.classList.add('filter-blocked');
   1602   } else {
   1603     filteringBehaviorDiv.textContent = '';
   1604   }
   1605 }
   1606 
   1607 /**
   1608  * Click handler for the 'Clear browsing data' dialog.
   1609  * @param {Event} e The click event.
   1610  */
   1611 function openClearBrowsingData(e) {
   1612   recordUmaAction('HistoryPage_InitClearBrowsingData');
   1613   chrome.send('clearBrowsingData');
   1614 }
   1615 
   1616 /**
   1617  * Shows the dialog for the user to confirm removal of selected history entries.
   1618  */
   1619 function showConfirmationOverlay() {
   1620   $('alertOverlay').classList.add('showing');
   1621   $('overlay').hidden = false;
   1622   uber.invokeMethodOnParent('beginInterceptingEvents');
   1623 }
   1624 
   1625 /**
   1626  * Hides the confirmation overlay used to confirm selected history entries.
   1627  */
   1628 function hideConfirmationOverlay() {
   1629   $('alertOverlay').classList.remove('showing');
   1630   $('overlay').hidden = true;
   1631   uber.invokeMethodOnParent('stopInterceptingEvents');
   1632 }
   1633 
   1634 /**
   1635  * Shows the confirmation alert for history deletions and permits browser tests
   1636  * to override the dialog.
   1637  * @param {function=} okCallback A function to be called when the user presses
   1638  *     the ok button.
   1639  * @param {function=} cancelCallback A function to be called when the user
   1640  *     presses the cancel button.
   1641  */
   1642 function confirmDeletion(okCallback, cancelCallback) {
   1643   alertOverlay.setValues(
   1644       loadTimeData.getString('removeSelected'),
   1645       loadTimeData.getString('deleteWarning'),
   1646       loadTimeData.getString('cancel'),
   1647       loadTimeData.getString('deleteConfirm'),
   1648       cancelCallback,
   1649       okCallback);
   1650   showConfirmationOverlay();
   1651 }
   1652 
   1653 /**
   1654  * Click handler for the 'Remove selected items' button.
   1655  * Confirms the deletion with the user, and then deletes the selected visits.
   1656  */
   1657 function removeItems() {
   1658   recordUmaAction('HistoryPage_RemoveSelected');
   1659   if (!loadTimeData.getBoolean('allowDeletingHistory'))
   1660     return;
   1661 
   1662   var checked = $('results-display').querySelectorAll(
   1663       '.entry-box input[type=checkbox]:checked:not([disabled])');
   1664   var disabledItems = [];
   1665   var toBeRemoved = [];
   1666 
   1667   for (var i = 0; i < checked.length; i++) {
   1668     var checkbox = checked[i];
   1669     var entry = findAncestorByClass(checkbox, 'entry');
   1670     toBeRemoved.push(entry.visit);
   1671 
   1672     // Disable the checkbox and put a strikethrough style on the link, so the
   1673     // user can see what will be deleted.
   1674     var link = entry.querySelector('a');
   1675     checkbox.disabled = true;
   1676     link.classList.add('to-be-removed');
   1677     disabledItems.push(checkbox);
   1678     var integerId = parseInt(entry.visit.id_, 10);
   1679     // Record the ID of the entry to signify how many entries are above this
   1680     // link on the page.
   1681     recordUmaHistogram('HistoryPage.RemoveEntryPosition',
   1682                        UMA_MAX_BUCKET_VALUE,
   1683                        integerId);
   1684     if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
   1685       recordUmaHistogram('HistoryPage.RemoveEntryPositionSubset',
   1686                          UMA_MAX_SUBSET_BUCKET_VALUE,
   1687                          integerId);
   1688     }
   1689     if (entry.parentNode.className == 'search-results')
   1690       recordUmaAction('HistoryPage_SearchResultRemove');
   1691   }
   1692 
   1693   function onConfirmRemove() {
   1694     recordUmaAction('HistoryPage_ConfirmRemoveSelected');
   1695     historyModel.removeVisitsFromHistory(toBeRemoved,
   1696         historyView.reload.bind(historyView));
   1697     $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
   1698     hideConfirmationOverlay();
   1699   }
   1700 
   1701   function onCancelRemove() {
   1702     recordUmaAction('HistoryPage_CancelRemoveSelected');
   1703     // Return everything to its previous state.
   1704     for (var i = 0; i < disabledItems.length; i++) {
   1705       var checkbox = disabledItems[i];
   1706       checkbox.disabled = false;
   1707 
   1708       var entryBox = findAncestorByClass(checkbox, 'entry-box');
   1709       entryBox.querySelector('a').classList.remove('to-be-removed');
   1710     }
   1711     $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
   1712     hideConfirmationOverlay();
   1713   }
   1714 
   1715   if (checked.length) {
   1716     confirmDeletion(onConfirmRemove, onCancelRemove);
   1717     $('overlay').addEventListener('cancelOverlay', onCancelRemove);
   1718   }
   1719 }
   1720 
   1721 /**
   1722  * Handler for the 'click' event on a checkbox.
   1723  * @param {Event} e The click event.
   1724  */
   1725 function checkboxClicked(e) {
   1726   handleCheckboxStateChange(e.currentTarget, e.shiftKey);
   1727 }
   1728 
   1729 /**
   1730  * Post-process of checkbox state change. This handles range selection and
   1731  * updates internal state.
   1732  * @param {!HTMLInputElement} checkbox Clicked checkbox.
   1733  * @param {boolean} shiftKey true if shift key is pressed.
   1734  */
   1735 function handleCheckboxStateChange(checkbox, shiftKey) {
   1736   updateParentCheckbox(checkbox);
   1737   var id = Number(checkbox.id.slice('checkbox-'.length));
   1738   // Handle multi-select if shift was pressed.
   1739   if (shiftKey && (selectionAnchor != -1)) {
   1740     var checked = checkbox.checked;
   1741     // Set all checkboxes from the anchor up to the clicked checkbox to the
   1742     // state of the clicked one.
   1743     var begin = Math.min(id, selectionAnchor);
   1744     var end = Math.max(id, selectionAnchor);
   1745     for (var i = begin; i <= end; i++) {
   1746       var checkbox = document.querySelector('#checkbox-' + i);
   1747       if (checkbox) {
   1748         checkbox.checked = checked;
   1749         updateParentCheckbox(checkbox);
   1750       }
   1751     }
   1752   }
   1753   selectionAnchor = id;
   1754 
   1755   historyView.updateSelectionEditButtons();
   1756 }
   1757 
   1758 /**
   1759  * Handler for the 'click' event on a domain checkbox. Checkes or unchecks the
   1760  * checkboxes of the visits to this domain in the respective group.
   1761  * @param {Event} e The click event.
   1762  */
   1763 function domainCheckboxClicked(e) {
   1764   var siteEntry = findAncestorByClass(e.currentTarget, 'site-entry');
   1765   var checkboxes =
   1766       siteEntry.querySelectorAll('.site-results input[type=checkbox]');
   1767   for (var i = 0; i < checkboxes.length; i++)
   1768     checkboxes[i].checked = e.currentTarget.checked;
   1769   historyView.updateSelectionEditButtons();
   1770   // Stop propagation as clicking the checkbox would otherwise trigger the
   1771   // group to collapse/expand.
   1772   e.stopPropagation();
   1773 }
   1774 
   1775 /**
   1776  * Updates the domain checkbox for this visit checkbox if it has been
   1777  * unchecked.
   1778  * @param {Element} checkbox The checkbox that has been clicked.
   1779  */
   1780 function updateParentCheckbox(checkbox) {
   1781   if (checkbox.checked)
   1782     return;
   1783 
   1784   var entry = findAncestorByClass(checkbox, 'site-entry');
   1785   if (!entry)
   1786     return;
   1787 
   1788   var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
   1789   if (groupCheckbox)
   1790       groupCheckbox.checked = false;
   1791 }
   1792 
   1793 function entryBoxMousedown(event) {
   1794   // Prevent text selection when shift-clicking to select multiple entries.
   1795   if (event.shiftKey)
   1796     event.preventDefault();
   1797 }
   1798 
   1799 /**
   1800  * Handle click event for entryBox labels.
   1801  * @param {!MouseEvent} event A click event.
   1802  */
   1803 function entryBoxClick(event) {
   1804   // Do nothing if a bookmark star is clicked.
   1805   if (event.defaultPrevented)
   1806     return;
   1807   var element = event.target;
   1808   // Do nothing if the event happened in an interactive element.
   1809   for (; element != event.currentTarget; element = element.parentNode) {
   1810     switch (element.tagName) {
   1811       case 'A':
   1812       case 'BUTTON':
   1813       case 'INPUT':
   1814         return;
   1815     }
   1816   }
   1817   var checkbox = event.currentTarget.control;
   1818   checkbox.checked = !checkbox.checked;
   1819   handleCheckboxStateChange(checkbox, event.shiftKey);
   1820   // We don't want to focus on the checkbox.
   1821   event.preventDefault();
   1822 }
   1823 
   1824 // This is pulled out so we can wait for it in tests.
   1825 function removeNodeWithoutTransition(node) {
   1826   node.parentNode.removeChild(node);
   1827 }
   1828 
   1829 /**
   1830  * Triggers a fade-out animation, and then removes |node| from the DOM.
   1831  * @param {Node} node The node to be removed.
   1832  * @param {Function?} onRemove A function to be called after the node
   1833  *     has been removed from the DOM.
   1834  */
   1835 function removeNode(node, onRemove) {
   1836   node.classList.add('fade-out'); // Trigger CSS fade out animation.
   1837 
   1838   // Delete the node when the animation is complete.
   1839   node.addEventListener('webkitTransitionEnd', function() {
   1840     removeNodeWithoutTransition(node);
   1841     if (onRemove)
   1842       onRemove();
   1843   });
   1844 }
   1845 
   1846 /**
   1847  * Removes a single entry from the view. Also removes gaps before and after
   1848  * entry if necessary.
   1849  * @param {Node} entry The DOM node representing the entry to be removed.
   1850  */
   1851 function removeEntryFromView(entry) {
   1852   var nextEntry = entry.nextSibling;
   1853   var previousEntry = entry.previousSibling;
   1854 
   1855   removeNode(entry, function() {
   1856     historyView.updateSelectionEditButtons();
   1857   });
   1858 
   1859   // if there is no previous entry, and the next entry is a gap, remove it
   1860   if (!previousEntry && nextEntry && nextEntry.className == 'gap')
   1861     removeNode(nextEntry);
   1862 
   1863   // if there is no next entry, and the previous entry is a gap, remove it
   1864   if (!nextEntry && previousEntry && previousEntry.className == 'gap')
   1865     removeNode(previousEntry);
   1866 
   1867   // if both the next and previous entries are gaps, remove one
   1868   if (nextEntry && nextEntry.className == 'gap' &&
   1869       previousEntry && previousEntry.className == 'gap') {
   1870     removeNode(nextEntry);
   1871   }
   1872 }
   1873 
   1874 /**
   1875  * Toggles an element in the grouped history.
   1876  * @param {Element} e The element which was clicked on.
   1877  */
   1878 function toggleHandler(e) {
   1879   var innerResultList = e.currentTarget.parentElement.querySelector(
   1880       '.site-results');
   1881   var innerArrow = e.currentTarget.parentElement.querySelector(
   1882       '.site-domain-arrow');
   1883   if (innerArrow.classList.contains('collapse')) {
   1884     innerResultList.style.height = 'auto';
   1885     // -webkit-transition does not work on height:auto elements so first set
   1886     // the height to auto so that it is computed and then set it to the
   1887     // computed value in pixels so the transition works properly.
   1888     var height = innerResultList.clientHeight;
   1889     innerResultList.style.height = height + 'px';
   1890     innerArrow.classList.remove('collapse');
   1891     innerArrow.classList.add('expand');
   1892   } else {
   1893     innerResultList.style.height = 0;
   1894     innerArrow.classList.remove('expand');
   1895     innerArrow.classList.add('collapse');
   1896   }
   1897 }
   1898 
   1899 /**
   1900  * Builds the DOM elements to show the managed status of a domain/URL.
   1901  * @param {ManagedModeFilteringBehavior} filteringBehavior The filter behavior
   1902  *     for this item.
   1903  * @return {Element} Returns the DOM elements which show the status.
   1904  */
   1905 function getManagedStatusDOM(filteringBehavior) {
   1906   var filterStatusDiv = createElementWithClassName('div', 'filter-status');
   1907   var filteringBehaviorDiv =
   1908       createElementWithClassName('div', 'filtering-behavior');
   1909   filterStatusDiv.appendChild(filteringBehaviorDiv);
   1910 
   1911   updateHostStatus(filterStatusDiv, filteringBehavior);
   1912   return filterStatusDiv;
   1913 }
   1914 
   1915 
   1916 ///////////////////////////////////////////////////////////////////////////////
   1917 // Chrome callbacks:
   1918 
   1919 /**
   1920  * Our history system calls this function with results from searches.
   1921  * @param {Object} info An object containing information about the query.
   1922  * @param {Array} results A list of results.
   1923  */
   1924 function historyResult(info, results) {
   1925   historyModel.addResults(info, results);
   1926 }
   1927 
   1928 /**
   1929  * Called by the history backend when history removal is successful.
   1930  */
   1931 function deleteComplete() {
   1932   historyModel.deleteComplete();
   1933 }
   1934 
   1935 /**
   1936  * Called by the history backend when history removal is unsuccessful.
   1937  */
   1938 function deleteFailed() {
   1939   window.console.log('Delete failed');
   1940 }
   1941 
   1942 /**
   1943  * Called when the history is deleted by someone else.
   1944  */
   1945 function historyDeleted() {
   1946   var anyChecked = document.querySelector('.entry input:checked') != null;
   1947   // Reload the page, unless the user has any items checked.
   1948   // TODO(dubroy): We should just reload the page & restore the checked items.
   1949   if (!anyChecked)
   1950     historyView.reload();
   1951 }
   1952 
   1953 // Add handlers to HTML elements.
   1954 document.addEventListener('DOMContentLoaded', load);
   1955 
   1956 // This event lets us enable and disable menu items before the menu is shown.
   1957 document.addEventListener('canExecute', function(e) {
   1958   e.canExecute = true;
   1959 });
   1960