PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / public / js / code-snippet-tab.js

code-snippet-tab.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/public/js/code-snippet-tab.js

331 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * BetterDocs Code Snippet Tab Frontend JavaScript
3 *
4 * Reuses the Code Snippet shell (wrapper / panels / copy button) but switches
5 * panels via a row of HTTP-status tabs instead of a language dropdown. Handles
6 * self-hosted highlight.js + copy-to-clipboard, scoped to
7 * `.betterdocs-code-snippet-tab` wrappers so it stays independent of the Code
8 * Snippet script (either may be absent on a given page).
9 */
10
11 (function () {
12 'use strict';
13
14 window.BetterDocsCodeSnippetTab = window.BetterDocsCodeSnippetTab || {};
15
16 const localConfig = window.BetterDocsCodeSnippetTabConfig || window.BetterDocsCodeSnippetConfig || {};
17 const config = {
18 highlightJsUrl: localConfig.hljsUrl || '',
19 highlightJsCssLight: localConfig.cssLight || '',
20 highlightJsCssDark: localConfig.cssDark || '',
21 copySuccessTimeout: 2000
22 };
23
24 let highlightJsLoaded = false;
25 let highlightJsLoading = false;
26
27 function loadScript(src, callback) {
28 const script = document.createElement('script');
29 script.src = src;
30 script.onload = callback;
31 script.onerror = function () {
32 console.warn('BetterDocs Code Snippet Tab: Failed to load script:', src);
33 if (callback) callback(false);
34 };
35 document.head.appendChild(script);
36 }
37
38 function loadStylesheet(href) {
39 if (!href || document.querySelector(`link[href="${href}"]`)) return;
40 const link = document.createElement('link');
41 link.rel = 'stylesheet';
42 link.href = href;
43 document.head.appendChild(link);
44 }
45
46 function loadHighlightJs(callback) {
47 if (highlightJsLoaded || window.hljs) {
48 highlightJsLoaded = true;
49 if (callback) callback(true);
50 return;
51 }
52 if (highlightJsLoading) {
53 const checkLoaded = setInterval(function () {
54 if (highlightJsLoaded || !highlightJsLoading) {
55 clearInterval(checkLoaded);
56 if (callback) callback(highlightJsLoaded);
57 }
58 }, 100);
59 return;
60 }
61 if (!config.highlightJsUrl) {
62 if (callback) callback(false);
63 return;
64 }
65
66 highlightJsLoading = true;
67
68 const wrappers = document.querySelectorAll('.betterdocs-code-snippet-tab');
69 let hasDark = false;
70 let hasLight = false;
71 wrappers.forEach(function (w) {
72 if (w.classList.contains('theme-dark')) hasDark = true;
73 if (w.classList.contains('theme-light')) hasLight = true;
74 });
75 if (hasDark) loadStylesheet(config.highlightJsCssDark);
76 if (hasLight) loadStylesheet(config.highlightJsCssLight);
77
78 loadScript(config.highlightJsUrl, function (success) {
79 highlightJsLoading = false;
80 highlightJsLoaded = !!(success && window.hljs);
81 if (callback) callback(highlightJsLoaded);
82 });
83 }
84
85 function applySyntaxHighlighting() {
86 if (!window.hljs) return;
87 // Highlight the inner <code>, not the <pre>: hljs replaces the innerHTML
88 // of whatever it is handed, and eating the <code> breaks the copy button.
89 const codeBlocks = document.querySelectorAll('.betterdocs-code-snippet-tab .betterdocs-code-snippet-code > code:not(.hljs)');
90 codeBlocks.forEach(function (block) {
91 try {
92 window.hljs.highlightElement(block);
93 } catch (error) {
94 console.warn('BetterDocs Code Snippet Tab: Error highlighting code block:', error);
95 }
96 });
97 }
98
99 function copyToClipboard(text, callback) {
100 if (navigator.clipboard && window.isSecureContext) {
101 navigator.clipboard.writeText(text).then(function () {
102 if (callback) callback(true);
103 }).catch(function () {
104 fallbackCopyToClipboard(text, callback);
105 });
106 } else {
107 fallbackCopyToClipboard(text, callback);
108 }
109 }
110
111 function fallbackCopyToClipboard(text, callback) {
112 const textArea = document.createElement('textarea');
113 textArea.value = text;
114 textArea.style.position = 'fixed';
115 textArea.style.left = '-999999px';
116 textArea.style.top = '-999999px';
117 document.body.appendChild(textArea);
118 try {
119 textArea.focus();
120 textArea.select();
121 const successful = document.execCommand('copy');
122 if (callback) callback(successful);
123 } catch (error) {
124 if (callback) callback(false);
125 } finally {
126 document.body.removeChild(textArea);
127 }
128 }
129
130 function getTooltip(button) {
131 const container = button.closest('.betterdocs-code-snippet-copy-container');
132 return container ? container.querySelector('.betterdocs-code-snippet-tooltip') : null;
133 }
134
135 /**
136 * Flip the button into a transient state ("copied" / "error") and back.
137 * Class + text driven, so repeated clicks can't corrupt the icon markup.
138 */
139 function flashState(button, state, label) {
140 const tooltip = getTooltip(button);
141
142 clearTimeout(button._betterDocsResetTimer);
143
144 button.classList.remove('is-copied', 'is-error');
145 button.classList.add(state);
146
147 if (tooltip) {
148 tooltip.textContent = label;
149 tooltip.classList.add('show', state);
150 }
151
152 button._betterDocsResetTimer = setTimeout(function () {
153 button.classList.remove('is-copied', 'is-error');
154 if (tooltip) {
155 tooltip.textContent = tooltip.dataset.copyLabel || 'Copy';
156 tooltip.classList.remove('is-copied', 'is-error');
157 if (!button.matches(':hover') && !button.matches(':focus-visible')) {
158 tooltip.classList.remove('show');
159 }
160 }
161 }, config.copySuccessTimeout);
162 }
163
164 function showCopySuccess(button) {
165 const tooltip = getTooltip(button);
166 flashState(button, 'is-copied', tooltip ? (tooltip.dataset.copiedLabel || 'Copied!') : '');
167 }
168
169 function showCopyError(button) {
170 const tooltip = getTooltip(button);
171 flashState(button, 'is-error', tooltip ? (tooltip.dataset.errorLabel || 'Copy failed') : '');
172 }
173
174 /**
175 * Hover/focus tooltip. Placement is pure CSS, so nothing to reposition.
176 */
177 function initTooltip(button) {
178 const tooltip = getTooltip(button);
179 if (!tooltip) return;
180
181 const show = function () {
182 tooltip.classList.add('show');
183 };
184 const hide = function () {
185 if (button.classList.contains('is-copied') || button.classList.contains('is-error')) {
186 return;
187 }
188 tooltip.classList.remove('show');
189 };
190
191 button.addEventListener('mouseenter', show);
192 button.addEventListener('focus', show);
193 button.addEventListener('mouseleave', hide);
194 button.addEventListener('blur', hide);
195 }
196
197 /**
198 * The code element of the currently-active status panel. hljs may have
199 * replaced the inner <code>, so accept the <pre> too — same `textContent`.
200 */
201 function getActiveCodeElement(snippet) {
202 const panel = snippet.querySelector('.betterdocs-code-snippet-panel.is-active')
203 || snippet.querySelector('.betterdocs-code-snippet-panel')
204 || snippet;
205
206 return panel.querySelector('.betterdocs-code-snippet-code code')
207 || panel.querySelector('.betterdocs-code-snippet-code');
208 }
209
210 function initCopyButton(snippet) {
211 const copyButton = snippet.querySelector('.betterdocs-code-snippet-copy-button');
212 if (!copyButton) return;
213
214 // The Code Snippet script matches this wrapper too — bind once.
215 if (copyButton._betterDocsCopyInitialized) return;
216 copyButton._betterDocsCopyInitialized = true;
217
218 initTooltip(copyButton);
219
220 copyButton._betterDocsClickHandler = function (event) {
221 event.preventDefault();
222 const codeElement = getActiveCodeElement(snippet);
223 const codeText = codeElement ? (codeElement.textContent || codeElement.innerText || '') : '';
224 if (!codeText.trim()) {
225 showCopyError(copyButton);
226 return;
227 }
228 copyToClipboard(codeText, function (success) {
229 if (success) {
230 showCopySuccess(copyButton);
231 } else {
232 showCopyError(copyButton);
233 }
234 });
235 };
236 copyButton.addEventListener('click', copyButton._betterDocsClickHandler);
237 }
238
239 /**
240 * Switch the active status panel + tab.
241 */
242 function setActivePanel(snippet, index) {
243 const panels = snippet.querySelectorAll('.betterdocs-code-snippet-panel');
244 const tabs = snippet.querySelectorAll('.betterdocs-code-snippet-tab-item');
245
246 panels.forEach(function (panel) {
247 const isActive = String(panel.dataset.tabIndex) === String(index);
248 panel.classList.toggle('is-active', isActive);
249 if (isActive) {
250 panel.removeAttribute('hidden');
251 const code = panel.querySelector('.betterdocs-code-snippet-code');
252 if (code) {
253 snippet.dataset.language = (code.className.match(/language-([\w-]+)/) || [])[1] || snippet.dataset.language;
254 }
255 } else {
256 panel.setAttribute('hidden', '');
257 }
258 });
259
260 tabs.forEach(function (tab) {
261 const isActive = String(tab.dataset.tabIndex) === String(index);
262 tab.classList.toggle('is-active', isActive);
263 tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
264 });
265 }
266
267 function initTabs(snippet) {
268 const tabs = snippet.querySelectorAll('.betterdocs-code-snippet-tab-item');
269 if (!tabs.length || snippet._betterDocsTabsInitialized) return;
270 snippet._betterDocsTabsInitialized = true;
271
272 tabs.forEach(function (tab) {
273 tab.addEventListener('click', function () {
274 setActivePanel(snippet, tab.dataset.tabIndex);
275 });
276 });
277 }
278
279 function initializeCodeSnippetTabs() {
280 const snippets = document.querySelectorAll('.betterdocs-code-snippet-tab');
281 if (snippets.length === 0) return;
282
283 loadHighlightJs(function (loaded) {
284 if (loaded) applySyntaxHighlighting();
285 });
286
287 snippets.forEach(function (snippet) {
288 if (snippet.dataset.copyButton === 'true') {
289 initCopyButton(snippet);
290 }
291 initTabs(snippet);
292 });
293 }
294
295 window.BetterDocsCodeSnippetTab = {
296 init: initializeCodeSnippetTabs,
297 reinit: initializeCodeSnippetTabs,
298 initCopyButton: initCopyButton,
299 applySyntaxHighlighting: applySyntaxHighlighting,
300 loadHighlightJs: loadHighlightJs
301 };
302
303 if (document.readyState === 'loading') {
304 document.addEventListener('DOMContentLoaded', initializeCodeSnippetTabs);
305 } else {
306 initializeCodeSnippetTabs();
307 }
308
309 if (window.MutationObserver) {
310 const observer = new MutationObserver(function (mutations) {
311 let shouldReinit = false;
312 mutations.forEach(function (mutation) {
313 if (mutation.type === 'childList') {
314 mutation.addedNodes.forEach(function (node) {
315 if (node.nodeType === 1) {
316 if (node.classList && node.classList.contains('betterdocs-code-snippet-tab')) {
317 shouldReinit = true;
318 } else if (node.querySelector && node.querySelector('.betterdocs-code-snippet-tab')) {
319 shouldReinit = true;
320 }
321 }
322 });
323 }
324 });
325 if (shouldReinit) setTimeout(initializeCodeSnippetTabs, 100);
326 });
327 observer.observe(document.body, { childList: true, subtree: true });
328 }
329
330 })();
331