diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/payments/res/containers/address-picker.js | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/browser/components/payments/res/containers/address-picker.js b/browser/components/payments/res/containers/address-picker.js new file mode 100644 index 0000000000..b76a0f5d02 --- /dev/null +++ b/browser/components/payments/res/containers/address-picker.js @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import AddressForm from "./address-form.js"; +import AddressOption from "../components/address-option.js"; +import RichPicker from "./rich-picker.js"; +import paymentRequest from "../paymentRequest.js"; +import HandleEventMixin from "../mixins/HandleEventMixin.js"; + +/** + * <address-picker></address-picker> + * Container around add/edit links and <rich-select> with + * <address-option> listening to savedAddresses & tempAddresses. + */ + +export default class AddressPicker extends HandleEventMixin(RichPicker) { + static get pickerAttributes() { + return ["address-fields", "break-after-nth-field", "data-field-separator"]; + } + + static get observedAttributes() { + return RichPicker.observedAttributes.concat(AddressPicker.pickerAttributes); + } + + constructor() { + super(); + this.dropdown.setAttribute("option-type", "address-option"); + } + + attributeChangedCallback(name, oldValue, newValue) { + super.attributeChangedCallback(name, oldValue, newValue); + // connectedCallback may add and adjust elements & values + // so avoid calling render before the element is connected + if ( + this.isConnected && + AddressPicker.pickerAttributes.includes(name) && + oldValue !== newValue + ) { + this.render(this.requestStore.getState()); + } + } + + get fieldNames() { + if (this.hasAttribute("address-fields")) { + let names = this.getAttribute("address-fields") + .trim() + .split(/\s+/); + if (names.length) { + return names; + } + } + + return [ + // "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE + "address-level2", + "country", + "name", + "postal-code", + "street-address", + ]; + } + + /** + * De-dupe and filter addresses for the given set of fields that will be visible + * + * @param {object} addresses + * @param {array?} fieldNames - optional list of field names that be used when + * de-duping and excluding entries + * @returns {object} filtered copy of given addresses + */ + filterAddresses(addresses, fieldNames = this.fieldNames) { + let uniques = new Set(); + let result = {}; + for (let [guid, address] of Object.entries(addresses)) { + let addressCopy = {}; + let isMatch = false; + // exclude addresses that are missing all of the requested fields + for (let name of fieldNames) { + if (address[name]) { + isMatch = true; + addressCopy[name] = address[name]; + } + } + if (isMatch) { + let key = JSON.stringify(addressCopy); + // exclude duplicated addresses + if (!uniques.has(key)) { + uniques.add(key); + result[guid] = address; + } + } + } + return result; + } + + get options() { + return this.dropdown.popupBox.options; + } + + /** + * @param {object} state - See `PaymentsStore.setState` + * The value of the picker is retrieved from state store rather than the DOM + * @returns {string} guid + */ + getCurrentValue(state) { + let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|"); + let guid = state[selectedKey]; + if (selectedLeaf) { + guid = guid[selectedLeaf]; + } + return guid; + } + + render(state) { + let selectedAddressGUID = this.getCurrentValue(state) || ""; + let addresses = paymentRequest.getAddresses(state); + let desiredOptions = []; + let filteredAddresses = this.filterAddresses(addresses, this.fieldNames); + for (let [guid, address] of Object.entries(filteredAddresses)) { + let optionEl = this.dropdown.getOptionByValue(guid); + if (!optionEl) { + optionEl = document.createElement("option"); + optionEl.value = guid; + } + + for (let key of AddressOption.recordAttributes) { + let val = address[key]; + if (val) { + optionEl.setAttribute(key, val); + } else { + optionEl.removeAttribute(key); + } + } + + optionEl.dataset.fieldSeparator = this.dataset.fieldSeparator; + + if (this.hasAttribute("address-fields")) { + optionEl.setAttribute( + "address-fields", + this.getAttribute("address-fields") + ); + } else { + optionEl.removeAttribute("address-fields"); + } + + if (this.hasAttribute("break-after-nth-field")) { + optionEl.setAttribute( + "break-after-nth-field", + this.getAttribute("break-after-nth-field") + ); + } else { + optionEl.removeAttribute("break-after-nth-field"); + } + + // fieldNames getter is not used here because it returns a default array with + // attributes even when "address-fields" observed attribute is null. + let addressFields = this.getAttribute("address-fields"); + optionEl.textContent = AddressOption.formatSingleLineLabel( + address, + addressFields + ); + desiredOptions.push(optionEl); + } + + this.dropdown.popupBox.textContent = ""; + + if (this._allowEmptyOption) { + let optionEl = document.createElement("option"); + optionEl.value = ""; + desiredOptions.unshift(optionEl); + } + + for (let option of desiredOptions) { + this.dropdown.popupBox.appendChild(option); + } + + // Update selectedness after the options are updated + this.dropdown.value = selectedAddressGUID; + + if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) { + throw new Error( + `${this.selectedStateKey} option ${selectedAddressGUID} ` + + `does not exist in the address picker` + ); + } + + super.render(state); + } + + get selectedStateKey() { + return this.getAttribute("selected-state-key"); + } + + errorForSelectedOption(state) { + let superError = super.errorForSelectedOption(state); + if (superError) { + return superError; + } + + if (!this.selectedOption) { + return ""; + } + + let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm( + state, + this.selectedStateKey.split("|") + ); + // TODO: errors in priority order. + return ( + Object.values(merchantFieldErrors).find(msg => { + return typeof msg == "string" && msg.length; + }) || "" + ); + } + + onChange(event) { + let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|"); + if (!selectedKey) { + return; + } + // selectedStateKey can be a '|' delimited string indicating a path into the state object + // to update with the new value + let newState = {}; + + if (selectedLeaf) { + let currentState = this.requestStore.getState(); + newState[selectedKey] = Object.assign({}, currentState[selectedKey], { + [selectedLeaf]: this.dropdown.value, + }); + } else { + newState[selectedKey] = this.dropdown.value; + } + this.requestStore.setState(newState); + } + + onClick({ target }) { + let pageId; + let currentState = this.requestStore.getState(); + let nextState = { + page: {}, + }; + + switch (this.selectedStateKey) { + case "selectedShippingAddress": + pageId = "shipping-address-page"; + break; + case "selectedPayerAddress": + pageId = "payer-address-page"; + break; + case "basic-card-page|billingAddressGUID": + pageId = "billing-address-page"; + break; + default: { + throw new Error( + "onClick, un-matched selectedStateKey: " + this.selectedStateKey + ); + } + } + nextState.page.id = pageId; + let addressFields = this.getAttribute("address-fields"); + nextState[pageId] = { addressFields }; + + switch (target) { + case this.addLink: { + nextState[pageId].guid = null; + break; + } + case this.editLink: { + nextState[pageId].guid = this.getCurrentValue(currentState); + break; + } + default: { + throw new Error("Unexpected onClick"); + } + } + + this.requestStore.setState(nextState); + } +} + +customElements.define("address-picker", AddressPicker); |