PageSourceSearch

https://naukaru.ru/bundles/asutemplate/Base/js/editorum-async-autocomplete.js?v=20260419-typeahead-wave4

js naukaru.ru collected 2026-10-02 07:32:55 UTC 9,921 bytes, 339 lines download raw bytes

1(function (window, document) {
2    'use strict';
3
4    var namespace = window.EditorumAsyncAutocomplete = window.EditorumAsyncAutocomplete || {};
5    var instances = [];
6
7    function toArray(value) {
8        return Array.prototype.slice.call(value || []);
9    }
10
11    function isFunction(value) {
12        return typeof value === 'function';
13    }
14
15    function escapeHtml(value) {
16        return String(value)
17            .replace(/&/g, '&')
18            .replace(/</g, '&lt;')
19            .replace(/>/g, '&gt;')
20            .replace(/"/g, '&quot;')
21            .replace(/'/g, '&#39;');
22    }
23
24    function getInputElement(target) {
25        if (!target) {
26            return null;
27        }
28
29        if (typeof target === 'string') {
30            return document.querySelector(target);
31        }
32
33        return target.nodeType === 1 ? target : null;
34    }
35
36    function closeAll(exceptState) {
37        instances.slice().forEach(function (state) {
38            if (state !== exceptState) {
39                closeMenu(state);
40            }
41        });
42    }
43
44    function ensureShell(input) {
45        var shell = document.createElement('div');
46        var menu = document.createElement('div');
47
48        shell.className = 'editorum-async-autocomplete';
49        input.parentNode.insertBefore(shell, input);
50        shell.appendChild(input);
51
52        menu.className = 'editorum-async-autocomplete__menu';
53        menu.hidden = true;
54        menu.setAttribute('role', 'listbox');
55        shell.appendChild(menu);
56
57        input.setAttribute('autocomplete', 'off');
58        input.setAttribute('aria-autocomplete', 'list');
59        input.setAttribute('aria-expanded', 'false');
60
61        return {
62            shell: shell,
63            menu: menu
64        };
65    }
66
67    function setMenuState(state, html, empty) {
68        state.menu.innerHTML = html;
69        state.menu.hidden = false;
70        state.input.setAttribute('aria-expanded', 'true');
71        state.menu.classList.toggle('is-empty', !!empty);
72    }
73
74    function closeMenu(state) {
75        state.highlightedIndex = -1;
76        state.items = [];
77        state.menu.hidden = true;
78        state.menu.innerHTML = '';
79        state.input.setAttribute('aria-expanded', 'false');
80    }
81
82    function highlightItem(state, index) {
83        var options = state.menu.querySelectorAll('.editorum-async-autocomplete__option');
84
85        state.highlightedIndex = index;
86
87        toArray(options).forEach(function (option, optionIndex) {
88            var isActive = optionIndex === index;
89
90            option.classList.toggle('is-active', isActive);
91            option.setAttribute('aria-selected', isActive ? 'true' : 'false');
92
93            if (isActive) {
94                option.scrollIntoView({ block: 'nearest' });
95            }
96        });
97    }
98
99    function renderItems(state, items, query) {
100        var html;
101
102        state.items = items;
103        state.highlightedIndex = items.length ? 0 : -1;
104
105        if (!items.length) {
106            setMenuState(
107                state,
108                '<div class="editorum-async-autocomplete__status">' +
109                    escapeHtml(state.config.emptyText || 'No results found') +
110                '</div>',
111                true
112            );
113            return;
114        }
115
116        html = items.map(function (item, index) {
117            var content = isFunction(state.config.renderSuggestion)
118                ? state.config.renderSuggestion(item, query)
119                : escapeHtml(state.config.getValue(item));
120
121            return (
122                '<button type="button" class="editorum-async-autocomplete__option' +
123                (index === 0 ? ' is-active' : '') +
124                '" role="option" aria-selected="' + (index === 0 ? 'true' : 'false') +
125                '" data-index="' + index + '">' + content + '</button>'
126            );
127        }).join('');
128
129        setMenuState(state, html, false);
130    }
131
132    function selectItem(state, item) {
133        if (!item) {
134            return;
135        }
136
137        state.input.value = state.config.getValue(item);
138
139        if (isFunction(state.config.onSelect)) {
140            state.config.onSelect(item, state.input, state);
141        }
142
143        closeMenu(state);
144    }
145
146    function handleResults(state, items, query, requestId) {
147        if (requestId !== state.requestId) {
148            return;
149        }
150
151        renderItems(state, items || [], query);
152    }
153
154    function requestItems(state, query) {
155        var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
156        var requestId;
157
158        state.requestId += 1;
159        requestId = state.requestId;
160
161        if (state.abortController) {
162            state.abortController.abort();
163        }
164
165        state.abortController = controller;
166
167        setMenuState(
168            state,
169            '<div class="editorum-async-autocomplete__status">' +
170                escapeHtml(state.config.pendingText || 'Loading...') +
171            '</div>',
172            true
173        );
174
175        Promise.resolve(state.config.fetchSuggestions(query, state.input, controller && controller.signal))
176            .then(function (items) {
177                handleResults(state, items, query, requestId);
178            })
179            .catch(function (error) {
180                if (error && error.name === 'AbortError') {
181                    return;
182                }
183
184                handleResults(state, [], query, requestId);
185            });
186    }
187
188    function queueRequest(state) {
189        var query = state.input.value.trim();
190
191        window.clearTimeout(state.timerId);
192
193        if (query.length < state.config.minLength) {
194            closeMenu(state);
195            return;
196        }
197
198        state.timerId = window.setTimeout(function () {
199            requestItems(state, query);
200        }, state.config.delay);
201    }
202
203    function onKeyDown(state, event) {
204        var lastIndex = state.items.length - 1;
205
206        if (state.menu.hidden) {
207            if (event.key === 'ArrowDown') {
208                queueRequest(state);
209            }
210            return;
211        }
212
213        if (event.key === 'ArrowDown') {
214            event.preventDefault();
215            highlightItem(state, Math.min(lastIndex, state.highlightedIndex + 1));
216            return;
217        }
218
219        if (event.key === 'ArrowUp') {
220            event.preventDefault();
221            highlightItem(state, Math.max(0, state.highlightedIndex - 1));
222            return;
223        }
224
225        if (event.key === 'Enter') {
226            if (state.highlightedIndex >= 0 && state.items[state.highlightedIndex]) {
227                event.preventDefault();
228                selectItem(state, state.items[state.highlightedIndex]);
229            }
230            return;
231        }
232
233        if (event.key === 'Escape') {
234            event.preventDefault();
235            closeMenu(state);
236        }
237    }
238
239    function bindEvents(state) {
240        state.input.addEventListener('input', function () {
241            queueRequest(state);
242        });
243
244        state.input.addEventListener('focus', function () {
245            if (state.input.value.trim().length >= state.config.minLength) {
246                queueRequest(state);
247            }
248        });
249
250        state.input.addEventListener('keydown', function (event) {
251            onKeyDown(state, event);
252        });
253
254        state.input.addEventListener('blur', function () {
255            window.setTimeout(function () {
256                closeMenu(state);
257            }, 120);
258        });
259
260        state.menu.addEventListener('mousedown', function (event) {
261            var option = event.target.closest('.editorum-async-autocomplete__option');
262            var index;
263
264            if (!option) {
265                return;
266            }
267
268            event.preventDefault();
269            index = parseInt(option.getAttribute('data-index'), 10);
270            selectItem(state, state.items[index]);
271        });
272
273        state.menu.addEventListener('mousemove', function (event) {
274            var option = event.target.closest('.editorum-async-autocomplete__option');
275            var index;
276
277            if (!option) {
278                return;
279            }
280
281            index = parseInt(option.getAttribute('data-index'), 10);
282            highlightItem(state, index);
283        });
284    }
285
286    namespace.attach = function (target, config) {
287        var input = getInputElement(target);
288        var shell;
289        var state;
290
291        if (!input || input.__editorumAsyncAutocompleteReady) {
292            return null;
293        }
294
295        if (!config || !isFunction(config.fetchSuggestions) || !isFunction(config.getValue)) {
296            return null;
297        }
298
299        shell = ensureShell(input);
300
301        state = {
302            input: input,
303            menu: shell.menu,
304            config: {
305                delay: typeof config.delay === 'number' ? config.delay : 180,
306                emptyText: config.emptyText || 'No results found',
307                fetchSuggestions: config.fetchSuggestions,
308                getValue: config.getValue,
309                minLength: typeof config.minLength === 'number' ? config.minLength : 2,
310                onSelect: config.onSelect || null,
311                pendingText: config.pendingText || 'Loading...',
312                renderSuggestion: config.renderSuggestion || null
313            },
314            requestId: 0,
315            highlightedIndex: -1,
316            items: [],
317            timerId: null,
318            abortController: null
319        };
320
321        input.__editorumAsyncAutocompleteReady = true;
322        instances.push(state);
323        bindEvents(state);
324
325        return {
326            close: function () {
327                closeMenu(state);
328            }
329        };
330    };
331
332    document.addEventListener('click', function (event) {
333        var insideAutocomplete = event.target.closest('.editorum-async-autocomplete');
334
335        if (!insideAutocomplete) {
336            closeAll(null);
337        }
338    });
339})(window, document);

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.