|
a |
|
b/web/docs/docs.js |
|
|
1 |
/** |
|
|
2 |
* DNAnalyzer - Documentation Page JavaScript |
|
|
3 |
* Handles navigation, search, and interactive elements |
|
|
4 |
*/ |
|
|
5 |
|
|
|
6 |
document.addEventListener('DOMContentLoaded', function() { |
|
|
7 |
// Initialize mobile navigation |
|
|
8 |
initMobileNav(); |
|
|
9 |
|
|
|
10 |
// Initialize smooth scrolling |
|
|
11 |
initSmoothScroll(); |
|
|
12 |
|
|
|
13 |
// Initialize tabs |
|
|
14 |
initTabs(); |
|
|
15 |
|
|
|
16 |
// Initialize code copy buttons |
|
|
17 |
initCodeCopy(); |
|
|
18 |
|
|
|
19 |
// Initialize FAQ accordions |
|
|
20 |
initFaqAccordions(); |
|
|
21 |
|
|
|
22 |
// Initialize active link tracking |
|
|
23 |
initActiveLinkTracking(); |
|
|
24 |
|
|
|
25 |
// Initialize search functionality |
|
|
26 |
initSearch(); |
|
|
27 |
}); |
|
|
28 |
|
|
|
29 |
/** |
|
|
30 |
* Initialize mobile navigation |
|
|
31 |
*/ |
|
|
32 |
function initMobileNav() { |
|
|
33 |
const sidebar = document.getElementById('docsSidebar'); |
|
|
34 |
const sidebarToggle = document.getElementById('sidebarToggle'); |
|
|
35 |
const closeSidebar = document.getElementById('closeSidebar'); |
|
|
36 |
|
|
|
37 |
if (sidebar && sidebarToggle) { |
|
|
38 |
// Toggle sidebar on mobile |
|
|
39 |
sidebarToggle.addEventListener('click', function() { |
|
|
40 |
sidebar.classList.add('active'); |
|
|
41 |
}); |
|
|
42 |
|
|
|
43 |
// Close sidebar on mobile |
|
|
44 |
if (closeSidebar) { |
|
|
45 |
closeSidebar.addEventListener('click', function() { |
|
|
46 |
sidebar.classList.remove('active'); |
|
|
47 |
}); |
|
|
48 |
} |
|
|
49 |
|
|
|
50 |
// Close sidebar when clicking on links (mobile) |
|
|
51 |
const sidebarLinks = sidebar.querySelectorAll('a'); |
|
|
52 |
sidebarLinks.forEach(link => { |
|
|
53 |
link.addEventListener('click', function() { |
|
|
54 |
if (window.innerWidth <= 768) { |
|
|
55 |
sidebar.classList.remove('active'); |
|
|
56 |
} |
|
|
57 |
}); |
|
|
58 |
}); |
|
|
59 |
|
|
|
60 |
// Close sidebar when clicking outside (mobile) |
|
|
61 |
document.addEventListener('click', function(event) { |
|
|
62 |
if (window.innerWidth <= 768 && |
|
|
63 |
!sidebar.contains(event.target) && |
|
|
64 |
event.target !== sidebarToggle && |
|
|
65 |
!sidebarToggle.contains(event.target)) { |
|
|
66 |
sidebar.classList.remove('active'); |
|
|
67 |
} |
|
|
68 |
}); |
|
|
69 |
} |
|
|
70 |
} |
|
|
71 |
|
|
|
72 |
/** |
|
|
73 |
* Initialize smooth scrolling for anchor links |
|
|
74 |
*/ |
|
|
75 |
function initSmoothScroll() { |
|
|
76 |
document.querySelectorAll('a[href^="#"]').forEach(anchor => { |
|
|
77 |
anchor.addEventListener('click', function(e) { |
|
|
78 |
const targetId = this.getAttribute('href'); |
|
|
79 |
|
|
|
80 |
// Skip if it's just "#" or not an ID selector |
|
|
81 |
if (targetId === '#' || !targetId.startsWith('#')) return; |
|
|
82 |
|
|
|
83 |
const targetElement = document.querySelector(targetId); |
|
|
84 |
|
|
|
85 |
if (targetElement) { |
|
|
86 |
e.preventDefault(); |
|
|
87 |
|
|
|
88 |
const navbarHeight = 70; // Height of the fixed navbar |
|
|
89 |
const docsHeaderHeight = 50; // Height of the docs header (mobile) |
|
|
90 |
const offset = window.innerWidth <= 768 ? navbarHeight + docsHeaderHeight : navbarHeight; |
|
|
91 |
|
|
|
92 |
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - offset; |
|
|
93 |
|
|
|
94 |
window.scrollTo({ |
|
|
95 |
top: targetPosition, |
|
|
96 |
behavior: 'smooth' |
|
|
97 |
}); |
|
|
98 |
} |
|
|
99 |
}); |
|
|
100 |
}); |
|
|
101 |
} |
|
|
102 |
|
|
|
103 |
/** |
|
|
104 |
* Initialize tabs functionality |
|
|
105 |
*/ |
|
|
106 |
function initTabs() { |
|
|
107 |
const tabButtons = document.querySelectorAll('.tab-button'); |
|
|
108 |
|
|
|
109 |
tabButtons.forEach(button => { |
|
|
110 |
button.addEventListener('click', function() { |
|
|
111 |
const tabId = this.getAttribute('data-tab'); |
|
|
112 |
const tabContent = document.getElementById(tabId); |
|
|
113 |
|
|
|
114 |
// Remove active class from all buttons and contents |
|
|
115 |
document.querySelectorAll('.tab-button').forEach(btn => { |
|
|
116 |
btn.classList.remove('active'); |
|
|
117 |
}); |
|
|
118 |
|
|
|
119 |
document.querySelectorAll('.tab-content').forEach(content => { |
|
|
120 |
content.classList.remove('active'); |
|
|
121 |
}); |
|
|
122 |
|
|
|
123 |
// Add active class to current button and content |
|
|
124 |
this.classList.add('active'); |
|
|
125 |
if (tabContent) { |
|
|
126 |
tabContent.classList.add('active'); |
|
|
127 |
} |
|
|
128 |
}); |
|
|
129 |
}); |
|
|
130 |
} |
|
|
131 |
|
|
|
132 |
/** |
|
|
133 |
* Initialize code copy functionality |
|
|
134 |
*/ |
|
|
135 |
function initCodeCopy() { |
|
|
136 |
const copyButtons = document.querySelectorAll('.copy-button'); |
|
|
137 |
|
|
|
138 |
copyButtons.forEach(button => { |
|
|
139 |
button.addEventListener('click', function() { |
|
|
140 |
const codeBlock = this.closest('.code-block'); |
|
|
141 |
const code = codeBlock.querySelector('code').textContent; |
|
|
142 |
|
|
|
143 |
// Copy to clipboard |
|
|
144 |
navigator.clipboard.writeText(code) |
|
|
145 |
.then(() => { |
|
|
146 |
// Success feedback |
|
|
147 |
const originalText = this.textContent; |
|
|
148 |
this.textContent = 'Copied!'; |
|
|
149 |
this.style.background = 'var(--success)'; |
|
|
150 |
|
|
|
151 |
// Reset after 2 seconds |
|
|
152 |
setTimeout(() => { |
|
|
153 |
this.textContent = originalText; |
|
|
154 |
this.style.background = ''; |
|
|
155 |
}, 2000); |
|
|
156 |
}) |
|
|
157 |
.catch(err => { |
|
|
158 |
console.error('Could not copy text: ', err); |
|
|
159 |
|
|
|
160 |
// Fallback for older browsers |
|
|
161 |
const textarea = document.createElement('textarea'); |
|
|
162 |
textarea.value = code; |
|
|
163 |
textarea.style.position = 'fixed'; |
|
|
164 |
document.body.appendChild(textarea); |
|
|
165 |
textarea.focus(); |
|
|
166 |
textarea.select(); |
|
|
167 |
|
|
|
168 |
try { |
|
|
169 |
document.execCommand('copy'); |
|
|
170 |
// Success feedback |
|
|
171 |
const originalText = this.textContent; |
|
|
172 |
this.textContent = 'Copied!'; |
|
|
173 |
this.style.background = 'var(--success)'; |
|
|
174 |
|
|
|
175 |
// Reset after 2 seconds |
|
|
176 |
setTimeout(() => { |
|
|
177 |
this.textContent = originalText; |
|
|
178 |
this.style.background = ''; |
|
|
179 |
}, 2000); |
|
|
180 |
} catch (err) { |
|
|
181 |
console.error('Fallback copy failed: ', err); |
|
|
182 |
this.textContent = 'Failed!'; |
|
|
183 |
this.style.background = 'var(--error)'; |
|
|
184 |
|
|
|
185 |
setTimeout(() => { |
|
|
186 |
this.textContent = 'Copy'; |
|
|
187 |
this.style.background = ''; |
|
|
188 |
}, 2000); |
|
|
189 |
} |
|
|
190 |
|
|
|
191 |
document.body.removeChild(textarea); |
|
|
192 |
}); |
|
|
193 |
}); |
|
|
194 |
}); |
|
|
195 |
} |
|
|
196 |
|
|
|
197 |
/** |
|
|
198 |
* Initialize FAQ accordions |
|
|
199 |
*/ |
|
|
200 |
function initFaqAccordions() { |
|
|
201 |
const faqItems = document.querySelectorAll('.faq-item'); |
|
|
202 |
|
|
|
203 |
faqItems.forEach(item => { |
|
|
204 |
const question = item.querySelector('.faq-question'); |
|
|
205 |
|
|
|
206 |
if (question) { |
|
|
207 |
question.addEventListener('click', function() { |
|
|
208 |
// Toggle active class on the FAQ item |
|
|
209 |
item.classList.toggle('active'); |
|
|
210 |
|
|
|
211 |
// If this item was activated, close others |
|
|
212 |
if (item.classList.contains('active')) { |
|
|
213 |
faqItems.forEach(otherItem => { |
|
|
214 |
if (otherItem !== item) { |
|
|
215 |
otherItem.classList.remove('active'); |
|
|
216 |
} |
|
|
217 |
}); |
|
|
218 |
} |
|
|
219 |
}); |
|
|
220 |
} |
|
|
221 |
}); |
|
|
222 |
} |
|
|
223 |
|
|
|
224 |
/** |
|
|
225 |
* Initialize active link tracking based on scroll position |
|
|
226 |
*/ |
|
|
227 |
function initActiveLinkTracking() { |
|
|
228 |
const sections = document.querySelectorAll('.doc-section'); |
|
|
229 |
const navLinks = document.querySelectorAll('.sidebar-nav a'); |
|
|
230 |
|
|
|
231 |
if (sections.length === 0 || navLinks.length === 0) return; |
|
|
232 |
|
|
|
233 |
// Update active link on scroll |
|
|
234 |
function updateActiveLink() { |
|
|
235 |
let currentSection = ''; |
|
|
236 |
const navbarHeight = 70; |
|
|
237 |
const docsHeaderHeight = 50; |
|
|
238 |
const totalOffset = window.innerWidth <= 768 ? navbarHeight + docsHeaderHeight + 20 : navbarHeight + 20; |
|
|
239 |
|
|
|
240 |
sections.forEach(section => { |
|
|
241 |
const sectionTop = section.offsetTop - totalOffset; |
|
|
242 |
const sectionHeight = section.offsetHeight; |
|
|
243 |
const sectionId = section.getAttribute('id'); |
|
|
244 |
|
|
|
245 |
if (window.scrollY >= sectionTop && window.scrollY < sectionTop + sectionHeight) { |
|
|
246 |
currentSection = '#' + sectionId; |
|
|
247 |
} |
|
|
248 |
}); |
|
|
249 |
|
|
|
250 |
// Update active class on nav links |
|
|
251 |
navLinks.forEach(link => { |
|
|
252 |
link.classList.remove('active'); |
|
|
253 |
if (link.getAttribute('href') === currentSection) { |
|
|
254 |
link.classList.add('active'); |
|
|
255 |
} |
|
|
256 |
}); |
|
|
257 |
} |
|
|
258 |
|
|
|
259 |
// Initial call to set active link on page load |
|
|
260 |
updateActiveLink(); |
|
|
261 |
|
|
|
262 |
// Update active link on scroll |
|
|
263 |
window.addEventListener('scroll', updateActiveLink); |
|
|
264 |
} |
|
|
265 |
|
|
|
266 |
/** |
|
|
267 |
* Initialize search functionality |
|
|
268 |
*/ |
|
|
269 |
function initSearch() { |
|
|
270 |
const searchInput = document.getElementById('docsSearch'); |
|
|
271 |
const sections = document.querySelectorAll('.doc-section'); |
|
|
272 |
|
|
|
273 |
if (!searchInput || sections.length === 0) return; |
|
|
274 |
|
|
|
275 |
searchInput.addEventListener('input', function() { |
|
|
276 |
const query = this.value.trim().toLowerCase(); |
|
|
277 |
|
|
|
278 |
if (query.length < 2) { |
|
|
279 |
// If query is too short, show all sections |
|
|
280 |
sections.forEach(section => { |
|
|
281 |
section.style.display = 'block'; |
|
|
282 |
|
|
|
283 |
// Remove any highlights |
|
|
284 |
removeHighlights(section); |
|
|
285 |
}); |
|
|
286 |
return; |
|
|
287 |
} |
|
|
288 |
|
|
|
289 |
// Search and filter sections |
|
|
290 |
sections.forEach(section => { |
|
|
291 |
const sectionText = section.textContent.toLowerCase(); |
|
|
292 |
const headings = Array.from(section.querySelectorAll('h1, h2, h3, h4')).map(h => h.textContent.toLowerCase()); |
|
|
293 |
|
|
|
294 |
// Check if section contains the query in text or headings |
|
|
295 |
const containsQuery = sectionText.includes(query) || headings.some(h => h.includes(query)); |
|
|
296 |
|
|
|
297 |
if (containsQuery) { |
|
|
298 |
section.style.display = 'block'; |
|
|
299 |
|
|
|
300 |
// Highlight matches |
|
|
301 |
removeHighlights(section); |
|
|
302 |
highlightText(section, query); |
|
|
303 |
} else { |
|
|
304 |
section.style.display = 'none'; |
|
|
305 |
} |
|
|
306 |
}); |
|
|
307 |
|
|
|
308 |
// If search is cleared, reset highlights |
|
|
309 |
if (query.length === 0) { |
|
|
310 |
sections.forEach(section => { |
|
|
311 |
removeHighlights(section); |
|
|
312 |
}); |
|
|
313 |
} |
|
|
314 |
}); |
|
|
315 |
} |
|
|
316 |
|
|
|
317 |
/** |
|
|
318 |
* Highlight matching text in an element |
|
|
319 |
* @param {HTMLElement} element - The element to search in |
|
|
320 |
* @param {string} query - The text to highlight |
|
|
321 |
*/ |
|
|
322 |
function highlightText(element, query) { |
|
|
323 |
// Only highlight text in paragraphs, list items, and code blocks |
|
|
324 |
const textNodes = element.querySelectorAll('p, li, code'); |
|
|
325 |
|
|
|
326 |
textNodes.forEach(node => { |
|
|
327 |
const html = node.innerHTML; |
|
|
328 |
// Create regex with word boundary for whole words, or without for partial matches |
|
|
329 |
const regex = new RegExp(`(\\b${query}\\b|${query})`, 'gi'); |
|
|
330 |
const newHtml = html.replace(regex, '<mark>$1</mark>'); |
|
|
331 |
|
|
|
332 |
if (newHtml !== html) { |
|
|
333 |
node.innerHTML = newHtml; |
|
|
334 |
} |
|
|
335 |
}); |
|
|
336 |
} |
|
|
337 |
|
|
|
338 |
/** |
|
|
339 |
* Remove highlights from an element |
|
|
340 |
* @param {HTMLElement} element - The element to remove highlights from |
|
|
341 |
*/ |
|
|
342 |
function removeHighlights(element) { |
|
|
343 |
const marks = element.querySelectorAll('mark'); |
|
|
344 |
|
|
|
345 |
marks.forEach(mark => { |
|
|
346 |
// Replace mark with its text content |
|
|
347 |
const textNode = document.createTextNode(mark.textContent); |
|
|
348 |
mark.parentNode.replaceChild(textNode, mark); |
|
|
349 |
}); |
|
|
350 |
} |