PageSourceSearch

https://www.ecoflix.com/assets/panels.js

js ecoflix.com collected 2026-09-24 19:52:54 UTC 9,794 bytes, 234 lines download raw bytes

1/* =====================================================================
2   Expanding-panels engine for the project carousels
3   ---------------------------------------------------------------------
4   Turns #hscrollTrack into a row of expanding panels (accordion gallery):
5   click a collapsed panel to expand it; click the already-expanded panel
6   (or its CTA) to open the project. Replaces the old fan-stack 3D engine
7   while preserving filters, Grid/List toggle, Saved, dots/counter,
8   prev/next and deep links.
9
10   Loaded on: index.html, projects/index.html, design/index.html
11   ===================================================================== */
12(function () {
13  'use strict';
14
15  function init(stage) {
16    if (!stage || stage.__pcsPanels) return;
17    var allCards = Array.prototype.slice.call(stage.querySelectorAll('.proj-card'));
18    if (!allCards.length) return;
19    stage.__pcsPanels = true;
20    stage.classList.add('pcs-panels');
21
22    // --- Optional controls (present on the homepage / projects page) ---
23    var wrap = stage.closest('.pcs-wrap') ||
24               document.getElementById('hpProjectsCarousel') ||
25               stage.parentElement;
26    var prevBtn = wrap && wrap.querySelector('.pcs-btn-prev');
27    var nextBtn = wrap && wrap.querySelector('.pcs-btn-next');
28    var dotsEl  = document.getElementById('hpPcsDots') || (wrap && wrap.querySelector('.pcs-dots'));
29    var counter = document.getElementById('hpPcsCounter') || (wrap && wrap.querySelector('.pcs-counter'));
30
31    // Slugs whose project-page folder name differs from the card id.
32    var slugMap = {
33      'wolf-connection': 'wolf-connection-sanctuary',
34      'wildlife-on-the-edge': 'wildlife-on-the-edge-bears',
35      'going-wild': 'going-wild-ranger-training'
36    };
37    function resolveHref(card) {
38      if (card.dataset.href) return card.dataset.href;
39      var id = card.dataset.id;
40      if (id) return '/projects/' + (slugMap[id] || id) + '/';
41      return null;
42    }
43
44    function pad2(n) { return String(n).padStart(2, '0'); }
45
46    // --- Normalise each card for panel layout --------------------------
47    allCards.forEach(function (card) {
48      // Wipe any inline styles left by the (now-removed) fan engine.
49      card.style.transform = '';
50      card.style.opacity = '';
51      card.style.zIndex = '';
52      card.style.pointerEvents = '';
53      card.style.display = '';
54      card.setAttribute('role', 'link');
55      if (!card.hasAttribute('tabindex')) card.setAttribute('tabindex', '0');
56      var href = resolveHref(card);
57      if (href) card.dataset.href = href;
58      // Vertical mini-label for the collapsed sliver.
59      if (!card.querySelector('.pcs-vlabel')) {
60        var v = document.createElement('span');
61        v.className = 'pcs-vlabel';
62        v.setAttribute('aria-hidden', 'true');
63        var h3 = card.querySelector('.meta h3, h3');
64        v.textContent = card.dataset.name || (h3 ? h3.textContent.trim() : '');
65        card.appendChild(v);
66      }
67    });
68
69    var visible = [];
70    var active = 0;
71
72    function recollect() {
73      visible = allCards.filter(function (c) { return !c.classList.contains('is-filtered-out'); });
74      if (active >= visible.length) active = Math.max(0, visible.length - 1);
75    }
76
77    function render(scrollToActive) {
78      recollect();
79      allCards.forEach(function (c) { c.classList.remove('is-active'); });
80      visible.forEach(function (c, i) { c.style.setProperty('--pcs-i', i); });
81
82      var act = visible[active];
83      if (act) act.classList.add('is-active');
84
85      if (counter) {
86        counter.innerHTML = visible.length
87          ? '<b>' + pad2(active + 1) + '</b> / ' + pad2(visible.length)
88          : '<b>00</b> / 00';
89      }
90
91      if (dotsEl) {
92        if (dotsEl.children.length !== visible.length) {
93          dotsEl.innerHTML = '';
94          visible.forEach(function (_, i) {
95            var b = document.createElement('button');
96            b.type = 'button';
97            b.className = 'pcs-dot' + (i === active ? ' is-on' : '');
98            b.setAttribute('aria-label', 'Show project ' + (i + 1));
99            b.addEventListener('click', function () { active = i; render(true); });
100            dotsEl.appendChild(b);
101          });
102        } else {
103          Array.prototype.forEach.call(dotsEl.children, function (d, i) {
104            d.classList.toggle('is-on', i === active);
105          });
106        }
107      }
108
109      if (prevBtn) prevBtn.disabled = visible.length < 2;
110      if (nextBtn) nextBtn.disabled = visible.length < 2;
111
112      if (scrollToActive && act && typeof act.scrollIntoView === 'function') {
113        act.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
114      }
115    }
116
117    function setActiveCard(card) {
118      recollect();
119      var i = visible.indexOf(card);
120      if (i >= 0 && i !== active) { active = i; render(true); return true; }
121      return false;
122    }
123    function goPrev() { if (visible.length < 2) return; active = (active - 1 + visible.length) % visible.length; render(true); }
124    function goNext() { if (visible.length < 2) return; active = (active + 1) % visible.length; render(true); }
125
126    // --- Click: expand a collapsed panel, or open the active one -------
127    // Capture phase so we run before any legacy per-card navigate handler.
128    stage.addEventListener('click', function (e) {
129      var card = e.target.closest('.proj-card');
130      if (!card || !stage.contains(card)) return;
131      // Inner links / buttons (leaf CTA, save heart) keep their own behaviour.
132      if (e.target.closest('a, button')) return;
133      // Swallow the click that ends a swipe.
134      if ((window.__pcsDragMoved || 0) > 8) { window.__pcsDragMoved = 0; return; }
135
136      if (!card.classList.contains('is-active')) {
137        e.preventDefault();
138        e.stopImmediatePropagation();
139        setActiveCard(card);
140      } else {
141        var dest = card.dataset.href;
142        if (dest) {
143          e.preventDefault();
144          e.stopImmediatePropagation();
145          window.location.href = dest;
146        }
147      }
148    }, true);
149
150    // --- Hover to expand on precise pointers (desktop) -----------------
151    if (window.matchMedia && window.matchMedia('(hover:hover) and (pointer:fine)').matches) {
152      allCards.forEach(function (card) {
153        card.addEventListener('mouseenter', function () {
154          if (!card.classList.contains('is-filtered-out')) setActiveCard(card);
155        });
156      });
157    }
158
159    // --- Keyboard ------------------------------------------------------
160    stage.addEventListener('keydown', function (e) {
161      if (e.key === 'ArrowLeft') { e.preventDefault(); goPrev(); }
162      else if (e.key === 'ArrowRight') { e.preventDefault(); goNext(); }
163      else if (e.key === 'Enter' || e.key === ' ') {
164        var c = visible[active];
165        if (c && c.dataset.href) { e.preventDefault(); window.location.href = c.dataset.href; }
166      }
167    });
168
169    // --- Drag / swipe moves the active panel ---------------------------
170    var dragging = null;
171    stage.addEventListener('pointerdown', function (e) {
172      if (e.button && e.button !== 0) return;
173      if (e.target.closest('.pcs-btn, .pcs-dot, a, button')) return;
174      dragging = { x: e.clientX, moved: 0 };
175    });
176    stage.addEventListener('pointermove', function (e) {
177      if (!dragging) return;
178      dragging.moved = Math.max(dragging.moved, Math.abs(e.clientX - dragging.x));
179    });
180    stage.addEventListener('pointerup', function (e) {
181      if (!dragging) return;
182      var dx = e.clientX - dragging.x;
183      window.__pcsDragMoved = dragging.moved;
184      setTimeout(function () { window.__pcsDragMoved = 0; }, 250);
185      var threshold = 55;
186      if (dx > threshold) goPrev();
187      else if (dx < -threshold) goNext();
188      dragging = null;
189    });
190    stage.addEventListener('pointercancel', function () { dragging = null; });
191
192    if (prevBtn) prevBtn.addEventListener('click', goPrev);
193    if (nextBtn) nextBtn.addEventListener('click', goNext);
194
195    // --- Filters: the page's own chip handler toggles .is-filtered-out;
196    //     we just recollect + reset to the first match afterwards. -------
197    document.querySelectorAll('.proj-chip').forEach(function (chip) {
198      chip.addEventListener('click', function () {
199        setTimeout(function () { active = 0; render(false); }, 0);
200      });
201    });
202
203    // Programmatic re-render hook. site.js dispatches 'pcs:refilter' on the
204    // stage after applying a deep-link filter (e.g. ?ecosystem=oceans),
205    // whose toggles land after our own chip listeners are bound; recollect
206    // the now-visible cards and reset to the first match.
207    stage.addEventListener('pcs:refilter', function () { active = 0; render(false); });
208
209    // --- View toggle: list view is handled by CSS (:not(.view-list));
210    //     re-render when we come back to grid. ----------------------------
211    document.querySelectorAll('.view-toggle button').forEach(function (b) {
212      b.addEventListener('click', function () { setTimeout(function () { render(false); }, 0); });
213    });
214
215    render(false);
216
217    // Enable transitions only after the initial layout has painted, so the
218    // first collapse/expand snaps into place instead of animating from the
219    // pre-panels card size (which can freeze off-screen). Use rAF for the
220    // common case, plus a timeout fallback for when rAF is paused (the tab
221    // is hidden / loaded in the background).
222    function markReady() { stage.classList.add('pcs-ready'); }
223    requestAnimationFrame(function () { requestAnimationFrame(markReady); });
224    setTimeout(markReady, 120);
225  }
226
227  function boot() { init(document.getElementById('hscrollTrack')); }
228
229  if (document.readyState === 'loading') {
230    document.addEventListener('DOMContentLoaded', boot);
231  } else {
232    boot();
233  }
234})();

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.