1import CepLookupController from "controllers/cep_lookup_controller" 2 3export default class extends CepLookupController { 4 static targets = ["cep", "logradouro", "bairro", "cidade", "uf", "tipo", "numero", "complemento"] 5 6 connect() { 7 this.lastSearchedCep = "" 8 this.loading = false 9 } 10 11 async search(event) { 12 if (event) event.preventDefault() 13 if (this.loading) return 14 15 // If triggered by blur and value hasn't changed or is empty, ignore 16 if (event?.type === 'blur') { 17 const currentCep = this.normalizedCep() 18 if (currentCep === this.lastSearchedCep || currentCep === "") return 19 } 20 21 const cep = this.normalizedCep() 22 23 if (cep.length !== 8) return 24 25 // UI Feedback 26 const btn = this.element.querySelector('button[data-action*="cep-search#search"]') 27 28 if (btn) { 29 btn.dataset.originalLabel ||= btn.innerHTML 30 btn.innerHTML = '<span class="ax-spinner" aria-hidden="true"></span>' 31 btn.disabled = true 32 btn.setAttribute("aria-busy", "true") 33 } 34 35 // Add loading class to input 36 this.loading = true 37 this.cepTarget.classList.add("ax-control--loading") 38 39 try { 40 const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`) 41 42 if (!response.ok) throw new Error('CEP não encontrado') 43 44 const data = await response.json() 45 if (data.erro) throw new Error('CEP não encontrado') 46 47 this.populateForm(data) 48 this.lastSearchedCep = cep 49 50 // Visual success feedback 51 this.cepTarget.classList.remove("is-invalid", "ax-control--invalid") 52 this.cepTarget.classList.add("ax-control--valid") 53 setTimeout(() => this.cepTarget.classList.remove("ax-control--valid"), 2000) 54 55 } catch (error) { 56 console.error(error) 57 this.cepTarget.classList.add("ax-control--invalid") 58 // Optional: Clean invalid class on input change 59 } finally { 60 if (btn) { 61 btn.innerHTML = btn.dataset.originalLabel || btn.innerHTML 62 btn.disabled = false 63 btn.removeAttribute("aria-busy") 64 } 65 this.loading = false 66 this.cepTarget.classList.remove("ax-control--loading") 67 } 68 } 69 70 populateForm(data) { 71 if (!data.logradouro) return 72 73 // 1. UF 74 if (this.hasUfTarget) { 75 this.setTomSelectValue(this.ufTarget, data.uf) 76 this.triggerChange(this.ufTarget) 77 } 78 79 // 2. Cidade (TomSelect) 80 if (this.hasCidadeTarget) { 81 this.setTomSelectValue(this.cidadeTarget, data.localidade) 82 } 83 84 // 3. Bairro (TomSelect) 85 if (this.hasBairroTarget) { 86 this.setTomSelectValue(this.bairroTarget, data.bairro) 87 } 88 89 // 4. Logradouro & Tipo 90 // Try to split Street Type from Name 91 let streetName = data.logradouro 92 93 if (this.hasTipoTarget) { 94 const matched = this.matchStreetType(data.logradouro) 95 if (matched) { 96 this.setTomSelectValue(this.tipoTarget, matched.value) 97 this.triggerChange(this.tipoTarget) 98 streetName = matched.streetName 99 } 100 } 101 102 if (this.hasLogradouroTarget) { 103 this.logradouroTarget.value = streetName 104 } 105 106 // 5. Focus Number 107 if (this.hasNumeroTarget) { 108 setTimeout(() => this.numeroTarget.focus(), 100) 109 } 110 } 111 112 normalizedCep() { 113 return this.cepTarget.value.replace(/\D/g, "") 114 } 115 116 matchStreetType(fullStreet) { 117 const raw = (fullStreet || "").trim() 118 if (!raw) return null 119 120 const normalizedRaw = this.normalize(raw) 121 const options = Array.from(this.tipoTarget.options) 122 .filter((opt) => opt.value && opt.text) 123 .map((opt) => ({ 124 value: opt.value, 125 text: opt.text, 126 normalized: this.normalize(opt.text) 127 })) 128 129 // Primeiro tenta bater com o nome completo da opção. 130 const direct = options.find((opt) => normalizedRaw.startsWith(`${opt.normalized} `)) 131 if (direct) { 132 return { 133 value: direct.value, 134 streetName: raw.substring(direct.text.length).trim() 135 } 136 } 137 138 // Depois tenta aliases comuns vindos de CEP (Av., Rod., Trav., etc). 139 const aliasMap = { 140 "av": "avenida", 141 "av.": "avenida", 142 "aven": "avenida", 143 "r": "rua", 144 "r.": "rua", 145 "rod": "rodovia", 146 "rod.": "rodovia", 147 "trav": "travessa", 148 "trav.": "travessa", 149 "al": "alameda", 150 "al.": "alameda", 151 "estr": "estrada", 152 "estr.": "estrada", 153 "vl": "viela", 154 "vl.": "viela" 155 } 156 157 const firstTokenRaw = raw.split(/\s+/)[0] || "" 158 const firstToken = this.normalize(firstTokenRaw) 159 const canonical = aliasMap[firstToken] || firstToken 160 const byAlias = options.find((opt) => opt.normalized === canonical) 161 162 if (!byAlias) return null 163 164 const rest = raw.replace(new RegExp(`^${this.escapeRegex(firstTokenRaw)}\\s*`, "i"), "").trim() 165 return { 166 value: byAlias.value, 167 streetName: rest 168 } 169 } 170 171 normalize(value) { 172 return value 173 .toString() 174 .normalize("NFD") 175 .replace(/[\u0300-\u036f]/g, "") 176 .replace(/[^\w\s./-]/g, "") 177 .trim() 178 .toLowerCase() 179 } 180 181 escapeRegex(value) { 182 return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") 183 } 184};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.