1/** 2 * mod_deps_product_search live AJAX search 3 */ 4(function () { 5 'use strict'; 6 7 const DEBOUNCE_MS = 280; 8 9 function appendSearchParam(base, kw) { 10 const sep = base.indexOf('?') >= 0 ? '&' : '?'; 11 return base + sep + 'search=' + encodeURIComponent(String(kw)); 12 } 13 14 function ensureScriptOptionsLoaded() { 15 if (typeof Joomla !== 'undefined' && typeof Joomla.loadOptions === 'function') { 16 Joomla.loadOptions(); 17 } 18 } 19 20 function getConfigFromScriptTag(optionsKey) { 21 const scripts = document.querySelectorAll('script.joomla-script-options'); 22 23 for (let i = 0; i < scripts.length; i += 1) { 24 const content = scripts[i].text || scripts[i].textContent; 25 26 if (!content) { 27 continue; 28 } 29 30 try { 31 const parsed = JSON.parse(content); 32 33 if (parsed && parsed[optionsKey]) { 34 return parsed[optionsKey]; 35 } 36 } catch (e) { 37 // ignore malformed script options 38 } 39 } 40 41 return null; 42 } 43 44 function getConfig(wrapper) { 45 const moduleId = wrapper.dataset.moduleId; 46 const optionsKey = 'mod_deps_product_search.module.' + moduleId; 47 48 ensureScriptOptionsLoaded(); 49 50 if (typeof Joomla !== 'undefined' && Joomla.getOptions(optionsKey)) { 51 return Joomla.getOptions(optionsKey); 52 } 53 54 const fromScript = getConfigFromScriptTag(optionsKey); 55 56 if (fromScript) { 57 return fromScript; 58 } 59 60 return window.modDepsProductSearch && window.modDepsProductSearch[moduleId] 61 ? window.modDepsProductSearch[moduleId] 62 : null; 63 } 64 65 function getSearchLink(wrapper, config) { 66 if (config && config.searchLink) { 67 return config.searchLink; 68 } 69 70 if (wrapper.dataset.searchLink) { 71 return wrapper.dataset.searchLink; 72 } 73 74 const form = wrapper.querySelector('.search-form'); 75 76 if (form && form.action) { 77 return form.action; 78 } 79 80 return ''; 81 } 82 83 function navigateToSearchResults(wrapper, config, keyword) { 84 const trimmed = String(keyword).trim(); 85 const base = getSearchLink(wrapper, config); 86 87 if (!trimmed || !base) { 88 return false; 89 } 90 91 window.location.href = appendSearchParam(base, trimmed); 92 return true; 93 } 94 95 function withCsrf(fields, config) { 96 const payload = Object.assign({}, fields); 97 98 if (config && config.csrfToken && config.csrfTokenName) { 99 payload[config.csrfTokenName] = config.csrfToken; 100 } 101 102 return payload; 103 } 104 105 function buildPostBody(fields) { 106 const body = new URLSearchParams(); 107 Object.keys(fields).forEach((key) => { 108 if (fields[key] !== undefined && fields[key] !== null) { 109 body.append(key, String(fields[key])); 110 } 111 }); 112 return body.toString(); 113 } 114 115 function buildAjaxUrl(query) { 116 const paths = typeof Joomla !== 'undefined' ? Joomla.getOptions('system.paths') : null; 117 const root = paths && paths.root ? paths.root : '/'; 118 const base = root.endsWith('/') ? root : root + '/'; 119 return base + 'index.php?' + query; 120 } 121 122 function createSearchState() { 123 return { 124 debounceTimer: null, 125 activeXhr: null, 126 requestId: 0, 127 }; 128 } 129 130 function clearDebounce(state) { 131 if (state.debounceTimer) { 132 clearTimeout(state.debounceTimer); 133 state.debounceTimer = null; 134 } 135 } 136 137 function abortActiveRequest(state) { 138 if (state.activeXhr) { 139 state.activeXhr.abort(); 140 state.activeXhr = null; 141 } 142 } 143 144 function setSearching(wrapper, isSearching) { 145 wrapper.classList.toggle('is-searching', isSearching); 146 } 147
148 function renderSearchResponse(wrapper, config, response, keyword) { 149 const popup = wrapper.querySelector('.search-popup'); 150 const input = wrapper.querySelector('.vssearch'); 151 const currentKeyword = input ? input.value : ''; 152 153 if (String(currentKeyword).trim() !== String(keyword).trim()) { 154 return; 155 } 156 157 if (popup) { 158 popup.innerHTML = response; 159 } 160 161 if (popup && popup.querySelector('.has-more-results')) { 162 const hrefAll = appendSearchParam(getSearchLink(wrapper, config), String(currentKeyword).trim()); 163 const btn = document.createElement('a'); 164 btn.href = hrefAll; 165 btn.className = 'button_default mt-10'; 166 btn.textContent = Joomla.JText._('MOD_DEPS_PRODUCT_SEARCH_VIEW_ALL_RESULTS'); 167 168 if (window.innerWidth >= 768) { 169 const left = popup.querySelector('.search-left'); 170 if (left) { 171 left.appendChild(btn); 172 } 173 } else { 174 popup.appendChild(btn); 175 } 176 } 177 178 const resetBtn = wrapper.querySelector('.search-reset-btn'); 179 if (resetBtn) { 180 resetBtn.style.visibility = 'visible'; 181 } 182 183 if (popup) { 184 popup.style.display = 'block'; 185 popup.classList.remove('history-active'); 186 } 187 } 188 189 function clearSearchResults(wrapper) { 190 const popup = wrapper.querySelector('.search-popup'); 191 abortActiveRequest(wrapper._depsSearchState); 192 setSearching(wrapper, false); 193 194 if (popup) { 195 popup.innerHTML = ''; 196 popup.style.display = 'none'; 197 popup.classList.remove('history-active'); 198 } 199 } 200 201 function runSearch(wrapper, keyword, state) { 202 const config = getConfig(wrapper); 203 if (!config) { 204 return; 205 } 206 207 const minChars = parseInt(wrapper.dataset.minChars || '2', 10); 208 const lang = wrapper.dataset.lang || ''; 209 const trimmed = String(keyword).trim(); 210 211 if (trimmed.length <= minChars) { 212 clearSearchResults(wrapper); 213 return; 214 } 215 216 abortActiveRequest(state); 217 state.requestId += 1; 218 const requestId = state.requestId; 219 220 setSearching(wrapper, true); 221 222 state.activeXhr = Joomla.request({ 223 url: buildAjaxUrl('option=com_ajax&module=' + config.ajaxModule + '&format=raw'), 224 method: 'POST', 225 data: buildPostBody(withCsrf({ 226 keyword: trimmed, 227 module_id: wrapper.dataset.moduleId, 228 lang: lang, 229 }, config)), 230 onSuccess(response) { 231 if (requestId !== state.requestId) { 232 return; 233 } 234 renderSearchResponse(wrapper, config, response, trimmed); 235 }, 236 onError(xhr) { 237 if (!xhr || xhr.status === 0) { 238 return; 239 } 240 }, 241 onComplete() { 242 if (requestId !== state.requestId) { 243 return; 244 } 245 state.activeXhr = null; 246 setSearching(wrapper, false); 247 }, 248 }); 249 } 250 251 function scheduleSearch(wrapper, state, immediate) { 252 clearDebounce(state); 253 254 if (immediate) { 255 const keyword = wrapper.querySelector('.vssearch')?.value || ''; 256 runSearch(wrapper, keyword, state); 257 return; 258 } 259 260 state.debounceTimer = setTimeout(() => { 261 state.debounceTimer = null; 262 const keyword = wrapper.querySelector('.vssearch')?.value || ''; 263 runSearch(wrapper, keyword, state); 264 }, DEBOUNCE_MS); 265 } 266 267 function initProductSearch() { 268 ensureScriptOptionsLoaded(); 269 270 document.querySelectorAll('.mod-deps-product-search').forEach(function (wrapper) { 271 const input = wrapper.querySelector('.vssearch'); 272 const config = getConfig(wrapper); 273 const popup = wrapper.querySelector('.search-popup'); 274 const form = wrapper.querySelector('.search-form'); 275 const state = createSearchState(); 276 277 wrapper._depsSearchState = state; 278 279 if (!input) { 280 return; 281 } 282 283 input.addEventListener('input', function () { 284 const resetBtn = wrapper.querySelector('.search-reset-btn'); 285 if (resetBtn) { 286 resetBtn.style.visibility = input.value ? 'visible' : 'hidden'; 287 } 288 scheduleSearch(wrapper, state, false); 289 }); 290 291 input.addEventListener('focus', function () { 292 wrapper.classList.add('active'); 293 document.body.classList.add('mod-deps-product-search-active'); 294 if (popup && popup.classList.contains('history-active')) { 295 popup.style.display = 'block'; 296 } 297 }); 298 299 wrapper.addEventListener('click', function (event) { 300 const target = event.target.closest('.search-reset-btn'); 301 if (!target) { 302 return; 303 } 304 305 clearDebounce(state); 306 abortActiveRequest(state); 307 state.requestId += 1; 308 309 input.value = ''; 310 target.style.visibility = 'hidden'; 311 setSearching(wrapper, false); 312 313 if (!config) { 314 return; 315 } 316 317 Joomla.request({ 318 url: buildAjaxUrl('option=com_ajax&module=' + config.ajaxModule + '&method=clearsearch&format=raw'), 319 method: 'POST', 320 data: buildPostBody(withCsrf({ module_id: wrapper.dataset.moduleId }, config)), 321 onSuccess(response) { 322 if (popup) { 323 popup.innerHTML = response || ''; 324 if (!response) { 325 popup.style.display = 'none'; 326 } else { 327 popup.classList.add('history-active'); 328 popup.style.display = 'block'; 329 } 330 } 331 }, 332 }); 333 }); 334 335 wrapper.addEventListener('click', function (event) { 336 const phraseBtn = event.target.closest('.search-phrase'); 337 if (!phraseBtn) { 338 return; 339 } 340 341 const phrase = phraseBtn.getAttribute('data-phrase') || ''; 342 input.value = phrase; 343 scheduleSearch(wrapper, state, true); 344 }); 345 346 if (form) { 347 form.addEventListener('submit', function (event) { 348 event.preventDefault();
349 const keyword = input.value.trim(); 350 351 if (!keyword) { 352 return; 353 } 354 355 clearDebounce(state); 356 abortActiveRequest(state); 357 document.body.classList.remove('mod-deps-product-search-active'); 358 wrapper.classList.remove('active'); 359 navigateToSearchResults(wrapper, config, keyword); 360 }); 361 } 362 363 wrapper.addEventListener('click', function (event) { 364 if (!event.target.closest('.clear-history')) { 365 return; 366 } 367 368 if (!config) { 369 return; 370 } 371 372 Joomla.request({ 373 url: buildAjaxUrl('option=com_ajax&module=' + config.ajaxModule + '&method=clearhistory&format=raw'), 374 method: 'POST', 375 data: buildPostBody(withCsrf({ module_id: wrapper.dataset.moduleId }, config)), 376 onSuccess() { 377 if (popup) { 378 popup.classList.remove('history-active'); 379 popup.style.display = 'none'; 380 } 381 }, 382 }); 383 }); 384 385 const bg = wrapper.querySelector('.search_popup_bg'); 386 if (bg) { 387 bg.addEventListener('click', function () { 388 document.body.classList.remove('mod-deps-product-search-active'); 389 wrapper.classList.remove('active'); 390 if (popup) { 391 popup.style.display = 'none'; 392 } 393 }); 394 } 395 396 input.addEventListener('keydown', function (event) { 397 if (event.key === 'Escape') { 398 clearDebounce(state); 399 abortActiveRequest(state); 400 wrapper.classList.remove('active'); 401 document.body.classList.remove('mod-deps-product-search-active'); 402 if (popup) { 403 popup.style.display = 'none'; 404 } 405 } 406 }); 407 }); 408 } 409 410 if (document.readyState === 'loading') { 411 document.addEventListener('DOMContentLoaded', initProductSearch); 412 } else { 413 initProductSearch(); 414 } 415})();
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.