summaryrefslogtreecommitdiffstats
path: root/assets/js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js')
-rw-r--r--assets/js/cepis-autofill.min.js1
-rw-r--r--assets/js/cepis-woocommerce-blocks.js243
-rw-r--r--assets/js/cepis-woocommerce-classic.js167
3 files changed, 411 insertions, 0 deletions
diff --git a/assets/js/cepis-autofill.min.js b/assets/js/cepis-autofill.min.js
new file mode 100644
index 0000000..1cb513e
--- /dev/null
+++ b/assets/js/cepis-autofill.min.js
@@ -0,0 +1 @@
+(()=>{var{defineProperty:D,getOwnPropertyNames:E,getOwnPropertyDescriptor:h}=Object,f=Object.prototype.hasOwnProperty;function m(V){return this[V]}var g=(V)=>{var K=(M??=new WeakMap).get(V),J;if(K)return K;if(K=D({},"__esModule",{value:!0}),V&&typeof V==="object"||typeof V==="function"){for(var B of E(V))if(!f.call(K,B))D(K,B,{get:m.bind(V,B),enumerable:!(J=h(V,B))||J.enumerable})}return M.set(V,K),K},M;var c=(V)=>V;function u(V,K){this[V]=c.bind(null,K)}var p=(V,K)=>{for(var J in K)D(V,J,{get:K[J],enumerable:!0,configurable:!0,set:u.bind(K,J)})};var s={};p(s,{default:()=>o});var y="0.1.0";var k=/\D+/g,d=/[\u0300-\u036f]/g,l=["cep","logradouro","complemento","unidade","bairro","localidade","uf","estado","regiao","ibge","gia","ddd","siafi"],Q={idle:"",loading:"Buscando endereço…",found:"Endereço encontrado.",not_found:"CEP não encontrado. Confira o número ou preencha o endereço manualmente.",invalid:"CEP inválido. Digite os 8 dígitos.",rate_limited:"Muitas consultas em pouco tempo. Aguarde alguns segundos e tente de novo.",server:"O serviço de CEP está indisponível. Preencha o endereço manualmente.",network:"Não foi possível consultar o CEP. Verifique sua conexão e tente de novo.",timeout:"A consulta do CEP demorou demais. Tente de novo ou preencha manualmente.",invalid_response:"Resposta inesperada do serviço de CEP. Preencha o endereço manualmente."};class L extends Error{constructor(V,K,J={}){super(K,J.cause===void 0?void 0:{cause:J.cause});this.name="CepisAutofillError",this.code=V,this.status=J.status,this.retryAfterMs=J.retryAfterMs,this.cep=J.cep}}function U(V){let K=String(V==null?"":V).replace(k,"");if(K.length!==8)throw new L("invalid",Q.invalid,{cep:K});return K}var R=new Map;function b(){R.clear()}function a(V){if(!V)return;let K=V.trim();if(/^\d+$/.test(K))return Number(K)*1000;let J=Date.parse(K);return Number.isNaN(J)?void 0:Math.max(0,J-Date.now())}function n(V,K){try{let J=JSON.parse(V);if(J&&typeof J.mensagem==="string"&&J.mensagem!=="")return J.mensagem}catch{return K}return K}async function G(V,K={}){let J=U(V),B=(K.baseUrl??"https://cep.is").replace(/\/+$/,""),N=`${B}|${J}`,q=R.get(N);if(q){if(q.address)return q.address;throw new L(q.code,q.message,{cep:J})}let F=new AbortController,Y=()=>F.abort();if(K.signal){if(K.signal.aborted)throw new L("aborted","consulta cancelada",{cep:J});K.signal.addEventListener("abort",Y)}let j=!1,_=setTimeout(()=>{j=!0,F.abort()},K.timeoutMs??8000),H,O;try{H=await fetch(`${B}/${J}`,{method:"GET",credentials:"omit",headers:{Accept:"application/json"},signal:F.signal}),O=await H.text()}catch(W){if(j)throw new L("timeout",Q.timeout,{cep:J,cause:W});if(F.signal.aborted)throw new L("aborted","consulta cancelada",{cep:J});throw new L("network",Q.network,{cep:J,cause:W})}finally{if(clearTimeout(_),K.signal)K.signal.removeEventListener("abort",Y)}if(H.status===404)throw R.set(N,{code:"not_found",message:Q.not_found}),new L("not_found",Q.not_found,{cep:J});if(H.status===400)throw new L("invalid",n(O,Q.invalid),{cep:J});if(H.status===429)throw new L("rate_limited",Q.rate_limited,{cep:J,retryAfterMs:a(H.headers.get("Retry-After"))});if(H.status>=500)throw new L("server",Q.server,{cep:J,status:H.status});if(!H.ok)throw new L("invalid_response",Q.invalid_response,{cep:J,status:H.status});let z;try{z=JSON.parse(O)}catch(W){throw new L("invalid_response",Q.invalid_response,{cep:J,cause:W})}if(!z||typeof z!=="object"||Array.isArray(z))throw new L("invalid_response",Q.invalid_response,{cep:J});return R.set(N,{address:z}),z}function T(V){if(!V)return null;if(typeof V==="string")return document.querySelector(V);return V.nodeType===1?V:null}function I(V){return String(V??"").normalize("NFD").replace(d,"").trim().toLowerCase()}function C(V,K){let J=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(V),"value");if(J&&J.set)J.set.call(V,K);else V.value=K;V.dispatchEvent(new Event("input",{bubbles:!0})),V.dispatchEvent(new Event("change",{bubbles:!0}))}function r(V,K){let J=K.filter(Boolean).map(I);for(let B of Array.from(V.options))if(J.includes(I(B.value))||J.includes(I(B.textContent)))return C(V,B.value),B.value;return null}function i(V,K,J){let B=String(V.value??"");if(K===!0)return!0;if(B.trim()==="")return!0;if(K===!1)return!1;return J.get(V)===B}var w=0;function A(V){if(!V||!V.cep)throw TypeError("attach() needs a `cep` selector or element");let K=(V.baseUrl??"https://cep.is").replace(/\/+$/,""),J=V.debounceMs??250,B=V.timeoutMs??8000,N=V.overwrite??"auto",q={...Q,...V.messages??{}},F=new WeakMap,Y={seq:0,timer:0,controller:null,region:null,describedBy:!1,detached:!1},j=()=>T(V.cep);function _(){if(V.status===!1)return null;if(Y.region&&Y.region.isConnected)return Y.region;if(V.status)Y.region=T(V.status);else{let $=j();if(!$)return null;let x=document.createElement("div");x.className="cepis-autofill-status",x.style.minHeight="1.25em",$.insertAdjacentElement("afterend",x),Y.region=x}let X=Y.region;if(!X)return null;if(X.setAttribute("role","status"),X.setAttribute("aria-live","polite"),X.setAttribute("aria-atomic","true"),!X.id)w+=1,X.id=`cepis-autofill-status-${w}`;let Z=j();if(Z&&!(Z.getAttribute("aria-describedby")??"").includes(X.id)){let $=Z.getAttribute("aria-describedby");Z.setAttribute("aria-describedby",$?`${$} ${X.id}`:X.id),Y.describedBy=!0}return X}function H(X){let Z=q[X]??"",$=_();if($)$.setAttribute("data-cepis-state",X==="found"||X==="loading"?X:X==="idle"?"idle":"error"),$.textContent=Z;let x=j();if(x){if(X==="loading")x.setAttribute("aria-busy","true");else x.removeAttribute("aria-busy");x.setAttribute("aria-invalid",X==="invalid"||X==="not_found"?"true":"false")}if(V.onStatus)V.onStatus(X,Z)}function O(X){for(let Z of l){if(Z==="cep"||!V[Z])continue;let $=T(V[Z]),x=X[Z];if(!$||typeof x!=="string"||x==="")continue;if(!i($,N,F))continue;if($.tagName==="SELECT"){if(r($,Z==="uf"?[x,X.estado]:Z==="estado"?[x,X.uf]:[x])!==null)F.set($,$.value);continue}C($,x),F.set($,x)}}async function z(X){Y.seq+=1;let Z=Y.seq;if(Y.controller)Y.controller.abort();let $=new AbortController;Y.controller=$,H("loading");try{let x=await G(X,{baseUrl:K,timeoutMs:B,signal:$.signal});if(Z!==Y.seq||Y.detached)return null;if(O(x),H("found"),V.onResult)V.onResult(x,{cep:X});let P=T(V.numero);if(V.focusNumero&&P&&document.activeElement===j())P.focus();return x}catch(x){if(Z!==Y.seq||Y.detached||x.code==="aborted")return null;let P=x instanceof L?x.code:"network";if(H(P),V.onError)V.onError(x,{cep:X});return null}finally{if(Y.controller===$)Y.controller=null}}function W(X){let Z=X.target;if(!(typeof V.cep==="string"?Z&&Z.nodeType===1&&Z.matches(V.cep):Z===V.cep))return;v(String(Z.value??""))}function v(X){clearTimeout(Y.timer);let Z=X.replace(k,"");if(Z.length!==8){if(Y.seq+=1,Y.controller)Y.controller.abort(),Y.controller=null;H(Z.length>8?"invalid":"idle");return}Y.timer=setTimeout(()=>z(Z),J)}return H("idle"),document.addEventListener("input",W,!0),document.addEventListener("change",W,!0),{detach(){if(Y.detached=!0,Y.seq+=1,clearTimeout(Y.timer),Y.controller)Y.controller.abort(),Y.controller=null;document.removeEventListener("input",W,!0),document.removeEventListener("change",W,!0);let X=j();if(X)X.removeAttribute("aria-busy"),X.removeAttribute("aria-invalid");if(Y.region&&Y.describedBy&&X){let Z=(X.getAttribute("aria-describedby")??"").split(/\s+/).filter(($)=>$&&$!==Y.region.id).join(" ");if(Z)X.setAttribute("aria-describedby",Z);else X.removeAttribute("aria-describedby")}if(Y.region&&!V.status)Y.region.remove();Y.region=null},lookup(X){clearTimeout(Y.timer);let Z=j(),$=X??(Z?Z.value:""),x;try{x=U($)}catch(P){if(H("invalid"),V.onError)V.onError(P,{cep:String($)});return Promise.resolve(null)}return z(x)},element:j}}var S={attach:A,lookup:G,normalizeCep:U,clearCache:b,CepisAutofillError:L,MESSAGES:Q,VERSION:y};if(typeof window<"u")window.CepisAutofill=S;var o=S;})();
diff --git a/assets/js/cepis-woocommerce-blocks.js b/assets/js/cepis-woocommerce-blocks.js
new file mode 100644
index 0000000..bcbab93
--- /dev/null
+++ b/assets/js/cepis-woocommerce-blocks.js
@@ -0,0 +1,243 @@
+/**
+ * WooCommerce Cart/Checkout Blocks.
+ *
+ * The block checkout is React and its address inputs are controlled: a value
+ * written into the DOM from outside is discarded on the next render, and the
+ * `wc/store/cart` data store — which is what the order is actually built from —
+ * never hears about it. So nothing here touches a field. The postcode is read
+ * from the data store, and the resolved address is written back through the
+ * store's own `setShippingAddress` / `setBillingAddress` actions, which is the
+ * documented extension point. React then re-renders the inputs itself.
+ *
+ * Only the request layer is shared with the classic integration:
+ * `CepisAutofill.lookup` is the widget's HTTP client without its DOM half —
+ * same cache, same error codes, same handling of 400, 404, 429 and 5xx.
+ *
+ * @module
+ */
+
+(function () {
+ var config = window.cepisWooCommerce;
+ var data = window.wp && window.wp.data;
+
+ if (!config || !config.fields || !window.CepisAutofill || !data || !data.select) {
+ return;
+ }
+
+ var CART = "wc/store/cart";
+ var NOTICES = "core/notices";
+
+ /**
+ * Where a notice has to be rendered for the Checkout block to show it.
+ */
+ var NOTICE_CONTEXT = "wc/checkout";
+
+ /**
+ * One id, so a new failure replaces the previous message instead of stacking.
+ */
+ var NOTICE_ID = "cepis-woocommerce-lookup";
+
+ var SECTIONS = {
+ shipping: { address: "shippingAddress", write: "setShippingAddress" },
+ billing: { address: "billingAddress", write: "setBillingAddress" },
+ };
+
+ var progress = {
+ shipping: { cep: "", timer: 0, written: {} },
+ billing: { cep: "", timer: 0, written: {} },
+ };
+
+ /**
+ * The one field the shopper is never asked about.
+ *
+ * A street name is worth protecting — the shopper may know better than the
+ * database. A state is not: the CEP decides it. WooCommerce also starts the
+ * checkout with the store's own state already selected, which the `"auto"`
+ * rule would read as the shopper's choice and never touch, quietly filing a
+ * Rio address under São Paulo.
+ */
+ var ALWAYS = "uf";
+
+ /**
+ * Whether a field may be written.
+ *
+ * Mirrors the widget's `overwrite` option: `"auto"` protects anything the
+ * shopper typed but replaces what a previous lookup wrote, so correcting the
+ * street by hand survives the next CEP.
+ *
+ * @param {string} field The API's name for the field.
+ * @param {string} current Value the store holds now.
+ * @param {string|undefined} written Value this plugin last wrote there.
+ * @returns {boolean}
+ */
+ function writable(field, current, written) {
+ if (field === ALWAYS || config.overwrite === true) {
+ return true;
+ }
+ if (current.trim() === "") {
+ return true;
+ }
+ if (config.overwrite === false) {
+ return false;
+ }
+ return written === current;
+ }
+
+ var pending = {};
+
+ /**
+ * Look a CEP up once, however many sections want it.
+ *
+ * With "use the same address for billing" on — the default — the shipping
+ * and the billing observer see the same postcode land in the same tick. The
+ * widget's own cache is only filled once a response is back, so without this
+ * the store would send two identical requests for every CEP a shopper types.
+ *
+ * @param {string} digits The eight digits.
+ * @returns {Promise<Record<string, any>>}
+ */
+ function resolve(digits) {
+ if (!pending[digits]) {
+ var forget = function () {
+ delete pending[digits];
+ };
+
+ pending[digits] = window.CepisAutofill.lookup(digits, {
+ baseUrl: config.baseUrl,
+ timeoutMs: config.timeoutMs,
+ });
+ pending[digits].then(forget, forget);
+ }
+
+ return pending[digits];
+ }
+
+ /**
+ * Show one message in the Checkout block's own notice area.
+ *
+ * @param {string} text Message, already translated.
+ */
+ function announce(text) {
+ data.dispatch(NOTICES).createNotice("error", text, {
+ id: NOTICE_ID,
+ context: NOTICE_CONTEXT,
+ isDismissible: true,
+ });
+ }
+
+ /** Take the message away. */
+ function silence() {
+ data.dispatch(NOTICES).removeNotice(NOTICE_ID, NOTICE_CONTEXT);
+ }
+
+ /**
+ * Write a resolved address into the store.
+ *
+ * @param {string} name Section name, `shipping` or `billing`.
+ * @param {Record<string, any>} address The API's answer.
+ */
+ function apply(name, address) {
+ var customer = data.select(CART).getCustomerData();
+ var current = (customer && customer[SECTIONS[name].address]) || {};
+ var written = progress[name].written;
+ var patch = {};
+ var changed = false;
+
+ Object.keys(config.fields).forEach(function (field) {
+ var key = config.fields[field];
+ var value = address[field];
+
+ if (!key || typeof value !== "string" || value === "") {
+ return;
+ }
+
+ var held = current[key] == null ? "" : String(current[key]);
+ if (!writable(field, held, written[key])) {
+ return;
+ }
+
+ patch[key] = value;
+ written[key] = value;
+ changed = true;
+ });
+
+ if (changed) {
+ data.dispatch(CART)[SECTIONS[name].write](patch);
+ }
+ }
+
+ /**
+ * Resolve one CEP and write what comes back.
+ *
+ * @param {string} name Section name, `shipping` or `billing`.
+ * @param {string} digits The eight digits.
+ */
+ function run(name, digits) {
+ silence();
+
+ resolve(digits).then(
+ function (address) {
+ if (progress[name].cep === digits) {
+ apply(name, address);
+ }
+ },
+ function (error) {
+ if (progress[name].cep !== digits || error.code === "aborted") {
+ return;
+ }
+ announce(config.messages[error.code] || error.message);
+ },
+ );
+ }
+
+ /**
+ * React to whatever the store now holds for one address.
+ *
+ * @param {string} name Section name, `shipping` or `billing`.
+ * @param {Record<string, any>|undefined} address Address from the store.
+ */
+ function observe(name, address) {
+ var seen = progress[name];
+
+ if (!address || String(address.country || "") !== "BR") {
+ return;
+ }
+
+ var digits = String(address.postcode || "").replace(/\D+/g, "");
+
+ if (digits.length !== 8) {
+ if (seen.cep !== "") {
+ seen.cep = "";
+ window.clearTimeout(seen.timer);
+ silence();
+ }
+ return;
+ }
+
+ if (digits === seen.cep) {
+ return;
+ }
+
+ seen.cep = digits;
+ window.clearTimeout(seen.timer);
+ seen.timer = window.setTimeout(function () {
+ run(name, digits);
+ }, config.debounceMs);
+ }
+
+ data.subscribe(function () {
+ var store = data.select(CART);
+ if (!store || typeof store.getCustomerData !== "function") {
+ return;
+ }
+
+ var customer = store.getCustomerData();
+ if (!customer) {
+ return;
+ }
+
+ Object.keys(SECTIONS).forEach(function (name) {
+ observe(name, customer[SECTIONS[name].address]);
+ });
+ }, CART);
+})();
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;
+})();