PageSourceSearch

https://www.dontfail.ai/staticforms.js?v=20260823

js dontfail.ai collected 2026-10-05 14:59:47 UTC 22,179 bytes, 566 lines download raw bytes

1(() => {
2  'use strict';
3
4  const staticFormsEndpoint = 'https://api.staticforms.dev/submit';
5
6  // Build a consistent, accessible mobile menu from the site's primary links.
7  // Some pages already contain a hamburger button while others do not, so this
8  // routine upgrades the existing control where present and creates it where needed.
9  function initialiseMobileNavigation() {
10    const nav = document.querySelector('nav');
11    if (!nav || nav.querySelector('#dontfail-mobile-menu')) return;
12
13    const primaryHrefs = [
14      'index.html',
15      'episodes.html',
16      'hall-of-fame.html',
17      'tools-hub.html'
18    ];
19
20    const labels = {
21      'index.html': 'Home',
22      'episodes.html': 'Episodes',
23      'hall-of-fame.html': 'Hall of Fame',
24      'tools-hub.html': 'Prevention Tools'
25    };
26
27    const navRow = Array.from(nav.querySelectorAll('div')).find((element) =>
28      element.classList.contains('flex') &&
29      element.classList.contains('justify-between') &&
30      element.classList.contains('items-center')
31    );
32    if (!navRow) return;
33
34    let toggleButton = Array.from(nav.querySelectorAll('button')).find((button) => {
35      const srOnly = button.querySelector('.sr-only');
36      return srOnly && /open menu/i.test(srOnly.textContent || '');
37    });
38
39    if (!toggleButton) {
40      const toggleWrapper = document.createElement('div');
41      toggleWrapper.className = '-mr-2 flex items-center sm:hidden';
42      toggleWrapper.innerHTML = `
43        <button type="button" class="text-gray-200 hover:text-white p-2" aria-label="Open menu">
44          <span class="sr-only">Open menu</span>
45          <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
46            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
47          </svg>
48        </button>`;
49      navRow.appendChild(toggleWrapper);
50      toggleButton = toggleWrapper.querySelector('button');
51    }
52
53    toggleButton.type = 'button';
54    toggleButton.setAttribute('aria-controls', 'dontfail-mobile-menu');
55    toggleButton.setAttribute('aria-expanded', 'false');
56    if (!toggleButton.getAttribute('aria-label')) toggleButton.setAttribute('aria-label', 'Open menu');
57    const toggleIcon = toggleButton.querySelector('svg');
58    if (toggleIcon) toggleIcon.setAttribute('aria-hidden', 'true');
59
60    const mobileMenu = document.createElement('div');
61    mobileMenu.id = 'dontfail-mobile-menu';
62    mobileMenu.className = 'hidden sm:hidden border-t border-white/10 bg-navy';
63
64    const menuInner = document.createElement('div');
65    menuInner.className = 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 space-y-1';
66
67    const currentPage = (window.location.pathname.split('/').pop() || 'index.html').toLowerCase();
68    primaryHrefs.forEach((href) => {
69      const link = document.createElement('a');
70      link.href = href;
71      link.textContent = labels[href];
72      link.className = 'block rounded px-3 py-3 text-base font-medium text-white hover:bg-white/10 hover:text-gold transition';
73
74      if (currentPage === href.toLowerCase()) {
75        link.classList.add('bg-white/10', 'text-gold');
76        link.setAttribute('aria-current', 'page');
77      }
78
79      link.addEventListener('click', () => closeMenu());
80      menuInner.appendChild(link);
81    });
82
83    mobileMenu.appendChild(menuInner);
84    nav.appendChild(mobileMenu);
85
86    function setMenuState(open) {
87      mobileMenu.classList.toggle('hidden', !open);
88      toggleButton.setAttribute('aria-expanded', open ? 'true' : 'false');
89      toggleButton.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
90      const srOnly = toggleButton.querySelector('.sr-only');
91      if (srOnly) srOnly.textContent = open ? 'Close menu' : 'Open menu';
92    }
93
94    function closeMenu() {
95      setMenuState(false);
96    }
97
98    toggleButton.addEventListener('click', () => {
99      const isOpen = toggleButton.getAttribute('aria-expanded') === 'true';
100      setMenuState(!isOpen);
101    });
102
103    document.addEventListener('keydown', (event) => {
104      if (event.key === 'Escape') closeMenu();
105    });
106
107    document.addEventListener('click', (event) => {
108      if (toggleButton.getAttribute('aria-expanded') !== 'true') return;
109      if (!nav.contains(event.target)) closeMenu();
110    });
111
112    window.addEventListener('resize', () => {
113      if (window.innerWidth >= 640) closeMenu();
114    });
115  }
116
117  // Hall of Fame keeps a page-specific provenance block above the standard site footer.
118  // Its legacy footer is upgraded here before StaticForms handlers are attached.
119  function ensureHallOfFameFooter() {
120    if (!document.getElementById('fail-submission-form')) return;
121
122    const existingFooter = document.querySelector('footer');
123    if (!existingFooter) return;
124    if (existingFooter.querySelector('form[aria-label="dontfail.ai newsletter signup"]')) return;
125
126    existingFooter.outerHTML = `
127      <footer class="bg-dark text-white pt-12 pb-8">
128        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
129          <div class="bg-navy rounded-xl p-8 md:p-12 flex flex-col md:flex-row items-center justify-between shadow-2xl mb-12 border border-gray-700">
130            <div class="mb-6 md:mb-0 md:pr-8">
131              <h2 class="text-2xl font-bold text-white mb-2">Never miss a disaster</h2>
132              <p class="text-gray-300">Subscribe for episode notifications and Monday Morning Actions.</p>
133            </div>
134            <div class="w-full md:w-auto">
135              <form
136                class="flex flex-col sm:flex-row gap-3"
137                data-staticforms="true"
138                data-success-message="Thanks - you're signed up for dontfail.ai updates."
139                data-error-message="Something went wrong. Please try again."
140                aria-label="dontfail.ai newsletter signup"
141              >
142                <input type="hidden" name="accessKey" value="" />
143                <input type="hidden" name="subject" value="dontfail.ai newsletter signup" />
144                <input type="hidden" name="formType" value="Newsletter signup" />
145                <input type="text" name="honeypot" class="hidden" tabindex="-1" autocomplete="off" aria-hidden="true" />
146                <div class="flex flex-col gap-2">
147                  <label for="newsletter-email-hall" class="sr-only">Work email address</label>
148                  <input
149                    id="newsletter-email-hall"
150                    name="email"
151                    type="email"
152                    placeholder="[email protected]"
153                    autocomplete="email"
154                    class="px-4 py-3 rounded text-dark focus:outline-none focus:ring-2 focus:ring-gold w-full sm:w-64"
155                    required
156                  />
157                  <div class="form-status text-sm" role="status" aria-live="polite" hidden></div>
158                </div>
159                <button
160                  type="submit"
161                  class="bg-gold hover:bg-[#bfa030] text-navy font-bold px-6 py-3 rounded transition whitespace-nowrap self-start sm:self-auto"
162                >
163                  Subscribe
164                </button>
165              </form>
166            </div>
167          </div>
168
169          <div class="border-t border-gray-700 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-400">
170            <p>
171              &copy; 2025-2026
172              <a href="https://www.aibok.org/" class="hover:text-white hover:underline">AIBoK</a>
173              &amp;
174              <a href="https://www.cirrolytix.com/" class="hover:text-white hover:underline">CirroLytix</a>.
175              All rights reserved.
176            </p>
177            <div class="flex space-x-6 mt-4 md:mt-0">
178              <a href="privacy-policy.html" class="hover:text-white">Privacy Policy</a>
179              <a href="terms-of-use.html" class="hover:text-white">Terms of Use</a>
180            </div>
181          </div>
182        </div>
183      </footer>`;
184  }
185
186  function ensureHallOfFameModalAccessibility() {
187    function initialise() {
188      const modal = document.getElementById('incidentModal');
189      const modalContent = document.getElementById('modalContent');
190      const closeButton = document.getElementById('closeModal');
191      const incidentsGrid = document.getElementById('incidentsGrid');
192      if (!modal || !modalContent || !closeButton || !incidentsGrid) return;
193
194      modalContent.setAttribute('role', 'dialog');
195      modalContent.setAttribute('aria-modal', 'true');
196      modalContent.setAttribute('aria-labelledby', 'm-title');
197      modalContent.setAttribute('aria-describedby', 'm-desc');
198      modalContent.setAttribute('tabindex', '-1');
199      modal.setAttribute('aria-hidden', 'true');
200
201      closeButton.type = 'button';
202      closeButton.setAttribute('aria-label', 'Close incident details');
203      const closeIcon = closeButton.querySelector('svg');
204      if (closeIcon) closeIcon.setAttribute('aria-hidden', 'true');
205
206      let lastFocusedElement = null;
207      let modalWasOpen = false;
208
209      function isModalOpen() {
210        return document.body.classList.contains('modal-active') && !modal.classList.contains('hidden');
211      }
212
213      function getFocusableElements() {
214        const selector = [
215          'a[href]',
216          'button:not([disabled])',
217          'input:not([disabled])',
218          'select:not([disabled])',
219          'textarea:not([disabled])',
220          '[tabindex]:not([tabindex="-1"])'
221        ].join(',');
222
223        return Array.from(modalContent.querySelectorAll(selector)).filter((element) =>
224          element.getClientRects().length > 0 && element.getAttribute('aria-hidden') !== 'true'
225        );
226      }
227
228      function upgradeIncidentCards() {
229        incidentsGrid.querySelectorAll('.incident-card').forEach((card) => {
230          if (card.dataset.a11yReady === 'true') return;
231          card.dataset.a11yReady = 'true';
232          card.setAttribute('role', 'button');
233          card.setAttribute('tabindex', '0');
234          card.setAttribute('aria-haspopup', 'dialog');
235
236          const title = card.querySelector('h3');
237          if (title && title.textContent.trim()) {
238            card.setAttribute('aria-label', `View incident details: ${title.textContent.trim()}`);
239          }
240
241          card.addEventListener('click', () => {
242            lastFocusedElement = card;
243          });
244
245          card.addEventListener('keydown', (event) => {
246            if (event.key !== 'Enter' && event.key !== ' ') return;
247            event.preventDefault();
248            lastFocusedElement = card;
249            card.click();
250          });
251        });
252      }
253
254      function syncModalState() {
255        const open = isModalOpen();
256        modal.setAttribute('aria-hidden', open ? 'false' : 'true');
257
258        if (open && !modalWasOpen) {
259          if (!lastFocusedElement && document.activeElement && !modalContent.contains(document.activeElement)) {
260            lastFocusedElement = document.activeElement;
261          }
262          requestAnimationFrame(() => closeButton.focus());
263        }
264
265        if (!open && modalWasOpen) {
266          const elementToRestore = lastFocusedElement;
267          lastFocusedElement = null;
268          if (elementToRestore && elementToRestore.isConnected && typeof elementToRestore.focus === 'function') {
269            requestAnimationFrame(() => elementToRestore.focus());
270          }
271        }
272
273        modalWasOpen = open;
274      }
275
276      incidentsGrid.addEventListener('click', (event) => {
277        const card = event.target.closest('.incident-card');
278        if (card) lastFocusedElement = card;
279      }, true);
280
281      const gridObserver = new MutationObserver(upgradeIncidentCards);
282      gridObserver.observe(incidentsGrid, { childList: true });
283      upgradeIncidentCards();
284
285      const modalObserver = new MutationObserver(syncModalState);
286      modalObserver.observe(modal, { attributes: true, attributeFilter: ['class'] });
287      syncModalState();
288
289      document.addEventListener('keydown', (event) => {
290        if (event.key !== 'Tab' || !isModalOpen()) return;
291
292        const focusableElements = getFocusableElements();
293        if (focusableElements.length === 0) {
294          event.preventDefault();
295          modalContent.focus();
296          return;
297        }
298
299        const firstElement = focusableElements[0];
300        const lastElement = focusableElements[focusableElements.length - 1];
301
302        if (event.shiftKey && document.activeElement === firstElement) {
303          event.preventDefault();
304          lastElement.focus();
305        } else if (!event.shiftKey && document.activeElement === lastElement) {
306          event.preventDefault();
307          firstElement.focus();
308        } else if (!modalContent.contains(document.activeElement)) {
309          event.preventDefault();
310          firstElement.focus();
311        }
312      });
313    }
314
315    if (document.readyState === 'loading') {
316      document.addEventListener('DOMContentLoaded', initialise, { once: true });
317    } else {
318      initialise();
319    }
320  }
321
322  function ensureEpisodesInteractionIntegrity() {
323    function initialise() {
324      const filter = document.getElementById('filter');
325      const articles = Array.from(document.querySelectorAll('main article'));
326      if (!filter || articles.length === 0) return;
327
328      function applyFilter() {
329        const selected = filter.value.trim().toLowerCase();
330        articles.forEach((article) => {
331          if (selected === 'all tags') {
332            article.hidden = false;
333            return;
334          }
335
336          const hasSelectedTag = Array.from(article.querySelectorAll('span')).some((span) =>
337            span.textContent.trim().toLowerCase() === selected
338          );
339          article.hidden = !hasSelectedTag;
340        });
341      }
342
343      filter.addEventListener('change', applyFilter);
344      applyFilter();
345
346      articles.forEach((article) => {
347        const showNotesButton = Array.from(article.querySelectorAll('button')).find((button) =>
348          /show notes/i.test(button.textContent || '')
349        );
350
351        if (showNotesButton) {
352          const onclick = showNotesButton.getAttribute('onclick') || '';
353          const match = onclick.match(/toggleNotes\(['"]([^'"]+)['"]\)/);
354          if (match) {
355            const panelId = match[1];
356            const panel = document.getElementById(panelId);
357            showNotesButton.setAttribute('aria-controls', panelId);
358            showNotesButton.setAttribute(
359              'aria-expanded',
360              panel && !panel.classList.contains('hidden') ? 'true' : 'false'
361            );
362
363            showNotesButton.addEventListener('click', () => {
364              requestAnimationFrame(() => {
365                if (!panel) return;
366                showNotesButton.setAttribute(
367                  'aria-expanded',
368                  panel.classList.contains('hidden') ? 'false' : 'true'
369                );
370              });
371            });
372          }
373
374          const showNotesIcon = showNotesButton.querySelector('svg');
375          if (showNotesIcon) showNotesIcon.setAttribute('aria-hidden', 'true');
376        }
377
378        const listenButton = Array.from(article.querySelectorAll('button')).find((button) =>
379          /^\s*listen\s*$/i.test(button.textContent || '')
380        );
381        if (listenButton) {
382          listenButton.disabled = true;
383          listenButton.setAttribute('aria-disabled', 'true');
384          listenButton.setAttribute('aria-label', 'Listen — not yet available');
385          listenButton.setAttribute('title', 'Audio not yet available');
386          listenButton.classList.add('opacity-50', 'cursor-not-allowed');
387          const icon = listenButton.querySelector('svg');
388          if (icon) icon.setAttribute('aria-hidden', 'true');
389        }
390
391        const checklistLink = Array.from(article.querySelectorAll('a')).find((link) =>
392          /prevention checklist/i.test(link.textContent || '') && link.getAttribute('href') === '#'
393        );
394        if (checklistLink) {
395          checklistLink.removeAttribute('href');
396          checklistLink.setAttribute('aria-disabled', 'true');
397          checklistLink.setAttribute('title', 'Prevention checklist not yet available');
398          checklistLink.classList.add('opacity-50', 'cursor-not-allowed');
399          const icon = checklistLink.querySelector('svg');
400          if (icon) icon.setAttribute('aria-hidden', 'true');
401        }
402      });
403    }
404
405    if (document.readyState === 'loading') {
406      document.addEventListener('DOMContentLoaded', initialise, { once: true });
407    } else {
408      initialise();
409    }
410  }
411
412  // Every StaticForms form displays the same short privacy notice.
413  function ensurePrivacyNotice(form) {
414    if (form.querySelector('.form-privacy-note')) return;
415
416    const note = document.createElement('p');
417    note.className = 'form-privacy-note text-xs text-gray-400 mt-1';
418    note.innerHTML = 'Consistent with our <a href="https://www.dontfail.ai/privacy-policy.html" class="underline hover:text-white">Privacy Policy</a>.';
419
420    const status = form.querySelector('.form-status');
421    if (status) status.insertAdjacentElement('afterend', note);
422    else form.appendChild(note);
423  }
424
425  // Keep the newsletter button aligned to the 48px email field even when
426  // status/privacy text makes the neighbouring column taller.
427  function normaliseNewsletterSubmitButton(form) {
428    if (form.getAttribute('aria-label') !== 'dontfail.ai newsletter signup') return;
429
430    const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
431    if (!submitButton) return;
432
433    submitButton.style.height = '48px';
434    submitButton.style.minHeight = '48px';
435    submitButton.style.alignSelf = 'flex-start';
436  }
437
438  function showFeedback(form, type) {
439    const status = form.querySelector('.form-status');
440    if (!status) return;
441
442    const successMessage = form.dataset.successMessage || 'Thanks - your submission has been received.';
443    const errorMessage = form.dataset.errorMessage || 'Something went wrong. Please try again.';
444
445    status.textContent = type === 'success' ? successMessage : errorMessage;
446    status.hidden = false;
447    status.classList.remove('text-green-300', 'text-red-300');
448    status.classList.add(type === 'success' ? 'text-green-300' : 'text-red-300');
449    if (!status.hasAttribute('tabindex')) status.setAttribute('tabindex', '-1');
450    if (typeof status.focus === 'function') status.focus();
451  }
452
453  function pushFormAnalyticsEvent(eventName, parameters = {}) {
454    if (typeof window.dontfailPushAnalyticsEvent === 'function') {
455      window.dontfailPushAnalyticsEvent(eventName, parameters);
456      return;
457    }
458
459    // Preserve the event if analytics.js is delayed; GTM will process the queue when available.
460    window.dataLayer = window.dataLayer || [];
461    window.dataLayer.push({
462      event: eventName,
463      ...parameters
464    });
465  }
466
467  function trackSuccessfulFormSubmission(form, submittedData) {
468    const formType = String(submittedData.formType || '').trim();
469    const subject = String(submittedData.subject || '').trim();
470    const isNewsletter = form.getAttribute('aria-label') === 'dontfail.ai newsletter signup' || formType === 'Newsletter signup';
471    const isFailSubmission = form.id === 'fail-submission-form' || /submit.*fail/i.test(subject) || /fail submission/i.test(formType);
472
473    // Never send email addresses or submitted form content into analytics.
474    if (isNewsletter) {
475      pushFormAnalyticsEvent('newsletter_signup', {
476        page_path: window.location.pathname
477      });
478    } else if (isFailSubmission) {
479      pushFormAnalyticsEvent('submit_a_fail', {
480        page_path: window.location.pathname
481      });
482    }
483  }
484
485  async function handleStaticForm(form) {
486    if (!form.checkValidity()) {
487      form.reportValidity();
488      return;
489    }
490
491    const honeypot = form.querySelector('[name="honeypot"], [name="website"]');
492    if (honeypot && honeypot.value) return;
493
494    const accessKeyInput = form.querySelector('input[name="accessKey"]');
495    const accessKey = accessKeyInput ? accessKeyInput.value.trim() : '';
496    if (!accessKey) {
497      showFeedback(form, 'error');
498      return;
499    }
500
501    const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
502    const originalLabel = submitButton
503      ? (submitButton.tagName === 'INPUT' ? submitButton.value : submitButton.textContent)
504      : '';
505
506    if (submitButton) {
507      submitButton.disabled = true;
508      submitButton.setAttribute('aria-busy', 'true');
509      if (submitButton.tagName === 'INPUT') submitButton.value = 'Sending…';
510      else submitButton.textContent = 'Sending…';
511    }
512
513    const data = {};
514    new FormData(form).forEach((value, key) => {
515      if (data[key]) data[key] = [].concat(data[key], value);
516      else data[key] = value;
517    });
518
519    try {
520      const response = await fetch(staticFormsEndpoint, {
521        method: 'POST',
522        headers: {
523          'Content-Type': 'application/json',
524          Accept: 'application/json'
525        },
526        body: JSON.stringify(data)
527      });
528
529      let result = null;
530      try { result = await response.json(); } catch (_err) {}
531
532      if (!response.ok || !result || result.success !== true) {
533        throw new Error('Submission failed');
534      }
535
536      trackSuccessfulFormSubmission(form, data);
537      form.reset();
538      if (accessKeyInput) accessKeyInput.value = accessKey;
539      showFeedback(form, 'success');
540    } catch (_err) {
541      showFeedback(form, 'error');
542    } finally {
543      if (submitButton) {
544        submitButton.disabled = false;
545        submitButton.removeAttribute('aria-busy');
546        if (submitButton.tagName === 'INPUT') submitButton.value = originalLabel;
547        else submitButton.textContent = originalLabel;
548      }
549    }
550  }
551
552  initialiseMobileNavigation();
553  ensureHallOfFameFooter();
554  ensureHallOfFameModalAccessibility();
555  ensureEpisodesInteractionIntegrity();
556
557  document.querySelectorAll('form[data-staticforms="true"]').forEach((form) => {
558    ensurePrivacyNotice(form);
559    normaliseNewsletterSubmitButton(form);
560
561    form.addEventListener('submit', (event) => {
562      event.preventDefault();
563      handleStaticForm(form);
564    });
565  });
566})();

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.