Skip to main content

Search the page feature

See it here. Add this to an html block on the page. 
 

Staff Directory search interface with input field and buttons: Prev, Next, Clear. Instructions for automatic updates and navigation.

<div class="poc-search-wrapper" role="search">
  <label for="poc-search" style="display:block; font-weight:600;">Find person/content</label>
  <div class="poc-search-controls">
    <input id="poc-search" type="text" placeholder="Type a name or keyword" aria-label="Search within items" />
    <button id="poc-search-prev" type="button" aria-label="Find previous match">Prev</button>
    <button id="poc-search-next" type="button" aria-label="Find next match">Next</button>
    <button id="poc-search-clear" type="button" aria-label="Clear search">Clear</button>
  </div>
  <div id="poc-search-status" aria-live="polite" style="margin-top:0.5rem;"></div>
</div>

<style>
/* Layout + responsive */
.poc-search-controls { display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap; margin-top:0.25rem; }
#poc-search { padding:0.5rem; min-width:16rem; max-width:100%; }

/* Highlighting */
.poc-hit { outline: 3px solid #1a73e8; outline-offset: 4px; border-radius: 6px; }
.poc-hit:focus { box-shadow: 0 0 0 4px rgba(26,115,232,0.3); }

/* Mark styling with strong contrast */
mark.poc-mark {
  background: #ffeb3b; /* bright yellow */
  color: #111;         /* dark text for contrast */
  padding: 0.05em 0.15em;
  border-radius: 0.2em;
}

/* Buttons */
#poc-search-prev, #poc-search-next, #poc-search-clear {
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border: 1px solid #ccc;
  border-radius: 6px;
  background: #f7f7f7;
}
#poc-search-prev:focus, #poc-search-next:focus, #poc-search-clear:focus { outline: 3px solid #1a73e8; outline-offset: 2px; }

/* Avoid affecting other pages if helpful:
body:not(.events-page) .poc-search-wrapper { } */
</style>

<script>
(function () {
  const SELECTOR = '.full-poc.has-no-date.has-no-image';
  const input = document.getElementById('poc-search');
  const btnPrev = document.getElementById('poc-search-prev');
  const btnNext = document.getElementById('poc-search-next');
  const btnClear = document.getElementById('poc-search-clear');
  const status = document.getElementById('poc-search-status');

  let hits = [];
  let hitIndex = -1;
  let lastQuery = '';
  let debounceTimer;

  function escapeRegExp(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function highlightInNode(node, regex) {
    if (node.nodeType !== 3) return 0;
    const text = node.nodeValue;
    const match = regex.exec(text);
    if (!match) return 0;

    const mark = document.createElement('mark');
    mark.className = 'poc-mark';
    mark.textContent = match[0];

    const after = node.splitText(match.index);
    after.nodeValue = after.nodeValue.substring(match[0].length);
    node.parentNode.insertBefore(mark, after);

    return 1 + highlightInNode(after, regex);
  }

  function traverseAndHighlight(el, regex) {
    let count = 0;
    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
      acceptNode(n) {
        const p = n.parentNode;
        if (!p) return NodeFilter.FILTER_REJECT;
        const tag = p.nodeName.toLowerCase();
        if (tag === 'script' || tag === 'style') return NodeFilter.FILTER_REJECT;
        return /\S/.test(n.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
      }
    });
    const textNodes = [];
    while (walker.nextNode()) textNodes.push(walker.currentNode);
    for (const tn of textNodes) {
      regex.lastIndex = 0;
      count += highlightInNode(tn, regex);
    }
    return count;
  }

  function clearHighlights() {
    document.querySelectorAll('mark.poc-mark').forEach(mark => {
      const parent = mark.parentNode;
      if (!parent) return;
      parent.replaceChild(document.createTextNode(mark.textContent), mark);
      parent.normalize();
    });
    document.querySelectorAll(SELECTOR + '.poc-hit').forEach(el => el.classList.remove('poc-hit'));
    hits = [];
    hitIndex = -1;
  }

  function buildHits() {
    hits = [];
    document.querySelectorAll(SELECTOR + ' mark.poc-mark').forEach((m, idx) => {
      const container = m.closest(SELECTOR) || m.parentElement;
      if (container) hits.push({ el: container, mark: m, idx });
    });
  }

  function announce(msg) {
    status.textContent = msg;
  }

  function scrollToHit(index) {
    if (index < 0 || index >= hits.length) return;
    document.querySelectorAll(SELECTOR + '.poc-hit').forEach(el => el.classList.remove('poc-hit'));
    const { el, mark } = hits[index];
    el.classList.add('poc-hit');
    mark.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
    const prevTabIndex = el.getAttribute('tabindex');
    el.setAttribute('tabindex', '-1');
    el.focus({ preventScroll: true });
    if (prevTabIndex === null) setTimeout(() => el.removeAttribute('tabindex'), 1000);
    announce(`Result ${index + 1} of ${hits.length}`);
  }

  function runSearch(startDirection = 'next') {
    const q = input.value.trim();
    // Auto-clear if empty or if query changed
    clearHighlights();
    if (!q) {
      lastQuery = '';
      announce('Search cleared.');
      return;
    }

    const regex = new RegExp(escapeRegExp(q), 'gi');
    const containers = document.querySelectorAll(SELECTOR);
    let totalMarks = 0;

    containers.forEach(el => {
      if (regex.test(el.textContent)) {
        totalMarks += traverseAndHighlight(el, regex);
      }
      regex.lastIndex = 0;
    });

    buildHits();

    if (!hits.length) {
      announce('No matches found.');
      lastQuery = q;
      return;
    }

    hitIndex = startDirection === 'prev' ? hits.length - 1 : 0;
    scrollToHit(hitIndex);
    announce(`Found ${hits.length} match${hits.length > 1 ? 'es' : ''}. Showing ${hitIndex + 1}.`);
    lastQuery = q;
  }

  function gotoNext() {
    const q = input.value.trim();
    if (!hits.length || q !== lastQuery) return runSearch('next');
    hitIndex = (hitIndex + 1) % hits.length;
    scrollToHit(hitIndex);
  }

  function gotoPrev() {
    const q = input.value.trim();
    if (!hits.length || q !== lastQuery) return runSearch('prev');
    hitIndex = (hitIndex - 1 + hits.length) % hits.length;
    scrollToHit(hitIndex);
  }

  // Buttons
  btnNext.addEventListener('click', gotoNext);
  btnPrev.addEventListener('click', gotoPrev);
  btnClear.addEventListener('click', () => {
    input.value = '';
    runSearch('next'); // will auto-clear & announce
  });

  // Enter = Next, Shift+Enter = Prev
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      if (e.shiftKey) gotoPrev(); else gotoNext();
    }
  });

  // ★ NEW: Auto-clear & re-search on input (debounced)
  input.addEventListener('input', () => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      const q = input.value.trim();
      if (!q) {
        runSearch(); // clears + status
      } else if (q !== lastQuery) {
        runSearch('next'); // clears old marks, applies new, scrolls
      }
    }, 150); // small delay for performance & mobile typing
  });

  announce('Type a term; matches update automatically. Enter = Next, Shift+Enter = Prev.');
})();
</script>