| Index: chrome/browser/resources/history/history.js
|
| diff --git a/chrome/browser/resources/history/history.js b/chrome/browser/resources/history/history.js
|
| deleted file mode 100644
|
| index 5da3eaca257a8ac023169c934824522ce326383b..0000000000000000000000000000000000000000
|
| --- a/chrome/browser/resources/history/history.js
|
| +++ /dev/null
|
| @@ -1,2419 +0,0 @@
|
| -// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -// <include src="../uber/uber_utils.js">
|
| -// <include src="history_focus_manager.js">
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// Globals:
|
| -/** @const */ var RESULTS_PER_PAGE = 150;
|
| -
|
| -// Amount of time between pageviews that we consider a 'break' in browsing,
|
| -// measured in milliseconds.
|
| -/** @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000;
|
| -
|
| -// The largest bucket value for UMA histogram, based on entry ID. All entries
|
| -// with IDs greater than this will be included in this bucket.
|
| -/** @const */ var UMA_MAX_BUCKET_VALUE = 1000;
|
| -
|
| -// The largest bucket value for a UMA histogram that is a subset of above.
|
| -/** @const */ var UMA_MAX_SUBSET_BUCKET_VALUE = 100;
|
| -
|
| -// TODO(glen): Get rid of these global references, replace with a controller
|
| -// or just make the classes own more of the page.
|
| -var historyModel;
|
| -var historyView;
|
| -var pageState;
|
| -var selectionAnchor = -1;
|
| -var activeVisit = null;
|
| -
|
| -/** @const */ var Command = cr.ui.Command;
|
| -/** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
|
| -/** @const */ var Menu = cr.ui.Menu;
|
| -/** @const */ var MenuItem = cr.ui.MenuItem;
|
| -
|
| -/**
|
| - * Enum that shows the filtering behavior for a host or URL to a supervised
|
| - * user. Must behave like the FilteringBehavior enum from
|
| - * supervised_user_url_filter.h.
|
| - * @enum {number}
|
| - */
|
| -var SupervisedUserFilteringBehavior = {
|
| - ALLOW: 0,
|
| - WARN: 1,
|
| - BLOCK: 2
|
| -};
|
| -
|
| -/**
|
| - * Returns true if the mobile (non-desktop) version is being shown.
|
| - * @return {boolean} true if the mobile version is being shown.
|
| - */
|
| -function isMobileVersion() {
|
| - return !document.body.classList.contains('uber-frame');
|
| -}
|
| -
|
| -/**
|
| - * Record an action in UMA.
|
| - * @param {string} actionDesc The name of the action to be logged.
|
| - */
|
| -function recordUmaAction(actionDesc) {
|
| - chrome.send('metricsHandler:recordAction', [actionDesc]);
|
| -}
|
| -
|
| -/**
|
| - * Record a histogram value in UMA. If specified value is larger than the max
|
| - * bucket value, record the value in the largest bucket.
|
| - * @param {string} histogram The name of the histogram to be recorded in.
|
| - * @param {number} maxBucketValue The max value for the last histogram bucket.
|
| - * @param {number} value The value to record in the histogram.
|
| - */
|
| -function recordUmaHistogram(histogram, maxBucketValue, value) {
|
| - chrome.send('metricsHandler:recordInHistogram',
|
| - [histogram,
|
| - ((value > maxBucketValue) ? maxBucketValue : value),
|
| - maxBucketValue]);
|
| -}
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// Visit:
|
| -
|
| -/**
|
| - * Class to hold all the information about an entry in our model.
|
| - * @param {HistoryEntry} result An object containing the visit's data.
|
| - * @param {boolean} continued Whether this visit is on the same day as the
|
| - * visit before it.
|
| - * @param {HistoryModel} model The model object this entry belongs to.
|
| - * @constructor
|
| - */
|
| -function Visit(result, continued, model) {
|
| - this.model_ = model;
|
| - this.title_ = result.title;
|
| - this.url_ = result.url;
|
| - this.domain_ = result.domain;
|
| - this.starred_ = result.starred;
|
| - this.fallbackFaviconText_ = result.fallbackFaviconText;
|
| -
|
| - // These identify the name and type of the device on which this visit
|
| - // occurred. They will be empty if the visit occurred on the current device.
|
| - this.deviceName = result.deviceName;
|
| - this.deviceType = result.deviceType;
|
| -
|
| - // The ID will be set according to when the visit was displayed, not
|
| - // received. Set to -1 to show that it has not been set yet.
|
| - this.id_ = -1;
|
| -
|
| - this.isRendered = false; // Has the visit already been rendered on the page?
|
| -
|
| - // All the date information is public so that owners can compare properties of
|
| - // two items easily.
|
| -
|
| - this.date = new Date(result.time);
|
| -
|
| - // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
|
| - // get all of these.
|
| - this.dateRelativeDay = result.dateRelativeDay;
|
| - this.dateTimeOfDay = result.dateTimeOfDay;
|
| - this.dateShort = result.dateShort;
|
| -
|
| - // Shows the filtering behavior for that host (only used for supervised
|
| - // users).
|
| - // A value of |SupervisedUserFilteringBehavior.ALLOW| is not displayed so it
|
| - // is used as the default value.
|
| - this.hostFilteringBehavior = SupervisedUserFilteringBehavior.ALLOW;
|
| - if (result.hostFilteringBehavior)
|
| - this.hostFilteringBehavior = result.hostFilteringBehavior;
|
| -
|
| - this.blockedVisit = result.blockedVisit;
|
| -
|
| - // Whether this is the continuation of a previous day.
|
| - this.continued = continued;
|
| -
|
| - this.allTimestamps = result.allTimestamps;
|
| -}
|
| -
|
| -// Visit, public: -------------------------------------------------------------
|
| -
|
| -/**
|
| - * Returns a dom structure for a browse page result or a search page result.
|
| - * @param {Object} propertyBag A bag of configuration properties, false by
|
| - * default:
|
| - * - isSearchResult: Whether or not the result is a search result.
|
| - * - addTitleFavicon: Whether or not the favicon should be added.
|
| - * - useMonthDate: Whether or not the full date should be inserted (used for
|
| - * monthly view).
|
| - * @return {Node} A DOM node to represent the history entry or search result.
|
| - */
|
| -Visit.prototype.getResultDOM = function(propertyBag) {
|
| - var isSearchResult = propertyBag.isSearchResult || false;
|
| - var addTitleFavicon = propertyBag.addTitleFavicon || false;
|
| - var useMonthDate = propertyBag.useMonthDate || false;
|
| - var focusless = propertyBag.focusless || false;
|
| - var node = createElementWithClassName('li', 'entry');
|
| - var time = createElementWithClassName('span', 'time');
|
| - var entryBox = createElementWithClassName('div', 'entry-box');
|
| - var domain = createElementWithClassName('div', 'domain');
|
| -
|
| - this.id_ = this.model_.getNextVisitId();
|
| - var self = this;
|
| -
|
| - // Only create the checkbox if it can be used to delete an entry.
|
| - if (this.model_.editingEntriesAllowed) {
|
| - var checkbox = document.createElement('input');
|
| - checkbox.type = 'checkbox';
|
| - checkbox.id = 'checkbox-' + this.id_;
|
| - checkbox.time = this.date.getTime();
|
| - checkbox.setAttribute('aria-label', loadTimeData.getStringF(
|
| - 'entrySummary',
|
| - this.dateTimeOfDay,
|
| - this.starred_ ? loadTimeData.getString('bookmarked') : '',
|
| - this.title_,
|
| - this.domain_));
|
| - checkbox.addEventListener('click', checkboxClicked);
|
| - entryBox.appendChild(checkbox);
|
| -
|
| - if (focusless)
|
| - checkbox.tabIndex = -1;
|
| -
|
| - if (!isMobileVersion()) {
|
| - // Clicking anywhere in the entryBox will check/uncheck the checkbox.
|
| - entryBox.setAttribute('for', checkbox.id);
|
| - entryBox.addEventListener('mousedown', this.handleMousedown_.bind(this));
|
| - entryBox.addEventListener('click', entryBoxClick);
|
| - entryBox.addEventListener('keydown', this.handleKeydown_.bind(this));
|
| - }
|
| - }
|
| -
|
| - // Keep track of the drop down that triggered the menu, so we know
|
| - // which element to apply the command to.
|
| - // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
|
| - var setActiveVisit = function(e) {
|
| - activeVisit = self;
|
| - var menu = $('action-menu');
|
| - menu.dataset.devicename = self.deviceName;
|
| - menu.dataset.devicetype = self.deviceType;
|
| - };
|
| - domain.textContent = this.domain_;
|
| -
|
| - entryBox.appendChild(time);
|
| -
|
| - var bookmarkSection = createElementWithClassName(
|
| - 'button', 'bookmark-section custom-appearance');
|
| - if (this.starred_) {
|
| - bookmarkSection.title = loadTimeData.getString('removeBookmark');
|
| - bookmarkSection.classList.add('starred');
|
| - bookmarkSection.addEventListener('click', function f(e) {
|
| - recordUmaAction('HistoryPage_BookmarkStarClicked');
|
| - chrome.send('removeBookmark', [self.url_]);
|
| -
|
| - this.model_.getView().onBeforeUnstarred(this);
|
| - bookmarkSection.classList.remove('starred');
|
| - this.model_.getView().onAfterUnstarred(this);
|
| -
|
| - bookmarkSection.removeEventListener('click', f);
|
| - e.preventDefault();
|
| - }.bind(this));
|
| - }
|
| -
|
| - if (focusless)
|
| - bookmarkSection.tabIndex = -1;
|
| -
|
| - entryBox.appendChild(bookmarkSection);
|
| -
|
| - if (addTitleFavicon || this.blockedVisit) {
|
| - var faviconSection = createElementWithClassName('div', 'favicon');
|
| - if (this.blockedVisit)
|
| - faviconSection.classList.add('blocked-icon');
|
| - else
|
| - this.loadFavicon_(faviconSection);
|
| - entryBox.appendChild(faviconSection);
|
| - }
|
| -
|
| - var visitEntryWrapper = /** @type {HTMLElement} */(
|
| - entryBox.appendChild(document.createElement('div')));
|
| - if (addTitleFavicon || this.blockedVisit)
|
| - visitEntryWrapper.classList.add('visit-entry');
|
| - if (this.blockedVisit) {
|
| - visitEntryWrapper.classList.add('blocked-indicator');
|
| - visitEntryWrapper.appendChild(this.getVisitAttemptDOM_());
|
| - } else {
|
| - var title = visitEntryWrapper.appendChild(
|
| - this.getTitleDOM_(isSearchResult));
|
| -
|
| - if (focusless)
|
| - title.querySelector('a').tabIndex = -1;
|
| -
|
| - visitEntryWrapper.appendChild(domain);
|
| - }
|
| -
|
| - if (isMobileVersion()) {
|
| - if (this.model_.editingEntriesAllowed) {
|
| - var removeButton = createElementWithClassName('button', 'remove-entry');
|
| - removeButton.setAttribute('aria-label',
|
| - loadTimeData.getString('removeFromHistory'));
|
| - removeButton.classList.add('custom-appearance');
|
| - removeButton.addEventListener(
|
| - 'click', this.removeEntryFromHistory_.bind(this));
|
| - entryBox.appendChild(removeButton);
|
| -
|
| - // Support clicking anywhere inside the entry box.
|
| - entryBox.addEventListener('click', function(e) {
|
| - if (!e.defaultPrevented) {
|
| - self.titleLink.focus();
|
| - self.titleLink.click();
|
| - }
|
| - });
|
| - }
|
| - } else {
|
| - var dropDown = createElementWithClassName('button', 'drop-down');
|
| - dropDown.value = 'Open action menu';
|
| - dropDown.title = loadTimeData.getString('actionMenuDescription');
|
| - dropDown.setAttribute('menu', '#action-menu');
|
| - dropDown.setAttribute('aria-haspopup', 'true');
|
| -
|
| - if (focusless)
|
| - dropDown.tabIndex = -1;
|
| -
|
| - cr.ui.decorate(dropDown, cr.ui.MenuButton);
|
| - dropDown.respondToArrowKeys = false;
|
| -
|
| - dropDown.addEventListener('mousedown', setActiveVisit);
|
| - dropDown.addEventListener('focus', setActiveVisit);
|
| -
|
| - // Prevent clicks on the drop down from affecting the checkbox. We need to
|
| - // call blur() explicitly because preventDefault() cancels any focus
|
| - // handling.
|
| - dropDown.addEventListener('click', function(e) {
|
| - e.preventDefault();
|
| - document.activeElement.blur();
|
| - });
|
| - entryBox.appendChild(dropDown);
|
| - }
|
| -
|
| - // Let the entryBox be styled appropriately when it contains keyboard focus.
|
| - entryBox.addEventListener('focus', function() {
|
| - this.classList.add('contains-focus');
|
| - }, true);
|
| - entryBox.addEventListener('blur', function() {
|
| - this.classList.remove('contains-focus');
|
| - }, true);
|
| -
|
| - var entryBoxContainer =
|
| - createElementWithClassName('div', 'entry-box-container');
|
| - node.appendChild(entryBoxContainer);
|
| - entryBoxContainer.appendChild(entryBox);
|
| -
|
| - if (isSearchResult || useMonthDate) {
|
| - // Show the day instead of the time.
|
| - time.appendChild(document.createTextNode(this.dateShort));
|
| - } else {
|
| - time.appendChild(document.createTextNode(this.dateTimeOfDay));
|
| - }
|
| -
|
| - this.domNode_ = node;
|
| - node.visit = this;
|
| -
|
| - return node;
|
| -};
|
| -
|
| -/**
|
| - * Remove this visit from the history.
|
| - */
|
| -Visit.prototype.removeFromHistory = function() {
|
| - recordUmaAction('HistoryPage_EntryMenuRemoveFromHistory');
|
| - this.model_.removeVisitsFromHistory([this], function() {
|
| - this.model_.getView().removeVisit(this);
|
| - }.bind(this));
|
| -};
|
| -
|
| -// Closure Compiler doesn't support Object.defineProperty().
|
| -// https://github.com/google/closure-compiler/issues/302
|
| -Object.defineProperty(Visit.prototype, 'checkBox', {
|
| - get: /** @this {Visit} */function() {
|
| - return this.domNode_.querySelector('input[type=checkbox]');
|
| - },
|
| -});
|
| -
|
| -Object.defineProperty(Visit.prototype, 'bookmarkStar', {
|
| - get: /** @this {Visit} */function() {
|
| - return this.domNode_.querySelector('.bookmark-section.starred');
|
| - },
|
| -});
|
| -
|
| -Object.defineProperty(Visit.prototype, 'titleLink', {
|
| - get: /** @this {Visit} */function() {
|
| - return this.domNode_.querySelector('.title a');
|
| - },
|
| -});
|
| -
|
| -Object.defineProperty(Visit.prototype, 'dropDown', {
|
| - get: /** @this {Visit} */function() {
|
| - return this.domNode_.querySelector('button.drop-down');
|
| - },
|
| -});
|
| -
|
| -// Visit, private: ------------------------------------------------------------
|
| -
|
| -/**
|
| - * Add child text nodes to a node such that occurrences of the specified text is
|
| - * highlighted.
|
| - * @param {Node} node The node under which new text nodes will be made as
|
| - * children.
|
| - * @param {string} content Text to be added beneath |node| as one or more
|
| - * text nodes.
|
| - * @param {string} highlightText Occurences of this text inside |content| will
|
| - * be highlighted.
|
| - * @private
|
| - */
|
| -Visit.prototype.addHighlightedText_ = function(node, content, highlightText) {
|
| - var i = 0;
|
| - if (highlightText) {
|
| - var re = new RegExp(quoteString(highlightText), 'gim');
|
| - var match;
|
| - while (match = re.exec(content)) {
|
| - if (match.index > i)
|
| - node.appendChild(document.createTextNode(content.slice(i,
|
| - match.index)));
|
| - i = re.lastIndex;
|
| - // Mark the highlighted text in bold.
|
| - var b = document.createElement('b');
|
| - b.textContent = content.substring(match.index, i);
|
| - node.appendChild(b);
|
| - }
|
| - }
|
| - if (i < content.length)
|
| - node.appendChild(document.createTextNode(content.slice(i)));
|
| -};
|
| -
|
| -/**
|
| - * Returns the DOM element containing a link on the title of the URL for the
|
| - * current visit.
|
| - * @param {boolean} isSearchResult Whether or not the entry is a search result.
|
| - * @return {Element} DOM representation for the title block.
|
| - * @private
|
| - */
|
| -Visit.prototype.getTitleDOM_ = function(isSearchResult) {
|
| - var node = createElementWithClassName('div', 'title');
|
| - var link = document.createElement('a');
|
| - link.href = this.url_;
|
| - link.id = 'id-' + this.id_;
|
| - link.target = '_top';
|
| - var integerId = parseInt(this.id_, 10);
|
| - link.addEventListener('click', function() {
|
| - recordUmaAction('HistoryPage_EntryLinkClick');
|
| - // Record the ID of the entry to signify how many entries are above this
|
| - // link on the page.
|
| - recordUmaHistogram('HistoryPage.ClickPosition',
|
| - UMA_MAX_BUCKET_VALUE,
|
| - integerId);
|
| - if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
|
| - recordUmaHistogram('HistoryPage.ClickPositionSubset',
|
| - UMA_MAX_SUBSET_BUCKET_VALUE,
|
| - integerId);
|
| - }
|
| - });
|
| - link.addEventListener('contextmenu', function() {
|
| - recordUmaAction('HistoryPage_EntryLinkRightClick');
|
| - });
|
| -
|
| - if (isSearchResult) {
|
| - link.addEventListener('click', function() {
|
| - recordUmaAction('HistoryPage_SearchResultClick');
|
| - });
|
| - }
|
| -
|
| - // Add a tooltip, since it might be ellipsized.
|
| - // TODO(dubroy): Find a way to show the tooltip only when necessary.
|
| - link.title = this.title_;
|
| -
|
| - this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
|
| - node.appendChild(link);
|
| -
|
| - return node;
|
| -};
|
| -
|
| -/**
|
| - * Returns the DOM element containing the text for a blocked visit attempt.
|
| - * @return {Element} DOM representation of the visit attempt.
|
| - * @private
|
| - */
|
| -Visit.prototype.getVisitAttemptDOM_ = function() {
|
| - var node = createElementWithClassName('div', 'title');
|
| - node.innerHTML = loadTimeData.getStringF('blockedVisitText',
|
| - this.url_,
|
| - this.id_,
|
| - this.domain_);
|
| - return node;
|
| -};
|
| -
|
| -/**
|
| - * Load the favicon for an element.
|
| - * @param {Element} faviconDiv The DOM element for which to load the icon.
|
| - * @private
|
| - */
|
| -Visit.prototype.loadFavicon_ = function(faviconDiv) {
|
| - if (cr.isAndroid) {
|
| - // On Android, if a large icon is unavailable, an HTML/CSS fallback favicon
|
| - // is generated because Android does not yet support text drawing in native.
|
| -
|
| - // Check whether a fallback favicon needs to be generated.
|
| - var desiredPixelSize = 32 * window.devicePixelRatio;
|
| - var img = new Image();
|
| - img.onload = this.onLargeFaviconLoadedAndroid_.bind(this, faviconDiv);
|
| - img.src = 'chrome://large-icon/' + desiredPixelSize + '/' + this.url_;
|
| - } else {
|
| - faviconDiv.style.backgroundImage = cr.icon.getFavicon(this.url_);
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Called when the chrome://large-icon image has finished loading.
|
| - * @param {Element} faviconDiv The DOM element to add the favicon to.
|
| - * @param {Event} event The onload event.
|
| - * @private
|
| - */
|
| -Visit.prototype.onLargeFaviconLoadedAndroid_ = function(faviconDiv, event) {
|
| - // The loaded image should either:
|
| - // - Have the desired size.
|
| - // OR
|
| - // - Be 1x1 px with the background color for the fallback icon.
|
| - var loadedImg = event.target;
|
| - if (loadedImg.width == 1) {
|
| - faviconDiv.classList.add('fallback-favicon');
|
| - faviconDiv.textContent = this.fallbackFaviconText_;
|
| - }
|
| - faviconDiv.style.backgroundImage = url(loadedImg.src);
|
| -};
|
| -
|
| -/**
|
| - * Launch a search for more history entries from the same domain.
|
| - * @private
|
| - */
|
| -Visit.prototype.showMoreFromSite_ = function() {
|
| - recordUmaAction('HistoryPage_EntryMenuShowMoreFromSite');
|
| - historyView.setSearch(this.domain_);
|
| - $('search-field').focus();
|
| -};
|
| -
|
| -/**
|
| - * @param {Event} e A keydown event to handle.
|
| - * @private
|
| - */
|
| -Visit.prototype.handleKeydown_ = function(e) {
|
| - // Delete or Backspace should delete the entry if allowed.
|
| - if (e.key == 'Backspace' || e.key == 'Delete')
|
| - this.removeEntryFromHistory_(e);
|
| -};
|
| -
|
| -/**
|
| - * @param {Event} event A mousedown event.
|
| - * @private
|
| - */
|
| -Visit.prototype.handleMousedown_ = function(event) {
|
| - // Prevent text selection when shift-clicking to select multiple entries.
|
| - if (event.shiftKey) {
|
| - event.preventDefault();
|
| -
|
| - var target = assertInstanceof(event.target, HTMLElement);
|
| - if (this.model_.getView().isInFocusGrid(target))
|
| - target.focus();
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Removes a history entry on click or keydown and finds a new entry to focus.
|
| - * @param {Event} e A click or keydown event.
|
| - * @private
|
| - */
|
| -Visit.prototype.removeEntryFromHistory_ = function(e) {
|
| - if (!this.model_.deletingHistoryAllowed || this.model_.isDeletingVisits() ||
|
| - this.domNode_.classList.contains('fade-out')) {
|
| - return;
|
| - }
|
| -
|
| - this.model_.getView().onBeforeRemove(this);
|
| - this.removeFromHistory();
|
| - e.preventDefault();
|
| -};
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// HistoryModel:
|
| -
|
| -/**
|
| - * Global container for history data. Future optimizations might include
|
| - * allowing the creation of a HistoryModel for each search string, allowing
|
| - * quick flips back and forth between results.
|
| - *
|
| - * The history model is based around pages, and only fetching the data to
|
| - * fill the currently requested page. This is somewhat dependent on the view,
|
| - * and so future work may wish to change history model to operate on
|
| - * timeframe (day or week) based containers.
|
| - *
|
| - * @constructor
|
| - */
|
| -function HistoryModel() {
|
| - this.clearModel_();
|
| -}
|
| -
|
| -// HistoryModel, Public: ------------------------------------------------------
|
| -
|
| -/** @enum {number} */
|
| -HistoryModel.Range = {
|
| - ALL_TIME: 0,
|
| - WEEK: 1,
|
| - MONTH: 2
|
| -};
|
| -
|
| -/**
|
| - * Sets our current view that is called when the history model changes.
|
| - * @param {HistoryView} view The view to set our current view to.
|
| - */
|
| -HistoryModel.prototype.setView = function(view) {
|
| - this.view_ = view;
|
| -};
|
| -
|
| -
|
| -/**
|
| - * @return {HistoryView|undefined} Returns the view for this model (if set).
|
| - */
|
| -HistoryModel.prototype.getView = function() {
|
| - return this.view_;
|
| -};
|
| -
|
| -/**
|
| - * Reload our model with the current parameters.
|
| - */
|
| -HistoryModel.prototype.reload = function() {
|
| - // Save user-visible state, clear the model, and restore the state.
|
| - var search = this.searchText_;
|
| - var page = this.requestedPage_;
|
| - var range = this.rangeInDays_;
|
| - var offset = this.offset_;
|
| - var groupByDomain = this.groupByDomain_;
|
| -
|
| - this.clearModel_();
|
| - this.searchText_ = search;
|
| - this.requestedPage_ = page;
|
| - this.rangeInDays_ = range;
|
| - this.offset_ = offset;
|
| - this.groupByDomain_ = groupByDomain;
|
| - this.queryHistory_();
|
| -};
|
| -
|
| -/**
|
| - * @return {string} The current search text.
|
| - */
|
| -HistoryModel.prototype.getSearchText = function() {
|
| - return this.searchText_;
|
| -};
|
| -
|
| -/**
|
| - * Tell the model that the view will want to see the current page. When
|
| - * the data becomes available, the model will call the view back.
|
| - * @param {number} page The page we want to view.
|
| - */
|
| -HistoryModel.prototype.requestPage = function(page) {
|
| - this.requestedPage_ = page;
|
| - this.updateSearch_();
|
| -};
|
| -
|
| -/**
|
| - * Receiver for history query.
|
| - * @param {HistoryQuery} info An object containing information about the query.
|
| - * @param {Array<HistoryEntry>} results A list of results.
|
| - */
|
| -HistoryModel.prototype.addResults = function(info, results) {
|
| - // If no requests are in flight then this was an old request so we drop the
|
| - // results. Double check the search term as well.
|
| - if (!this.inFlight_ || info.term != this.searchText_)
|
| - return;
|
| -
|
| - $('loading-spinner').hidden = true;
|
| - this.inFlight_ = false;
|
| - this.isQueryFinished_ = info.finished;
|
| - this.queryInterval = info.queryInterval;
|
| -
|
| - var lastVisit = this.visits_.slice(-1)[0];
|
| - var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
|
| -
|
| - for (var i = 0, result; result = results[i]; i++) {
|
| - var thisDay = result.dateRelativeDay;
|
| - var isSameDay = lastDay == thisDay;
|
| - this.visits_.push(new Visit(result, isSameDay, this));
|
| - lastDay = thisDay;
|
| - }
|
| -
|
| - this.updateSearch_();
|
| -};
|
| -
|
| -/**
|
| - * @return {number} The number of visits in the model.
|
| - */
|
| -HistoryModel.prototype.getSize = function() {
|
| - return this.visits_.length;
|
| -};
|
| -
|
| -/**
|
| - * Get a list of visits between specified index positions.
|
| - * @param {number} start The start index.
|
| - * @param {number} end The end index.
|
| - * @return {Array<Visit>} A list of visits.
|
| - */
|
| -HistoryModel.prototype.getNumberedRange = function(start, end) {
|
| - return this.visits_.slice(start, end);
|
| -};
|
| -
|
| -/**
|
| - * Return true if there are more results beyond the current page.
|
| - * @return {boolean} true if the there are more results, otherwise false.
|
| - */
|
| -HistoryModel.prototype.hasMoreResults = function() {
|
| - return this.haveDataForPage_(this.requestedPage_ + 1) ||
|
| - !this.isQueryFinished_;
|
| -};
|
| -
|
| -/**
|
| - * Removes a list of visits from the history, and calls |callback| when the
|
| - * removal has successfully completed.
|
| - * @param {Array<Visit>} visits The visits to remove.
|
| - * @param {Function} callback The function to call after removal succeeds.
|
| - */
|
| -HistoryModel.prototype.removeVisitsFromHistory = function(visits, callback) {
|
| - assert(this.deletingHistoryAllowed);
|
| -
|
| - var toBeRemoved = [];
|
| - for (var i = 0; i < visits.length; i++) {
|
| - toBeRemoved.push({
|
| - url: visits[i].url_,
|
| - timestamps: visits[i].allTimestamps
|
| - });
|
| - }
|
| -
|
| - this.deleteCompleteCallback_ = callback;
|
| - chrome.send('removeVisits', toBeRemoved);
|
| -};
|
| -
|
| -/** @return {boolean} Whether the model is currently deleting a visit. */
|
| -HistoryModel.prototype.isDeletingVisits = function() {
|
| - return !!this.deleteCompleteCallback_;
|
| -};
|
| -
|
| -/**
|
| - * Called when visits have been succesfully removed from the history.
|
| - */
|
| -HistoryModel.prototype.deleteComplete = function() {
|
| - // Call the callback, with 'this' undefined inside the callback.
|
| - this.deleteCompleteCallback_.call();
|
| - this.deleteCompleteCallback_ = null;
|
| -};
|
| -
|
| -// Getter and setter for HistoryModel.rangeInDays_.
|
| -Object.defineProperty(HistoryModel.prototype, 'rangeInDays', {
|
| - get: /** @this {HistoryModel} */function() {
|
| - return this.rangeInDays_;
|
| - },
|
| - set: /** @this {HistoryModel} */function(range) {
|
| - this.rangeInDays_ = range;
|
| - }
|
| -});
|
| -
|
| -/**
|
| - * Getter and setter for HistoryModel.offset_. The offset moves the current
|
| - * query 'window' |range| days behind. As such for range set to WEEK an offset
|
| - * of 0 refers to the last 7 days, an offset of 1 refers to the 7 day period
|
| - * that ended 7 days ago, etc. For MONTH an offset of 0 refers to the current
|
| - * calendar month, 1 to the previous one, etc.
|
| - */
|
| -Object.defineProperty(HistoryModel.prototype, 'offset', {
|
| - get: /** @this {HistoryModel} */function() {
|
| - return this.offset_;
|
| - },
|
| - set: /** @this {HistoryModel} */function(offset) {
|
| - this.offset_ = offset;
|
| - }
|
| -});
|
| -
|
| -// Setter for HistoryModel.requestedPage_.
|
| -Object.defineProperty(HistoryModel.prototype, 'requestedPage', {
|
| - set: /** @this {HistoryModel} */function(page) {
|
| - this.requestedPage_ = page;
|
| - }
|
| -});
|
| -
|
| -/**
|
| - * Removes |visit| from this model.
|
| - * @param {Visit} visit A visit to remove.
|
| - */
|
| -HistoryModel.prototype.removeVisit = function(visit) {
|
| - var index = this.visits_.indexOf(visit);
|
| - if (index >= 0)
|
| - this.visits_.splice(index, 1);
|
| -};
|
| -
|
| -/**
|
| - * Automatically generates a new visit ID.
|
| - * @return {number} The next visit ID.
|
| - */
|
| -HistoryModel.prototype.getNextVisitId = function() {
|
| - return this.nextVisitId_++;
|
| -};
|
| -
|
| -// HistoryModel, Private: -----------------------------------------------------
|
| -
|
| -/**
|
| - * Clear the history model.
|
| - * @private
|
| - */
|
| -HistoryModel.prototype.clearModel_ = function() {
|
| - this.inFlight_ = false; // Whether a query is inflight.
|
| - this.searchText_ = '';
|
| - // Whether this user is a supervised user.
|
| - this.isSupervisedProfile = loadTimeData.getBoolean('isSupervisedProfile');
|
| - this.deletingHistoryAllowed = loadTimeData.getBoolean('allowDeletingHistory');
|
| -
|
| - // Only create checkboxes for editing entries if they can be used either to
|
| - // delete an entry or to block/allow it.
|
| - this.editingEntriesAllowed = this.deletingHistoryAllowed;
|
| -
|
| - // Flag to show that the results are grouped by domain or not.
|
| - this.groupByDomain_ = false;
|
| -
|
| - this.visits_ = []; // Date-sorted list of visits (most recent first).
|
| - this.nextVisitId_ = 0;
|
| - selectionAnchor = -1;
|
| -
|
| - // The page that the view wants to see - we only fetch slightly past this
|
| - // point. If the view requests a page that we don't have data for, we try
|
| - // to fetch it and call back when we're done.
|
| - this.requestedPage_ = 0;
|
| -
|
| - // The range of history to view or search over.
|
| - this.rangeInDays_ = HistoryModel.Range.ALL_TIME;
|
| -
|
| - // Skip |offset_| * weeks/months from the begining.
|
| - this.offset_ = 0;
|
| -
|
| - // Keeps track of whether or not there are more results available than are
|
| - // currently held in |this.visits_|.
|
| - this.isQueryFinished_ = false;
|
| -
|
| - if (this.view_)
|
| - this.view_.clear_();
|
| -};
|
| -
|
| -/**
|
| - * Figure out if we need to do more queries to fill the currently requested
|
| - * page. If we think we can fill the page, call the view and let it know
|
| - * we're ready to show something. This only applies to the daily time-based
|
| - * view.
|
| - * @private
|
| - */
|
| -HistoryModel.prototype.updateSearch_ = function() {
|
| - var doneLoading = this.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
|
| - this.isQueryFinished_ ||
|
| - this.canFillPage_(this.requestedPage_);
|
| -
|
| - // Try to fetch more results if more results can arrive and the page is not
|
| - // full.
|
| - if (!doneLoading && !this.inFlight_)
|
| - this.queryHistory_();
|
| -
|
| - // Show the result or a message if no results were returned.
|
| - this.view_.onModelReady(doneLoading);
|
| -};
|
| -
|
| -/**
|
| - * Query for history, either for a search or time-based browsing.
|
| - * @private
|
| - */
|
| -HistoryModel.prototype.queryHistory_ = function() {
|
| - var maxResults =
|
| - (this.rangeInDays_ == HistoryModel.Range.ALL_TIME) ? RESULTS_PER_PAGE : 0;
|
| -
|
| - // If there are already some visits, pick up the previous query where it
|
| - // left off.
|
| - var lastVisit = this.visits_.slice(-1)[0];
|
| - var endTime = lastVisit ? lastVisit.date.getTime() : 0;
|
| -
|
| - $('loading-spinner').hidden = false;
|
| - this.inFlight_ = true;
|
| - chrome.send('queryHistory',
|
| - [this.searchText_, this.offset_, this.rangeInDays_, endTime, maxResults]);
|
| -};
|
| -
|
| -/**
|
| - * Check to see if we have data for the given page.
|
| - * @param {number} page The page number.
|
| - * @return {boolean} Whether we have any data for the given page.
|
| - * @private
|
| - */
|
| -HistoryModel.prototype.haveDataForPage_ = function(page) {
|
| - return page * RESULTS_PER_PAGE < this.getSize();
|
| -};
|
| -
|
| -/**
|
| - * Check to see if we have data to fill the given page.
|
| - * @param {number} page The page number.
|
| - * @return {boolean} Whether we have data to fill the page.
|
| - * @private
|
| - */
|
| -HistoryModel.prototype.canFillPage_ = function(page) {
|
| - return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
|
| -};
|
| -
|
| -/**
|
| - * Gets whether we are grouped by domain.
|
| - * @return {boolean} Whether the results are grouped by domain.
|
| - */
|
| -HistoryModel.prototype.getGroupByDomain = function() {
|
| - return this.groupByDomain_;
|
| -};
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// HistoryFocusRow:
|
| -
|
| -/**
|
| - * Provides an implementation for a single column grid.
|
| - * @param {!Element} root
|
| - * @param {?Element} boundary
|
| - * @constructor
|
| - * @extends {cr.ui.FocusRow}
|
| - */
|
| -function HistoryFocusRow(root, boundary) {
|
| - cr.ui.FocusRow.call(this, root, boundary);
|
| -
|
| - // None of these are guaranteed to exist in all versions of the UI.
|
| - this.addItem('checkbox', '.entry-box input');
|
| - this.addItem('checkbox', '.domain-checkbox');
|
| - this.addItem('star', '.bookmark-section.starred');
|
| - this.addItem('domain', '[is="action-link"]');
|
| - this.addItem('title', '.title a');
|
| - this.addItem('menu', '.drop-down');
|
| -}
|
| -
|
| -HistoryFocusRow.prototype = {
|
| - __proto__: cr.ui.FocusRow.prototype,
|
| -
|
| - /** @override */
|
| - getCustomEquivalent: function(sampleElement) {
|
| - var equivalent;
|
| -
|
| - switch (this.getTypeForElement(sampleElement)) {
|
| - case 'star':
|
| - equivalent = this.getFirstFocusable('title') ||
|
| - this.getFirstFocusable('domain');
|
| - break;
|
| - case 'domain':
|
| - equivalent = this.getFirstFocusable('title');
|
| - break;
|
| - case 'title':
|
| - equivalent = this.getFirstFocusable('domain');
|
| - break;
|
| - case 'menu':
|
| - equivalent = this.getFocusableElements().slice(-1)[0];
|
| - break;
|
| - }
|
| -
|
| - return equivalent ||
|
| - cr.ui.FocusRow.prototype.getCustomEquivalent.call(this, sampleElement);
|
| - },
|
| -};
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// HistoryView:
|
| -
|
| -/**
|
| - * Functions and state for populating the page with HTML. This should one-day
|
| - * contain the view and use event handlers, rather than pushing HTML out and
|
| - * getting called externally.
|
| - * @param {HistoryModel} model The model backing this view.
|
| - * @constructor
|
| - */
|
| -function HistoryView(model) {
|
| - this.editButtonTd_ = $('edit-button');
|
| - this.editingControlsDiv_ = $('editing-controls');
|
| - this.resultDiv_ = $('results-display');
|
| - this.focusGrid_ = new cr.ui.FocusGrid();
|
| - this.pageDiv_ = $('results-pagination');
|
| - this.model_ = model;
|
| - this.pageIndex_ = 0;
|
| - this.lastDisplayed_ = [];
|
| - this.hasRenderedResults_ = false;
|
| -
|
| - this.model_.setView(this);
|
| -
|
| - this.currentVisits_ = [];
|
| -
|
| - // If there is no search button, use the search button label as placeholder
|
| - // text in the search field.
|
| - if ($('search-button').offsetWidth == 0)
|
| - $('search-field').placeholder = $('search-button').value;
|
| -
|
| - var self = this;
|
| -
|
| - $('clear-browsing-data').addEventListener('click', openClearBrowsingData);
|
| - $('remove-selected').addEventListener('click', removeItems);
|
| -
|
| - // Add handlers for the page navigation buttons at the bottom.
|
| - $('newest-button').addEventListener('click', function() {
|
| - recordUmaAction('HistoryPage_NewestHistoryClick');
|
| - self.setPage(0);
|
| - });
|
| - $('newer-button').addEventListener('click', function() {
|
| - recordUmaAction('HistoryPage_NewerHistoryClick');
|
| - self.setPage(self.pageIndex_ - 1);
|
| - });
|
| - $('older-button').addEventListener('click', function() {
|
| - recordUmaAction('HistoryPage_OlderHistoryClick');
|
| - self.setPage(self.pageIndex_ + 1);
|
| - });
|
| -
|
| - $('timeframe-controls').onchange = function(e) {
|
| - var value = parseInt(e.target.value, 10);
|
| - self.setRangeInDays(/** @type {HistoryModel.Range<number>} */(value));
|
| - };
|
| -
|
| - $('range-previous').addEventListener('click', function(e) {
|
| - if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
|
| - self.setPage(self.pageIndex_ + 1);
|
| - else
|
| - self.setOffset(self.getOffset() + 1);
|
| - });
|
| - $('range-next').addEventListener('click', function(e) {
|
| - if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
|
| - self.setPage(self.pageIndex_ - 1);
|
| - else
|
| - self.setOffset(self.getOffset() - 1);
|
| - });
|
| - $('range-today').addEventListener('click', function(e) {
|
| - if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
|
| - self.setPage(0);
|
| - else
|
| - self.setOffset(0);
|
| - });
|
| -}
|
| -
|
| -// HistoryView, public: -------------------------------------------------------
|
| -/**
|
| - * Do a search on a specific term.
|
| - * @param {string} term The string to search for.
|
| - */
|
| -HistoryView.prototype.setSearch = function(term) {
|
| - window.scrollTo(0, 0);
|
| - this.setPageState(term, 0, this.getRangeInDays(), this.getOffset());
|
| -};
|
| -
|
| -/**
|
| - * Reload the current view.
|
| - */
|
| -HistoryView.prototype.reload = function() {
|
| - this.model_.reload();
|
| - this.updateSelectionEditButtons();
|
| - this.updateRangeButtons_();
|
| -};
|
| -
|
| -/**
|
| - * Sets all the parameters for the history page and then reloads the view to
|
| - * update the results.
|
| - * @param {string} searchText The search string to set.
|
| - * @param {number} page The page to be viewed.
|
| - * @param {HistoryModel.Range} range The range to view or search over.
|
| - * @param {number} offset Set the begining of the query to the specific offset.
|
| - */
|
| -HistoryView.prototype.setPageState = function(searchText, page, range, offset) {
|
| - this.clear_();
|
| - this.model_.searchText_ = searchText;
|
| - this.pageIndex_ = page;
|
| - this.model_.requestedPage_ = page;
|
| - this.model_.rangeInDays_ = range;
|
| - this.model_.groupByDomain_ = false;
|
| - if (range != HistoryModel.Range.ALL_TIME)
|
| - this.model_.groupByDomain_ = true;
|
| - this.model_.offset_ = offset;
|
| - this.reload();
|
| - pageState.setUIState(this.model_.getSearchText(),
|
| - this.pageIndex_,
|
| - this.getRangeInDays(),
|
| - this.getOffset());
|
| -};
|
| -
|
| -/**
|
| - * Switch to a specified page.
|
| - * @param {number} page The page we wish to view.
|
| - */
|
| -HistoryView.prototype.setPage = function(page) {
|
| - // TODO(sergiu): Move this function to setPageState as well and see why one
|
| - // of the tests fails when using setPageState.
|
| - this.clear_();
|
| - this.pageIndex_ = parseInt(page, 10);
|
| - window.scrollTo(0, 0);
|
| - this.model_.requestPage(page);
|
| - pageState.setUIState(this.model_.getSearchText(),
|
| - this.pageIndex_,
|
| - this.getRangeInDays(),
|
| - this.getOffset());
|
| -};
|
| -
|
| -/**
|
| - * @return {number} The page number being viewed.
|
| - */
|
| -HistoryView.prototype.getPage = function() {
|
| - return this.pageIndex_;
|
| -};
|
| -
|
| -/**
|
| - * Set the current range for grouped results.
|
| - * @param {HistoryModel.Range} range The number of days to which the range
|
| - * should be set.
|
| - */
|
| -HistoryView.prototype.setRangeInDays = function(range) {
|
| - // Set the range, offset and reset the page.
|
| - this.setPageState(this.model_.getSearchText(), 0, range, 0);
|
| -};
|
| -
|
| -/**
|
| - * Get the current range in days.
|
| - * @return {HistoryModel.Range} Current range in days from the model.
|
| - */
|
| -HistoryView.prototype.getRangeInDays = function() {
|
| - return this.model_.rangeInDays;
|
| -};
|
| -
|
| -/**
|
| - * Set the current offset for grouped results.
|
| - * @param {number} offset Offset to set.
|
| - */
|
| -HistoryView.prototype.setOffset = function(offset) {
|
| - // If there is another query already in flight wait for that to complete.
|
| - if (this.model_.inFlight_)
|
| - return;
|
| - this.setPageState(this.model_.getSearchText(),
|
| - this.pageIndex_,
|
| - this.getRangeInDays(),
|
| - offset);
|
| -};
|
| -
|
| -/**
|
| - * Get the current offset.
|
| - * @return {number} Current offset from the model.
|
| - */
|
| -HistoryView.prototype.getOffset = function() {
|
| - return this.model_.offset;
|
| -};
|
| -
|
| -/**
|
| - * Callback for the history model to let it know that it has data ready for us
|
| - * to view.
|
| - * @param {boolean} doneLoading Whether the current request is complete.
|
| - */
|
| -HistoryView.prototype.onModelReady = function(doneLoading) {
|
| - this.displayResults_(doneLoading);
|
| -
|
| - // Allow custom styling based on whether there are any results on the page.
|
| - // To make this easier, add a class to the body if there are any results.
|
| - var hasResults = this.model_.visits_.length > 0;
|
| - document.body.classList.toggle('has-results', hasResults);
|
| -
|
| - this.updateFocusGrid_();
|
| - this.updateNavBar_();
|
| -
|
| - if (isMobileVersion()) {
|
| - // Hide the search field if it is empty and there are no results.
|
| - var isSearch = this.model_.getSearchText().length > 0;
|
| - $('search-field').hidden = !(hasResults || isSearch);
|
| - }
|
| -
|
| - if (!this.hasRenderedResults_) {
|
| - this.hasRenderedResults_ = true;
|
| - setTimeout(function() {
|
| - chrome.send(
|
| - 'metricsHandler:recordTime',
|
| - ['History.ResultsRenderedTime', window.performance.now()]);
|
| - });
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Enables or disables the buttons that control editing entries depending on
|
| - * whether there are any checked boxes.
|
| - */
|
| -HistoryView.prototype.updateSelectionEditButtons = function() {
|
| - if (loadTimeData.getBoolean('allowDeletingHistory')) {
|
| - var anyChecked = document.querySelector('.entry input:checked') != null;
|
| - $('remove-selected').disabled = !anyChecked;
|
| - } else {
|
| - $('remove-selected').disabled = true;
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Shows the notification bar at the top of the page with |innerHTML| as its
|
| - * content.
|
| - * @param {string} innerHTML The HTML content of the warning.
|
| - * @param {boolean} isWarning If true, style the notification as a warning.
|
| - */
|
| -HistoryView.prototype.showNotification = function(innerHTML, isWarning) {
|
| - var bar = $('notification-bar');
|
| - bar.innerHTML = innerHTML;
|
| - bar.hidden = false;
|
| - if (isWarning)
|
| - bar.classList.add('warning');
|
| - else
|
| - bar.classList.remove('warning');
|
| -
|
| - // Make sure that any links in the HTML are targeting the top level.
|
| - var links = bar.querySelectorAll('a');
|
| - for (var i = 0; i < links.length; i++)
|
| - links[i].target = '_top';
|
| -
|
| - this.positionNotificationBar();
|
| -};
|
| -
|
| -/**
|
| - * Shows a notification about whether there are any synced results, and whether
|
| - * there are other forms of browsing history on the server.
|
| - * @param {boolean} hasSyncedResults Whether there are synced results.
|
| - * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
|
| - * a sentence about the existence of other forms of browsing history.
|
| - */
|
| -HistoryView.prototype.showWebHistoryNotification = function(
|
| - hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
|
| - var message = '';
|
| -
|
| - if (loadTimeData.getBoolean('isUserSignedIn')) {
|
| - message += '<span>' + loadTimeData.getString(
|
| - hasSyncedResults ? 'hasSyncedResults' : 'noSyncedResults') + '</span>';
|
| - }
|
| -
|
| - if (includeOtherFormsOfBrowsingHistory) {
|
| - message += ' ' /* A whitespace to separate <span>s. */ + '<span>' +
|
| - loadTimeData.getString('otherFormsOfBrowsingHistory') + '</span>';
|
| - }
|
| -
|
| - if (message)
|
| - this.showNotification(message, false /* isWarning */);
|
| -};
|
| -
|
| -/**
|
| - * @param {Visit} visit The visit about to be removed from this view.
|
| - */
|
| -HistoryView.prototype.onBeforeRemove = function(visit) {
|
| - assert(this.currentVisits_.indexOf(visit) >= 0);
|
| -
|
| - var rowIndex = this.focusGrid_.getRowIndexForTarget(document.activeElement);
|
| - if (rowIndex == -1)
|
| - return;
|
| -
|
| - var rowToFocus = this.focusGrid_.rows[rowIndex + 1] ||
|
| - this.focusGrid_.rows[rowIndex - 1];
|
| - if (rowToFocus)
|
| - rowToFocus.getEquivalentElement(document.activeElement).focus();
|
| -};
|
| -
|
| -/** @param {Visit} visit The visit about to be unstarred. */
|
| -HistoryView.prototype.onBeforeUnstarred = function(visit) {
|
| - assert(this.currentVisits_.indexOf(visit) >= 0);
|
| - assert(visit.bookmarkStar == document.activeElement);
|
| -
|
| - var rowIndex = this.focusGrid_.getRowIndexForTarget(document.activeElement);
|
| - var row = this.focusGrid_.rows[rowIndex];
|
| -
|
| - // Focus the title or domain when the bookmarked star is removed because the
|
| - // star will no longer be focusable.
|
| - row.root.querySelector('[focus-type=title], [focus-type=domain]').focus();
|
| -};
|
| -
|
| -/** @param {Visit} visit The visit that was just unstarred. */
|
| -HistoryView.prototype.onAfterUnstarred = function(visit) {
|
| - this.updateFocusGrid_();
|
| -};
|
| -
|
| -/**
|
| - * Removes a single entry from the view. Also removes gaps before and after
|
| - * entry if necessary.
|
| - * @param {Visit} visit The visit to be removed.
|
| - */
|
| -HistoryView.prototype.removeVisit = function(visit) {
|
| - var entry = visit.domNode_;
|
| - var previousEntry = entry.previousSibling;
|
| - var nextEntry = entry.nextSibling;
|
| - var toRemove = [entry];
|
| -
|
| - // If there is no previous entry, and the next entry is a gap, remove it.
|
| - if (!previousEntry && nextEntry && nextEntry.classList.contains('gap'))
|
| - toRemove.push(nextEntry);
|
| -
|
| - // If there is no next entry, and the previous entry is a gap, remove it.
|
| - if (!nextEntry && previousEntry && previousEntry.classList.contains('gap'))
|
| - toRemove.push(previousEntry);
|
| -
|
| - // If both the next and previous entries are gaps, remove the next one.
|
| - if (nextEntry && nextEntry.classList.contains('gap') &&
|
| - previousEntry && previousEntry.classList.contains('gap')) {
|
| - toRemove.push(nextEntry);
|
| - }
|
| -
|
| - // If removing the last entry on a day, remove the entire day.
|
| - var dayResults = findAncestorByClass(entry, 'day-results');
|
| - if (dayResults && dayResults.querySelectorAll('.entry').length <= 1) {
|
| - toRemove.push(dayResults.previousSibling); // Remove the 'h3'.
|
| - toRemove.push(dayResults);
|
| - }
|
| -
|
| - // Callback to be called when each node has finished animating. It detects
|
| - // when all the animations have completed.
|
| - function onRemove() {
|
| - for (var i = 0; i < toRemove.length; ++i) {
|
| - if (toRemove[i].parentNode)
|
| - return;
|
| - }
|
| - onEntryRemoved();
|
| - }
|
| -
|
| - // Kick off the removal process.
|
| - for (var i = 0; i < toRemove.length; ++i) {
|
| - removeNode(toRemove[i], onRemove, this);
|
| - }
|
| - this.updateFocusGrid_();
|
| -
|
| - var index = this.currentVisits_.indexOf(visit);
|
| - if (index >= 0)
|
| - this.currentVisits_.splice(index, 1);
|
| -
|
| - this.model_.removeVisit(visit);
|
| -};
|
| -
|
| -/**
|
| - * Called when an individual history entry has been removed from the page.
|
| - * This will only be called when all the elements affected by the deletion
|
| - * have been removed from the DOM and the animations have completed.
|
| - */
|
| -HistoryView.prototype.onEntryRemoved = function() {
|
| - this.updateSelectionEditButtons();
|
| -
|
| - if (this.model_.getSize() == 0) {
|
| - this.clear_();
|
| - this.onModelReady(true); // Shows "No entries" message.
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Adjusts the position of the notification bar based on the size of the page.
|
| - */
|
| -HistoryView.prototype.positionNotificationBar = function() {
|
| - var bar = $('notification-bar');
|
| - var container = $('top-container');
|
| -
|
| - // If the bar does not fit beside the editing controls, or if it contains
|
| - // more than one message, put it into the overflow state.
|
| - var shouldOverflow =
|
| - (bar.getBoundingClientRect().top >=
|
| - $('editing-controls').getBoundingClientRect().bottom) ||
|
| - bar.childElementCount > 1;
|
| - container.classList.toggle('overflow', shouldOverflow);
|
| -};
|
| -
|
| -/**
|
| - * @param {!Element} el An element to look for.
|
| - * @return {boolean} Whether |el| is in |this.focusGrid_|.
|
| - */
|
| -HistoryView.prototype.isInFocusGrid = function(el) {
|
| - return this.focusGrid_.getRowIndexForTarget(el) != -1;
|
| -};
|
| -
|
| -// HistoryView, private: ------------------------------------------------------
|
| -
|
| -/**
|
| - * Clear the results in the view. Since we add results piecemeal, we need
|
| - * to clear them out when we switch to a new page or reload.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.clear_ = function() {
|
| - var alertOverlay = $('alertOverlay');
|
| - if (alertOverlay && alertOverlay.classList.contains('showing'))
|
| - hideConfirmationOverlay();
|
| -
|
| - // Remove everything but <h3 id="results-header"> (the first child).
|
| - while (this.resultDiv_.children.length > 1) {
|
| - this.resultDiv_.removeChild(this.resultDiv_.lastElementChild);
|
| - }
|
| - $('results-header').textContent = '';
|
| -
|
| - this.currentVisits_.forEach(function(visit) {
|
| - visit.isRendered = false;
|
| - });
|
| - this.currentVisits_ = [];
|
| -
|
| - document.body.classList.remove('has-results');
|
| -};
|
| -
|
| -/**
|
| - * Record that the given visit has been rendered.
|
| - * @param {Visit} visit The visit that was rendered.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.setVisitRendered_ = function(visit) {
|
| - visit.isRendered = true;
|
| - this.currentVisits_.push(visit);
|
| -};
|
| -
|
| -/**
|
| - * Generates and adds the grouped visits DOM for a certain domain. This
|
| - * includes the clickable arrow and domain name and the visit entries for
|
| - * that domain.
|
| - * @param {Element} results DOM object to which to add the elements.
|
| - * @param {string} domain Current domain name.
|
| - * @param {Array} domainVisits Array of visits for this domain.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.getGroupedVisitsDOM_ = function(
|
| - results, domain, domainVisits) {
|
| - // Add a new domain entry.
|
| - var siteResults = results.appendChild(
|
| - createElementWithClassName('li', 'site-entry'));
|
| -
|
| - var siteDomainWrapper = siteResults.appendChild(
|
| - createElementWithClassName('div', 'site-domain-wrapper'));
|
| - // Make a row that will contain the arrow, the favicon and the domain.
|
| - var siteDomainRow = siteDomainWrapper.appendChild(
|
| - createElementWithClassName('div', 'site-domain-row'));
|
| -
|
| - if (this.model_.editingEntriesAllowed) {
|
| - var siteDomainCheckbox =
|
| - createElementWithClassName('input', 'domain-checkbox');
|
| -
|
| - siteDomainCheckbox.type = 'checkbox';
|
| - siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
|
| - siteDomainCheckbox.domain_ = domain;
|
| - siteDomainCheckbox.setAttribute('aria-label', domain);
|
| - siteDomainRow.appendChild(siteDomainCheckbox);
|
| - }
|
| -
|
| - var siteArrow = siteDomainRow.appendChild(
|
| - createElementWithClassName('div', 'site-domain-arrow'));
|
| - var siteFavicon = siteDomainRow.appendChild(
|
| - createElementWithClassName('div', 'favicon'));
|
| - var siteDomain = siteDomainRow.appendChild(
|
| - createElementWithClassName('div', 'site-domain'));
|
| - var siteDomainLink = siteDomain.appendChild(new ActionLink);
|
| - siteDomainLink.textContent = domain;
|
| - var numberOfVisits = createElementWithClassName('span', 'number-visits');
|
| - var domainElement = document.createElement('span');
|
| -
|
| - numberOfVisits.textContent =
|
| - loadTimeData.getStringF('numberVisits',
|
| - domainVisits.length.toLocaleString());
|
| - siteDomain.appendChild(numberOfVisits);
|
| -
|
| - domainVisits[0].loadFavicon_(siteFavicon);
|
| -
|
| - siteDomainWrapper.addEventListener(
|
| - 'click', this.toggleGroupedVisits_.bind(this));
|
| -
|
| - if (this.model_.isSupervisedProfile) {
|
| - siteDomainRow.appendChild(
|
| - getFilteringStatusDOM(domainVisits[0].hostFilteringBehavior));
|
| - }
|
| -
|
| - siteResults.appendChild(siteDomainWrapper);
|
| - var resultsList = siteResults.appendChild(
|
| - createElementWithClassName('ol', 'site-results'));
|
| - resultsList.classList.add('grouped');
|
| -
|
| - // Collapse until it gets toggled.
|
| - resultsList.style.height = 0;
|
| - resultsList.setAttribute('aria-hidden', 'true');
|
| -
|
| - // Add the results for each of the domain.
|
| - var isMonthGroupedResult = this.getRangeInDays() == HistoryModel.Range.MONTH;
|
| - for (var j = 0, visit; visit = domainVisits[j]; j++) {
|
| - resultsList.appendChild(visit.getResultDOM({
|
| - focusless: true,
|
| - useMonthDate: isMonthGroupedResult,
|
| - }));
|
| - this.setVisitRendered_(visit);
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Enables or disables the time range buttons.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.updateRangeButtons_ = function() {
|
| - // The enabled state for the previous, today and next buttons.
|
| - var previousState = false;
|
| - var todayState = false;
|
| - var nextState = false;
|
| - var usePage = (this.getRangeInDays() == HistoryModel.Range.ALL_TIME);
|
| -
|
| - // Use pagination for most recent visits, offset otherwise.
|
| - // TODO(sergiu): Maybe send just one variable in the future.
|
| - if (usePage) {
|
| - if (this.getPage() != 0) {
|
| - nextState = true;
|
| - todayState = true;
|
| - }
|
| - previousState = this.model_.hasMoreResults();
|
| - } else {
|
| - if (this.getOffset() != 0) {
|
| - nextState = true;
|
| - todayState = true;
|
| - }
|
| - previousState = !this.model_.isQueryFinished_;
|
| - }
|
| -
|
| - $('range-previous').disabled = !previousState;
|
| - $('range-today').disabled = !todayState;
|
| - $('range-next').disabled = !nextState;
|
| -};
|
| -
|
| -/**
|
| - * Groups visits by domain, sorting them by the number of visits.
|
| - * @param {Array} visits Visits received from the query results.
|
| - * @param {Element} results Object where the results are added to.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) {
|
| - var visitsByDomain = {};
|
| - var domains = [];
|
| -
|
| - // Group the visits into a dictionary and generate a list of domains.
|
| - for (var i = 0, visit; visit = visits[i]; i++) {
|
| - var domain = visit.domain_;
|
| - if (!visitsByDomain[domain]) {
|
| - visitsByDomain[domain] = [];
|
| - domains.push(domain);
|
| - }
|
| - visitsByDomain[domain].push(visit);
|
| - }
|
| - var sortByVisits = function(a, b) {
|
| - return visitsByDomain[b].length - visitsByDomain[a].length;
|
| - };
|
| - domains.sort(sortByVisits);
|
| -
|
| - for (var i = 0; i < domains.length; ++i) {
|
| - var domain = domains[i];
|
| - this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]);
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Adds the results for a month.
|
| - * @param {Array} visits Visits returned by the query.
|
| - * @param {Node} parentNode Node to which to add the results to.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.addMonthResults_ = function(visits, parentNode) {
|
| - if (visits.length == 0)
|
| - return;
|
| -
|
| - var monthResults = /** @type {HTMLOListElement} */(parentNode.appendChild(
|
| - createElementWithClassName('ol', 'month-results')));
|
| - // Don't add checkboxes if entries can not be edited.
|
| - if (!this.model_.editingEntriesAllowed)
|
| - monthResults.classList.add('no-checkboxes');
|
| -
|
| - this.groupVisitsByDomain_(visits, monthResults);
|
| -};
|
| -
|
| -/**
|
| - * Adds the results for a certain day. This includes a title with the day of
|
| - * the results and the results themselves, grouped or not.
|
| - * @param {Array} visits Visits returned by the query.
|
| - * @param {Node} parentNode Node to which to add the results to.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.addDayResults_ = function(visits, parentNode) {
|
| - if (visits.length == 0)
|
| - return;
|
| -
|
| - var firstVisit = visits[0];
|
| - var day = parentNode.appendChild(createElementWithClassName('h3', 'day'));
|
| - day.appendChild(document.createTextNode(firstVisit.dateRelativeDay));
|
| - if (firstVisit.continued) {
|
| - day.appendChild(document.createTextNode(' ' +
|
| - loadTimeData.getString('cont')));
|
| - }
|
| - var dayResults = /** @type {HTMLElement} */(parentNode.appendChild(
|
| - createElementWithClassName('ol', 'day-results')));
|
| -
|
| - // Don't add checkboxes if entries can not be edited.
|
| - if (!this.model_.editingEntriesAllowed)
|
| - dayResults.classList.add('no-checkboxes');
|
| -
|
| - if (this.model_.getGroupByDomain()) {
|
| - this.groupVisitsByDomain_(visits, dayResults);
|
| - } else {
|
| - var lastTime;
|
| -
|
| - for (var i = 0, visit; visit = visits[i]; i++) {
|
| - // If enough time has passed between visits, indicate a gap in browsing.
|
| - var thisTime = visit.date.getTime();
|
| - if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME)
|
| - dayResults.appendChild(createElementWithClassName('li', 'gap'));
|
| -
|
| - // Insert the visit into the DOM.
|
| - dayResults.appendChild(visit.getResultDOM({ addTitleFavicon: true }));
|
| - this.setVisitRendered_(visit);
|
| -
|
| - lastTime = thisTime;
|
| - }
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Adds the text that shows the current interval, used for week and month
|
| - * results.
|
| - * @param {Node} resultsFragment The element to which the interval will be
|
| - * added to.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.addTimeframeInterval_ = function(resultsFragment) {
|
| - if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME)
|
| - return;
|
| -
|
| - // If this is a time range result add some text that shows what is the
|
| - // time range for the results the user is viewing.
|
| - var timeFrame = resultsFragment.appendChild(
|
| - createElementWithClassName('h2', 'timeframe'));
|
| - // TODO(sergiu): Figure the best way to show this for the first day of
|
| - // the month.
|
| - timeFrame.appendChild(
|
| - document.createTextNode(this.model_.queryInterval));
|
| -};
|
| -
|
| -/**
|
| - * Update the page with results.
|
| - * @param {boolean} doneLoading Whether the current request is complete.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.displayResults_ = function(doneLoading) {
|
| - // Either show a page of results received for the all time results or all the
|
| - // received results for the weekly and monthly view.
|
| - var results = this.model_.visits_;
|
| - if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME) {
|
| - var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
|
| - var rangeEnd = rangeStart + RESULTS_PER_PAGE;
|
| - results = this.model_.getNumberedRange(rangeStart, rangeEnd);
|
| - }
|
| - var searchText = this.model_.getSearchText();
|
| - var groupByDomain = this.model_.getGroupByDomain();
|
| -
|
| - if (searchText) {
|
| - var headerText;
|
| - if (!doneLoading) {
|
| - headerText = loadTimeData.getStringF('searchResultsFor', searchText);
|
| - } else if (results.length == 0) {
|
| - headerText = loadTimeData.getString('noSearchResults');
|
| - } else {
|
| - var resultId = results.length == 1 ? 'searchResult' : 'searchResults';
|
| - headerText = loadTimeData.getStringF('foundSearchResults',
|
| - results.length,
|
| - loadTimeData.getString(resultId),
|
| - searchText);
|
| - }
|
| - $('results-header').textContent = headerText;
|
| -
|
| - this.addTimeframeInterval_(this.resultDiv_);
|
| -
|
| - var searchResults = createElementWithClassName('ol', 'search-results');
|
| -
|
| - // Don't add checkboxes if entries can not be edited.
|
| - if (!this.model_.editingEntriesAllowed)
|
| - searchResults.classList.add('no-checkboxes');
|
| -
|
| - if (doneLoading) {
|
| - for (var i = 0, visit; visit = results[i]; i++) {
|
| - if (!visit.isRendered) {
|
| - searchResults.appendChild(visit.getResultDOM({
|
| - isSearchResult: true,
|
| - addTitleFavicon: true
|
| - }));
|
| - this.setVisitRendered_(visit);
|
| - }
|
| - }
|
| - }
|
| - this.resultDiv_.appendChild(searchResults);
|
| - } else {
|
| - var resultsFragment = document.createDocumentFragment();
|
| -
|
| - this.addTimeframeInterval_(resultsFragment);
|
| -
|
| - var noResults = results.length == 0 && doneLoading;
|
| - $('results-header').textContent = noResults ?
|
| - loadTimeData.getString('noResults') : '';
|
| -
|
| - if (noResults)
|
| - return;
|
| -
|
| - if (this.getRangeInDays() == HistoryModel.Range.MONTH &&
|
| - groupByDomain) {
|
| - // Group everything together in the month view.
|
| - this.addMonthResults_(results, resultsFragment);
|
| - } else {
|
| - var dayStart = 0;
|
| - var dayEnd = 0;
|
| - // Go through all of the visits and process them in chunks of one day.
|
| - while (dayEnd < results.length) {
|
| - // Skip over the ones that are already rendered.
|
| - while (dayStart < results.length && results[dayStart].isRendered)
|
| - ++dayStart;
|
| - var dayEnd = dayStart + 1;
|
| - while (dayEnd < results.length && results[dayEnd].continued)
|
| - ++dayEnd;
|
| -
|
| - this.addDayResults_(
|
| - results.slice(dayStart, dayEnd), resultsFragment);
|
| - }
|
| - }
|
| -
|
| - // Add all the days and their visits to the page.
|
| - this.resultDiv_.appendChild(resultsFragment);
|
| - }
|
| - // After the results have been added to the DOM, determine the size of the
|
| - // time column.
|
| - this.setTimeColumnWidth_();
|
| -};
|
| -
|
| -var focusGridRowSelector = [
|
| - ':-webkit-any(.day-results, .search-results) > .entry:not(.fade-out)',
|
| - '.expand .grouped .entry:not(.fade-out)',
|
| - '.site-domain-wrapper'
|
| -].join(', ');
|
| -
|
| -/** @private */
|
| -HistoryView.prototype.updateFocusGrid_ = function() {
|
| - var rows = this.resultDiv_.querySelectorAll(focusGridRowSelector);
|
| - this.focusGrid_.destroy();
|
| -
|
| - for (var i = 0; i < rows.length; ++i) {
|
| - assert(rows[i].parentNode);
|
| - this.focusGrid_.addRow(new HistoryFocusRow(rows[i], this.resultDiv_));
|
| - }
|
| - this.focusGrid_.ensureRowActive();
|
| -};
|
| -
|
| -/**
|
| - * Update the visibility of the page navigation buttons.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.updateNavBar_ = function() {
|
| - this.updateRangeButtons_();
|
| -
|
| - // If grouping by domain is enabled, there's a control bar on top, don't show
|
| - // the one on the bottom as well.
|
| - if (!loadTimeData.getBoolean('groupByDomain')) {
|
| - $('newest-button').hidden = this.pageIndex_ == 0;
|
| - $('newer-button').hidden = this.pageIndex_ == 0;
|
| - $('older-button').hidden =
|
| - this.model_.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
|
| - !this.model_.hasMoreResults();
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Updates the visibility of the 'Clear browsing data' button.
|
| - * Only used on mobile platforms.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.updateClearBrowsingDataButton_ = function() {
|
| - // Ideally, we should hide the 'Clear browsing data' button whenever the
|
| - // soft keyboard is visible. This is not possible, so instead, hide the
|
| - // button whenever the search field has focus.
|
| - $('clear-browsing-data').hidden =
|
| - (document.activeElement === $('search-field'));
|
| -};
|
| -
|
| -/**
|
| - * Dynamically sets the min-width of the time column for history entries.
|
| - * This ensures that all entry times will have the same width, without
|
| - * imposing a fixed width that may not be appropriate for some locales.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.setTimeColumnWidth_ = function() {
|
| - // Find the maximum width of all the time elements on the page.
|
| - var times = this.resultDiv_.querySelectorAll('.entry .time');
|
| - Array.prototype.forEach.call(times, function(el) {
|
| - el.style.minWidth = '-webkit-min-content';
|
| - });
|
| - var widths = Array.prototype.map.call(times, function(el) {
|
| - // Add an extra pixel to prevent rounding errors from causing the text to
|
| - // be ellipsized at certain zoom levels (see crbug.com/329779).
|
| - return el.clientWidth + 1;
|
| - });
|
| - Array.prototype.forEach.call(times, function(el) {
|
| - el.style.minWidth = '';
|
| - });
|
| - var maxWidth = widths.length ? Math.max.apply(null, widths) : 0;
|
| -
|
| - // Add a dynamic stylesheet to the page (or replace the existing one), to
|
| - // ensure that all entry times have the same width.
|
| - var styleEl = $('timeColumnStyle');
|
| - if (!styleEl) {
|
| - styleEl = document.head.appendChild(document.createElement('style'));
|
| - styleEl.id = 'timeColumnStyle';
|
| - }
|
| - styleEl.textContent = '.entry .time { min-width: ' + maxWidth + 'px; }';
|
| -};
|
| -
|
| -/**
|
| - * Toggles an element in the grouped history.
|
| - * @param {Event} e The event with element |e.target| which was clicked on.
|
| - * @private
|
| - */
|
| -HistoryView.prototype.toggleGroupedVisits_ = function(e) {
|
| - var entry = findAncestorByClass(/** @type {Element} */(e.target),
|
| - 'site-entry');
|
| - var innerResultList = entry.querySelector('.site-results');
|
| -
|
| - if (entry.classList.contains('expand')) {
|
| - innerResultList.style.height = 0;
|
| - innerResultList.setAttribute('aria-hidden', 'true');
|
| - } else {
|
| - innerResultList.setAttribute('aria-hidden', 'false');
|
| - innerResultList.style.height = 'auto';
|
| - // transition does not work on height:auto elements so first set
|
| - // the height to auto so that it is computed and then set it to the
|
| - // computed value in pixels so the transition works properly.
|
| - var height = innerResultList.clientHeight;
|
| - innerResultList.style.height = 0;
|
| - setTimeout(function() {
|
| - innerResultList.style.height = height + 'px';
|
| - }, 0);
|
| - }
|
| -
|
| - entry.classList.toggle('expand');
|
| -
|
| - var root = entry.querySelector('.site-domain-wrapper');
|
| -
|
| - this.focusGrid_.rows.forEach(function(row) {
|
| - row.makeActive(row.root == root);
|
| - });
|
| -
|
| - this.updateFocusGrid_();
|
| -};
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// State object:
|
| -/**
|
| - * An 'AJAX-history' implementation.
|
| - * @param {HistoryModel} model The model we're representing.
|
| - * @param {HistoryView} view The view we're representing.
|
| - * @constructor
|
| - */
|
| -function PageState(model, view) {
|
| - // Enforce a singleton.
|
| - if (PageState.instance) {
|
| - return PageState.instance;
|
| - }
|
| -
|
| - this.model = model;
|
| - this.view = view;
|
| -
|
| - if (typeof this.checker_ != 'undefined' && this.checker_) {
|
| - clearInterval(this.checker_);
|
| - }
|
| -
|
| - // TODO(glen): Replace this with a bound method so we don't need
|
| - // public model and view.
|
| - this.checker_ = window.setInterval(function() {
|
| - var hashData = this.getHashData();
|
| - var page = parseInt(hashData.page, 10);
|
| - var range = parseInt(hashData.range, 10);
|
| - var offset = parseInt(hashData.offset, 10);
|
| - if (hashData.q != this.model.getSearchText() ||
|
| - page != this.view.getPage() ||
|
| - range != this.model.rangeInDays ||
|
| - offset != this.model.offset) {
|
| - this.view.setPageState(hashData.q, page, range, offset);
|
| - }
|
| - }.bind(this), 50);
|
| -}
|
| -
|
| -/**
|
| - * Holds the singleton instance.
|
| - */
|
| -PageState.instance = null;
|
| -
|
| -/**
|
| - * @return {Object} An object containing parameters from our window hash.
|
| - */
|
| -PageState.prototype.getHashData = function() {
|
| - var result = {
|
| - q: '',
|
| - page: 0,
|
| - range: 0,
|
| - offset: 0
|
| - };
|
| -
|
| - if (!window.location.hash)
|
| - return result;
|
| -
|
| - var hashSplit = window.location.hash.substr(1).split('&');
|
| - for (var i = 0; i < hashSplit.length; i++) {
|
| - var pair = hashSplit[i].split('=');
|
| - if (pair.length > 1)
|
| - result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
|
| - }
|
| -
|
| - return result;
|
| -};
|
| -
|
| -/**
|
| - * Set the hash to a specified state, this will create an entry in the
|
| - * session history so the back button cycles through hash states, which
|
| - * are then picked up by our listener.
|
| - * @param {string} term The current search string.
|
| - * @param {number} page The page currently being viewed.
|
| - * @param {HistoryModel.Range} range The range to view or search over.
|
| - * @param {number} offset Set the begining of the query to the specific offset.
|
| - */
|
| -PageState.prototype.setUIState = function(term, page, range, offset) {
|
| - // Make sure the form looks pretty.
|
| - $('search-field').value = term;
|
| - var hash = this.getHashData();
|
| - if (hash.q != term || hash.page != page || hash.range != range ||
|
| - hash.offset != offset) {
|
| - window.location.hash = PageState.getHashString(term, page, range, offset);
|
| - }
|
| -};
|
| -
|
| -/**
|
| - * Static method to get the hash string for a specified state
|
| - * @param {string} term The current search string.
|
| - * @param {number} page The page currently being viewed.
|
| - * @param {HistoryModel.Range} range The range to view or search over.
|
| - * @param {number} offset Set the begining of the query to the specific offset.
|
| - * @return {string} The string to be used in a hash.
|
| - */
|
| -PageState.getHashString = function(term, page, range, offset) {
|
| - // Omit elements that are empty.
|
| - var newHash = [];
|
| -
|
| - if (term)
|
| - newHash.push('q=' + encodeURIComponent(term));
|
| -
|
| - if (page)
|
| - newHash.push('page=' + page);
|
| -
|
| - if (range)
|
| - newHash.push('range=' + range);
|
| -
|
| - if (offset)
|
| - newHash.push('offset=' + offset);
|
| -
|
| - return newHash.join('&');
|
| -};
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// Document Functions:
|
| -/**
|
| - * Window onload handler, sets up the page.
|
| - */
|
| -function load() {
|
| - uber.onContentFrameLoaded();
|
| - FocusOutlineManager.forDocument(document);
|
| -
|
| - var searchField = $('search-field');
|
| -
|
| - historyModel = new HistoryModel();
|
| - historyView = new HistoryView(historyModel);
|
| - pageState = new PageState(historyModel, historyView);
|
| -
|
| - // Create default view.
|
| - var hashData = pageState.getHashData();
|
| - var page = parseInt(hashData.page, 10) || historyView.getPage();
|
| - var range = /** @type {HistoryModel.Range} */(parseInt(hashData.range, 10)) ||
|
| - historyView.getRangeInDays();
|
| - var offset = parseInt(hashData.offset, 10) || historyView.getOffset();
|
| - historyView.setPageState(hashData.q, page, range, offset);
|
| -
|
| - if ($('overlay')) {
|
| - cr.ui.overlay.setupOverlay($('overlay'));
|
| - cr.ui.overlay.globalInitialization();
|
| - }
|
| - HistoryFocusManager.getInstance().initialize();
|
| -
|
| - var doSearch = function(e) {
|
| - recordUmaAction('HistoryPage_Search');
|
| - historyView.setSearch(searchField.value);
|
| -
|
| - if (isMobileVersion())
|
| - searchField.blur(); // Dismiss the keyboard.
|
| - };
|
| -
|
| - var removeMenu = getRequiredElement('remove-visit');
|
| - // Decorate remove-visit before disabling/hiding because the values are
|
| - // overwritten when decorating a MenuItem that has a Command.
|
| - cr.ui.decorate(removeMenu, MenuItem);
|
| - removeMenu.disabled = !loadTimeData.getBoolean('allowDeletingHistory');
|
| - removeMenu.hidden = loadTimeData.getBoolean('hideDeleteVisitUI');
|
| -
|
| - document.addEventListener('command', handleCommand);
|
| -
|
| - searchField.addEventListener('search', doSearch);
|
| - $('search-button').addEventListener('click', doSearch);
|
| -
|
| - $('more-from-site').addEventListener('activate', function(e) {
|
| - activeVisit.showMoreFromSite_();
|
| - activeVisit = null;
|
| - });
|
| -
|
| - // Only show the controls if the command line switch is activated or the user
|
| - // is supervised.
|
| - if (loadTimeData.getBoolean('groupByDomain')) {
|
| - $('history-page').classList.add('big-topbar-page');
|
| - $('filter-controls').hidden = false;
|
| - }
|
| - // Hide the top container which has the "Clear browsing data" and "Remove
|
| - // selected entries" buttons if deleting history is not allowed.
|
| - if (!loadTimeData.getBoolean('allowDeletingHistory'))
|
| - $('top-container').hidden = true;
|
| -
|
| - uber.setTitle(loadTimeData.getString('title'));
|
| -
|
| - // Adjust the position of the notification bar when the window size changes.
|
| - window.addEventListener('resize',
|
| - historyView.positionNotificationBar.bind(historyView));
|
| -
|
| - if (isMobileVersion()) {
|
| - // Move the search box out of the header.
|
| - var resultsDisplay = $('results-display');
|
| - resultsDisplay.parentNode.insertBefore($('search-field'), resultsDisplay);
|
| -
|
| - window.addEventListener(
|
| - 'resize', historyView.updateClearBrowsingDataButton_);
|
| -
|
| -// <if expr="is_ios">
|
| - // Trigger window resize event when search field is focused to force update
|
| - // of the clear browsing button, which should disappear when search field
|
| - // is active. The window is not resized when the virtual keyboard is shown
|
| - // on iOS.
|
| - searchField.addEventListener('focus', function() {
|
| - cr.dispatchSimpleEvent(window, 'resize');
|
| - });
|
| -// </if> /* is_ios */
|
| -
|
| - // When the search field loses focus, add a delay before updating the
|
| - // visibility, otherwise the button will flash on the screen before the
|
| - // keyboard animates away.
|
| - searchField.addEventListener('blur', function() {
|
| - setTimeout(historyView.updateClearBrowsingDataButton_, 250);
|
| - });
|
| -
|
| - // Move the button to the bottom of the page.
|
| - $('history-page').appendChild($('clear-browsing-data'));
|
| - } else {
|
| - window.addEventListener('message', function(e) {
|
| - e = /** @type {!MessageEvent<!{method: string}>} */(e);
|
| - if (e.data.method == 'frameSelected')
|
| - searchField.focus();
|
| - });
|
| - searchField.focus();
|
| - }
|
| -
|
| -// <if expr="is_ios">
|
| - function checkKeyboardVisibility() {
|
| - // Figure out the real height based on the orientation, becauase
|
| - // screen.width and screen.height don't update after rotation.
|
| - var screenHeight = window.orientation % 180 ? screen.width : screen.height;
|
| -
|
| - // Assume that the keyboard is visible if more than 30% of the screen is
|
| - // taken up by window chrome.
|
| - var isKeyboardVisible = (window.innerHeight / screenHeight) < 0.7;
|
| -
|
| - document.body.classList.toggle('ios-keyboard-visible', isKeyboardVisible);
|
| - }
|
| - window.addEventListener('orientationchange', checkKeyboardVisibility);
|
| - window.addEventListener('resize', checkKeyboardVisibility);
|
| -// </if> /* is_ios */
|
| -}
|
| -
|
| -/**
|
| - * Handle all commands in the history page.
|
| - * @param {!Event} e is a command event.
|
| - */
|
| -function handleCommand(e) {
|
| - switch (e.command.id) {
|
| - case 'remove-visit-command':
|
| - // Removing visited items needs to be done with a command in order to have
|
| - // proper focus. This is because the command event is handled after the
|
| - // menu dialog is no longer visible and focus has returned to the history
|
| - // items. The activate event is handled when the menu dialog is still
|
| - // visible and focus is lost.
|
| - // removeEntryFromHistory_ will update activeVisit to the newly focused
|
| - // history item.
|
| - assert(!$('remove-visit').disabled);
|
| - activeVisit.removeEntryFromHistory_(e);
|
| - break;
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Updates the filter status labels of a host/URL entry to the current value.
|
| - * @param {Element} statusElement The div which contains the status labels.
|
| - * @param {SupervisedUserFilteringBehavior} newStatus The filter status of the
|
| - * current domain/URL.
|
| - */
|
| -function updateHostStatus(statusElement, newStatus) {
|
| - var filteringBehaviorDiv =
|
| - statusElement.querySelector('.filtering-behavior');
|
| - // Reset to the base class first, then add modifier classes if needed.
|
| - filteringBehaviorDiv.className = 'filtering-behavior';
|
| - if (newStatus == SupervisedUserFilteringBehavior.BLOCK) {
|
| - filteringBehaviorDiv.textContent =
|
| - loadTimeData.getString('filterBlocked');
|
| - filteringBehaviorDiv.classList.add('filter-blocked');
|
| - } else {
|
| - filteringBehaviorDiv.textContent = '';
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Click handler for the 'Clear browsing data' dialog.
|
| - * @param {Event} e The click event.
|
| - */
|
| -function openClearBrowsingData(e) {
|
| - recordUmaAction('HistoryPage_InitClearBrowsingData');
|
| - chrome.send('clearBrowsingData');
|
| -}
|
| -
|
| -/**
|
| - * Shows the dialog for the user to confirm removal of selected history entries.
|
| - */
|
| -function showConfirmationOverlay() {
|
| - $('alertOverlay').classList.add('showing');
|
| - $('overlay').hidden = false;
|
| - $('history-page').setAttribute('aria-hidden', 'true');
|
| - uber.invokeMethodOnParent('beginInterceptingEvents');
|
| -
|
| - // Change focus to the overlay if any other control was focused by keyboard
|
| - // before. Otherwise, no one should have focus.
|
| - var focusOverlay = FocusOutlineManager.forDocument(document).visible &&
|
| - document.activeElement != document.body;
|
| - if ($('history-page').contains(document.activeElement))
|
| - document.activeElement.blur();
|
| -
|
| - if (focusOverlay) {
|
| - // Wait until the browser knows the button has had a chance to become
|
| - // visible.
|
| - window.requestAnimationFrame(function() {
|
| - var button = cr.ui.overlay.getDefaultButton($('overlay'));
|
| - if (button)
|
| - button.focus();
|
| - });
|
| - }
|
| - $('alertOverlay').classList.toggle('focus-on-hide', focusOverlay);
|
| -}
|
| -
|
| -/**
|
| - * Hides the confirmation overlay used to confirm selected history entries.
|
| - */
|
| -function hideConfirmationOverlay() {
|
| - $('alertOverlay').classList.remove('showing');
|
| - $('overlay').hidden = true;
|
| - $('history-page').removeAttribute('aria-hidden');
|
| - uber.invokeMethodOnParent('stopInterceptingEvents');
|
| -}
|
| -
|
| -/**
|
| - * Shows the confirmation alert for history deletions and permits browser tests
|
| - * to override the dialog.
|
| - * @param {function()=} okCallback A function to be called when the user presses
|
| - * the ok button.
|
| - * @param {function()=} cancelCallback A function to be called when the user
|
| - * presses the cancel button.
|
| - */
|
| -function confirmDeletion(okCallback, cancelCallback) {
|
| - alertOverlay.setValues(
|
| - loadTimeData.getString('removeSelected'),
|
| - loadTimeData.getString('deleteWarning'),
|
| - loadTimeData.getString('deleteConfirm'),
|
| - loadTimeData.getString('cancel'),
|
| - okCallback,
|
| - cancelCallback);
|
| - showConfirmationOverlay();
|
| -}
|
| -
|
| -/**
|
| - * Click handler for the 'Remove selected items' button.
|
| - * Confirms the deletion with the user, and then deletes the selected visits.
|
| - */
|
| -function removeItems() {
|
| - recordUmaAction('HistoryPage_RemoveSelected');
|
| - if (!loadTimeData.getBoolean('allowDeletingHistory'))
|
| - return;
|
| -
|
| - var checked = $('results-display').querySelectorAll(
|
| - '.entry-box input[type=checkbox]:checked:not([disabled])');
|
| - var disabledItems = [];
|
| - var toBeRemoved = [];
|
| -
|
| - for (var i = 0; i < checked.length; i++) {
|
| - var checkbox = checked[i];
|
| - var entry = findAncestorByClass(checkbox, 'entry');
|
| - toBeRemoved.push(entry.visit);
|
| -
|
| - // Disable the checkbox and put a strikethrough style on the link, so the
|
| - // user can see what will be deleted.
|
| - checkbox.disabled = true;
|
| - entry.visit.titleLink.classList.add('to-be-removed');
|
| - disabledItems.push(checkbox);
|
| - var integerId = parseInt(entry.visit.id_, 10);
|
| - // Record the ID of the entry to signify how many entries are above this
|
| - // link on the page.
|
| - recordUmaHistogram('HistoryPage.RemoveEntryPosition',
|
| - UMA_MAX_BUCKET_VALUE,
|
| - integerId);
|
| - if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
|
| - recordUmaHistogram('HistoryPage.RemoveEntryPositionSubset',
|
| - UMA_MAX_SUBSET_BUCKET_VALUE,
|
| - integerId);
|
| - }
|
| - if (entry.parentNode.className == 'search-results')
|
| - recordUmaAction('HistoryPage_SearchResultRemove');
|
| - }
|
| -
|
| - function onConfirmRemove() {
|
| - recordUmaAction('HistoryPage_ConfirmRemoveSelected');
|
| - historyModel.removeVisitsFromHistory(toBeRemoved,
|
| - historyView.reload.bind(historyView));
|
| - $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
|
| - hideConfirmationOverlay();
|
| - if ($('alertOverlay').classList.contains('focus-on-hide') &&
|
| - FocusOutlineManager.forDocument(document).visible) {
|
| - $('search-field').focus();
|
| - }
|
| - }
|
| -
|
| - function onCancelRemove() {
|
| - recordUmaAction('HistoryPage_CancelRemoveSelected');
|
| - // Return everything to its previous state.
|
| - for (var i = 0; i < disabledItems.length; i++) {
|
| - var checkbox = disabledItems[i];
|
| - checkbox.disabled = false;
|
| -
|
| - var entry = findAncestorByClass(checkbox, 'entry');
|
| - entry.visit.titleLink.classList.remove('to-be-removed');
|
| - }
|
| - $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
|
| - hideConfirmationOverlay();
|
| - if ($('alertOverlay').classList.contains('focus-on-hide') &&
|
| - FocusOutlineManager.forDocument(document).visible) {
|
| - $('remove-selected').focus();
|
| - }
|
| - }
|
| -
|
| - if (checked.length) {
|
| - confirmDeletion(onConfirmRemove, onCancelRemove);
|
| - $('overlay').addEventListener('cancelOverlay', onCancelRemove);
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Handler for the 'click' event on a checkbox.
|
| - * @param {Event} e The click event.
|
| - */
|
| -function checkboxClicked(e) {
|
| - handleCheckboxStateChange(/** @type {!HTMLInputElement} */(e.currentTarget),
|
| - e.shiftKey);
|
| -}
|
| -
|
| -/**
|
| - * Post-process of checkbox state change. This handles range selection and
|
| - * updates internal state.
|
| - * @param {!HTMLInputElement} checkbox Clicked checkbox.
|
| - * @param {boolean} shiftKey true if shift key is pressed.
|
| - */
|
| -function handleCheckboxStateChange(checkbox, shiftKey) {
|
| - updateParentCheckbox(checkbox);
|
| - var id = Number(checkbox.id.slice('checkbox-'.length));
|
| - // Handle multi-select if shift was pressed.
|
| - if (shiftKey && (selectionAnchor != -1)) {
|
| - var checked = checkbox.checked;
|
| - // Set all checkboxes from the anchor up to the clicked checkbox to the
|
| - // state of the clicked one.
|
| - var begin = Math.min(id, selectionAnchor);
|
| - var end = Math.max(id, selectionAnchor);
|
| - for (var i = begin; i <= end; i++) {
|
| - var ithCheckbox = document.querySelector('#checkbox-' + i);
|
| - if (ithCheckbox) {
|
| - ithCheckbox.checked = checked;
|
| - updateParentCheckbox(ithCheckbox);
|
| - }
|
| - }
|
| - }
|
| - selectionAnchor = id;
|
| -
|
| - historyView.updateSelectionEditButtons();
|
| -}
|
| -
|
| -/**
|
| - * Handler for the 'click' event on a domain checkbox. Checkes or unchecks the
|
| - * checkboxes of the visits to this domain in the respective group.
|
| - * @param {Event} e The click event.
|
| - */
|
| -function domainCheckboxClicked(e) {
|
| - var siteEntry = findAncestorByClass(/** @type {Element} */(e.currentTarget),
|
| - 'site-entry');
|
| - var checkboxes =
|
| - siteEntry.querySelectorAll('.site-results input[type=checkbox]');
|
| - for (var i = 0; i < checkboxes.length; i++)
|
| - checkboxes[i].checked = e.currentTarget.checked;
|
| - historyView.updateSelectionEditButtons();
|
| - // Stop propagation as clicking the checkbox would otherwise trigger the
|
| - // group to collapse/expand.
|
| - e.stopPropagation();
|
| -}
|
| -
|
| -/**
|
| - * Updates the domain checkbox for this visit checkbox if it has been
|
| - * unchecked.
|
| - * @param {Element} checkbox The checkbox that has been clicked.
|
| - */
|
| -function updateParentCheckbox(checkbox) {
|
| - if (checkbox.checked)
|
| - return;
|
| -
|
| - var entry = findAncestorByClass(checkbox, 'site-entry');
|
| - if (!entry)
|
| - return;
|
| -
|
| - var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
|
| - if (groupCheckbox)
|
| - groupCheckbox.checked = false;
|
| -}
|
| -
|
| -/**
|
| - * Handle click event for entryBoxes.
|
| - * @param {!Event} event A click event.
|
| - */
|
| -function entryBoxClick(event) {
|
| - event = /** @type {!MouseEvent} */(event);
|
| - // Do nothing if a bookmark star is clicked.
|
| - if (event.defaultPrevented)
|
| - return;
|
| - var element = event.target;
|
| - // Do nothing if the event happened in an interactive element.
|
| - for (; element != event.currentTarget; element = element.parentNode) {
|
| - switch (element.tagName) {
|
| - case 'A':
|
| - case 'BUTTON':
|
| - case 'INPUT':
|
| - return;
|
| - }
|
| - }
|
| - var checkbox = assertInstanceof($(event.currentTarget.getAttribute('for')),
|
| - HTMLInputElement);
|
| - checkbox.checked = !checkbox.checked;
|
| - handleCheckboxStateChange(checkbox, event.shiftKey);
|
| - // We don't want to focus on the checkbox.
|
| - event.preventDefault();
|
| -}
|
| -
|
| -/**
|
| - * Called when an individual history entry has been removed from the page.
|
| - * This will only be called when all the elements affected by the deletion
|
| - * have been removed from the DOM and the animations have completed.
|
| - */
|
| -function onEntryRemoved() {
|
| - historyView.onEntryRemoved();
|
| -}
|
| -
|
| -/**
|
| - * Triggers a fade-out animation, and then removes |node| from the DOM.
|
| - * @param {Node} node The node to be removed.
|
| - * @param {Function?} onRemove A function to be called after the node
|
| - * has been removed from the DOM.
|
| - * @param {*=} opt_scope An optional scope object to call |onRemove| with.
|
| - */
|
| -function removeNode(node, onRemove, opt_scope) {
|
| - node.classList.add('fade-out'); // Trigger CSS fade out animation.
|
| -
|
| - // Delete the node when the animation is complete.
|
| - node.addEventListener('transitionend', function(e) {
|
| - node.parentNode.removeChild(node);
|
| -
|
| - // In case there is nested deletion happening, prevent this event from
|
| - // being handled by listeners on ancestor nodes.
|
| - e.stopPropagation();
|
| -
|
| - if (onRemove)
|
| - onRemove.call(opt_scope);
|
| - });
|
| -}
|
| -
|
| -/**
|
| - * Builds the DOM elements to show the filtering status of a domain/URL.
|
| - * @param {SupervisedUserFilteringBehavior} filteringBehavior The filter
|
| - * behavior for this item.
|
| - * @return {Element} Returns the DOM elements which show the status.
|
| - */
|
| -function getFilteringStatusDOM(filteringBehavior) {
|
| - var filterStatusDiv = createElementWithClassName('div', 'filter-status');
|
| - var filteringBehaviorDiv =
|
| - createElementWithClassName('div', 'filtering-behavior');
|
| - filterStatusDiv.appendChild(filteringBehaviorDiv);
|
| -
|
| - updateHostStatus(filterStatusDiv, filteringBehavior);
|
| - return filterStatusDiv;
|
| -}
|
| -
|
| -
|
| -///////////////////////////////////////////////////////////////////////////////
|
| -// Chrome callbacks:
|
| -
|
| -/**
|
| - * Our history system calls this function with results from searches.
|
| - * @param {HistoryQuery} info An object containing information about the query.
|
| - * @param {Array<HistoryEntry>} results A list of results.
|
| - */
|
| -function historyResult(info, results) {
|
| - historyModel.addResults(info, results);
|
| -}
|
| -
|
| -/**
|
| - * Called by the history backend after receiving results and after discovering
|
| - * the existence of other forms of browsing history.
|
| - * @param {boolean} hasSyncedResults Whether there are synced results.
|
| - * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
|
| - * a sentence about the existence of other forms of browsing history.
|
| - */
|
| -function showNotification(
|
| - hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
|
| - historyView.showWebHistoryNotification(
|
| - hasSyncedResults, includeOtherFormsOfBrowsingHistory);
|
| -}
|
| -
|
| -/**
|
| - * Called by the history backend when history removal is successful.
|
| - */
|
| -function deleteComplete() {
|
| - historyModel.deleteComplete();
|
| -}
|
| -
|
| -/**
|
| - * Called by the history backend when history removal is unsuccessful.
|
| - */
|
| -function deleteFailed() {
|
| - window.console.log('Delete failed');
|
| -}
|
| -
|
| -/**
|
| - * Called when the history is deleted by someone else.
|
| - */
|
| -function historyDeleted() {
|
| - var anyChecked = document.querySelector('.entry input:checked') != null;
|
| - // Reload the page, unless the user has any items checked.
|
| - // TODO(dubroy): We should just reload the page & restore the checked items.
|
| - if (!anyChecked)
|
| - historyView.reload();
|
| -}
|
| -
|
| -// Add handlers to HTML elements.
|
| -document.addEventListener('DOMContentLoaded', load);
|
| -
|
| -// This event lets us enable and disable menu items before the menu is shown.
|
| -document.addEventListener('canExecute', function(e) {
|
| - e.canExecute = true;
|
| -});
|
|
|