• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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:
11var RESULTS_PER_PAGE = 150;
12var MAX_SEARCH_DEPTH_MONTHS = 18;
13
14// Amount of time between pageviews that we consider a 'break' in browsing,
15// measured in milliseconds.
16var BROWSING_GAP_TIME = 15 * 60 * 1000;
17
18function $(o) {return document.getElementById(o);}
19
20function 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.
27function 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.
34var historyModel;
35var historyView;
36var localStrings;
37var pageState;
38var deleteQueue = [];
39var deleteInFlight = false;
40var selectionAnchor = -1;
41var 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 */
52function 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 */
87Page.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 */
126Page.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 */
137Page.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 */
155Page.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 */
178Page.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 */
209Page.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 */
225function 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 */
236HistoryModel.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 */
246HistoryModel.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 */
256HistoryModel.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 */
268HistoryModel.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 */
277HistoryModel.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 */
288HistoryModel.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 */
330HistoryModel.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 */
338HistoryModel.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 */
348HistoryModel.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 */
360HistoryModel.prototype.getEditMode = function() {
361  return this.editMode_;
362};
363
364/**
365 * @param {boolean} edit_mode Control whether we are in edit mode.
366 */
367HistoryModel.prototype.setEditMode = function(edit_mode) {
368  this.editMode_ = edit_mode;
369};
370
371// HistoryModel, Private: -----------------------------------------------------
372HistoryModel.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 */
398HistoryModel.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 */
430HistoryModel.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 */
449HistoryModel.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 */
458HistoryModel.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 */
470function 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 */
503HistoryView.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 */
518HistoryView.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 */
527HistoryView.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 */
536HistoryView.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 */
544HistoryView.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 */
556HistoryView.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 */
564HistoryView.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 */
573HistoryView.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
583HistoryView.prototype.setPageRendered_ = function(page) {
584  page.isRendered = true;
585  this.currentPages_.push(page);
586};
587
588/**
589 * Update the page with results.
590 */
591HistoryView.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 */
658HistoryView.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 */
671HistoryView.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 */
717HistoryView.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 */
729HistoryView.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 */
755HistoryView.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 */
774HistoryView.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 */
809function 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
835PageState.instance = null;
836
837/**
838 * @return {Object} An object containing parameters from our window hash.
839 */
840PageState.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 */
869PageState.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 */
885PageState.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 */
905function 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 */
926function 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 */
937function 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 */
947function toggleEditMode() {
948  if (historyView) {
949    historyView.toggleEditMode();
950    historyView.reload();
951  }
952}
953
954/**
955 * Delete the next item in our deletion queue.
956 */
957function 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 */
968function openClearBrowsingData() {
969  chrome.send('clearBrowsingData', []);
970  return false;
971}
972
973/**
974 * Collect IDs from checked checkboxes and send to Chrome for deletion.
975 */
976function 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 */
1027function 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 */
1047function historyResult(info, results) {
1048  historyModel.addResults(info, results);
1049}
1050
1051/**
1052 * Our history system calls this function when a deletion has finished.
1053 */
1054function 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 */
1070function 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}
1164html[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}
1175html[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 */
1188html[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