Current File : //opt/alt/ruby40/share/gems/gems/rdoc-7.0.4/lib/rdoc/generator/template/aliki/js/aliki.js
'use strict';

/* ===== Method Source Code Toggling ===== */

function showSource(e) {
  let target = e.target;
  while (!target.classList.contains('method-detail')) {
    target = target.parentNode;
  }
  if (typeof target !== "undefined" && target !== null) {
    target = target.querySelector('.method-source-code');
  }
  if (typeof target !== "undefined" && target !== null) {
    target.classList.toggle('active-menu')
  }
}

function hookSourceViews() {
  document.querySelectorAll('.method-source-toggle').forEach((codeObject) => {
    codeObject.addEventListener('click', showSource);
  });
}

/* ===== Search Functionality ===== */

function createSearchInstance(input, result) {
  if (!input || !result) return null;

  result.classList.remove("initially-hidden");

  const search = new SearchController(search_data, input, result);

  search.renderItem = function(result) {
    const li = document.createElement('li');
    let html = '';

    // TODO add relative path to <script> per-page
    html += `<p class="search-match"><a href="${index_rel_prefix}${this.escapeHTML(result.path)}">${this.hlt(result.title)}`;
    if (result.params)
      html += `<span class="params">${result.params}</span>`;
    html += '</a>';

    // Add type indicator
    if (result.type) {
      const typeLabel = this.formatType(result.type);
      const typeClass = result.type.replace(/_/g, '-');
      html += `<span class="search-type search-type-${this.escapeHTML(typeClass)}">${typeLabel}</span>`;
    }

    if (result.snippet)
      html += `<div class="search-snippet">${result.snippet}</div>`;

    li.innerHTML = html;

    return li;
  }

  search.formatType = function(type) {
    const typeLabels = {
      'class': 'class',
      'module': 'module',
      'constant': 'const',
      'instance_method': 'method',
      'class_method': 'method'
    };
    return typeLabels[type] || type;
  }

  search.select = function(result) {
    window.location.href = result.firstChild.firstChild.href;
  }

  search.scrollIntoView = search.scrollInWindow;

  return search;
}

function hookSearch() {
  const input  = document.querySelector('#search-field');
  const result = document.querySelector('#search-results-desktop');

  if (!input || !result) return; // Exit if search elements not found

  const search_section = document.querySelector('#search-section');
  if (search_section) {
    search_section.classList.remove("initially-hidden");
  }

  const search = createSearchInstance(input, result);
  if (!search) return;

  // Hide search results when clicking outside the search area
  document.addEventListener('click', (e) => {
    if (!e.target.closest('.navbar-search-desktop')) {
      search.hide();
    }
  });

  // Show search results when focusing on input (if there's a query)
  input.addEventListener('focus', () => {
    if (input.value.trim()) {
      search.show();
    }
  });

  // Check for ?q= URL parameter and trigger search automatically
  if (typeof URLSearchParams !== 'undefined') {
    const urlParams = new URLSearchParams(window.location.search);
    const queryParam = urlParams.get('q');
    if (queryParam) {
      input.value = queryParam;
      search.search(queryParam, false);
    }
  }
}

/* ===== Keyboard Shortcuts ===== */

function hookFocus() {
  document.addEventListener("keydown", (event) => {
    if (document.activeElement.tagName === 'INPUT') {
      return;
    }
    if (event.key === "/") {
      event.preventDefault();
      document.querySelector('#search-field').focus();
    }
  });
}

/* ===== Mobile Navigation ===== */

function hookSidebar() {
  const navigation = document.querySelector('#navigation');
  const navigationToggle = document.querySelector('#navigation-toggle');

  if (!navigation || !navigationToggle) return;

  const closeNav = () => {
    navigation.hidden = true;
    navigationToggle.ariaExpanded = 'false';
    document.body.classList.remove('nav-open');
  };

  const openNav = () => {
    navigation.hidden = false;
    navigationToggle.ariaExpanded = 'true';
    document.body.classList.add('nav-open');
  };

  const toggleNav = () => {
    if (navigation.hidden) {
      openNav();
    } else {
      closeNav();
    }
  };

  navigationToggle.addEventListener('click', (e) => {
    e.stopPropagation();
    toggleNav();
  });

  const isSmallViewport = window.matchMedia("(max-width: 1023px)").matches;

  // The sidebar is hidden by default with the `hidden` attribute
  // On large viewports, we display the sidebar with JavaScript
  // This is better than the opposite approach of hiding it with JavaScript
  // because it avoids flickering the sidebar when the page is loaded, especially on mobile devices
  if (isSmallViewport) {
    // Close nav when clicking links inside it
    document.addEventListener('click', (e) => {
      if (e.target.closest('#navigation a')) {
        closeNav();
      }
    });

    // Close nav when clicking backdrop
    document.addEventListener('click', (e) => {
      if (!navigation.hidden &&
          !e.target.closest('#navigation') &&
          !e.target.closest('#navigation-toggle')) {
        closeNav();
      }
    });
  } else {
    openNav();
  }
}

