function initializeSearch()

in lib/resources/script.js [160:432]


function initializeSearch(input, index) {
  input.disabled = false;
  input.setAttribute('placeholder', 'Search API Docs');

  // Handle grabbing focus when the users types / outside of the input
  document.addEventListener('keypress', (event) => {
    if (event.code === 'Slash' && !(document.activeElement instanceof HTMLInputElement)) {
      event.preventDefault();
      input.focus();
    }
  });

  // Prepare elements

  const parentForm = input.parentNode;
  const wrapper = document.createElement('div');
  wrapper.classList.add('tt-wrapper');

  parentForm.replaceChild(wrapper, input);

  const inputHint = document.createElement('input');
  inputHint.setAttribute('type', 'text');
  inputHint.setAttribute('autocomplete', 'off');
  inputHint.setAttribute('readonly', 'true');
  inputHint.setAttribute('spellcheck', 'false');
  inputHint.setAttribute('tabindex', '-1');
  inputHint.classList.add('typeahead', 'tt-hint');

  wrapper.appendChild(inputHint);

  input.setAttribute('autocomplete', 'off');
  input.setAttribute('spellcheck', 'false');
  input.classList.add('tt-input');

  wrapper.appendChild(input);

  const listBox = document.createElement('div');
  listBox.setAttribute('role', 'listbox');
  listBox.setAttribute('aria-expanded', 'false');
  listBox.style.display = 'none';
  listBox.classList.add('tt-menu');

  const presentation = document.createElement('div');
  presentation.classList.add('tt-elements');

  listBox.appendChild(presentation);

  wrapper.appendChild(listBox);

  // Set up various search functionality

  function highlight(text, query) {
    query = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    return text.replace(new RegExp(query, 'gi'), (matched) => {
      return `<strong class='tt-highlight'>${matched}</strong>`;
    });
  }

  function createSuggestion(query, match) {
    const suggestion = document.createElement('div');
    suggestion.setAttribute('data-href', match.href);
    suggestion.classList.add('tt-suggestion');

    const suggestionTitle = document.createElement('span');
    suggestionTitle.classList.add('tt-suggestion-title');
    suggestionTitle.innerHTML = highlight(`${match.name} ${match.type.toLowerCase()}`, query);

    suggestion.appendChild(suggestionTitle);

    if (match.enclosedBy) {
      const fromLib = document.createElement('div');
      fromLib.classList.add('search-from-lib');
      fromLib.innerHTML = `from ${highlight(match.enclosedBy.name, query)}`;

      suggestion.appendChild(fromLib);
    }

    suggestion.addEventListener('mousedown', event => {
      event.preventDefault();
    });

    suggestion.addEventListener('click', event => {
      if (match.href) {
        window.location = baseHref + match.href;
        event.preventDefault();
      }
    });

    return suggestion;
  }

  let storedValue = null;
  let actualValue = '';
  let hint = null;

  let suggestionElements = [];
  let suggestionsInfo = [];
  let selectedElement = null;

  function setHint(value) {
    hint = value;
    inputHint.value = value || '';
  }

  function updateSuggestions(query, suggestions) {
    suggestionsInfo = [];
    suggestionElements = [];
    presentation.textContent = '';

    if (suggestions.length < minLength) {
      setHint(null)
      hideSuggestions();
      return;
    }

    for (let i = 0; i < suggestions.length; i++) {
      const element = createSuggestion(query, suggestions[i]);
      suggestionElements.push(element);
      presentation.appendChild(element);
    }

    suggestionsInfo = suggestions;

    setHint(query + suggestions[0].name.slice(query.length));
    selectedElement = null;

    showSuggestions();
  }

  function handle(newValue, forceUpdate) {
    if (actualValue === newValue && !forceUpdate) {
      return;
    }

    if (newValue === null || newValue.length === 0) {
      updateSuggestions('', []);
      return;
    }

    const suggestions = findMatches(index, newValue).slice(0, suggestionLimit);
    actualValue = newValue;

    updateSuggestions(newValue, suggestions);
  }

  function showSuggestions() {
    if (presentation.hasChildNodes()) {
      listBox.style.display = 'block';
      listBox.setAttribute('aria-expanded', 'true');
    }
  }

  function hideSuggestions() {
    listBox.style.display = 'none';
    listBox.setAttribute('aria-expanded', 'false');
  }

  // Hook up events

  input.addEventListener('focus', () => {
    handle(input.value, true);
  });

  input.addEventListener('blur', () => {
    selectedElement = null;
    if (storedValue !== null) {
      input.value = storedValue;
      storedValue = null;
    }
    hideSuggestions();
    setHint(null);
  });

  input.addEventListener('input', event => {
    handle(event.target.value);
  });

  input.addEventListener('keydown', event => {
    if (suggestionElements.length === 0) {
      return;
    }

    if (event.code === 'Enter') {
      const selectingElement = selectedElement || 0;
      const href = suggestionElements[selectingElement].dataset.href;
      if (href) {
        window.location = baseHref + href;
      }
      return;
    }

    if (event.code === 'Tab') {
      if (selectedElement === null) {
        // The user wants to fill the field with the hint
        if (hint !== null) {
          input.value = hint;
          handle(hint);
          event.preventDefault();
        }
      } else {
        // The user wants to fill the input field with their currently selected suggestion
        handle(suggestionsInfo[selectedElement].name);
        storedValue = null;
        selectedElement = null;
        event.preventDefault();
      }
      return;
    }

    const lastIndex = suggestionElements.length - 1;
    const previousSelectedElement = selectedElement;

    if (event.code === 'ArrowUp') {
      if (selectedElement === null) {
        selectedElement = lastIndex;
      } else if (selectedElement === 0) {
        selectedElement = null;
      } else {
        selectedElement--;
      }
    } else if (event.code === 'ArrowDown') {
      if (selectedElement === null) {
        selectedElement = 0;
      } else if (selectedElement === lastIndex) {
        selectedElement = null;
      } else {
        selectedElement++;
      }
    } else {
      if (storedValue !== null) {
        storedValue = null;
        handle(input.value);
      }
      return;
    }

    if (previousSelectedElement !== null) {
      suggestionElements[previousSelectedElement].classList.remove('tt-cursor');
    }

    if (selectedElement !== null) {
      const selected = suggestionElements[selectedElement];
      selected.classList.add('tt-cursor');

      // Guarantee the selected element is visible
      if (selectedElement === 0) {
        listBox.scrollTop = 0;
      } else if (selectedElement === lastIndex) {
        listBox.scrollTop = listBox.scrollHeight;
      } else {
        const offsetTop = selected.offsetTop;
        const parentOffsetHeight = listBox.offsetHeight;
        if (offsetTop < parentOffsetHeight || parentOffsetHeight < (offsetTop + selected.offsetHeight)) {
          selected.scrollIntoView({behavior: 'auto', block: 'nearest'});
        }
      }

      if (storedValue === null) {
        // Store the actual input value to display their currently selected item
        storedValue = input.value;
      }
      input.value = suggestionsInfo[selectedElement].name;
      setHint('');
    } else if (storedValue !== null && previousSelectedElement !== null) {
      // They are moving back to the input field, so return the stored value
      input.value = storedValue;
      setHint(storedValue + suggestionsInfo[0].name.slice(storedValue.length));
      storedValue = null;
    }

    event.preventDefault();
  });
}