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