PageSourceSearch

https://www.bioguldborgsund.dk/scripts/cludo-voice-search.js

js bioguldborgsund.dk collected 2026-10-05 19:09:00 UTC 9,925 bytes, 224 lines download raw bytes

1class voiceSearchInstance {
2
3    /** Timing values */
4    initDelay = 5000;           // wait this long for initial voice input before cancelling
5    continueDelay = 2000;       // wait this long for pauses in voice input before searching
6
7    /** Settings assigned from options argument or resolved from DOM */
8    elements = {
9        button: null,
10        input: null,
11        form: null
12    }
13    callbacks = {
14        onStart: undefined,
15        onEnd: undefined,
16        onError: undefined
17    }
18
19    constructor(clickEvent, options) {
20        this.setElements(clickEvent, options);
21        this.setCallbacks(options);
22        this.language = options?.language || navigator.language || navigator.userLanguage;
23        this.activeClass = options?.activeClass || 'voice-search-active';
24        this.disableInputUpdate = options?.disableInputUpdate ?? false;
25        this.disableFormSubmit = options?.disableFormSubmit ?? false;
26    }
27
28    setElements(clickEvent, options) {
29        this.elements.button =
30            clickEvent instanceof Event ?
31                clickEvent.currentTarget || clickEvent.srcElement :
32                undefined;
33        this.elements.input =
34            options?.inputElement &&
35                options.inputElement instanceof HTMLInputElement ?
36                options.inputElement :
37                this.helpers.getSiblingInput(this.elements.button);
38        this.elements.form =
39            options?.formElement &&
40                options.formElement instanceof HTMLFormElement ?
41                options.formElement :
42                this.helpers.getParentForm(this.elements.button);
43    }
44
45    setCallbacks(options) {
46        if (options?.onStart) this.callbacks.onStart = options.onStart;
47        if (options?.onEnd) this.callbacks.onEnd = options.onEnd;
48        if (options?.onError) this.callbacks.onError = options.onError;
49    }
50
51    initSpeechRecognition() {
52        if (!('webkitSpeechRecognition' in window)) {
53            console.warn('Warning: voice search initialized but webkit speech recognition is not available on this browser');
54            return;
55        }
56        if (this.elements.input.value != '') {
57            this.elements.input.value = '';
58        } else if (this.elements.input && !this.disableInputUpdate) {
59            this.originalPlaceholder = this.elements.input.placeholder;
60            this.elements.input.value = '';
61        }
62
63        let timeout;
64        const recognition = new window.webkitSpeechRecognition();
65        recognition.continuous = this.helpers.isBrowserSafari(); // avoid a bug on Safari by setting continuous to true (https://developer.apple.com/forums//thread/694847?answerId=731859022#731859022)
66        recognition.interimResults = true;
67        recognition.lang = "da";
68        console.log(this.language)
69        recognition.start();
70
71        recognition.onstart = () => {
72            // set timeout to trigger speechend if user never starts speaking
73            timeout = window.setTimeout(() => {
74                this.endSpeechAndSearch('', recognition);
75            }, this.initDelay);
76            // set active class on voice search button
77            if (this.elements.button && !this.elements.button.classList.contains(this.activeClass)) {
78                this.elements.button.classList.add(this.activeClass);
79            }
80            // call onStart callback
81            if (this.callbacks.onStart) {
82                this.callbacks.onStart();
83            }
84        };
85
86        recognition.onresult = (event) => {
87            let interim_transcript = '';
88            let final_transcript = '';
89
90            // loop through result and keep track of words spoken so far
91            // trigger speechend when result is final or after user stops speaking
92            for (let i = event.resultIndex; i < event.results.length; ++i) {
93                if (event.results[i].isFinal) {
94                    final_transcript = event.results[i][0].transcript;
95                    this.endSpeechAndSearch(final_transcript, recognition);
96                } else {
97                    interim_transcript += event.results[i][0].transcript;
98                    if (this.elements.input && !this.disableInputUpdate) {
99                        this.elements.input.placeholder = interim_transcript;
100                    }
101                    if (timeout) {
102                        clearTimeout(timeout);
103                    }
104                    timeout = window.setTimeout(() => {
105                        this.endSpeechAndSearch(interim_transcript, recognition);
106                    }, this.continueDelay);
107                }
108            }
109        };
110
111        recognition.onspeechend = () => {
112            if (this.elements.button) {
113                this.elements.button.classList.remove(this.activeClass);
114            }
115            if (timeout) {
116                clearTimeout(timeout);
117            }
118        };
119
120        recognition.onerror = (event) => {
121            if (this.elements.button) {
122                this.elements.button.classList.remove(this.activeClass);
123            }
124            if (timeout) {
125                clearTimeout(timeout);
126            }
127            // suppress error for non-fatal 'abort' errors
128            if (event.error !== 'aborted') {
129                if (this.callbacks.onError) {
130                    this.callbacks.onError(event);
131                }
132                console.error('Error: voice recognition failed: ', event);
133            }
134        };
135    }
136
137    endSpeechAndSearch(query, recognition) {
138        if (query && query.length) {
139            if (!this.elements.input && !this.callbacks.onEnd) {
140                console.warn('Warning: no input or callback provided to handle voice search query');
141                return;
142            }
143            // massage query
144            if (this.helpers.isBrowserSafari()) {
145                query = this.helpers.removeDuplicateTranscript(query);
146            }
147            query = this.helpers.stripSpecialChars(query);
148            // handle query
149            if (this.callbacks.onEnd) {
150                this.callbacks.onEnd(query);
151            }
152            if (this.elements.input && !this.disableInputUpdate) {
153                this.elements.input.placeholder = this.originalPlaceholder;
154                this.elements.input.value = query;
155            }
156            if (this.elements.form && !this.disableFormSubmit) {
157                this.elements.form.submit();
158            }
159
160        }
161        if (this.elements.button) {
162            this.elements.button.classList.remove(this.activeClass);
163        }
164        recognition.stop();
165    }
166
167    /** Helper functions */
168    helpers = {
169        isBrowserSafari() {
170            const ua = window.navigator.userAgent;
171            return (ua.indexOf("Safari") > -1) && !(ua.indexOf("Chrome") > -1);
172        },
173        getParentForm: function (target) {
174            if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement) {
175                return target.form;
176            }
177            return undefined;
178        },
179        getSiblingInput: function (target) {
180            if (target && target.parentNode) {
181                const siblingInput = target.parentNode.querySelector('input[type="search"]') || target.parentNode.querySelector('input[type="text"]');
182                return siblingInput;
183            }
184            return undefined;
185        },
186        removeDuplicateTranscript(final_transcript) {
187            const transcript = final_transcript.toLowerCase();
188            const numWords = transcript.split(' ').length;
189            if ((numWords & 2) !== 0) {
190                return transcript;
191            }
192            const firstHalf = transcript.substring(0, transcript.length / 2).split(' ').join('');
193            const secondHalf = transcript.substring(transcript.length / 2, transcript.length).split(' ').join('');
194            if (firstHalf === secondHalf) {
195                return transcript.substring(0, transcript.length / 2);
196            } else {
197                return transcript;
198            }
199        },
200        stripSpecialChars(query) {
201            return query.replace(/[–—―,.+?:-]/g, ' ').trim();
202        }
203    }
204}
205
206/**
207 * Initialize a voice search.
208 * @param {Object} options - An optional object of configurations for voice search.
209 * @param {Function} options.onStart - A callback that will fire when voice recognition begins.
210 * @param {Function} options.onEnd - A callback that will fire when voice recognition ends. The spoken text will be returned as an argument to the callback.
211 * @param {Function} options.onError - A callback that will fire when voice recognition fails. The error event will be returned as an argument to the callback.
212 * @param {HTMLInputElement} options.inputElement - Manually provide an input element that should be updated as the user speaks.
213 * @param {HTMLFormElement} options.formElement - Manually provide a form element that should be submitted after the user is done speaking.
214 * @param {string} options.language - The BCP 47 language code that should be used for speech recognition. Defaults to the browser's language property.
215 * @param {string} options.activeClass - A class that will be added to the voice search activation button while in listening mode. Defaults to 'voice-search-active'.
216 * @param {boolean} options.disableInputUpdate - Disable automatic updating of any input during and after speech recognition.
217 * @param {boolean} options.disableFormSubmit -Disable automatic submitting of any form after speech recognition.
218 */
219window.initVoiceSearch = function (e, options) {
220    e.preventDefault();
221    const instance = new voiceSearchInstance(e, options);
222    instance.initSpeechRecognition();
223}
224

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.