PageSourceSearch

https://saluteimoveis.com.br/assets/controllers/cep_search_control…d01b74d7bbba2e0cea3863a769d637e63a867484.js

js saluteimoveis.com.br collected 2026-10-10 00:27:01 UTC 5,106 bytes, 184 lines download raw bytes

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.