/** * Docs & Knowledge Base - Frontend JavaScript * Live search, TOC generation, smooth scroll, analytics * * @package King_Addons */ (function() { 'use strict'; // Configuration const CONFIG = { searchDebounce: 300, scrollOffset: 80, stickyOffset: 100 }; // API endpoints const API = { search: kngDocsKB?.restUrl + 'search' || '/wp-json/king-addons/v1/docs/search', nonce: kngDocsKB?.nonce || '' }; /** * Debounce helper */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Live Search */ class DocsSearch { constructor(container) { this.container = container; this.input = container.querySelector('.kng-docs-search-input'); this.results = container.querySelector('.kng-docs-search-results'); this.loader = container.querySelector('.kng-docs-search-loader'); if (!this.input || !this.results) return; this.minChars = parseInt(this.input.dataset.minChars) || 2; this.categoryId = this.input.dataset.category || ''; this.init(); } init() { this.input.addEventListener('input', debounce(() => this.handleSearch(), CONFIG.searchDebounce)); this.input.addEventListener('focus', () => this.showResults()); // Close on outside click document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.hideResults(); } }); // Keyboard navigation this.input.addEventListener('keydown', (e) => this.handleKeyboard(e)); } async handleSearch() { const query = this.input.value.trim(); if (query.length < this.minChars) { this.hideResults(); return; } this.showLoader(); try { let url = `${API.search}?s=${encodeURIComponent(query)}&per_page=10`; if (this.categoryId) { url += `&category=${this.categoryId}`; } const response = await fetch(url, { headers: { 'X-WP-Nonce': API.nonce } }); const data = await response.json(); this.renderResults(data); } catch (error) { console.error('Search error:', error); this.renderEmpty(); } this.hideLoader(); } renderResults(results) { if (!results || results.length === 0) { this.renderEmpty(); return; } const html = results.map(item => `
${item.title}
${item.category ? `
${item.category}
` : ''}
`).join(''); this.results.innerHTML = html; this.showResults(); } renderEmpty() { this.results.innerHTML = `
${kngDocsKB?.i18n?.noResults || 'No results found'}
`; this.showResults(); } showResults() { if (this.input.value.trim().length >= this.minChars) { this.results.style.display = 'block'; } } hideResults() { this.results.style.display = 'none'; } showLoader() { if (this.loader) { this.loader.style.display = 'flex'; } } hideLoader() { if (this.loader) { this.loader.style.display = 'none'; } } handleKeyboard(e) { const items = this.results.querySelectorAll('.kng-docs-search-result'); const activeItem = this.results.querySelector('.kng-docs-search-result:focus'); const activeIndex = activeItem ? Array.from(items).indexOf(activeItem) : -1; switch (e.key) { case 'ArrowDown': e.preventDefault(); if (activeIndex < items.length - 1) { items[activeIndex + 1].focus(); } else if (activeIndex === -1 && items.length > 0) { items[0].focus(); } break; case 'ArrowUp': e.preventDefault(); if (activeIndex > 0) { items[activeIndex - 1].focus(); } else { this.input.focus(); } break; case 'Escape': this.hideResults(); this.input.blur(); break; } } } /** * Table of Contents Generator */ class TableOfContents { constructor(contentEl, tocEl, floatingTocEl = null) { this.content = contentEl; this.toc = tocEl; this.floatingToc = floatingTocEl; if (!this.content) return; this.headings = this.content.dataset.tocHeadings || 'h2,h3'; this.init(); } init() { const elements = this.content.querySelectorAll(this.headings); if (elements.length === 0) { if (this.toc) this.toc.style.display = 'none'; return; } const tocItems = []; elements.forEach((el, index) => { const id = `toc-${index}`; el.id = id; const level = el.tagName.toLowerCase(); const text = el.textContent; tocItems.push({ id, text, level }); }); const html = this.generateHTML(tocItems); if (this.toc) { const list = this.toc.querySelector('.kng-docs-toc-list'); if (list) { list.innerHTML = html; this.toc.style.display = ''; } } if (this.floatingToc) { this.floatingToc.innerHTML = html; } // Initialize scroll spy this.initScrollSpy(tocItems); // Smooth scroll this.initSmoothScroll(); } generateHTML(items) { return items.map(item => `
  • ${item.text}
  • `).join(''); } initScrollSpy(items) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { const link = document.querySelector(`.kng-docs-toc-item a[href="#${entry.target.id}"]`); if (link) { if (entry.isIntersecting) { // Remove active from all document.querySelectorAll('.kng-docs-toc-item').forEach(item => { item.classList.remove('is-active'); }); link.parentElement.classList.add('is-active'); } } }); }, { rootMargin: '-100px 0px -66%', threshold: 0 }); items.forEach(item => { const el = document.getElementById(item.id); if (el) observer.observe(el); }); } initSmoothScroll() { document.querySelectorAll('.kng-docs-toc-list a').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetId = link.getAttribute('href').slice(1); const target = document.getElementById(targetId); if (target) { const offsetTop = target.getBoundingClientRect().top + window.pageYOffset - CONFIG.scrollOffset; window.scrollTo({ top: offsetTop, behavior: 'smooth' }); // Update URL without scrolling history.pushState(null, '', link.getAttribute('href')); } }); }); } } /** * Article Feedback */ class ArticleFeedback { constructor(container) { this.container = container; this.docId = container.dataset.docId; this.buttons = container.querySelector('.kng-docs-feedback-buttons'); this.thanks = container.querySelector('.kng-docs-feedback-thanks'); this.question = container.querySelector('.kng-docs-feedback-question'); if (!this.buttons) return; this.init(); } init() { const btns = this.container.querySelectorAll('.kng-docs-feedback-btn'); btns.forEach(btn => { btn.addEventListener('click', () => this.submitFeedback(btn.dataset.value)); }); } async submitFeedback(value) { // Check if already submitted if (localStorage.getItem(`kng_doc_feedback_${this.docId}`)) { this.showThanks(); return; } try { const formData = new FormData(); formData.append('action', 'king_addons_docs_feedback'); formData.append('doc_id', this.docId); formData.append('feedback', value); formData.append('nonce', kngDocsKB?.feedbackNonce || ''); await fetch(kngDocsKB?.ajaxUrl || '/wp-admin/admin-ajax.php', { method: 'POST', body: formData }); // Store in localStorage to prevent duplicate submissions localStorage.setItem(`kng_doc_feedback_${this.docId}`, value); this.showThanks(); } catch (error) { console.error('Feedback error:', error); } } showThanks() { if (this.buttons) this.buttons.style.display = 'none'; if (this.question) this.question.style.display = 'none'; if (this.thanks) this.thanks.style.display = 'flex'; } } /** * Sticky Elements */ class StickyElement { constructor(element) { this.element = element; this.isSticky = element.classList.contains('is-sticky'); if (!this.isSticky) return; this.init(); } init() { const inner = this.element.querySelector('.kng-docs-sidebar-inner, .kng-docs-toc-floating-inner'); if (inner) { inner.style.position = 'sticky'; inner.style.top = `${CONFIG.stickyOffset}px`; } } } /** * Copy Code Button */ class CopyCode { constructor() { this.init(); } init() { const codeBlocks = document.querySelectorAll('.kng-docs-article-content pre'); codeBlocks.forEach(block => { const button = document.createElement('button'); button.className = 'kng-docs-copy-btn'; button.innerHTML = ` `; button.title = kngDocsKB?.i18n?.copy || 'Copy'; block.style.position = 'relative'; block.appendChild(button); button.addEventListener('click', () => this.copyToClipboard(block, button)); }); } async copyToClipboard(block, button) { const code = block.querySelector('code') || block; const text = code.textContent; try { await navigator.clipboard.writeText(text); button.classList.add('copied'); button.innerHTML = ` `; setTimeout(() => { button.classList.remove('copied'); button.innerHTML = ` `; }, 2000); } catch (error) { console.error('Copy failed:', error); } } } /** * Reading Progress */ class ReadingProgress { constructor(article) { this.article = article; if (!this.article) return; this.init(); } init() { const progressBar = document.createElement('div'); progressBar.className = 'kng-docs-reading-progress'; progressBar.innerHTML = '
    '; document.body.appendChild(progressBar); const bar = progressBar.querySelector('.kng-docs-reading-progress-bar'); window.addEventListener('scroll', () => { const articleRect = this.article.getBoundingClientRect(); const articleTop = articleRect.top + window.pageYOffset; const articleHeight = this.article.offsetHeight; const windowHeight = window.innerHeight; const scrollTop = window.pageYOffset; const start = articleTop - windowHeight; const end = articleTop + articleHeight; const progress = (scrollTop - start) / (end - start); bar.style.width = `${Math.min(Math.max(progress * 100, 0), 100)}%`; }); } } /** * Initialize */ function init() { // Search document.querySelectorAll('.kng-docs-search').forEach(el => { new DocsSearch(el); }); // Table of Contents const content = document.querySelector('.kng-docs-article-content'); const toc = document.getElementById('kng-docs-toc'); const floatingToc = document.querySelector('.kng-docs-toc-floating-list'); if (content) { new TableOfContents(content, toc, floatingToc); } // Feedback document.querySelectorAll('.kng-docs-feedback').forEach(el => { new ArticleFeedback(el); }); // Sticky elements document.querySelectorAll('.kng-docs-sidebar, .kng-docs-toc-floating').forEach(el => { new StickyElement(el); }); // Copy code buttons if (document.querySelector('.kng-docs-article-content pre')) { new CopyCode(); } // Reading progress (optional) const article = document.querySelector('.kng-docs-article'); if (article && kngDocsKB?.showReadingProgress) { new ReadingProgress(article); } } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();