PageSourceSearch

https://ofk.no/script/vanilla/autosuggest/autosuggest.vanilla.js?v=20260831

js ofk.no collected 2026-09-24 21:48:52 UTC 16,529 bytes, 393 lines download raw bytes

1(function () {
2    "use strict";
3    var requests = [];
4
5    var AutoSuggest = {
6        init: function (element, options) {
7            if (!element || !element.nodeType) {
8                console.error('AutoSuggest.init: Invalid element provided');
9                return;
10            }
11            
12            const settings = Object.assign({
13                'PortalID': -1,
14                'SpakID': -1,
15                'Token': '',
16                'AutoSuggestWordsURL': '',
17                'DidYouMeanURL': '',
18                'btnSearch': null,
19                Container: null,
20                oSeResultatFor: 'Se resultat for:',
21                MinimumWordLength: 3
22            }, options || {});
23
24            var inputSearch = null;
25            var container = null;
26
27            inputSearch = element;
28            container = settings.Container || element;
29
30            var timeout = null;
31            var previousLi = null;
32
33            inputSearch.addEventListener("keydown", function (e) {
34                var autoSuggestEl = document.getElementById("SokAutoSuggest");
35                if (e.keyCode == 13 && autoSuggestEl) {
36                    var hoverLi = autoSuggestEl.querySelector("li.hover");
37                    if (hoverLi) {
38                        e.preventDefault();
39                        inputSearch.value = hoverLi.textContent;
40                        hoverLi.click();
41                    }
42                }
43            });
44
45            inputSearch.addEventListener("keyup", function (e) {
46                if (timeout != null) {
47                    clearTimeout(timeout);
48                }
49                
50                var autoSuggestEl = document.getElementById("SokAutoSuggest");
51                
52                // Arrow key navigation
53                if ((e.keyCode == 40 || e.keyCode == 38) && autoSuggestEl) {
54                    var hoverLi = autoSuggestEl.querySelector("li.hover");
55                    var allLis = autoSuggestEl.querySelectorAll("li");
56                    
57                    if (hoverLi) {
58                        var currentIndex = Array.prototype.indexOf.call(allLis, hoverLi);
59                        
60                        if (currentIndex == allLis.length - 1 && e.keyCode == 40) {
61                            // At bottom and pressing down, jump to first
62                            if (previousLi) {
63                                AutoSuggest.triggerMouseOut(previousLi);
64                            }
65                            AutoSuggest.triggerMouseOver(allLis[0]);
66                            previousLi = allLis[0];
67                        }
68                        else if (currentIndex == 0 && e.keyCode == 38) {
69                            // At top and pressing up, jump to last
70                            if (previousLi) {
71                                AutoSuggest.triggerMouseOut(previousLi);
72                            }
73                            AutoSuggest.triggerMouseOver(allLis[allLis.length - 1]);
74                            previousLi = allLis[allLis.length - 1];
75                        }
76                        else {
77                            // Normal navigation
78                            if (previousLi) {
79                                AutoSuggest.triggerMouseOut(previousLi);
80                            }
81                            if (e.keyCode == 38) {
82                                AutoSuggest.triggerMouseOver(allLis[currentIndex - 1]);
83                                previousLi = allLis[currentIndex - 1];
84                            } else {
85                                AutoSuggest.triggerMouseOver(allLis[currentIndex + 1]);
86                                previousLi = allLis[currentIndex + 1];
87                            }
88                        }
89                    } else {
90                        // First time user presses arrow in navigation
91                        if (allLis.length > 0) {
92                            AutoSuggest.triggerMouseOver(allLis[0]);
93                            previousLi = allLis[0];
94                        }
95                    }
96                } else {
97                    previousLi = null;
98                    timeout = setTimeout(function () {
99                        if (inputSearch.value.length < settings.MinimumWordLength) {
100                            var autoSuggestEl = document.getElementById("SokAutoSuggest");
101                            if (autoSuggestEl) {
102                                autoSuggestEl.remove();
103                            }
104                        }
105
106                        if (inputSearch.value.length >= settings.MinimumWordLength) {
107                            AutoSuggest.Query(inputSearch, container, settings);
108                        }
109                    }, 250);
110                }
111            });
112        },
113
114        triggerMouseOver: function(element) {
115            element.style.backgroundColor = "#EDEDED";
116            element.classList.add("hover");
117        },
118
119        triggerMouseOut: function(element) {
120            element.style.backgroundColor = "";
121            element.classList.remove("hover");
122        },
123
124        Query: function (inputSearch, container, settings) {
125            if (requests.length > 0) {
126                requests.pop().abort();
127            }
128
129            // Capture the query as it was when the request was sent. If the user keeps
130            // typing before the response arrives, inputSearch.value would otherwise no
131            // longer match the results, causing mismatched/incorrect highlighting.
132            var queryAtRequestTime = inputSearch.value;
133
134            var xhr = new XMLHttpRequest();
135            xhr.open("POST", settings.AutoSuggestWordsURL, true);
136            xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
137            xhr.setRequestHeader("PortalId", settings.PortalID);
138            xhr.setRequestHeader("SprakId", settings.SprakID);
139            xhr.setRequestHeader("RequestVerificationToken", settings.Token);
140            
141            xhr.onreadystatechange = function() {
142                if (xhr.readyState === XMLHttpRequest.DONE) {
143                    if (xhr.status === 200 && inputSearch.value === queryAtRequestTime) {
144                        var resultat = JSON.parse(xhr.responseText);
145                        AutoSuggest.CreateBox(resultat, queryAtRequestTime, inputSearch, container, settings);
146                    }
147                }
148            };
149            
150            xhr.send(JSON.stringify(queryAtRequestTime));
151            requests.push(xhr);
152        },
153
154        CreateBox: function (resultat, query, inputSearch, container, settings) {
155            var existingBox = document.getElementById("SokAutoSuggest");
156            if (existingBox) {
157                existingBox.remove();
158            }
159
160            if (resultat.length == 0) {
161                AutoSuggest.DidYouMean(inputSearch, container, settings);
162                return false;
163            }
164
165            var offset = AutoSuggest.getOffset(container);
166            var offsetInput = AutoSuggest.getOffset(inputSearch);
167            var width = container.offsetWidth;
168            
169            if (offsetInput.left - 25 > offset.left) {
170                // Container is not styled, use inputsearch box
171                container = inputSearch;
172                offset = offsetInput;
173                width = inputSearch.offsetWidth + 3;
174            }
175            else if(!container.offsetHeight){
176                container = inputSearch;
177            }
178
179            var wrapper = document.createElement("div");
180            wrapper.id = "SokAutoSuggest";
181            wrapper.style.width = width + "px";
182            wrapper.style.position = "absolute";
183            wrapper.style.top = (offset.top + container.offsetHeight) + "px";
184            wrapper.style.left = offset.left + "px";
185            wrapper.style.backgroundColor = "white";
186            wrapper.style.border = "1px solid #ccc";
187            wrapper.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
188            
189            var hoverTimeout;
190            wrapper.addEventListener("mouseenter", function () {
191                if (hoverTimeout) {
192                    clearTimeout(hoverTimeout);
193                }
194            });
195            
196            wrapper.addEventListener("mouseleave", function () {
197                var that = this;
198                hoverTimeout = setTimeout(function () {
199                    that.remove();
200                }, 1500);
201            });
202
203            var ul = document.createElement("ul");
204            ul.style.listStyle = "none";
205            ul.style.margin = "0";
206            ul.style.padding = "0";
207
208            var finnesIListe = false;
209            for (var i = 0; i < resultat.length; i++) {
210                if (resultat[i].toLowerCase() == query.toLowerCase()) {
211                    finnesIListe = true;
212                }
213                ul.appendChild(AutoSuggest.LagLi(resultat[i], query, inputSearch, settings));
214            }
215            if (!finnesIListe) {
216                ul.insertBefore(AutoSuggest.LagLi(query, query, inputSearch, settings), ul.firstChild);
217            }
218            wrapper.appendChild(ul);
219            document.body.appendChild(wrapper);
220        },
221
222        getOffset: function(element) {
223            if (!element || !element.getBoundingClientRect) {
224                return { top: 0, left: 0 };
225            }
226            var rect = element.getBoundingClientRect();
227            var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
228            var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
229            return {
230                top: rect.top + scrollTop,
231                left: rect.left + scrollLeft
232            };
233        },
234
235        LagLi: function (resultat, query, inputSearch, settings) {
236            var activeElement = document.activeElement;
237            var computedStyle = window.getComputedStyle(activeElement);
238            var padding = computedStyle.padding;
239            var text = AutoSuggest.MarkBold(resultat, query);
240            
241            var li = document.createElement("li");
242            li.className = "resultat";
243            li.style.padding = padding;
244            li.style.cursor = "pointer";
245            li.innerHTML = text;
246            
247            li.addEventListener("click", function () {
248                inputSearch.value = this.textContent;
249                if (settings.btnSearch) {
250                    settings.btnSearch.click();
251                }
252                var autoSuggestEl = document.getElementById("SokAutoSuggest");
253                if (autoSuggestEl) {
254                    autoSuggestEl.remove();
255                }
256            });
257            
258            li.addEventListener("mouseenter", function () {
259                this.style.backgroundColor = "#EDEDED";
260                this.classList.add("hover");
261            });
262            
263            li.addEventListener("mouseleave", function () {
264                this.style.backgroundColor = "";
265                this.classList.remove("hover");
266            });
267            
268            return li;
269        },
270
271        MarkBold: function (text, val) {
272            // Security: HTML-escape the suggestion text before applying <strong> highlighting,
273            text = AutoSuggest.escapeHtml(text);
274
275            // Match all query words in a single pass over the original (escaped) text.
276            // Replacing word-by-word on the already-modified string is unsafe: once the
277            // first word is wrapped in <strong>, a later word whose letters overlap with
278            // "strong" (e.g. "ro", "on", "tr") can match inside that inserted tag markup
279            // and corrupt/duplicate it, which can make the tags render as literal text.
280            var words = val.split(' ').filter(function (w) { return w.length > 0; });
281
282            if (words.length === 0) {
283                return text;
284            }
285
286            // Longest words first so a longer match "wins" over a shorter one it contains.
287            var sortedEscaped = words
288                .map(AutoSuggest.escapeRegExp)
289                .sort(function (a, b) { return b.length - a.length; });
290
291            var regex = new RegExp("(" + sortedEscaped.join("|") + ")", "gi");
292            return text.replace(regex, "<strong>$1</strong>");
293        },
294
295        escapeRegExp: function(string) {
296            return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
297        },
298
299        escapeHtml: function (string) {
300            if (string == null) {
301                return '';
302            }
303            return String(string)
304                .replace(/&/g, '&amp;')
305                .replace(/</g, '&lt;')
306                .replace(/>/g, '&gt;')
307                .replace(/"/g, '&quot;')
308                .replace(/'/g, '&#39;');
309        },
310
311        DidYouMean: function (inputSearch, container, settings) {
312            if (requests.length > 0) {
313                requests.pop().abort();
314            }
315            
316            if (inputSearch.value.length > settings.MinimumWordLength) {
317                var xhr = new XMLHttpRequest();
318                xhr.open("POST", settings.DidYouMeanURL, true);
319                xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
320                xhr.setRequestHeader("PortalId", settings.PortalID);
321                xhr.setRequestHeader("SprakId", settings.SprakID);
322                xhr.setRequestHeader("RequestVerificationToken", settings.Token);
323                
324                xhr.onreadystatechange = function() {
325                    if (xhr.readyState === XMLHttpRequest.DONE) {
326                        if (xhr.status === 200) {
327                            var resultat = JSON.parse(xhr.responseText);
328                            var forslag = resultat;
329                            if(forslag != null) {
330                                AutoSuggest.CreateBox(forslag.Ord, forslag.KorrektOrd, inputSearch, container, settings);
331                            }
332                        }
333                    }
334                };
335                
336                xhr.send(JSON.stringify(inputSearch.value));
337                requests.push(xhr);
338            }
339        }
340    };
341
342    // Public API
343    window.AutoSuggest = {
344        init: function(selector, options) {
345            var elements = [];
346            
347            if (!selector) {
348                console.error('AutoSuggest: No selector provided');
349                return;
350            }
351            
352            if (typeof selector === 'string') {
353                elements = document.querySelectorAll(selector);
354                if (elements.length === 0) {
355                    console.warn('AutoSuggest: No elements found for selector "' + selector + '"');
356                    return;
357                }
358            } else if (selector && selector.nodeType) {
359                elements = [selector];
360            } else if (selector && selector.length !== undefined) {
361                // Array-like object (NodeList, HTMLCollection, Array)
362                elements = selector;
363            } else {
364                console.error('AutoSuggest: Invalid selector type');
365                return;
366            }
367            
368            Array.prototype.forEach.call(elements, function(element) {
369                if (element && element.nodeType === 1) { // Ensure it's an element node
370                    AutoSuggest.init(element, options);
371                }
372            });
373        }
374    };
375
376    // Alternative usage pattern similar to jQuery
377    window.autosuggest = function(selector, options) {
378        if (!selector) {
379            console.error('autosuggest: No selector provided');
380            return;
381        }
382        return window.AutoSuggest.init(selector, options);
383    };
384
385    // For backward compatibility - wait for DOM to be ready if needed
386    if (typeof document !== 'undefined') {
387        if (document.readyState === 'loading') {
388  
388          document.addEventListener('DOMContentLoaded', function() {
389                // DOM is ready
390            });
391        }
392    }
393})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.