blob: e58584a556e25195ffed2a04e0a63d3552b3bc4d [file] [log] [blame]
// Copyright 2015 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
* 'site-list' shows a list of Allowed and Blocked sites for a given
* category.
*/
Polymer({
is: 'site-list',
behaviors: [
SiteSettingsBehavior,
WebUIListenerBehavior,
ListPropertyUpdateBehavior,
],
properties: {
/**
* Some content types (like Location) do not allow the user to manually
* edit the exception list from within Settings.
* @private
*/
readOnlyList: {
type: Boolean,
value: false,
},
categoryHeader: String,
/**
* The site serving as the model for the currently open action menu.
* @private {?SiteException}
*/
actionMenuSite_: Object,
/**
* Whether the "edit exception" dialog should be shown.
* @private
*/
showEditExceptionDialog_: Boolean,
/**
* Array of sites to display in the widget.
* @type {!Array<SiteException>}
*/
sites: {
type: Array,
value: function() {
return [];
},
},
/**
* The type of category this widget is displaying data for. Normally
* either 'allow' or 'block', representing which sites are allowed or
* blocked respectively.
*/
categorySubtype: {
type: String,
value: settings.INVALID_CATEGORY_SUBTYPE,
},
/** @private */
hasIncognito_: Boolean,
/** @private */
showAddSiteDialog_: Boolean,
/**
* Whether to show the Allow action in the action menu.
* @private
*/
showAllowAction_: Boolean,
/**
* Whether to show the Block action in the action menu.
* @private
*/
showBlockAction_: Boolean,
/**
* Whether to show the 'Clear on exit' action in the action
* menu.
* @private
*/
showSessionOnlyAction_: Boolean,
/**
* All possible actions in the action menu.
* @private
*/
actions_: {
readOnly: true,
type: Object,
values: {
ALLOW: 'Allow',
BLOCK: 'Block',
RESET: 'Reset',
SESSION_ONLY: 'SessionOnly',
}
},
/** @private */
lastFocused_: Object,
/** @private */
listBlurred_: Boolean,
/** @private */
tooltipText_: String,
},
// <if expr="chromeos">
/**
* Android messages info object containing messages feature state and
* exception origin.
* @private {?settings.AndroidSmsInfo}
*/
androidSmsInfo_: null,
// </if>
/**
* The element to return focus to, when the currently active dialog is closed.
* @private {?HTMLElement}
*/
activeDialogAnchor_: null,
observers: ['configureWidget_(category, categorySubtype)'],
/** @override */
ready: function() {
this.addWebUIListener(
'contentSettingSitePermissionChanged',
this.siteWithinCategoryChanged_.bind(this));
this.addWebUIListener(
'onIncognitoStatusChanged', this.onIncognitoStatusChanged_.bind(this));
// <if expr="chromeos">
this.addWebUIListener('settings.onAndroidSmsInfoChange', (info) => {
this.androidSmsInfo_ = info;
this.populateList_();
});
// </if>
this.browserProxy.updateIncognitoStatus();
},
/**
* Called when a site changes permission.
* @param {string} category The category of the site that changed.
* @param {string} site The site that changed.
* @private
*/
siteWithinCategoryChanged_: function(category, site) {
if (category == this.category)
this.configureWidget_();
},
/**
* Called for each site list when incognito is enabled or disabled. Only
* called on change (opening N incognito windows only fires one message).
* Another message is sent when the *last* incognito window closes.
* @private
*/
onIncognitoStatusChanged_: function(hasIncognito) {
this.hasIncognito_ = hasIncognito;
// The SESSION_ONLY list won't have any incognito exceptions. (Minor
// optimization, not required).
if (this.categorySubtype == settings.ContentSetting.SESSION_ONLY)
return;
// A change notification is not sent for each site. So we repopulate the
// whole list when the incognito profile is created or destroyed.
this.populateList_();
},
/**
* Configures the action menu, visibility of the widget and shows the list.
* @private
*/
configureWidget_: function() {
if (this.category == undefined)
return;
// The observer for All Sites fires before the attached/ready event, so
// initialize this here.
if (this.browserProxy_ === undefined) {
this.browserProxy_ =
settings.SiteSettingsPrefsBrowserProxyImpl.getInstance();
}
this.setUpActionMenu_();
// <if expr="not chromeos">
this.populateList_();
// </if>
// <if expr="chromeos">
this.updateAndroidSmsInfo_().then(this.populateList_.bind(this));
// </if>
// The Session permissions are only for cookies.
if (this.categorySubtype == settings.ContentSetting.SESSION_ONLY) {
this.$.category.hidden =
this.category != settings.ContentSettingsTypes.COOKIES;
}
},
/**
* Whether there are any site exceptions added for this content setting.
* @return {boolean}
* @private
*/
hasSites_: function() {
return !!this.sites.length;
},
/**
* A handler for the Add Site button.
* @private
*/
onAddSiteTap_: function() {
assert(!this.readOnlyList);
this.showAddSiteDialog_ = true;
},
/** @private */
onAddSiteDialogClosed_: function() {
this.showAddSiteDialog_ = false;
cr.ui.focusWithoutInk(assert(this.$.addSite));
},
/**
* Need to use common tooltip since the tooltip in the entry is cut off from
* the iron-list.
* @param {!{detail: {target: HTMLElement, text: string}}} e
* @private
*/
onShowTooltip_: function(e) {
this.tooltipText_ = e.detail.text;
const target = e.detail.target;
// paper-tooltip normally determines the target from the |for| property,
// which is a selector. Here paper-tooltip is being reused by multiple
// potential targets. Since paper-tooltip does not expose a public property
// or method to update the target, the private property |_target| is
// updated directly.
const tooltip = this.$.tooltip;
/** @type {{updatePosition: Function}} */ (tooltip).updatePosition();
tooltip._target = target;
const parentRect = tooltip.offsetParent.getBoundingClientRect();
const rect = tooltip.getBoundingClientRect();
if (parentRect.left + parentRect.width < rect.left + rect.width) {
tooltip.style.right = '0';
tooltip.style.left = 'auto';
}
const hide = () => {
this.$.tooltip.hide();
target.removeEventListener('mouseleave', hide);
target.removeEventListener('blur', hide);
target.removeEventListener('tap', hide);
this.$.tooltip.removeEventListener('mouseenter', hide);
};
target.addEventListener('mouseleave', hide);
target.addEventListener('blur', hide);
target.addEventListener('tap', hide);
this.$.tooltip.addEventListener('mouseenter', hide);
this.$.tooltip.show();
},
// <if expr="chromeos">
/**
* Load android sms info if required and sets it to the |androidSmsInfo_|
* property. Returns a promise that resolves when load is complete.
* @private
*/
updateAndroidSmsInfo_: function() {
// |androidSmsInfo_| is only relevant for NOTIFICATIONS category. Don't
// bother fetching it for other categories.
if (this.category === settings.ContentSettingsTypes.NOTIFICATIONS &&
loadTimeData.valueExists('multideviceAllowedByPolicy') &&
loadTimeData.getBoolean('multideviceAllowedByPolicy') &&
!this.androidSmsInfo_) {
const multideviceSetupProxy =
settings.MultiDeviceBrowserProxyImpl.getInstance();
return multideviceSetupProxy.getAndroidSmsInfo().then((info) => {
this.androidSmsInfo_ = info;
});
}
return Promise.resolve();
},
/**
* Processes exceptions and adds showAndroidSmsNote field to
* the required exception item.
* @private
*/
processExceptionsForAndroidSmsInfo_: function(sites) {
if (!this.androidSmsInfo_ || !this.androidSmsInfo_.enabled) {
return sites;
}
return sites.map((site) => {
if (site.origin === this.androidSmsInfo_.origin) {
return Object.assign({showAndroidSmsNote: true}, site);
} else {
return site;
}
});
},
// </if>
/**
* Populate the sites list for display.
* @private
*/
populateList_: function() {
this.browserProxy_.getExceptionList(this.category).then(exceptionList => {
this.processExceptions_(exceptionList);
this.closeActionMenu_();
});
},
/**
* Process the exception list returned from the native layer.
* @param {!Array<RawSiteException>} exceptionList
* @private
*/
processExceptions_: function(exceptionList) {
let sites =
exceptionList
.filter(
site => site.setting != settings.ContentSetting.DEFAULT &&
site.setting == this.categorySubtype)
.map(site => this.expandSiteException(site));
// <if expr="not chromeos">
this.updateList('sites', (x) => x.origin, sites);
// </if>
// <if expr="chromeos">
sites = this.processExceptionsForAndroidSmsInfo_(sites);
this.updateList('sites', (x) => x.origin + x.showAndroidSmsNote, sites);
// </if>
},
/**
* Set up the values to use for the action menu.
* @private
*/
setUpActionMenu_: function() {
this.showAllowAction_ =
this.categorySubtype != settings.ContentSetting.ALLOW;
this.showBlockAction_ =
this.categorySubtype != settings.ContentSetting.BLOCK;
this.showSessionOnlyAction_ =
this.categorySubtype != settings.ContentSetting.SESSION_ONLY &&
this.category == settings.ContentSettingsTypes.COOKIES;
},
/**
* @return {boolean} Whether to show the "Session Only" menu item for the
* currently active site.
* @private
*/
showSessionOnlyActionForSite_: function() {
// It makes no sense to show "clear on exit" for exceptions that only apply
// to incognito. It gives the impression that they might under some
// circumstances not be cleared on exit, which isn't true.
if (!this.actionMenuSite_ || this.actionMenuSite_.incognito)
return false;
return this.showSessionOnlyAction_;
},
/**
* @param {!settings.ContentSetting} contentSetting
* @private
*/
setContentSettingForActionMenuSite_: function(contentSetting) {
assert(this.actionMenuSite_);
this.browserProxy.setCategoryPermissionForPattern(
this.actionMenuSite_.origin, this.actionMenuSite_.embeddingOrigin,
this.category, contentSetting, this.actionMenuSite_.incognito);
},
/** @private */
onAllowTap_: function() {
this.setContentSettingForActionMenuSite_(settings.ContentSetting.ALLOW);
this.closeActionMenu_();
},
/** @private */
onBlockTap_: function() {
this.setContentSettingForActionMenuSite_(settings.ContentSetting.BLOCK);
this.closeActionMenu_();
},
/** @private */
onSessionOnlyTap_: function() {
this.setContentSettingForActionMenuSite_(
settings.ContentSetting.SESSION_ONLY);
this.closeActionMenu_();
},
/** @private */
onEditTap_: function() {
// Close action menu without resetting |this.actionMenuSite_| since it is
// bound to the dialog.
/** @type {!CrActionMenuElement} */ (this.$$('cr-action-menu')).close();
this.showEditExceptionDialog_ = true;
},
/** @private */
onEditExceptionDialogClosed_: function() {
this.showEditExceptionDialog_ = false;
this.actionMenuSite_ = null;
if (this.activeDialogAnchor_) {
this.activeDialogAnchor_.focus();
this.activeDialogAnchor_ = null;
}
},
/** @private */
onResetTap_: function() {
const site = this.actionMenuSite_;
assert(site);
this.browserProxy.resetCategoryPermissionForPattern(
site.origin, site.embeddingOrigin, this.category, site.incognito);
this.closeActionMenu_();
},
/**
* @param {!Event} e
* @private
*/
onShowActionMenu_: function(e) {
this.activeDialogAnchor_ = /** @type {!HTMLElement} */ (e.detail.anchor);
this.actionMenuSite_ = e.detail.model;
/** @type {!CrActionMenuElement} */ (this.$$('cr-action-menu'))
.showAt(this.activeDialogAnchor_);
},
/** @private */
closeActionMenu_: function() {
this.actionMenuSite_ = null;
this.activeDialogAnchor_ = null;
const actionMenu =
/** @type {!CrActionMenuElement} */ (this.$$('cr-action-menu'));
if (actionMenu.open)
actionMenu.close();
},
});