1(function () { 2 /* ---------- Utilidades básicas ---------- */ 3 const $ = id => document.getElementById(id); 4 const extend = (target, src) => Object.assign({}, target, src); 5 const isFunction = fn => typeof fn === 'function'; 6 7 /* ---------- Clase base Autocompleter ---------- */ 8 class Autocompleter { 9 constructor(element, options = {}) { 10 this.element = typeof element === 'string' ? $(element) : element; 11 this.options = extend({ 12 minLength: 1, 13 delay: 200, 14 width: 'auto', 15 overflow: false, 16 className: 'autocompleter-choices', 17 onSelection: null 18 }, options); 19 20 this.timer = null; 21 this.queryValue = ''; 22 this.build(); 23 this.bindEvents(); 24 25 // hide suggestions on window resize / orientation change and when input size changes 26 this._handleResize = () => this.hideChoices(); 27 window.addEventListener('resize', this._handleResize); 28 window.addEventListener('orientationchange', this._handleResize); 29 30 // observe direct size changes of the input (e.g. CSS/layout changes) 31 if (typeof ResizeObserver !== 'undefined') { 32 this._resizeObserver = new ResizeObserver(() => this.hideChoices()); 33 this._resizeObserver.observe(this.element); 34 } else { 35 // fallback: if ResizeObserver no está disponible, se usa scroll/resize global 36 this._fallbackResize = () => this.hideChoices(); 37 window.addEventListener('scroll', this._fallbackResize, true); 38 } 39 } 40 41 build() { 42 this.choices = document.createElement('ul'); 43 this.choices.className = this.options.className; 44 this.choices.style.position = 'absolute'; 45 this.choices.style.display = 'none'; 46 this.choices.style.zIndex = '999'; 47 this.choices.style.width = 48 this.options.width === 'auto' ? this.element.offsetWidth + 'px' : this.options.width + 'px'; 49 document.body.appendChild(this.choices); 50 } 51 52 bindEvents() { 53 this.element.addEventListener('keyup', e => this.onKeyUp(e)); 54 document.addEventListener('click', e => { 55 if (!this.choices.contains(e.target) && e.target !== this.element) this.hideChoices(); 56 }); 57 } 58 59 onKeyUp(e) { 60 const value = this.element.value.trim(); 61 if (value.length < this.options.minLength) { 62 this.hideChoices(); 63 return; 64 } 65 clearTimeout(this.timer); 66 this.timer = setTimeout(() => this.query(value), this.options.delay); 67 } 68 69 query(value) { 70 this.queryValue = value; 71 // Método que se redefine en las subclases 72 } 73 74 /*showChoices() { 75 const rect = this.element.getBoundingClientRect(); 76 const computed = window.getComputedStyle(this.element); 77 78 // Calcula ancho real incluyendo padding y borde 79 const width = 80 this.element.offsetWidth - 81 parseFloat(computed.paddingLeft) - 82 parseFloat(computed.paddingRight); 83 84 this.choices.style.left = rect.left + window.scrollX + 'px'; 85 this.choices.style.top = rect.bottom + window.scrollY + 'px'; 86 this.choices.style.width = width + 'px'; // ð¹ ancho igual al input 87 this.choices.style.display = ''; 88 }*/ 89 90 showChoices() { 91 const rect = this.element.getBoundingClientRect(); 92 const form = this.element.closest('form'); // ð¹ busca el formulario padre 93 const ref = form || this.element; // usa el form si existe, si no, el input 94 95 const refRect = ref.getBoundingClientRect(); 96 97 this.choices.style.left = refRect.left + window.scrollX + 'px'; 98 this.choices.style.top = rect.bottom + window.scrollY + 'px'; 99 this.choices.style.width = ref.offsetWidth + 'px'; // ð¹ mismo ancho que el formulario 100 this.choices.style.display = ''; 101 } 102 103 hideChoices() { 104 this.choices.style.display = 'none'; 105 } 106 107 addChoiceEvents(choice) { 108 choice.addEventListener('click', () => { 109 this.element.value = choice.inputValue; 110 this.hideChoices(); 111 if (isFunction(this.options.onSelection)) this.options.onSelection(choice); 112 }); 113 } 114 115 markQueryValue(str) { 116 if (!this.queryValue) return str; 117 const regex = new RegExp('(' + this.queryValue.escapeRegExp() + ')', 'gi'); 118 return str.replace(regex, '<span class="autocompleter-highlight">$1</span>'); 119 } 120 121 destroy(){ 122 // remove resize/observer listeners 123 if (this._handleResize) { 124 window.removeEventListener('resize', this._handleResize); 125 window.removeEventListener('orientationchange', this._handleResize); 126 this._handleResize = null; 127 } 128 if (this._resizeObserver) { 129 this._resizeObserver.disconnect(); 130 this._resizeObserver = null; 131 } 132 if (this._fallbackResize) { 133 window.removeEventListener('scroll', this._fallbackResize, true); 134 this._fallbackResize = null; 135 } 136 } 137 } 138 139 /* ---------- Utilidad para escapar expresiones ---------- */ 140 String.prototype.escapeRegExp = function () { 141 return this.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 142 }; 143 144 /* ---------- Autocompleter.Request ---------- */ 145 class AutocompleterRequest extends Autocompleter { 146 constructor(element, options = {}) { 147 super(element, options); 148 this.ajaxOptions = extend({ 149 url: '', 150 method: 'GET', 151 headers: { 'X-Requested-With': 'XMLHttpRequest' } 152 }, options.ajaxOptions || {}); 153 } 154 } 155 156 /* ---------- Autocompleter.Request.HTML ---------- */ 157 class AutocompleterRequestHTML extends AutocompleterRequest { 158 constructor(element, url, options = {}) { 159 if (typeof url === 'object') { 160 options = url; 161 url = options.url || ''; 162 } 163 options.ajaxOptions = extend({ 164 url: url || '', 165 method: 'GET', 166 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 167 postVar: 'value' 168 }, options.ajaxOptions || {}); 169 super(element, options); 170 } 171 172 query(value) { 173 this.queryValue = value; 174 if (!this.ajaxOptions.url) return; 175 176 const controller = new AbortController(); 177 this.lastController = controller; 178 const url = 179 (this.ajaxOptions.method || 'GET').toUpperCase() === 'GET' 180 ? this.ajaxOptions.url + 181 (this.ajaxOptions.url.includes('?') ? '&' : '?') + 182 encodeURIComponent(this.ajaxOptions.postVar || 'value') + 183 '=' + 184 encodeURIComponent(value) 185 : this.ajaxOptions.url; 186 187 const fetchOpts = { 188 method: "POST", 189 headers: this.ajaxOptions.headers, 190 signal: controller.signal 191 }; 192 if (fetchOpts.method !== 'GET') { 193 fetchOpts.body = new URLSearchParams({ [this.ajaxOptions.postVar || 'value']: value }); 194 } 195 196 fetch(url, fetchOpts) 197 .then(r => { 198 if (!r.ok) throw new Error('Error en la red'); 199 return r.text(); 200 }) 201 .then(html => { 202 const tmp = document.createElement('div'); 203 tmp.innerHTML = html.trim(); 204 const list = tmp.querySelectorAll('li'); 205 this.choices.innerHTML = ''; 206 list.forEach(choice => { 207 if (isFunction(this.options.injectChoice)) { 208 this.options.injectChoice.call(this, choice); 209 } else { 210 const text = choice.textContent.trim(); 211 choice.inputValue = text; 212 choice.innerHTML = this.markQueryValue(text); 213 this.addChoiceEvents(choice); 214 } 215 this.choices.appendChild(choice); 216 }); 217 this.showChoices(); 218 }) 219 .catch(() => { 220 this.hideChoices(); 221 }) 222 .finally(() => { 223 this.lastController = null; 224 }); 225 } 226 } 227 228 /* ---------- Export global ---------- */ 229 window.Autocompleter = Autocompleter; 230 window.Autocompleter.Request = AutocompleterRequest; 231 window.Autocompleter.Request.HTML = AutocompleterRequestHTML; 232})();
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.