/**
* Docs & Knowledge Base v2 – Apple Liquid-Glass Frontend Scripts
*
* Modules:
* 1. DocsSearch — AJAX live search with debounce & keyboard nav
* 2. TableOfContents — auto-generated, hierarchical, sticky, scroll-spy
* 3. ScrollReveal — IntersectionObserver fade-in animations
* 4. Reactions — emoji reactions (happy / neutral / sad)
* 5. DarkMode — light / dark / auto from cookie / system
* 6. SocialShare — copy link, Twitter, Facebook, LinkedIn
* 7. CopyAnchor — heading anchor copy-to-clipboard
*
* @package King_Addons
*/
(function () {
'use strict';
/* ========================================
HELPERS
======================================== */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
function debounce(fn, ms = 300) {
let t;
return (...a) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, a), ms);
};
}
function setCookie(n, v, days = 365) {
const d = new Date();
d.setTime(d.getTime() + days * 864e5);
document.cookie = `${n}=${v};expires=${d.toUTCString()};path=/;SameSite=Lax`;
}
function getCookie(n) {
return (document.cookie.match(`(^|;)\\s*${n}=([^;]*)`) || [])[2] || '';
}
/* ========================================
1. DocsSearch
======================================== */
class DocsSearch {
constructor(container) {
this.container = container;
this.input = $('.kng-docs-search-input', container);
this.results = $('.kng-docs-search-results', container);
this.loader = $('.kng-docs-search-loader', container);
this.shortcut = $('.kng-docs-search-shortcut', container);
if (!this.input || !this.results) return;
this.activeIdx = -1;
this._bind();
}
_bind() {
this.input.addEventListener('input', debounce(() => this._search(), 250));
this.input.addEventListener('keydown', (e) => this._keydown(e));
this.input.addEventListener('focus', () => {
if (this.input.value.trim().length >= 2 && this.results.childElementCount) {
this.results.style.display = 'block';
}
});
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) this._close();
});
/* ⌘K / Ctrl+K shortcut */
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
this.input.focus();
}
if (e.key === 'Escape') this._close();
});
}
async _search() {
const q = this.input.value.trim();
if (q.length < 2) {
this._close();
return;
}
this._showLoader(true);
try {
const r = await fetch(
`${kingDocsKB.restUrl}search?q=${encodeURIComponent(q)}`,
{ headers: { 'X-WP-Nonce': kingDocsKB.nonce } }
);
const data = await r.json();
this._render(data.results || [], q);
} catch (err) {
console.error('[KNG Docs] search error', err);
} finally {
this._showLoader(false);
}
}
_render(items, query) {
this.activeIdx = -1;
if (!items.length) {
this.results.innerHTML = `
${kingDocsKB.i18n?.noResults || 'No articles found.'}
`;
this.results.style.display = 'block';
return;
}
const re = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
this.results.innerHTML = items.map(i => {
const title = i.title.replace(re, '$1');
return `
${title}
${i.category ? `${i.category}` : ''}
${i.category && i.reading_time ? `` : ''}
${i.reading_time ? `${i.reading_time}` : ''}
`;
}).join('');
this.results.style.display = 'block';
}
_keydown(e) {
const items = $$('.kng-docs-search-result', this.results);
if (!items.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
this.activeIdx = Math.min(this.activeIdx + 1, items.length - 1);
this._highlight(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.activeIdx = Math.max(this.activeIdx - 1, 0);
this._highlight(items);
} else if (e.key === 'Enter' && this.activeIdx >= 0) {
e.preventDefault();
items[this.activeIdx].click();
}
}
_highlight(items) {
items.forEach((el, i) => el.classList.toggle('is-focused', i === this.activeIdx));
if (this.activeIdx >= 0) items[this.activeIdx].scrollIntoView({ block: 'nearest' });
}
_showLoader(show) {
if (this.loader) this.loader.style.display = show ? 'block' : 'none';
if (this.shortcut) this.shortcut.style.display = show ? 'none' : '';
}
_close() {
this.results.style.display = 'none';
this.activeIdx = -1;
}
}
/* ========================================
2. TableOfContents — auto-generated
======================================== */
class TableOfContents {
constructor(contentEl, tocListEl, floatingListEl) {
this.content = contentEl;
this.tocList = tocListEl;
this.floatingList = floatingListEl;
if (!this.content) return;
this.headings = $$('h2, h3, h4', this.content);
if (!this.headings.length) return;
this._build();
this._spy();
}
_build() {
const html = this.headings.map((h, i) => {
if (!h.id) h.id = `kng-heading-${i}`;
const tag = h.tagName.toLowerCase();
return `
${h.textContent}
`;
}).join('');
if (this.tocList) this.tocList.innerHTML = html;
if (this.floatingList) this.floatingList.innerHTML = html;
}
_spy() {
if (!('IntersectionObserver' in window)) return;
const items = [
...(this.tocList ? $$('.kng-docs-toc-item', this.tocList) : []),
...(this.floatingList ? $$('.kng-docs-toc-item', this.floatingList) : []),
];
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const id = e.target.id;
items.forEach(li => li.classList.toggle('is-active', li.dataset.id === id));
}
});
}, { rootMargin: '-80px 0px -70% 0px', threshold: 0 });
this.headings.forEach(h => io.observe(h));
}
}
/* ========================================
3. ScrollReveal
======================================== */
class ScrollReveal {
constructor() {
if (!('IntersectionObserver' in window)) {
$$('.kng-docs-reveal, .kng-docs-reveal-stagger').forEach(el => el.classList.add('is-visible'));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('is-visible');
io.unobserve(e.target);
}
});
}, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
$$('.kng-docs-reveal, .kng-docs-reveal-stagger').forEach(el => io.observe(el));
}
}
/* ========================================
4. Reactions
======================================== */
class Reactions {
constructor(container) {
this.el = container;
if (!this.el) return;
this.postId = this.el.dataset.postId;
this.buttons = $$('.kng-docs-reaction-btn', this.el);
this.thanks = $('.kng-docs-reactions-thanks', this.el);
const stored = getCookie(`kng_docs_reaction_${this.postId}`);
if (stored) {
this._showThanks();
return;
}
this.buttons.forEach(btn => btn.addEventListener('click', () => this._react(btn)));
}
async _react(btn) {
const type = btn.dataset.reaction;
btn.classList.add('is-selected');
try {
await fetch(kingDocsKB.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
action: 'king_docs_reaction',
post_id: this.postId,
reaction: type,
nonce: kingDocsKB.reactionNonce,
}),
});
} catch (err) {
console.error('[KNG Docs] reaction error', err);
}
setCookie(`kng_docs_reaction_${this.postId}`, type, 30);
this._showThanks();
}
_showThanks() {
this.buttons.forEach(btn => btn.style.display = 'none');
if (this.thanks) this.thanks.classList.add('is-visible');
}
}
/* ========================================
5. DarkMode
======================================== */
class DarkMode {
constructor() {
const page = $('.kng-docs-page');
if (!page) return;
this.page = page;
this.mode = page.dataset.kngThemeMode || 'auto'; /* light | dark | auto */
this.toggle = $('.kng-docs-dark-toggle');
this._apply();
if (this.toggle) {
this.toggle.addEventListener('click', () => this._cycle());
}
if (this.mode === 'auto') {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this._apply());
}
}
_apply() {
if (this.mode === 'auto') {
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.page.dataset.kngTheme = systemDark ? 'dark' : 'light';
} else {
this.page.dataset.kngTheme = this.mode;
}
}
_cycle() {
const order = ['light', 'dark', 'auto'];
const idx = order.indexOf(this.mode);
this.mode = order[(idx + 1) % order.length];
this.page.dataset.kngThemeMode = this.mode;
this._apply();
setCookie('kng_docs_theme', this.mode, 365);
}
}
/* ========================================
6. SocialShare
======================================== */
class SocialShare {
constructor() {
$$('.kng-docs-share-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const type = btn.dataset.share;
const url = encodeURIComponent(window.location.href);
const title = encodeURIComponent(document.title);
switch (type) {
case 'copy':
navigator.clipboard.writeText(window.location.href).then(() => {
this._tooltip(btn, 'Copied!');
});
break;
case 'twitter':
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${title}`, '_blank', 'width=600,height=400');
break;
case 'facebook':
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
break;
case 'linkedin':
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=600,height=400');
break;
}
});
});
}
_tooltip(el, text) {
let tip = document.querySelector('.kng-docs-tooltip');
if (!tip) {
tip = document.createElement('div');
tip.className = 'kng-docs-tooltip';
document.body.appendChild(tip);
}
tip.textContent = text;
const rect = el.getBoundingClientRect();
tip.style.left = `${rect.left + rect.width / 2 - 30}px`;
tip.style.top = `${rect.top - 36}px`;
tip.classList.add('is-visible');
setTimeout(() => tip.classList.remove('is-visible'), 1200);
}
}
/* ========================================
7. CopyAnchor
======================================== */
class CopyAnchor {
constructor() {
const content = $('.kng-docs-article-content');
if (!content) return;
$$('h2, h3, h4', content).forEach(h => {
if (!h.id) return;
const btn = document.createElement('button');
btn.className = 'kng-docs-heading-anchor';
btn.setAttribute('aria-label', 'Copy link');
btn.innerHTML = '';
btn.addEventListener('click', () => {
const url = `${window.location.origin}${window.location.pathname}#${h.id}`;
navigator.clipboard.writeText(url);
});
h.style.position = 'relative';
h.appendChild(btn);
});
}
}
/* ========================================
INIT
======================================== */
document.addEventListener('DOMContentLoaded', () => {
/* Search */
const searchEl = $('.kng-docs-search');
if (searchEl) new DocsSearch(searchEl);
/* TOC */
const articleContent = $('.kng-docs-article-content');
const tocList = $('.kng-docs-toc-list');
const floatingList = $('.kng-docs-toc-floating-list');
if (articleContent) new TableOfContents(articleContent, tocList, floatingList);
/* Scroll reveal */
new ScrollReveal();
/* Reactions */
const reactionsEl = $('.kng-docs-reactions');
if (reactionsEl) new Reactions(reactionsEl);
/* Dark mode */
new DarkMode();
/* Social share */
new SocialShare();
/* Copy anchor */
new CopyAnchor();
});
})();