PageSourceSearch

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

js ecoflix.com collected 2026-09-24 19:52:55 UTC 9,890 bytes, 189 lines download raw bytes

1/* Ecoflix header navigation — single source of truth (the dropdown menu).
2   Loaded on EVERY page so the top nav is identical everywhere. */
3(function navRewrite(){
4  const nav = document.querySelector('header.header .nav');
5  if (!nav) return;
6
7  // Self-contained dropdown styling so the menu renders identically on EVERY
8  // page, whether or not it loads site.css (the homepage, for example, doesn't).
9  if (!document.getElementById('eco-nav-css')){
10    const css = document.createElement('style');
11    css.id = 'eco-nav-css';
12    css.textContent = ".nav-enhanced{display:none;align-items:center;gap:6px}.nav-group{position:relative;display:inline-flex}"
13      // Top-level flat links styled IDENTICALLY to dropdown triggers: same weight,
14      // size, padding and hover pill, so every top-level item matches and spaces evenly.
15      + ".nav-enhanced>a{font-family:var(--body,inherit);font-size:14px;font-weight:600;color:var(--mist,#9aa);padding:8px 14px;border-radius:99px;display:inline-flex;align-items:center;text-decoration:none;transition:color .2s,background .2s}"
16      + ".nav-enhanced>a:hover{color:var(--bone,#fff);background:rgba(255,255,255,.04)}"
17      + ".nav-enhanced>a[aria-current='page']{color:var(--leaf,#A8F341)}"
18      + ".nav-trigger{appearance:none;border:0;background:none;cursor:pointer;font-family:var(--body,inherit);font-size:14px;font-weight:600;color:var(--mist,#9aa);padding:8px 14px;border-radius:99px;display:inline-flex;align-items:center;gap:6px;transition:color .2s,background .2s}"
19      + ".nav-trigger .nav-chev{font-size:10px;color:var(--mist-2,#778);transition:transform .25s ease,color .2s;display:inline-block;line-height:1;margin-top:1px}"
20      + ".nav-trigger:hover{color:var(--bone,#fff);background:rgba(255,255,255,.04)}.nav-trigger:hover .nav-chev{color:var(--leaf,#A8F341)}"
21      + ".nav-trigger:focus-visible{outline:2px solid var(--leaf,#A8F341);outline-offset:2px;color:var(--bone,#fff)}"
22      + ".nav-group.is-open .nav-trigger{color:var(--bone,#fff);background:rgba(168,243,65,.08)}.nav-group.is-open .nav-trigger .nav-chev{transform:rotate(180deg);color:var(--leaf,#A8F341)}"
23      + ".nav-trigger.is-active-section,.nav-trigger.is-active-section .nav-chev{color:var(--leaf,#A8F341)}"
24      + ".nav-menu{position:absolute;top:calc(100% + 12px);left:0;z-index:50;min-width:240px;padding:10px 8px;border-radius:14px;background:rgba(7,13,35,.94);border:1px solid rgba(168,243,65,.22);box-shadow:0 24px 60px -16px rgba(0,0,0,.65),0 8px 20px -6px rgba(0,0,0,.45);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);display:flex;flex-direction:column;gap:2px}"
25      + ".nav-menu::before{content:'';position:absolute;left:18px;right:18px;top:0;height:1px;background:linear-gradient(90deg,transparent,rgba(168,243,65,.6),transparent)}"
26      + ".nav-menu[hidden]{display:none}"
27      + ".nav-menu a{display:flex;align-items:center;padding:9px 14px;border-radius:9px;font-size:13.5px;color:var(--bone,#fff);text-decoration:none;transition:background .15s,color .15s}"
28      + ".nav-menu a:hover,.nav-menu a:focus-visible{background:rgba(168,243,65,.10);color:var(--leaf,#A8F341);outline:0}"
29      + ".nav-menu a[aria-current='page']{background:rgba(168,243,65,.14);color:var(--leaf,#A8F341)}"
30      + ".nav-group:nth-last-of-type(-n+2) .nav-menu{left:auto;right:0}"
31      // The desktop links only appear at >=1024px (matching .nav in site.css /
32      // the homepage's inline CSS); below that the hamburger menu takes over.
33      // Without this guard the enhanced nav forced display:flex at every width,
34      // so the full link row showed (and overflowed) on mobile next to the burger.
35      + "@media (min-width:1024px){.nav-enhanced{display:flex}}"
36      + "@media (max-width:1180px){.nav-enhanced{gap:4px}.nav-trigger{padding:8px 10px}}";
37    document.head.appendChild(css);
38  }
39
40  // Note: `key` fields are i18n translation keys read by /assets/i18n.js.
41  // The English `label` is the fallback shown if i18n.js doesn't load
42  // or doesn't recognise the key.
43  const NAV = [
44    { type:'link', href:'/watch/',    label:'Watch',    key:'nav.watch' },
45    { type:'link', href:'/podcasts/', label:'Listen' },
46    { type:'group', label:'Read', items:[
47      { href:'/news/',     label:'News',     key:'nav.news' },
48      { href:'/features/', label:'Features' },
49    ]},
50    { type:'group', label:'Projects', items:[
51      { href:'/projects/',      label:'All projects',  key:'nav.projects' },
52      { href:'/ngo-partners/',  label:'NGO partners' },
53      { href:'/impact-report/', label:'Impact report' },
54    ]},
55    { type:'group', label:'About', key:'nav.about', items:[
56      { href:'/mission/',      label:'Mission',     key:'nav.mission' },
57      { href:'/how-it-works/', label:'How it works' },
58      { href:'/how-we-help/',  label:'How we help', key:'nav.how_we_help' },
59      { href:'/history/',      label:'Our story' },
60      { href:'/team/',         label:'Team' },
61      { href:'/connect/',      label:'Connect' },
62      { href:'/faq/',          label:'FAQ' },
63    ]},
64    { type:'group', label:'Business', key:'nav.business', items:[
65      { href:'/distribution/',      label:'Distribution & ads' },
66      { href:'/partners/',          label:'Corporate partners' },
67      { href:'/news/',              label:'Newsroom' },
68      { href:'/contact/',           label:'Contact BD desk' },
69    ]},
70    { type:'group', label:'Change', key:'nav.give', items:[
71      { href:'/donate/',            label:'Donate',           key:'cta.donate' },
72      { href:'/circle-of-giving/',  label:'Circle of Giving' },
73      { href:'/gift-memberships/',  label:'Gift memberships' },
74      { href:'/partners/',          label:'Become a partner' },
75      { href:'/teachers/',          label:'For teachers' },
76    ]},
77  ];
78
79  // Resolve current path → mark matching item aria-current="page".
80  const currentPath = location.pathname.replace(/\/index\.html$/, '/');
81  function isCurrent(href){
82    if (!href || href.startsWith('http')) return false;
83    return href === currentPath || (href !== '/' && currentPath.startsWith(href));
84  }
85
86  // Build the new markup.
87  const frag = document.createDocumentFragment();
88  let groupId = 0;
89  NAV.forEach(entry => {
90    if (entry.type === 'link'){
91      const a = document.createElement('a');
92      a.href = entry.href;
93      a.textContent = entry.label;
94      if (entry.key) a.setAttribute('data-i18n', entry.key);
95      if (entry.external){ a.setAttribute('rel','noopener'); }
96      if (isCurrent(entry.href)) a.setAttribute('aria-current','page');
97      frag.appendChild(a);
98      return;
99    }
100    // group
101    const wrap = document.createElement('div');
102    wrap.className = 'nav-group';
103    const id = 'navmenu-' + (++groupId);
104
105    const btn = document.createElement('button');
106    btn.type = 'button';
107    btn.className = 'nav-trigger';
108    btn.setAttribute('aria-expanded','false');
109    btn.setAttribute('aria-controls', id);
110    // Label span gets the data-i18n; chev preserved as a sibling for the
111    // translation pass which only replaces the first text node.
112    const labelSpan = entry.key
113      ? '<span data-i18n="' + entry.key + '">' + entry.label + '</span>'
114      : entry.label;
115    btn.innerHTML = labelSpan + '<span class="nav-chev" aria-hidden="true">▾</span>';
116    wrap.appendChild(btn);
117
118    const menu = document.createElement('div');
119    menu.className = 'nav-menu';
120    menu.id = id;
121    menu.setAttribute('role','menu');
122    menu.hidden = true;
123    entry.items.forEach(item => {
124      const a = document.createElement('a');
125      a.href = item.href;
126      a.textContent = item.label;
127      a.setAttribute('role','menuitem');
128      if (item.key) a.setAttribute('data-i18n', item.key);
129      if (isCurrent(item.href)){
130        a.setAttribute('aria-current','page');
131        // Reflect on trigger so the whole group reads as active.
132        btn.classList.add('is-active-section');
133      }
134      menu.appendChild(a);
135    });
136    wrap.appendChild(menu);
137
138    // Open/close on trigger click.
139    btn.addEventListener('click', e => {
140      e.stopPropagation();
141      const isOpen = btn.getAttribute('aria-expanded') === 'true';
142      closeAll();
143      if (!isOpen){
144        btn.setAttribute('aria-expanded','true');
145        menu.hidden = false;
146        wrap.classList.add('is-open');
147      }
148    });
149    // Keyboard: ArrowDown/Enter/Space opens & focuses first item.
150    btn.addEventListener('keydown', e => {
151      if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' '){
152        e.preventDefault();
153        btn.click();
154        const first = menu.querySelector('a');
155        if (first) first.focus();
156      }
157    });
158    menu.addEventListener('keydown', e => {
159      const links = Array.from(menu.querySelectorAll('a'));
160      const idx = links.indexOf(document.activeElement);
161      if (e.key === 'Escape'){ closeAll(); btn.focus(); }
162      else if (e.key === 'ArrowDown'){ e.preventDefault(); links[(idx + 1) % links.length].focus(); }
163      else if (e.key === 'ArrowUp')  { e.preventDefault(); links[(idx - 1 + links.length) % links.length].focus(); }
164      else if (e.key === 'Tab' && !e.shiftKey && idx === links.length - 1){ closeAll(); }
165    });
166
167    frag.appendChild(wrap);
168  });
169
170  // Replace existing nav contents.
171  nav.innerHTML = '';
172  nav.appendChild(frag);
173  nav.classList.add('nav-enhanced');
174
175  // Outside click / Escape close.
176  function closeAll(){
177    nav.querySelectorAll('.nav-group.is-open').forEach(g => {
178      g.classList.remove('is-open');
179      const t = g.querySelector('.nav-trigger');
180      const m = g.querySelector('.nav-menu');
181      if (t) t.setAttribute('aria-expanded','false');
182      if (m) m.hidden = true;
183    });
184  }
185  document.addEventListener('click', e => {
186    if (!nav.contains(e.target)) closeAll();
187  });
188  document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAll(); });
189})();

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.