← back to Commercialrealestate

public/infinite-scroll.js

97 lines

/* infinite-scroll.js — reusable windowed infinite scroll for the CRCP custom-render
 * grid pages (deals-flow, contractors, broker-grid, direct-listings) that DON'T go
 * through three-view.js (which has its own equivalent windowing built in).
 *
 * Instead of dumping the whole filtered set into the DOM (which froze the heavy
 * pages), reveal BATCH rows and load the next batch as a sentinel scrolls near view.
 *
 * Usage:
 *   const inf = InfScroll.attach({
 *     batch: 120,
 *     getRows:  () => currentFilteredSortedRows,   // full array
 *     getMount: () => document.querySelector(activeContainerSelector), // visible list/grid/table el
 *     render:   (windowRows) => { mount.innerHTML = windowRows.map(...).join(''); },
 *   });
 *   // after ANY filter / sort / view change:
 *   inf.reset();            // resets the window to the first batch and repaints
 *   inf.shownCount();       // how many are currently revealed (for a "showing N of M" label)
 *
 * Degrades to "render everything" where IntersectionObserver is unavailable.
 * Zero deps. $0, local.
 */
(function (global) {
  const IO_OK = typeof IntersectionObserver !== 'undefined';

  function attach(O) {
    const BATCH = O.batch || 120;
    let shown = BATCH;
    let last = [];
    let sentinel = null, io = null, growing = false;

    function ensureSentinel() {
      if (!IO_OK) return null;
      if (!sentinel) {
        sentinel = document.createElement('div');
        sentinel.className = 'inf-sentinel';
        sentinel.setAttribute('aria-hidden', 'true');
        sentinel.style.cssText = 'height:1px;width:100%;';
      }
      return sentinel;
    }
    // Find the element that actually scrolls (the mount itself or an ancestor with an
    // overflow:auto/scroll AND real overflow). Returns null when the WINDOW scrolls — the
    // common case (condos/deals-flow/contractors/direct-listings), where the observer roots
    // to the viewport and the sentinel sits AFTER the mount. Inner-scroll panes (broker-grid's
    // fixed-height #tableView) return that pane so we root the observer to it and drop the
    // sentinel INSIDE it, or window-scroll would never reach the sentinel.
    function findScroller(el) {
      let n = el;
      while (n && n !== document.body && n !== document.documentElement) {
        const cs = getComputedStyle(n);
        if (/(auto|scroll)/.test(cs.overflowY) && n.scrollHeight > n.clientHeight + 4) return n;
        n = n.parentElement;
      }
      return null;
    }
    let curRoot = undefined;   // the IO root we last built for (undefined = none yet)
    function positionSentinel(hasMore) {
      const s = ensureSentinel(); if (!s) return;
      if (!hasMore) { if (s.parentNode) s.parentNode.removeChild(s); if (io) io.unobserve(s); return; }
      const mount = O.getMount && O.getMount();
      if (!mount || !mount.parentNode) return;
      const scroller = findScroller(mount);   // null → window scroll
      // (re)build the observer if the scroll root changed (view switch can change the pane)
      if (scroller !== curRoot) {
        if (io) io.disconnect();
        io = new IntersectionObserver(es => { if (es.some(e => e.isIntersecting)) grow(); }, { root: scroller || null, rootMargin: '600px 0px' });
        curRoot = scroller;
      }
      if (scroller) {
        // inner-scroll pane → sentinel lives at the BOTTOM of the scrolling content
        if (s.parentNode !== scroller || scroller.lastElementChild !== s) scroller.appendChild(s);
      } else {
        // window scroll → sentinel sits right after the mount in normal flow
        if (mount.nextSibling !== s) mount.parentNode.insertBefore(s, mount.nextSibling);
      }
      io.observe(s);
    }
    function paint() {
      last = (O.getRows && O.getRows()) || [];
      if (shown > last.length) shown = Math.max(BATCH, Math.min(shown, last.length));
      const win = IO_OK ? last.slice(0, shown) : last;
      if (O.render) O.render(win);
      positionSentinel(IO_OK && shown < last.length);
      if (O.onPaint) O.onPaint(Math.min(shown, last.length), last.length);
    }
    function grow() {
      if (growing || shown >= last.length) return;
      growing = true; shown = Math.min(shown + BATCH, last.length); paint(); growing = false;
    }
    function reset() { shown = BATCH; paint(); }

    return { reset, paint, grow, shownCount: () => Math.min(shown, last.length), total: () => last.length };
  }

  global.InfScroll = { attach };
})(window);