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