PageSourceSearch

https://dronexl.co/site.js?v=6327ab1ceb

js dronexl.co collected 2026-10-02 03:25:13 UTC 10,576 bytes, 270 lines download raw bytes

1// EVXL — site behaviors (theme toggle, mobile drawer, sticky CTA)
2(function () {
3  // ---- Theme (light/dark) ----
4  var STORAGE_KEY = 'dxl-theme';
5  function setTheme(t) {
6    document.documentElement.setAttribute('data-theme', t);
7    try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {}
8  }
9  function getTheme() {
10    try { var t = localStorage.getItem(STORAGE_KEY); if (t) return t; } catch (e) {}
11    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
12  }
13  // apply ASAP to avoid FOUC (the inline script in <head> usually does this;
14  // we set again here in case the page didn't include one)
15  if (!document.documentElement.getAttribute('data-theme')) setTheme(getTheme());
16
17  document.addEventListener('click', function (e) {
18    var t = e.target.closest('[data-toggle="theme"]');
19    if (t) {
20      e.preventDefault();
21      var cur = document.documentElement.getAttribute('data-theme');
22      setTheme(cur === 'dark' ? 'light' : 'dark');
23    }
24  });
25
26  // ---- Mobile drawer ----
27  function setDrawer(open) {
28    var d = document.getElementById('drawer');
29    if (!d) return;
30    if (open) {
31      d.classList.add('open');
32      d.setAttribute('aria-hidden', 'false');
33      document.body.style.overflow = 'hidden';
34      // move focus into the dialog for keyboard/screen-reader users
35      var c = d.querySelector('[data-toggle="drawer-close"]');
36      if (c) c.focus();
37    } else {
38      var wasOpen = d.classList.contains('open');
39      // return focus to the menu button BEFORE hiding, so focus never
40      // sits inside an aria-hidden subtree
41      if (wasOpen && d.contains(document.activeElement)) {
42        var m = document.querySelector('[data-toggle="drawer-open"]');
43        if (m) m.focus();
44      }
45      d.classList.remove('open');
46      d.setAttribute('aria-hidden', 'true');
47      document.body.style.overflow = '';
48    }
49  }
50  document.addEventListener('click', function (e) {
51    if (e.target.closest('[data-toggle="drawer-open"]')) { e.preventDefault(); setDrawer(true); }
52    else if (e.target.closest('[data-toggle="drawer-close"]')) { e.preventDefault(); setDrawer(false); }
53    else if (e.target.id === 'drawer') { setDrawer(false); }
54  });
55  document.addEventListener('keydown', function (e) {
56    if (e.key === 'Escape') setDrawer(false);
57  });
58
59  // ---- Sticky CTA dismiss ----
60  var STICKY_KEY = 'dxl-cta-dismissed';
61  function initCta() {
62    var cta = document.querySelector('.mobile-cta');
63    if (!cta) return;
64    try {
65      if (sessionStorage.getItem(STICKY_KEY) === '1') { cta.style.display = 'none'; return; }
66    } catch (e) {}
67    document.body.classList.add('has-mobile-cta');
68    var dismiss = cta.querySelector('.dismiss');
69    if (dismiss) dismiss.addEventListener('click', function () {
70      cta.style.display = 'none';
71      document.body.classList.remove('has-mobile-cta');
72      try { sessionStorage.setItem(STICKY_KEY, '1'); } catch (e) {}
73    });
74  }
75
76  // ---- Image fade-in (real photos overlaying .ph skeletons) ----
77  function initImages() {
78    var imgs = document.querySelectorAll('.ph img.cover');
79    imgs.forEach(function (img) {
80      if (img.complete && img.naturalWidth) {
81        img.classList.add('loaded');
82      } else {
83        img.addEventListener('load', function () { img.classList.add('loaded'); });
84        img.addEventListener('error', function () { img.remove(); });
85      }
86    });
87  }
88
89  // ---- Reading progress bar (article pages) ----
90  function initReadProgress() {
91    var bar = document.querySelector('.read-progress span');
92    if (!bar) return;
93    var ticking = false;
94    function update() {
95      var h = document.documentElement;
96      var max = (h.scrollHeight - h.clientHeight);
97      var pct = max > 0 ? Math.min(100, Math.max(0, (h.scrollTop / max) * 100)) : 0;
98      bar.style.width = pct + '%';
99      ticking = false;
100    }
101    window.addEventListener('scroll', function () {
102      if (!ticking) { requestAnimationFrame(update); ticking = true; }
103    }, { passive: true });
104    update();
105  }
106
107  // ---- Ticker: render from data + persist scroll across pages ----
108  var TICKER_ANCHOR_KEY = 'dxl-ticker-anchor';
109  var TICKER_DURATION = 90; // seconds (matches CSS .ticker-track animation)
110  function initTicker() {
111    var track = document.querySelector('.ticker-track');
112    if (!track) return;
113    // Render items from window.DXL_DEALS if present (deals.js loaded before this).
114    var deals = window.DXL_DEALS;
115    if (deals && deals.length) {
116      var html = '';
117      // duplicate the list for seamless loop
118      var loop = deals.concat(deals);
119      for (var i = 0; i < loop.length; i++) {
120        var d = loop[i];
121        var tone = d.tone || 'mut';
122        html +=
123          '<a class="ticker-item" href="' + (d.url || '#') +
124          '" target="_blank" rel="sponsored noopener">' +
125          '<span class="sym">' + d.brand + '</span> ' +
126          '<span>' + d.product + '</span> ' +
127          '<span class="' + tone + '">' + d.note + '</span>' +
128          '</a>';
129      }
130      track.innerHTML = html;
131    }
132    // Persist animation timing across pages via a single anchor timestamp.
133    var anchor;
134    try { anchor = Number(localStorage.getItem(TICKER_ANCHOR_KEY)); } catch (e) {}
135    if (!anchor || isNaN(anchor)) {
136      anchor = Date.now();
137      try { localStorage.setItem(TICKER_ANCHOR_KEY, String(anchor)); } catch (e) {}
138    }
139    var elapsed = (Date.now() - anchor) / 1000;
140    var offset = elapsed % TICKER_DURATION;
141    track.style.animationDelay = '-' + offset + 's';
142  }
143
144  function init() {
145    initCta();
146    initImages();
147    initReadProgress();
148    initTicker();
149  }
150  if (document.readyState !== 'loading') init();
151  else document.addEventListener('DOMContentLoaded', init);
152})();
153
154
155// ---- The Wire: beat filter tabs (any page with [data-wire-filter]) ----
156(function () {
157  var nav = document.querySelector('[data-wire-filter]');
158  if (!nav) return;
159  var rows = Array.prototype.slice.call(document.querySelectorAll('.wire-row:not(.head)'));
160  nav.addEventListener('click', function (e) {
161    var a = e.target.closest('a[data-filter]');
162    if (!a) return;
163    e.preventDefault();
164    Array.prototype.forEach.call(nav.querySelectorAll('a'), function (x) { x.classList.remove('active'); });
165    a.classList.add('active');
166    var f = a.getAttribute('data-filter');
167    var tags = f ? f.split(',') : null;
168    rows.forEach(function (r) {
169      var tagEl = r.querySelector('.tag');
170      var t = tagEl ? tagEl.textContent.trim() : '';
171      r.style.display = (!tags || tags.indexOf(t) !== -1) ? '' : 'none';
172    });
173  });
174})();
175
176// ---- Category: sort control (#cat-sort) ----
177(function () {
178  var sel = document.getElementById('cat-sort');
179  var list = document.querySelector('.cat-list');
180  if (!sel || !list) return;
181  var items = Array.prototype.slice.call(list.querySelectorAll('.item'));
182  function dateOf(item) {
183    // production: items carry an ISO timestamp (works across years / brands)
184    var iso = item.getAttribute('data-date');
185    if (iso) {
186      var di = new Date(iso);
187      if (!isNaN(di.getTime())) return di.getTime();
188    }
189    var spans = item.querySelectorAll('.byline span');
190    var txt = spans.length ? spans[spans.length - 1].textContent : '';
191    var d = new Date(txt + ' 2026');
192    return isNaN(d.getTime()) ? 0 : d.getTime();
193  }
194  function titleOf(item) {
195    var h = item.querySelector('h3');
196    return h ? h.textContent.trim().toLowerCase() : '';
197  }
198  sel.addEventListener('change', function () {
199    var mode = sel.value;
200    var sorted = items.slice().sort(function (a, b) {
201      if (mode === 'Oldest') return dateOf(a) - dateOf(b);
202      if (mode.indexOf('Title') === 0) return titleOf(a) < titleOf(b) ? -1 : 1;
203      return dateOf(b) - dateOf(a);
204    });
205    sorted.forEach(function (el) { list.appendChild(el); });
206  });
207})();
208
209// ---- Category: search preview (?q=) ----
210(function () {
211  var hero = document.querySelector('.cat-hero');
212  if (!hero) return;
213  var q = null;
214  try { q = new URLSearchParams(location.search).get('q'); } catch (e) {}
215  if (!q) return;
216  var h1 = hero.querySelector('h1');
217  var deck = hero.querySelector('.deck');
218  var crumb = hero.querySelector('.crumbs span');
219  if (crumb) crumb.textContent = 'Search';
220  if (h1) h1.textContent = '\u201C' + q + '\u201D';
221  if (deck) deck.textContent = 'Every story matching your search, ranked by relevance.';
222})();
223
224// ---- Newsletter forms (form[data-subscribe] → data-endpoint, e.g. Beehiiv) ----
225// Brand-agnostic: without an endpoint the button flips to SUBSCRIBED ✓ (the
226// design's placeholder behavior); with one it POSTs { email } as JSON.
227(function () {
228  document.addEventListener('submit', function (e) {
229    var form = e.target && e.target.closest ? e.target.closest('form[data-subscribe]') : null;
230    if (!form) return;
231    e.preventDefault();
232    var input = form.querySelector('input[type="email"]');
233    var btn = form.querySelector('button');
234    var email = input && input.value ? input.value.trim() : '';
235    if (!email) { if (input) input.focus(); return; }
236    var endpoint = form.getAttribute('data-endpoint');
237    if (!endpoint) { if (btn) btn.textContent = 'SUBSCRIBED ✓'; return; }
238    if (btn) { btn.disabled = true; btn.textContent = '…'; }
239    fetch(endpoint, {
240      method: 'POST',
241      headers: { 'Content-Type': 'application/json' },
242      body: JSON.stringify({ email: email })
243    }).then(function (res) {
244      if (!res.ok) throw new Error('subscribe failed');
245      if (btn) { btn.disabled = false; btn.textContent = 'SUBSCRIBED ✓'; }
246      if (input) input.value = '';
247    }).catch(function () {
248      if (btn) { btn.disabled = false; btn.textContent = 'TRY AGAIN ↻'; }
249    });
250  });
251})();
252
253// ---- Share buttons ([data-share]) — native share sheet, clipboard fallback ----
254(function () {
255  document.addEventListener('click', function (e) {
256    var b = e.target && e.target.closest ? e.target.closest('[data-share]') : null;
257    if (!b) return;
258    e.preventDefault();
259    var url = location.href.split('#')[0];
260    var title = b.getAttribute('data-share-title') || document.title;
261    if (navigator.share) { navigator.share({ title: title, url: url }).catch(function () {}); return; }
262    if (navigator.clipboard && navigator.clipboard.writeText) {
263      navigator.clipboard.writeText(url).then(function () {
264        var t = b.getAttribute('title');
265        b.setAttribute('title', 'Link copied');
266        setTimeout(function () { b.setAttribute('title', t || 'Share'); }, 1500);
267      });
268    }
269  });
270})();

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.