| // 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 {assertNotReached} from 'chrome://resources/js/assert.m.js'; |
| import {addSingletonGetter} from 'chrome://resources/js/cr.m.js'; |
| import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/js/i18n_behavior.m.js'; |
| import {flush, html, microTask, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import {loadTimeData} from '../i18n_setup.js'; |
| |
| |
| /** |
| * @constructor |
| * @extends {PolymerElement} |
| * @implements {I18nBehaviorInterface} |
| */ |
| const SettingsAddressEditDialogElementBase = |
| mixinBehaviors([I18nBehavior], PolymerElement); |
| |
| /** @polymer */ |
| class SettingsAddressEditDialogElement extends |
| SettingsAddressEditDialogElementBase { |
| static get is() { |
| return 'settings-address-edit-dialog'; |
| } |
| |
| static get template() { |
| return html`{__html_template__}`; |
| } |
| |
| static get properties() { |
| return { |
| /** @type {chrome.autofillPrivate.AddressEntry} */ |
| address: Object, |
| |
| /** @private */ |
| title_: String, |
| |
| /** @private {!Array<!chrome.autofillPrivate.CountryEntry>} */ |
| countries_: Array, |
| |
| /** |
| * Updates the address wrapper. |
| * @private {string|undefined} |
| */ |
| countryCode_: { |
| type: String, |
| observer: 'onUpdateCountryCode_', |
| }, |
| |
| /** @private {!Array<!Array<!AddressComponentUI>>} */ |
| addressWrapper_: Object, |
| |
| /** @private */ |
| phoneNumber_: String, |
| |
| /** @private */ |
| email_: String, |
| |
| /** @private */ |
| canSave_: Boolean, |
| |
| /** |
| * True if honorifics are enabled. |
| * @private |
| */ |
| showHonorific_: { |
| type: Boolean, |
| value() { |
| return loadTimeData.getBoolean('showHonorific'); |
| } |
| } |
| }; |
| } |
| |
| |
| constructor() { |
| super(); |
| |
| /** @type {!CountryDetailManager} */ |
| this.countryInfo = CountryDetailManagerImpl.getInstance(); |
| } |
| |
| /** @override */ |
| 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. |
| } |
| |
| /** |
| * @param {string} eventName |
| * @param {*=} detail |
| * @private |
| */ |
| fire_(eventName, detail) { |
| this.dispatchEvent( |
| new CustomEvent(eventName, {bubbles: true, composed: true, detail})); |
| } |
| |
| /** |
| * Returns a class to denote how long this entry is. |
| * @param {AddressComponentUI} setting |
| * @return {string} |
| */ |
| long_(setting) { |
| 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(/** @type {string} */ (countryCode)) |
| .then(format => { |
| 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. |
| |
| const dialog = /** @type {HTMLDialogElement} */ (this.$.dialog); |
| if (!dialog.open) { |
| dialog.showModal(); |
| } |
| }); |
| } |
| |
| updateCanSave_() { |
| const inputs = this.$.dialog.querySelectorAll('.address-column, select'); |
| |
| 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. |
| } |
| |
| /** |
| * @param {!chrome.autofillPrivate.CountryEntry} country |
| * @return {string} |
| * @private |
| */ |
| getCode_(country) { |
| return country.countryCode || 'SPACER'; |
| } |
| |
| /** |
| * @param {!chrome.autofillPrivate.CountryEntry} country |
| * @return {string} |
| * @private |
| */ |
| getName_(country) { |
| return country.name || '------'; |
| } |
| |
| /** |
| * @param {!chrome.autofillPrivate.CountryEntry} country |
| * @return {boolean} |
| * @private |
| */ |
| isDivision_(country) { |
| 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. |
| * @param {string|undefined} countryCode |
| * @private |
| */ |
| onUpdateCountryCode_(countryCode) { |
| this.address.countryCode = countryCode; |
| this.updateAddressWrapper_(); |
| } |
| |
| /** @private */ |
| onCountryChange_() { |
| const countrySelect = |
| /** @type {!HTMLSelectElement} */ ( |
| 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_() { |
| const countrySelect = |
| /** @type {!HTMLSelectElement} */ ( |
| this.shadowRoot.querySelector('select')); |
| countrySelect.focus(); |
| } |
| |
| /** |
| * Prevents clicking random spaces within country row but outside of <select> |
| * from triggering focus. |
| * @param {!Event} e |
| * @private |
| */ |
| onCountryRowPointerDown_(e) { |
| if (e.path[0].tagName !== 'SELECT') { |
| e.preventDefault(); |
| } |
| } |
| |
| /** |
| * @param {!chrome.autofillPrivate.AddressEntry} address |
| * @returns {AddressComponentUI} |
| */ |
| createHonorificAddressComponentUI(address) { |
| return new AddressComponentUI(address, { |
| field: chrome.autofillPrivate.AddressField.HONORIFIC, |
| fieldName: this.i18n('honorificLabel'), |
| isLongField: true, |
| placerholder: undefined, |
| }); |
| } |
| } |
| |
| customElements.define( |
| SettingsAddressEditDialogElement.is, SettingsAddressEditDialogElement); |
| |
| /** |
| * Creates a wrapper against a single data member for an address. |
| */ |
| class AddressComponentUI { |
| /** |
| * @param {!chrome.autofillPrivate.AddressEntry} address |
| * @param {!chrome.autofillPrivate.AddressComponent} component |
| */ |
| constructor(address, component) { |
| 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. |
| * @return {string|undefined} |
| * @private |
| */ |
| getValue_() { |
| 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(); |
| } |
| } |
| |
| /** |
| * Sets the value in the address that's associated with this component. |
| * @param {string} value |
| * @private |
| */ |
| setValue_(value) { |
| 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(); |
| } |
| } |
| } |
| |
| /** @interface */ |
| class 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. |
| * @return {!Promise<!Array<!chrome.autofillPrivate.CountryEntry>>} |
| */ |
| getCountryList() {} |
| |
| /** |
| * Gets the address format for a given country code. |
| * @param {string} countryCode |
| * @return {!Promise<!chrome.autofillPrivate.AddressComponents>} |
| */ |
| getAddressFormat(countryCode) {} |
| } |
| |
| /** |
| * Default implementation. Override for testing. |
| * @implements {CountryDetailManager} |
| */ |
| export class CountryDetailManagerImpl { |
| /** @override */ |
| getCountryList() { |
| return new Promise(function(callback) { |
| chrome.autofillPrivate.getCountryList(callback); |
| }); |
| } |
| |
| /** @override */ |
| getAddressFormat(countryCode) { |
| return new Promise(function(callback) { |
| chrome.autofillPrivate.getAddressComponents(countryCode, callback); |
| }); |
| } |
| } |
| |
| addSingletonGetter(CountryDetailManagerImpl); |