summaryrefslogtreecommitdiffstats
path: root/assets/js/cepis-woocommerce-classic.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js/cepis-woocommerce-classic.js')
-rw-r--r--assets/js/cepis-woocommerce-classic.js167
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;
+})();