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
}