1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src="../uber/uber_utils.js">
6 <include src="history_focus_manager.js">
8 ///////////////////////////////////////////////////////////////////////////////
10 /** @const */ var RESULTS_PER_PAGE = 150;
12 // Amount of time between pageviews that we consider a 'break' in browsing,
13 // measured in milliseconds.
14 /** @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000;
16 // The largest bucket value for UMA histogram, based on entry ID. All entries
17 // with IDs greater than this will be included in this bucket.
18 /** @const */ var UMA_MAX_BUCKET_VALUE = 1000;
20 // The largest bucket value for a UMA histogram that is a subset of above.
21 /** @const */ var UMA_MAX_SUBSET_BUCKET_VALUE = 100;
23 // TODO(glen): Get rid of these global references, replace with a controller
24 // or just make the classes own more of the page.
28 var selectionAnchor = -1;
29 var activeVisit = null;
31 /** @const */ var Command = cr.ui.Command;
32 /** @const */ var Menu = cr.ui.Menu;
33 /** @const */ var MenuButton = cr.ui.MenuButton;
36 * Enum that shows the filtering behavior for a host or URL to a managed user.
37 * Must behave like the FilteringBehavior enum from managed_mode_url_filter.h.
40 ManagedModeFilteringBehavior = {
46 MenuButton.createDropDownArrows();
49 * Returns true if the mobile (non-desktop) version is being shown.
50 * @return {boolean} true if the mobile version is being shown.
52 function isMobileVersion() {
53 return !document.body.classList.contains('uber-frame');
57 * Record an action in UMA.
58 * @param {string} actionDesc The name of the action to be logged.
60 function recordUmaAction(actionDesc) {
61 chrome.send('metricsHandler:recordAction', [actionDesc]);
65 * Record a histogram value in UMA. If specified value is larger than the max
66 * bucket value, record the value in the largest bucket.
67 * @param {string} histogram The name of the histogram to be recorded in.
68 * @param {integer} maxBucketValue The max value for the last histogram bucket.
69 * @param {integer} value The value to record in the histogram.
72 function recordUmaHistogram(histogram, maxBucketValue, value) {
73 chrome.send('metricsHandler:recordInHistogram',
75 ((value > maxBucketValue) ? maxBucketValue : value),
79 ///////////////////////////////////////////////////////////////////////////////
83 * Class to hold all the information about an entry in our model.
84 * @param {Object} result An object containing the visit's data.
85 * @param {boolean} continued Whether this visit is on the same day as the
87 * @param {HistoryModel} model The model object this entry belongs to.
90 function Visit(result, continued, model) {
92 this.title_ = result.title;
93 this.url_ = result.url;
94 this.domain_ = result.domain;
95 this.starred_ = result.starred;
97 // These identify the name and type of the device on which this visit
98 // occurred. They will be empty if the visit occurred on the current device.
99 this.deviceName = result.deviceName;
100 this.deviceType = result.deviceType;
102 // The ID will be set according to when the visit was displayed, not
103 // received. Set to -1 to show that it has not been set yet.
106 this.isRendered = false; // Has the visit already been rendered on the page?
108 // All the date information is public so that owners can compare properties of
111 this.date = new Date(result.time);
113 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
115 this.dateRelativeDay = result.dateRelativeDay || '';
116 this.dateTimeOfDay = result.dateTimeOfDay || '';
117 this.dateShort = result.dateShort || '';
119 // Shows the filtering behavior for that host (only used for managed users).
120 // A value of |ManagedModeFilteringBehavior.ALLOW| is not displayed so it is
121 // used as the default value.
122 this.hostFilteringBehavior = ManagedModeFilteringBehavior.ALLOW;
123 if (typeof result.hostFilteringBehavior != 'undefined')
124 this.hostFilteringBehavior = result.hostFilteringBehavior;
126 this.blockedVisit = result.blockedVisit || false;
128 // Whether this is the continuation of a previous day.
129 this.continued = continued;
131 this.allTimestamps = result.allTimestamps;
134 // Visit, public: -------------------------------------------------------------
137 * Returns a dom structure for a browse page result or a search page result.
138 * @param {Object} propertyBag A bag of configuration properties, false by
140 * - isSearchResult: Whether or not the result is a search result.
141 * - addTitleFavicon: Whether or not the favicon should be added.
142 * - useMonthDate: Whether or not the full date should be inserted (used for
144 * @return {Node} A DOM node to represent the history entry or search result.
146 Visit.prototype.getResultDOM = function(propertyBag) {
147 var isSearchResult = propertyBag.isSearchResult || false;
148 var addTitleFavicon = propertyBag.addTitleFavicon || false;
149 var useMonthDate = propertyBag.useMonthDate || false;
150 var node = createElementWithClassName('li', 'entry');
151 var time = createElementWithClassName('div', 'time');
152 var entryBox = createElementWithClassName('label', 'entry-box');
153 var domain = createElementWithClassName('div', 'domain');
155 this.id_ = this.model_.nextVisitId_++;
157 // Only create the checkbox if it can be used either to delete an entry or to
159 if (this.model_.editingEntriesAllowed) {
160 var checkbox = document.createElement('input');
161 checkbox.type = 'checkbox';
162 checkbox.id = 'checkbox-' + this.id_;
163 checkbox.time = this.date.getTime();
164 checkbox.addEventListener('click', checkboxClicked);
165 entryBox.appendChild(checkbox);
167 // Clicking anywhere in the entryBox will check/uncheck the checkbox.
168 entryBox.setAttribute('for', checkbox.id);
169 entryBox.addEventListener('mousedown', entryBoxMousedown);
170 entryBox.addEventListener('click', entryBoxClick);
173 // Keep track of the drop down that triggered the menu, so we know
174 // which element to apply the command to.
175 // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
177 var setActiveVisit = function(e) {
179 var menu = $('action-menu');
180 menu.dataset.devicename = self.deviceName;
181 menu.dataset.devicetype = self.deviceType;
183 domain.textContent = this.domain_;
185 entryBox.appendChild(time);
187 var bookmarkSection = createElementWithClassName('div', 'bookmark-section');
189 bookmarkSection.classList.add('starred');
190 bookmarkSection.addEventListener('click', function f(e) {
191 recordUmaAction('HistoryPage_BookmarkStarClicked');
192 bookmarkSection.classList.remove('starred');
193 chrome.send('removeBookmark', [self.url_]);
194 bookmarkSection.removeEventListener('click', f);
198 entryBox.appendChild(bookmarkSection);
200 var visitEntryWrapper = entryBox.appendChild(document.createElement('div'));
201 if (addTitleFavicon || this.blockedVisit)
202 visitEntryWrapper.classList.add('visit-entry');
203 if (this.blockedVisit) {
204 visitEntryWrapper.classList.add('blocked-indicator');
205 visitEntryWrapper.appendChild(this.getVisitAttemptDOM_());
207 visitEntryWrapper.appendChild(this.getTitleDOM_(isSearchResult));
209 this.addFaviconToElement_(visitEntryWrapper);
210 visitEntryWrapper.appendChild(domain);
213 if (isMobileVersion()) {
214 var removeButton = createElementWithClassName('button', 'remove-entry');
215 removeButton.setAttribute('aria-label',
216 loadTimeData.getString('removeFromHistory'));
217 removeButton.classList.add('custom-appearance');
218 removeButton.addEventListener('click', function(e) {
219 self.removeFromHistory();
223 entryBox.appendChild(removeButton);
225 // Support clicking anywhere inside the entry box.
226 entryBox.addEventListener('click', function(e) {
227 e.currentTarget.querySelector('a').click();
230 var dropDown = createElementWithClassName('button', 'drop-down');
231 dropDown.value = 'Open action menu';
232 dropDown.title = loadTimeData.getString('actionMenuDescription');
233 dropDown.setAttribute('menu', '#action-menu');
234 dropDown.setAttribute('aria-haspopup', 'true');
235 cr.ui.decorate(dropDown, MenuButton);
237 dropDown.addEventListener('mousedown', setActiveVisit);
238 dropDown.addEventListener('focus', setActiveVisit);
240 // Prevent clicks on the drop down from affecting the checkbox. We need to
241 // call blur() explicitly because preventDefault() cancels any focus
243 dropDown.addEventListener('click', function(e) {
245 document.activeElement.blur();
247 entryBox.appendChild(dropDown);
250 // Let the entryBox be styled appropriately when it contains keyboard focus.
251 entryBox.addEventListener('focus', function() {
252 this.classList.add('contains-focus');
254 entryBox.addEventListener('blur', function() {
255 this.classList.remove('contains-focus');
258 var entryBoxContainer =
259 createElementWithClassName('div', 'entry-box-container');
260 node.appendChild(entryBoxContainer);
261 entryBoxContainer.appendChild(entryBox);
263 if (isSearchResult || useMonthDate) {
264 // Show the day instead of the time.
265 time.appendChild(document.createTextNode(this.dateShort));
267 time.appendChild(document.createTextNode(this.dateTimeOfDay));
270 this.domNode_ = node;
277 * Remove this visit from the history.
279 Visit.prototype.removeFromHistory = function() {
280 recordUmaAction('HistoryPage_EntryMenuRemoveFromHistory');
282 this.model_.removeVisitsFromHistory([this], function() {
283 removeEntryFromView(self.domNode_);
287 // Visit, private: ------------------------------------------------------------
290 * Add child text nodes to a node such that occurrences of the specified text is
292 * @param {Node} node The node under which new text nodes will be made as
294 * @param {string} content Text to be added beneath |node| as one or more
296 * @param {string} highlightText Occurences of this text inside |content| will
300 Visit.prototype.addHighlightedText_ = function(node, content, highlightText) {
303 var re = new RegExp(Visit.pregQuote_(highlightText), 'gim');
305 while (match = re.exec(content)) {
307 node.appendChild(document.createTextNode(content.slice(i,
310 // Mark the highlighted text in bold.
311 var b = document.createElement('b');
312 b.textContent = content.substring(match.index, i);
316 if (i < content.length)
317 node.appendChild(document.createTextNode(content.slice(i)));
321 * Returns the DOM element containing a link on the title of the URL for the
323 * @param {boolean} isSearchResult Whether or not the entry is a search result.
324 * @return {Element} DOM representation for the title block.
327 Visit.prototype.getTitleDOM_ = function(isSearchResult) {
328 var node = createElementWithClassName('div', 'title');
329 var link = document.createElement('a');
330 link.href = this.url_;
331 link.id = 'id-' + this.id_;
332 link.target = '_top';
333 var integerId = parseInt(this.id_, 10);
334 link.addEventListener('click', function() {
335 recordUmaAction('HistoryPage_EntryLinkClick');
336 // Record the ID of the entry to signify how many entries are above this
338 recordUmaHistogram('HistoryPage.ClickPosition',
339 UMA_MAX_BUCKET_VALUE,
341 if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
342 recordUmaHistogram('HistoryPage.ClickPositionSubset',
343 UMA_MAX_SUBSET_BUCKET_VALUE,
347 link.addEventListener('contextmenu', function() {
348 recordUmaAction('HistoryPage_EntryLinkRightClick');
351 if (isSearchResult) {
352 link.addEventListener('click', function() {
353 recordUmaAction('HistoryPage_SearchResultClick');
357 // Add a tooltip, since it might be ellipsized.
358 // TODO(dubroy): Find a way to show the tooltip only when necessary.
359 link.title = this.title_;
361 this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
362 node.appendChild(link);
368 * Returns the DOM element containing the text for a blocked visit attempt.
369 * @return {Element} DOM representation of the visit attempt.
372 Visit.prototype.getVisitAttemptDOM_ = function() {
373 var node = createElementWithClassName('div', 'title');
374 node.innerHTML = loadTimeData.getStringF('blockedVisitText',
382 * Set the favicon for an element.
383 * @param {Element} el The DOM element to which to add the icon.
386 Visit.prototype.addFaviconToElement_ = function(el) {
387 var url = isMobileVersion() ?
388 getFaviconImageSet(this.url_, 32, 'touch-icon') :
389 getFaviconImageSet(this.url_);
390 el.style.backgroundImage = url;
394 * Launch a search for more history entries from the same domain.
397 Visit.prototype.showMoreFromSite_ = function() {
398 recordUmaAction('HistoryPage_EntryMenuShowMoreFromSite');
399 historyView.setSearch(this.domain_);
400 $('search-field').focus();
403 // Visit, private, static: ----------------------------------------------------
406 * Quote a string so it can be used in a regular expression.
407 * @param {string} str The source string.
408 * @return {string} The escaped string.
411 Visit.pregQuote_ = function(str) {
412 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
415 ///////////////////////////////////////////////////////////////////////////////
419 * Global container for history data. Future optimizations might include
420 * allowing the creation of a HistoryModel for each search string, allowing
421 * quick flips back and forth between results.
423 * The history model is based around pages, and only fetching the data to
424 * fill the currently requested page. This is somewhat dependent on the view,
425 * and so future work may wish to change history model to operate on
426 * timeframe (day or week) based containers.
430 function HistoryModel() {
434 // HistoryModel, Public: ------------------------------------------------------
436 /** @enum {number} */
437 HistoryModel.Range = {
444 * Sets our current view that is called when the history model changes.
445 * @param {HistoryView} view The view to set our current view to.
447 HistoryModel.prototype.setView = function(view) {
452 * Reload our model with the current parameters.
454 HistoryModel.prototype.reload = function() {
455 // Save user-visible state, clear the model, and restore the state.
456 var search = this.searchText_;
457 var page = this.requestedPage_;
458 var range = this.rangeInDays_;
459 var offset = this.offset_;
460 var groupByDomain = this.groupByDomain_;
463 this.searchText_ = search;
464 this.requestedPage_ = page;
465 this.rangeInDays_ = range;
466 this.offset_ = offset;
467 this.groupByDomain_ = groupByDomain;
468 this.queryHistory_();
472 * @return {string} The current search text.
474 HistoryModel.prototype.getSearchText = function() {
475 return this.searchText_;
479 * Tell the model that the view will want to see the current page. When
480 * the data becomes available, the model will call the view back.
481 * @param {number} page The page we want to view.
483 HistoryModel.prototype.requestPage = function(page) {
484 this.requestedPage_ = page;
485 this.updateSearch_();
489 * Receiver for history query.
490 * @param {Object} info An object containing information about the query.
491 * @param {Array} results A list of results.
493 HistoryModel.prototype.addResults = function(info, results) {
494 // If no requests are in flight then this was an old request so we drop the
495 // results. Double check the search term as well.
496 if (!this.inFlight_ || info.term != this.searchText_)
499 $('loading-spinner').hidden = true;
500 this.inFlight_ = false;
501 this.isQueryFinished_ = info.finished;
502 this.queryStartTime = info.queryStartTime;
503 this.queryEndTime = info.queryEndTime;
505 var lastVisit = this.visits_.slice(-1)[0];
506 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
508 for (var i = 0, result; result = results[i]; i++) {
509 var thisDay = result.dateRelativeDay;
510 var isSameDay = lastDay == thisDay;
511 this.visits_.push(new Visit(result, isSameDay, this));
515 if (loadTimeData.getBoolean('isUserSignedIn')) {
516 var message = loadTimeData.getString(
517 info.hasSyncedResults ? 'hasSyncedResults' : 'noSyncedResults');
518 this.view_.showNotification(message);
521 this.updateSearch_();
525 * @return {number} The number of visits in the model.
527 HistoryModel.prototype.getSize = function() {
528 return this.visits_.length;
532 * Get a list of visits between specified index positions.
533 * @param {number} start The start index.
534 * @param {number} end The end index.
535 * @return {Array.<Visit>} A list of visits.
537 HistoryModel.prototype.getNumberedRange = function(start, end) {
538 return this.visits_.slice(start, end);
542 * Return true if there are more results beyond the current page.
543 * @return {boolean} true if the there are more results, otherwise false.
545 HistoryModel.prototype.hasMoreResults = function() {
546 return this.haveDataForPage_(this.requestedPage_ + 1) ||
547 !this.isQueryFinished_;
551 * Removes a list of visits from the history, and calls |callback| when the
552 * removal has successfully completed.
553 * @param {Array<Visit>} visits The visits to remove.
554 * @param {Function} callback The function to call after removal succeeds.
556 HistoryModel.prototype.removeVisitsFromHistory = function(visits, callback) {
557 var toBeRemoved = [];
558 for (var i = 0; i < visits.length; i++) {
561 timestamps: visits[i].allTimestamps
564 chrome.send('removeVisits', toBeRemoved);
565 this.deleteCompleteCallback_ = callback;
569 * Called when visits have been succesfully removed from the history.
571 HistoryModel.prototype.deleteComplete = function() {
572 // Call the callback, with 'this' undefined inside the callback.
573 this.deleteCompleteCallback_.call();
574 this.deleteCompleteCallback_ = null;
577 // Getter and setter for HistoryModel.rangeInDays_.
578 Object.defineProperty(HistoryModel.prototype, 'rangeInDays', {
580 return this.rangeInDays_;
582 set: function(range) {
583 this.rangeInDays_ = range;
588 * Getter and setter for HistoryModel.offset_. The offset moves the current
589 * query 'window' |range| days behind. As such for range set to WEEK an offset
590 * of 0 refers to the last 7 days, an offset of 1 refers to the 7 day period
591 * that ended 7 days ago, etc. For MONTH an offset of 0 refers to the current
592 * calendar month, 1 to the previous one, etc.
594 Object.defineProperty(HistoryModel.prototype, 'offset', {
598 set: function(offset) {
599 this.offset_ = offset;
603 // Setter for HistoryModel.requestedPage_.
604 Object.defineProperty(HistoryModel.prototype, 'requestedPage', {
605 set: function(page) {
606 this.requestedPage_ = page;
610 // HistoryModel, Private: -----------------------------------------------------
613 * Clear the history model.
616 HistoryModel.prototype.clearModel_ = function() {
617 this.inFlight_ = false; // Whether a query is inflight.
618 this.searchText_ = '';
619 // Whether this user is a managed user.
620 this.isManagedProfile = loadTimeData.getBoolean('isManagedProfile');
621 this.deletingHistoryAllowed = loadTimeData.getBoolean('allowDeletingHistory');
623 // Only create checkboxes for editing entries if they can be used either to
624 // delete an entry or to block/allow it.
625 this.editingEntriesAllowed = this.deletingHistoryAllowed;
627 // Flag to show that the results are grouped by domain or not.
628 this.groupByDomain_ = false;
630 this.visits_ = []; // Date-sorted list of visits (most recent first).
631 this.nextVisitId_ = 0;
632 selectionAnchor = -1;
634 // The page that the view wants to see - we only fetch slightly past this
635 // point. If the view requests a page that we don't have data for, we try
636 // to fetch it and call back when we're done.
637 this.requestedPage_ = 0;
639 // The range of history to view or search over.
640 this.rangeInDays_ = HistoryModel.Range.ALL_TIME;
642 // Skip |offset_| * weeks/months from the begining.
645 // Keeps track of whether or not there are more results available than are
646 // currently held in |this.visits_|.
647 this.isQueryFinished_ = false;
654 * Figure out if we need to do more queries to fill the currently requested
655 * page. If we think we can fill the page, call the view and let it know
656 * we're ready to show something. This only applies to the daily time-based
660 HistoryModel.prototype.updateSearch_ = function() {
661 var doneLoading = this.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
662 this.isQueryFinished_ ||
663 this.canFillPage_(this.requestedPage_);
665 // Try to fetch more results if more results can arrive and the page is not
667 if (!doneLoading && !this.inFlight_)
668 this.queryHistory_();
670 // Show the result or a message if no results were returned.
671 this.view_.onModelReady(doneLoading);
675 * Query for history, either for a search or time-based browsing.
678 HistoryModel.prototype.queryHistory_ = function() {
680 (this.rangeInDays_ == HistoryModel.Range.ALL_TIME) ? RESULTS_PER_PAGE : 0;
682 // If there are already some visits, pick up the previous query where it
684 var lastVisit = this.visits_.slice(-1)[0];
685 var endTime = lastVisit ? lastVisit.date.getTime() : 0;
687 $('loading-spinner').hidden = false;
688 this.inFlight_ = true;
689 chrome.send('queryHistory',
690 [this.searchText_, this.offset_, this.rangeInDays_, endTime, maxResults]);
694 * Check to see if we have data for the given page.
695 * @param {number} page The page number.
696 * @return {boolean} Whether we have any data for the given page.
699 HistoryModel.prototype.haveDataForPage_ = function(page) {
700 return page * RESULTS_PER_PAGE < this.getSize();
704 * Check to see if we have data to fill the given page.
705 * @param {number} page The page number.
706 * @return {boolean} Whether we have data to fill the page.
709 HistoryModel.prototype.canFillPage_ = function(page) {
710 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
714 * Enables or disables grouping by domain.
715 * @param {boolean} groupByDomain New groupByDomain_ value.
717 HistoryModel.prototype.setGroupByDomain = function(groupByDomain) {
718 this.groupByDomain_ = groupByDomain;
723 * Gets whether we are grouped by domain.
724 * @return {boolean} Whether the results are grouped by domain.
726 HistoryModel.prototype.getGroupByDomain = function() {
727 return this.groupByDomain_;
730 ///////////////////////////////////////////////////////////////////////////////
734 * Functions and state for populating the page with HTML. This should one-day
735 * contain the view and use event handlers, rather than pushing HTML out and
736 * getting called externally.
737 * @param {HistoryModel} model The model backing this view.
740 function HistoryView(model) {
741 this.editButtonTd_ = $('edit-button');
742 this.editingControlsDiv_ = $('editing-controls');
743 this.resultDiv_ = $('results-display');
744 this.pageDiv_ = $('results-pagination');
747 this.lastDisplayed_ = [];
749 this.model_.setView(this);
751 this.currentVisits_ = [];
753 // If there is no search button, use the search button label as placeholder
754 // text in the search field.
755 if ($('search-button').offsetWidth == 0)
756 $('search-field').placeholder = $('search-button').value;
760 $('clear-browsing-data').addEventListener('click', openClearBrowsingData);
761 $('remove-selected').addEventListener('click', removeItems);
763 // Add handlers for the page navigation buttons at the bottom.
764 $('newest-button').addEventListener('click', function() {
765 recordUmaAction('HistoryPage_NewestHistoryClick');
768 $('newer-button').addEventListener('click', function() {
769 recordUmaAction('HistoryPage_NewerHistoryClick');
770 self.setPage(self.pageIndex_ - 1);
772 $('older-button').addEventListener('click', function() {
773 recordUmaAction('HistoryPage_OlderHistoryClick');
774 self.setPage(self.pageIndex_ + 1);
777 var handleRangeChange = function(e) {
778 // Update the results and save the last state.
779 self.setRangeInDays(parseInt(e.target.value, 10));
782 // Add handlers for the range options.
783 $('timeframe-filter-all').addEventListener('change', handleRangeChange);
784 $('timeframe-filter-week').addEventListener('change', handleRangeChange);
785 $('timeframe-filter-month').addEventListener('change', handleRangeChange);
787 $('range-previous').addEventListener('click', function(e) {
788 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
789 self.setPage(self.pageIndex_ + 1);
791 self.setOffset(self.getOffset() + 1);
793 $('range-next').addEventListener('click', function(e) {
794 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
795 self.setPage(self.pageIndex_ - 1);
797 self.setOffset(self.getOffset() - 1);
799 $('range-today').addEventListener('click', function(e) {
800 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
807 // HistoryView, public: -------------------------------------------------------
809 * Do a search on a specific term.
810 * @param {string} term The string to search for.
812 HistoryView.prototype.setSearch = function(term) {
813 window.scrollTo(0, 0);
814 this.setPageState(term, 0, this.getRangeInDays(), this.getOffset());
818 * Reload the current view.
820 HistoryView.prototype.reload = function() {
821 this.model_.reload();
822 this.updateSelectionEditButtons();
823 this.updateRangeButtons_();
827 * Sets all the parameters for the history page and then reloads the view to
828 * update the results.
829 * @param {string} searchText The search string to set.
830 * @param {number} page The page to be viewed.
831 * @param {HistoryModel.Range} range The range to view or search over.
832 * @param {number} offset Set the begining of the query to the specific offset.
834 HistoryView.prototype.setPageState = function(searchText, page, range, offset) {
836 this.model_.searchText_ = searchText;
837 this.pageIndex_ = page;
838 this.model_.requestedPage_ = page;
839 this.model_.rangeInDays_ = range;
840 this.model_.groupByDomain_ = false;
841 if (range != HistoryModel.Range.ALL_TIME)
842 this.model_.groupByDomain_ = true;
843 this.model_.offset_ = offset;
845 pageState.setUIState(this.model_.getSearchText(),
847 this.getRangeInDays(),
852 * Switch to a specified page.
853 * @param {number} page The page we wish to view.
855 HistoryView.prototype.setPage = function(page) {
856 // TODO(sergiu): Move this function to setPageState as well and see why one
857 // of the tests fails when using setPageState.
859 this.pageIndex_ = parseInt(page, 10);
860 window.scrollTo(0, 0);
861 this.model_.requestPage(page);
862 pageState.setUIState(this.model_.getSearchText(),
864 this.getRangeInDays(),
869 * @return {number} The page number being viewed.
871 HistoryView.prototype.getPage = function() {
872 return this.pageIndex_;
876 * Set the current range for grouped results.
877 * @param {string} range The number of days to which the range should be set.
879 HistoryView.prototype.setRangeInDays = function(range) {
880 // Set the range, offset and reset the page.
881 this.setPageState(this.model_.getSearchText(), 0, range, 0);
885 * Get the current range in days.
886 * @return {number} Current range in days from the model.
888 HistoryView.prototype.getRangeInDays = function() {
889 return this.model_.rangeInDays;
893 * Set the current offset for grouped results.
894 * @param {number} offset Offset to set.
896 HistoryView.prototype.setOffset = function(offset) {
897 // If there is another query already in flight wait for that to complete.
898 if (this.model_.inFlight_)
900 this.setPageState(this.model_.getSearchText(),
902 this.getRangeInDays(),
907 * Get the current offset.
908 * @return {number} Current offset from the model.
910 HistoryView.prototype.getOffset = function() {
911 return this.model_.offset;
915 * Callback for the history model to let it know that it has data ready for us
917 * @param {boolean} doneLoading Whether the current request is complete.
919 HistoryView.prototype.onModelReady = function(doneLoading) {
920 this.displayResults_(doneLoading);
922 // Allow custom styling based on whether there are any results on the page.
923 // To make this easier, add a class to the body if there are any results.
924 if (this.model_.visits_.length)
925 document.body.classList.add('has-results');
927 document.body.classList.remove('has-results');
929 this.updateNavBar_();
931 if (isMobileVersion()) {
932 // Hide the search field if it is empty and there are no results.
933 var hasResults = this.model_.visits_.length > 0;
934 var isSearch = this.model_.getSearchText().length > 0;
935 $('search-field').hidden = !(hasResults || isSearch);
940 * Enables or disables the buttons that control editing entries depending on
941 * whether there are any checked boxes.
943 HistoryView.prototype.updateSelectionEditButtons = function() {
944 if (loadTimeData.getBoolean('allowDeletingHistory')) {
945 var anyChecked = document.querySelector('.entry input:checked') != null;
946 $('remove-selected').disabled = !anyChecked;
948 $('remove-selected').disabled = true;
953 * Shows the notification bar at the top of the page with |innerHTML| as its
955 * @param {string} innerHTML The HTML content of the warning.
956 * @param {boolean} isWarning If true, style the notification as a warning.
958 HistoryView.prototype.showNotification = function(innerHTML, isWarning) {
959 var bar = $('notification-bar');
960 bar.innerHTML = innerHTML;
963 bar.classList.add('warning');
965 bar.classList.remove('warning');
967 // Make sure that any links in the HTML are targeting the top level.
968 var links = bar.querySelectorAll('a');
969 for (var i = 0; i < links.length; i++)
970 links[i].target = '_top';
972 this.positionNotificationBar();
976 * Adjusts the position of the notification bar based on the size of the page.
978 HistoryView.prototype.positionNotificationBar = function() {
979 var bar = $('notification-bar');
981 // If the bar does not fit beside the editing controls, put it into the
983 if (bar.getBoundingClientRect().top >=
984 $('editing-controls').getBoundingClientRect().bottom) {
985 bar.classList.add('alone');
987 bar.classList.remove('alone');
991 // HistoryView, private: ------------------------------------------------------
994 * Clear the results in the view. Since we add results piecemeal, we need
995 * to clear them out when we switch to a new page or reload.
998 HistoryView.prototype.clear_ = function() {
999 var alertOverlay = $('alertOverlay');
1000 if (alertOverlay && alertOverlay.classList.contains('showing'))
1001 hideConfirmationOverlay();
1003 this.resultDiv_.textContent = '';
1005 this.currentVisits_.forEach(function(visit) {
1006 visit.isRendered = false;
1008 this.currentVisits_ = [];
1010 document.body.classList.remove('has-results');
1014 * Record that the given visit has been rendered.
1015 * @param {Visit} visit The visit that was rendered.
1018 HistoryView.prototype.setVisitRendered_ = function(visit) {
1019 visit.isRendered = true;
1020 this.currentVisits_.push(visit);
1024 * Generates and adds the grouped visits DOM for a certain domain. This
1025 * includes the clickable arrow and domain name and the visit entries for
1027 * @param {Element} results DOM object to which to add the elements.
1028 * @param {string} domain Current domain name.
1029 * @param {Array} domainVisits Array of visits for this domain.
1032 HistoryView.prototype.getGroupedVisitsDOM_ = function(
1033 results, domain, domainVisits) {
1034 // Add a new domain entry.
1035 var siteResults = results.appendChild(
1036 createElementWithClassName('li', 'site-entry'));
1038 // Make a wrapper that will contain the arrow, the favicon and the domain.
1039 var siteDomainWrapper = siteResults.appendChild(
1040 createElementWithClassName('div', 'site-domain-wrapper'));
1042 if (this.model_.editingEntriesAllowed) {
1043 var siteDomainCheckbox =
1044 createElementWithClassName('input', 'domain-checkbox');
1046 siteDomainCheckbox.type = 'checkbox';
1047 siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
1048 siteDomainCheckbox.domain_ = domain;
1050 siteDomainWrapper.appendChild(siteDomainCheckbox);
1053 var siteArrow = siteDomainWrapper.appendChild(
1054 createElementWithClassName('div', 'site-domain-arrow collapse'));
1055 var siteDomain = siteDomainWrapper.appendChild(
1056 createElementWithClassName('div', 'site-domain'));
1057 var siteDomainLink = siteDomain.appendChild(
1058 createElementWithClassName('button', 'link-button'));
1059 siteDomainLink.addEventListener('click', function(e) { e.preventDefault(); });
1060 siteDomainLink.textContent = domain;
1061 var numberOfVisits = createElementWithClassName('span', 'number-visits');
1062 var domainElement = document.createElement('span');
1064 numberOfVisits.textContent = loadTimeData.getStringF('numberVisits',
1065 domainVisits.length);
1066 siteDomain.appendChild(numberOfVisits);
1068 domainVisits[0].addFaviconToElement_(siteDomain);
1070 siteDomainWrapper.addEventListener('click', toggleHandler);
1072 if (this.model_.isManagedProfile) {
1073 siteDomainWrapper.appendChild(
1074 getManagedStatusDOM(domainVisits[0].hostFilteringBehavior));
1077 siteResults.appendChild(siteDomainWrapper);
1078 var resultsList = siteResults.appendChild(
1079 createElementWithClassName('ol', 'site-results'));
1080 resultsList.classList.add('grouped');
1082 // Collapse until it gets toggled.
1083 resultsList.style.height = 0;
1085 // Add the results for each of the domain.
1086 var isMonthGroupedResult = this.getRangeInDays() == HistoryModel.Range.MONTH;
1087 for (var j = 0, visit; visit = domainVisits[j]; j++) {
1088 resultsList.appendChild(visit.getResultDOM({
1089 useMonthDate: isMonthGroupedResult
1091 this.setVisitRendered_(visit);
1096 * Enables or disables the time range buttons.
1099 HistoryView.prototype.updateRangeButtons_ = function() {
1100 // The enabled state for the previous, today and next buttons.
1101 var previousState = false;
1102 var todayState = false;
1103 var nextState = false;
1104 var usePage = (this.getRangeInDays() == HistoryModel.Range.ALL_TIME);
1106 // Use pagination for most recent visits, offset otherwise.
1107 // TODO(sergiu): Maybe send just one variable in the future.
1109 if (this.getPage() != 0) {
1113 previousState = this.model_.hasMoreResults();
1115 if (this.getOffset() != 0) {
1119 previousState = !this.model_.isQueryFinished_;
1122 $('range-previous').disabled = !previousState;
1123 $('range-today').disabled = !todayState;
1124 $('range-next').disabled = !nextState;
1128 * Groups visits by domain, sorting them by the number of visits.
1129 * @param {Array} visits Visits received from the query results.
1130 * @param {Element} results Object where the results are added to.
1133 HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) {
1134 var visitsByDomain = {};
1137 // Group the visits into a dictionary and generate a list of domains.
1138 for (var i = 0, visit; visit = visits[i]; i++) {
1139 var domain = visit.domain_;
1140 if (!visitsByDomain[domain]) {
1141 visitsByDomain[domain] = [];
1142 domains.push(domain);
1144 visitsByDomain[domain].push(visit);
1146 var sortByVisits = function(a, b) {
1147 return visitsByDomain[b].length - visitsByDomain[a].length;
1149 domains.sort(sortByVisits);
1151 for (var i = 0; i < domains.length; ++i) {
1152 var domain = domains[i];
1153 this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]);
1158 * Adds the results for a month.
1159 * @param {Array} visits Visits returned by the query.
1160 * @param {Element} parentElement Element to which to add the results to.
1163 HistoryView.prototype.addMonthResults_ = function(visits, parentElement) {
1164 if (visits.length == 0)
1167 var monthResults = parentElement.appendChild(
1168 createElementWithClassName('ol', 'month-results'));
1169 // Don't add checkboxes if entries can not be edited.
1170 if (!this.model_.editingEntriesAllowed)
1171 monthResults.classList.add('no-checkboxes');
1173 this.groupVisitsByDomain_(visits, monthResults);
1177 * Adds the results for a certain day. This includes a title with the day of
1178 * the results and the results themselves, grouped or not.
1179 * @param {Array} visits Visits returned by the query.
1180 * @param {Element} parentElement Element to which to add the results to.
1183 HistoryView.prototype.addDayResults_ = function(visits, parentElement) {
1184 if (visits.length == 0)
1187 var firstVisit = visits[0];
1188 var day = parentElement.appendChild(createElementWithClassName('h3', 'day'));
1189 day.appendChild(document.createTextNode(firstVisit.dateRelativeDay));
1190 if (firstVisit.continued) {
1191 day.appendChild(document.createTextNode(' ' +
1192 loadTimeData.getString('cont')));
1194 var dayResults = parentElement.appendChild(
1195 createElementWithClassName('ol', 'day-results'));
1197 // Don't add checkboxes if entries can not be edited.
1198 if (!this.model_.editingEntriesAllowed)
1199 dayResults.classList.add('no-checkboxes');
1201 if (this.model_.getGroupByDomain()) {
1202 this.groupVisitsByDomain_(visits, dayResults);
1206 for (var i = 0, visit; visit = visits[i]; i++) {
1207 // If enough time has passed between visits, indicate a gap in browsing.
1208 var thisTime = visit.date.getTime();
1209 if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME)
1210 dayResults.appendChild(createElementWithClassName('li', 'gap'));
1212 // Insert the visit into the DOM.
1213 dayResults.appendChild(visit.getResultDOM({ addTitleFavicon: true }));
1214 this.setVisitRendered_(visit);
1216 lastTime = thisTime;
1222 * Adds the text that shows the current interval, used for week and month
1224 * @param {Element} resultsFragment The element to which the interval will be
1228 HistoryView.prototype.addTimeframeInterval_ = function(resultsFragment) {
1229 if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME)
1232 // If this is a time range result add some text that shows what is the
1233 // time range for the results the user is viewing.
1234 var timeFrame = resultsFragment.appendChild(
1235 createElementWithClassName('h2', 'timeframe'));
1236 // TODO(sergiu): Figure the best way to show this for the first day of
1238 timeFrame.appendChild(document.createTextNode(loadTimeData.getStringF(
1240 this.model_.queryStartTime,
1241 this.model_.queryEndTime)));
1245 * Update the page with results.
1246 * @param {boolean} doneLoading Whether the current request is complete.
1249 HistoryView.prototype.displayResults_ = function(doneLoading) {
1250 // Either show a page of results received for the all time results or all the
1251 // received results for the weekly and monthly view.
1252 var results = this.model_.visits_;
1253 if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME) {
1254 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
1255 var rangeEnd = rangeStart + RESULTS_PER_PAGE;
1256 results = this.model_.getNumberedRange(rangeStart, rangeEnd);
1258 var searchText = this.model_.getSearchText();
1259 var groupByDomain = this.model_.getGroupByDomain();
1262 // Add a header for the search results, if there isn't already one.
1263 if (!this.resultDiv_.querySelector('h3')) {
1264 var header = document.createElement('h3');
1265 header.textContent = loadTimeData.getStringF('searchResultsFor',
1267 this.resultDiv_.appendChild(header);
1270 this.addTimeframeInterval_(this.resultDiv_);
1272 var searchResults = createElementWithClassName('ol', 'search-results');
1274 // Don't add checkboxes if entries can not be edited.
1275 if (!this.model_.editingEntriesAllowed)
1276 searchResults.classList.add('no-checkboxes');
1278 if (results.length == 0 && doneLoading) {
1279 var noSearchResults = searchResults.appendChild(
1280 createElementWithClassName('div', 'no-results-message'));
1281 noSearchResults.textContent = loadTimeData.getString('noSearchResults');
1283 for (var i = 0, visit; visit = results[i]; i++) {
1284 if (!visit.isRendered) {
1285 searchResults.appendChild(visit.getResultDOM({
1286 isSearchResult: true,
1287 addTitleFavicon: true
1289 this.setVisitRendered_(visit);
1293 this.resultDiv_.appendChild(searchResults);
1295 var resultsFragment = document.createDocumentFragment();
1297 this.addTimeframeInterval_(resultsFragment);
1299 if (results.length == 0 && doneLoading) {
1300 var noResults = resultsFragment.appendChild(
1301 createElementWithClassName('div', 'no-results-message'));
1302 noResults.textContent = loadTimeData.getString('noResults');
1303 this.resultDiv_.appendChild(resultsFragment);
1307 if (this.getRangeInDays() == HistoryModel.Range.MONTH &&
1309 // Group everything together in the month view.
1310 this.addMonthResults_(results, resultsFragment);
1314 // Go through all of the visits and process them in chunks of one day.
1315 while (dayEnd < results.length) {
1316 // Skip over the ones that are already rendered.
1317 while (dayStart < results.length && results[dayStart].isRendered)
1319 var dayEnd = dayStart + 1;
1320 while (dayEnd < results.length && results[dayEnd].continued)
1323 this.addDayResults_(
1324 results.slice(dayStart, dayEnd), resultsFragment, groupByDomain);
1328 // Add all the days and their visits to the page.
1329 this.resultDiv_.appendChild(resultsFragment);
1331 // After the results have been added to the DOM, determine the size of the
1333 this.setTimeColumnWidth_(this.resultDiv_);
1337 * Update the visibility of the page navigation buttons.
1340 HistoryView.prototype.updateNavBar_ = function() {
1341 this.updateRangeButtons_();
1343 // Managed users have the control bar on top, don't show it on the bottom
1345 if (!loadTimeData.getBoolean('isManagedProfile')) {
1346 $('newest-button').hidden = this.pageIndex_ == 0;
1347 $('newer-button').hidden = this.pageIndex_ == 0;
1348 $('older-button').hidden =
1349 this.model_.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
1350 !this.model_.hasMoreResults();
1355 * Updates the visibility of the 'Clear browsing data' button.
1356 * Only used on mobile platforms.
1359 HistoryView.prototype.updateClearBrowsingDataButton_ = function() {
1360 // Ideally, we should hide the 'Clear browsing data' button whenever the
1361 // soft keyboard is visible. This is not possible, so instead, hide the
1362 // button whenever the search field has focus.
1363 $('clear-browsing-data').hidden =
1364 (document.activeElement === $('search-field'));
1368 * Dynamically sets the min-width of the time column for history entries.
1369 * This ensures that all entry times will have the same width, without
1370 * imposing a fixed width that may not be appropriate for some locales.
1373 HistoryView.prototype.setTimeColumnWidth_ = function() {
1374 // Find the maximum width of all the time elements on the page.
1375 var times = this.resultDiv_.querySelectorAll('.entry .time');
1376 var widths = Array.prototype.map.call(times, function(el) {
1377 el.style.minWidth = '-webkit-min-content';
1378 var width = el.clientWidth;
1379 el.style.minWidth = '';
1381 // Add an extra pixel to prevent rounding errors from causing the text to
1382 // be ellipsized at certain zoom levels (see crbug.com/329779).
1385 var maxWidth = widths.length ? Math.max.apply(null, widths) : 0;
1387 // Add a dynamic stylesheet to the page (or replace the existing one), to
1388 // ensure that all entry times have the same width.
1389 var styleEl = $('timeColumnStyle');
1391 styleEl = document.head.appendChild(document.createElement('style'));
1392 styleEl.id = 'timeColumnStyle';
1394 styleEl.textContent = '.entry .time { min-width: ' + maxWidth + 'px; }';
1397 ///////////////////////////////////////////////////////////////////////////////
1400 * An 'AJAX-history' implementation.
1401 * @param {HistoryModel} model The model we're representing.
1402 * @param {HistoryView} view The view we're representing.
1405 function PageState(model, view) {
1406 // Enforce a singleton.
1407 if (PageState.instance) {
1408 return PageState.instance;
1414 if (typeof this.checker_ != 'undefined' && this.checker_) {
1415 clearInterval(this.checker_);
1418 // TODO(glen): Replace this with a bound method so we don't need
1419 // public model and view.
1420 this.checker_ = window.setInterval(function(stateObj) {
1421 var hashData = stateObj.getHashData();
1422 var page = parseInt(hashData.page, 10);
1423 var range = parseInt(hashData.range, 10);
1424 var offset = parseInt(hashData.offset, 10);
1425 if (hashData.q != stateObj.model.getSearchText() ||
1426 page != stateObj.view.getPage() ||
1427 range != stateObj.model.rangeInDays ||
1428 offset != stateObj.model.offset) {
1429 stateObj.view.setPageState(hashData.q, page, range, offset);
1435 * Holds the singleton instance.
1437 PageState.instance = null;
1440 * @return {Object} An object containing parameters from our window hash.
1442 PageState.prototype.getHashData = function() {
1451 if (!window.location.hash)
1454 var hashSplit = window.location.hash.substr(1).split('&');
1455 for (var i = 0; i < hashSplit.length; i++) {
1456 var pair = hashSplit[i].split('=');
1457 if (pair.length > 1) {
1458 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
1466 * Set the hash to a specified state, this will create an entry in the
1467 * session history so the back button cycles through hash states, which
1468 * are then picked up by our listener.
1469 * @param {string} term The current search string.
1470 * @param {number} page The page currently being viewed.
1471 * @param {HistoryModel.Range} range The range to view or search over.
1472 * @param {number} offset Set the begining of the query to the specific offset.
1474 PageState.prototype.setUIState = function(term, page, range, offset) {
1475 // Make sure the form looks pretty.
1476 $('search-field').value = term;
1477 var hash = this.getHashData();
1478 if (hash.q != term || hash.page != page || hash.range != range ||
1479 hash.offset != offset) {
1480 window.location.hash = PageState.getHashString(term, page, range, offset);
1485 * Static method to get the hash string for a specified state
1486 * @param {string} term The current search string.
1487 * @param {number} page The page currently being viewed.
1488 * @param {HistoryModel.Range} range The range to view or search over.
1489 * @param {number} offset Set the begining of the query to the specific offset.
1490 * @return {string} The string to be used in a hash.
1492 PageState.getHashString = function(term, page, range, offset) {
1493 // Omit elements that are empty.
1497 newHash.push('q=' + encodeURIComponent(term));
1500 newHash.push('page=' + page);
1503 newHash.push('range=' + range);
1506 newHash.push('offset=' + offset);
1508 return newHash.join('&');
1511 ///////////////////////////////////////////////////////////////////////////////
1512 // Document Functions:
1514 * Window onload handler, sets up the page.
1517 uber.onContentFrameLoaded();
1519 var searchField = $('search-field');
1521 historyModel = new HistoryModel();
1522 historyView = new HistoryView(historyModel);
1523 pageState = new PageState(historyModel, historyView);
1525 // Create default view.
1526 var hashData = pageState.getHashData();
1527 var grouped = (hashData.grouped == 'true') || historyModel.getGroupByDomain();
1528 var page = parseInt(hashData.page, 10) || historyView.getPage();
1529 var range = parseInt(hashData.range, 10) || historyView.getRangeInDays();
1530 var offset = parseInt(hashData.offset, 10) || historyView.getOffset();
1531 historyView.setPageState(hashData.q, page, range, offset);
1534 cr.ui.overlay.setupOverlay($('overlay'));
1535 cr.ui.overlay.globalInitialization();
1537 HistoryFocusManager.getInstance().initialize();
1539 var doSearch = function(e) {
1540 recordUmaAction('HistoryPage_Search');
1541 historyView.setSearch(searchField.value);
1543 if (isMobileVersion())
1544 searchField.blur(); // Dismiss the keyboard.
1547 var mayRemoveVisits = loadTimeData.getBoolean('allowDeletingHistory');
1548 $('remove-visit').disabled = !mayRemoveVisits;
1550 if (mayRemoveVisits) {
1551 $('remove-visit').addEventListener('activate', function(e) {
1552 activeVisit.removeFromHistory();
1557 if (!loadTimeData.getBoolean('showDeleteVisitUI'))
1558 $('remove-visit').hidden = true;
1560 searchField.addEventListener('search', doSearch);
1561 $('search-button').addEventListener('click', doSearch);
1563 $('more-from-site').addEventListener('activate', function(e) {
1564 activeVisit.showMoreFromSite_();
1568 // Only show the controls if the command line switch is activated.
1569 if (loadTimeData.getBoolean('groupByDomain') ||
1570 loadTimeData.getBoolean('isManagedProfile')) {
1571 // Hide the top container which has the "Clear browsing data" and "Remove
1572 // selected entries" buttons since they're unavailable in managed mode
1573 $('top-container').hidden = true;
1574 $('history-page').classList.add('big-topbar-page');
1575 $('filter-controls').hidden = false;
1578 uber.setTitle(loadTimeData.getString('title'));
1580 // Adjust the position of the notification bar when the window size changes.
1581 window.addEventListener('resize',
1582 historyView.positionNotificationBar.bind(historyView));
1584 cr.ui.FocusManager.disableMouseFocusOnButtons();
1586 if (isMobileVersion()) {
1587 // Move the search box out of the header.
1588 var resultsDisplay = $('results-display');
1589 resultsDisplay.parentNode.insertBefore($('search-field'), resultsDisplay);
1591 window.addEventListener(
1592 'resize', historyView.updateClearBrowsingDataButton_);
1594 // When the search field loses focus, add a delay before updating the
1595 // visibility, otherwise the button will flash on the screen before the
1596 // keyboard animates away.
1597 searchField.addEventListener('blur', function() {
1598 setTimeout(historyView.updateClearBrowsingDataButton_, 250);
1601 // Move the button to the bottom of the page.
1602 $('history-page').appendChild($('clear-browsing-data'));
1604 window.addEventListener('message', function(e) {
1605 if (e.data.method == 'frameSelected')
1606 searchField.focus();
1608 searchField.focus();
1612 function checkKeyboardVisibility() {
1613 // Figure out the real height based on the orientation, becauase
1614 // screen.width and screen.height don't update after rotation.
1615 var screenHeight = window.orientation % 180 ? screen.width : screen.height;
1617 // Assume that the keyboard is visible if more than 30% of the screen is
1618 // taken up by window chrome.
1619 var isKeyboardVisible = (window.innerHeight / screenHeight) < 0.7;
1621 document.body.classList.toggle('ios-keyboard-visible', isKeyboardVisible);
1623 window.addEventListener('orientationchange', checkKeyboardVisibility);
1624 window.addEventListener('resize', checkKeyboardVisibility);
1629 * Updates the managed filter status labels of a host/URL entry to the current
1631 * @param {Element} statusElement The div which contains the status labels.
1632 * @param {ManagedModeFilteringBehavior} newStatus The filter status of the
1633 * current domain/URL.
1635 function updateHostStatus(statusElement, newStatus) {
1636 var filteringBehaviorDiv =
1637 statusElement.querySelector('.filtering-behavior');
1638 // Reset to the base class first, then add modifier classes if needed.
1639 filteringBehaviorDiv.className = 'filtering-behavior';
1640 if (newStatus == ManagedModeFilteringBehavior.BLOCK) {
1641 filteringBehaviorDiv.textContent =
1642 loadTimeData.getString('filterBlocked');
1643 filteringBehaviorDiv.classList.add('filter-blocked');
1645 filteringBehaviorDiv.textContent = '';
1650 * Click handler for the 'Clear browsing data' dialog.
1651 * @param {Event} e The click event.
1653 function openClearBrowsingData(e) {
1654 recordUmaAction('HistoryPage_InitClearBrowsingData');
1655 chrome.send('clearBrowsingData');
1659 * Shows the dialog for the user to confirm removal of selected history entries.
1661 function showConfirmationOverlay() {
1662 $('alertOverlay').classList.add('showing');
1663 $('overlay').hidden = false;
1664 uber.invokeMethodOnParent('beginInterceptingEvents');
1668 * Hides the confirmation overlay used to confirm selected history entries.
1670 function hideConfirmationOverlay() {
1671 $('alertOverlay').classList.remove('showing');
1672 $('overlay').hidden = true;
1673 uber.invokeMethodOnParent('stopInterceptingEvents');
1677 * Shows the confirmation alert for history deletions and permits browser tests
1678 * to override the dialog.
1679 * @param {function=} okCallback A function to be called when the user presses
1681 * @param {function=} cancelCallback A function to be called when the user
1682 * presses the cancel button.
1684 function confirmDeletion(okCallback, cancelCallback) {
1685 alertOverlay.setValues(
1686 loadTimeData.getString('removeSelected'),
1687 loadTimeData.getString('deleteWarning'),
1688 loadTimeData.getString('cancel'),
1689 loadTimeData.getString('deleteConfirm'),
1692 showConfirmationOverlay();
1696 * Click handler for the 'Remove selected items' button.
1697 * Confirms the deletion with the user, and then deletes the selected visits.
1699 function removeItems() {
1700 recordUmaAction('HistoryPage_RemoveSelected');
1701 if (!loadTimeData.getBoolean('allowDeletingHistory'))
1704 var checked = $('results-display').querySelectorAll(
1705 '.entry-box input[type=checkbox]:checked:not([disabled])');
1706 var disabledItems = [];
1707 var toBeRemoved = [];
1709 for (var i = 0; i < checked.length; i++) {
1710 var checkbox = checked[i];
1711 var entry = findAncestorByClass(checkbox, 'entry');
1712 toBeRemoved.push(entry.visit);
1714 // Disable the checkbox and put a strikethrough style on the link, so the
1715 // user can see what will be deleted.
1716 var link = entry.querySelector('a');
1717 checkbox.disabled = true;
1718 link.classList.add('to-be-removed');
1719 disabledItems.push(checkbox);
1720 var integerId = parseInt(entry.visit.id_, 10);
1721 // Record the ID of the entry to signify how many entries are above this
1722 // link on the page.
1723 recordUmaHistogram('HistoryPage.RemoveEntryPosition',
1724 UMA_MAX_BUCKET_VALUE,
1726 if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
1727 recordUmaHistogram('HistoryPage.RemoveEntryPositionSubset',
1728 UMA_MAX_SUBSET_BUCKET_VALUE,
1731 if (entry.parentNode.className == 'search-results')
1732 recordUmaAction('HistoryPage_SearchResultRemove');
1735 function onConfirmRemove() {
1736 recordUmaAction('HistoryPage_ConfirmRemoveSelected');
1737 historyModel.removeVisitsFromHistory(toBeRemoved,
1738 historyView.reload.bind(historyView));
1739 $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
1740 hideConfirmationOverlay();
1743 function onCancelRemove() {
1744 recordUmaAction('HistoryPage_CancelRemoveSelected');
1745 // Return everything to its previous state.
1746 for (var i = 0; i < disabledItems.length; i++) {
1747 var checkbox = disabledItems[i];
1748 checkbox.disabled = false;
1750 var entryBox = findAncestorByClass(checkbox, 'entry-box');
1751 entryBox.querySelector('a').classList.remove('to-be-removed');
1753 $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
1754 hideConfirmationOverlay();
1757 if (checked.length) {
1758 confirmDeletion(onConfirmRemove, onCancelRemove);
1759 $('overlay').addEventListener('cancelOverlay', onCancelRemove);
1764 * Handler for the 'click' event on a checkbox.
1765 * @param {Event} e The click event.
1767 function checkboxClicked(e) {
1768 handleCheckboxStateChange(e.currentTarget, e.shiftKey);
1772 * Post-process of checkbox state change. This handles range selection and
1773 * updates internal state.
1774 * @param {!HTMLInputElement} checkbox Clicked checkbox.
1775 * @param {boolean} shiftKey true if shift key is pressed.
1777 function handleCheckboxStateChange(checkbox, shiftKey) {
1778 updateParentCheckbox(checkbox);
1779 var id = Number(checkbox.id.slice('checkbox-'.length));
1780 // Handle multi-select if shift was pressed.
1781 if (shiftKey && (selectionAnchor != -1)) {
1782 var checked = checkbox.checked;
1783 // Set all checkboxes from the anchor up to the clicked checkbox to the
1784 // state of the clicked one.
1785 var begin = Math.min(id, selectionAnchor);
1786 var end = Math.max(id, selectionAnchor);
1787 for (var i = begin; i <= end; i++) {
1788 var checkbox = document.querySelector('#checkbox-' + i);
1790 checkbox.checked = checked;
1791 updateParentCheckbox(checkbox);
1795 selectionAnchor = id;
1797 historyView.updateSelectionEditButtons();
1801 * Handler for the 'click' event on a domain checkbox. Checkes or unchecks the
1802 * checkboxes of the visits to this domain in the respective group.
1803 * @param {Event} e The click event.
1805 function domainCheckboxClicked(e) {
1806 var siteEntry = findAncestorByClass(e.currentTarget, 'site-entry');
1808 siteEntry.querySelectorAll('.site-results input[type=checkbox]');
1809 for (var i = 0; i < checkboxes.length; i++)
1810 checkboxes[i].checked = e.currentTarget.checked;
1811 historyView.updateSelectionEditButtons();
1812 // Stop propagation as clicking the checkbox would otherwise trigger the
1813 // group to collapse/expand.
1814 e.stopPropagation();
1818 * Updates the domain checkbox for this visit checkbox if it has been
1820 * @param {Element} checkbox The checkbox that has been clicked.
1822 function updateParentCheckbox(checkbox) {
1823 if (checkbox.checked)
1826 var entry = findAncestorByClass(checkbox, 'site-entry');
1830 var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
1832 groupCheckbox.checked = false;
1835 function entryBoxMousedown(event) {
1836 // Prevent text selection when shift-clicking to select multiple entries.
1838 event.preventDefault();
1842 * Handle click event for entryBox labels.
1843 * @param {!MouseEvent} event A click event.
1845 function entryBoxClick(event) {
1846 // Do nothing if a bookmark star is clicked.
1847 if (event.defaultPrevented)
1849 var element = event.target;
1850 // Do nothing if the event happened in an interactive element.
1851 for (; element != event.currentTarget; element = element.parentNode) {
1852 switch (element.tagName) {
1859 var checkbox = event.currentTarget.control;
1860 checkbox.checked = !checkbox.checked;
1861 handleCheckboxStateChange(checkbox, event.shiftKey);
1862 // We don't want to focus on the checkbox.
1863 event.preventDefault();
1867 * Called when an individual history entry has been removed from the page.
1868 * This will only be called when all the elements affected by the deletion
1869 * have been removed from the DOM and the animations have completed.
1871 function onEntryRemoved() {
1872 historyView.updateSelectionEditButtons();
1876 * Triggers a fade-out animation, and then removes |node| from the DOM.
1877 * @param {Node} node The node to be removed.
1878 * @param {Function?} onRemove A function to be called after the node
1879 * has been removed from the DOM.
1881 function removeNode(node, onRemove) {
1882 node.classList.add('fade-out'); // Trigger CSS fade out animation.
1884 // Delete the node when the animation is complete.
1885 node.addEventListener('webkitTransitionEnd', function(e) {
1886 node.parentNode.removeChild(node);
1888 // In case there is nested deletion happening, prevent this event from
1889 // being handled by listeners on ancestor nodes.
1890 e.stopPropagation();
1898 * Removes a single entry from the view. Also removes gaps before and after
1899 * entry if necessary.
1900 * @param {Node} entry The DOM node representing the entry to be removed.
1902 function removeEntryFromView(entry) {
1903 var nextEntry = entry.nextSibling;
1904 var previousEntry = entry.previousSibling;
1905 var dayResults = findAncestorByClass(entry, 'day-results');
1907 var toRemove = [entry];
1909 // if there is no previous entry, and the next entry is a gap, remove it
1910 if (!previousEntry && nextEntry && nextEntry.className == 'gap')
1911 toRemove.push(nextEntry);
1913 // if there is no next entry, and the previous entry is a gap, remove it
1914 if (!nextEntry && previousEntry && previousEntry.className == 'gap')
1915 toRemove.push(previousEntry);
1917 // if both the next and previous entries are gaps, remove one
1918 if (nextEntry && nextEntry.className == 'gap' &&
1919 previousEntry && previousEntry.className == 'gap') {
1920 toRemove.push(nextEntry);
1923 // If removing the last entry on a day, remove the entire day.
1924 if (dayResults && dayResults.querySelectorAll('.entry').length == 1) {
1925 toRemove.push(dayResults.previousSibling); // Remove the 'h3'.
1926 toRemove.push(dayResults);
1929 // Callback to be called when each node has finished animating. It detects
1930 // when all the animations have completed, and then calls |onEntryRemoved|.
1931 function onRemove() {
1932 for (var i = 0; i < toRemove.length; ++i) {
1933 if (toRemove[i].parentNode)
1939 // Kick off the removal process.
1940 for (var i = 0; i < toRemove.length; ++i) {
1941 removeNode(toRemove[i], onRemove);
1946 * Toggles an element in the grouped history.
1947 * @param {Element} e The element which was clicked on.
1949 function toggleHandler(e) {
1950 var innerResultList = e.currentTarget.parentElement.querySelector(
1952 var innerArrow = e.currentTarget.parentElement.querySelector(
1953 '.site-domain-arrow');
1954 if (innerArrow.classList.contains('collapse')) {
1955 innerResultList.style.height = 'auto';
1956 // -webkit-transition does not work on height:auto elements so first set
1957 // the height to auto so that it is computed and then set it to the
1958 // computed value in pixels so the transition works properly.
1959 var height = innerResultList.clientHeight;
1960 innerResultList.style.height = 0;
1961 setTimeout(function() {
1962 innerResultList.style.height = height + 'px';
1964 innerArrow.classList.remove('collapse');
1965 innerArrow.classList.add('expand');
1967 innerResultList.style.height = 0;
1968 innerArrow.classList.remove('expand');
1969 innerArrow.classList.add('collapse');
1974 * Builds the DOM elements to show the managed status of a domain/URL.
1975 * @param {ManagedModeFilteringBehavior} filteringBehavior The filter behavior
1977 * @return {Element} Returns the DOM elements which show the status.
1979 function getManagedStatusDOM(filteringBehavior) {
1980 var filterStatusDiv = createElementWithClassName('div', 'filter-status');
1981 var filteringBehaviorDiv =
1982 createElementWithClassName('div', 'filtering-behavior');
1983 filterStatusDiv.appendChild(filteringBehaviorDiv);
1985 updateHostStatus(filterStatusDiv, filteringBehavior);
1986 return filterStatusDiv;
1990 ///////////////////////////////////////////////////////////////////////////////
1991 // Chrome callbacks:
1994 * Our history system calls this function with results from searches.
1995 * @param {Object} info An object containing information about the query.
1996 * @param {Array} results A list of results.
1998 function historyResult(info, results) {
1999 historyModel.addResults(info, results);
2003 * Called by the history backend when history removal is successful.
2005 function deleteComplete() {
2006 historyModel.deleteComplete();
2010 * Called by the history backend when history removal is unsuccessful.
2012 function deleteFailed() {
2013 window.console.log('Delete failed');
2017 * Called when the history is deleted by someone else.
2019 function historyDeleted() {
2020 var anyChecked = document.querySelector('.entry input:checked') != null;
2021 // Reload the page, unless the user has any items checked.
2022 // TODO(dubroy): We should just reload the page & restore the checked items.
2024 historyView.reload();
2027 // Add handlers to HTML elements.
2028 document.addEventListener('DOMContentLoaded', load);
2030 // This event lets us enable and disable menu items before the menu is shown.
2031 document.addEventListener('canExecute', function(e) {
2032 e.canExecute = true;