blob: b2cb27a9ffd78ffb5b9d04b71d2c4c0881e63ad6 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2016 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5/**
6 * An autocomplete library for javascript.
7 * Public API
8 * - _ac_install() install global handlers required for everything else to
9 * function.
10 * - _ac_register(SC) register a store constructor (see below)
11 * - _ac_isCompleting() true iff focus is in an auto complete box and the user
12 * has triggered completion with a keystroke, and completion has not been
13 * cancelled (programatically or otherwise).
14 * - _ac_isCompleteListShowing() true if _as_isCompleting and the complete list
15 * is visible to the user.
16 * - _ac_cancel() if completing, stop it, otherwise a no-op.
17 *
18 *
19 * A quick example
20 * // an auto complete store
21 * var myFavoritestAutoCompleteStore = new _AC_SimpleStore(
22 * ['some', 'strings', 'to', 'complete']);
23 *
24 * // a store constructor
25 * _ac_register(function (inputNode, keyEvent) {
26 * if (inputNode.id == 'my-auto-completing-check-box') {
27 * return myFavoritestAutoCompleteStore;
28 * }
29 * return null;
30 * });
31 *
32 * <html>
33 * <head>
34 * <script type=text/javascript src=ac.js></script>
35 * </head>
36 * <body onload=_ac_install()>
37 * <!-- the constructor above looks at the id. It could as easily
38 * - look at the class, name, or value.
39 * - The autocomplete=off stops browser autocomplete from
40 * - interfering with our autocomplete
41 * -->
42 * <input type=text id="my-auto-completing-check-box"
43 * autocomplete=off>
44 * </body>
45 * </html>
46 *
47 *
48 * Concepts
49 * - Store Constructor function
50 * A store constructor is a policy function with the signature
51 * _AC_Store myStoreConstructor(
52 * HtmlInputElement|HtmlTextAreaElement inputNode, Event keyEvent)
53 * When a key event is received on a text input or text area, the autocomplete
54 * library will try each of the store constructors in turn until it finds one
55 * that returns an AC_Store which will be used for auto-completion of that
56 * text box until focus is lost.
57 *
58 * - interface _AC_Store
59 * An autocomplete store encapsulates all operations that affect how a
60 * particular text node is autocompleted. It has the following operations:
61 * - String completable(String inputValue, int caret)
62 * This method returns null if not completable or the section of inputValue
63 * that is subject to completion. If autocomplete works on items in a
64 * comma separated list, then the input value "foo, ba" might yield "ba"
65 * as the completable chunk since it is separated from its predecessor by
66 * a comma.
67 * caret is the position of the text cursor (caret) in the text input.
68 * - _AC_Completion[] completions(String completable,
69 * _AC_Completion[] toFilter)
70 * This method returns null if there are no completions. If toFilter is
71 * not null or undefined, then this method may assume that toFilter was
72 * returned as a set of completions that contain completable.
73 * - String substitute(String inputValue, int caret,
74 * String completable, _AC_Completion completion)
75 * returns the inputValue with the given completion substituted for the
76 * given completable. caret has the same meaning as in the
77 * completable operation.
78 * - String oncomplete(boolean completed, String key,
79 * HTMLElement element, String text)
80 * This method is called when the user hits a completion key. The default
81 * value is to do nothing, but you can override it if you want. Note that
82 * key will be null if the user clicked on it to select
83 * - Boolean autoselectFirstRow()
84 * This method returns True by default, but subclasses can override it
85 * to make autocomplete fields that require the user to press the down
86 * arrow or do a mouseover once before any completion option is considered
87 * to be selected.
88 *
89 * - class _AC_SimpleStore
90 * An implementation of _AC_Store that completes a set of strings given at
91 * construct time in a text field with a comma separated value.
92 *
93 * - struct _AC_Completion
94 * a struct with two fields
95 * - String value : the plain text completion value
96 * - String html : the value, as html, with the completable in bold.
97 *
98 * Key Handling
99 * Several keys affect completion in an autocompleted input.
100 * ESC - the escape key cancels autocompleting. The autocompletion will have
101 * no effect on the focused textbox until it loses focus, regains it, and
102 * a key is pressed.
103 * ENTER - completes using the currently selected completion, or if there is
104 * only one, uses that completion.
105 * UP ARROW - selects the completion above the current selection.
106 * DOWN ARROW - selects the completion below the current selection.
107 *
108 *
109 * CSS styles
110 * The following CSS selector rules can be used to change the completion list
111 * look:
112 * #ac-list style of the auto-complete list
113 * #ac-list .selected style of the selected item
114 * #ac-list b style of the matching text in a candidate completion
115 *
116 * Dependencies
117 * The library depends on the following libraries:
118 * javascript:base for definition of key constants and SetCursorPos
119 * javascript:shapes for nodeBounds()
120 */
121
122/**
123 * install global handlers required for the rest of the module to function.
124 */
125function _ac_install() {
126 ac_addHandler_(document.body, 'onkeydown', ac_keyevent_);
127 ac_addHandler_(document.body, 'onkeypress', ac_keyevent_);
128}
129
130/**
131 * register a store constructor
132 * @param storeConstructor a function like
133 * _AC_Store myStoreConstructor(HtmlInputElement|HtmlTextArea, Event)
134 */
135function _ac_register(storeConstructor) {
136 // check that not already registered
137 for (let i = ac_storeConstructors.length; --i >= 0;) {
138 if (ac_storeConstructors[i] === storeConstructor) {
139 return;
140 }
141 }
142 ac_storeConstructors.push(storeConstructor);
143}
144
145/**
146 * may be attached as an onfocus handler to a text input to popup autocomplete
147 * immediately on the box gaining focus.
148 */
149function _ac_onfocus(event) {
150 ac_keyevent_(event);
151}
152
153/**
154 * true iff the autocomplete widget is currently active.
155 */
156function _ac_isCompleting() {
157 return !!ac_store && !ac_suppressCompletions;
158}
159
160/**
161 * true iff the completion list is displayed.
162 */
163function _ac_isCompleteListShowing() {
164 return !!ac_store && !ac_suppressCompletions && ac_completions &&
165 ac_completions.length;
166}
167
168/**
169 * cancel any autocomplete in progress.
170 */
171function _ac_cancel() {
172 ac_suppressCompletions = true;
173 ac_updateCompletionList(false);
174}
175
176/** add a handler without whacking any existing handler. @private */
177function ac_addHandler_(node, handlerName, handler) {
178 const oldHandler = node[handlerName];
179 if (!oldHandler) {
180 node[handlerName] = handler;
181 } else {
182 node[handlerName] = ac_fnchain_(node[handlerName], handler);
183 }
184 return oldHandler;
185}
186
187/** cancel the event. @private */
188function ac_cancelEvent_(event) {
189 if ('stopPropagation' in event) {
190 event.stopPropagation();
191 } else {
192 event.cancelBubble = true;
193 }
194
195 // This is handled in IE by returning false from the handler
196 if ('preventDefault' in event) {
197 event.preventDefault();
198 }
199}
200
201/** Call two functions, a and b, and return false if either one returns
202 false. This is used as a primitive way to attach multiple event
203 handlers to an element without using addEventListener(). This
204 library predates the availablity of addEventListener().
205 @private
206*/
207function ac_fnchain_(a, b) {
208 return function() {
209 const ar = a.apply(this, arguments);
210 const br = b.apply(this, arguments);
211
212 // NOTE 1: (undefined && false) -> undefined
213 // NOTE 2: returning FALSE from a onkeypressed cancels it,
214 // returning UNDEFINED does not.
215 // As such, we specifically look for falses here
216 if (ar === false || br === false) {
217 return false;
218 } else {
219 return true;
220 }
221 };
222}
223
224/** key press handler. @private */
225function ac_keyevent_(event) {
226 event = event || window.event;
227
228 const source = getTargetFromEvent(event);
229 const isInput = 'INPUT' == source.tagName &&
230 source.type.match(/^text|email$/i);
231 const isTextarea = 'TEXTAREA' == source.tagName;
232 if (!isInput && !isTextarea) return true;
233
234 const key = event.key;
235 const isDown = event.type == 'keydown';
236 const isShiftKey = event.shiftKey;
237 let storeFound = true;
238
239 if ((source !== ac_focusedInput) || (ac_store === null)) {
240 ac_focusedInput = source;
241 storeFound = false;
242 if (ENTER_KEYNAME !== key && ESC_KEYNAME !== key) {
243 for (let i = 0; i < ac_storeConstructors.length; ++i) {
244 const store = (ac_storeConstructors[i])(source, event);
245 if (store) {
246 ac_store = store;
247 ac_store.setAvoid(event);
248 ac_oldBlurHandler = ac_addHandler_(
249 ac_focusedInput, 'onblur', _ac_ob);
250 storeFound = true;
251 break;
252 }
253 }
254
255 // There exists an odd condition where an edit box with autocomplete
256 // attached can be removed from the DOM without blur being called
257 // In which case we are left with a store around that will try to
258 // autocomplete the next edit box to receive focus. We need to clean
259 // this up
260
261 // If we can't find a store, force a blur
262 if (!storeFound) {
263 _ac_ob(null);
264 }
265 }
266 // ac-table rows need to be removed when switching to another input.
267 ac_updateCompletionList(false);
268 }
269 // If the user typed Esc when the auto-complete menu was not shown,
270 // then blur the input text field so that the user can use keyboard
271 // shortcuts.
272 const acList = document.getElementById('ac-list');
273 if (ESC_KEYNAME == key &&
274 (!acList || acList.style.display == 'none')) {
275 ac_focusedInput.blur();
276 }
277
278 if (!storeFound) return true;
279
280 const isCompletion = ac_store.isCompletionKey(key, isDown, isShiftKey);
281 const hasResults = ac_completions && (ac_completions.length > 0);
282 let cancelEvent = false;
283
284 if (isCompletion && hasResults) {
285 // Cancel any enter keystrokes if something is selected so that the
286 // browser doesn't go submitting the form.
287 cancelEvent = (!ac_suppressCompletions && !!ac_completions &&
288 (ac_selected != -1));
289 window.setTimeout(function() {
290 if (ac_store) {
291 ac_handleKey_(key, isDown, isShiftKey);
292 }
293 }, 0);
294 } else if (!isCompletion) {
295 // Don't want to also blur the field. Up and down move the cursor (in
296 // Firefox) to the start/end of the field. We also don't want that while
297 // the list is showing.
298 cancelEvent = (key == ESC_KEYNAME ||
299 key == DOWN_KEYNAME ||
300 key == UP_KEYNAME);
301
302 window.setTimeout(function() {
303 if (ac_store) {
304 ac_handleKey_(key, isDown, isShiftKey);
305 }
306 }, 0);
307 } else { // implicit if (isCompletion && !hasResults)
308 if (ac_store.oncomplete) {
309 ac_store.oncomplete(false, key, ac_focusedInput, undefined);
310 }
311 }
312
313 if (cancelEvent) {
314 ac_cancelEvent_(event);
315 }
316
317 return !cancelEvent;
318}
319
320/** Autocomplete onblur handler. */
321function _ac_ob(event) {
322 if (ac_focusedInput) {
323 ac_focusedInput.onblur = ac_oldBlurHandler;
324 }
325 ac_store = null;
326 ac_focusedInput = null;
327 ac_everTyped = false;
328 ac_oldBlurHandler = null;
329 ac_suppressCompletions = false;
330 ac_updateCompletionList(false);
331}
332
333/** @constructor */
334function _AC_Store() {
335}
336/** returns the chunk of the input to treat as completable. */
337_AC_Store.prototype.completable = function(inputValue, caret) {
338 console.log('UNIMPLEMENTED completable');
339};
340/** returns the chunk of the input to treat as completable. */
341_AC_Store.prototype.completions = function(prefix, tofilter) {
342 console.log('UNIMPLEMENTED completions');
343};
344/** returns the chunk of the input to treat as completable. */
345_AC_Store.prototype.oncomplete = function(completed, key, element, text) {
346 // Call the onkeyup handler so that choosing an autocomplete option has
347 // the same side-effect as typing. E.g., exposing the next row of input
348 // fields.
349 element.dispatchEvent(new Event('keyup'));
350 _ac_ob();
351};
352/** substitutes a completion for a completable in a text input's value. */
353_AC_Store.prototype.substitute =
354 function(inputValue, caret, completable, completion) {
355 console.log('UNIMPLEMENTED substitute');
356 };
357/** true iff hitting a comma key should complete. */
358_AC_Store.prototype.commaCompletes = true;
359/**
360 * true iff the given keystroke should cause a completion (and be consumed in
361 * the process.
362 */
363_AC_Store.prototype.isCompletionKey = function(key, isDown, isShiftKey) {
364 if (!isDown && (ENTER_KEYNAME === key ||
365 (COMMA_KEYNAME == key && this.commaCompletes))) {
366 return true;
367 }
368 if (TAB_KEYNAME === key && !isShiftKey) {
369 // IE doesn't fire an event for tab on click in a text field, and firefox
370 // requires that the onkeypress event for tab be consumed or it navigates
371 // to next field.
372 return false;
373 // JER: return isDown == BR_IsIE();
374 }
375 return false;
376};
377
378_AC_Store.prototype.setAvoid = function(event) {
379 if (event && event.avoidValues) {
380 ac_avoidValues = event.avoidValues;
381 } else {
382 ac_avoidValues = this.computeAvoid();
383 }
384 ac_avoidValues = ac_avoidValues.map((val) => val.toLowerCase());
385};
386
387/* Subclasses may implement this to compute values to avoid
388 offering in the current input field, i.e., because those
389 values are already used. */
390_AC_Store.prototype.computeAvoid = function() {
391 return [];
392};
393
394
395function _AC_AddItemToFirstCharMap(firstCharMap, ch, s) {
396 let l = firstCharMap[ch];
397 if (!l) {
398 l = firstCharMap[ch] = [];
399 } else if (l[l.length - 1].value == s) {
400 return;
401 }
402 l.push(new _AC_Completion(s, null, ''));
403}
404
405/**
406 * an _AC_Store implementation suitable for completing lists of email
407 * addresses.
408 * @constructor
409 */
410function _AC_SimpleStore(strings, opt_docStrings) {
411 this.firstCharMap_ = {};
412
413 for (let i = 0; i < strings.length; ++i) {
414 let s = strings[i];
415 if (!s) {
416 continue;
417 }
418 if (opt_docStrings && opt_docStrings[s]) {
419 s = s + ' ' + opt_docStrings[s];
420 }
421
422 const parts = s.split(/\W+/);
423 for (let j = 0; j < parts.length; ++j) {
424 if (parts[j]) {
425 _AC_AddItemToFirstCharMap(
426 this.firstCharMap_, parts[j].charAt(0).toLowerCase(), strings[i]);
427 }
428 }
429 }
430
431 // The maximimum number of results that we are willing to show
432 this.countThreshold = 2500;
433 this.docstrings = opt_docStrings || {};
434}
435_AC_SimpleStore.prototype = new _AC_Store();
436_AC_SimpleStore.prototype.constructor = _AC_SimpleStore;
437
438_AC_SimpleStore.prototype.completable =
439 function(inputValue, caret) {
440 // complete after the last comma not inside ""s
441 let start = 0;
442 let state = 0;
443 for (let i = 0; i < caret; ++i) {
444 const ch = inputValue.charAt(i);
445 switch (state) {
446 case 0:
447 if ('"' == ch) {
448 state = 1;
449 } else if (',' == ch || ' ' == ch) {
450 start = i + 1;
451 }
452 break;
453 case 1:
454 if ('"' == ch) {
455 state = 0;
456 }
457 break;
458 }
459 }
460 while (start < caret &&
461 ' \t\r\n'.indexOf(inputValue.charAt(start)) >= 0) {
462 ++start;
463 }
464 return inputValue.substring(start, caret);
465 };
466
467
468/** Simple function to create a <span> with matching text in bold.
469 */
470function _AC_CreateSpanWithMatchHighlighted(match) {
471 const span = document.createElement('span');
472 span.appendChild(document.createTextNode(match[1] || ''));
473 const bold = document.createElement('b');
474 span.appendChild(bold);
475 bold.appendChild(document.createTextNode(match[2]));
476 span.appendChild(document.createTextNode(match[3] || ''));
477 return span;
478};
479
480
481/**
482 * Get all completions matching the given prefix.
483 * @param {string} prefix The prefix of the text to autocomplete on.
484 * @param {List.<string>?} toFilter Optional list to filter on. Otherwise will
485 * use this.firstCharMap_ using the prefix's first character.
486 * @return {List.<_AC_Completion>} The computed list of completions.
487 */
488_AC_SimpleStore.prototype.completions = function(prefix) {
489 if (!prefix) {
490 return [];
491 }
492 toFilter = this.firstCharMap_[prefix.charAt(0).toLowerCase()];
493
494 // Since we use prefix to build a regular expression, we need to escape RE
495 // characters. We match '-', '{', '$' and others in the prefix and convert
496 // them into "\-", "\{", "\$".
497 const regexForRegexCharacters = /([\^*+\-\$\\\{\}\(\)\[\]\#?\.])/g;
498 const modifiedPrefix = prefix.replace(regexForRegexCharacters, '\\$1');
499
500 // Match the modifiedPrefix anywhere as long as it is either at the very
501 // beginning "Th" -> "The Hobbit", or comes immediately after a word separator
502 // such as "Ga" -> "The-Great-Gatsby".
503 const patternRegex = '^(.*\\W)?(' + modifiedPrefix + ')(.*)';
504 const pattern = new RegExp(patternRegex, 'i' /* ignore case */);
505
506 // We keep separate lists of possible completions that were generated
507 // by matching a value or generated by matching a docstring. We return
508 // a concatenated list so that value matches all come before docstring
509 // matches.
510 const completions = [];
511 const docCompletions = [];
512
513 if (toFilter) {
514 const toFilterLength = toFilter.length;
515 for (let i = 0; i < toFilterLength; ++i) {
516 const docStr = this.docstrings[toFilter[i].value];
517 let compSpan = null;
518 let docSpan = null;
519 const matches = toFilter[i].value.match(pattern);
520 const docMatches = docStr && docStr.match(pattern);
521 if (matches) {
522 compSpan = _AC_CreateSpanWithMatchHighlighted(matches);
523 if (docStr) docSpan = document.createTextNode(docStr);
524 } else if (docMatches) {
525 compSpan = document.createTextNode(toFilter[i].value);
526 docSpan = _AC_CreateSpanWithMatchHighlighted(docMatches);
527 }
528
529 if (compSpan) {
530 const newCompletion = new _AC_Completion(
531 toFilter[i].value, compSpan, docSpan);
532
533 if (matches) {
534 completions.push(newCompletion);
535 } else {
536 docCompletions.push(newCompletion);
537 }
538 if (completions.length + docCompletions.length > this.countThreshold) {
539 break;
540 }
541 }
542 }
543 }
544
545 return completions.concat(docCompletions);
546};
547
548// Normally, when the user types a few characters, we aggressively
549// select the first possible completion (if any). When the user
550// hits ENTER, that first completion is substituted. When that
551// behavior is not desired, override this to return false.
552_AC_SimpleStore.prototype.autoselectFirstRow = function() {
553 return true;
554};
555
556// Comparison function for _AC_Completion
557function _AC_CompareACCompletion(a, b) {
558 // convert it to lower case and remove all leading junk
559 const aval = a.value.toLowerCase().replace(/^\W*/, '');
560 const bval = b.value.toLowerCase().replace(/^\W*/, '');
561
562 if (a.value === b.value) {
563 return 0;
564 } else if (aval < bval) {
565 return -1;
566 } else {
567 return 1;
568 }
569}
570
571_AC_SimpleStore.prototype.substitute =
572function(inputValue, caret, completable, completion) {
573 return inputValue.substring(0, caret - completable.length) +
574 completion.value + ', ' + inputValue.substring(caret);
575};
576
577/**
578 * a possible completion.
579 * @constructor
580 */
581function _AC_Completion(value, compSpan, docSpan) {
582 /** plain text. */
583 this.value = value;
584 if (typeof compSpan == 'string') compSpan = document.createTextNode(compSpan);
585 this.compSpan = compSpan;
586 if (typeof docSpan == 'string') docSpan = document.createTextNode(docSpan);
587 this.docSpan = docSpan;
588}
589_AC_Completion.prototype.toString = function() {
590 return '(AC_Completion: ' + this.value + ')';
591};
592
593/** registered store constructors. @private */
594var ac_storeConstructors = [];
595/**
596 * the focused text input or textarea whether store is null or not.
597 * A text input may have focus and this may be null iff no key has been typed in
598 * the text input.
599 */
600var ac_focusedInput = null;
601/**
602 * null or the autocomplete store used to complete ac_focusedInput.
603 * @private
604 */
605var ac_store = null;
606/** store handler from ac_focusedInput. @private */
607var ac_oldBlurHandler = null;
608/**
609 * true iff user has indicated completions are unwanted (via ESC key)
610 * @private
611 */
612var ac_suppressCompletions = false;
613/**
614 * chunk of completable text seen last keystroke.
615 * Used to generate ac_completions.
616 * @private
617 */
618let ac_lastCompletable = null;
619/** an array of _AC_Completions. @private */
620var ac_completions = null;
621/** -1 or in [0, _AC_Completions.length). @private */
622var ac_selected = -1;
623
624/** Maximum number of options displayed in menu. @private */
625const ac_max_options = 100;
626
627/** Don't offer these values because they are already used. @private */
628let ac_avoidValues = [];
629
630/**
631 * handles all the key strokes, updating the completion list, tracking selected
632 * element, performing substitutions, etc.
633 * @private
634 */
635function ac_handleKey_(key, isDown, isShiftKey) {
636 // check completions
637 ac_checkCompletions();
638 let show = true;
639 const numCompletions = ac_completions ? ac_completions.length : 0;
640 // handle enter and tab on key press and the rest on key down
641 if (ac_store.isCompletionKey(key, isDown, isShiftKey)) {
642 if (ac_selected < 0 && numCompletions >= 1 &&
643 ac_store.autoselectFirstRow()) {
644 ac_selected = 0;
645 }
646 if (ac_selected >= 0) {
647 const backupInput = ac_focusedInput;
648 const completeValue = ac_completions[ac_selected].value;
649 ac_complete();
650 if (ac_store.oncomplete) {
651 ac_store.oncomplete(true, key, backupInput, completeValue);
652 }
653 }
654 } else {
655 switch (key) {
656 case ESC_KEYNAME: // escape
657 // JER?? ac_suppressCompletions = true;
658 ac_selected = -1;
659 show = false;
660 break;
661 case UP_KEYNAME: // up
662 if (isDown) {
663 // firefox fires arrow events on both down and press, but IE only fires
664 // then on press.
665 ac_selected = Math.max(numCompletions >= 0 ? 0 : -1, ac_selected - 1);
666 }
667 break;
668 case DOWN_KEYNAME: // down
669 if (isDown) {
670 ac_selected = Math.min(
671 ac_max_options - 1, Math.min(numCompletions - 1, ac_selected + 1));
672 }
673 break;
674 }
675
676 if (isDown) {
677 switch (key) {
678 case ESC_KEYNAME:
679 case ENTER_KEYNAME:
680 case UP_KEYNAME:
681 case DOWN_KEYNAME:
682 case RIGHT_KEYNAME:
683 case LEFT_KEYNAME:
684 case TAB_KEYNAME:
685 case SHIFT_KEYNAME:
686 case BACKSPACE_KEYNAME:
687 case DELETE_KEYNAME:
688 break;
689 default: // User typed some new characters.
690 ac_everTyped = true;
691 }
692 }
693 }
694
695 if (ac_focusedInput) {
696 ac_updateCompletionList(show);
697 }
698}
699
700/**
701 * called when an option is clicked on to select that option.
702 */
703function _ac_select(optionIndex) {
704 ac_selected = optionIndex;
705 ac_complete();
706 if (ac_store.oncomplete) {
707 ac_store.oncomplete(true, null, ac_focusedInput, ac_focusedInput.value);
708 }
709
710 // check completions
711 ac_checkCompletions();
712 ac_updateCompletionList(true);
713}
714
715function _ac_mouseover(optionIndex) {
716 ac_selected = optionIndex;
717 ac_updateCompletionList(true);
718}
719
720/** perform the substitution of the currently selected item. */
721function ac_complete() {
722 const caret = ac_getCaretPosition_(ac_focusedInput);
723 const completion = ac_completions[ac_selected];
724
725 ac_focusedInput.value = ac_store.substitute(
726 ac_focusedInput.value, caret,
727 ac_lastCompletable, completion);
728 // When the prefix starts with '*' we want to return the complete set of all
729 // possible completions. We treat the ac_lastCompletable value as empty so
730 // that the caret is correctly calculated (i.e. the caret should not consider
731 // placeholder values like '*member').
732 let new_caret = caret + completion.value.length;
733 if (!ac_lastCompletable.startsWith('*')) {
734 // Only consider the ac_lastCompletable length if it does not start with '*'
735 new_caret = new_caret - ac_lastCompletable.length;
736 }
737 // If we inserted something ending in two quotation marks, position
738 // the cursor between the quotation marks. If we inserted a complete term,
739 // skip over the trailing space so that the user is ready to enter the next
740 // term. If we inserted just a search operator, leave the cursor immediately
741 // after the colon or equals and don't skip over the space.
742 if (completion.value.substring(completion.value.length - 2) == '""') {
743 new_caret--;
744 } else if (completion.value.substring(completion.value.length - 1) != ':' &&
745 completion.value.substring(completion.value.length - 1) != '=') {
746 new_caret++; // To account for the comma.
747 new_caret++; // To account for the space after the comma.
748 }
749 ac_selected = -1;
750 ac_completions = null;
751 ac_lastCompletable = null;
752 ac_everTyped = false;
753 SetCursorPos(window, ac_focusedInput, new_caret);
754}
755
756/**
757 * True if the user has ever typed any actual characters in the currently
758 * focused text field. False if they have only clicked, backspaced, and
759 * used the arrow keys.
760 */
761var ac_everTyped = false;
762
763/**
764 * maintains ac_completions, ac_selected, ac_lastCompletable.
765 * @private
766 */
767function ac_checkCompletions() {
768 if (ac_focusedInput && !ac_suppressCompletions) {
769 const caret = ac_getCaretPosition_(ac_focusedInput);
770 const completable = ac_store.completable(ac_focusedInput.value, caret);
771
772 // If we already have completed, then our work here is done.
773 if (completable == ac_lastCompletable) {
774 return;
775 }
776
777 ac_completions = null;
778 ac_selected = -1;
779
780 const oldSelected =
781 ((ac_selected >= 0 && ac_selected < ac_completions.length) ?
782 ac_completions[ac_selected].value : null);
783 ac_completions = ac_store.completions(completable);
784 // Don't offer options for values that the user has already used
785 // in another part of the current form.
786 ac_completions = ac_completions.filter((comp) =>
787 FindInArray(ac_avoidValues, comp.value.toLowerCase()) === -1);
788
789 ac_selected = oldSelected ? 0 : -1;
790 ac_lastCompletable = completable;
791 return;
792 }
793 ac_lastCompletable = null;
794 ac_completions = null;
795 ac_selected = -1;
796}
797
798/**
799 * maintains the completion list GUI.
800 * @private
801 */
802function ac_updateCompletionList(show) {
803 let clist = document.getElementById('ac-list');
804 const input = ac_focusedInput;
805 if (input) {
806 input.setAttribute('aria-activedescendant', 'ac-status-row-none');
807 }
808 let tableEl;
809 let tableBody;
810 if (show && ac_completions && ac_completions.length) {
811 if (!clist) {
812 clist = document.createElement('DIV');
813 clist.id = 'ac-list';
814 clist.style.position = 'absolute';
815 clist.style.display = 'none';
816 // with 'listbox' and 'option' roles, screenreader narrates total
817 // number of options eg. 'New = issue has not .... 1 of 9'
818 document.body.appendChild(clist);
819 tableEl = document.createElement('table');
820 tableEl.setAttribute('cellpadding', 0);
821 tableEl.setAttribute('cellspacing', 0);
822 tableEl.id = 'ac-table';
823 tableEl.setAttribute('role', 'presentation');
824 tableBody = document.createElement('tbody');
825 tableBody.id = 'ac-table-body';
826 tableEl.appendChild(tableBody);
827 tableBody.setAttribute('role', 'listbox');
828 clist.appendChild(tableEl);
829 input.setAttribute('aria-controls', 'ac-table');
830 input.setAttribute('aria-haspopup', 'grid');
831 } else {
832 tableEl = document.getElementById('ac-table');
833 tableBody = document.getElementById('ac-table-body');
834 while (tableBody.childNodes.length) {
835 tableBody.removeChild(tableBody.childNodes[0]);
836 }
837 }
838
839 // If no choice is selected, then select the first item, if desired.
840 if (ac_selected < 0 && ac_store && ac_store.autoselectFirstRow()) {
841 ac_selected = 0;
842 }
843
844 let headerCount= 0;
845 for (let i = 0; i < Math.min(ac_max_options, ac_completions.length); ++i) {
846 if (ac_completions[i].heading) {
847 var rowEl = document.createElement('tr');
848 tableBody.appendChild(rowEl);
849 const cellEl = document.createElement('th');
850 rowEl.appendChild(cellEl);
851 cellEl.setAttribute('colspan', 2);
852 if (headerCount) {
853 cellEl.appendChild(document.createElement('br'));
854 }
855 cellEl.appendChild(
856 document.createTextNode(ac_completions[i].heading));
857 headerCount++;
858 } else {
859 var rowEl = document.createElement('tr');
860 tableBody.appendChild(rowEl);
861 if (i == ac_selected) {
862 rowEl.className = 'selected';
863 }
864 rowEl.id = `ac-status-row-${i}`;
865 rowEl.setAttribute('data-index', i);
866 rowEl.setAttribute('role', 'option');
867 rowEl.addEventListener('mousedown', function(event) {
868 event.preventDefault();
869 });
870 rowEl.addEventListener('mouseup', function(event) {
871 let target = event.target;
872 while (target && target.tagName != 'TR') {
873 target = target.parentNode;
874 }
875 const idx = Number(target.getAttribute('data-index'));
876 try {
877 _ac_select(idx);
878 } finally {
879 return false;
880 }
881 });
882 rowEl.addEventListener('mouseover', function(event) {
883 let target = event.target;
884 while (target && target.tagName != 'TR') {
885 target = target.parentNode;
886 }
887 const idx = Number(target.getAttribute('data-index'));
888 _ac_mouseover(idx);
889 });
890 const valCellEl = document.createElement('td');
891 rowEl.appendChild(valCellEl);
892 if (ac_completions[i].compSpan) {
893 valCellEl.appendChild(ac_completions[i].compSpan);
894 }
895 const docCellEl = document.createElement('td');
896 rowEl.appendChild(docCellEl);
897 if (ac_completions[i].docSpan &&
898 ac_completions[i].docSpan.textContent) {
899 docCellEl.appendChild(document.createTextNode(' = '));
900 docCellEl.appendChild(ac_completions[i].docSpan);
901 }
902 }
903 }
904
905 // position
906 const inputBounds = nodeBounds(ac_focusedInput);
907 clist.style.left = inputBounds.x + 'px';
908 clist.style.top = (inputBounds.y + inputBounds.h) + 'px';
909
910 window.setTimeout(ac_autoscroll, 100);
911 input.setAttribute('aria-activedescendant', `ac-status-row-${ac_selected}`);
912 // Note - we use '' instead of 'block', since 'block' has odd effects on
913 // the screen in IE, and causes scrollbars to resize
914 clist.style.display = '';
915 } else {
916 tableBody = document.getElementById('ac-table-body');
917 if (clist && tableBody) {
918 clist.style.display = 'none';
919 while (tableBody.childNodes.length) {
920 tableBody.removeChild(tableBody.childNodes[0]);
921 }
922 }
923 }
924}
925
926// TODO(jrobbins): make arrow keys and mouse not conflict if they are
927// used at the same time.
928
929
930/** Scroll the autocomplete menu to show the currently selected row. */
931function ac_autoscroll() {
932 const acList = document.getElementById('ac-list');
933 const acSelRow = acList.getElementsByClassName('selected')[0];
934 const acSelRowTop = acSelRow ? acSelRow.offsetTop : 0;
935 const acSelRowHeight = acSelRow ? acSelRow.offsetHeight : 0;
936
937
938 const EXTRA = 8; // Go an extra few pixels so the next row is partly exposed.
939
940 if (!acList || !acSelRow) return;
941
942 // Autoscroll upward if the selected item is above the visible area,
943 // else autoscroll downward if the selected item is below the visible area.
944 if (acSelRowTop < acList.scrollTop) {
945 acList.scrollTop = acSelRowTop - EXTRA;
946 } else if (acSelRowTop + acSelRowHeight + EXTRA >
947 acList.scrollTop + acList.offsetHeight) {
948 acList.scrollTop = (acSelRowTop + acSelRowHeight -
949 acList.offsetHeight + EXTRA);
950 }
951}
952
953
954/** the position of the text caret in the given text field.
955 *
956 * @param textField an INPUT node with type=text or a TEXTAREA node
957 * @return an index in [0, textField.value.length]
958 */
959function ac_getCaretPosition_(textField) {
960 if ('INPUT' == textField.tagName) {
961 let caret = textField.value.length;
962
963 // chrome/firefox
964 if (undefined != textField.selectionStart) {
965 caret = textField.selectionEnd;
966
967 // JER: Special treatment for issue status field that makes all
968 // options show up more often
969 if (textField.id.startsWith('status')) {
970 caret = textField.selectionStart;
971 }
972 // ie
973 } else if (document.selection) {
974 // get an empty selection range
975 const range = document.selection.createRange();
976 const origSelectionLength = range.text.length;
977 // Force selection start to 0 position
978 range.moveStart('character', -caret);
979 // the caret end position is the new selection length
980 caret = range.text.length;
981
982 // JER: Special treatment for issue status field that makes all
983 // options show up more often
984 if (textField.id.startsWith('status')) {
985 // The amount that the selection grew when we forced start to
986 // position 0 is == the original start position.
987 caret = range.text.length - origSelectionLength;
988 }
989 }
990
991 return caret;
992 } else {
993 // a textarea
994
995 return GetCursorPos(window, textField);
996 }
997}
998
999function getTargetFromEvent(event) {
1000 let targ = event.target || event.srcElement;
1001 if (targ.shadowRoot) {
1002 // Find the element within the shadowDOM.
1003 const path = event.path || event.composedPath();
1004 targ = path[0];
1005 }
1006 return targ;
1007}