| /* Copyright 2016 The Chromium Authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style |
| * license that can be found in the LICENSE file or at |
| * https://developers.google.com/open-source/licenses/bsd |
| */ |
| |
| /** |
| * An autocomplete library for javascript. |
| * Public API |
| * - _ac_install() install global handlers required for everything else to |
| * function. |
| * - _ac_register(SC) register a store constructor (see below) |
| * - _ac_isCompleting() true iff focus is in an auto complete box and the user |
| * has triggered completion with a keystroke, and completion has not been |
| * cancelled (programatically or otherwise). |
| * - _ac_isCompleteListShowing() true if _as_isCompleting and the complete list |
| * is visible to the user. |
| * - _ac_cancel() if completing, stop it, otherwise a no-op. |
| * |
| * |
| * A quick example |
| * // an auto complete store |
| * var myFavoritestAutoCompleteStore = new _AC_SimpleStore( |
| * ['some', 'strings', 'to', 'complete']); |
| * |
| * // a store constructor |
| * _ac_register(function (inputNode, keyEvent) { |
| * if (inputNode.id == 'my-auto-completing-check-box') { |
| * return myFavoritestAutoCompleteStore; |
| * } |
| * return null; |
| * }); |
| * |
| * <html> |
| * <head> |
| * <script type=text/javascript src=ac.js></script> |
| * </head> |
| * <body onload=_ac_install()> |
| * <!-- the constructor above looks at the id. It could as easily |
| * - look at the class, name, or value. |
| * - The autocomplete=off stops browser autocomplete from |
| * - interfering with our autocomplete |
| * --> |
| * <input type=text id="my-auto-completing-check-box" |
| * autocomplete=off> |
| * </body> |
| * </html> |
| * |
| * |
| * Concepts |
| * - Store Constructor function |
| * A store constructor is a policy function with the signature |
| * _AC_Store myStoreConstructor( |
| * HtmlInputElement|HtmlTextAreaElement inputNode, Event keyEvent) |
| * When a key event is received on a text input or text area, the autocomplete |
| * library will try each of the store constructors in turn until it finds one |
| * that returns an AC_Store which will be used for auto-completion of that |
| * text box until focus is lost. |
| * |
| * - interface _AC_Store |
| * An autocomplete store encapsulates all operations that affect how a |
| * particular text node is autocompleted. It has the following operations: |
| * - String completable(String inputValue, int caret) |
| * This method returns null if not completable or the section of inputValue |
| * that is subject to completion. If autocomplete works on items in a |
| * comma separated list, then the input value "foo, ba" might yield "ba" |
| * as the completable chunk since it is separated from its predecessor by |
| * a comma. |
| * caret is the position of the text cursor (caret) in the text input. |
| * - _AC_Completion[] completions(String completable, |
| * _AC_Completion[] toFilter) |
| * This method returns null if there are no completions. If toFilter is |
| * not null or undefined, then this method may assume that toFilter was |
| * returned as a set of completions that contain completable. |
| * - String substitute(String inputValue, int caret, |
| * String completable, _AC_Completion completion) |
| * returns the inputValue with the given completion substituted for the |
| * given completable. caret has the same meaning as in the |
| * completable operation. |
| * - String oncomplete(boolean completed, String key, |
| * HTMLElement element, String text) |
| * This method is called when the user hits a completion key. The default |
| * value is to do nothing, but you can override it if you want. Note that |
| * key will be null if the user clicked on it to select |
| * - Boolean autoselectFirstRow() |
| * This method returns True by default, but subclasses can override it |
| * to make autocomplete fields that require the user to press the down |
| * arrow or do a mouseover once before any completion option is considered |
| * to be selected. |
| * |
| * - class _AC_SimpleStore |
| * An implementation of _AC_Store that completes a set of strings given at |
| * construct time in a text field with a comma separated value. |
| * |
| * - struct _AC_Completion |
| * a struct with two fields |
| * - String value : the plain text completion value |
| * - String html : the value, as html, with the completable in bold. |
| * |
| * Key Handling |
| * Several keys affect completion in an autocompleted input. |
| * ESC - the escape key cancels autocompleting. The autocompletion will have |
| * no effect on the focused textbox until it loses focus, regains it, and |
| * a key is pressed. |
| * ENTER - completes using the currently selected completion, or if there is |
| * only one, uses that completion. |
| * UP ARROW - selects the completion above the current selection. |
| * DOWN ARROW - selects the completion below the current selection. |
| * |
| * |
| * CSS styles |
| * The following CSS selector rules can be used to change the completion list |
| * look: |
| * #ac-list style of the auto-complete list |
| * #ac-list .selected style of the selected item |
| * #ac-list b style of the matching text in a candidate completion |
| * |
| * Dependencies |
| * The library depends on the following libraries: |
| * javascript:base for definition of key constants and SetCursorPos |
| * javascript:shapes for nodeBounds() |
| */ |
| |
| /** |
| * install global handlers required for the rest of the module to function. |
| */ |
| function _ac_install() { |
| ac_addHandler_(document.body, 'onkeydown', ac_keyevent_); |
| ac_addHandler_(document.body, 'onkeypress', ac_keyevent_); |
| } |
| |
| /** |
| * register a store constructor |
| * @param storeConstructor a function like |
| * _AC_Store myStoreConstructor(HtmlInputElement|HtmlTextArea, Event) |
| */ |
| function _ac_register(storeConstructor) { |
| // check that not already registered |
| for (let i = ac_storeConstructors.length; --i >= 0;) { |
| if (ac_storeConstructors[i] === storeConstructor) { |
| return; |
| } |
| } |
| ac_storeConstructors.push(storeConstructor); |
| } |
| |
| /** |
| * may be attached as an onfocus handler to a text input to popup autocomplete |
| * immediately on the box gaining focus. |
| */ |
| function _ac_onfocus(event) { |
| ac_keyevent_(event); |
| } |
| |
| /** |
| * true iff the autocomplete widget is currently active. |
| */ |
| function _ac_isCompleting() { |
| return !!ac_store && !ac_suppressCompletions; |
| } |
| |
| /** |
| * true iff the completion list is displayed. |
| */ |
| function _ac_isCompleteListShowing() { |
| return !!ac_store && !ac_suppressCompletions && ac_completions && |
| ac_completions.length; |
| } |
| |
| /** |
| * cancel any autocomplete in progress. |
| */ |
| function _ac_cancel() { |
| ac_suppressCompletions = true; |
| ac_updateCompletionList(false); |
| } |
| |
| /** add a handler without whacking any existing handler. @private */ |
| function ac_addHandler_(node, handlerName, handler) { |
| const oldHandler = node[handlerName]; |
| if (!oldHandler) { |
| node[handlerName] = handler; |
| } else { |
| node[handlerName] = ac_fnchain_(node[handlerName], handler); |
| } |
| return oldHandler; |
| } |
| |
| /** cancel the event. @private */ |
| function ac_cancelEvent_(event) { |
| if ('stopPropagation' in event) { |
| event.stopPropagation(); |
| } else { |
| event.cancelBubble = true; |
| } |
| |
| // This is handled in IE by returning false from the handler |
| if ('preventDefault' in event) { |
| event.preventDefault(); |
| } |
| } |
| |
| /** Call two functions, a and b, and return false if either one returns |
| false. This is used as a primitive way to attach multiple event |
| handlers to an element without using addEventListener(). This |
| library predates the availablity of addEventListener(). |
| @private |
| */ |
| function ac_fnchain_(a, b) { |
| return function() { |
| const ar = a.apply(this, arguments); |
| const br = b.apply(this, arguments); |
| |
| // NOTE 1: (undefined && false) -> undefined |
| // NOTE 2: returning FALSE from a onkeypressed cancels it, |
| // returning UNDEFINED does not. |
| // As such, we specifically look for falses here |
| if (ar === false || br === false) { |
| return false; |
| } else { |
| return true; |
| } |
| }; |
| } |
| |
| /** key press handler. @private */ |
| function ac_keyevent_(event) { |
| event = event || window.event; |
| |
| const source = getTargetFromEvent(event); |
| const isInput = 'INPUT' == source.tagName && |
| source.type.match(/^text|email$/i); |
| const isTextarea = 'TEXTAREA' == source.tagName; |
| if (!isInput && !isTextarea) return true; |
| |
| const key = event.key; |
| const isDown = event.type == 'keydown'; |
| const isShiftKey = event.shiftKey; |
| let storeFound = true; |
| |
| if ((source !== ac_focusedInput) || (ac_store === null)) { |
| ac_focusedInput = source; |
| storeFound = false; |
| if (ENTER_KEYNAME !== key && ESC_KEYNAME !== key) { |
| for (let i = 0; i < ac_storeConstructors.length; ++i) { |
| const store = (ac_storeConstructors[i])(source, event); |
| if (store) { |
| ac_store = store; |
| ac_store.setAvoid(event); |
| ac_oldBlurHandler = ac_addHandler_( |
| ac_focusedInput, 'onblur', _ac_ob); |
| storeFound = true; |
| break; |
| } |
| } |
| |
| // There exists an odd condition where an edit box with autocomplete |
| // attached can be removed from the DOM without blur being called |
| // In which case we are left with a store around that will try to |
| // autocomplete the next edit box to receive focus. We need to clean |
| // this up |
| |
| // If we can't find a store, force a blur |
| if (!storeFound) { |
| _ac_ob(null); |
| } |
| } |
| // ac-table rows need to be removed when switching to another input. |
| ac_updateCompletionList(false); |
| } |
| // If the user typed Esc when the auto-complete menu was not shown, |
| // then blur the input text field so that the user can use keyboard |
| // shortcuts. |
| const acList = document.getElementById('ac-list'); |
| if (ESC_KEYNAME == key && |
| (!acList || acList.style.display == 'none')) { |
| ac_focusedInput.blur(); |
| } |
| |
| if (!storeFound) return true; |
| |
| const isCompletion = ac_store.isCompletionKey(key, isDown, isShiftKey); |
| const hasResults = ac_completions && (ac_completions.length > 0); |
| let cancelEvent = false; |
| |
| if (isCompletion && hasResults) { |
| // Cancel any enter keystrokes if something is selected so that the |
| // browser doesn't go submitting the form. |
| cancelEvent = (!ac_suppressCompletions && !!ac_completions && |
| (ac_selected != -1)); |
| window.setTimeout(function() { |
| if (ac_store) { |
| ac_handleKey_(key, isDown, isShiftKey); |
| } |
| }, 0); |
| } else if (!isCompletion) { |
| // Don't want to also blur the field. Up and down move the cursor (in |
| // Firefox) to the start/end of the field. We also don't want that while |
| // the list is showing. |
| cancelEvent = (key == ESC_KEYNAME || |
| key == DOWN_KEYNAME || |
| key == UP_KEYNAME); |
| |
| window.setTimeout(function() { |
| if (ac_store) { |
| ac_handleKey_(key, isDown, isShiftKey); |
| } |
| }, 0); |
| } else { // implicit if (isCompletion && !hasResults) |
| if (ac_store.oncomplete) { |
| ac_store.oncomplete(false, key, ac_focusedInput, undefined); |
| } |
| } |
| |
| if (cancelEvent) { |
| ac_cancelEvent_(event); |
| } |
| |
| return !cancelEvent; |
| } |
| |
| /** Autocomplete onblur handler. */ |
| function _ac_ob(event) { |
| if (ac_focusedInput) { |
| ac_focusedInput.onblur = ac_oldBlurHandler; |
| } |
| ac_store = null; |
| ac_focusedInput = null; |
| ac_everTyped = false; |
| ac_oldBlurHandler = null; |
| ac_suppressCompletions = false; |
| ac_updateCompletionList(false); |
| } |
| |
| /** @constructor */ |
| function _AC_Store() { |
| } |
| /** returns the chunk of the input to treat as completable. */ |
| _AC_Store.prototype.completable = function(inputValue, caret) { |
| console.log('UNIMPLEMENTED completable'); |
| }; |
| /** returns the chunk of the input to treat as completable. */ |
| _AC_Store.prototype.completions = function(prefix, tofilter) { |
| console.log('UNIMPLEMENTED completions'); |
| }; |
| /** returns the chunk of the input to treat as completable. */ |
| _AC_Store.prototype.oncomplete = function(completed, key, element, text) { |
| // Call the onkeyup handler so that choosing an autocomplete option has |
| // the same side-effect as typing. E.g., exposing the next row of input |
| // fields. |
| element.dispatchEvent(new Event('keyup')); |
| _ac_ob(); |
| }; |
| /** substitutes a completion for a completable in a text input's value. */ |
| _AC_Store.prototype.substitute = |
| function(inputValue, caret, completable, completion) { |
| console.log('UNIMPLEMENTED substitute'); |
| }; |
| /** true iff hitting a comma key should complete. */ |
| _AC_Store.prototype.commaCompletes = true; |
| /** |
| * true iff the given keystroke should cause a completion (and be consumed in |
| * the process. |
| */ |
| _AC_Store.prototype.isCompletionKey = function(key, isDown, isShiftKey) { |
| if (!isDown && (ENTER_KEYNAME === key || |
| (COMMA_KEYNAME == key && this.commaCompletes))) { |
| return true; |
| } |
| if (TAB_KEYNAME === key && !isShiftKey) { |
| // IE doesn't fire an event for tab on click in a text field, and firefox |
| // requires that the onkeypress event for tab be consumed or it navigates |
| // to next field. |
| return false; |
| // JER: return isDown == BR_IsIE(); |
| } |
| return false; |
| }; |
| |
| _AC_Store.prototype.setAvoid = function(event) { |
| if (event && event.avoidValues) { |
| ac_avoidValues = event.avoidValues; |
| } else { |
| ac_avoidValues = this.computeAvoid(); |
| } |
| ac_avoidValues = ac_avoidValues.map((val) => val.toLowerCase()); |
| }; |
| |
| /* Subclasses may implement this to compute values to avoid |
| offering in the current input field, i.e., because those |
| values are already used. */ |
| _AC_Store.prototype.computeAvoid = function() { |
| return []; |
| }; |
| |
| |
| function _AC_AddItemToFirstCharMap(firstCharMap, ch, s) { |
| let l = firstCharMap[ch]; |
| if (!l) { |
| l = firstCharMap[ch] = []; |
| } else if (l[l.length - 1].value == s) { |
| return; |
| } |
| l.push(new _AC_Completion(s, null, '')); |
| } |
| |
| /** |
| * an _AC_Store implementation suitable for completing lists of email |
| * addresses. |
| * @constructor |
| */ |
| function _AC_SimpleStore(strings, opt_docStrings) { |
| this.firstCharMap_ = {}; |
| |
| for (let i = 0; i < strings.length; ++i) { |
| let s = strings[i]; |
| if (!s) { |
| continue; |
| } |
| if (opt_docStrings && opt_docStrings[s]) { |
| s = s + ' ' + opt_docStrings[s]; |
| } |
| |
| const parts = s.split(/\W+/); |
| for (let j = 0; j < parts.length; ++j) { |
| if (parts[j]) { |
| _AC_AddItemToFirstCharMap( |
| this.firstCharMap_, parts[j].charAt(0).toLowerCase(), strings[i]); |
| } |
| } |
| } |
| |
| // The maximimum number of results that we are willing to show |
| this.countThreshold = 2500; |
| this.docstrings = opt_docStrings || {}; |
| } |
| _AC_SimpleStore.prototype = new _AC_Store(); |
| _AC_SimpleStore.prototype.constructor = _AC_SimpleStore; |
| |
| _AC_SimpleStore.prototype.completable = |
| function(inputValue, caret) { |
| // complete after the last comma not inside ""s |
| let start = 0; |
| let state = 0; |
| for (let i = 0; i < caret; ++i) { |
| const ch = inputValue.charAt(i); |
| switch (state) { |
| case 0: |
| if ('"' == ch) { |
| state = 1; |
| } else if (',' == ch || ' ' == ch) { |
| start = i + 1; |
| } |
| break; |
| case 1: |
| if ('"' == ch) { |
| state = 0; |
| } |
| break; |
| } |
| } |
| while (start < caret && |
| ' \t\r\n'.indexOf(inputValue.charAt(start)) >= 0) { |
| ++start; |
| } |
| return inputValue.substring(start, caret); |
| }; |
| |
| |
| /** Simple function to create a <span> with matching text in bold. |
| */ |
| function _AC_CreateSpanWithMatchHighlighted(match) { |
| const span = document.createElement('span'); |
| span.appendChild(document.createTextNode(match[1] || '')); |
| const bold = document.createElement('b'); |
| span.appendChild(bold); |
| bold.appendChild(document.createTextNode(match[2])); |
| span.appendChild(document.createTextNode(match[3] || '')); |
| return span; |
| }; |
| |
| |
| /** |
| * Get all completions matching the given prefix. |
| * @param {string} prefix The prefix of the text to autocomplete on. |
| * @param {List.<string>?} toFilter Optional list to filter on. Otherwise will |
| * use this.firstCharMap_ using the prefix's first character. |
| * @return {List.<_AC_Completion>} The computed list of completions. |
| */ |
| _AC_SimpleStore.prototype.completions = function(prefix) { |
| if (!prefix) { |
| return []; |
| } |
| toFilter = this.firstCharMap_[prefix.charAt(0).toLowerCase()]; |
| |
| // Since we use prefix to build a regular expression, we need to escape RE |
| // characters. We match '-', '{', '$' and others in the prefix and convert |
| // them into "\-", "\{", "\$". |
| const regexForRegexCharacters = /([\^*+\-\$\\\{\}\(\)\[\]\#?\.])/g; |
| const modifiedPrefix = prefix.replace(regexForRegexCharacters, '\\$1'); |
| |
| // Match the modifiedPrefix anywhere as long as it is either at the very |
| // beginning "Th" -> "The Hobbit", or comes immediately after a word separator |
| // such as "Ga" -> "The-Great-Gatsby". |
| const patternRegex = '^(.*\\W)?(' + modifiedPrefix + ')(.*)'; |
| const pattern = new RegExp(patternRegex, 'i' /* ignore case */); |
| |
| // We keep separate lists of possible completions that were generated |
| // by matching a value or generated by matching a docstring. We return |
| // a concatenated list so that value matches all come before docstring |
| // matches. |
| const completions = []; |
| const docCompletions = []; |
| |
| if (toFilter) { |
| const toFilterLength = toFilter.length; |
| for (let i = 0; i < toFilterLength; ++i) { |
| const docStr = this.docstrings[toFilter[i].value]; |
| let compSpan = null; |
| let docSpan = null; |
| const matches = toFilter[i].value.match(pattern); |
| const docMatches = docStr && docStr.match(pattern); |
| if (matches) { |
| compSpan = _AC_CreateSpanWithMatchHighlighted(matches); |
| if (docStr) docSpan = document.createTextNode(docStr); |
| } else if (docMatches) { |
| compSpan = document.createTextNode(toFilter[i].value); |
| docSpan = _AC_CreateSpanWithMatchHighlighted(docMatches); |
| } |
| |
| if (compSpan) { |
| const newCompletion = new _AC_Completion( |
| toFilter[i].value, compSpan, docSpan); |
| |
| if (matches) { |
| completions.push(newCompletion); |
| } else { |
| docCompletions.push(newCompletion); |
| } |
| if (completions.length + docCompletions.length > this.countThreshold) { |
| break; |
| } |
| } |
| } |
| } |
| |
| return completions.concat(docCompletions); |
| }; |
| |
| // Normally, when the user types a few characters, we aggressively |
| // select the first possible completion (if any). When the user |
| // hits ENTER, that first completion is substituted. When that |
| // behavior is not desired, override this to return false. |
| _AC_SimpleStore.prototype.autoselectFirstRow = function() { |
| return true; |
| }; |
| |
| // Comparison function for _AC_Completion |
| function _AC_CompareACCompletion(a, b) { |
| // convert it to lower case and remove all leading junk |
| const aval = a.value.toLowerCase().replace(/^\W*/, ''); |
| const bval = b.value.toLowerCase().replace(/^\W*/, ''); |
| |
| if (a.value === b.value) { |
| return 0; |
| } else if (aval < bval) { |
| return -1; |
| } else { |
| return 1; |
| } |
| } |
| |
| _AC_SimpleStore.prototype.substitute = |
| function(inputValue, caret, completable, completion) { |
| return inputValue.substring(0, caret - completable.length) + |
| completion.value + ', ' + inputValue.substring(caret); |
| }; |
| |
| /** |
| * a possible completion. |
| * @constructor |
| */ |
| function _AC_Completion(value, compSpan, docSpan) { |
| /** plain text. */ |
| this.value = value; |
| if (typeof compSpan == 'string') compSpan = document.createTextNode(compSpan); |
| this.compSpan = compSpan; |
| if (typeof docSpan == 'string') docSpan = document.createTextNode(docSpan); |
| this.docSpan = docSpan; |
| } |
| _AC_Completion.prototype.toString = function() { |
| return '(AC_Completion: ' + this.value + ')'; |
| }; |
| |
| /** registered store constructors. @private */ |
| var ac_storeConstructors = []; |
| /** |
| * the focused text input or textarea whether store is null or not. |
| * A text input may have focus and this may be null iff no key has been typed in |
| * the text input. |
| */ |
| var ac_focusedInput = null; |
| /** |
| * null or the autocomplete store used to complete ac_focusedInput. |
| * @private |
| */ |
| var ac_store = null; |
| /** store handler from ac_focusedInput. @private */ |
| var ac_oldBlurHandler = null; |
| /** |
| * true iff user has indicated completions are unwanted (via ESC key) |
| * @private |
| */ |
| var ac_suppressCompletions = false; |
| /** |
| * chunk of completable text seen last keystroke. |
| * Used to generate ac_completions. |
| * @private |
| */ |
| let ac_lastCompletable = null; |
| /** an array of _AC_Completions. @private */ |
| var ac_completions = null; |
| /** -1 or in [0, _AC_Completions.length). @private */ |
| var ac_selected = -1; |
| |
| /** Maximum number of options displayed in menu. @private */ |
| const ac_max_options = 100; |
| |
| /** Don't offer these values because they are already used. @private */ |
| let ac_avoidValues = []; |
| |
| /** |
| * handles all the key strokes, updating the completion list, tracking selected |
| * element, performing substitutions, etc. |
| * @private |
| */ |
| function ac_handleKey_(key, isDown, isShiftKey) { |
| // check completions |
| ac_checkCompletions(); |
| let show = true; |
| const numCompletions = ac_completions ? ac_completions.length : 0; |
| // handle enter and tab on key press and the rest on key down |
| if (ac_store.isCompletionKey(key, isDown, isShiftKey)) { |
| if (ac_selected < 0 && numCompletions >= 1 && |
| ac_store.autoselectFirstRow()) { |
| ac_selected = 0; |
| } |
| if (ac_selected >= 0) { |
| const backupInput = ac_focusedInput; |
| const completeValue = ac_completions[ac_selected].value; |
| ac_complete(); |
| if (ac_store.oncomplete) { |
| ac_store.oncomplete(true, key, backupInput, completeValue); |
| } |
| } |
| } else { |
| switch (key) { |
| case ESC_KEYNAME: // escape |
| // JER?? ac_suppressCompletions = true; |
| ac_selected = -1; |
| show = false; |
| break; |
| case UP_KEYNAME: // up |
| if (isDown) { |
| // firefox fires arrow events on both down and press, but IE only fires |
| // then on press. |
| ac_selected = Math.max(numCompletions >= 0 ? 0 : -1, ac_selected - 1); |
| } |
| break; |
| case DOWN_KEYNAME: // down |
| if (isDown) { |
| ac_selected = Math.min( |
| ac_max_options - 1, Math.min(numCompletions - 1, ac_selected + 1)); |
| } |
| break; |
| } |
| |
| if (isDown) { |
| switch (key) { |
| case ESC_KEYNAME: |
| case ENTER_KEYNAME: |
| case UP_KEYNAME: |
| case DOWN_KEYNAME: |
| case RIGHT_KEYNAME: |
| case LEFT_KEYNAME: |
| case TAB_KEYNAME: |
| case SHIFT_KEYNAME: |
| case BACKSPACE_KEYNAME: |
| case DELETE_KEYNAME: |
| break; |
| default: // User typed some new characters. |
| ac_everTyped = true; |
| } |
| } |
| } |
| |
| if (ac_focusedInput) { |
| ac_updateCompletionList(show); |
| } |
| } |
| |
| /** |
| * called when an option is clicked on to select that option. |
| */ |
| function _ac_select(optionIndex) { |
| ac_selected = optionIndex; |
| ac_complete(); |
| if (ac_store.oncomplete) { |
| ac_store.oncomplete(true, null, ac_focusedInput, ac_focusedInput.value); |
| } |
| |
| // check completions |
| ac_checkCompletions(); |
| ac_updateCompletionList(true); |
| } |
| |
| function _ac_mouseover(optionIndex) { |
| ac_selected = optionIndex; |
| ac_updateCompletionList(true); |
| } |
| |
| /** perform the substitution of the currently selected item. */ |
| function ac_complete() { |
| const caret = ac_getCaretPosition_(ac_focusedInput); |
| const completion = ac_completions[ac_selected]; |
| |
| ac_focusedInput.value = ac_store.substitute( |
| ac_focusedInput.value, caret, |
| ac_lastCompletable, completion); |
| // When the prefix starts with '*' we want to return the complete set of all |
| // possible completions. We treat the ac_lastCompletable value as empty so |
| // that the caret is correctly calculated (i.e. the caret should not consider |
| // placeholder values like '*member'). |
| let new_caret = caret + completion.value.length; |
| if (!ac_lastCompletable.startsWith('*')) { |
| // Only consider the ac_lastCompletable length if it does not start with '*' |
| new_caret = new_caret - ac_lastCompletable.length; |
| } |
| // If we inserted something ending in two quotation marks, position |
| // the cursor between the quotation marks. If we inserted a complete term, |
| // skip over the trailing space so that the user is ready to enter the next |
| // term. If we inserted just a search operator, leave the cursor immediately |
| // after the colon or equals and don't skip over the space. |
| if (completion.value.substring(completion.value.length - 2) == '""') { |
| new_caret--; |
| } else if (completion.value.substring(completion.value.length - 1) != ':' && |
| completion.value.substring(completion.value.length - 1) != '=') { |
| new_caret++; // To account for the comma. |
| new_caret++; // To account for the space after the comma. |
| } |
| ac_selected = -1; |
| ac_completions = null; |
| ac_lastCompletable = null; |
| ac_everTyped = false; |
| SetCursorPos(window, ac_focusedInput, new_caret); |
| } |
| |
| /** |
| * True if the user has ever typed any actual characters in the currently |
| * focused text field. False if they have only clicked, backspaced, and |
| * used the arrow keys. |
| */ |
| var ac_everTyped = false; |
| |
| /** |
| * maintains ac_completions, ac_selected, ac_lastCompletable. |
| * @private |
| */ |
| function ac_checkCompletions() { |
| if (ac_focusedInput && !ac_suppressCompletions) { |
| const caret = ac_getCaretPosition_(ac_focusedInput); |
| const completable = ac_store.completable(ac_focusedInput.value, caret); |
| |
| // If we already have completed, then our work here is done. |
| if (completable == ac_lastCompletable) { |
| return; |
| } |
| |
| ac_completions = null; |
| ac_selected = -1; |
| |
| const oldSelected = |
| ((ac_selected >= 0 && ac_selected < ac_completions.length) ? |
| ac_completions[ac_selected].value : null); |
| ac_completions = ac_store.completions(completable); |
| // Don't offer options for values that the user has already used |
| // in another part of the current form. |
| ac_completions = ac_completions.filter((comp) => |
| FindInArray(ac_avoidValues, comp.value.toLowerCase()) === -1); |
| |
| ac_selected = oldSelected ? 0 : -1; |
| ac_lastCompletable = completable; |
| return; |
| } |
| ac_lastCompletable = null; |
| ac_completions = null; |
| ac_selected = -1; |
| } |
| |
| /** |
| * maintains the completion list GUI. |
| * @private |
| */ |
| function ac_updateCompletionList(show) { |
| let clist = document.getElementById('ac-list'); |
| const input = ac_focusedInput; |
| if (input) { |
| input.setAttribute('aria-activedescendant', 'ac-status-row-none'); |
| } |
| let tableEl; |
| let tableBody; |
| if (show && ac_completions && ac_completions.length) { |
| if (!clist) { |
| clist = document.createElement('DIV'); |
| clist.id = 'ac-list'; |
| clist.style.position = 'absolute'; |
| clist.style.display = 'none'; |
| // with 'listbox' and 'option' roles, screenreader narrates total |
| // number of options eg. 'New = issue has not .... 1 of 9' |
| document.body.appendChild(clist); |
| tableEl = document.createElement('table'); |
| tableEl.setAttribute('cellpadding', 0); |
| tableEl.setAttribute('cellspacing', 0); |
| tableEl.id = 'ac-table'; |
| tableEl.setAttribute('role', 'presentation'); |
| tableBody = document.createElement('tbody'); |
| tableBody.id = 'ac-table-body'; |
| tableEl.appendChild(tableBody); |
| tableBody.setAttribute('role', 'listbox'); |
| clist.appendChild(tableEl); |
| input.setAttribute('aria-controls', 'ac-table'); |
| input.setAttribute('aria-haspopup', 'grid'); |
| } else { |
| tableEl = document.getElementById('ac-table'); |
| tableBody = document.getElementById('ac-table-body'); |
| while (tableBody.childNodes.length) { |
| tableBody.removeChild(tableBody.childNodes[0]); |
| } |
| } |
| |
| // If no choice is selected, then select the first item, if desired. |
| if (ac_selected < 0 && ac_store && ac_store.autoselectFirstRow()) { |
| ac_selected = 0; |
| } |
| |
| let headerCount= 0; |
| for (let i = 0; i < Math.min(ac_max_options, ac_completions.length); ++i) { |
| if (ac_completions[i].heading) { |
| var rowEl = document.createElement('tr'); |
| tableBody.appendChild(rowEl); |
| const cellEl = document.createElement('th'); |
| rowEl.appendChild(cellEl); |
| cellEl.setAttribute('colspan', 2); |
| if (headerCount) { |
| cellEl.appendChild(document.createElement('br')); |
| } |
| cellEl.appendChild( |
| document.createTextNode(ac_completions[i].heading)); |
| headerCount++; |
| } else { |
| var rowEl = document.createElement('tr'); |
| tableBody.appendChild(rowEl); |
| if (i == ac_selected) { |
| rowEl.className = 'selected'; |
| } |
| rowEl.id = `ac-status-row-${i}`; |
| rowEl.setAttribute('data-index', i); |
| rowEl.setAttribute('role', 'option'); |
| rowEl.addEventListener('mousedown', function(event) { |
| event.preventDefault(); |
| }); |
| rowEl.addEventListener('mouseup', function(event) { |
| let target = event.target; |
| while (target && target.tagName != 'TR') { |
| target = target.parentNode; |
| } |
| const idx = Number(target.getAttribute('data-index')); |
| try { |
| _ac_select(idx); |
| } finally { |
| return false; |
| } |
| }); |
| rowEl.addEventListener('mouseover', function(event) { |
| let target = event.target; |
| while (target && target.tagName != 'TR') { |
| target = target.parentNode; |
| } |
| const idx = Number(target.getAttribute('data-index')); |
| _ac_mouseover(idx); |
| }); |
| const valCellEl = document.createElement('td'); |
| rowEl.appendChild(valCellEl); |
| if (ac_completions[i].compSpan) { |
| valCellEl.appendChild(ac_completions[i].compSpan); |
| } |
| const docCellEl = document.createElement('td'); |
| rowEl.appendChild(docCellEl); |
| if (ac_completions[i].docSpan && |
| ac_completions[i].docSpan.textContent) { |
| docCellEl.appendChild(document.createTextNode(' = ')); |
| docCellEl.appendChild(ac_completions[i].docSpan); |
| } |
| } |
| } |
| |
| // position |
| const inputBounds = nodeBounds(ac_focusedInput); |
| clist.style.left = inputBounds.x + 'px'; |
| clist.style.top = (inputBounds.y + inputBounds.h) + 'px'; |
| |
| window.setTimeout(ac_autoscroll, 100); |
| input.setAttribute('aria-activedescendant', `ac-status-row-${ac_selected}`); |
| // Note - we use '' instead of 'block', since 'block' has odd effects on |
| // the screen in IE, and causes scrollbars to resize |
| clist.style.display = ''; |
| } else { |
| tableBody = document.getElementById('ac-table-body'); |
| if (clist && tableBody) { |
| clist.style.display = 'none'; |
| while (tableBody.childNodes.length) { |
| tableBody.removeChild(tableBody.childNodes[0]); |
| } |
| } |
| } |
| } |
| |
| // TODO(jrobbins): make arrow keys and mouse not conflict if they are |
| // used at the same time. |
| |
| |
| /** Scroll the autocomplete menu to show the currently selected row. */ |
| function ac_autoscroll() { |
| const acList = document.getElementById('ac-list'); |
| const acSelRow = acList.getElementsByClassName('selected')[0]; |
| const acSelRowTop = acSelRow ? acSelRow.offsetTop : 0; |
| const acSelRowHeight = acSelRow ? acSelRow.offsetHeight : 0; |
| |
| |
| const EXTRA = 8; // Go an extra few pixels so the next row is partly exposed. |
| |
| if (!acList || !acSelRow) return; |
| |
| // Autoscroll upward if the selected item is above the visible area, |
| // else autoscroll downward if the selected item is below the visible area. |
| if (acSelRowTop < acList.scrollTop) { |
| acList.scrollTop = acSelRowTop - EXTRA; |
| } else if (acSelRowTop + acSelRowHeight + EXTRA > |
| acList.scrollTop + acList.offsetHeight) { |
| acList.scrollTop = (acSelRowTop + acSelRowHeight - |
| acList.offsetHeight + EXTRA); |
| } |
| } |
| |
| |
| /** the position of the text caret in the given text field. |
| * |
| * @param textField an INPUT node with type=text or a TEXTAREA node |
| * @return an index in [0, textField.value.length] |
| */ |
| function ac_getCaretPosition_(textField) { |
| if ('INPUT' == textField.tagName) { |
| let caret = textField.value.length; |
| |
| // chrome/firefox |
| if (undefined != textField.selectionStart) { |
| caret = textField.selectionEnd; |
| |
| // JER: Special treatment for issue status field that makes all |
| // options show up more often |
| if (textField.id.startsWith('status')) { |
| caret = textField.selectionStart; |
| } |
| // ie |
| } else if (document.selection) { |
| // get an empty selection range |
| const range = document.selection.createRange(); |
| const origSelectionLength = range.text.length; |
| // Force selection start to 0 position |
| range.moveStart('character', -caret); |
| // the caret end position is the new selection length |
| caret = range.text.length; |
| |
| // JER: Special treatment for issue status field that makes all |
| // options show up more often |
| if (textField.id.startsWith('status')) { |
| // The amount that the selection grew when we forced start to |
| // position 0 is == the original start position. |
| caret = range.text.length - origSelectionLength; |
| } |
| } |
| |
| return caret; |
| } else { |
| // a textarea |
| |
| return GetCursorPos(window, textField); |
| } |
| } |
| |
| function getTargetFromEvent(event) { |
| let targ = event.target || event.srcElement; |
| if (targ.shadowRoot) { |
| // Find the element within the shadowDOM. |
| const path = event.path || event.composedPath(); |
| targ = path[0]; |
| } |
| return targ; |
| } |