blob: a8da55f7c1df067fee3d4a3c7b8b6a666f276df9 [file] [log] [blame]
// Copyright 2016 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.
/**
* @fileoverview
* 'all-sites' is the polymer element for showing the list of all sites under
* Site Settings.
*/
Polymer({
is: 'all-sites',
behaviors: [
SiteSettingsBehavior,
WebUIListenerBehavior,
settings.RouteObserverBehavior,
settings.GlobalScrollTargetBehavior,
],
properties: {
/**
* Map containing sites to display in the widget, grouped into their
* eTLD+1 names.
* @type {!Map<string, !SiteGroup>}
*/
siteGroupMap: {
type: Object,
value: function() {
return new Map();
},
},
/**
* Needed by GlobalScrollTargetBehavior.
* @override
*/
subpageRoute: {
type: Object,
value: settings.routes.SITE_SETTINGS_ALL,
readOnly: true,
},
/**
* The search query entered into the All Sites search textbox. Used to
* filter the All Sites list.
* @private
*/
searchQuery_: {
type: String,
value: '',
},
/**
* All possible sort methods.
* @type {!{name: string, mostVisited: string, storage: string}}
* @private
*/
sortMethods_: {
type: Object,
value: {
name: 'name',
mostVisited: 'most-visited',
storage: 'data-stored',
},
readOnly: true,
},
/**
* Stores the last selected item in the All Sites list.
* @type {?{item: !SiteGroup, index: number}}
* @private
*/
selectedItem_: Object,
/**
* Used to determine focus between settings pages.
* @type {!Map<string, (string|Function)>}
*/
focusConfig: {
type: Object,
observer: 'focusConfigChanged_',
},
},
/** @private {?settings.LocalDataBrowserProxy} */
localDataBrowserProxy_: null,
/** @override */
created: function() {
this.localDataBrowserProxy_ =
settings.LocalDataBrowserProxyImpl.getInstance();
},
/** @override */
ready: function() {
this.addWebUIListener(
'onLocalStorageListFetched', this.onLocalStorageListFetched.bind(this));
this.addWebUIListener(
'contentSettingSitePermissionChanged', this.populateList_.bind(this));
this.addEventListener(
'site-entry-selected',
(/** @type {!{detail: !{item: !SiteGroup, index: number}}} */ e) => {
this.selectedItem_ = e.detail;
});
this.addEventListener('site-entry-storage-updated', () => {
this.debounce('site-entry-storage-updated', () => {
if (this.sortMethods_ &&
this.$.sortMethod.value == this.sortMethods_.storage) {
this.onSortMethodChanged_();
}
}, 500);
});
this.populateList_();
},
/** @override */
attached: function() {
// Set scrollOffset so the iron-list scrolling accounts for the space the
// title takes.
Polymer.RenderStatus.afterNextRender(this, () => {
this.$.allSitesList.scrollOffset = this.$.allSitesList.offsetTop;
});
},
/**
* Retrieves a list of all known sites with site details.
* @private
*/
populateList_: function() {
/** @type {!Array<settings.ContentSettingsTypes>} */
const contentTypes = this.getCategoryList();
// Make sure to include cookies, because All Sites handles data storage +
// cookies as well as regular settings.ContentSettingsTypes.
if (!contentTypes.includes(settings.ContentSettingsTypes.COOKIES))
contentTypes.push(settings.ContentSettingsTypes.COOKIES);
this.browserProxy.getAllSites(contentTypes).then((response) => {
response.forEach(siteGroup => {
this.siteGroupMap.set(siteGroup.etldPlus1, siteGroup);
});
this.forceListUpdate_();
});
},
/**
* Integrate sites using local storage into the existing sites map, as there
* may be overlap between the existing sites.
* @param {!Array<!SiteGroup>} list The list of sites using local storage.
*/
onLocalStorageListFetched: function(list) {
list.forEach(storageSiteGroup => {
if (this.siteGroupMap.has(storageSiteGroup.etldPlus1)) {
const siteGroup = this.siteGroupMap.get(storageSiteGroup.etldPlus1);
const storageOriginInfoMap = new Map();
storageSiteGroup.origins.forEach(
originInfo =>
storageOriginInfoMap.set(originInfo.origin, originInfo));
// If there is an overlapping origin, update the original
// |originInfo|.
siteGroup.origins.forEach(originInfo => {
if (!storageOriginInfoMap.has(originInfo.origin))
return;
Object.apply(originInfo, storageOriginInfoMap.get(originInfo.origin));
storageOriginInfoMap.delete(originInfo.origin);
});
// Otherwise, add it to the list.
storageOriginInfoMap.forEach(
originInfo => siteGroup.origins.push(originInfo));
} else {
this.siteGroupMap.set(storageSiteGroup.etldPlus1, storageSiteGroup);
}
});
this.forceListUpdate_();
},
/**
* Filters the all sites list with the given search query text.
* @param {!Map<string, !SiteGroup>} siteGroupMap The map of sites to filter.
* @param {string} searchQuery The filter text.
* @return {!Array<!SiteGroup>}
* @private
*/
filterPopulatedList_: function(siteGroupMap, searchQuery) {
const result = [];
for (const [etldPlus1, siteGroup] of siteGroupMap) {
if (siteGroup.origins.find(
originInfo => originInfo.origin.includes(searchQuery))) {
result.push(siteGroup);
}
}
return this.sortSiteGroupList_(result);
},
/**
* Sorts the given SiteGroup list with the currently selected sort method.
* @param {!Array<!SiteGroup>} siteGroupList The list of sites to sort.
* @return {!Array<!SiteGroup>}
* @private
*/
sortSiteGroupList_: function(siteGroupList) {
const sortMethod = this.$.sortMethod.value;
if (!this.sortMethods_)
return siteGroupList;
if (sortMethod == this.sortMethods_.mostVisited) {
siteGroupList.sort(this.mostVisitedComparator_);
} else if (sortMethod == this.sortMethods_.storage) {
// Storage is loaded asynchronously, so make sure it's updated for every
// item in the list to ensure the sorting is correct.
const etldPlus1List = siteGroupList.reduce((list, siteGroup) => {
if (siteGroup.origins.length > 1 && siteGroup.etldPlus1.length > 0)
list.push(siteGroup.etldPlus1);
return list;
}, []);
this.localDataBrowserProxy_.getNumCookiesList(etldPlus1List)
.then(numCookiesList => {
assert(etldPlus1List.length == numCookiesList.length);
numCookiesList.forEach(cookiesPerEtldPlus1 => {
this.siteGroupMap.get(cookiesPerEtldPlus1.etldPlus1).numCookies =
cookiesPerEtldPlus1.numCookies;
});
// |siteGroupList| by this point should have already been provided
// to the iron list, so just sort in-place here and make sure to
// re-render the item order.
siteGroupList.sort(this.storageComparator_);
this.$.allSitesList.fire('iron-resize');
});
} else if (sortMethod == this.sortMethods_.name) {
siteGroupList.sort(this.nameComparator_);
}
return siteGroupList;
},
/**
* Comparator used to sort SiteGroups by the amount of engagement the user has
* with the origins listed inside it. Note only the maximum engagement is used
* for each SiteGroup (as opposed to the sum) in order to prevent domains with
* higher numbers of origins from always floating to the top of the list.
* @param {!SiteGroup} siteGroup1
* @param {!SiteGroup} siteGroup2
* @private
*/
mostVisitedComparator_: function(siteGroup1, siteGroup2) {
const getMaxEngagement = (max, originInfo) => {
return (max > originInfo.engagement) ? max : originInfo.engagement;
};
const score1 = siteGroup1.origins.reduce(getMaxEngagement, 0);
const score2 = siteGroup2.origins.reduce(getMaxEngagement, 0);
return score2 - score1;
},
/**
* Comparator used to sort SiteGroups by the amount of storage they use. Note
* this sorts in descending order.
* TODO(https://crbug.com/835712): Account for website storage in sorting by
* storage used.
* @param {!SiteGroup} siteGroup1
* @param {!SiteGroup} siteGroup2
* @private
*/
storageComparator_: function(siteGroup1, siteGroup2) {
const getOverallUsage = siteGroup => {
let usage = 0;
siteGroup.origins.forEach(originInfo => {
usage += originInfo.usage;
});
return usage;
};
const siteGroup1Size = getOverallUsage(siteGroup1);
const siteGroup2Size = getOverallUsage(siteGroup2);
// Use the number of cookies as a tie breaker.
return siteGroup2Size - siteGroup1Size ||
siteGroup2.numCookies - siteGroup1.numCookies;
},
/**
* Comparator used to sort SiteGroups by their eTLD+1 name (domain).
* @param {!SiteGroup} siteGroup1
* @param {!SiteGroup} siteGroup2
* @private
*/
nameComparator_: function(siteGroup1, siteGroup2) {
return siteGroup1.etldPlus1.localeCompare(siteGroup2.etldPlus1);
},
/**
* Called when the input text in the search textbox is updated.
* @private
*/
onSearchChanged_: function() {
const searchElement = this.$$('cr-search-field');
this.searchQuery_ = searchElement.getSearchInput().value.toLowerCase();
},
/**
* Called when the user chooses a different sort method to the default.
* @private
*/
onSortMethodChanged_: function() {
this.$.allSitesList.items =
this.sortSiteGroupList_(this.$.allSitesList.items);
// Force the iron-list to rerender its items, as the order has changed.
this.$.allSitesList.fire('iron-resize');
},
/**
* Forces the all sites list to update its list of items, taking into account
* the search query and the sort method, then re-renders it.
* @private
*/
forceListUpdate_: function() {
this.$.allSitesList.items =
this.filterPopulatedList_(this.siteGroupMap, this.searchQuery_);
this.$.allSitesList.fire('iron-resize');
},
/**
* @param {!Map<string, (string|Function)>} newConfig
* @param {?Map<string, (string|Function)>} oldConfig
* @private
*/
focusConfigChanged_: function(newConfig, oldConfig) {
// focusConfig is set only once on the parent, so this observer should only
// fire once.
assert(!oldConfig);
if (!settings.routes.SITE_SETTINGS_ALL)
return;
const onNavigatedTo = () => {
this.async(() => {
if (this.selectedItem_ == null || this.siteGroupMap.size == 0)
return;
// Focus the site-entry to ensure the iron-list renders it, otherwise
// the query selector will not be able to find it. Note the index is
// used here instead of the item, in case the item was already removed.
const index = Math.max(
0, Math.min(this.selectedItem_.index, this.siteGroupMap.size));
this.$.allSitesList.focusItem(index);
this.selectedItem_ = null;
});
};
this.focusConfig.set(
settings.routes.SITE_SETTINGS_SITE_DETAILS.path, onNavigatedTo);
},
});