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, '<') 19 .replace(/>/g, '>') 20 .replace(/"/g, '"') 21 .replace(/'/g, '''); 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.