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