/* ===== Right Sidebar Table of Contents ===== */

function generateToc() {
  const tocNav = document.querySelector('#toc-nav');
  if (!tocNav) return; // Exit if TOC nav doesn't exist

  const main = document.querySelector('main');
  if (!main) return;

  // Find all h2 and h3 headings in the main content
  const headings = main.querySelectorAll('h1, h2, h3');
  if (headings.length === 0) return;

  const tocList = document.createElement('ul');
  tocList.className = 'toc-list';

  headings.forEach((heading) => {
    // Skip if heading doesn't have an id
    if (!heading.id) return;

    const li = document.createElement('li');
    const level = heading.tagName.toLowerCase();
    li.className = `toc-item toc-${level}`;

    const link = document.createElement('a');
    link.href = `#${heading.id}`;
    link.className = 'toc-link';
    link.textContent = heading.textContent.trim();
    link.setAttribute('data-target', heading.id);

    li.appendChild(link);
    setHeadingScrollHandler(heading, link);
    tocList.appendChild(li);
  });

  if (tocList.children.length > 0) {
    tocNav.appendChild(tocList);
  } else {
    // Hide TOC if no headings found
    const tocContainer = document.querySelector('.table-of-contents');
    if (tocContainer) {
      tocContainer.style.display = 'none';
    }
  }
}

function hookTocActiveHighlighting() {
  const tocLinks = document.querySelectorAll('.toc-link');
  const targetHeadings = [];
  tocLinks.forEach((link) => {
    const targetId = link.getAttribute('data-target');
    const heading = document.getElementById(targetId);
    if (heading) {
      targetHeadings.push(heading);
    }
  });

  if (targetHeadings.length === 0) return;

  const observerOptions = {
    root: null,
    rootMargin: '0% 0px -35% 0px',
    threshold: 0
  };

  const intersectingHeadings = new Set();
  const update = () => {
    const firstIntersectingHeading = targetHeadings.find((heading) => {
      return intersectingHeadings.has(heading);
    });
    if (!firstIntersectingHeading) return;
    const correspondingLink = document.querySelector(`.toc-link[data-target="${firstIntersectingHeading.id}"]`);
    if (!correspondingLink) return;

    // Remove active class from all links
    tocLinks.forEach((link) => {
      link.classList.remove('active');
    });

    // Add active class to current link
    correspondingLink.classList.add('active');

    // Scroll link into view if needed
    const tocNav = document.querySelector('#toc-nav');
    if (tocNav) {
      const linkRect = correspondingLink.getBoundingClientRect();
      const navRect = tocNav.getBoundingClientRect();

      if (linkRect.top < navRect.top || linkRect.bottom > navRect.bottom) {
        correspondingLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
      }
    }
  };
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        intersectingHeadings.add(entry.target);
      } else {
        intersectingHeadings.delete(entry.target);
      }
    });
    update();
  }, observerOptions);

  // Observe all headings that have corresponding TOC links
  targetHeadings.forEach((heading) => {
    observer.observe(heading);
  });
}

function setHeadingScrollHandler(heading, link) {
  // Smooth scroll to heading when clicking link
  if (!heading.id) return;

  link.addEventListener('click', (e) => {
    e.preventDefault();
    heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
    history.pushState(null, '', `#${heading.id}`);
  });
}

function setHeadingSelfLinkScrollHandlers() {
  // Clicking link inside heading scrolls smoothly to heading itself
  const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
  headings.forEach((heading) => {
    if (!heading.id) return;

    const link = heading.querySelector(`a[href^="#${heading.id}"]`);
    if (link) setHeadingScrollHandler(heading, link);
  })
}

/* ===== Mobile Search Modal ===== */

