diff options
Diffstat (limited to 'assets/js/cepis-woocommerce-classic.js')
| -rw-r--r-- | assets/js/cepis-woocommerce-classic.js | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/assets/js/cepis-woocommerce-classic.js b/assets/js/cepis-woocommerce-classic.js new file mode 100644 index 0000000..69d8068 --- /dev/null +++ b/assets/js/cepis-woocommerce-classic.js @@ -0,0 +1,167 @@ +/** + * Classic (shortcode) WooCommerce checkout. + * + * The form is rendered by PHP and nothing owns the fields afterwards, so the + * widget is left to do the whole job: debounce, request, write, announce. Most + * of this file is handing it the selectors WooCommerce uses and the settings + * the store owner chose. + * + * Selectors are passed as strings rather than elements on purpose. The widget + * resolves them on every use and delegates its listener from `document`, so a + * theme or one-page-checkout plugin that re-renders the billing form does not + * break the autofill. + * + * @module + */ + +(function () { + var config = window.cepisWooCommerce; + + if (!config || !window.CepisAutofill || !config.fields) { + return; + } + + var DIACRITIC = /[\u0300-\u036f]/g; + + /** + * Compare labels the way a person would: accents and case do not count. + * + * @param {unknown} value Anything printable. + * @returns {string} + */ + function fold(value) { + return String(value == null ? "" : value) + .normalize("NFD") + .replace(DIACRITIC, "") + .trim() + .toLowerCase(); + } + + /** + * Set a value the way a person would, so validation in the page sees it. + * + * @param {HTMLInputElement|HTMLSelectElement} element Field to write. + * @param {string} value Value to put in it. + */ + function write(element, value) { + var descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "value"); + + if (descriptor && descriptor.set) { + descriptor.set.call(element, value); + } else { + element.value = value; + } + + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + } + + /** + * Fill the state field. + * + * This one field is kept away from the widget, because it needs the opposite + * of the widget's `overwrite` rule. A street name is worth protecting — the + * shopper may know better than the database. A state is not: the CEP decides + * it. And WooCommerce pre-selects the store's own state on a fresh checkout, + * which `overwrite: "auto"` would read as a choice the shopper made and + * never touch, quietly filing a Rio address under São Paulo. + * + * Nothing is cleared when no option matches, and this runs only after a + * successful lookup, so a CEP that does not exist leaves the field alone. + * + * Focus is put back where it was. WooCommerce runs the state field through + * selectWoo, which takes focus for itself when the value changes, and that + * would otherwise strand the cursor on the state box instead of the house + * number the shopper still has to type. + * + * @param {string} selector CSS selector for the state field. + * @param {string} uf Two-letter state code from the API. + * @param {string} estado Full state name from the API. + */ + function fillState(selector, uf, estado) { + var element = selector ? document.querySelector(selector) : null; + + if (!element || !uf) { + return; + } + + var value = uf; + + if ("SELECT" === element.tagName) { + var wanted = [uf, estado].filter(Boolean).map(fold); + var options = Array.prototype.slice.call(element.options); + var match = null; + + for (var i = 0; i < options.length; i += 1) { + if ( + wanted.indexOf(fold(options[i].value)) !== -1 || + wanted.indexOf(fold(options[i].textContent)) !== -1 + ) { + match = options[i].value; + break; + } + } + + if (null === match) { + return; + } + + value = match; + } + + if (element.value === value) { + return; + } + + var active = document.activeElement; + + write(element, value); + + if (active && active !== document.activeElement && "function" === typeof active.focus) { + active.focus(); + } + } + + /** + * Attach one address section — billing or shipping. + * + * @param {Record<string, string>} map Selectors, keyed by the API's field names. + * @returns {object|null} The widget handle, or null when there is no CEP field. + */ + function section(map) { + if (!map || !map.cep) { + return null; + } + + var options = { + baseUrl: config.baseUrl, + overwrite: config.overwrite, + focusNumero: !!config.focusNumero, + debounceMs: config.debounceMs, + timeoutMs: config.timeoutMs, + messages: config.messages, + onResult: function (address) { + fillState(map.uf, address.uf, address.estado); + }, + }; + + Object.keys(map).forEach(function (field) { + if ("uf" !== field) { + options[field] = map[field]; + } + }); + + return window.CepisAutofill.attach(options); + } + + var handles = []; + + Object.keys(config.fields).forEach(function (name) { + var handle = section(config.fields[name]); + if (handle) { + handles.push(handle); + } + }); + + window.cepisWooCommerceHandles = handles; +})(); |