Home | History | Annotate | Download | only in resources
      1 <!DOCTYPE HTML>
      2 <html i18n-values="dir:textdirection;">
      3 <head>
      4 <meta charset="utf-8">
      5 <title i18n-content="title"></title>
      6 <link rel="icon" href="../../app/theme/history_favicon.png">
      7 <script src="shared/js/local_strings.js"></script>
      8 <script src="shared/js/util.js"></script>
      9 <script>
     10 ///////////////////////////////////////////////////////////////////////////////
     11 // Globals:
     12 var RESULTS_PER_PAGE = 150;
     13 var MAX_SEARCH_DEPTH_MONTHS = 18;
     14 
     15 // Amount of time between pageviews that we consider a 'break' in browsing,
     16 // measured in milliseconds.
     17 var BROWSING_GAP_TIME = 15 * 60 * 1000;
     18 
     19 function $(o) {return document.getElementById(o);}
     20 
     21 function createElementWithClassName(type, className) {
     22   var elm = document.createElement(type);
     23   elm.className = className;
     24   return elm;
     25 }
     26 
     27 // Escapes a URI as appropriate for CSS.
     28 function encodeURIForCSS(uri) {
     29   // CSS uris need to have '(' and ')' escaped.
     30   return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)");
     31 }
     32 
     33 // TODO(glen): Get rid of these global references, replace with a controller
     34 //     or just make the classes own more of the page.
     35 var historyModel;
     36 var historyView;
     37 var localStrings;
     38 var pageState;
     39 var deleteQueue = [];
     40 var deleteInFlight = false;
     41 var selectionAnchor = -1;
     42 var id2checkbox = [];
     43 
     44 
     45 ///////////////////////////////////////////////////////////////////////////////
     46 // Page:
     47 /**
     48  * Class to hold all the information about an entry in our model.
     49  * @param {Object} result An object containing the page's data.
     50  * @param {boolean} continued Whether this page is on the same day as the
     51  *     page before it
     52  */
     53 function Page(result, continued, model, id) {
     54   this.model_ = model;
     55   this.title_ = result.title;
     56   this.url_ = result.url;
     57   this.starred_ = result.starred;
     58   this.snippet_ = result.snippet || "";
     59   this.id_ = id;
     60 
     61   this.changed = false;
     62 
     63   this.isRendered = false;
     64 
     65   // All the date information is public so that owners can compare properties of
     66   // two items easily.
     67 
     68   // We get the time in seconds, but we want it in milliseconds.
     69   this.time = new Date(result.time * 1000);
     70 
     71   // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
     72   // get all of these.
     73   this.dateRelativeDay = result.dateRelativeDay || "";
     74   this.dateTimeOfDay = result.dateTimeOfDay || "";
     75   this.dateShort = result.dateShort || "";
     76 
     77   // Whether this is the continuation of a previous day.
     78   this.continued = continued;
     79 }
     80 
     81 // Page, Public: --------------------------------------------------------------
     82 /**
     83  * @return {DOMObject} Gets the DOM representation of the page
     84  *     for use in browse results.
     85  */
     86 Page.prototype.getBrowseResultDOM = function() {
     87   var node = createElementWithClassName('div', 'entry');
     88   var time = createElementWithClassName('div', 'time');
     89   if (this.model_.getEditMode()) {
     90     var checkbox = document.createElement('input');
     91     checkbox.type = "checkbox";
     92     checkbox.name = this.id_;
     93     checkbox.time = this.time.toString();
     94     checkbox.addEventListener("click", checkboxClicked, false);
     95     id2checkbox[this.id_] = checkbox;
     96     time.appendChild(checkbox);
     97   }
     98   time.appendChild(document.createTextNode(this.dateTimeOfDay));
     99   node.appendChild(time);
    100   node.appendChild(this.getTitleDOM_());
    101   return node;
    102 };
    103 
    104 /**
    105  * @return {DOMObject} Gets the DOM representation of the page for
    106  *     use in search results.
    107  */
    108 Page.prototype.getSearchResultDOM = function() {
    109   var row = createElementWithClassName('tr', 'entry');
    110   var datecell = createElementWithClassName('td', 'time');
    111   datecell.appendChild(document.createTextNode(this.dateShort));
    112   row.appendChild(datecell);
    113   if (this.model_.getEditMode()) {
    114     var checkbox = document.createElement('input');
    115     checkbox.type = "checkbox";
    116     checkbox.name = this.id_;
    117     checkbox.time = this.time.toString();
    118     checkbox.addEventListener("click", checkboxClicked, false);
    119     id2checkbox[this.id_] = checkbox;
    120     datecell.appendChild(checkbox);
    121   }
    122 
    123   var titleCell = document.createElement('td');
    124   titleCell.valign = 'top';
    125   titleCell.appendChild(this.getTitleDOM_());
    126   var snippet = createElementWithClassName('div', 'snippet');
    127   this.addHighlightedText_(snippet,
    128                            this.snippet_,
    129                            this.model_.getSearchText());
    130   titleCell.appendChild(snippet);
    131   row.appendChild(titleCell);
    132 
    133   return row;
    134 };
    135 
    136 // Page, private: -------------------------------------------------------------
    137 /**
    138  * Add child text nodes to a node such that occurrences of the spcified text is
    139  * highligted.
    140  * @param {Node} node The node under which new text nodes will be made as
    141  *     children.
    142  * @param {string} content Text to be added beneath |node| as one or more
    143  *     text nodes.
    144  * @param {string} highlightText Occurences of this text inside |content| will
    145  *     be highlighted.
    146  */
    147 Page.prototype.addHighlightedText_ = function(node, content, highlightText) {
    148   var i = 0;
    149   if (highlightText) {
    150     var re = new RegExp(Page.pregQuote_(highlightText), 'gim');
    151     var match;
    152     while (match = re.exec(content)) {
    153       if (match.index > i)
    154         node.appendChild(document.createTextNode(content.slice(i,
    155                                                                match.index)));
    156       i = re.lastIndex;
    157       // Mark the highlighted text in bold.
    158       var b = document.createElement('b');
    159       b.textContent = content.substring(match.index, i);
    160       node.appendChild(b);
    161     }
    162   }
    163   if (i < content.length)
    164     node.appendChild(document.createTextNode(content.slice(i)));
    165 };
    166 
    167 /**
    168  * @return {DOMObject} DOM representation for the title block.
    169  */
    170 Page.prototype.getTitleDOM_ = function() {
    171   var node = document.createElement('div');
    172   node.className = 'title';
    173   var link = document.createElement('a');
    174   link.href = this.url_;
    175 
    176   link.style.backgroundImage =
    177       'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')';
    178   link.id = "id-" + this.id_;
    179   this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
    180 
    181   node.appendChild(link);
    182 
    183   if (this.starred_) {
    184     node.className += ' starred';
    185     node.appendChild(createElementWithClassName('div', 'starred'));
    186   }
    187 
    188   return node;
    189 };
    190 
    191 // Page, private, static: -----------------------------------------------------
    192 
    193 /**
    194  * Quote a string so it can be used in a regular expression.
    195  * @param {string} str The source string
    196  * @return {string} The escaped string
    197  */
    198 Page.pregQuote_ = function(str) {
    199   return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
    200 };
    201 
    202 ///////////////////////////////////////////////////////////////////////////////
    203 // HistoryModel:
    204 /**
    205  * Global container for history data. Future optimizations might include
    206  * allowing the creation of a HistoryModel for each search string, allowing
    207  * quick flips back and forth between results.
    208  *
    209  * The history model is based around pages, and only fetching the data to
    210  * fill the currently requested page. This is somewhat dependent on the view,
    211  * and so future work may wish to change history model to operate on
    212  * timeframe (day or week) based containers.
    213  */
    214 function HistoryModel() {
    215   this.clearModel_();
    216   this.setEditMode(false);
    217   this.view_;
    218 }
    219 
    220 // HistoryModel, Public: ------------------------------------------------------
    221 /**
    222  * Sets our current view that is called when the history model changes.
    223  * @param {HistoryView} view The view to set our current view to.
    224  */
    225 HistoryModel.prototype.setView = function(view) {
    226   this.view_ = view;
    227 };
    228 
    229 /**
    230  * Start a new search - this will clear out our model.
    231  * @param {String} searchText The text to search for
    232  * @param {Number} opt_page The page to view - this is mostly used when setting
    233  *     up an initial view, use #requestPage otherwise.
    234  */
    235 HistoryModel.prototype.setSearchText = function(searchText, opt_page) {
    236   this.clearModel_();
    237   this.searchText_ = searchText;
    238   this.requestedPage_ = opt_page ? opt_page : 0;
    239   this.getSearchResults_();
    240 };
    241 
    242 /**
    243  * Reload our model with the current parameters.
    244  */
    245 HistoryModel.prototype.reload = function() {
    246   var search = this.searchText_;
    247   var page = this.requestedPage_;
    248   this.clearModel_();
    249   this.searchText_ = search;
    250   this.requestedPage_ = page;
    251   this.getSearchResults_();
    252 };
    253 
    254 /**
    255  * @return {String} The current search text.
    256  */
    257 HistoryModel.prototype.getSearchText = function() {
    258   return this.searchText_;
    259 };
    260 
    261 /**
    262  * Tell the model that the view will want to see the current page. When
    263  * the data becomes available, the model will call the view back.
    264  * @page {Number} page The page we want to view.
    265  */
    266 HistoryModel.prototype.requestPage = function(page) {
    267   this.requestedPage_ = page;
    268   this.changed = true;
    269   this.updateSearch_(false);
    270 };
    271 
    272 /**
    273  * Receiver for history query.
    274  * @param {String} term The search term that the results are for.
    275  * @param {Array} results A list of results
    276  */
    277 HistoryModel.prototype.addResults = function(info, results) {
    278   this.inFlight_ = false;
    279   if (info.term != this.searchText_) {
    280     // If our results aren't for our current search term, they're rubbish.
    281     return;
    282   }
    283 
    284   // Currently we assume we're getting things in date order. This needs to
    285   // be updated if that ever changes.
    286   if (results) {
    287     var lastURL, lastDay;
    288     var oldLength = this.pages_.length;
    289     if (oldLength) {
    290       var oldPage = this.pages_[oldLength - 1];
    291       lastURL = oldPage.url;
    292       lastDay = oldPage.dateRelativeDay;
    293     }
    294 
    295     for (var i = 0, thisResult; thisResult = results[i]; i++) {
    296       var thisURL = thisResult.url;
    297       var thisDay = thisResult.dateRelativeDay;
    298 
    299       // Remove adjacent duplicates.
    300       if (!lastURL || lastURL != thisURL) {
    301         // Figure out if this page is in the same day as the previous page,
    302         // this is used to determine how day headers should be drawn.
    303         this.pages_.push(new Page(thisResult, thisDay == lastDay, this,
    304             this.last_id_++));
    305         lastDay = thisDay;
    306         lastURL = thisURL;
    307       }
    308     }
    309     if (results.length)
    310       this.changed = true;
    311   }
    312 
    313   this.updateSearch_(info.finished);
    314 };
    315 
    316 /**
    317  * @return {Number} The number of pages in the model.
    318  */
    319 HistoryModel.prototype.getSize = function() {
    320   return this.pages_.length;
    321 };
    322 
    323 /**
    324  * @return {boolean} Whether our history query has covered all of
    325  *     the user's history
    326  */
    327 HistoryModel.prototype.isComplete = function() {
    328   return this.complete_;
    329 };
    330 
    331 /**
    332  * Get a list of pages between specified index positions.
    333  * @param {Number} start The start index
    334  * @param {Number} end The end index
    335  * @return {Array} A list of pages
    336  */
    337 HistoryModel.prototype.getNumberedRange = function(start, end) {
    338   if (start >= this.getSize())
    339     return [];
    340 
    341   var end = end > this.getSize() ? this.getSize() : end;
    342   return this.pages_.slice(start, end);
    343 };
    344 
    345 /**
    346  * @return {boolean} Whether we are in edit mode where history items can be
    347  *    deleted
    348  */
    349 HistoryModel.prototype.getEditMode = function() {
    350   return this.editMode_;
    351 };
    352 
    353 /**
    354  * @param {boolean} edit_mode Control whether we are in edit mode.
    355  */
    356 HistoryModel.prototype.setEditMode = function(edit_mode) {
    357   this.editMode_ = edit_mode;
    358 };
    359 
    360 // HistoryModel, Private: -----------------------------------------------------
    361 HistoryModel.prototype.clearModel_ = function() {
    362   this.inFlight_ = false; // Whether a query is inflight.
    363   this.searchText_ = '';
    364   this.searchDepth_ = 0;
    365   this.pages_ = []; // Date-sorted list of pages.
    366   this.last_id_ = 0;
    367   selectionAnchor = -1;
    368   id2checkbox = [];
    369 
    370   // The page that the view wants to see - we only fetch slightly past this
    371   // point. If the view requests a page that we don't have data for, we try
    372   // to fetch it and call back when we're done.
    373   this.requestedPage_ = 0;
    374 
    375   this.complete_ = false;
    376 
    377   if (this.view_) {
    378     this.view_.clear_();
    379   }
    380 };
    381 
    382 /**
    383  * Figure out if we need to do more searches to fill the currently requested
    384  * page. If we think we can fill the page, call the view and let it know
    385  * we're ready to show something.
    386  */
    387 HistoryModel.prototype.updateSearch_ = function(finished) {
    388   if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) ||
    389       finished) {
    390     // We have maxed out. There will be no more data.
    391     this.complete_ = true;
    392     this.view_.onModelReady();
    393     this.changed = false;
    394   } else {
    395     // If we can't fill the requested page, ask for more data unless a request
    396     // is still in-flight.
    397     if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) {
    398       this.getSearchResults_(this.searchDepth_ + 1);
    399     }
    400 
    401     // If we have any data for the requested page, show it.
    402     if (this.changed && this.haveDataForPage_(this.requestedPage_)) {
    403       this.view_.onModelReady();
    404       this.changed = false;
    405     }
    406   }
    407 };
    408 
    409 /**
    410  * Get search results for a selected depth. Our history system is optimized
    411  * for queries that don't cross month boundaries, but an entire month's
    412  * worth of data is huge. When we're in browse mode (searchText is empty)
    413  * we request the data a day at a time. When we're searching, a month is
    414  * used.
    415  *
    416  * TODO: Fix this for when the user's clock goes across month boundaries.
    417  * @param {number} opt_day How many days back to do the search.
    418  */
    419 HistoryModel.prototype.getSearchResults_ = function(depth) {
    420   this.searchDepth_ = depth || 0;
    421 
    422   if (this.searchText_ == "") {
    423     chrome.send('getHistory',
    424         [String(this.searchDepth_)]);
    425   } else {
    426     chrome.send('searchHistory',
    427         [this.searchText_, String(this.searchDepth_)]);
    428   }
    429 
    430   this.inFlight_ = true;
    431 };
    432 
    433 /**
    434  * Check to see if we have data for a given page.
    435  * @param {number} page The page number
    436  * @return {boolean} Whether we have any data for the given page.
    437  */
    438 HistoryModel.prototype.haveDataForPage_ = function(page) {
    439   return (page * RESULTS_PER_PAGE < this.getSize());
    440 };
    441 
    442 /**
    443  * Check to see if we have data to fill a page.
    444  * @param {number} page The page number.
    445  * @return {boolean} Whether we have data to fill the page.
    446  */
    447 HistoryModel.prototype.canFillPage_ = function(page) {
    448   return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
    449 };
    450 
    451 ///////////////////////////////////////////////////////////////////////////////
    452 // HistoryView:
    453 /**
    454  * Functions and state for populating the page with HTML. This should one-day
    455  * contain the view and use event handlers, rather than pushing HTML out and
    456  * getting called externally.
    457  * @param {HistoryModel} model The model backing this view.
    458  */
    459 function HistoryView(model) {
    460   this.summaryTd_ = $('results-summary');
    461   this.summaryTd_.textContent = localStrings.getString('loading');
    462   this.editButtonTd_ = $('edit-button');
    463   this.editingControlsDiv_ = $('editing-controls');
    464   this.resultDiv_ = $('results-display');
    465   this.pageDiv_ = $('results-pagination');
    466   this.model_ = model
    467   this.pageIndex_ = 0;
    468   this.lastDisplayed_ = [];
    469 
    470   this.model_.setView(this);
    471 
    472   this.currentPages_ = [];
    473 
    474   var self = this;
    475   window.onresize = function() {
    476     self.updateEntryAnchorWidth_();
    477   };
    478   self.updateEditControls_();
    479 
    480   this.boundUpdateRemoveButton_ = function(e) {
    481     return self.updateRemoveButton_(e);
    482   };
    483 }
    484 
    485 // HistoryView, public: -------------------------------------------------------
    486 /**
    487  * Do a search and optionally view a certain page.
    488  * @param {string} term The string to search for.
    489  * @param {number} opt_page The page we wish to view, only use this for
    490  *     setting up initial views, as this triggers a search.
    491  */
    492 HistoryView.prototype.setSearch = function(term, opt_page) {
    493   this.pageIndex_ = parseInt(opt_page || 0, 10);
    494   window.scrollTo(0, 0);
    495   this.model_.setSearchText(term, this.pageIndex_);
    496   this.updateEditControls_();
    497   pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_);
    498 };
    499 
    500 /**
    501  * Controls edit mode where history can be deleted.
    502  * @param {boolean} edit_mode Whether to enable edit mode.
    503  */
    504 HistoryView.prototype.setEditMode = function(edit_mode) {
    505   this.model_.setEditMode(edit_mode);
    506   pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(),
    507                        this.pageIndex_);
    508 };
    509 
    510 /**
    511  * Toggles the edit mode and triggers UI update.
    512  */
    513 HistoryView.prototype.toggleEditMode = function() {
    514   var editMode = !this.model_.getEditMode();
    515   this.setEditMode(editMode);
    516   this.updateEditControls_();
    517 };
    518 
    519 /**
    520  * Reload the current view.
    521  */
    522 HistoryView.prototype.reload = function() {
    523   this.model_.reload();
    524 };
    525 
    526 /**
    527  * Switch to a specified page.
    528  * @param {number} page The page we wish to view.
    529  */
    530 HistoryView.prototype.setPage = function(page) {
    531   this.clear_();
    532   this.pageIndex_ = parseInt(page, 10);
    533   window.scrollTo(0, 0);
    534   this.model_.requestPage(page);
    535   pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(),
    536       this.pageIndex_);
    537 };
    538 
    539 /**
    540  * @return {number} The page number being viewed.
    541  */
    542 HistoryView.prototype.getPage = function() {
    543   return this.pageIndex_;
    544 };
    545 
    546 /**
    547  * Callback for the history model to let it know that it has data ready for us
    548  * to view.
    549  */
    550 HistoryView.prototype.onModelReady = function() {
    551   this.displayResults_();
    552 };
    553 
    554 // HistoryView, private: ------------------------------------------------------
    555 /**
    556  * Clear the results in the view.  Since we add results piecemeal, we need
    557  * to clear them out when we switch to a new page or reload.
    558  */
    559 HistoryView.prototype.clear_ = function() {
    560   this.resultDiv_.textContent = '';
    561 
    562   var pages = this.currentPages_;
    563   for (var i = 0; i < pages.length; i++) {
    564     pages[i].isRendered = false;
    565   }
    566   this.currentPages_ = [];
    567 };
    568 
    569 HistoryView.prototype.setPageRendered_ = function(page) {
    570   page.isRendered = true;
    571   this.currentPages_.push(page);
    572 };
    573 
    574 /**
    575  * Update the page with results.
    576  */
    577 HistoryView.prototype.displayResults_ = function() {
    578   var results = this.model_.getNumberedRange(
    579       this.pageIndex_ * RESULTS_PER_PAGE,
    580       this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE);
    581 
    582   if (this.model_.getSearchText()) {
    583     var resultTable = createElementWithClassName('table', 'results');
    584     resultTable.cellSpacing = 0;
    585     resultTable.cellPadding = 0;
    586     resultTable.border = 0;
    587 
    588     for (var i = 0, page; page = results[i]; i++) {
    589       if (!page.isRendered) {
    590         resultTable.appendChild(page.getSearchResultDOM());
    591         this.setPageRendered_(page);
    592       }
    593     }
    594     this.resultDiv_.appendChild(resultTable);
    595   } else {
    596     var lastTime = Math.infinity;
    597     for (var i = 0, page; page = results[i]; i++) {
    598       if (page.isRendered) {
    599         continue;
    600       }
    601       // Break across day boundaries and insert gaps for browsing pauses.
    602       var thisTime = page.time.getTime();
    603 
    604       if ((i == 0 && page.continued) || !page.continued) {
    605         var day = createElementWithClassName('div', 'day');
    606         day.appendChild(document.createTextNode(page.dateRelativeDay));
    607 
    608         if (i == 0 && page.continued) {
    609           day.appendChild(document.createTextNode(' ' +
    610               localStrings.getString('cont')));
    611         }
    612 
    613         this.resultDiv_.appendChild(day);
    614       } else if (lastTime - thisTime > BROWSING_GAP_TIME) {
    615         this.resultDiv_.appendChild(createElementWithClassName('div', 'gap'));
    616       }
    617       lastTime = thisTime;
    618 
    619       // Add entry.
    620       this.resultDiv_.appendChild(page.getBrowseResultDOM());
    621       this.setPageRendered_(page);
    622     }
    623   }
    624 
    625   this.displaySummaryBar_();
    626   this.displayNavBar_();
    627   this.updateEntryAnchorWidth_();
    628 };
    629 
    630 /**
    631  * Update the summary bar with descriptive text.
    632  */
    633 HistoryView.prototype.displaySummaryBar_ = function() {
    634   var searchText = this.model_.getSearchText();
    635   if (searchText != '') {
    636     this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor',
    637         searchText);
    638   } else {
    639     this.summaryTd_.textContent = localStrings.getString('history');
    640   }
    641 };
    642 
    643 /**
    644  * Update the widgets related to edit mode.
    645  */
    646 HistoryView.prototype.updateEditControls_ = function() {
    647   // Display a button (looking like a link) to enable/disable edit mode.
    648   var oldButton = this.editButtonTd_.firstChild;
    649   var editMode = this.model_.getEditMode();
    650   var button = createElementWithClassName('button', 'edit-button');
    651   button.onclick = toggleEditMode;
    652   button.textContent = localStrings.getString(editMode ?
    653                                               'doneediting' : 'edithistory');
    654   this.editButtonTd_.replaceChild(button, oldButton);
    655 
    656   this.editingControlsDiv_.textContent = '';
    657 
    658   if (editMode) {
    659     // Button to delete the selected items.
    660     button = document.createElement('button');
    661     button.onclick = removeItems;
    662     button.textContent = localStrings.getString('removeselected');
    663     button.disabled = true;
    664     this.editingControlsDiv_.appendChild(button);
    665     this.removeButton_ = button;
    666 
    667     // Button that opens up the clear browsing data dialog.
    668     button = document.createElement('button');
    669     button.onclick = openClearBrowsingData;
    670     button.textContent = localStrings.getString('clearallhistory');
    671     this.editingControlsDiv_.appendChild(button);
    672 
    673     // Listen for clicks in the page to sync the disabled state.
    674     document.addEventListener('click', this.boundUpdateRemoveButton_);
    675   } else {
    676     this.removeButton_ = null;
    677     document.removeEventListener('click', this.boundUpdateRemoveButton_);
    678   }
    679 };
    680 
    681 /**
    682  * Updates the disabled state of the remove button when in editing mode.
    683  * @param {!Event} e The click event object.
    684  * @private
    685  */
    686 HistoryView.prototype.updateRemoveButton_ = function(e) {
    687   if (e.target.tagName != 'INPUT')
    688     return;
    689 
    690   var anyChecked = document.querySelector('.entry input:checked') != null;
    691   if (this.removeButton_)
    692     this.removeButton_.disabled = !anyChecked;
    693 };
    694 
    695 /**
    696  * Update the pagination tools.
    697  */
    698 HistoryView.prototype.displayNavBar_ = function() {
    699   this.pageDiv_.textContent = '';
    700 
    701   if (this.pageIndex_ > 0) {
    702     this.pageDiv_.appendChild(
    703         this.createPageNav_(0, localStrings.getString('newest')));
    704     this.pageDiv_.appendChild(
    705         this.createPageNav_(this.pageIndex_ - 1,
    706                             localStrings.getString('newer')));
    707   }
    708 
    709   // TODO(feldstein): this causes the navbar to not show up when your first
    710   // page has the exact amount of results as RESULTS_PER_PAGE.
    711   if (this.model_.getSize() > (this.pageIndex_ + 1) * RESULTS_PER_PAGE) {
    712     this.pageDiv_.appendChild(
    713         this.createPageNav_(this.pageIndex_ + 1,
    714                             localStrings.getString('older')));
    715   }
    716 };
    717 
    718 /**
    719  * Make a DOM object representation of a page navigation link.
    720  * @param {number} page The page index the navigation element should link to
    721  * @param {string} name The text content of the link
    722  * @return {HTMLAnchorElement} the pagination link
    723  */
    724 HistoryView.prototype.createPageNav_ = function(page, name) {
    725   anchor = document.createElement('a');
    726   anchor.className = 'page-navigation';
    727   anchor.textContent = name;
    728   var hashString = PageState.getHashString(this.model_.getEditMode(),
    729                                            this.model_.getSearchText(), page);
    730   var link = 'chrome://history/' + (hashString ? '#' + hashString : '');
    731   anchor.href = link;
    732   anchor.onclick = function() {
    733     setPage(page);
    734     return false;
    735   };
    736   return anchor;
    737 };
    738 
    739 /**
    740  * Updates the CSS rule for the entry anchor.
    741  * @private
    742  */
    743 HistoryView.prototype.updateEntryAnchorWidth_ = function() {
    744   // We need to have at least on .title div to be able to calculate the
    745   // desired width of the anchor.
    746   var titleElement = document.querySelector('.entry .title');
    747   if (!titleElement)
    748     return;
    749 
    750   // Create new CSS rules and add them last to the last stylesheet.
    751   if (!this.entryAnchorRule_) {
    752      var styleSheets = document.styleSheets;
    753      var styleSheet = styleSheets[styleSheets.length - 1];
    754      var rules = styleSheet.cssRules;
    755      var createRule = function(selector) {
    756        styleSheet.insertRule(selector + '{}', rules.length);
    757        return rules[rules.length - 1];
    758      };
    759      this.entryAnchorRule_ = createRule('.entry .title > a');
    760      // The following rule needs to be more specific to have higher priority.
    761      this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a');
    762    }
    763 
    764    var anchorMaxWith = titleElement.offsetWidth;
    765    this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px';
    766    // Adjust by the width of star plus its margin.
    767    this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px';
    768 };
    769 
    770 ///////////////////////////////////////////////////////////////////////////////
    771 // State object:
    772 /**
    773  * An 'AJAX-history' implementation.
    774  * @param {HistoryModel} model The model we're representing
    775  * @param {HistoryView} view The view we're representing
    776  */
    777 function PageState(model, view) {
    778   // Enforce a singleton.
    779   if (PageState.instance) {
    780     return PageState.instance;
    781   }
    782 
    783   this.model = model;
    784   this.view = view;
    785 
    786   if (typeof this.checker_ != 'undefined' && this.checker_) {
    787     clearInterval(this.checker_);
    788   }
    789 
    790   // TODO(glen): Replace this with a bound method so we don't need
    791   //     public model and view.
    792   this.checker_ = setInterval((function(state_obj) {
    793     var hashData = state_obj.getHashData();
    794 
    795     if (hashData.q != state_obj.model.getSearchText(term)) {
    796       state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10));
    797     } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) {
    798       state_obj.view.setPage(hashData.p);
    799     }
    800   }), 50, this);
    801 }
    802 
    803 PageState.instance = null;
    804 
    805 /**
    806  * @return {Object} An object containing parameters from our window hash.
    807  */
    808 PageState.prototype.getHashData = function() {
    809   var result = {
    810     e : 0,
    811     q : '',
    812     p : 0
    813   };
    814 
    815   if (!window.location.hash) {
    816     return result;
    817   }
    818 
    819   var hashSplit = window.location.hash.substr(1).split('&');
    820   for (var i = 0; i < hashSplit.length; i++) {
    821     var pair = hashSplit[i].split('=');
    822     if (pair.length > 1) {
    823       result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
    824     }
    825   }
    826 
    827   return result;
    828 };
    829 
    830 /**
    831  * Set the hash to a specified state, this will create an entry in the
    832  * session history so the back button cycles through hash states, which
    833  * are then picked up by our listener.
    834  * @param {string} term The current search string.
    835  * @param {string} page The page currently being viewed.
    836  */
    837 PageState.prototype.setUIState = function(editMode, term, page) {
    838   // Make sure the form looks pretty.
    839   document.forms[0].term.value = term;
    840   var currentHash = this.getHashData();
    841   if (Boolean(currentHash.e) != editMode || currentHash.q != term ||
    842       currentHash.p != page) {
    843     window.location.hash = PageState.getHashString(editMode, term, page);
    844   }
    845 };
    846 
    847 /**
    848  * Static method to get the hash string for a specified state
    849  * @param {string} term The current search string.
    850  * @param {string} page The page currently being viewed.
    851  * @return {string} The string to be used in a hash.
    852  */
    853 PageState.getHashString = function(editMode, term, page) {
    854   var newHash = [];
    855   if (editMode) {
    856     newHash.push('e=1');
    857   }
    858   if (term) {
    859     newHash.push('q=' + encodeURIComponent(term));
    860   }
    861   if (page != undefined) {
    862     newHash.push('p=' + page);
    863   }
    864 
    865   return newHash.join('&');
    866 };
    867 
    868 ///////////////////////////////////////////////////////////////////////////////
    869 // Document Functions:
    870 /**
    871  * Window onload handler, sets up the page.
    872  */
    873 function load() {
    874   $('term').focus();
    875 
    876   localStrings = new LocalStrings();
    877   historyModel = new HistoryModel();
    878   historyView = new HistoryView(historyModel);
    879   pageState = new PageState(historyModel, historyView);
    880 
    881   // Create default view.
    882   var hashData = pageState.getHashData();
    883   if (Boolean(hashData.e)) {
    884     historyView.toggleEditMode();
    885   }
    886   historyView.setSearch(hashData.q, hashData.p);
    887 }
    888 
    889 /**
    890  * TODO(glen): Get rid of this function.
    891  * Set the history view to a specified page.
    892  * @param {String} term The string to search for
    893  */
    894 function setSearch(term) {
    895   if (historyView) {
    896     historyView.setSearch(term);
    897   }
    898 }
    899 
    900 /**
    901  * TODO(glen): Get rid of this function.
    902  * Set the history view to a specified page.
    903  * @param {number} page The page to set the view to.
    904  */
    905 function setPage(page) {
    906   if (historyView) {
    907     historyView.setPage(page);
    908   }
    909 }
    910 
    911 /**
    912  * TODO(glen): Get rid of this function.
    913  * Toggles edit mode.
    914  */
    915 function toggleEditMode() {
    916   if (historyView) {
    917     historyView.toggleEditMode();
    918     historyView.reload();
    919   }
    920 }
    921 
    922 /**
    923  * Delete the next item in our deletion queue.
    924  */
    925 function deleteNextInQueue() {
    926   if (!deleteInFlight && deleteQueue.length) {
    927     deleteInFlight = true;
    928     chrome.send('removeURLsOnOneDay',
    929                 [String(deleteQueue[0])].concat(deleteQueue[1]));
    930   }
    931 }
    932 
    933 /**
    934  * Open the clear browsing data dialog.
    935  */
    936 function openClearBrowsingData() {
    937   chrome.send('clearBrowsingData', []);
    938   return false;
    939 }
    940 
    941 /**
    942  * Collect IDs from checked checkboxes and send to Chrome for deletion.
    943  */
    944 function removeItems() {
    945   var checkboxes = document.getElementsByTagName('input');
    946   var ids = [];
    947   var disabledItems = [];
    948   var queue = [];
    949   var date = new Date();
    950   for (var i = 0; i < checkboxes.length; i++) {
    951     if (checkboxes[i].type == 'checkbox' && checkboxes[i].checked &&
    952         !checkboxes[i].disabled) {
    953       var cbDate = new Date(checkboxes[i].time);
    954       if (date.getFullYear() != cbDate.getFullYear() ||
    955           date.getMonth() != cbDate.getMonth() ||
    956           date.getDate() != cbDate.getDate()) {
    957         if (ids.length > 0) {
    958           queue.push(date.valueOf() / 1000);
    959           queue.push(ids);
    960         }
    961         ids = [];
    962         date = cbDate;
    963       }
    964       var link = $('id-' + checkboxes[i].name);
    965       checkboxes[i].disabled = true;
    966       link.style.textDecoration = 'line-through';
    967       disabledItems.push(checkboxes[i]);
    968       ids.push(link.href);
    969     }
    970   }
    971   if (ids.length > 0) {
    972     queue.push(date.valueOf() / 1000);
    973     queue.push(ids);
    974   }
    975   if (queue.length > 0) {
    976     if (confirm(localStrings.getString('deletewarning'))) {
    977       deleteQueue = deleteQueue.concat(queue);
    978       deleteNextInQueue();
    979     } else {
    980       // If the remove is cancelled, return the checkboxes to their
    981       // enabled, non-line-through state.
    982       for (var i = 0; i < disabledItems.length; i++) {
    983         var link = $('id-' + disabledItems[i].name);
    984         disabledItems[i].disabled = false;
    985         link.style.textDecoration = '';
    986       }
    987     }
    988   }
    989   return false;
    990 }
    991 
    992 /**
    993  * Toggle state of checkbox and handle Shift modifier.
    994  */
    995 function checkboxClicked(event) {
    996   if (event.shiftKey && (selectionAnchor != -1)) {
    997     var checked = this.checked;
    998     // Set all checkboxes from the anchor up to the clicked checkbox to the
    999     // state of the clicked one.
   1000     var begin = Math.min(this.name, selectionAnchor);
   1001     var end = Math.max(this.name, selectionAnchor);
   1002     for (var i = begin; i <= end; i++) {
   1003       id2checkbox[i].checked = checked;
   1004     }
   1005   }
   1006   selectionAnchor = this.name;
   1007   this.focus();
   1008 }
   1009 
   1010 ///////////////////////////////////////////////////////////////////////////////
   1011 // Chrome callbacks:
   1012 /**
   1013  * Our history system calls this function with results from searches.
   1014  */
   1015 function historyResult(info, results) {
   1016   historyModel.addResults(info, results);
   1017 }
   1018 
   1019 /**
   1020  * Our history system calls this function when a deletion has finished.
   1021  */
   1022 function deleteComplete() {
   1023   window.console.log('Delete complete');
   1024   deleteInFlight = false;
   1025   if (deleteQueue.length > 2) {
   1026     deleteQueue = deleteQueue.slice(2);
   1027     deleteNextInQueue();
   1028   } else {
   1029     deleteQueue = [];
   1030     historyView.reload();
   1031   }
   1032 }
   1033 
   1034 /**
   1035  * Our history system calls this function if a delete is not ready (e.g.
   1036  * another delete is in-progress).
   1037  */
   1038 function deleteFailed() {
   1039   window.console.log('Delete failed');
   1040   // The deletion failed - try again later.
   1041   deleteInFlight = false;
   1042   setTimeout(deleteNextInQueue, 500);
   1043 }
   1044 </script>
   1045 <link rel="stylesheet" href="webui.css">
   1046 <style>
   1047 #results-separator {
   1048   margin-top:12px;
   1049   border-top:1px solid #9cc2ef;
   1050   background-color:#ebeff9;
   1051   font-weight:bold;
   1052   padding:3px;
   1053   margin-bottom:-8px;
   1054 }
   1055 #results-separator table {
   1056   width: 100%;
   1057 }
   1058 #results-summary {
   1059   overflow: hidden;
   1060   white-space: nowrap;
   1061   text-overflow: ellipsis;
   1062   width: 50%;
   1063 }
   1064 #edit-button {
   1065   text-align: end;
   1066   overflow: hidden;
   1067   white-space: nowrap;
   1068   text-overflow: ellipsis;
   1069   width: 50%;
   1070 }
   1071 #editing-controls button {
   1072   margin-top: 18px;
   1073   margin-bottom: -8px;
   1074 }
   1075 #results-display {
   1076   max-width:740px;
   1077 }
   1078 .day {
   1079   margin-top:18px;
   1080   padding:0px 3px;
   1081   display:inline-block;
   1082 }
   1083 .edit-button {
   1084   display: inline;
   1085   -webkit-appearance: none;
   1086   background: none;
   1087   border: 0;
   1088   color: blue; /* -webkit-link makes it purple :'( */
   1089   cursor: pointer;
   1090   text-decoration: underline;
   1091   padding:0px 9px;
   1092   display:inline-block;
   1093   font:inherit;
   1094 }
   1095 .gap {
   1096   margin-left:18px;
   1097   width:15px;
   1098   border-right:1px solid #ddd;
   1099   height:14px;
   1100 }
   1101 .entry {
   1102   margin-left:18px;
   1103   margin-top:6px;
   1104   overflow:auto;
   1105 }
   1106 table.results {
   1107   margin-left:4px;
   1108 }
   1109 .entry .time {
   1110   color:#888;
   1111   float:left;
   1112   min-width:56px;
   1113   -webkit-margin-end:5px;
   1114   padding-top:1px;
   1115   white-space:nowrap;
   1116 }
   1117 html[dir='rtl'] .time {
   1118   float:right;
   1119 }
   1120 .entry .title {
   1121   max-width:600px;
   1122   overflow: hidden;
   1123   white-space: nowrap;
   1124   text-overflow: ellipsis;
   1125 }
   1126 .results .time, .results .title {
   1127   margin-top:18px;
   1128 }
   1129 .title > .starred {
   1130   background:url('shared/images/star_small.png');
   1131   background-repeat:no-repeat;
   1132   display:inline-block;
   1133   -webkit-margin-start:12px;
   1134   -webkit-margin-end:0;
   1135   width:11px;
   1136   height:11px;
   1137 }
   1138 .entry .title > a {
   1139   box-sizing: border-box;
   1140   background-repeat:no-repeat;
   1141   background-size:16px;
   1142   background-position:0px 1px;
   1143   padding:1px 0px 4px 22px;
   1144   display:inline-block;
   1145   overflow:hidden;
   1146   text-overflow:ellipsis;
   1147 }
   1148 html[dir='rtl'] .entry .title > a {
   1149   background-position-x:right;
   1150   padding-left:0px;
   1151   padding-right:22px;
   1152 }
   1153 #results-pagination {
   1154   padding-top:24px;
   1155   -webkit-margin-start:18px;
   1156 }
   1157 
   1158 </style>
   1159 </head>
   1160 <body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
   1161 <div class="header">
   1162   <a href="" onclick="setSearch(''); return false;">
   1163     <img src="shared/images/history_section.png"
   1164          width="67" height="67" class="logo" border="0"></a>
   1165   <form method="post" action=""
   1166       onsubmit="setSearch(this.term.value); return false;"
   1167       class="form">
   1168     <input type="text" name="term" id="term">
   1169     <input type="submit" name="submit" i18n-values="value:searchbutton">
   1170   </form>
   1171 </div>
   1172 <div class="main">
   1173   <div id="results-separator">
   1174     <table border="0" cellPadding="0" cellSpacing="0">
   1175       <tr><td id="results-summary"></td><td id="edit-button"><p></p></td></tr>
   1176     </table>
   1177   </div>
   1178   <div id="editing-controls"></div>
   1179   <div id="results-display"></div>
   1180   <div id="results-pagination"></div>
   1181 </div>
   1182 <div class="footer">
   1183 </div>
   1184 </body>
   1185 </html>
   1186