PageSourceSearch

https://www.clubtalleres.com.ar/wp-content/plugins/cat-filiales/assets/js/shortcode-filiales.js?ver=1.9.4

js clubtalleres.com.ar collected 2026-10-02 04:26:43 UTC 6,486 bytes, 222 lines download raw bytes

1// Combobox con búsqueda integrada + paginación (5 por página)
2// Búsqueda insensible a acentos y con tolerancia leve (edit distance <= 1)
3window.filialesSelector = {
4  // Datos
5  all: [],           // [{ id, title, json: [ {image_url, title, description}, ... ] }]
6  search: '',        // texto tipeado
7  selectedId: 0,     // id de la filial seleccionada
8
9  // Estado del combobox
10  open: false,
11  activeIndex: 0,    // índice de la opción resaltada dentro de "visible"
12
13  // Paginación del dropdown
14  pageSize: 5,
15  visibleCount: 5,   // cuántos mostrar de "filtered"
16
17  // ==========================
18  // Init
19  // ==========================
20  init(filiales) {
21    this.all = Array.isArray(filiales) ? filiales : [];
22    this.resetPagination();
23    // Si querés preseleccionar la primera:
24    // if (this.all.length) this.applySelection(this.all[0]);
25  },
26
27  // ==========================
28  // Normalización y helpers
29  // ==========================
30  _norm(s) {
31    return (s || '')
32      .toString()
33      .toLowerCase()
34      // quitar diacríticos (acentos)
35      .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
36      // reemplazar cualquier separador raro por espacio
37      .replace(/[^a-z0-9ñ\s]/g, ' ')
38      // colapsar espacios
39      .replace(/\s+/g, ' ')
40      .trim();
41  },
42
43  _tokens(s) {
44    const n = this._norm(s);
45    return n ? n.split(' ') : [];
46  },
47
48  // Distancia de edición simple (Levenshtein)
49  // Usamos umbral 1 para tolerar un pequeño typo.
50  _editDistance(a, b) {
51    const m = a.length, n = b.length;
52    if (!m) return n;
53    if (!n) return m;
54    const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
55    for (let i = 0; i <= m; i++) dp[i][0] = i;
56    for (let j = 0; j <= n; j++) dp[0][j] = j;
57    for (let i = 1; i <= m; i++) {
58      for (let j = 1; j <= n; j++) {
59        const cost = a[i - 1] === b[j - 1] ? 0 : 1;
60        dp[i][j] = Math.min(
61          dp[i - 1][j] + 1,      // eliminación
62          dp[i][j - 1] + 1,      // inserción
63          dp[i - 1][j - 1] + cost // sustitución
64        );
65      }
66    }
67    return dp[m][n];
68  },
69
70  // Score de coincidencia entre título y query (sin acentos)
71  // Regresa 0 si no matchea lo suficiente, o un número > 0 si matchea (mayor = mejor)
72  _matchScore(title, q) {
73    const t = this._norm(title);
74    const toks = this._tokens(q);
75    if (!toks.length) return 1; // sin query: todos pasan con score mínimo
76
77    const words = t.split(' ');
78
79    let present = 0;
80    let score = 0;
81
82    for (const tok of toks) {
83      if (!tok) continue;
84
85      // coincidencia directa: incluye
86      if (t.includes(tok)) {
87        present++;
88        // bonus por palabra completa o comienzo de palabra
89        if (new RegExp(`\\b${tok}\\b`).test(t)) score += 12;
90        else if (new RegExp(`\\b${tok}`).test(t)) score += 10;
91        else score += 8;
92        continue;
93      }
94
95      // tolerancia a error: si alguna palabra del título está a distancia 1 del token
96      // (solo aplicamos si el token tiene 4+ para evitar ruido)
97      if (tok.length >= 4) {
98        let near = false;
99        for (const w of words) {
100          if (Math.abs(w.length - tok.length) > 1) continue; // poda rápida
101          if (this._editDistance(w, tok) <= 1) { near = true; break; }
102        }
103        if (near) { present++; score += 6; continue; }
104      }
105
106      // si no matchea este token (ni tolerante), penalizamos
107      score -= 4;
108    }
109
110    // Requerimos al menos que todos los tokens tengan alguna coincidencia (débil)
111    if (present < toks.length) return 0;
112
113    // Bonus por inicio de cadena con el primer token
114    if (t.startsWith(toks[0])) score += 4;
115
116    return Math.max(score, 1);
117  },
118
119  // ==========================
120  // Filtrado y orden
121  // ==========================
122  get filtered() {
123    const q = this.search || '';
124    const scored = this.all.map(f => ({
125      item: f,
126      score: this._matchScore(f.title, q)
127    })).filter(x => x.score > 0);
128
129    // ordenar por score desc y luego por título asc
130    scored.sort((a, b) => (b.score - a.score) || String(a.item.title).localeCompare(b.item.title));
131    return scored.map(x => x.item);
132  },
133
134  // Subconjunto visible (paginado)
135  get visible() {
136    return this.filtered.slice(0, this.visibleCount);
137  },
138
139  // Opción activa (dentro de lo visible)
140  get active() {
141    return this.visible[this.activeIndex] || null;
142  },
143
144  // Para aria-activedescendant
145  get activeDesc() {
146    const a = this.active;
147    return a ? `filial-opt-${a.id}` : '';
148  },
149
150  // Filial seleccionada por selectedId
151  get current() {
152    const id = Number(this.selectedId) || 0;
153    if (!id) return null;
154    return this.all.find(f => Number(f.id) === id) || null;
155  },
156
157  // ==========================
158  // Navegación / selección
159  // ==========================
160  move(delta) {
161    if (!this.open) this.open = true;
162    const len = this.visible.length;
163    if (!len) return;
164    this.activeIndex = (this.activeIndex + delta + len) % len;
165  },
166
167  selectActive() {
168    if (!this.active) return;
169    this.applySelection(this.active);
170  },
171
172  selectByIndex(i) {
173    const item = this.visible[i];
174    if (!item) return;
175    this.applySelection(item);
176  },
177
178  selectById(id) {
179    const item = this.all.find(f => Number(f.id) === Number(id));
180    if (!item) return;
181    this.applySelection(item);
182  },
183
184  applySelection(item) {
185    this.selectedId = Number(item.id);
186    this.search = item.title || '';
187    this.open = false;
188    this.activeIndex = 0;
189  },
190
191  clearSelection() {
192    this.selectedId = 0;
193    this.search = '';
194    this.open = false;
195    this.activeIndex = 0;
196    this.resetPagination();
197  },
198
199  // ==========================
200  // Paginación
201  // ==========================
202  resetPagination() {
203    this.visibleCount = this.pageSize;
204    this.activeIndex = 0;
205  },
206
207  showMore() {
208    const total = this.filtered.length;
209    this.visibleCount = Math.min(this.visibleCount + this.pageSize, total);
210    if (this.activeIndex >= this.visible.length) {
211      this.activeIndex = Math.max(0, this.visible.length - 1);
212    }
213  },
214
215  // ==========================
216  // Util
217  // ==========================
218  prettyJSON(obj) {
219    try { return JSON.stringify(obj, null, 2); }
220    catch { return ''; }
221  }
222};

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.