PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
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.49, at includes/extensions/Docs_KB/assets/frontend.js

521 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Docs & Knowledge Base - Frontend JavaScript
3 * Live search, TOC generation, smooth scroll, analytics
4 *
5 * @package King_Addons
6 */
7
8 (function() {
9 'use strict';
10
11 // Configuration
12 const CONFIG = {
13 searchDebounce: 300,
14 scrollOffset: 80,
15 stickyOffset: 100
16 };
17
18 // API endpoints
19 const API = {
20 search: kngDocsKB?.restUrl + 'search' || '/wp-json/king-addons/v1/docs/search',
21 nonce: kngDocsKB?.nonce || ''
22 };
23
24 /**
25 * Debounce helper
26 */
27 function debounce(func, wait) {
28 let timeout;
29 return function executedFunction(...args) {
30 const later = () => {
31 clearTimeout(timeout);
32 func(...args);
33 };
34 clearTimeout(timeout);
35 timeout = setTimeout(later, wait);
36 };
37 }
38
39 /**
40 * Live Search
41 */
42 class DocsSearch {
43 constructor(container) {
44 this.container = container;
45 this.input = container.querySelector('.kng-docs-search-input');
46 this.results = container.querySelector('.kng-docs-search-results');
47 this.loader = container.querySelector('.kng-docs-search-loader');
48
49 if (!this.input || !this.results) return;
50
51 this.minChars = parseInt(this.input.dataset.minChars) || 2;
52 this.categoryId = this.input.dataset.category || '';
53
54 this.init();
55 }
56
57 init() {
58 this.input.addEventListener('input', debounce(() => this.handleSearch(), CONFIG.searchDebounce));
59 this.input.addEventListener('focus', () => this.showResults());
60
61 // Close on outside click
62 document.addEventListener('click', (e) => {
63 if (!this.container.contains(e.target)) {
64 this.hideResults();
65 }
66 });
67
68 // Keyboard navigation
69 this.input.addEventListener('keydown', (e) => this.handleKeyboard(e));
70 }
71
72 async handleSearch() {
73 const query = this.input.value.trim();
74
75 if (query.length < this.minChars) {
76 this.hideResults();
77 return;
78 }
79
80 this.showLoader();
81
82 try {
83 let url = `${API.search}?s=${encodeURIComponent(query)}&per_page=10`;
84 if (this.categoryId) {
85 url += `&category=${this.categoryId}`;
86 }
87
88 const response = await fetch(url, {
89 headers: {
90 'X-WP-Nonce': API.nonce
91 }
92 });
93
94 const data = await response.json();
95 this.renderResults(data);
96 } catch (error) {
97 console.error('Search error:', error);
98 this.renderEmpty();
99 }
100
101 this.hideLoader();
102 }
103
104 renderResults(results) {
105 if (!results || results.length === 0) {
106 this.renderEmpty();
107 return;
108 }
109
110 const html = results.map(item => `
111 <a href="${item.url}" class="kng-docs-search-result">
112 <div class="kng-docs-search-result-title">${item.title}</div>
113 ${item.category ? `<div class="kng-docs-search-result-category">${item.category}</div>` : ''}
114 </a>
115 `).join('');
116
117 this.results.innerHTML = html;
118 this.showResults();
119 }
120
121 renderEmpty() {
122 this.results.innerHTML = `
123 <div class="kng-docs-search-empty">
124 ${kngDocsKB?.i18n?.noResults || 'No results found'}
125 </div>
126 `;
127 this.showResults();
128 }
129
130 showResults() {
131 if (this.input.value.trim().length >= this.minChars) {
132 this.results.style.display = 'block';
133 }
134 }
135
136 hideResults() {
137 this.results.style.display = 'none';
138 }
139
140 showLoader() {
141 if (this.loader) {
142 this.loader.style.display = 'flex';
143 }
144 }
145
146 hideLoader() {
147 if (this.loader) {
148 this.loader.style.display = 'none';
149 }
150 }
151
152 handleKeyboard(e) {
153 const items = this.results.querySelectorAll('.kng-docs-search-result');
154 const activeItem = this.results.querySelector('.kng-docs-search-result:focus');
155 const activeIndex = activeItem ? Array.from(items).indexOf(activeItem) : -1;
156
157 switch (e.key) {
158 case 'ArrowDown':
159 e.preventDefault();
160 if (activeIndex < items.length - 1) {
161 items[activeIndex + 1].focus();
162 } else if (activeIndex === -1 && items.length > 0) {
163 items[0].focus();
164 }
165 break;
166
167 case 'ArrowUp':
168 e.preventDefault();
169 if (activeIndex > 0) {
170 items[activeIndex - 1].focus();
171 } else {
172 this.input.focus();
173 }
174 break;
175
176 case 'Escape':
177 this.hideResults();
178 this.input.blur();
179 break;
180 }
181 }
182 }
183
184 /**
185 * Table of Contents Generator
186 */
187 class TableOfContents {
188 constructor(contentEl, tocEl, floatingTocEl = null) {
189 this.content = contentEl;
190 this.toc = tocEl;
191 this.floatingToc = floatingTocEl;
192
193 if (!this.content) return;
194
195 this.headings = this.content.dataset.tocHeadings || 'h2,h3';
196 this.init();
197 }
198
199 init() {
200 const elements = this.content.querySelectorAll(this.headings);
201
202 if (elements.length === 0) {
203 if (this.toc) this.toc.style.display = 'none';
204 return;
205 }
206
207 const tocItems = [];
208
209 elements.forEach((el, index) => {
210 const id = `toc-${index}`;
211 el.id = id;
212
213 const level = el.tagName.toLowerCase();
214 const text = el.textContent;
215
216 tocItems.push({
217 id,
218 text,
219 level
220 });
221 });
222
223 const html = this.generateHTML(tocItems);
224
225 if (this.toc) {
226 const list = this.toc.querySelector('.kng-docs-toc-list');
227 if (list) {
228 list.innerHTML = html;
229 this.toc.style.display = '';
230 }
231 }
232
233 if (this.floatingToc) {
234 this.floatingToc.innerHTML = html;
235 }
236
237 // Initialize scroll spy
238 this.initScrollSpy(tocItems);
239
240 // Smooth scroll
241 this.initSmoothScroll();
242 }
243
244 generateHTML(items) {
245 return items.map(item => `
246 <li class="kng-docs-toc-item kng-docs-toc-${item.level}">
247 <a href="#${item.id}">${item.text}</a>
248 </li>
249 `).join('');
250 }
251
252 initScrollSpy(items) {
253 const observer = new IntersectionObserver((entries) => {
254 entries.forEach(entry => {
255 const link = document.querySelector(`.kng-docs-toc-item a[href="#${entry.target.id}"]`);
256 if (link) {
257 if (entry.isIntersecting) {
258 // Remove active from all
259 document.querySelectorAll('.kng-docs-toc-item').forEach(item => {
260 item.classList.remove('is-active');
261 });
262 link.parentElement.classList.add('is-active');
263 }
264 }
265 });
266 }, {
267 rootMargin: '-100px 0px -66%',
268 threshold: 0
269 });
270
271 items.forEach(item => {
272 const el = document.getElementById(item.id);
273 if (el) observer.observe(el);
274 });
275 }
276
277 initSmoothScroll() {
278 document.querySelectorAll('.kng-docs-toc-list a').forEach(link => {
279 link.addEventListener('click', (e) => {
280 e.preventDefault();
281 const targetId = link.getAttribute('href').slice(1);
282 const target = document.getElementById(targetId);
283
284 if (target) {
285 const offsetTop = target.getBoundingClientRect().top + window.pageYOffset - CONFIG.scrollOffset;
286 window.scrollTo({
287 top: offsetTop,
288 behavior: 'smooth'
289 });
290
291 // Update URL without scrolling
292 history.pushState(null, '', link.getAttribute('href'));
293 }
294 });
295 });
296 }
297 }
298
299 /**
300 * Article Feedback
301 */
302 class ArticleFeedback {
303 constructor(container) {
304 this.container = container;
305 this.docId = container.dataset.docId;
306 this.buttons = container.querySelector('.kng-docs-feedback-buttons');
307 this.thanks = container.querySelector('.kng-docs-feedback-thanks');
308 this.question = container.querySelector('.kng-docs-feedback-question');
309
310 if (!this.buttons) return;
311
312 this.init();
313 }
314
315 init() {
316 const btns = this.container.querySelectorAll('.kng-docs-feedback-btn');
317
318 btns.forEach(btn => {
319 btn.addEventListener('click', () => this.submitFeedback(btn.dataset.value));
320 });
321 }
322
323 async submitFeedback(value) {
324 // Check if already submitted
325 if (localStorage.getItem(`kng_doc_feedback_${this.docId}`)) {
326 this.showThanks();
327 return;
328 }
329
330 try {
331 const formData = new FormData();
332 formData.append('action', 'king_addons_docs_feedback');
333 formData.append('doc_id', this.docId);
334 formData.append('feedback', value);
335 formData.append('nonce', kngDocsKB?.feedbackNonce || '');
336
337 await fetch(kngDocsKB?.ajaxUrl || '/wp-admin/admin-ajax.php', {
338 method: 'POST',
339 body: formData
340 });
341
342 // Store in localStorage to prevent duplicate submissions
343 localStorage.setItem(`kng_doc_feedback_${this.docId}`, value);
344
345 this.showThanks();
346 } catch (error) {
347 console.error('Feedback error:', error);
348 }
349 }
350
351 showThanks() {
352 if (this.buttons) this.buttons.style.display = 'none';
353 if (this.question) this.question.style.display = 'none';
354 if (this.thanks) this.thanks.style.display = 'flex';
355 }
356 }
357
358 /**
359 * Sticky Elements
360 */
361 class StickyElement {
362 constructor(element) {
363 this.element = element;
364 this.isSticky = element.classList.contains('is-sticky');
365
366 if (!this.isSticky) return;
367
368 this.init();
369 }
370
371 init() {
372 const inner = this.element.querySelector('.kng-docs-sidebar-inner, .kng-docs-toc-floating-inner');
373
374 if (inner) {
375 inner.style.position = 'sticky';
376 inner.style.top = `${CONFIG.stickyOffset}px`;
377 }
378 }
379 }
380
381 /**
382 * Copy Code Button
383 */
384 class CopyCode {
385 constructor() {
386 this.init();
387 }
388
389 init() {
390 const codeBlocks = document.querySelectorAll('.kng-docs-article-content pre');
391
392 codeBlocks.forEach(block => {
393 const button = document.createElement('button');
394 button.className = 'kng-docs-copy-btn';
395 button.innerHTML = `
396 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
397 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
398 <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
399 </svg>
400 `;
401 button.title = kngDocsKB?.i18n?.copy || 'Copy';
402
403 block.style.position = 'relative';
404 block.appendChild(button);
405
406 button.addEventListener('click', () => this.copyToClipboard(block, button));
407 });
408 }
409
410 async copyToClipboard(block, button) {
411 const code = block.querySelector('code') || block;
412 const text = code.textContent;
413
414 try {
415 await navigator.clipboard.writeText(text);
416 button.classList.add('copied');
417 button.innerHTML = `
418 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
419 <polyline points="20 6 9 17 4 12"/>
420 </svg>
421 `;
422
423 setTimeout(() => {
424 button.classList.remove('copied');
425 button.innerHTML = `
426 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
427 <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
428 <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
429 </svg>
430 `;
431 }, 2000);
432 } catch (error) {
433 console.error('Copy failed:', error);
434 }
435 }
436 }
437
438 /**
439 * Reading Progress
440 */
441 class ReadingProgress {
442 constructor(article) {
443 this.article = article;
444
445 if (!this.article) return;
446
447 this.init();
448 }
449
450 init() {
451 const progressBar = document.createElement('div');
452 progressBar.className = 'kng-docs-reading-progress';
453 progressBar.innerHTML = '<div class="kng-docs-reading-progress-bar"></div>';
454 document.body.appendChild(progressBar);
455
456 const bar = progressBar.querySelector('.kng-docs-reading-progress-bar');
457
458 window.addEventListener('scroll', () => {
459 const articleRect = this.article.getBoundingClientRect();
460 const articleTop = articleRect.top + window.pageYOffset;
461 const articleHeight = this.article.offsetHeight;
462 const windowHeight = window.innerHeight;
463 const scrollTop = window.pageYOffset;
464
465 const start = articleTop - windowHeight;
466 const end = articleTop + articleHeight;
467 const progress = (scrollTop - start) / (end - start);
468
469 bar.style.width = `${Math.min(Math.max(progress * 100, 0), 100)}%`;
470 });
471 }
472 }
473
474 /**
475 * Initialize
476 */
477 function init() {
478 // Search
479 document.querySelectorAll('.kng-docs-search').forEach(el => {
480 new DocsSearch(el);
481 });
482
483 // Table of Contents
484 const content = document.querySelector('.kng-docs-article-content');
485 const toc = document.getElementById('kng-docs-toc');
486 const floatingToc = document.querySelector('.kng-docs-toc-floating-list');
487
488 if (content) {
489 new TableOfContents(content, toc, floatingToc);
490 }
491
492 // Feedback
493 document.querySelectorAll('.kng-docs-feedback').forEach(el => {
494 new ArticleFeedback(el);
495 });
496
497 // Sticky elements
498 document.querySelectorAll('.kng-docs-sidebar, .kng-docs-toc-floating').forEach(el => {
499 new StickyElement(el);
500 });
501
502 // Copy code buttons
503 if (document.querySelector('.kng-docs-article-content pre')) {
504 new CopyCode();
505 }
506
507 // Reading progress (optional)
508 const article = document.querySelector('.kng-docs-article');
509 if (article && kngDocsKB?.showReadingProgress) {
510 new ReadingProgress(article);
511 }
512 }
513
514 // Run on DOM ready
515 if (document.readyState === 'loading') {
516 document.addEventListener('DOMContentLoaded', init);
517 } else {
518 init();
519 }
520 })();
521