PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.76
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.76
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Docs_KB / assets / frontend.js

frontend.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.76, at includes/extensions/Docs_KB/assets/frontend.js

433 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Docs & Knowledge Base v2 – Apple Liquid-Glass Frontend Scripts
3 *
4 * Modules:
5 * 1. DocsSearch — AJAX live search with debounce & keyboard nav
6 * 2. TableOfContents — auto-generated, hierarchical, sticky, scroll-spy
7 * 3. ScrollReveal — IntersectionObserver fade-in animations
8 * 4. Reactions — emoji reactions (happy / neutral / sad)
9 * 5. DarkMode — light / dark / auto from cookie / system
10 * 6. SocialShare — copy link, Twitter, Facebook, LinkedIn
11 * 7. CopyAnchor — heading anchor copy-to-clipboard
12 *
13 * @package King_Addons
14 */
15
16 (function () {
17 'use strict';
18
19 /* ========================================
20 HELPERS
21 ======================================== */
22 const $ = (sel, ctx = document) => ctx.querySelector(sel);
23 const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
24
25 function debounce(fn, ms = 300) {
26 let t;
27 return (...a) => {
28 clearTimeout(t);
29 t = setTimeout(() => fn.apply(this, a), ms);
30 };
31 }
32
33 function setCookie(n, v, days = 365) {
34 const d = new Date();
35 d.setTime(d.getTime() + days * 864e5);
36 document.cookie = `${n}=${v};expires=${d.toUTCString()};path=/;SameSite=Lax`;
37 }
38
39 function getCookie(n) {
40 return (document.cookie.match(`(^|;)\\s*${n}=([^;]*)`) || [])[2] || '';
41 }
42
43 /* ========================================
44 1. DocsSearch
45 ======================================== */
46 class DocsSearch {
47 constructor(container) {
48 this.container = container;
49 this.input = $('.kng-docs-search-input', container);
50 this.results = $('.kng-docs-search-results', container);
51 this.loader = $('.kng-docs-search-loader', container);
52 this.shortcut = $('.kng-docs-search-shortcut', container);
53 if (!this.input || !this.results) return;
54
55 this.activeIdx = -1;
56 this._bind();
57 }
58
59 _bind() {
60 this.input.addEventListener('input', debounce(() => this._search(), 250));
61 this.input.addEventListener('keydown', (e) => this._keydown(e));
62 this.input.addEventListener('focus', () => {
63 if (this.input.value.trim().length >= 2 && this.results.childElementCount) {
64 this.results.style.display = 'block';
65 }
66 });
67
68 document.addEventListener('click', (e) => {
69 if (!this.container.contains(e.target)) this._close();
70 });
71
72 /* ⌘K / Ctrl+K shortcut */
73 document.addEventListener('keydown', (e) => {
74 if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
75 e.preventDefault();
76 this.input.focus();
77 }
78 if (e.key === 'Escape') this._close();
79 });
80 }
81
82 async _search() {
83 const q = this.input.value.trim();
84 if (q.length < 2) {
85 this._close();
86 return;
87 }
88
89 this._showLoader(true);
90
91 try {
92 const r = await fetch(
93 `${kingDocsKB.restUrl}search?q=${encodeURIComponent(q)}`,
94 { headers: { 'X-WP-Nonce': kingDocsKB.nonce } }
95 );
96 const data = await r.json();
97 this._render(data.results || [], q);
98 } catch (err) {
99 console.error('[KNG Docs] search error', err);
100 } finally {
101 this._showLoader(false);
102 }
103 }
104
105 _render(items, query) {
106 this.activeIdx = -1;
107 if (!items.length) {
108 this.results.innerHTML = `<div class="kng-docs-search-empty">${kingDocsKB.i18n?.noResults || 'No articles found.'}</div>`;
109 this.results.style.display = 'block';
110 return;
111 }
112
113 const re = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
114 this.results.innerHTML = items.map(i => {
115 const title = i.title.replace(re, '<mark>$1</mark>');
116 return `<a class="kng-docs-search-result" href="${i.url}">
117 <span class="kng-docs-search-result-title">${title}</span>
118 <span class="kng-docs-search-result-meta">
119 ${i.category ? `<span>${i.category}</span>` : ''}
120 ${i.category && i.reading_time ? `<span class="kng-docs-dot"></span>` : ''}
121 ${i.reading_time ? `<span>${i.reading_time}</span>` : ''}
122 </span>
123 </a>`;
124 }).join('');
125
126 this.results.style.display = 'block';
127 }
128
129 _keydown(e) {
130 const items = $$('.kng-docs-search-result', this.results);
131 if (!items.length) return;
132
133 if (e.key === 'ArrowDown') {
134 e.preventDefault();
135 this.activeIdx = Math.min(this.activeIdx + 1, items.length - 1);
136 this._highlight(items);
137 } else if (e.key === 'ArrowUp') {
138 e.preventDefault();
139 this.activeIdx = Math.max(this.activeIdx - 1, 0);
140 this._highlight(items);
141 } else if (e.key === 'Enter' && this.activeIdx >= 0) {
142 e.preventDefault();
143 items[this.activeIdx].click();
144 }
145 }
146
147 _highlight(items) {
148 items.forEach((el, i) => el.classList.toggle('is-focused', i === this.activeIdx));
149 if (this.activeIdx >= 0) items[this.activeIdx].scrollIntoView({ block: 'nearest' });
150 }
151
152 _showLoader(show) {
153 if (this.loader) this.loader.style.display = show ? 'block' : 'none';
154 if (this.shortcut) this.shortcut.style.display = show ? 'none' : '';
155 }
156
157 _close() {
158 this.results.style.display = 'none';
159 this.activeIdx = -1;
160 }
161 }
162
163 /* ========================================
164 2. TableOfContents — auto-generated
165 ======================================== */
166 class TableOfContents {
167 constructor(contentEl, tocListEl, floatingListEl) {
168 this.content = contentEl;
169 this.tocList = tocListEl;
170 this.floatingList = floatingListEl;
171 if (!this.content) return;
172
173 this.headings = $$('h2, h3, h4', this.content);
174 if (!this.headings.length) return;
175
176 this._build();
177 this._spy();
178 }
179
180 _build() {
181 const html = this.headings.map((h, i) => {
182 if (!h.id) h.id = `kng-heading-${i}`;
183 const tag = h.tagName.toLowerCase();
184 return `<li class="kng-docs-toc-item kng-docs-toc-${tag}" data-id="${h.id}">
185 <a href="#${h.id}">${h.textContent}</a>
186 </li>`;
187 }).join('');
188
189 if (this.tocList) this.tocList.innerHTML = html;
190 if (this.floatingList) this.floatingList.innerHTML = html;
191 }
192
193 _spy() {
194 if (!('IntersectionObserver' in window)) return;
195
196 const items = [
197 ...(this.tocList ? $$('.kng-docs-toc-item', this.tocList) : []),
198 ...(this.floatingList ? $$('.kng-docs-toc-item', this.floatingList) : []),
199 ];
200
201 const io = new IntersectionObserver((entries) => {
202 entries.forEach(e => {
203 if (e.isIntersecting) {
204 const id = e.target.id;
205 items.forEach(li => li.classList.toggle('is-active', li.dataset.id === id));
206 }
207 });
208 }, { rootMargin: '-80px 0px -70% 0px', threshold: 0 });
209
210 this.headings.forEach(h => io.observe(h));
211 }
212 }
213
214 /* ========================================
215 3. ScrollReveal
216 ======================================== */
217 class ScrollReveal {
218 constructor() {
219 if (!('IntersectionObserver' in window)) {
220 $$('.kng-docs-reveal, .kng-docs-reveal-stagger').forEach(el => el.classList.add('is-visible'));
221 return;
222 }
223
224 const io = new IntersectionObserver((entries) => {
225 entries.forEach(e => {
226 if (e.isIntersecting) {
227 e.target.classList.add('is-visible');
228 io.unobserve(e.target);
229 }
230 });
231 }, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
232
233 $$('.kng-docs-reveal, .kng-docs-reveal-stagger').forEach(el => io.observe(el));
234 }
235 }
236
237 /* ========================================
238 4. Reactions
239 ======================================== */
240 class Reactions {
241 constructor(container) {
242 this.el = container;
243 if (!this.el) return;
244
245 this.postId = this.el.dataset.postId;
246 this.buttons = $$('.kng-docs-reaction-btn', this.el);
247 this.thanks = $('.kng-docs-reactions-thanks', this.el);
248
249 const stored = getCookie(`kng_docs_reaction_${this.postId}`);
250 if (stored) {
251 this._showThanks();
252 return;
253 }
254
255 this.buttons.forEach(btn => btn.addEventListener('click', () => this._react(btn)));
256 }
257
258 async _react(btn) {
259 const type = btn.dataset.reaction;
260 btn.classList.add('is-selected');
261
262 try {
263 await fetch(kingDocsKB.ajaxUrl, {
264 method: 'POST',
265 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
266 body: new URLSearchParams({
267 action: 'king_docs_reaction',
268 post_id: this.postId,
269 reaction: type,
270 nonce: kingDocsKB.reactionNonce,
271 }),
272 });
273 } catch (err) {
274 console.error('[KNG Docs] reaction error', err);
275 }
276
277 setCookie(`kng_docs_reaction_${this.postId}`, type, 30);
278 this._showThanks();
279 }
280
281 _showThanks() {
282 this.buttons.forEach(btn => btn.style.display = 'none');
283 if (this.thanks) this.thanks.classList.add('is-visible');
284 }
285 }
286
287 /* ========================================
288 5. DarkMode
289 ======================================== */
290 class DarkMode {
291 constructor() {
292 const page = $('.kng-docs-page');
293 if (!page) return;
294
295 this.page = page;
296 this.mode = page.dataset.kngThemeMode || 'auto'; /* light | dark | auto */
297 this.toggle = $('.kng-docs-dark-toggle');
298
299 this._apply();
300
301 if (this.toggle) {
302 this.toggle.addEventListener('click', () => this._cycle());
303 }
304
305 if (this.mode === 'auto') {
306 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this._apply());
307 }
308 }
309
310 _apply() {
311 if (this.mode === 'auto') {
312 const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
313 this.page.dataset.kngTheme = systemDark ? 'dark' : 'light';
314 } else {
315 this.page.dataset.kngTheme = this.mode;
316 }
317 }
318
319 _cycle() {
320 const order = ['light', 'dark', 'auto'];
321 const idx = order.indexOf(this.mode);
322 this.mode = order[(idx + 1) % order.length];
323 this.page.dataset.kngThemeMode = this.mode;
324 this._apply();
325 setCookie('kng_docs_theme', this.mode, 365);
326 }
327 }
328
329 /* ========================================
330 6. SocialShare
331 ======================================== */
332 class SocialShare {
333 constructor() {
334 $$('.kng-docs-share-btn').forEach(btn => {
335 btn.addEventListener('click', (e) => {
336 e.preventDefault();
337 const type = btn.dataset.share;
338 const url = encodeURIComponent(window.location.href);
339 const title = encodeURIComponent(document.title);
340
341 switch (type) {
342 case 'copy':
343 navigator.clipboard.writeText(window.location.href).then(() => {
344 this._tooltip(btn, 'Copied!');
345 });
346 break;
347 case 'twitter':
348 window.open(`https://twitter.com/intent/tweet?url=${url}&text=${title}`, '_blank', 'width=600,height=400');
349 break;
350 case 'facebook':
351 window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
352 break;
353 case 'linkedin':
354 window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=600,height=400');
355 break;
356 }
357 });
358 });
359 }
360
361 _tooltip(el, text) {
362 let tip = document.querySelector('.kng-docs-tooltip');
363 if (!tip) {
364 tip = document.createElement('div');
365 tip.className = 'kng-docs-tooltip';
366 document.body.appendChild(tip);
367 }
368 tip.textContent = text;
369 const rect = el.getBoundingClientRect();
370 tip.style.left = `${rect.left + rect.width / 2 - 30}px`;
371 tip.style.top = `${rect.top - 36}px`;
372 tip.classList.add('is-visible');
373 setTimeout(() => tip.classList.remove('is-visible'), 1200);
374 }
375 }
376
377 /* ========================================
378 7. CopyAnchor
379 ======================================== */
380 class CopyAnchor {
381 constructor() {
382 const content = $('.kng-docs-article-content');
383 if (!content) return;
384
385 $$('h2, h3, h4', content).forEach(h => {
386 if (!h.id) return;
387 const btn = document.createElement('button');
388 btn.className = 'kng-docs-heading-anchor';
389 btn.setAttribute('aria-label', 'Copy link');
390 btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
391 btn.addEventListener('click', () => {
392 const url = `${window.location.origin}${window.location.pathname}#${h.id}`;
393 navigator.clipboard.writeText(url);
394 });
395
396 h.style.position = 'relative';
397 h.appendChild(btn);
398 });
399 }
400 }
401
402 /* ========================================
403 INIT
404 ======================================== */
405 document.addEventListener('DOMContentLoaded', () => {
406 /* Search */
407 const searchEl = $('.kng-docs-search');
408 if (searchEl) new DocsSearch(searchEl);
409
410 /* TOC */
411 const articleContent = $('.kng-docs-article-content');
412 const tocList = $('.kng-docs-toc-list');
413 const floatingList = $('.kng-docs-toc-floating-list');
414 if (articleContent) new TableOfContents(articleContent, tocList, floatingList);
415
416 /* Scroll reveal */
417 new ScrollReveal();
418
419 /* Reactions */
420 const reactionsEl = $('.kng-docs-reactions');
421 if (reactionsEl) new Reactions(reactionsEl);
422
423 /* Dark mode */
424 new DarkMode();
425
426 /* Social share */
427 new SocialShare();
428
429 /* Copy anchor */
430 new CopyAnchor();
431 });
432 })();
433