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