home *** CD-ROM | disk | FTP | other *** search
/ Freelog 115 / FreelogNo115-MaiJuin2013.iso / Internet / AvantBrowser / asetup.exe / _data / webkit / chrome.dll / 0 / BINDATA / 543 < prev    next >
Encoding:
Text File  |  2013-04-03  |  36.5 KB  |  1,207 lines

  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. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  6. // Use of this source code is governed by a BSD-style license that can be
  7. // found in the LICENSE file.
  8.  
  9. /**
  10.  * @fileoverview A collection of utility methods for UberPage and its contained
  11.  *     pages.
  12.  */
  13.  
  14. cr.define('uber', function() {
  15.  
  16.   /**
  17.    * Fixed position header elements on the page to be shifted by handleScroll.
  18.    * @type {NodeList}
  19.    */
  20.   var headerElements;
  21.  
  22.   /**
  23.    * This should be called by uber content pages when DOM content has loaded.
  24.    */
  25.   function onContentFrameLoaded() {
  26.     headerElements = document.getElementsByTagName('header');
  27.     document.addEventListener('scroll', handleScroll);
  28.  
  29.     // Trigger the scroll handler to tell the navigation if our page started
  30.     // with some scroll (happens when you use tab restore).
  31.     handleScroll();
  32.  
  33.     window.addEventListener('message', handleWindowMessage);
  34.   }
  35.  
  36.   /**
  37.    * Handles scroll events on the document. This adjusts the position of all
  38.    * headers and updates the parent frame when the page is scrolled.
  39.    * @private
  40.    */
  41.   function handleScroll() {
  42.     var offset = document.body.scrollLeft * -1;
  43.     for (var i = 0; i < headerElements.length; i++)
  44.       headerElements[i].style.webkitTransform = 'translateX(' + offset + 'px)';
  45.  
  46.     invokeMethodOnParent('adjustToScroll', document.body.scrollLeft);
  47.   };
  48.  
  49.   /**
  50.    * Handles 'message' events on window.
  51.    * @param {Event} e The message event.
  52.    */
  53.   function handleWindowMessage(e) {
  54.     if (e.data.method === 'frameSelected')
  55.       handleFrameSelected();
  56.     else if (e.data.method === 'mouseWheel')
  57.       handleMouseWheel(e.data.params);
  58.   }
  59.  
  60.   /**
  61.    * This is called when a user selects this frame via the navigation bar
  62.    * frame (and is triggered via postMessage() from the uber page).
  63.    * @private
  64.    */
  65.   function handleFrameSelected() {
  66.     document.body.scrollLeft = 0;
  67.   }
  68.  
  69.   /**
  70.    * Called when a user mouse wheels (or trackpad scrolls) over the nav frame.
  71.    * The wheel event is forwarded here and we scroll the body.
  72.    * There's no way to figure out the actual scroll amount for a given delta.
  73.    * It differs for every platform and even initWebKitWheelEvent takes a
  74.    * pixel amount instead of a wheel delta. So we just choose something
  75.    * reasonable and hope no one notices the difference.
  76.    * @param {Object} params A structure that holds wheel deltas in X and Y.
  77.    */
  78.   function handleMouseWheel(params) {
  79.     document.body.scrollTop -= params.deltaY * 49 / 120;
  80.     document.body.scrollLeft -= params.deltaX * 49 / 120;
  81.   }
  82.  
  83.   /**
  84.    * Invokes a method on the parent window (UberPage). This is a convenience
  85.    * method for API calls into the uber page.
  86.    * @param {String} method The name of the method to invoke.
  87.    * @param {Object=} opt_params Optional property bag of parameters to pass to
  88.    *     the invoked method.
  89.    * @private
  90.    */
  91.   function invokeMethodOnParent(method, opt_params) {
  92.     if (window.location == window.parent.location)
  93.       return;
  94.  
  95.     invokeMethodOnWindow(window.parent, method, opt_params, 'chrome://chrome');
  96.   }
  97.  
  98.   /**
  99.    * Invokes a method on the target window.
  100.    * @param {String} method The name of the method to invoke.
  101.    * @param {Object=} opt_params Optional property bag of parameters to pass to
  102.    *     the invoked method.
  103.    * @param {String=} opt_url The origin of the target window.
  104.    * @private
  105.    */
  106.   function invokeMethodOnWindow(targetWindow, method, opt_params, opt_url) {
  107.     var data = {method: method, params: opt_params};
  108.     targetWindow.postMessage(data, opt_url ? opt_url : '*');
  109.   }
  110.  
  111.   return {
  112.     invokeMethodOnParent: invokeMethodOnParent,
  113.     invokeMethodOnWindow: invokeMethodOnWindow,
  114.     onContentFrameLoaded: onContentFrameLoaded,
  115.   };
  116. });
  117.  
  118.  
  119. ///////////////////////////////////////////////////////////////////////////////
  120. // Globals:
  121. /** @const */ var RESULTS_PER_PAGE = 150;
  122.  
  123. // Amount of time between pageviews that we consider a 'break' in browsing,
  124. // measured in milliseconds.
  125. /** @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000;
  126.  
  127. function createElementWithClassName(type, className) {
  128.   var elm = document.createElement(type);
  129.   elm.className = className;
  130.   return elm;
  131. }
  132.  
  133. // TODO(glen): Get rid of these global references, replace with a controller
  134. //     or just make the classes own more of the page.
  135. var historyModel;
  136. var historyView;
  137. var pageState;
  138. var deleteQueue = [];
  139. var selectionAnchor = -1;
  140. var activeVisit = null;
  141.  
  142. /** @const */ var Command = cr.ui.Command;
  143. /** @const */ var Menu = cr.ui.Menu;
  144. /** @const */ var MenuButton = cr.ui.MenuButton;
  145.  
  146. MenuButton.createDropDownArrows();
  147.  
  148. ///////////////////////////////////////////////////////////////////////////////
  149. // Visit:
  150.  
  151. /**
  152.  * Class to hold all the information about an entry in our model.
  153.  * @param {Object} result An object containing the visit's data.
  154.  * @param {boolean} continued Whether this visit is on the same day as the
  155.  *     visit before it.
  156.  * @param {HistoryModel} model The model object this entry belongs to.
  157.  * @param {Number} id The identifier for the entry.
  158.  * @constructor
  159.  */
  160. function Visit(result, continued, model, id) {
  161.   this.model_ = model;
  162.   this.title_ = result.title;
  163.   this.url_ = result.url;
  164.   this.starred_ = result.starred;
  165.   this.snippet_ = result.snippet || '';
  166.   this.id_ = id;
  167.  
  168.   this.changed = false;
  169.  
  170.   this.isRendered = false;
  171.  
  172.   // All the date information is public so that owners can compare properties of
  173.   // two items easily.
  174.  
  175.   this.date = new Date(result.time);
  176.  
  177.   // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
  178.   // get all of these.
  179.   this.dateRelativeDay = result.dateRelativeDay || '';
  180.   this.dateTimeOfDay = result.dateTimeOfDay || '';
  181.   this.dateShort = result.dateShort || '';
  182.  
  183.   // Whether this is the continuation of a previous day.
  184.   this.continued = continued;
  185. }
  186.  
  187. // Visit, public: -------------------------------------------------------------
  188.  
  189. /**
  190.  * Returns a dom structure for a browse page result or a search page result.
  191.  * @param {boolean} searchResultFlag Indicates whether the result is a search
  192.  *     result or not.
  193.  * @return {Node} A DOM node to represent the history entry or search result.
  194.  */
  195. Visit.prototype.getResultDOM = function(searchResultFlag) {
  196.   var node = createElementWithClassName('li', 'entry');
  197.   var time = createElementWithClassName('div', 'time');
  198.   var entryBox = createElementWithClassName('label', 'entry-box');
  199.   var domain = createElementWithClassName('div', 'domain');
  200.  
  201.   var dropDown = createElementWithClassName('button', 'drop-down');
  202.   dropDown.value = 'Open action menu';
  203.   dropDown.title = loadTimeData.getString('actionMenuDescription');
  204.   dropDown.setAttribute('menu', '#action-menu');
  205.   cr.ui.decorate(dropDown, MenuButton);
  206.  
  207.   // Checkbox is always created, but only visible on hover & when checked.
  208.   var checkbox = document.createElement('input');
  209.   checkbox.type = 'checkbox';
  210.   checkbox.id = 'checkbox-' + this.id_;
  211.   checkbox.time = this.date.getTime();
  212.   checkbox.addEventListener('click', checkboxClicked);
  213.   time.appendChild(checkbox);
  214.  
  215.   // Keep track of the drop down that triggered the menu, so we know
  216.   // which element to apply the command to.
  217.   // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
  218.   var self = this;
  219.   var setActiveVisit = function(e) {
  220.     activeVisit = self;
  221.   };
  222.   dropDown.addEventListener('mousedown', setActiveVisit);
  223.   dropDown.addEventListener('focus', setActiveVisit);
  224.  
  225.   domain.textContent = this.getDomainFromURL_(this.url_);
  226.  
  227.   // Clicking anywhere in the entryBox will check/uncheck the checkbox.
  228.   entryBox.setAttribute('for', checkbox.id);
  229.   entryBox.addEventListener('mousedown', entryBoxMousedown);
  230.  
  231.   // Prevent clicks on the drop down from affecting the checkbox.
  232.   dropDown.addEventListener('click', function(e) { e.preventDefault(); });
  233.  
  234.   // We use a wrapper div so that the entry contents will be shinkwrapped.
  235.   entryBox.appendChild(time);
  236.   entryBox.appendChild(this.getTitleDOM_());
  237.   entryBox.appendChild(domain);
  238.   entryBox.appendChild(dropDown);
  239.  
  240.   // Let the entryBox be styled appropriately when it contains keyboard focus.
  241.   entryBox.addEventListener('focus', function() {
  242.     this.classList.add('contains-focus');
  243.   }, true);
  244.   entryBox.addEventListener('blur', function() {
  245.     this.classList.remove('contains-focus');
  246.   }, true);
  247.  
  248.   node.appendChild(entryBox);
  249.  
  250.   if (searchResultFlag) {
  251.     time.appendChild(document.createTextNode(this.dateShort));
  252.     var snippet = createElementWithClassName('div', 'snippet');
  253.     this.addHighlightedText_(snippet,
  254.                              this.snippet_,
  255.                              this.model_.getSearchText());
  256.     node.appendChild(snippet);
  257.   } else {
  258.     time.appendChild(document.createTextNode(this.dateTimeOfDay));
  259.   }
  260.  
  261.   this.domNode_ = node;
  262.  
  263.   return node;
  264. };
  265.  
  266. // Visit, private: ------------------------------------------------------------
  267.  
  268. /**
  269.  * Extracts and returns the domain (and subdomains) from a URL.
  270.  * @param {string} url The url.
  271.  * @return {string} The domain. An empty string is returned if no domain can
  272.  *     be found.
  273.  * @private
  274.  */
  275. Visit.prototype.getDomainFromURL_ = function(url) {
  276.   var domain = url.replace(/^.+:\/\//, '').match(/[^/]+/);
  277.   return domain ? domain[0] : '';
  278. };
  279.  
  280. /**
  281.  * Add child text nodes to a node such that occurrences of the specified text is
  282.  * highlighted.
  283.  * @param {Node} node The node under which new text nodes will be made as
  284.  *     children.
  285.  * @param {string} content Text to be added beneath |node| as one or more
  286.  *     text nodes.
  287.  * @param {string} highlightText Occurences of this text inside |content| will
  288.  *     be highlighted.
  289.  * @private
  290.  */
  291. Visit.prototype.addHighlightedText_ = function(node, content, highlightText) {
  292.   var i = 0;
  293.   if (highlightText) {
  294.     var re = new RegExp(Visit.pregQuote_(highlightText), 'gim');
  295.     var match;
  296.     while (match = re.exec(content)) {
  297.       if (match.index > i)
  298.         node.appendChild(document.createTextNode(content.slice(i,
  299.                                                                match.index)));
  300.       i = re.lastIndex;
  301.       // Mark the highlighted text in bold.
  302.       var b = document.createElement('b');
  303.       b.textContent = content.substring(match.index, i);
  304.       node.appendChild(b);
  305.     }
  306.   }
  307.   if (i < content.length)
  308.     node.appendChild(document.createTextNode(content.slice(i)));
  309. };
  310.  
  311. /**
  312.  * @return {DOMObject} DOM representation for the title block.
  313.  * @private
  314.  */
  315. Visit.prototype.getTitleDOM_ = function() {
  316.   var node = createElementWithClassName('div', 'title');
  317.   node.style.backgroundImage = url(getFaviconURL(this.url_));
  318.   node.style.backgroundSize = '16px';
  319.  
  320.   var link = document.createElement('a');
  321.   link.href = this.url_;
  322.   link.id = 'id-' + this.id_;
  323.   link.target = '_top';
  324.  
  325.   // Add a tooltip, since it might be ellipsized.
  326.   // TODO(dubroy): Find a way to show the tooltip only when necessary.
  327.   link.title = this.title_;
  328.  
  329.   this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
  330.   node.appendChild(link);
  331.  
  332.   if (this.starred_) {
  333.     var star = createElementWithClassName('div', 'starred');
  334.     node.appendChild(star);
  335.     star.addEventListener('click', this.starClicked_.bind(this));
  336.   }
  337.  
  338.   return node;
  339. };
  340.  
  341. /**
  342.  * Launch a search for more history entries from the same domain.
  343.  * @private
  344.  */
  345. Visit.prototype.showMoreFromSite_ = function() {
  346.   setSearch(this.getDomainFromURL_(this.url_));
  347. };
  348.  
  349. /**
  350.  * Remove a single entry from the history.
  351.  * @private
  352.  */
  353. Visit.prototype.removeFromHistory_ = function() {
  354.   var self = this;
  355.   var onSuccessCallback = function() {
  356.     removeEntryFromView(self.domNode_);
  357.   };
  358.   queueURLsForDeletion(this.date, [this.url_], onSuccessCallback);
  359.   deleteNextInQueue();
  360. };
  361.  
  362. /**
  363.  * Click event handler for the star icon that appears beside bookmarked URLs.
  364.  * When clicked, the bookmark is removed for that URL.
  365.  * @param {Event} event The click event.
  366.  * @private
  367.  */
  368. Visit.prototype.starClicked_ = function(event) {
  369.   chrome.send('removeBookmark', [this.url_]);
  370.   event.currentTarget.hidden = true;
  371.   event.preventDefault();
  372. };
  373.  
  374. // Visit, private, static: ----------------------------------------------------
  375.  
  376. /**
  377.  * Quote a string so it can be used in a regular expression.
  378.  * @param {string} str The source string
  379.  * @return {string} The escaped string
  380.  * @private
  381.  */
  382. Visit.pregQuote_ = function(str) {
  383.   return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
  384. };
  385.  
  386. ///////////////////////////////////////////////////////////////////////////////
  387. // HistoryModel:
  388.  
  389. /**
  390.  * Global container for history data. Future optimizations might include
  391.  * allowing the creation of a HistoryModel for each search string, allowing
  392.  * quick flips back and forth between results.
  393.  *
  394.  * The history model is based around pages, and only fetching the data to
  395.  * fill the currently requested page. This is somewhat dependent on the view,
  396.  * and so future work may wish to change history model to operate on
  397.  * timeframe (day or week) based containers.
  398.  *
  399.  * @constructor
  400.  */
  401. function HistoryModel() {
  402.   this.clearModel_();
  403. }
  404.  
  405. // HistoryModel, Public: ------------------------------------------------------
  406.  
  407. /**
  408.  * Sets our current view that is called when the history model changes.
  409.  * @param {HistoryView} view The view to set our current view to.
  410.  */
  411. HistoryModel.prototype.setView = function(view) {
  412.   this.view_ = view;
  413. };
  414.  
  415. /**
  416.  * Start a new search - this will clear out our model.
  417.  * @param {String} searchText The text to search for
  418.  * @param {Number} opt_page The page to view - this is mostly used when setting
  419.  *     up an initial view, use #requestPage otherwise.
  420.  */
  421. HistoryModel.prototype.setSearchText = function(searchText, opt_page) {
  422.   this.clearModel_();
  423.   this.searchText_ = searchText;
  424.   this.requestedPage_ = opt_page ? opt_page : 0;
  425.   this.queryHistory_();
  426. };
  427.  
  428. /**
  429.  * Reload our model with the current parameters.
  430.  */
  431. HistoryModel.prototype.reload = function() {
  432.   var search = this.searchText_;
  433.   var page = this.requestedPage_;
  434.   this.clearModel_();
  435.   this.searchText_ = search;
  436.   this.requestedPage_ = page;
  437.   this.queryHistory_();
  438. };
  439.  
  440. /**
  441.  * @return {String} The current search text.
  442.  */
  443. HistoryModel.prototype.getSearchText = function() {
  444.   return this.searchText_;
  445. };
  446.  
  447. /**
  448.  * Tell the model that the view will want to see the current page. When
  449.  * the data becomes available, the model will call the view back.
  450.  * @param {Number} page The page we want to view.
  451.  */
  452. HistoryModel.prototype.requestPage = function(page) {
  453.   this.requestedPage_ = page;
  454.   this.changed = true;
  455.   this.updateSearch_();
  456. };
  457.  
  458. /**
  459.  * Receiver for history query.
  460.  * @param {Object} info An object containing information about the query.
  461.  * @param {Array} results A list of results.
  462.  */
  463. HistoryModel.prototype.addResults = function(info, results) {
  464.   $('loading-spinner').hidden = true;
  465.   this.inFlight_ = false;
  466.   this.isQueryFinished_ = info.finished;
  467.   this.queryCursor_ = info.cursor;
  468.  
  469.   // If there are no results, or they're not for the current search term,
  470.   // there's nothing more to do.
  471.   if (!results || !results.length || info.term != this.searchText_)
  472.     return;
  473.  
  474.   // If necessary, sort the results from newest to oldest.
  475.   if (!results.sorted)
  476.     results.sort(function(a, b) { return b.time - a.time; });
  477.  
  478.   var lastVisit = this.visits_.slice(-1)[0];
  479.   var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
  480.  
  481.   for (var i = 0, thisResult; thisResult = results[i]; i++) {
  482.     var thisDay = thisResult.dateRelativeDay;
  483.     var isSameDay = lastDay == thisDay;
  484.  
  485.     // Keep track of all URLs seen on a particular day, and only use the
  486.     // latest visit from that day.
  487.     if (!isSameDay)
  488.       this.urlsFromLastSeenDay_ = {};
  489.     else if (thisResult.url in this.urlsFromLastSeenDay_)
  490.       continue;
  491.     this.urlsFromLastSeenDay_[thisResult.url] = thisResult.time;
  492.  
  493.     this.visits_.push(new Visit(thisResult, isSameDay, this, this.last_id_++));
  494.     this.changed = true;
  495.     lastDay = thisDay;
  496.   }
  497.  
  498.   this.updateSearch_();
  499. };
  500.  
  501. /**
  502.  * @return {Number} The number of visits in the model.
  503.  */
  504. HistoryModel.prototype.getSize = function() {
  505.   return this.visits_.length;
  506. };
  507.  
  508. /**
  509.  * Get a list of visits between specified index positions.
  510.  * @param {Number} start The start index
  511.  * @param {Number} end The end index
  512.  * @return {Array.<Visit>} A list of visits
  513.  */
  514. HistoryModel.prototype.getNumberedRange = function(start, end) {
  515.   return this.visits_.slice(start, end);
  516. };
  517.  
  518. /**
  519.  * Return true if there are more results beyond the current page.
  520.  * @return {boolean} true if the there are more results, otherwise false.
  521.  */
  522. HistoryModel.prototype.hasMoreResults = function() {
  523.   return this.haveDataForPage_(this.requestedPage_ + 1) ||
  524.       !this.isQueryFinished_;
  525. };
  526.  
  527. // HistoryModel, Private: -----------------------------------------------------
  528.  
  529. /**
  530.  * Clear the history model.
  531.  * @private
  532.  */
  533. HistoryModel.prototype.clearModel_ = function() {
  534.   this.inFlight_ = false;  // Whether a query is inflight.
  535.   this.searchText_ = '';
  536.  
  537.   this.visits_ = [];  // Date-sorted list of visits (most recent first).
  538.   this.last_id_ = 0;
  539.   selectionAnchor = -1;
  540.  
  541.   // The page that the view wants to see - we only fetch slightly past this
  542.   // point. If the view requests a page that we don't have data for, we try
  543.   // to fetch it and call back when we're done.
  544.   this.requestedPage_ = 0;
  545.  
  546.   // Keeps track of whether or not there are more results available than are
  547.   // currently held in |this.visits_|.
  548.   this.isQueryFinished_ = false;
  549.  
  550.   // An opaque value that is returned with the query results. This is used to
  551.   // fetch the next page of results for a query.
  552.   this.queryCursor_ = null;
  553.  
  554.   // A map of URLs of visits on the same day as the last known visit.
  555.   // This is used for de-duping URLs, so that we only show the most recent
  556.   // visit to a URL on any day.
  557.   this.urlsFromLastSeenDay_ = {};
  558.  
  559.   if (this.view_)
  560.     this.view_.clear_();
  561. };
  562.  
  563. /**
  564.  * Figure out if we need to do more queries to fill the currently requested
  565.  * page. If we think we can fill the page, call the view and let it know
  566.  * we're ready to show something.
  567.  * @private
  568.  */
  569. HistoryModel.prototype.updateSearch_ = function() {
  570.   var doneLoading =
  571.       this.canFillPage_(this.requestedPage_) || this.isQueryFinished_;
  572.  
  573.   // Try to fetch more results if the current page isn't full.
  574.   if (!doneLoading && !this.inFlight_)
  575.     this.queryHistory_();
  576.  
  577.   // If we have any data for the requested page, show it.
  578.   if (this.changed && this.haveDataForPage_(this.requestedPage_)) {
  579.     this.view_.onModelReady();
  580.     this.changed = false;
  581.   }
  582. };
  583.  
  584. /**
  585.  * Query for history, either for a search or time-based browsing.
  586.  * @private
  587.  */
  588. HistoryModel.prototype.queryHistory_ = function() {
  589.   var endTime = 0;
  590.  
  591.   // If there are already some visits, pick up the previous query where it
  592.   // left off.
  593.   if (this.visits_.length > 0) {
  594.     var lastVisit = this.visits_.slice(-1)[0];
  595.     endTime = lastVisit.date.getTime();
  596.     cursor = this.queryCursor_;
  597.   }
  598.  
  599.   $('loading-spinner').hidden = false;
  600.   this.inFlight_ = true;
  601.   chrome.send('queryHistory',
  602.       [this.searchText_, endTime, this.queryCursor_, RESULTS_PER_PAGE]);
  603. };
  604.  
  605. /**
  606.  * Check to see if we have data for the given page.
  607.  * @param {Number} page The page number.
  608.  * @return {boolean} Whether we have any data for the given page.
  609.  * @private
  610.  */
  611. HistoryModel.prototype.haveDataForPage_ = function(page) {
  612.   return (page * RESULTS_PER_PAGE < this.getSize());
  613. };
  614.  
  615. /**
  616.  * Check to see if we have data to fill the given page.
  617.  * @param {Number} page The page number.
  618.  * @return {boolean} Whether we have data to fill the page.
  619.  * @private
  620.  */
  621. HistoryModel.prototype.canFillPage_ = function(page) {
  622.   return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
  623. };
  624.  
  625. ///////////////////////////////////////////////////////////////////////////////
  626. // HistoryView:
  627.  
  628. /**
  629.  * Functions and state for populating the page with HTML. This should one-day
  630.  * contain the view and use event handlers, rather than pushing HTML out and
  631.  * getting called externally.
  632.  * @param {HistoryModel} model The model backing this view.
  633.  * @constructor
  634.  */
  635. function HistoryView(model) {
  636.   this.editButtonTd_ = $('edit-button');
  637.   this.editingControlsDiv_ = $('editing-controls');
  638.   this.resultDiv_ = $('results-display');
  639.   this.pageDiv_ = $('results-pagination');
  640.   this.model_ = model;
  641.   this.pageIndex_ = 0;
  642.   this.lastDisplayed_ = [];
  643.  
  644.   this.model_.setView(this);
  645.  
  646.   this.currentVisits_ = [];
  647.  
  648.   var self = this;
  649.  
  650.   $('clear-browsing-data').addEventListener('click', openClearBrowsingData);
  651.   $('remove-selected').addEventListener('click', removeItems);
  652.  
  653.   // Add handlers for the page navigation buttons at the bottom.
  654.   $('newest-button').addEventListener('click', function() {
  655.     self.setPage(0);
  656.   });
  657.   $('newer-button').addEventListener('click', function() {
  658.     self.setPage(self.pageIndex_ - 1);
  659.   });
  660.   $('older-button').addEventListener('click', function() {
  661.     self.setPage(self.pageIndex_ + 1);
  662.   });
  663. }
  664.  
  665. // HistoryView, public: -------------------------------------------------------
  666. /**
  667.  * Do a search and optionally view a certain page.
  668.  * @param {string} term The string to search for.
  669.  * @param {number} opt_page The page we wish to view, only use this for
  670.  *     setting up initial views, as this triggers a search.
  671.  */
  672. HistoryView.prototype.setSearch = function(term, opt_page) {
  673.   this.pageIndex_ = parseInt(opt_page || 0, 10);
  674.   window.scrollTo(0, 0);
  675.   this.model_.setSearchText(term, this.pageIndex_);
  676.   pageState.setUIState(term, this.pageIndex_);
  677. };
  678.  
  679. /**
  680.  * Reload the current view.
  681.  */
  682. HistoryView.prototype.reload = function() {
  683.   this.model_.reload();
  684.   this.updateRemoveButton();
  685. };
  686.  
  687. /**
  688.  * Switch to a specified page.
  689.  * @param {number} page The page we wish to view.
  690.  */
  691. HistoryView.prototype.setPage = function(page) {
  692.   this.clear_();
  693.   this.pageIndex_ = parseInt(page, 10);
  694.   window.scrollTo(0, 0);
  695.   this.model_.requestPage(page);
  696.   pageState.setUIState(this.model_.getSearchText(), this.pageIndex_);
  697. };
  698.  
  699. /**
  700.  * @return {number} The page number being viewed.
  701.  */
  702. HistoryView.prototype.getPage = function() {
  703.   return this.pageIndex_;
  704. };
  705.  
  706. /**
  707.  * Callback for the history model to let it know that it has data ready for us
  708.  * to view.
  709.  */
  710. HistoryView.prototype.onModelReady = function() {
  711.   this.displayResults_();
  712.   this.updateNavBar_();
  713. };
  714.  
  715. /**
  716.  * Enables or disables the 'Remove selected items' button as appropriate.
  717.  */
  718. HistoryView.prototype.updateRemoveButton = function() {
  719.   var anyChecked = document.querySelector('.entry input:checked') != null;
  720.   $('remove-selected').disabled = !anyChecked;
  721. };
  722.  
  723. // HistoryView, private: ------------------------------------------------------
  724.  
  725. /**
  726.  * Clear the results in the view.  Since we add results piecemeal, we need
  727.  * to clear them out when we switch to a new page or reload.
  728.  * @private
  729.  */
  730. HistoryView.prototype.clear_ = function() {
  731.   this.resultDiv_.textContent = '';
  732.  
  733.   this.currentVisits_.forEach(function(visit) {
  734.     visit.isRendered = false;
  735.   });
  736.   this.currentVisits_ = [];
  737. };
  738.  
  739. /**
  740.  * Record that the given visit has been rendered.
  741.  * @param {Visit} visit The visit that was rendered.
  742.  * @private
  743.  */
  744. HistoryView.prototype.setVisitRendered_ = function(visit) {
  745.   visit.isRendered = true;
  746.   this.currentVisits_.push(visit);
  747. };
  748.  
  749. /**
  750.  * Update the page with results.
  751.  * @private
  752.  */
  753. HistoryView.prototype.displayResults_ = function() {
  754.   var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
  755.   var rangeEnd = rangeStart + RESULTS_PER_PAGE;
  756.   var results = this.model_.getNumberedRange(rangeStart, rangeEnd);
  757.  
  758.   var searchText = this.model_.getSearchText();
  759.   if (searchText) {
  760.     // Add a header for the search results, if there isn't already one.
  761.     if (!this.resultDiv_.querySelector('h3')) {
  762.       var header = document.createElement('h3');
  763.       header.textContent = loadTimeData.getStringF('searchresultsfor',
  764.                                                    searchText);
  765.       this.resultDiv_.appendChild(header);
  766.     }
  767.  
  768.     var searchResults = createElementWithClassName('ol', 'search-results');
  769.     if (results.length == 0) {
  770.       var noResults = document.createElement('div');
  771.       noResults.textContent = loadTimeData.getString('noresults');
  772.       searchResults.appendChild(noResults);
  773.     } else {
  774.       for (var i = 0, visit; visit = results[i]; i++) {
  775.         if (!visit.isRendered) {
  776.           searchResults.appendChild(visit.getResultDOM(true));
  777.           this.setVisitRendered_(visit);
  778.         }
  779.       }
  780.     }
  781.     this.resultDiv_.appendChild(searchResults);
  782.   } else {
  783.     var resultsFragment = document.createDocumentFragment();
  784.     var lastTime = Math.infinity;
  785.     var dayResults;
  786.  
  787.     for (var i = 0, visit; visit = results[i]; i++) {
  788.       if (visit.isRendered)
  789.         continue;
  790.  
  791.       var thisTime = visit.date.getTime();
  792.  
  793.       // Break across day boundaries and insert gaps for browsing pauses.
  794.       // Create a dayResults element to contain results for each day.
  795.       if ((i == 0 && visit.continued) || !visit.continued) {
  796.         // It's the first visit of the day, or the day is continued from
  797.         // the previous page. Create a header for the day on the current page.
  798.         var day = createElementWithClassName('h3', 'day');
  799.         day.appendChild(document.createTextNode(visit.dateRelativeDay));
  800.         if (visit.continued) {
  801.           day.appendChild(document.createTextNode(' ' +
  802.               loadTimeData.getString('cont')));
  803.         }
  804.  
  805.         resultsFragment.appendChild(day);
  806.         dayResults = createElementWithClassName('ol', 'day-results');
  807.         resultsFragment.appendChild(dayResults);
  808.       } else if (dayResults && lastTime - thisTime > BROWSING_GAP_TIME) {
  809.         dayResults.appendChild(createElementWithClassName('li', 'gap'));
  810.       }
  811.       lastTime = thisTime;
  812.  
  813.       // Add the entry to the appropriate day.
  814.       dayResults.appendChild(visit.getResultDOM(false));
  815.       this.setVisitRendered_(visit);
  816.     }
  817.     this.resultDiv_.appendChild(resultsFragment);
  818.   }
  819. };
  820.  
  821. /**
  822.  * Update the visibility of the page navigation buttons.
  823.  * @private
  824.  */
  825. HistoryView.prototype.updateNavBar_ = function() {
  826.   $('newest-button').hidden = this.pageIndex_ == 0;
  827.   $('newer-button').hidden = this.pageIndex_ == 0;
  828.   $('older-button').hidden = !this.model_.hasMoreResults();
  829. };
  830.  
  831. ///////////////////////////////////////////////////////////////////////////////
  832. // State object:
  833. /**
  834.  * An 'AJAX-history' implementation.
  835.  * @param {HistoryModel} model The model we're representing.
  836.  * @param {HistoryView} view The view we're representing.
  837.  * @constructor
  838.  */
  839. function PageState(model, view) {
  840.   // Enforce a singleton.
  841.   if (PageState.instance) {
  842.     return PageState.instance;
  843.   }
  844.  
  845.   this.model = model;
  846.   this.view = view;
  847.  
  848.   if (typeof this.checker_ != 'undefined' && this.checker_) {
  849.     clearInterval(this.checker_);
  850.   }
  851.  
  852.   // TODO(glen): Replace this with a bound method so we don't need
  853.   //     public model and view.
  854.   this.checker_ = setInterval((function(state_obj) {
  855.     var hashData = state_obj.getHashData();
  856.     if (hashData.q != state_obj.model.getSearchText()) {
  857.       state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10));
  858.     } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) {
  859.       state_obj.view.setPage(hashData.p);
  860.     }
  861.   }), 50, this);
  862. }
  863.  
  864. /**
  865.  * Holds the singleton instance.
  866.  */
  867. PageState.instance = null;
  868.  
  869. /**
  870.  * @return {Object} An object containing parameters from our window hash.
  871.  */
  872. PageState.prototype.getHashData = function() {
  873.   var result = {
  874.     e: 0,
  875.     q: '',
  876.     p: 0
  877.   };
  878.  
  879.   if (!window.location.hash) {
  880.     return result;
  881.   }
  882.  
  883.   var hashSplit = window.location.hash.substr(1).split('&');
  884.   for (var i = 0; i < hashSplit.length; i++) {
  885.     var pair = hashSplit[i].split('=');
  886.     if (pair.length > 1) {
  887.       result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
  888.     }
  889.   }
  890.  
  891.   return result;
  892. };
  893.  
  894. /**
  895.  * Set the hash to a specified state, this will create an entry in the
  896.  * session history so the back button cycles through hash states, which
  897.  * are then picked up by our listener.
  898.  * @param {string} term The current search string.
  899.  * @param {string} page The page currently being viewed.
  900.  */
  901. PageState.prototype.setUIState = function(term, page) {
  902.   // Make sure the form looks pretty.
  903.   $('search-field').value = term;
  904.   var currentHash = this.getHashData();
  905.   if (currentHash.q != term || currentHash.p != page) {
  906.     window.location.hash = PageState.getHashString(term, page);
  907.   }
  908. };
  909.  
  910. /**
  911.  * Static method to get the hash string for a specified state
  912.  * @param {string} term The current search string.
  913.  * @param {string} page The page currently being viewed.
  914.  * @return {string} The string to be used in a hash.
  915.  */
  916. PageState.getHashString = function(term, page) {
  917.   var newHash = [];
  918.   if (term) {
  919.     newHash.push('q=' + encodeURIComponent(term));
  920.   }
  921.   if (page != undefined) {
  922.     newHash.push('p=' + page);
  923.   }
  924.  
  925.   return newHash.join('&');
  926. };
  927.  
  928. ///////////////////////////////////////////////////////////////////////////////
  929. // Document Functions:
  930. /**
  931.  * Window onload handler, sets up the page.
  932.  */
  933. function load() {
  934.   uber.onContentFrameLoaded();
  935.  
  936.   var searchField = $('search-field');
  937.   searchField.focus();
  938.  
  939.   historyModel = new HistoryModel();
  940.   historyView = new HistoryView(historyModel);
  941.   pageState = new PageState(historyModel, historyView);
  942.  
  943.   // Create default view.
  944.   var hashData = pageState.getHashData();
  945.   historyView.setSearch(hashData.q, hashData.p);
  946.  
  947.   $('search-form').onsubmit = function() {
  948.     setSearch(searchField.value);
  949.     return false;
  950.   };
  951.  
  952.   $('remove-visit').addEventListener('activate', function(e) {
  953.     activeVisit.removeFromHistory_();
  954.     activeVisit = null;
  955.   });
  956.   $('more-from-site').addEventListener('activate', function(e) {
  957.     activeVisit.showMoreFromSite_();
  958.     activeVisit = null;
  959.   });
  960.  
  961.   var title = loadTimeData.getString('title');
  962.   uber.invokeMethodOnParent('setTitle', {title: title});
  963.  
  964.   window.addEventListener('message', function(e) {
  965.     if (e.data.method == 'frameSelected')
  966.       searchField.focus();
  967.   });
  968. }
  969.  
  970. /**
  971.  * TODO(glen): Get rid of this function.
  972.  * Set the history view to a specified page.
  973.  * @param {String} term The string to search for
  974.  */
  975. function setSearch(term) {
  976.   if (historyView) {
  977.     historyView.setSearch(term);
  978.   }
  979. }
  980.  
  981. /**
  982.  * TODO(glen): Get rid of this function.
  983.  * Set the history view to a specified page.
  984.  * @param {number} page The page to set the view to.
  985.  */
  986. function setPage(page) {
  987.   if (historyView) {
  988.     historyView.setPage(page);
  989.   }
  990. }
  991.  
  992. /**
  993.  * Delete the next item in our deletion queue.
  994.  */
  995. function deleteNextInQueue() {
  996.   if (deleteQueue.length > 0) {
  997.     // Call the native function to remove history entries.
  998.     // First arg is a time (in ms since the epoch) identifying the day.
  999.     // Remaining args are URLs of history entries from that day to delete.
  1000.     chrome.send('removeURLsOnOneDay',
  1001.                 [deleteQueue[0].date.getTime()].concat(deleteQueue[0].urls));
  1002.   }
  1003. }
  1004.  
  1005. /**
  1006.  * Click handler for the 'Clear browsing data' dialog.
  1007.  * @param {Event} e The click event.
  1008.  */
  1009. function openClearBrowsingData(e) {
  1010.   chrome.send('clearBrowsingData');
  1011. }
  1012.  
  1013. /**
  1014.  * Queue a set of URLs from the same day for deletion.
  1015.  * @param {Date} date A date indicating the day the URLs were visited.
  1016.  * @param {Array} urls Array of URLs from the same day to be deleted.
  1017.  * @param {Function} opt_callback An optional callback to be executed when
  1018.  *        the deletion is complete.
  1019.  */
  1020. function queueURLsForDeletion(date, urls, opt_callback) {
  1021.   deleteQueue.push({ 'date': date, 'urls': urls, 'callback': opt_callback });
  1022. }
  1023.  
  1024. function reloadHistory() {
  1025.   historyView.reload();
  1026. }
  1027.  
  1028. /**
  1029.  * Click handler for the 'Remove selected items' button.
  1030.  * Collect IDs from the checked checkboxes and send to Chrome for deletion.
  1031.  */
  1032. function removeItems() {
  1033.   var checked = document.querySelectorAll(
  1034.       'input[type=checkbox]:checked:not([disabled])');
  1035.   var urls = [];
  1036.   var disabledItems = [];
  1037.   var queue = [];
  1038.   var date = new Date();
  1039.  
  1040.   for (var i = 0; i < checked.length; i++) {
  1041.     var checkbox = checked[i];
  1042.     var cbDate = new Date(checkbox.time);
  1043.     if (date.getFullYear() != cbDate.getFullYear() ||
  1044.         date.getMonth() != cbDate.getMonth() ||
  1045.         date.getDate() != cbDate.getDate()) {
  1046.       if (urls.length > 0) {
  1047.         queue.push([date, urls]);
  1048.       }
  1049.       urls = [];
  1050.       date = cbDate;
  1051.     }
  1052.     var link = findAncestorByClass(checkbox, 'entry-box').querySelector('a');
  1053.     checkbox.disabled = true;
  1054.     link.classList.add('to-be-removed');
  1055.     disabledItems.push(checkbox);
  1056.     urls.push(link.href);
  1057.   }
  1058.   if (urls.length > 0) {
  1059.     queue.push([date, urls]);
  1060.   }
  1061.   if (checked.length > 0 && confirm(loadTimeData.getString('deletewarning'))) {
  1062.     for (var i = 0; i < queue.length; i++) {
  1063.       // Reload the page when the final entry has been deleted.
  1064.       var callback = i == 0 ? reloadHistory : null;
  1065.  
  1066.       queueURLsForDeletion(queue[i][0], queue[i][1], callback);
  1067.     }
  1068.     deleteNextInQueue();
  1069.   } else {
  1070.     // If the remove is cancelled, return the checkboxes to their
  1071.     // enabled, non-line-through state.
  1072.     for (var i = 0; i < disabledItems.length; i++) {
  1073.       var checkbox = disabledItems[i];
  1074.       var link = findAncestorByClass(checkbox, 'entry-box').querySelector('a');
  1075.       checkbox.disabled = false;
  1076.       link.classList.remove('to-be-removed');
  1077.     }
  1078.   }
  1079. }
  1080.  
  1081. /**
  1082.  * Handler for the 'click' event on a checkbox.
  1083.  * @param {Event} e The click event.
  1084.  */
  1085. function checkboxClicked(e) {
  1086.   var checkbox = e.currentTarget;
  1087.   var id = Number(checkbox.id.slice('checkbox-'.length));
  1088.   // Handle multi-select if shift was pressed.
  1089.   if (event.shiftKey && (selectionAnchor != -1)) {
  1090.     var checked = checkbox.checked;
  1091.     // Set all checkboxes from the anchor up to the clicked checkbox to the
  1092.     // state of the clicked one.
  1093.     var begin = Math.min(id, selectionAnchor);
  1094.     var end = Math.max(id, selectionAnchor);
  1095.     for (var i = begin; i <= end; i++) {
  1096.       var checkbox = document.querySelector('#checkbox-' + i);
  1097.       if (checkbox)
  1098.         checkbox.checked = checked;
  1099.     }
  1100.   }
  1101.   selectionAnchor = id;
  1102.  
  1103.   historyView.updateRemoveButton();
  1104. }
  1105.  
  1106. function entryBoxMousedown(event) {
  1107.   // Prevent text selection when shift-clicking to select multiple entries.
  1108.   if (event.shiftKey) {
  1109.     event.preventDefault();
  1110.   }
  1111. }
  1112.  
  1113. function removeNode(node) {
  1114.   node.classList.add('fade-out'); // Trigger CSS fade out animation.
  1115.  
  1116.   // Delete the node when the animation is complete.
  1117.   node.addEventListener('webkitTransitionEnd', function() {
  1118.     node.parentNode.removeChild(node);
  1119.   });
  1120. }
  1121.  
  1122. /**
  1123.  * Removes a single entry from the view. Also removes gaps before and after
  1124.  * entry if necessary.
  1125.  * @param {Node} entry The DOM node representing the entry to be removed.
  1126.  */
  1127. function removeEntryFromView(entry) {
  1128.   var nextEntry = entry.nextSibling;
  1129.   var previousEntry = entry.previousSibling;
  1130.  
  1131.   removeNode(entry);
  1132.  
  1133.   // if there is no previous entry, and the next entry is a gap, remove it
  1134.   if (!previousEntry && nextEntry && nextEntry.className == 'gap') {
  1135.     removeNode(nextEntry);
  1136.   }
  1137.  
  1138.   // if there is no next entry, and the previous entry is a gap, remove it
  1139.   if (!nextEntry && previousEntry && previousEntry.className == 'gap') {
  1140.     removeNode(previousEntry);
  1141.   }
  1142.  
  1143.   // if both the next and previous entries are gaps, remove one
  1144.   if (nextEntry && nextEntry.className == 'gap' &&
  1145.       previousEntry && previousEntry.className == 'gap') {
  1146.     removeNode(nextEntry);
  1147.   }
  1148. }
  1149.  
  1150. ///////////////////////////////////////////////////////////////////////////////
  1151. // Chrome callbacks:
  1152.  
  1153. /**
  1154.  * Our history system calls this function with results from searches.
  1155.  * @param {Object} info An object containing information about the query.
  1156.  * @param {Array} results A list of results.
  1157.  */
  1158. function historyResult(info, results) {
  1159.   historyModel.addResults(info, results);
  1160. }
  1161.  
  1162. /**
  1163.  * Our history system calls this function when a deletion has finished.
  1164.  */
  1165. function deleteComplete() {
  1166.   if (deleteQueue.length > 0) {
  1167.     // Remove the successfully deleted entry from the queue.
  1168.     if (deleteQueue[0].callback)
  1169.       deleteQueue[0].callback.apply();
  1170.     deleteQueue.splice(0, 1);
  1171.     deleteNextInQueue();
  1172.   } else {
  1173.     console.error('Received deleteComplete but queue is empty.');
  1174.   }
  1175. }
  1176.  
  1177. /**
  1178.  * Our history system calls this function if a delete is not ready (e.g.
  1179.  * another delete is in-progress).
  1180.  */
  1181. function deleteFailed() {
  1182.   window.console.log('Delete failed');
  1183.  
  1184.   // The deletion failed - try again later.
  1185.   // TODO(dubroy): We should probably give up at some point.
  1186.   setTimeout(deleteNextInQueue, 500);
  1187. }
  1188.  
  1189. /**
  1190.  * Called when the history is deleted by someone else.
  1191.  */
  1192. function historyDeleted() {
  1193.   var anyChecked = document.querySelector('.entry input:checked') != null;
  1194.   // Reload the page, unless the user has any items checked.
  1195.   // TODO(dubroy): We should just reload the page & restore the checked items.
  1196.   if (!anyChecked)
  1197.     historyView.reload();
  1198. }
  1199.  
  1200. // Add handlers to HTML elements.
  1201. document.addEventListener('DOMContentLoaded', load);
  1202.  
  1203. // This event lets us enable and disable menu items before the menu is shown.
  1204. document.addEventListener('canExecute', function(e) {
  1205.   e.canExecute = true;
  1206. });
  1207.