LabCodeHub öffentliche Ansicht
Anmelden
Küpper / Quildrop öffentlich
Branch: main
Quildrop / themes / default / static / js / search.js
Verlauf Rohdaten
R Rüdiger Küpper add binaries
e6455c95 vor 16 Tagen
themes/default/static/js/search.js 146 Zeilen · 4.6 KB · JavaScript
1
(function () {
2
    'use strict';
3
4
    let searchIndex = null;
5
    let debounceTimer = null;
6
7
    const toggle = document.getElementById('search-toggle');
8
    const container = document.getElementById('search-container');
9
    const box = document.getElementById('search-box');
10
    const input = document.getElementById('search-input');
11
    const results = document.getElementById('search-results');
12
13
    if (!toggle || !input || !results) return;
14
15
    // Toggle search box
16
    toggle.addEventListener('click', function (e) {
17
        e.stopPropagation();
18
        const isOpen = container.classList.toggle('active');
19
        if (isOpen) {
20
            input.focus();
21
            loadIndex();
22
        } else {
23
            closeSearch();
24
        }
25
    });
26
27
    // Load search index (lazy, only once)
28
    function loadIndex() {
29
        if (searchIndex !== null) return;
30
        fetch('/search-index.json')
31
            .then(function (res) { return res.json(); })
32
            .then(function (data) { searchIndex = data; })
33
            .catch(function (err) {
34
                console.error('Search index load failed:', err);
35
            });
36
    }
37
38
    // Search on input
39
    input.addEventListener('input', function () {
40
        clearTimeout(debounceTimer);
41
        debounceTimer = setTimeout(doSearch, 200);
42
    });
43
44
    function doSearch() {
45
        var query = input.value.trim().toLowerCase();
46
        if (!query || !searchIndex) {
47
            results.innerHTML = '';
48
            results.classList.remove('visible');
49
            return;
50
        }
51
52
        var terms = query.split(/\s+/);
53
        var matches = searchIndex.filter(function (entry) {
54
            var haystack = [
55
                entry.title,
56
                entry.preview,
57
                (entry.tags || []).join(' '),
58
                (entry.categories || []).join(' ')
59
            ].join(' ').toLowerCase();
60
61
            return terms.every(function (term) {
62
                return haystack.indexOf(term) !== -1;
63
            });
64
        });
65
66
        renderResults(matches.slice(0, 8));
67
    }
68
69
    function renderResults(items) {
70
        if (items.length === 0) {
71
            results.innerHTML = '<div class="search-no-results">Keine Ergebnisse gefunden</div>';
72
            results.classList.add('visible');
73
            return;
74
        }
75
76
        var html = '';
77
        items.forEach(function (item) {
78
            var tags = (item.tags || []).slice(0, 3).map(function (t) {
79
                return '<span class="search-result-tag">' + escapeHtml(t) + '</span>';
80
            }).join('');
81
82
            html += '<a href="' + escapeHtml(item.url) + '" class="search-result-item">' +
83
                '<div class="search-result-title">' + highlightMatch(escapeHtml(item.title), input.value.trim()) + '</div>' +
84
                '<div class="search-result-meta">' +
85
                    '<span class="search-result-date">' + escapeHtml(item.date) + '</span>' +
86
                    (tags ? '<span class="search-result-tags">' + tags + '</span>' : '') +
87
                '</div>' +
88
            '</a>';
89
        });
90
91
        results.innerHTML = html;
92
        results.classList.add('visible');
93
    }
94
95
    function highlightMatch(text, query) {
96
        if (!query) return text;
97
        var terms = query.toLowerCase().split(/\s+/);
98
        terms.forEach(function (term) {
99
            if (!term) return;
100
            var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
101
            text = text.replace(regex, '<mark>$1</mark>');
102
        });
103
        return text;
104
    }
105
106
    function escapeHtml(str) {
107
        if (!str) return '';
108
        return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
109
    }
110
111
    function closeSearch() {
112
        container.classList.remove('active');
113
        input.value = '';
114
        results.innerHTML = '';
115
        results.classList.remove('visible');
116
    }
117
118
    // Keyboard shortcuts
119
    input.addEventListener('keydown', function (e) {
120
        if (e.key === 'Escape') {
121
            closeSearch();
122
        }
123
    });
124
125
    // Close on click outside
126
    document.addEventListener('click', function (e) {
127
        if (!container.contains(e.target)) {
128
            closeSearch();
129
        }
130
    });
131
132
    // Prevent search box clicks from closing
133
    box.addEventListener('click', function (e) {
134
        e.stopPropagation();
135
    });
136
137
    // Global shortcut: Ctrl+K or Cmd+K to open search
138
    document.addEventListener('keydown', function (e) {
139
        if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
140
            e.preventDefault();
141
            container.classList.add('active');
142
            input.focus();
143
            loadIndex();
144
        }
145
    });
146
})();