PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 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 All 41 releases
← All changes | includes/extensions/Docs_KB/assets/frontend.js +321 -409 51.1.44 → 51.1.87 View file →
@@ -1,520 +1,432 @@
1 1 /**
2 - * Docs & Knowledge Base - Frontend JavaScript
3 - * Live search, TOC generation, smooth scroll, analytics
2 + * Docs & Knowledge Base v2 – Apple Liquid-Glass Frontend Scripts
4 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 + *
5 13 * @package King_Addons
6 14 */
7 15
8 -(function() {
16 +(function () {
9 17 'use strict';
10 18
11 - // Configuration
12 - const CONFIG = {
13 - searchDebounce: 300,
14 - scrollOffset: 80,
15 - stickyOffset: 100
16 - };
19 + /* ========================================
20 + HELPERS
21 + ======================================== */
22 + const $ = (sel, ctx = document) => ctx.querySelector(sel);
23 + const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
17 24
18 - // API endpoints
19 - const API = {
20 - search: kngDocsKB?.restUrl + 'search' || '/wp-json/king-addons/v1/docs/search',
21 - nonce: kngDocsKB?.nonce || ''
22 - };
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 + }
23 32
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 - };
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 37 }
38 38
39 - /**
40 - * Live Search
41 - */
39 + function getCookie(n) {
40 + return (document.cookie.match(`(^|;)\\s*${n}=([^;]*)`) || [])[2] || '';
41 + }
42 +
43 + /* ========================================
44 + 1. DocsSearch
45 + ======================================== */
42 46 class DocsSearch {
43 47 constructor(container) {
44 48 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 + 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);
49 53 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();
54 +
55 + this.activeIdx = -1;
56 + this._bind();
55 57 }
56 58
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
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 +
62 68 document.addEventListener('click', (e) => {
63 - if (!this.container.contains(e.target)) {
64 - this.hideResults();
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();
65 77 }
78 + if (e.key === 'Escape') this._close();
66 79 });
67 -
68 - // Keyboard navigation
69 - this.input.addEventListener('keydown', (e) => this.handleKeyboard(e));
70 80 }
71 81
72 - async handleSearch() {
73 - const query = this.input.value.trim();
74 -
75 - if (query.length < this.minChars) {
76 - this.hideResults();
82 + async _search() {
83 + const q = this.input.value.trim();
84 + if (q.length < 2) {
85 + this._close();
77 86 return;
78 87 }
79 88
80 - this.showLoader();
89 + this._showLoader(true);
81 90
82 91 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();
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);
99 102 }
100 -
101 - this.hideLoader();
102 103 }
103 104
104 - renderResults(results) {
105 - if (!results || results.length === 0) {
106 - this.renderEmpty();
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';
107 110 return;
108 111 }
109 112
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('');
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('');
116 125
117 - this.results.innerHTML = html;
118 - this.showResults();
126 + this.results.style.display = 'block';
119 127 }
120 128
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 + _keydown(e) {
130 + const items = $$('.kng-docs-search-result', this.results);
131 + if (!items.length) return;
129 132
130 - showResults() {
131 - if (this.input.value.trim().length >= this.minChars) {
132 - this.results.style.display = 'block';
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();
133 144 }
134 145 }
135 146
136 - hideResults() {
137 - this.results.style.display = 'none';
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' });
138 150 }
139 151
140 - showLoader() {
141 - if (this.loader) {
142 - this.loader.style.display = 'flex';
143 - }
152 + _showLoader(show) {
153 + if (this.loader) this.loader.style.display = show ? 'block' : 'none';
154 + if (this.shortcut) this.shortcut.style.display = show ? 'none' : '';
144 155 }
145 156
146 - hideLoader() {
147 - if (this.loader) {
148 - this.loader.style.display = 'none';
149 - }
157 + _close() {
158 + this.results.style.display = 'none';
159 + this.activeIdx = -1;
150 160 }
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 161 }
183 162
184 - /**
185 - * Table of Contents Generator
186 - */
163 + /* ========================================
164 + 2. TableOfContents — auto-generated
165 + ======================================== */
187 166 class TableOfContents {
188 - constructor(contentEl, tocEl, floatingTocEl = null) {
167 + constructor(contentEl, tocListEl, floatingListEl) {
189 168 this.content = contentEl;
190 - this.toc = tocEl;
191 - this.floatingToc = floatingTocEl;
192 -
169 + this.tocList = tocListEl;
170 + this.floatingList = floatingListEl;
193 171 if (!this.content) return;
194 -
195 - this.headings = this.content.dataset.tocHeadings || 'h2,h3';
196 - this.init();
197 - }
198 172
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 - }
173 + this.headings = $$('h2, h3, h4', this.content);
174 + if (!this.headings.length) return;
206 175
207 - const tocItems = [];
176 + this._build();
177 + this._spy();
178 + }
208 179
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;
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('');
215 188
216 - tocItems.push({
217 - id,
218 - text,
219 - level
220 - });
221 - });
189 + if (this.tocList) this.tocList.innerHTML = html;
190 + if (this.floatingList) this.floatingList.innerHTML = html;
191 + }
222 192
223 - const html = this.generateHTML(tocItems);
193 + _spy() {
194 + if (!('IntersectionObserver' in window)) return;
224 195
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 - }
196 + const items = [
197 + ...(this.tocList ? $$('.kng-docs-toc-item', this.tocList) : []),
198 + ...(this.floatingList ? $$('.kng-docs-toc-item', this.floatingList) : []),
199 + ];
232 200
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 - }
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));
264 206 }
265 207 });
266 - }, {
267 - rootMargin: '-100px 0px -66%',
268 - threshold: 0
269 - });
208 + }, { rootMargin: '-80px 0px -70% 0px', threshold: 0 });
270 209
271 - items.forEach(item => {
272 - const el = document.getElementById(item.id);
273 - if (el) observer.observe(el);
274 - });
210 + this.headings.forEach(h => io.observe(h));
275 211 }
212 + }
276 213
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 - });
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 + }
290 223
291 - // Update URL without scrolling
292 - history.pushState(null, '', link.getAttribute('href'));
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);
293 229 }
294 230 });
295 - });
231 + }, { threshold: 0.08, rootMargin: '0px 0px -40px 0px' });
232 +
233 + $$('.kng-docs-reveal, .kng-docs-reveal-stagger').forEach(el => io.observe(el));
296 234 }
297 235 }
298 236
299 - /**
300 - * Article Feedback
301 - */
302 - class ArticleFeedback {
237 + /* ========================================
238 + 4. Reactions
239 + ======================================== */
240 + class Reactions {
303 241 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 - }
242 + this.el = container;
243 + if (!this.el) return;
314 244
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 - }
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);
322 248
323 - async submitFeedback(value) {
324 - // Check if already submitted
325 - if (localStorage.getItem(`kng_doc_feedback_${this.docId}`)) {
326 - this.showThanks();
249 + const stored = getCookie(`kng_docs_reaction_${this.postId}`);
250 + if (stored) {
251 + this._showThanks();
327 252 return;
328 253 }
329 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 +
330 262 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', {
263 + await fetch(kingDocsKB.ajaxUrl, {
338 264 method: 'POST',
339 - body: formData
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 + }),
340 272 });
273 + } catch (err) {
274 + // console.error('[KNG Docs] reaction error', err);
275 + }
341 276
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 - }
277 + setCookie(`kng_docs_reaction_${this.postId}`, type, 30);
278 + this._showThanks();
349 279 }
350 280
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';
281 + _showThanks() {
282 + this.buttons.forEach(btn => btn.style.display = 'none');
283 + if (this.thanks) this.thanks.classList.add('is-visible');
355 284 }
356 285 }
357 286
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();
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 + }
369 308 }
370 309
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`;
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;
377 316 }
378 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 + }
379 327 }
380 328
381 - /**
382 - * Copy Code Button
383 - */
384 - class CopyCode {
329 + /* ========================================
330 + 6. SocialShare
331 + ======================================== */
332 + class SocialShare {
385 333 constructor() {
386 - this.init();
387 - }
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);
388 340
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));
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 + });
407 358 });
408 359 }
409 360
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);
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);
434 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);
435 374 }
436 375 }
437 376
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 - }
377 + /* ========================================
378 + 7. CopyAnchor
379 + ======================================== */
380 + class CopyAnchor {
381 + constructor() {
382 + const content = $('.kng-docs-article-content');
383 + if (!content) return;
449 384
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);
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 + });
455 395
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)}%`;
396 + h.style.position = 'relative';
397 + h.appendChild(btn);
470 398 });
471 399 }
472 400 }
473 401
474 - /**
475 - * Initialize
476 - */
477 - function init() {
478 - // Search
479 - document.querySelectorAll('.kng-docs-search').forEach(el => {
480 - new DocsSearch(el);
481 - });
402 + /* ========================================
403 + INIT
404 + ======================================== */
405 + document.addEventListener('DOMContentLoaded', () => {
406 + /* Search */
407 + const searchEl = $('.kng-docs-search');
408 + if (searchEl) new DocsSearch(searchEl);
482 409
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 - }
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);
491 415
492 - // Feedback
493 - document.querySelectorAll('.kng-docs-feedback').forEach(el => {
494 - new ArticleFeedback(el);
495 - });
416 + /* Scroll reveal */
417 + new ScrollReveal();
496 418
497 - // Sticky elements
498 - document.querySelectorAll('.kng-docs-sidebar, .kng-docs-toc-floating').forEach(el => {
499 - new StickyElement(el);
500 - });
419 + /* Reactions */
420 + const reactionsEl = $('.kng-docs-reactions');
421 + if (reactionsEl) new Reactions(reactionsEl);
501 422
502 - // Copy code buttons
503 - if (document.querySelector('.kng-docs-article-content pre')) {
504 - new CopyCode();
505 - }
423 + /* Dark mode */
424 + new DarkMode();
506 425
507 - // Reading progress (optional)
508 - const article = document.querySelector('.kng-docs-article');
509 - if (article && kngDocsKB?.showReadingProgress) {
510 - new ReadingProgress(article);
511 - }
512 - }
426 + /* Social share */
427 + new SocialShare();
513 428
514 - // Run on DOM ready
515 - if (document.readyState === 'loading') {
516 - document.addEventListener('DOMContentLoaded', init);
517 - } else {
518 - init();
519 - }
429 + /* Copy anchor */
430 + new CopyAnchor();
431 + });
520 432 })();