blob: 2963bd3666869bb6de9806f4c4f908767e2e7db8 [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 'address-edit-dialog' is the dialog that allows editing a saved
* address.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/cr_elements/md_select_css.m.js';
import '../settings_shared_css.js';
import '../settings_vars_css.js';
import '../controls/settings_textarea.js';
import {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import {assertNotReached} from 'chrome://resources/js/assert.m.js';
import {I18nMixin} from 'chrome://resources/js/i18n_mixin.js';
import {flush, html, microTask, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from '../i18n_setup.js';
import {getTemplate} from './address_edit_dialog.html.js';
export interface SettingsAddressEditDialogElement {
$: {
dialog: CrDialogElement,
emailInput: CrInputElement,
phoneInput: CrInputElement,
saveButton: CrButtonElement,
cancelButton: CrButtonElement,
};
}
const SettingsAddressEditDialogElementBase = I18nMixin(PolymerElement);
export class SettingsAddressEditDialogElement extends
SettingsAddressEditDialogElementBase {
static get is() {
return 'settings-address-edit-dialog';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
address: Object,
title_: String,
countries_: Array,
/**
* Updates the address wrapper.
*/
countryCode_: {
type: String,
observer: 'onUpdateCountryCode_',
},
addressWrapper_: Object,
phoneNumber_: String,
email_: String,
canSave_: Boolean,
/**
* True if honorifics are enabled.
*/
showHonorific_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('showHonorific');
}
}
};
}
address: chrome.autofillPrivate.AddressEntry;
private title_: string;
private countries_: Array<chrome.autofillPrivate.CountryEntry>;
private countryCode_: string|undefined;
private addressWrapper_: Array<Array<AddressComponentUI>>;
private phoneNumber_: string;
private email_: string;
private canSave_: boolean;
private showHonorific_: boolean;
private countryInfo_: CountryDetailManager =
CountryDetailManagerImpl.getInstance();
connectedCallback() {
super.connectedCallback();
this.countryInfo_.getCountryList().then(countryList => {
this.countries_ = countryList;
this.title_ =
this.i18n(this.address.guid ? 'editAddressTitle' : 'addAddressTitle');
// |phoneNumbers| and |emailAddresses| are a single item array.
// See crbug.com/497934 for details.
this.phoneNumber_ =
this.address.phoneNumbers ? this.address.phoneNumbers[0] : '';
this.email_ =
this.address.emailAddresses ? this.address.emailAddresses[0] : '';
microTask.run(() => {
if (Object.keys(this.address).length === 0 && countryList.length > 0) {
// If the address is completely empty, the dialog is creating a new
// address. The first address in the country list is what we suspect
// the user's country is.
this.address.countryCode = countryList[0].countryCode;
}
if (this.countryCode_ === this.address.countryCode) {
this.updateAddressWrapper_();
} else {
this.countryCode_ = this.address.countryCode;
}
});
});
// Open is called on the dialog after the address wrapper has been
// updated.
}
private fire_(eventName: string, detail?: any) {
this.dispatchEvent(
new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
}
/**
* @return A CSS class to denote how long this entry is.
*/
private long_(setting: AddressComponentUI): string {
return setting.component.isLongField ? 'long' : '';
}
/**
* Updates the wrapper that represents this address in the country's format.
*/
private updateAddressWrapper_() {
// Default to the last country used if no country code is provided.
const countryCode = this.countryCode_ || this.countries_[0].countryCode;
this.countryInfo_.getAddressFormat(countryCode as string).then(format => {
this.address.languageCode = format.languageCode;
this.addressWrapper_ = format.components.flatMap(component => {
// If this is the name field, add a honorific title row before the
// name.
const addHonorific = component.row[0].field ===
chrome.autofillPrivate.AddressField.FULL_NAME &&
this.showHonorific_;
const row = component.row.map(
component => new AddressComponentUI(this.address, component));
return addHonorific ?
[[this.createHonorificAddressComponentUI(this.address)], row] :
[row];
});
// Flush dom before resize and savability updates.
flush();
this.updateCanSave_();
this.fire_('on-update-address-wrapper'); // For easier testing.
if (!this.$.dialog.open) {
this.$.dialog.showModal();
}
});
}
private updateCanSave_() {
const inputs = this.$.dialog.querySelectorAll('.address-column, select') as
NodeListOf<HTMLSelectElement|CrInputElement>;
for (let i = 0; i < inputs.length; ++i) {
if (inputs[i].value) {
this.canSave_ = true;
this.fire_('on-update-can-save'); // For easier testing.
return;
}
}
this.canSave_ = false;
this.fire_('on-update-can-save'); // For easier testing.
}
private getCode_(country: chrome.autofillPrivate.CountryEntry): string {
return country.countryCode || 'SPACER';
}
private getName_(country: chrome.autofillPrivate.CountryEntry): string {
return country.name || '------';
}
private isDivision_(country: chrome.autofillPrivate.CountryEntry): boolean {
return !country.countryCode;
}
private onCancelTap_() {
this.$.dialog.cancel();
}
/**
* Handler for tapping the save button.
*/
private onSaveButtonTap_() {
// The Enter key can call this function even if the button is disabled.
if (!this.canSave_) {
return;
}
// Set a default country if none is set.
if (!this.address.countryCode) {
this.address.countryCode = this.countries_[0].countryCode;
}
this.address.phoneNumbers = this.phoneNumber_ ? [this.phoneNumber_] : [];
this.address.emailAddresses = this.email_ ? [this.email_] : [];
this.fire_('save-address', this.address);
this.$.dialog.close();
}
/**
* Syncs the country code back to the address and rebuilds the address
* wrapper for the new location.
*/
private onUpdateCountryCode_(countryCode: string|undefined) {
this.address.countryCode = countryCode;
this.updateAddressWrapper_();
}
private onCountryChange_() {
const countrySelect = this.shadowRoot!.querySelector('select');
this.countryCode_ = countrySelect!.value;
}
/**
* Propagates focus to the <select> when country row is focused
* (e.g. using tab navigation).
*/
private onCountryRowFocus_() {
this.shadowRoot!.querySelector('select')!.focus();
}
/**
* Prevents clicking random spaces within country row but outside of <select>
* from triggering focus.
*/
private onCountryRowPointerDown_(e: Event) {
if ((e.composedPath()[0] as HTMLElement).tagName !== 'SELECT') {
e.preventDefault();
}
}
createHonorificAddressComponentUI(
address: chrome.autofillPrivate.AddressEntry): AddressComponentUI {
return new AddressComponentUI(address, {
field: chrome.autofillPrivate.AddressField.HONORIFIC,
fieldName: this.i18n('honorificLabel'),
isLongField: true,
placeholder: undefined,
});
}
}
declare global {
interface HTMLElementTagNameMap {
'settings-address-edit-dialog': SettingsAddressEditDialogElement;
}
}
customElements.define(
SettingsAddressEditDialogElement.is, SettingsAddressEditDialogElement);
/**
* Creates a wrapper against a single data member for an address.
*/
class AddressComponentUI {
private address_: chrome.autofillPrivate.AddressEntry;
component: chrome.autofillPrivate.AddressComponent;
isTextArea: boolean;
constructor(
address: chrome.autofillPrivate.AddressEntry,
component: chrome.autofillPrivate.AddressComponent) {
Object.defineProperty(this, 'value', {
get() {
return this.getValue_();
},
set(newValue) {
this.setValue_(newValue);
},
});
this.address_ = address;
this.component = component;
this.isTextArea =
component.field === chrome.autofillPrivate.AddressField.ADDRESS_LINES;
}
/**
* Gets the value from the address that's associated with this component.
*/
private getValue_(): string|undefined {
const address = this.address_;
switch (this.component.field) {
case chrome.autofillPrivate.AddressField.HONORIFIC:
return address.honorific;
case chrome.autofillPrivate.AddressField.FULL_NAME:
// |fullNames| is a single item array. See crbug.com/497934 for
// details.
return address.fullNames ? address.fullNames[0] : undefined;
case chrome.autofillPrivate.AddressField.COMPANY_NAME:
return address.companyName;
case chrome.autofillPrivate.AddressField.ADDRESS_LINES:
return address.addressLines;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_1:
return address.addressLevel1;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_2:
return address.addressLevel2;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_3:
return address.addressLevel3;
case chrome.autofillPrivate.AddressField.POSTAL_CODE:
return address.postalCode;
case chrome.autofillPrivate.AddressField.SORTING_CODE:
return address.sortingCode;
case chrome.autofillPrivate.AddressField.COUNTRY_CODE:
return address.countryCode;
default:
assertNotReached();
return '';
}
}
/**
* Sets the value in the address that's associated with this component.
*/
private setValue_(value: string) {
const address = this.address_;
switch (this.component.field) {
case chrome.autofillPrivate.AddressField.HONORIFIC:
address.honorific = value;
break;
case chrome.autofillPrivate.AddressField.FULL_NAME:
address.fullNames = [value];
break;
case chrome.autofillPrivate.AddressField.COMPANY_NAME:
address.companyName = value;
break;
case chrome.autofillPrivate.AddressField.ADDRESS_LINES:
address.addressLines = value;
break;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_1:
address.addressLevel1 = value;
break;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_2:
address.addressLevel2 = value;
break;
case chrome.autofillPrivate.AddressField.ADDRESS_LEVEL_3:
address.addressLevel3 = value;
break;
case chrome.autofillPrivate.AddressField.POSTAL_CODE:
address.postalCode = value;
break;
case chrome.autofillPrivate.AddressField.SORTING_CODE:
address.sortingCode = value;
break;
case chrome.autofillPrivate.AddressField.COUNTRY_CODE:
address.countryCode = value;
break;
default:
assertNotReached();
}
}
}
export interface CountryDetailManager {
/**
* Gets the list of available countries.
* The default country will be first, followed by a separator, followed by
* an alphabetized list of countries available.
*/
getCountryList(): Promise<Array<chrome.autofillPrivate.CountryEntry>>;
/**
* Gets the address format for a given country code.
*/
getAddressFormat(countryCode: string):
Promise<chrome.autofillPrivate.AddressComponents>;
}
/**
* Default implementation. Override for testing.
*/
export class CountryDetailManagerImpl implements CountryDetailManager {
getCountryList() {
return new Promise<Array<chrome.autofillPrivate.CountryEntry>>(function(
callback) {
chrome.autofillPrivate.getCountryList(callback);
});
}
getAddressFormat(countryCode: string) {
return new Promise<chrome.autofillPrivate.AddressComponents>(function(
callback) {
chrome.autofillPrivate.getAddressComponents(countryCode, callback);
});
}
static getInstance(): CountryDetailManager {
return instance || (instance = new CountryDetailManagerImpl());
}
static setInstance(obj: CountryDetailManager) {
instance = obj;
}
}
let instance: CountryDetailManager|null = null;