PageSourceSearch

https://tooqtechnology.com/app.js

js tooqtechnology.com collected 2026-10-08 21:21:23 UTC 8,989 bytes, 181 lines download raw bytes

1(() => {
2  'use strict';
3  const themeButton = document.querySelector('.theme-toggle');
4  const root = document.documentElement;
5  const syncTheme = theme => {
6    const dark = theme === 'dark';
7    root.dataset.theme = dark ? 'dark' : 'light';
8    root.style.colorScheme = dark ? 'dark' : 'light';
9    themeButton?.setAttribute('aria-checked', String(dark));
10    themeButton?.setAttribute('title', dark ? 'Switch to light mode' : 'Switch to dark mode');
11    document.querySelector('meta[name="theme-color"]')?.setAttribute('content', dark ? '#050e32' : '#eef1f6');
12  };
13  syncTheme(root.dataset.theme);
14  themeButton?.addEventListener('click', () => {
15    const theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
16    syncTheme(theme);
17    try { localStorage.setItem('tooq-theme', theme); } catch { /* The switch works without storage. */ }
18  });
19  window.addEventListener('storage', event => {
20    if (event.key === 'tooq-theme') syncTheme(event.newValue);
21  });
22  const menuButton = document.querySelector('.menu-toggle');
23  const nav = document.querySelector('.nav-links');
24  const dropdowns = [...document.querySelectorAll('.nav-dropdown')];
25  const closeMenu = () => {
26    nav?.classList.remove('is-open');
27    menuButton?.setAttribute('aria-expanded', 'false');
28    menuButton?.setAttribute('aria-label', 'Open navigation');
29    dropdowns.forEach(item => { item.open = false; });
30  };
31  menuButton?.addEventListener('click', () => {
32    const isOpen = menuButton.getAttribute('aria-expanded') !== 'true';
33    nav.classList.toggle('is-open', isOpen);
34    menuButton.setAttribute('aria-expanded', String(isOpen));
35    menuButton.setAttribute('aria-label', isOpen ? 'Close navigation' : 'Open navigation');
36  });
37  nav?.addEventListener('click', event => { if (event.target.closest('a')) closeMenu(); });
38  document.querySelector('.floating-links')?.addEventListener('click', event => { if (event.target.closest('a')) closeMenu(); });
39  dropdowns.forEach(item => item.addEventListener('toggle', () => {
40    if (item.open) dropdowns.forEach(other => { if (other !== item) other.open = false; });
41  }));
42  document.addEventListener('click', event => {
43    if (!event.target.closest('.site-header,.floating-header')) closeMenu();
44  });
45  document.addEventListener('keydown', event => {
46    if (event.key !== 'Escape') return;
47    const openDropdown = dropdowns.find(item => item.open);
48    if (openDropdown) {
49      openDropdown.open = false;
50      openDropdown.querySelector('summary').focus();
51    } else if (nav?.classList.contains('is-open')) {
52      closeMenu();
53      menuButton.focus();
54    }
55  });
56  const wideScreen = window.matchMedia('(min-width: 701px)');
57  wideScreen.addEventListener('change', () => closeMenu());
58
59  // Swap navigation only after the complete opening header has left the viewport.
60  // The same theme button moves between slots, preserving its state and listeners.
61  const header = document.querySelector('.site-header');
62  const floatingHeader = document.querySelector('.floating-header');
63  const themeSlot = document.querySelector('.theme-slot');
64  const themeDock = document.querySelector('.theme-dock');
65  if (header && floatingHeader && themeSlot && themeDock && themeButton) {
66    let floating = false;
67    const setFloating = next => {
68      if (next === floating) return;
69      const focused = document.activeElement;
70      const themeFocused = focused === themeButton;
71      const outgoing = next ? header : floatingHeader;
72      const incoming = next ? floatingHeader : header;
73      const moveFocus = outgoing.contains(focused) && !themeFocused;
74      const focusKey = focused?.dataset.navKey || 'home';
75      floating = next;
76      closeMenu();
77      (next ? themeDock : themeSlot).append(themeButton);
78      root.classList.toggle('nav-is-floating', next);
79      incoming.inert = false;
80      floatingHeader.setAttribute('aria-hidden', String(!next));
81      if (themeFocused) themeButton.focus({ preventScroll: true });
82      else if (moveFocus) (incoming.querySelector(`[data-nav-key="${focusKey}
82"]`) || incoming.querySelector('[data-nav-key="home"]'))?.focus({ preventScroll: true });
83      outgoing.inert = true;
84    };
85    const updateNavigation = () => setFloating(header.getBoundingClientRect().bottom <= 0);
86    if ('IntersectionObserver' in window) {
87      const observer = new IntersectionObserver(updateNavigation, { threshold: 0 });
88      observer.observe(header);
89    } else {
90      window.addEventListener('scroll', updateNavigation, { passive: true });
91    }
92    window.addEventListener('resize', updateNavigation, { passive: true });
93    window.addEventListener('pageshow', updateNavigation);
94    updateNavigation();
95
96    // The floating pill keeps its links and button on one row only while they fit, whatever the font size,
97    // zoom, or text. Otherwise it uses the two-row layout from phones (.is-stacked in brand-theme.css).
98    const floatingShell = floatingHeader.querySelector('.floating-shell');
99    const floatingLinks = floatingHeader.querySelector('.floating-links');
100    if (floatingShell && floatingLinks && 'ResizeObserver' in window) {
101      const fitFloating = () => {
102        floatingHeader.classList.remove('is-stacked');
103        const edge = floatingShell.getBoundingClientRect().right - parseFloat(getComputedStyle(floatingShell).paddingRight);
104        // Compare the links themselves, not scrollWidth: their hover pills reach past the row by design.
105        const row = floatingLinks.getBoundingClientRect().right;
106        const overflows = [...floatingLinks.children].some(link => link.getBoundingClientRect().right > row + 1) || [...floatingShell.children].some(child => child.getBoundingClientRect().right > edge + 1);
107        floatingHeader.classList.toggle('is-stacked', overflows);
108      };
109      new ResizeObserver(fitFloating).observe(document.documentElement);
110      document.fonts?.ready.then(fitFloating);
111      fitFloating();
112    }
113  }
114
115  // Use navy text over the bright Signal Lime beginning of the page.
116  // This only measures document geometry; it never changes the background.
117  const home = document.querySelector('body.home-page');
118  if (home) {
119    const inkTargets = [...home.querySelectorAll('.section-heading,.product-directory,.company-intro,.company-copy,.footer-top,.footer-grid,.footer-bottom')];
120    const updateInk = () => {
121      const height = home.scrollHeight;
122      inkTargets.forEach(element => {
123        const rect = element.getBoundingClientRect();
124        const center = rect.top + window.scrollY + rect.height / 2;
125        element.classList.toggle('is-on-bright', center / height < .18);
126      });
127    };
128    if ('ResizeObserver' in window) new ResizeObserver(updateInk).observe(home);
129    window.addEventListener('resize', updateInk, { passive: true });
130    document.fonts?.ready.then(updateInk);
131    updateInk();
132  }
133
134  const search = document.querySelector('#manual-search');
135  if (search) {
136    const terms = [...document.querySelectorAll('.manual-term')];
137    const parts = [...document.querySelectorAll('.manual-part')];
138    const count = document.querySelector('#manual-count');
139    const empty = document.querySelector('.manual-empty');
140    const entries = terms.map(term => ({
141      element: term,
142      link: document.querySelector(`.reading-sidebar a[href="#${term.id}"]`),
143      text: term.textContent.toLocaleLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
144    }));
145    const filter = () => {
146      const query = search.value.trim().toLocaleLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
147      let visible = 0;
148      entries.forEach(entry => {
149        const match = entry.text.includes(query);
150        entry.element.hidden = !match;
151        if (entry.link) entry.link.hidden = !match;
152        if (match) visible++;
153      });
154      parts.forEach(part => { part.hidden = ![...part.querySelectorAll('.manual-term')].some(term => !term.hidden); });
155      count.textContent = query ? `${visible} / ${terms.length} terms` : `${terms.length} terms`;
156      empty.hidden = visible !== 0;
157    };
158    search.addEventListener('input', filter);
159    document.querySelector('[data-clear-search]')?.addEventListener('click', () => {
160      search.value = '';
161      filter();
162      search.focus();
163    });
164    filter();
165  }
166  const readingLinks = [...document.querySelectorAll('.reading-sidebar nav a')];
167  if (readingLinks.length && 'IntersectionObserver' in window) {
168    const observer = new IntersectionObserver(entries => {
169      const visible = entries.find(entry => entry.isIntersecting);
170      if (!visible) return;
171      readingLinks.forEach(link => {
172        if (link.hash === `#${visible.target.id}`) link.setAttribute('aria-current', 'true');
173        else link.removeAttribute('aria-current');
174      });
175    }, { rootMargin: '-5% 0px -70% 0px', threshold: 0 });
176    readingLinks.forEach(link => {
177      const target = document.getElementById(link.hash.slice(1));
178      if (target) observer.observe(target);
179    });
180  }
181})();

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.