/** * 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; /** Class the widget gives the region it announces through. */ var STATUS = "cepis-autofill-status"; /** * 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(); } } /** * The `aria-live` region the widget announces through. * * The widget inserts it immediately after the CEP field and points the * field's `aria-describedby` at it, but only once, when it attaches. * WooCommerce re-renders the address fields afterwards — that is what * `country_to_state_changed` does — and the attribute does not survive it * while the region itself does. So the sibling is what is actually reliable * here, and the surrounding form row is the fallback for a theme that wraps * the input in something of its own. * * Never widened to a document-wide search: billing and shipping each have a * region, and picking the wrong one would tell a shopper about the address * they are not editing. * * @param {string} selector CSS selector for the CEP field. * @returns {HTMLElement|null} */ function statusRegion(selector) { var field = document.querySelector(selector); if (!field) { return null; } var sibling = field.nextElementSibling; if (sibling && sibling.classList && sibling.classList.contains(STATUS)) { return sibling; } var node = field.parentElement; for (var depth = 0; node && depth < 2; depth += 1) { var region = node.querySelector("." + STATUS); if (region) { return region; } node = node.parentElement; } return null; } /** * Say which municipality a CEP that does not exist would have been in. * * Rewrites the line the widget just wrote, and nothing else — no field is * touched here, on this path, ever. The CEP was not found; there is no * address to fill anything with, and inventing one from the municipality * would send the parcel to the wrong town. * * Both guards before the write matter, because the answer arrives one round * trip late: the shopper may have typed on, and the widget may have replaced * the line with a "found" of its own in the meantime. * * @param {string} selector CSS selector for the CEP field. * @param {string} cep The CEP that was not found. */ function explain(selector, cep) { var helper = window.cepisWooCommerceFaixa; var region = statusRegion(selector); if (!helper || !region) { return; } var shown = region.textContent; helper.message(cep, shown, function (message) { var current = statusRegion(selector); var field = document.querySelector(selector); var typed = field ? String(field.value == null ? "" : field.value).replace(/\D+/g, "") : ""; if (message === shown || !current || current.textContent !== shown || typed !== cep) { return; } current.textContent = message; }); } /** * Attach one address section — billing or shipping. * * @param {Record} 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); }, onError: function (error, context) { if (error && "not_found" === error.code) { explain(map.cep, context.cep); } }, }; 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; })();