PageSourceSearch

https://prioritymechanical.ca/wp-content/themes/Divi-Child/plugins…/hvac-products-frontend.js?ver=1782314932

js prioritymechanical.ca collected 2026-10-06 05:29:34 UTC 13,399 bytes, 406 lines download raw bytes

1/* global jQuery, HVACPF */
2(function(window, $){
3  'use strict';
4  if (typeof HVACPF === 'undefined') return;
5 
6
7
8  const $root     = $('#hvacpf');
9  const $brandsEl = $('#hvacpf-brands-list');
10  const $catsEl   = $('#hvacpf-cats-list');
11  const $grid     = $('#hvacpf-products-grid');
12  const $search   = $('#hvacpf-search');
13  const $countB   = $('#hvacpf-brands-count');
14  const $countC   = $('#hvacpf-cats-count');
15  const $countP   = $('#hvacpf-products-count');
16  const $more     = $('#hvacpf-load-more');
17  const headerOffset = parseInt(HVACPF.scrollOffset || 0, 10); // e.g. set to 150 in localized data if you want
18  
19  // --- NEW: sentinel right after the grid (no class name changes)
20  let $sentinel = $('#hvacpf-sentinel');
21  if (!$sentinel.length) {
22    $sentinel = $('<div id="hvacpf-sentinel" aria-hidden="true" style="height:1px;"></div>');
23    $grid.after($sentinel);
24  }
25
26  // State
27  const perPage = parseInt(HVACPF.per_page || 12, 10);
28  let activeBrand = parseInt(HVACPF.initial.brand_id || 0, 10);   // 0 = All Brands
29  let activeCat   = parseInt(HVACPF.initial.categoryId || 0, 10); // 0 = All Categories
30  let page = 1;
31  let loading = false;
32  let hasMore = true;
33  let q = '';
34  let activeRequest = null;
35
36  // Selection (what admin allowed)
37  const selection = HVACPF.selection || { brands: [], categories: {}, products: {} };
38  const brandsAll = (HVACPF.brands || []).filter(b => selection.brands.includes(parseInt(b.id,10)));
39  const categoriesAll = HVACPF.categories || [];
40
41  // Helpers
42  function esc(s){ return String(s||'').replace(/[&<>\"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m])); }
43  function toInt(v){ const n = parseInt(v,10); return isNaN(n) ? 0 : n; }
44
45  function brandNameById(id){
46    if (!id) return 'All Brands';
47    const b = brandsAll.find(x => toInt(x.id) === toInt(id));
48    return b ? b.name : 'Brand #'+id;
49  }
50  function categoryById(id){
51    if (!id) return { id: 0, name: 'All Categories', slug: 'all' };
52    const c = categoriesAll.find(x => toInt(x.id) === toInt(id));
53    return c ? c : { id, name: 'Category #'+id, slug: '' };
54  }
55  function categorySlugById(id){
56    const c = categoryById(id);
57    return c.slug || 'all';
58  }
59    
60    function scrollToRoot(){
61      const y = Math.max(0, $grid.offset().top - headerOffset);
62      // prefers-reduced-motion friendly
63      const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
64      if ('scrollBehavior' in document.documentElement.style && !reduce) {
65        window.scrollTo({ top: y, behavior: 'smooth' });
66      } else {
67        $('html, body').stop(true).animate({ scrollTop: y }, 500);
68      }
69    }
70
71  // URL sync ?category=slug&brand_id=N
72  function updateURL(){
73    const slug = categorySlugById(activeCat);
74    const urlBase = HVACPF.urlBase || (window.location.origin + window.location.pathname);
75    const newQS = '?category=' + encodeURIComponent(slug) + '&brand_id=' + encodeURIComponent(activeBrand);
76    const newHref = urlBase + newQS;
77    window.history.replaceState({}, '', newHref);
78  }
79
80  // Build Brands list (with "All Brands")
81  function renderBrands(){
82    $brandsEl.empty();
83
84    const allItem = $(
85      '<button type="button" class="hvacpf-pill '+(activeBrand===0?'is-active':'')+'" data-id="0">'+
86        '<span class="pill-name">All Brands</span>'+
87      '</button>'
88    );
89    $brandsEl.append(allItem);
90
91    brandsAll.forEach(b => {
92      const el = $(
93        '<button type="button" class="hvacpf-pill '+(activeBrand===toInt(b.id)?'is-active':'')+'" data-id="'+b.id+'">'+
94          (b.logo ? '<span class="pill-logo"><img src="'+esc(b.logo)+'" alt="'+esc(b.name)+'"></span>' : '')+
95          '<span class="pill-name">'+esc(b.name)+'</span>'+
96        '</button>');
97      $brandsEl.append(el);
98    });
99
100    const total = 1 + brandsAll.length;
101    $countB.text(total + ' brand' + (total===1?'':'s'));
102  }
103
104  // Build Categories list (depending on active brand)
105function renderCategories(){
106  $catsEl.empty();
107
108  // Helper: build ordered list of category IDs for the current brand scope
109  function getOrderedCatIds(){
110    const seen = new Set();
111    const out = [];
112
113    if (activeBrand === 0) {
114      // All Brands: walk brands in saved order, then each brand's categories in saved order
115      (selection.brands || []).forEach(bid => {
116        const list = selection.categories[bid] || [];
117        list.forEach(cid => {
118          const key = String(cid);
119          if (!seen.has(key)) { seen.add(key); out.push(parseInt(cid,10)); }
120        });
121      });
122    } else {
123      // Single brand: use its saved order directly
124      (selection.categories[activeBrand] || []).forEach(cid => {
125        out.push(parseInt(cid,10));
126      });
127    }
128    return out;
129  }
130
131  const orderedCatIds = getOrderedCatIds();
132
133  // "All Categories"
134  const allBtn = $(
135    '<button type="button" class="hvacpf-pill '+(activeCat===0?'is-active':'')+'" data-id="0">'+
136      '<span class="pill-name">All Categories</span>'+
137    '</button>'
138  );
139  $catsEl.append(allBtn);
140
141  // Render each category in the computed order
142  const byId = {};
143  (HVACPF.categories || []).forEach(c => { byId[parseInt(c.id,10)] = c; });
144
145  orderedCatIds.forEach(id => {
146    const c = byId[id];
147    if (!c) return;
148    const el = $(
149      '<button type="button" class="hvacpf-pill '+(activeCat===id?'is-active':'')+'" data-id="'+id+'">'+
150        '<span class="pill-name">'+esc(c.name||('Category #'+id))+'</span>'+
151      '</button>'
152    );
153    $catsEl.append(el);
154  });
155
156  const total = 1 + orderedCatIds.length; // includes "All Categories"
157  $countC.text(total + ' categor' + (total===1?'y':'ies'));
158}
159
160
161
162  // Card builder
163    function productCard(p){
164      const img = p.image
165        ? '<div class="card-thumb"><img src="'+esc(p.image)+'" alt="'+esc(p.title)+'" loading="lazy" decoding="async"></div>'
166        : '<div class="card-thumb card-thumb--placeholder"></div>';
167
168      const href = (HVACPF.productUrl || '/products/product/') + '?id=' + encodeURIComponent(p.id);
169
170      return $(
171        '<article class="hvacpf-card" data-id="'+p.id+'">'+
172          '<a class="card-link" href="'+href+'">'+
173            img+
174            '<h3 class="card-title">'+esc(p.title)+'</h3>'+
175          '</a>'+
176        '</article>'
177      );
178    }
179
180  function resetGrid(){
181    page = 1;
182    hasMore = true;
183    $grid.empty();
184    $countP.text('');
185  }
186
187  function setLoading(state){
188    loading = !!state;
189    $more.toggle(loading || hasMore);
190    $more.find('.hvacpf-spinner').toggle(!!state);
191    if (!hasMore && !loading) {
192      $more.text('No more products');
193    } else {
194      $more.html('<span class="hvacpf-spinner"></span> Loading more products…');
195    }
196  }
197
198  // --- NEW: Keep fetching until content exceeds viewport (or no more)
199  function autoFill(){
200    // Avoid tight loops; re-check after each fetch completes
201    if (loading || !hasMore) return;
202    const scrollable = $(document).height() > ($(window).height() + 50);
203    if (!scrollable) {
204      fetchPage(page + 1).then(function(){
205        // If still not scrollable and more exists, keep going
206        setTimeout(autoFill, 0);
207      });
208    }
209  }
210
211  function fetchPage(nextPage){
212    if (loading) {
213      if (nextPage === 1 && activeRequest && activeRequest.readyState !== 4) {
214        activeRequest.abort();
215        loading = false;
216      } else {
217        return $.Deferred().resolve().promise();
218      }
219    }
220    if (!hasMore && nextPage > 1) return $.Deferred().resolve().promise();
221
222    setLoading(true);
223    const request = $.post(HVACPF.ajax, {
224      action: 'hvac_products_query',
225      nonce: HVACPF.nonce,
226      brand_id: activeBrand,
227      cat_id: activeCat,
228      q: q,
229      page: nextPage,
230      per_page: perPage
231    });
232    activeRequest = request;
233
234    return request.then(function(res){
235      if (!res || !res.success) return;
236      const data = res.data || {};
237      const items = data.items || [];
238      // Ensure boolean
239      hasMore = !!data.has_more;
240
241      items.forEach(it => $grid.append(productCard(it)));
242
243      const total = parseInt(data.total || 0, 10);
244      $countP.text(total + ' product' + (total === 1 ? '' : 's'));
245
246      page = nextPage;
247    }).always(function(){
248      if (activeRequest === request) {
249        activeRequest = null;
250      }
251      setLoading(false);
252    });
253  }
254
255  // Initial renders
256  renderBrands();
257  renderCategories();
258  resetGrid();
259  fetchPage(1).then(function(){
260    updateURL();
261    autoFill(); // --- NEW: ensure we have enough items to enable scrolling
262  });
263
264  // Interactions
265  $brandsEl.on('click', '.hvacpf-pill', function(){
266    const id = toInt($(this).data('id'));
267    if (id === activeBrand) return;
268    activeBrand = id;
269    activeCat = 0;
270    q = ''; $search.val('');
271    renderBrands();
272    renderCategories();
273    resetGrid();
274      scrollToRoot();
275    fetchPage(1).then(function(){
276      updateURL();
277      autoFill();
278    });
279  });
280
281  $catsEl.on('click', '.hvacpf-pill', function(){
282    const id = toInt($(this).data('id'));
283    if (id === activeCat) return;
284    activeCat = id;
285    q = ''; $search.val('');
286    renderCategories();
287    resetGrid();
288      scrollToRoot();
289    fetchPage(1).then(function(){
290      updateURL();
291      autoFill();
292    });
293  });
294
295  let searchTimer = null;
296
297  // Live search for products
298  $search.on('input', function(){
299    q = $(this).val() || '';
300    clearTimeout(searchTimer);
301    searchTimer = setTimeout(function(){
302      resetGrid();
303      fetchPage(1).then(autoFill);
304    }, 250);
305  }).on('keydown', function(e){
306    if (e.key === 'Enter') e.preventDefault();
307  });
308
309  // --- NEW: IntersectionObserver for robust infinite load
310  if ('IntersectionObserver' in window) {
311    const io = new IntersectionObserver((entries) => {
312      if (loading || !hasMore) return;
313      if (entries.some(e => e.isIntersecting)) {
314        fetchPage(page + 1);
315      }
316    }, { root: null, rootMargin: '300px 0px', threshold: 0 });
317    io.observe($sentinel[0]);
318  }
319
320  // Existing scroll fallback (kept)
321  const preloadOffset = 300;
322  $(window).on('scroll.hvacpf', function(){
323    if (loading || !hasMore) return;
324    const scrollBottom = $(window).scrollTop() + $(window).height();
325    const docHeight    = $(document).height();
326    if (docHeight - scrollBottom < preloadOffset) {
327      fetchPage(page + 1);
328    }
329  });
330    
331    
332       
333    
334var $wrap = $('#hvacpf-product');
335if (!$wrap.length) return;
336
337var $desc = $('#description');
338var $feat = $('#features');
339
340// If tabs/pills were removed, just ensure both are visible and optionally scroll to hash.
341if (!$desc.length && !$feat.length) return;
342
343$desc.show().addClass('is-active');
344$feat.show().addClass('is-active');
345
346// Optional: support direct links with hash without toggling visibility
347var hash = (location.hash || '').toLowerCase();
348if (hash === '#features' && $feat.length) {
349  setTimeout(function(){
350    var top = $feat.offset().top - Math.max(0, headerOffset || 0);
351    $('html, body').stop(true).animate({ scrollTop: top }, 300);
352  }, 0);
353} else if (hash === '#description' && $desc.length) {
354  setTimeout(function(){
355    var top2 = $desc.offset().top - Math.max(0, headerOffset || 0);
356    $('html, body').stop(true).animate({ scrollTop: top2 }, 300);
357  }, 0);
358}
359
360    function showTab(tabKey, doScroll) {
361      // Toggle pills
362      $pills.removeClass('is-active').attr('aria-selected', 'false')
363        .filter('[data-tab="'+tabKey+'"]').addClass('is-active').attr('aria-selected', 'true');
364
365      // Toggle content sections
366      var $desc = $('#description'); // <section id="description" data-tab="desc">
367      var $feat = $('#features');    // <section id="features" data-tab="features">
368      var $target = tabKey === 'features' ? $feat : $desc;
369
370      $desc.removeClass('is-active').hide();
371      $feat.removeClass('is-active').hide();
372      $target.addClass('is-active').show();
373
374      // Update hash without jump
375      if (history.replaceState) {
376        history.replaceState(null, '', tabKey === 'features' ? '#features' : '#description');
377      }
378
379      // Smooth scroll to the section ID (not the buttons)
380      if (doScroll !== false && $target.length) {
381        var top = $target.offset().top - Math.max(0, offset);
382        $('html, body').stop(true).animate({ scrollTop: top }, 300);
383      }
384    }
385
386    // Clicks on the pills
387    $wrap.on('click', '.hvacpf-pills .hvacpf-pill', function (e) {
388      e.preventDefault();
389      var key = $(this).data('tab'); // 'desc' | 'features'
390      showTab(key === 'features' ? 'features' : 'desc', true);
391    });
392
393    // On load: respect hash (#features | #description)
394    var hash = (location.hash || '').toLowerCase();
395    if (hash === '#features') {
396      setTimeout(function(){ showTab('features', true); }, 0);
397    } else if (hash === '#description') {
398      setTimeout(function(){ showTab('desc', true); }, 0);
399    } else {
400      // Initialize without scrolling
401      var initial = $pills.filter('.is-active').data('tab') || 'desc';
402      showTab(initial, false);
403    }
404    
405
406})(window, 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.