/** @const */ var MenuButton = cr.ui.MenuButton;
/**
- * Enum that shows the filtering behavior for a host or URL to a managed user.
- * Must behave like the FilteringBehavior enum from managed_mode_url_filter.h.
+ * 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}
*/
-ManagedModeFilteringBehavior = {
+var SupervisedUserFilteringBehavior = {
ALLOW: 0,
WARN: 1,
BLOCK: 2
};
+/**
+ * The type of the history result object. The definition is based on
+ * chrome/browser/ui/webui/history_ui.cc:
+ * BrowsingHistoryHandler::HistoryEntry::ToValue()
+ * @typedef {{allTimestamps: Array.<number>,
+ * blockedVisit: (boolean|undefined),
+ * dateRelativeDay: (string|undefined),
+ * dateShort: string,
+ * dateTimeOfDay: (string|undefined),
+ * deviceName: string,
+ * deviceType: string,
+ * domain: string,
+ * hostFilteringBehavior: (number|undefined),
+ * snippet: (string|undefined),
+ * starred: boolean,
+ * time: number,
+ * title: string,
+ * url: string}}
+ */
+var HistoryEntry;
+
+/**
+ * The type of the history results info object. The definition is based on
+ * chrome/browser/ui/webui/history_ui.cc:
+ * BrowsingHistoryHandler::QueryComplete()
+ * @typedef {{finished: boolean,
+ * hasSyncedResults: (boolean|undefined),
+ * queryEndTime: string,
+ * queryStartTime: string,
+ * term: string}}
+ */
+var HistoryQuery;
+
MenuButton.createDropDownArrows();
/**
* 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 {integer} maxBucketValue The max value for the last histogram bucket.
- * @param {integer} value The value to record in the histogram.
+ * @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,
/**
* Class to hold all the information about an entry in our model.
- * @param {Object} result An object containing the visit's data.
+ * @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.
this.dateTimeOfDay = result.dateTimeOfDay || '';
this.dateShort = result.dateShort || '';
- // Shows the filtering behavior for that host (only used for managed users).
- // A value of |ManagedModeFilteringBehavior.ALLOW| is not displayed so it is
- // used as the default value.
- this.hostFilteringBehavior = ManagedModeFilteringBehavior.ALLOW;
+ // 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 (typeof result.hostFilteringBehavior != 'undefined')
this.hostFilteringBehavior = result.hostFilteringBehavior;
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('div', 'time');
- var entryBox = createElementWithClassName('label', 'entry-box');
+ var time = createElementWithClassName('label', 'time');
+ var entryBox = createElementWithClassName('div', 'entry-box');
var domain = createElementWithClassName('div', 'domain');
this.id_ = this.model_.nextVisitId_++;
+ var self = this;
// Only create the checkbox if it can be used either to delete an entry or to
// block/allow it.
checkbox.id = 'checkbox-' + this.id_;
checkbox.time = this.date.getTime();
checkbox.addEventListener('click', checkboxClicked);
+ time.setAttribute('for', checkbox.id);
entryBox.appendChild(checkbox);
- // Clicking anywhere in the entryBox will check/uncheck the checkbox.
- entryBox.setAttribute('for', checkbox.id);
- entryBox.addEventListener('mousedown', entryBoxMousedown);
- entryBox.addEventListener('click', entryBoxClick);
+ 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', entryBoxMousedown);
+ 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 self = this;
var setActiveVisit = function(e) {
activeVisit = self;
var menu = $('action-menu');
entryBox.appendChild(time);
- var bookmarkSection = createElementWithClassName('div', 'bookmark-section');
+ 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');
- bookmarkSection.classList.remove('starred');
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));
}
entryBox.appendChild(bookmarkSection);
- var visitEntryWrapper = entryBox.appendChild(document.createElement('div'));
+ 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 {
- visitEntryWrapper.appendChild(this.getTitleDOM_(isSearchResult));
+ var title = visitEntryWrapper.appendChild(
+ this.getTitleDOM_(isSearchResult));
+
if (addTitleFavicon)
this.addFaviconToElement_(visitEntryWrapper);
+
+ if (focusless)
+ title.querySelector('a').tabIndex = -1;
+
visitEntryWrapper.appendChild(domain);
}
removeButton.setAttribute('aria-label',
loadTimeData.getString('removeFromHistory'));
removeButton.classList.add('custom-appearance');
- removeButton.addEventListener('click', function(e) {
- self.removeFromHistory();
- e.stopPropagation();
- e.preventDefault();
- });
+ removeButton.addEventListener(
+ 'click', this.removeEntryFromHistory_.bind(this));
entryBox.appendChild(removeButton);
// Support clicking anywhere inside the entry box.
entryBox.addEventListener('click', function(e) {
- e.currentTarget.querySelector('a').click();
+ if (!e.defaultPrevented)
+ self.titleLink.click();
});
} else {
var dropDown = createElementWithClassName('button', 'drop-down');
dropDown.title = loadTimeData.getString('actionMenuDescription');
dropDown.setAttribute('menu', '#action-menu');
dropDown.setAttribute('aria-haspopup', 'true');
+
+ if (focusless)
+ dropDown.tabIndex = -1;
+
cr.ui.decorate(dropDown, MenuButton);
+ dropDown.respondToArrowKeys = false;
dropDown.addEventListener('mousedown', setActiveVisit);
dropDown.addEventListener('focus', setActiveVisit);
- // Prevent clicks on the drop down from affecting the checkbox.
- dropDown.addEventListener('click', function(e) { e.preventDefault(); });
+ // 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);
}
*/
Visit.prototype.removeFromHistory = function() {
recordUmaAction('HistoryPage_EntryMenuRemoveFromHistory');
- var self = this;
this.model_.removeVisitsFromHistory([this], function() {
- removeEntryFromView(self.domNode_);
- });
+ 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: ------------------------------------------------------------
/**
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.keyIdentifier == 'U+0008' || e.keyIdentifier == 'U+007F') &&
+ !this.model_.isDeletingVisits()) {
+ this.removeEntryFromHistory_(e);
+ }
+};
+
+/**
+ * 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)
+ return;
+
+ this.model_.getView().onBeforeRemove(this);
+ this.removeFromHistory();
+ e.preventDefault();
};
// Visit, private, static: ----------------------------------------------------
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.
*/
/**
* Receiver for history query.
- * @param {Object} info An object containing information about the query.
- * @param {Array} results A list of results.
+ * @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
/**
* 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 {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({
timestamps: visits[i].allTimestamps
});
}
+
chrome.send('removeVisits', toBeRemoved);
this.deleteCompleteCallback_ = callback;
};
+/** @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.
*/
// Getter and setter for HistoryModel.rangeInDays_.
Object.defineProperty(HistoryModel.prototype, 'rangeInDays', {
- get: function() {
+ get: /** @this {HistoryModel} */function() {
return this.rangeInDays_;
},
- set: function(range) {
+ set: /** @this {HistoryModel} */function(range) {
this.rangeInDays_ = range;
}
});
* calendar month, 1 to the previous one, etc.
*/
Object.defineProperty(HistoryModel.prototype, 'offset', {
- get: function() {
+ get: /** @this {HistoryModel} */function() {
return this.offset_;
},
- set: function(offset) {
+ set: /** @this {HistoryModel} */function(offset) {
this.offset_ = offset;
}
});
// Setter for HistoryModel.requestedPage_.
Object.defineProperty(HistoryModel.prototype, 'requestedPage', {
- set: function(page) {
+ 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);
+};
+
// HistoryModel, Private: -----------------------------------------------------
/**
HistoryModel.prototype.clearModel_ = function() {
this.inFlight_ = false; // Whether a query is inflight.
this.searchText_ = '';
- // Whether this user is a managed user.
- this.isManagedProfile = loadTimeData.getBoolean('isManagedProfile');
+ // 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
};
/**
- * Enables or disables grouping by domain.
- * @param {boolean} groupByDomain New groupByDomain_ value.
- */
-HistoryModel.prototype.setGroupByDomain = function(groupByDomain) {
- this.groupByDomain_ = groupByDomain;
- this.offset_ = 0;
-};
-
-/**
* Gets whether we are grouped by domain.
* @return {boolean} Whether the results are grouped by domain.
*/
};
///////////////////////////////////////////////////////////////////////////////
+// HistoryFocusObserver:
+
+/**
+ * @constructor
+ * @implements {cr.ui.FocusRow.Observer}
+ */
+function HistoryFocusObserver() {}
+
+HistoryFocusObserver.prototype = {
+ /** @override */
+ onActivate: function(row) {
+ this.getActiveRowElement_(row).classList.add('active');
+ },
+
+ /** @override */
+ onDeactivate: function(row) {
+ this.getActiveRowElement_(row).classList.remove('active');
+ },
+
+ /**
+ * @param {cr.ui.FocusRow} row The row to find an element for.
+ * @return {Element} |row|'s "active" element.
+ * @private
+ */
+ getActiveRowElement_: function(row) {
+ return findAncestorByClass(row.items[0], 'entry') ||
+ findAncestorByClass(row.items[0], 'site-domain-wrapper');
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////////
// HistoryView:
/**
this.editButtonTd_ = $('edit-button');
this.editingControlsDiv_ = $('editing-controls');
this.resultDiv_ = $('results-display');
+ this.focusGrid_ = new cr.ui.FocusGrid(this.resultDiv_,
+ new HistoryFocusObserver);
this.pageDiv_ = $('results-pagination');
this.model_ = model;
this.pageIndex_ = 0;
var handleRangeChange = function(e) {
// Update the results and save the last state.
- self.setRangeInDays(parseInt(e.target.value, 10));
+ var value = parseInt(e.target.value, 10);
+ self.setRangeInDays(/** @type {HistoryModel.Range.<number>} */(value));
};
// Add handlers for the range options.
/**
* Set the current range for grouped results.
- * @param {string} range The number of days to which the range should be set.
+ * @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.
/**
* Get the current range in days.
- * @return {number} Current range in days from the model.
+ * @return {HistoryModel.Range} Current range in days from the model.
*/
HistoryView.prototype.getRangeInDays = function() {
return this.model_.rangeInDays;
// 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.
- if (this.model_.visits_.length)
- document.body.classList.add('has-results');
- else
- document.body.classList.remove('has-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 hasResults = this.model_.visits_.length > 0;
var isSearch = this.model_.getSearchText().length > 0;
$('search-field').hidden = !(hasResults || isSearch);
}
};
/**
+ * @param {Visit} visit The visit about to be removed from this view.
+ */
+HistoryView.prototype.onBeforeRemove = function(visit) {
+ assert(this.currentVisits_.indexOf(visit) >= 0);
+
+ var pos = this.focusGrid_.getPositionForTarget(document.activeElement);
+ if (!pos)
+ return;
+
+ var row = this.focusGrid_.rows[pos.row + 1] ||
+ this.focusGrid_.rows[pos.row - 1];
+ if (row)
+ row.focusIndex(Math.min(pos.col, row.items.length - 1));
+};
+
+/** @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 pos = this.focusGrid_.getPositionForTarget(document.activeElement);
+ var row = this.focusGrid_.rows[pos.row];
+ row.focusIndex(Math.min(pos.col + 1, row.items.length - 1));
+};
+
+/** @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.onModelReady(true); // Shows "No entries" message.
+};
+
+/**
* Adjusts the position of the notification bar based on the size of the page.
*/
HistoryView.prototype.positionNotificationBar = function() {
* @private
*/
HistoryView.prototype.clear_ = function() {
+ var alertOverlay = $('alertOverlay');
+ if (alertOverlay && alertOverlay.classList.contains('showing'))
+ hideConfirmationOverlay();
+
this.resultDiv_.textContent = '';
this.currentVisits_.forEach(function(visit) {
var siteResults = results.appendChild(
createElementWithClassName('li', 'site-entry'));
- // Make a wrapper that will contain the arrow, the favicon and the domain.
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 =
siteDomainCheckbox.type = 'checkbox';
siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
siteDomainCheckbox.domain_ = domain;
-
- siteDomainWrapper.appendChild(siteDomainCheckbox);
+ siteDomainCheckbox.setAttribute('aria-label', domain);
+ siteDomainRow.appendChild(siteDomainCheckbox);
}
- var siteArrow = siteDomainWrapper.appendChild(
- createElementWithClassName('div', 'site-domain-arrow collapse'));
- var siteDomain = siteDomainWrapper.appendChild(
+ var siteArrow = siteDomainRow.appendChild(
+ createElementWithClassName('div', 'site-domain-arrow'));
+ var siteDomain = siteDomainRow.appendChild(
createElementWithClassName('div', 'site-domain'));
var siteDomainLink = siteDomain.appendChild(
createElementWithClassName('button', 'link-button'));
domainVisits[0].addFaviconToElement_(siteDomain);
- siteDomainWrapper.addEventListener('click', toggleHandler);
+ siteDomainWrapper.addEventListener(
+ 'click', this.toggleGroupedVisits_.bind(this));
- if (this.model_.isManagedProfile) {
- siteDomainWrapper.appendChild(
- getManagedStatusDOM(domainVisits[0].hostFilteringBehavior));
+ if (this.model_.isSupervisedProfile) {
+ siteDomainRow.appendChild(
+ getFilteringStatusDOM(domainVisits[0].hostFilteringBehavior));
}
siteResults.appendChild(siteDomainWrapper);
// 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({
- useMonthDate: isMonthGroupedResult
+ focusless: true,
+ useMonthDate: isMonthGroupedResult,
}));
this.setVisitRendered_(visit);
}
/**
* Adds the results for a month.
* @param {Array} visits Visits returned by the query.
- * @param {Element} parentElement Element to which to add the results to.
+ * @param {Node} parentNode Node to which to add the results to.
* @private
*/
-HistoryView.prototype.addMonthResults_ = function(visits, parentElement) {
+HistoryView.prototype.addMonthResults_ = function(visits, parentNode) {
if (visits.length == 0)
return;
- var monthResults = parentElement.appendChild(
- createElementWithClassName('ol', 'month-results'));
+ 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');
* 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 {Element} parentElement Element to which to add the results to.
+ * @param {Node} parentNode Node to which to add the results to.
* @private
*/
-HistoryView.prototype.addDayResults_ = function(visits, parentElement) {
+HistoryView.prototype.addDayResults_ = function(visits, parentNode) {
if (visits.length == 0)
return;
var firstVisit = visits[0];
- var day = parentElement.appendChild(createElementWithClassName('h3', 'day'));
+ 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 = parentElement.appendChild(
- createElementWithClassName('ol', 'day-results'));
+ var dayResults = /** @type {HTMLElement} */(parentNode.appendChild(
+ createElementWithClassName('ol', 'day-results')));
// Don't add checkboxes if entries can not be edited.
if (!this.model_.editingEntriesAllowed)
/**
* Adds the text that shows the current interval, used for week and month
* results.
- * @param {Element} resultsFragment The element to which the interval will be
+ * @param {Node} resultsFragment The element to which the interval will be
* added to.
* @private
*/
++dayEnd;
this.addDayResults_(
- results.slice(dayStart, dayEnd), resultsFragment, groupByDomain);
+ results.slice(dayStart, dayEnd), resultsFragment);
}
}
}
// After the results have been added to the DOM, determine the size of the
// time column.
- this.setTimeColumnWidth_(this.resultDiv_);
+ this.setTimeColumnWidth_();
+};
+
+var focusGridRowSelector = [
+ '.day-results > .entry:not(.fade-out)',
+ '.expand .grouped .entry:not(.fade-out)',
+ '.site-domain-wrapper'
+].join(', ');
+
+var focusGridColumnSelector = [
+ '.entry-box input',
+ '.bookmark-section.starred',
+ '.title a',
+ '.drop-down',
+ '.domain-checkbox',
+ '.link-button',
+].join(', ');
+
+/** @private */
+HistoryView.prototype.updateFocusGrid_ = function() {
+ var rows = this.resultDiv_.querySelectorAll(focusGridRowSelector);
+ var grid = [];
+
+ for (var i = 0; i < rows.length; ++i) {
+ assert(rows[i].parentNode);
+ grid.push(rows[i].querySelectorAll(focusGridColumnSelector));
+ }
+
+ this.focusGrid_.setGrid(grid);
};
/**
HistoryView.prototype.updateNavBar_ = function() {
this.updateRangeButtons_();
- // Managed users have the control bar on top, don't show it on the bottom
+ // Supervised users have the control bar on top, don't show it on the bottom
// as well.
- if (!loadTimeData.getBoolean('isManagedProfile')) {
+ if (!loadTimeData.getBoolean('isSupervisedProfile')) {
$('newest-button').hidden = this.pageIndex_ == 0;
$('newer-button').hidden = this.pageIndex_ == 0;
$('older-button').hidden =
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';
+ // -webkit-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');
+ this.updateFocusGrid_();
+};
+
///////////////////////////////////////////////////////////////////////////////
// State object:
/**
var result = {
q: '',
page: 0,
- grouped: false,
range: 0,
offset: 0
};
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) {
+ if (pair.length > 1)
result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
- }
}
return result;
// Create default view.
var hashData = pageState.getHashData();
- var grouped = (hashData.grouped == 'true') || historyModel.getGroupByDomain();
var page = parseInt(hashData.page, 10) || historyView.getPage();
- var range = parseInt(hashData.range, 10) || historyView.getRangeInDays();
+ 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);
// Only show the controls if the command line switch is activated.
if (loadTimeData.getBoolean('groupByDomain') ||
- loadTimeData.getBoolean('isManagedProfile')) {
+ loadTimeData.getBoolean('isSupervisedProfile')) {
// Hide the top container which has the "Clear browsing data" and "Remove
- // selected entries" buttons since they're unavailable in managed mode
+ // selected entries" buttons since they're unavailable for supervised users.
$('top-container').hidden = true;
$('history-page').classList.add('big-topbar-page');
$('filter-controls').hidden = false;
}
- var title = loadTimeData.getString('title');
- uber.invokeMethodOnParent('setTitle', {title: title});
+ uber.setTitle(loadTimeData.getString('title'));
// Adjust the position of the notification bar when the window size changes.
window.addEventListener('resize',
historyView.positionNotificationBar.bind(historyView));
- cr.ui.FocusManager.disableMouseFocusOnButtons();
-
if (isMobileVersion()) {
// Move the search box out of the header.
var resultsDisplay = $('results-display');
$('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();
});
}
/**
- * Updates the managed filter status labels of a host/URL entry to the current
- * value.
+ * 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 {ManagedModeFilteringBehavior} newStatus The filter status of the
+ * @param {SupervisedUserFilteringBehavior} newStatus The filter status of the
* current domain/URL.
*/
function updateHostStatus(statusElement, newStatus) {
statusElement.querySelector('.filtering-behavior');
// Reset to the base class first, then add modifier classes if needed.
filteringBehaviorDiv.className = 'filtering-behavior';
- if (newStatus == ManagedModeFilteringBehavior.BLOCK) {
+ if (newStatus == SupervisedUserFilteringBehavior.BLOCK) {
filteringBehaviorDiv.textContent =
loadTimeData.getString('filterBlocked');
filteringBehaviorDiv.classList.add('filter-blocked');
function showConfirmationOverlay() {
$('alertOverlay').classList.add('showing');
$('overlay').hidden = false;
+ $('history-page').setAttribute('aria-hidden', 'true');
uber.invokeMethodOnParent('beginInterceptingEvents');
+
+ // If an element is focused behind the confirm overlay, blur it so focus
+ // doesn't accidentally get stuck behind it.
+ if ($('history-page').contains(document.activeElement))
+ document.activeElement.blur();
}
/**
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
+ * @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
+ * @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('cancel'),
loadTimeData.getString('deleteConfirm'),
- cancelCallback,
- okCallback);
+ loadTimeData.getString('cancel'),
+ okCallback,
+ cancelCallback);
showConfirmationOverlay();
}
// Disable the checkbox and put a strikethrough style on the link, so the
// user can see what will be deleted.
- var link = entry.querySelector('a');
checkbox.disabled = true;
- link.classList.add('to-be-removed');
+ 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
var checkbox = disabledItems[i];
checkbox.disabled = false;
- var entryBox = findAncestorByClass(checkbox, 'entry-box');
- entryBox.querySelector('a').classList.remove('to-be-removed');
+ var entry = findAncestorByClass(checkbox, 'entry');
+ entry.visit.titleLink.classList.remove('to-be-removed');
}
$('overlay').removeEventListener('cancelOverlay', onCancelRemove);
hideConfirmationOverlay();
* @param {Event} e The click event.
*/
function checkboxClicked(e) {
- handleCheckboxStateChange(e.currentTarget, e.shiftKey);
+ handleCheckboxStateChange(/** @type {!HTMLInputElement} */(e.currentTarget),
+ e.shiftKey);
}
/**
var begin = Math.min(id, selectionAnchor);
var end = Math.max(id, selectionAnchor);
for (var i = begin; i <= end; i++) {
- var checkbox = document.querySelector('#checkbox-' + i);
- if (checkbox) {
- checkbox.checked = checked;
- updateParentCheckbox(checkbox);
+ var ithCheckbox = document.querySelector('#checkbox-' + i);
+ if (ithCheckbox) {
+ ithCheckbox.checked = checked;
+ updateParentCheckbox(ithCheckbox);
}
}
}
* @param {Event} e The click event.
*/
function domainCheckboxClicked(e) {
- var siteEntry = findAncestorByClass(e.currentTarget, 'site-entry');
+ 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++)
var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
if (groupCheckbox)
- groupCheckbox.checked = false;
+ groupCheckbox.checked = false;
}
function entryBoxMousedown(event) {
}
/**
- * Handle click event for entryBox labels.
- * @param {!MouseEvent} event A click event.
+ * 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;
return;
}
}
- var checkbox = event.currentTarget.control;
+ 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.
* have been removed from the DOM and the animations have completed.
*/
function onEntryRemoved() {
- historyView.updateSelectionEditButtons();
+ historyView.onEntryRemoved();
}
/**
* @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) {
+function removeNode(node, onRemove, opt_scope) {
node.classList.add('fade-out'); // Trigger CSS fade out animation.
// Delete the node when the animation is complete.
e.stopPropagation();
if (onRemove)
- onRemove();
+ onRemove.call(opt_scope);
});
}
/**
- * Removes a single entry from the view. Also removes gaps before and after
- * entry if necessary.
- * @param {Node} entry The DOM node representing the entry to be removed.
- */
-function removeEntryFromView(entry) {
- var nextEntry = entry.nextSibling;
- var previousEntry = entry.previousSibling;
- var dayResults = findAncestorByClass(entry, 'day-results');
-
- var toRemove = [entry];
-
- // if there is no previous entry, and the next entry is a gap, remove it
- if (!previousEntry && nextEntry && nextEntry.className == 'gap')
- toRemove.push(nextEntry);
-
- // if there is no next entry, and the previous entry is a gap, remove it
- if (!nextEntry && previousEntry && previousEntry.className == 'gap')
- toRemove.push(previousEntry);
-
- // if both the next and previous entries are gaps, remove one
- if (nextEntry && nextEntry.className == 'gap' &&
- previousEntry && previousEntry.className == 'gap') {
- toRemove.push(nextEntry);
- }
-
- // If removing the last entry on a day, remove the entire day.
- 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, and then calls |onEntryRemoved|.
- 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);
- }
-}
-
-/**
- * Toggles an element in the grouped history.
- * @param {Element} e The element which was clicked on.
- */
-function toggleHandler(e) {
- var innerResultList = e.currentTarget.parentElement.querySelector(
- '.site-results');
- var innerArrow = e.currentTarget.parentElement.querySelector(
- '.site-domain-arrow');
- if (innerArrow.classList.contains('collapse')) {
- innerResultList.style.height = 'auto';
- // -webkit-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);
- innerArrow.classList.remove('collapse');
- innerArrow.classList.add('expand');
- } else {
- innerResultList.style.height = 0;
- innerArrow.classList.remove('expand');
- innerArrow.classList.add('collapse');
- }
-}
-
-/**
- * Builds the DOM elements to show the managed status of a domain/URL.
- * @param {ManagedModeFilteringBehavior} filteringBehavior The filter behavior
- * for this item.
+ * 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 getManagedStatusDOM(filteringBehavior) {
+function getFilteringStatusDOM(filteringBehavior) {
var filterStatusDiv = createElementWithClassName('div', 'filter-status');
var filteringBehaviorDiv =
createElementWithClassName('div', 'filtering-behavior');
/**
* Our history system calls this function with results from searches.
- * @param {Object} info An object containing information about the query.
- * @param {Array} results A list of results.
+ * @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);