← back to NationalPaperHangers

public/js/find-filter.js

114 lines

/**
 * find-filter.js — debounced fetch-on-change for /find filters
 * Intercepts form changes + submission, fetches rendered HTML from the server,
 * replaces .installer-grid and .result-count with updated content via DOMParser.
 * Uses history.pushState so the URL stays bookmarkable.
 */
(function () {
  'use strict';

  var form = document.querySelector('.find-filters');
  if (!form) return;

  var grid = document.querySelector('.installer-grid');
  var resultCount = document.querySelector('.result-count');
  var debounceTimer = null;
  var currentController = null;

  function buildUrl() {
    var params = new URLSearchParams(new FormData(form));
    // remove blank values to keep URLs clean
    for (var key of Array.from(params.keys())) {
      if (!params.get(key)) params.delete(key);
    }
    return '/find?' + params.toString();
  }

  function showLoading() {
    if (grid) {
      grid.style.opacity = '0.45';
      grid.style.pointerEvents = 'none';
    }
    if (resultCount) {
      resultCount.dataset.original = resultCount.textContent;
      resultCount.textContent = 'Loading…';
    }
  }

  function hideLoading() {
    if (grid) {
      grid.style.opacity = '';
      grid.style.pointerEvents = '';
    }
  }

  function doFetch() {
    var url = buildUrl();

    // abort any in-flight request
    if (currentController) currentController.abort();
    currentController = new AbortController();

    showLoading();
    history.pushState(null, '', url);

    fetch(url, {
      headers: { 'Accept': 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
      signal: currentController.signal
    })
      .then(function (res) {
        if (!res.ok) throw new Error('Bad response: ' + res.status);
        return res.text();
      })
      .then(function (html) {
        var parser = new DOMParser();
        var doc = parser.parseFromString(html, 'text/html');

        var newGrid = doc.querySelector('.installer-grid');
        var newCount = doc.querySelector('.result-count');

        if (newGrid && grid) {
          grid.innerHTML = newGrid.innerHTML;
        }
        if (newCount && resultCount) {
          resultCount.textContent = newCount.textContent;
        }
        hideLoading();
      })
      .catch(function (err) {
        if (err.name === 'AbortError') return; // superseded by newer request
        hideLoading();
        if (resultCount && resultCount.dataset.original) {
          resultCount.textContent = resultCount.dataset.original;
        }
        console.warn('[find-filter] fetch error:', err);
      });
  }

  function scheduleSearch() {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(doFetch, 300);
  }

  // Intercept native submit
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    clearTimeout(debounceTimer);
    doFetch();
  });

  // Debounce on any input or select change
  form.addEventListener('input', scheduleSearch);
  form.addEventListener('change', scheduleSearch);

  // Handle browser back/forward
  window.addEventListener('popstate', function () {
    // Re-sync form from URL then fetch
    var params = new URLSearchParams(window.location.search);
    form.querySelectorAll('input, select').forEach(function (el) {
      if (el.name) el.value = params.get(el.name) || '';
    });
    doFetch();
  });
})();