function hookSearchModal() {
  const searchToggle = document.querySelector('#search-toggle');
  const searchModal = document.querySelector('#search-modal');
  const searchModalClose = document.querySelector('#search-modal-close');
  const searchModalBackdrop = document.querySelector('.search-modal-backdrop');
  const searchInput = document.querySelector('#search-field-mobile');
  const searchResults = document.querySelector('#search-results-mobile');
  const searchEmpty = document.querySelector('.search-modal-empty');

  if (!searchToggle || !searchModal) return;

  // Initialize search for mobile modal
  const mobileSearch = createSearchInstance(searchInput, searchResults);
  if (!mobileSearch) return;

  // Hide empty state when there are results
  const originalRenderItem = mobileSearch.renderItem;
  mobileSearch.renderItem = function(result) {
    if (searchEmpty) searchEmpty.style.display = 'none';
    return originalRenderItem.call(this, result);
  };

  const openSearchModal = () => {
    searchModal.hidden = false;
    document.body.style.overflow = 'hidden';
    // Focus input after animation
    setTimeout(() => {
      if (searchInput) searchInput.focus();
    }, 100);
  };

  const closeSearchModal = () => {
    searchModal.hidden = true;
    document.body.style.overflow = '';
  };

  // Open on button click
  searchToggle.addEventListener('click', openSearchModal);

  // Close on close button click
  if (searchModalClose) {
    searchModalClose.addEventListener('click', closeSearchModal);
  }

  // Close on backdrop click
  if (searchModalBackdrop) {
    searchModalBackdrop.addEventListener('click', closeSearchModal);
  }

  // Close on Escape key
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !searchModal.hidden) {
      closeSearchModal();
    }
  });

  // Check for ?q= URL parameter on mobile and open modal
  if (typeof URLSearchParams !== 'undefined') {
    const urlParams = new URLSearchParams(window.location.search);
    const queryParam = urlParams.get('q');
    const isSmallViewport = window.matchMedia("(max-width: 1023px)").matches;

    if (queryParam && isSmallViewport) {
      openSearchModal();
      searchInput.value = queryParam;
      mobileSearch.search(queryParam, false);
    }
  }
}

/* ===== Code Block Copy Functionality ===== */

function createCopyButton() {
  const button = document.createElement('button');
  button.className = 'copy-code-button';
  button.type = 'button';
  button.setAttribute('aria-label', 'Copy code to clipboard');
  button.setAttribute('title', 'Copy code');

  // Create clipboard icon SVG
  const clipboardIcon = `
    <svg viewBox="0 0 24 24">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
  `;

  // Create checkmark icon SVG (for copied state)
  const checkIcon = `
    <svg viewBox="0 0 24 24">
      <polyline points="20 6 9 17 4 12"></polyline>
    </svg>
  `;

  button.innerHTML = clipboardIcon;
  button.dataset.clipboardIcon = clipboardIcon;
  button.dataset.checkIcon = checkIcon;

  return button;
}

function wrapCodeBlocksWithCopyButton() {
  // Copy buttons are generated dynamically rather than statically in rhtml templates because:
  // - Code blocks are generated by RDoc's markup formatter (RDoc::Markup::ToHtml),
  //   not directly in rhtml templates
  // - Modifying the formatter would require extending RDoc's core internals

  // Find all pre elements that are not already wrapped
  const preElements = document.querySelectorAll('main pre:not(.code-block-wrapper pre)');

  preElements.forEach((pre) => {
    // Skip if already wrapped
    if (pre.parentElement.classList.contains('code-block-wrapper')) {
      return;
    }

    // Create wrapper
    const wrapper = document.createElement('div');
    wrapper.className = 'code-block-wrapper';

    // Insert wrapper before pre
    pre.parentNode.insertBefore(wrapper, pre);

    // Move pre into wrapper
    wrapper.appendChild(pre);

    // Create and add copy button
    const copyButton = createCopyButton();
    wrapper.appendChild(copyButton);

    // Add click handler
    copyButton.addEventListener('click', () => {
      copyCodeToClipboard(pre, copyButton);
    });
  });
}

function copyCodeToClipboard(preElement, button) {
  const code = preElement.textContent;

  // Use the Clipboard API (supported by all modern browsers)
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(code).then(() => {
      showCopySuccess(button);
    }).catch(() => {
      alert('Failed to copy code.');
    });
  } else {
    alert('Failed to copy code.');
  }
}

function showCopySuccess(button) {
  // Change icon to checkmark
  button.innerHTML = button.dataset.checkIcon;
  button.classList.add('copied');
  button.setAttribute('aria-label', 'Copied!');
  button.setAttribute('title', 'Copied!');

  // Revert back after 2 seconds
  setTimeout(() => {
    button.innerHTML = button.dataset.clipboardIcon;
    button.classList.remove('copied');
    button.setAttribute('aria-label', 'Copy code to clipboard');
    button.setAttribute('title', 'Copy code');
  }, 2000);
}

/* ===== Initialization ===== */

document.addEventListener('DOMContentLoaded', () => {
  hookSourceViews();
  hookSearch();
  hookFocus();
  hookSidebar();
  generateToc();
  setHeadingSelfLinkScrollHandlers();
  hookTocActiveHighlighting();
  hookSearchModal();
  wrapCodeBlocksWithCopyButton();
});