1/** 2 * MLSImport standalone (theme_id 990) front-end filtering. 3 * Submits the search form to admin-ajax, repaints the results grid + pager 4 * without a page reload. No dependencies. Config is localized as MLSImportListings. 5 */ 6( function () { 7 'use strict'; 8 9 // Bail entirely if the localized config never got printed. 10 if ( typeof window.MLSImportListings === 'undefined' ) { 11 return; 12 } 13 14 // Localized config: { action, nonce, ajaxurl }. 15 var cfg = window.MLSImportListings; 16 17 /** 18 * forEach helper that works on array-like NodeLists. 19 * @param {ArrayLike} list Items to iterate. 20 * @param {Function} fn Callback per item. 21 */ 22 function each( list, fn ) { 23 Array.prototype.forEach.call( list, fn ); 24 } 25 26 // FormData captures every selected option of a multi-select; a name ending in [] 27 // (e.g. state[]) is grouped under its base key (state) as an array so toBody can 28 // re-emit state[]=a&state[]=b â without this only the first option would be sent. 29 /** 30 * Gather the AJAX request params from the search form, page, and sort control. 31 * @param {HTMLFormElement} form The search form (may be null). 32 * @param {number} page 1-based page number. 33 * @param {HTMLSelectElement} sortSelect Toolbar sort control (outside the form). 34 * @return {Object} Param map (arrays for []-suffixed multi fields). 35 */ 36 function collectParams( form, page, sortSelect ) { 37 // Seed with the AJAX envelope. 38 var params = { 39 action: cfg.action, 40 nonce: cfg.nonce, 41 page: page || 1 42 }; 43 if ( form ) { 44 // Fold each form control into params, mirroring half-map's formFilters. 45 new FormData( form ).forEach( function ( value, name ) { 46 // Skip empty values and the envelope keys. 47 if ( value === '' || name === 'action' || name === 'nonce' || name === 'page' ) { 48 return; 49 } 50 // Base key + whether this was a []-suffixed multi field. 51 var key = name.replace( /\[\]$/, '' ); 52 var multi = key !== name; 53 // Merge repeated keys into an array; else set scalar / single-item array. 54 if ( Object.prototype.hasOwnProperty.call( params, key ) ) { 55 if ( ! Array.isArray( params[ key ] ) ) { 56 params[ key ] = [ params[ key ] ]; 57 } 58 params[ key ].push( value ); 59 } else { 60 params[ key ] = multi ? [ value ] : value; 61 } 62 } ); 63 } 64 // The Sort control sits in the results toolbar, OUTSIDE the search form, so 65 // FormData( form ) never captures it â read it in so refine/paginate keep the sort. 66 if ( sortSelect && sortSelect.value !== '' ) { 67 params.orderby = sortSelect.value; 68 } 69 return params; 70 } 71 72 /** 73 * Serialize a param map to an application/x-www-form-urlencoded body. 74 * @param {Object} params Param map (values may be arrays). 75 * @return {string} Encoded body string. 76 */ 77 function toBody( params ) { 78 var parts = []; 79 Object.keys( params ).forEach( function ( k ) { 80 var v = params[ k ]; 81 // Arrays re-emit as repeated key[]=item pairs. 82 if ( Array.isArray( v ) ) { 83 v.forEach( function ( item ) { 84 parts.push( encodeURIComponent( k + '[]' ) + '=' + encodeURIComponent( item ) ); 85 } ); 86 } else { 87 parts.push( encodeURIComponent( k ) + '=' + encodeURIComponent( v ) ); 88 } 89 } ); 90 return parts.join( '&' ); 91 } 92 93 // One skeleton card mirrors the real card's box (.mlsimport-listing-card) so the 94 // grid keeps its exact columns and sizing while cards load; the media block and 95 // text lines shimmer (styled in mlsimport-listings.css). 96 /** 97 * @param {number} count How many skeleton cards to build. 98 * @return {string} Concatenated skeleton-card markup. 99 */ 100 function skeletonCards( count ) { 101 // Markup for a single shimmering placeholder card. 102 var one = '<article class="mlsimport-listing-card mlsimport-skeleton-card" aria-hidden="true">' + 103 '<div class="mlsimport-listing-card__media mlsimport-skeleton__box"></div>' + 104 '<div class="mlsimport-listing-card__body">' + 105 '<span class="mlsimport-skeleton__line mlsimport-skeleton__line--price"></span>' + 106 '<span class="mlsimport-skeleton__line mlsimport-skeleton__line--title"></span>' + 107 '<span class="mlsimport-skeleton__line mlsimport-skeleton__line--specs"></span>' + 108 '</div></article>'; 109 // Repeat the single card `count` times. 110 var out = ''; 111 for ( var i = 0; i < count; i++ ) { 112 out += one; 113 } 114 return out; 115 } 116 117 // Show as many placeholders as cards currently on screen so the grid height 118 // doesn't jump; on a first load with no cards yet, fall back to the page size. 119 /** 120 * @param {Element} grid The results grid (may be null). 121 * @param {HTMLFormElement} form The search form (read for the page limit). 122 * @return {number} Skeleton count, capped at 24. 123 */ 124 function skeletonCount( grid, form ) { 125 // Prefer matching the number of cards already rendered. 126 var existing = grid ? grid.querySelectorAll( '.mlsimport-listing-card' ).length : 0; 127 if ( existing > 0 ) { 128 return Math.min( existing, 24 ); 129 } 130 // First load: fall back to the form's limit, else a small default of 6. 131 var limitEl = form ? form.querySelector( '[name="limit"]' ) : null; 132 var n = limitEl ? parseInt( limitEl.value, 10 ) : 0; 133 return n > 0 ? Math.min( n, 24 ) : 6; 134 } 135 136 /** 137 * Paint the server response into the grid, result meta, and pager. 138 * @param {Element} root The .mlsimport-listings block root. 139 * @param {Object} data Response payload ({ html, total, pager }). 140 */ 141 function render( root, data ) { 142 // Locate the three output regions. 143 var grid = root.querySelector( '.mlsimport-results__grid' ); 144 var meta = root.querySelector( '.mlsimport-results__meta' ); 145 var pager = root.querySelector( '.mlsimport-results__pager' ); 146 if ( grid ) { 147 // Cards markup, or an empty-state message. 148 grid.innerHTML = data.html || '<p class="mlsimport-results__empty">No listings match your search.</p>'; 149 // Let listeners (e.g. favorites hydration) re-process the fresh cards. 150 grid.dispatchEvent( new CustomEvent( 'mlsimport:cards-rendered', { bubbles: true } ) ); 151 } 152 if ( meta ) { 153 // Result count with singular/plural "result(s)". 154 meta.textContent = ( data.total || 0 ) + ' result' + ( 1 === data.total ? '' : 's' ); 155 } 156 if ( pager ) { 157 pager.innerHTML = data.pager || ''; 158 } 159 } 160
161 /** 162 * Fire the AJAX search and repaint the block with skeletons in the interim. 163 * @param {Element} root The block root. 164 * @param {HTMLFormElement} form The search form. 165 * @param {number} page 1-based page to fetch. 166 */ 167 function request( root, form, page ) { 168 // Results wrapper, grid, and (out-of-form) sort control. 169 var results = root.querySelector( '.mlsimport-results' ); 170 var grid = root.querySelector( '.mlsimport-results__grid' ); 171 var sortSelect = root.querySelector( '.mlsimport-results__toolbar select[name="orderby"]' ); 172 // Flag the block as loading (for CSS state). 173 if ( results ) { 174 results.classList.add( 'is-loading' ); 175 } 176 // Swap real cards for shimmering skeletons for the duration of the fetch 177 // (count read before the swap, so it reflects what was on screen). 178 if ( grid ) { 179 grid.innerHTML = skeletonCards( skeletonCount( grid, form ) ); 180 } 181 182 // Build and send the urlencoded POST. 183 var xhr = new XMLHttpRequest(); 184 xhr.open( 'POST', cfg.ajaxurl ); 185 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); 186 // Failure paths must clear the skeletons too, or they shimmer forever. 187 function fail() { 188 if ( grid ) { 189 grid.innerHTML = '<p class="mlsimport-results__empty">Could not load listings. Please try again.</p>'; 190 } 191 } 192 // On response: clear loading, parse JSON, render on success else fail. 193 xhr.onload = function () { 194 if ( results ) { 195 results.classList.remove( 'is-loading' ); 196 } 197 try { 198 var res = JSON.parse( xhr.responseText ); 199 if ( res && res.success ) { 200 render( root, res.data ); 201 } else { 202 fail(); 203 } 204 } catch ( e ) { 205 fail(); 206 } 207 }; 208 // On transport error: clear loading and show the failure message. 209 xhr.onerror = function () { 210 if ( results ) { 211 results.classList.remove( 'is-loading' ); 212 } 213 fail(); 214 }; 215 // Encode all params and send. 216 xhr.send( toBody( collectParams( form, page, sortSelect ) ) ); 217 } 218 219 /** 220 * Wire one listings block: submit, sort change, and pager clicks. 221 * @param {Element} root The .mlsimport-listings block root. 222 */ 223 function init( root ) { 224 // The search form and the toolbar sort control. 225 var form = root.querySelector( '.mlsimport-search' ); 226 var sort = root.querySelector( '.mlsimport-results__toolbar select[name="orderby"]' ); 227 228 // Form submit refines from page 1 (no reload). 229 if ( form ) { 230 form.addEventListener( 'submit', function ( e ) { 231 e.preventDefault(); 232 request( root, form, 1 ); 233 } ); 234 } 235 236 // The toolbar Sort is not inside the form, so a form submit is neither required nor 237 // intuitive above the results â changing it re-sorts (repaints from page 1) at once. 238 if ( sort ) { 239 sort.addEventListener( 'change', function () { 240 request( root, form, 1 ); 241 } ); 242 } 243 244 // Delegated pager clicks fetch the requested page. 245 root.addEventListener( 'click', function ( e ) { 246 var btn = e.target.closest ? e.target.closest( '.mlsimport-pager__btn' ) : null; 247 if ( btn && btn.dataset.page ) { 248 e.preventDefault(); 249 request( root, form, parseInt( btn.dataset.page, 10 ) || 1 ); 250 } 251 } ); 252 } 253 254 // Initialise every listings block once the DOM is ready. 255 document.addEventListener( 'DOMContentLoaded', function () { 256 each( document.querySelectorAll( '.mlsimport-listings' ), init ); 257 } ); 258}() );
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.