1/* global _WPFILTERS */ 2 3( function($) { 4 5 'use strict'; 6 7 // Fetches filtered markup and replaces result/pagination regions in place. 8 const app = { 9 10 /** 11 * Init. 12 * 13 * @since 0.0.1 14 */ 15 init: () => { 16 17 $( app.ready ); 18 }, 19 20 /** 21 * Document ready 22 * 23 * @since 0.0.1 24 */ 25 ready: () => { 26 27 // Shared request payload consumed by frontend elements.js. 28 window.WPFILTERS_HTTP = { 29 "url_vars": {} 30 }; 31 32 let loop = jQuery( '.wpfilters-results' ); 33 34 // Inside a SearchWP template, the .wpfilters-results wrapper is 35 // only present when the "Enable WPFilters" setting is on. 36 // When absent, skip all fallback detection (loop + pagination). 37 const isSearchWPTemplate = !! document.querySelector( '.swp-rp-main' ); 38 const wpfiltersDisabled = isSearchWPTemplate && ! loop.length; 39 40 if ( ! loop.length && ! wpfiltersDisabled ) { 41 const has_loop = app.detectLoop(document.body); 42 43 if (has_loop) { 44 app.addClass('wpfilters-results', app.constructor(has_loop)); 45 } 46 } 47 48 if ( ! wpfiltersDisabled ) { 49 const has_pagination = app.detectPagination(document.body); 50 51 if (has_pagination) { 52 app.addClass('wpfilters-pagination', app.constructor(has_pagination)); 53 } 54 } 55 56 }, 57 58 sendRequest: () => { 59 60 // Visual loading state for common loop containers. 61 const $resultsTarget = jQuery('.wpfilters-results'); 62 63 $resultsTarget.css( 'opacity', 0.5 ); 64 65 // Request server-rendered HTML based on current normalized filter params. 66 $.post( document.URL, JSON.stringify({ 67 data: { 68 'http_params': WPFILTERS_HTTP, 69 }, 70 action: _WPFILTERS.prefix + 'request' 71 }), function (response){ 72 73 try { 74 const json = JSON.parse(response); 75 if (!json || typeof json.template !== 'string') { 76 return; 77 } 78 79 // Rebuild response as a DOM fragment and extract known targets. 80 const obj = app.constructor(json.template); 81 let $tpl = app.find('.wpfilters-results', obj); 82 83 // Preserve legacy marker support for non-SearchWP contexts. 84 if ($tpl.nodes && $tpl.nodes.length < 1 && obj.nodes && obj.nodes.length > 0) { 85 const loop = app.detectLoop(obj.nodes[0]); 86 87 if (loop) { 88 $tpl = app.addClass('wpfilters-results', app.constructor(loop)); 89 } 90 } 91 92 if ($tpl.nodes && $tpl.nodes.length > 0) { 93 const inject = $tpl.nodes[0].innerHTML; 94 95 if (inject) { 96 jQuery('.wpfilters-results').html(inject); 97 98 } 99 } 100 101 const $currentPagination = jQuery('.wpfilters-pagination'); 102 let $pagination = app.find('.wpfilters-pagination', obj); 103 104 // Preserve legacy marker support for non-SearchWP contexts. 105 if ($pagination.nodes && $pagination.nodes.length < 1 && obj.nodes && obj.nodes.length > 0) { 106 const pagination = app.detectPagination(obj.nodes[0]); 107 108 if (pagination) { 109 $pagination = app.addClass('wpfilters-pagination', app.constructor(pagination)); 110 } 111 } 112 113 let paginationInject = null; 114 if ($pagination.nodes && $pagination.nodes.length > 0) { 115 paginationInject = $pagination.nodes[0].innerHTML; 116 } 117 118 // Missing pagination in response is a valid state. 119 if (!paginationInject && $currentPagination.length) { 120 $currentPagination.empty(); 121 } else if (paginationInject && $currentPagination.length) { 122 $currentPagination.html(paginationInject); 123 } else if (paginationInject && !$currentPagination.length) { 124 // Pagination exists in response but not in DOM yet. 125 // Only inject a separate wrapper if the pagination is outside 126 // the results container in the response. When it's inside, 127 // it was already injected as part of the results HTML above. 128 if (!app.isPaginationInsideResults(obj)) { 129 const $resultsEl = jQuery('.wpfilters-results'); 130 if ($resultsEl.length) { 131 const paginationWrapper = document.createElement('div'); 132 paginationWrapper.className = 'wpfilters-pagination'; 133 paginationWrapper.innerHTML = paginationInject; 134 135 if (app.isPaginationBeforeResults(obj)) { 136 $resultsEl.before(paginationWrapper); 137 } else { 138 $resultsEl.after(paginationWrapper); 139 } 140 } 141 } 142 } 143 144 const $resultCount = app.find('.woocommerce-result-count', obj); 145 if ($resultCount.nodes && $resultCount.nodes.length > 0 && jQuery('.woocommerce-result-count').length > 0) { 146 jQuery('.woocommerce-result-count').replaceWith($resultCount.nodes[0].cloneNode(true)); 147 } 148 149 // Replace the SearchWP Template page header so the H1 150 // ("Search results for: â¦") and the embedded SearchWP 151 // search-form input reflect the new keyword on every 152 // WPFilters refresh. The header is rendered OUTSIDE 153 // `.wpfilters-results` (see 154 // `searchwp/includes/Templates/Frontend.php` `render_page_header()`), 155 // so it isn't picked up by the inner-content replacement 156 // above. We use `outerHTML` + `replaceWith` so any 157 // attributes on the wrapper (`class`, `role`, etc.) 158 // from the server-rendered response also update. 159 const $newPageHeader = app.find('.swp-rp-page-header', obj); 160 const $currentPageHeader = jQuery('.swp-rp-page-header'); 161 if ( 162 $newPageHeader.nodes && 163 $newPageHeader.nodes.length > 0 && 164 $currentPageHeader.length > 0 165 ) { 166 $currentPageHeader.replaceWith($newPageHeader.nodes[0].outerHTML); 167 } 168 169 // Refresh on-page filter elements with the new filter 170 // HTML (option counts, available options) shipped by 171 // the server in `json.filter_data`. Filter elements 172 // typically live OUTSIDE `.wpfilters-results` (in 173 // sidebars or as standalone widgets), so the 174 // inner-content replace above doesn't reach them. 175 // Without this loop, filter option counts stay at 176 // their page-load values forever â most visibly 177 // after clicking Reset, where users expect counts to 178 // revert to unfiltered values. 179 if (json.filter_data && typeof json.filter_data === 'object') {
180 Object.keys(json.filter_data).forEach(function(elementId) { 181 const id = parseInt(elementId, 10); 182 const html = json.filter_data[elementId]; 183 if (!html) { 184 return; 185 } 186 187 // Match both already-injected elements (with 188 // data-element-id preserved by `injectFilterHtml`) 189 // AND any fresh placeholders that may have just 190 // landed inside `.wpfilters-results` via the 191 // inner-content replace above. 192 const $current = jQuery( 193 '.wpfilters-element[data-element-id="' + id + '"], ' + 194 '.wpfilters-element-placeholder[data-element-id="' + id + '"]' 195 ); 196 if ($current.length === 0) { 197 return; 198 } 199 200 // Don't disrupt the element the user is 201 // actively TYPING in â the Search element's 202 // text input or any similar text-bearing 203 // control. Replacing would steal focus and 204 // lose the caret. 205 // 206 // Narrowly scoped to text inputs because the 207 // previous broader check ("any focused 208 // element inside this filter") also matched 209 // just-clicked checkboxes/radios. After 210 // un-checking a filter value, the checkbox 211 // still holds focus when the AJAX response 212 // arrives â skipping replacement then left 213 // stale option counts on the page (the 214 // freshly-computed counts under the new 215 // un-narrowed scope never landed). For 216 // checkbox/radio/select, focus loss after 217 // replacement is fine because the user 218 // isn't typing. 219 const active = document.activeElement; 220 if ( 221 active && 222 (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') && 223 (active.type === 'search' || active.type === 'text' || active.type === 'email' || active.type === 'url' || active.tagName === 'TEXTAREA') && 224 $current.get(0) === active.closest('.wpfilters-element, .wpfilters-element-placeholder') 225 ) { 226 return; 227 } 228 229 const $new = jQuery(html); 230 if ($new.length === 0) { 231 return; 232 } 233 $new.first().attr('data-element-id', String(id)); 234 $current.replaceWith($new); 235 }); 236 237 // Keep the in-memory cache fresh for any code 238 // path that reads `_WPFILTERS.filterData` later 239 // (e.g. a second refresh, or a future feature). 240 if (typeof window._WPFILTERS !== 'undefined') { 241 window._WPFILTERS.filterData = Object.assign( 242 {}, 243 window._WPFILTERS.filterData || {}, 244 json.filter_data 245 ); 246 } 247 248 // Tell elements.js to re-run per-element 249 // enhancements (multi-select chips, show-more 250 // toggle) against the fresh DOM. 251 jQuery(document).trigger('wpfilters:elementsRefreshed'); 252 } 253 } 254 catch(e) { 255 console.error(e); 256 } 257 finally { 258 $resultsTarget.css( 'opacity', 1 ); 259 } 260 }, 'text'); 261 }, 262 263 detectLoop: (node) => { 264 return app.detectComment(node, 'wpfilters-loop'); 265 }, 266 267 detectPagination: (node) => { 268 return app.detectComment(node, 'wpfilters-pagination'); 269 }, 270 271 detectComment: (node, nodeValue) => { 272 // Finds sections wrapped by marker comments in rendered HTML. 273 const parents = []; 274 let curNode = null; 275 const iterator = document.createNodeIterator(node, NodeFilter.SHOW_COMMENT, () => { 276 return NodeFilter.FILTER_ACCEPT; /* IE expects a function */ 277 }, false); 278 279 while (curNode = iterator.nextNode()) { 280 if (curNode.nodeType === 8 && curNode.nodeValue === nodeValue) { 281 parents.push(curNode.parentNode); 282 } 283 } 284 285 return parents.length > 0 ? parents : false; 286 }, 287 288 constructor: (selector) => { 289 // Lightweight wrapper that normalizes selectors, nodes, arrays, and HTML strings. 290 291 const obj = { 292 nodes: [], 293 } 294 295 if (typeof selector === 'string' || selector instanceof String) { // string 296 var selector = selector.replace(':selected', ':checked'); 297 298 if ('' === selector) { 299 obj.nodes = []; 300 } 301 else if (app.isValidSelector(selector)) { 302 obj.nodes = Array.from(document.querySelectorAll(selector)); 303 } 304 else { 305 var tpl = document.createElement('template'); 306 tpl.innerHTML = selector; 307 obj.nodes = [tpl.content]; 308 } 309 } 310 else if (Array.isArray(selector)) { // array of nodes 311 obj.nodes = selector; 312 } 313 else if (typeof selector === 'object' && selector.nodeName) { // node 314 obj.nodes = [selector]; 315 } 316 else if (selector === window) { // window 317 obj.nodes = [window]; 318 } 319 else { // document 320 obj.nodes = [document]; 321 } 322 323 return obj; 324 }, 325 326 isValidSelector: (string) => { 327 try { 328 document.createDocumentFragment().querySelector(string); 329 } 330 catch(err) { 331 return false;
332 } 333 return true; 334 }, 335 336 find: (selector, obj) => { 337 // Query utility scoped to a wrapped object graph. 338 var selector = selector.replace(':selected', ':checked'); 339 let nodes = []; 340 let clone = app.clone(obj.nodes); 341 342 clone.nodes.forEach(node => { 343 nodes = nodes.concat(Array.from(node.querySelectorAll(selector))); 344 }); 345 346 clone.nodes = nodes; 347 return clone; 348 }, 349 350 clone: (nodes) => { 351 return app.constructor(nodes); 352 }, 353 354 addClass: (className, obj) => { 355 obj.nodes.forEach(node => node.classList.add(className)); 356 return obj; 357 }, 358 359 isPaginationInsideResults: (obj) => { 360 // Check if the pagination element is a descendant of the results element 361 // in the response DOM. 362 if (!obj.nodes || !obj.nodes.length) { 363 return false; 364 } 365 366 const root = obj.nodes[0]; 367 const results = root.querySelector('.wpfilters-results'); 368 const pagination = root.querySelector('.wpfilters-pagination'); 369 370 if (!results || !pagination) { 371 return false; 372 } 373 374 return results.contains(pagination); 375 }, 376 377 isPaginationBeforeResults: (obj) => { 378 // Walk the response DOM to determine if pagination appears before results. 379 if (!obj.nodes || !obj.nodes.length) { 380 return false; 381 } 382 383 const root = obj.nodes[0]; 384 const results = root.querySelector('.wpfilters-results'); 385 const pagination = root.querySelector('.wpfilters-pagination'); 386 387 if (!results || !pagination) { 388 return false; 389 } 390 391 // compareDocumentPosition bit 4 (DOCUMENT_POSITION_FOLLOWING) means 392 // pagination comes before results in document order. 393 return !!(pagination.compareDocumentPosition(results) & Node.DOCUMENT_POSITION_FOLLOWING); 394 }, 395 396 }; 397 398 app.init(); 399 400 window.wpfilters = window.wpfilters || {}; 401 402 window.wpfilters.Render = app; 403 404}( jQuery ) );
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.