| Index: appengine/monorail/static/js/tracker/ac.js
|
| diff --git a/appengine/monorail/static/js/tracker/ac.js b/appengine/monorail/static/js/tracker/ac.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..503efe45c9f3eebfda5c55a06d7be887d6dc3d9b
|
| --- /dev/null
|
| +++ b/appengine/monorail/static/js/tracker/ac.js
|
| @@ -0,0 +1,953 @@
|
| +/* 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, int keycode,
|
| + * 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
|
| + * keycode 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 (var 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) {
|
| + var 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 () {
|
| + var ar = a.apply(this, arguments);
|
| + var 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;
|
| +
|
| + var source = event.target || event.srcElement;
|
| + if (('INPUT' == source.tagName && source.type.match(/^text|email$/i))
|
| + || 'TEXTAREA' == source.tagName) {
|
| + var code = GetKeyCode(event);
|
| + var isDown = event.type == 'keydown';
|
| + var isShiftKey = event.shiftKey;
|
| + var storeFound = true;
|
| +
|
| + if ((source !== ac_focusedInput) || (ac_store === null)) {
|
| + ac_focusedInput = source;
|
| + storeFound = false;
|
| + if (ENTER_KEYCODE !== code && ESC_KEYCODE !== code) {
|
| + for (var i = 0; i < ac_storeConstructors.length; ++i) {
|
| + var store = (ac_storeConstructors[i])(source, event);
|
| + if (store) {
|
| + ac_store = store;
|
| + 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);
|
| + }
|
| + }
|
| + }
|
| + if (storeFound) {
|
| + var isCompletion = ac_store.isCompletionKey(code, isDown, isShiftKey);
|
| + var hasResults = ac_completions && (ac_completions.length > 0);
|
| + var 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_(code, 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 = (code == ESC_KEYCODE ||
|
| + code == DOWN_KEYCODE ||
|
| + code == UP_KEYCODE);
|
| +
|
| + window.setTimeout(function () {
|
| + if (ac_store) { ac_handleKey_(code, isDown, isShiftKey); }
|
| + }, 0);
|
| +
|
| + } else { // implicit if (isCompletion && !hasResults)
|
| + if (ac_store.oncomplete) {
|
| + ac_store.oncomplete(false, code, ac_focusedInput, undefined);
|
| + }
|
| + }
|
| +
|
| + if (cancelEvent) {
|
| + ac_cancelEvent_(event);
|
| + }
|
| +
|
| + return !cancelEvent;
|
| + }
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +/** on blur handler. Since webkit gives spurious onblur events,
|
| + * so ignore all onblur and use a document-wide onclick instead. */
|
| +function _ac_ob(event) {
|
| + // WebKit browsers: we use a document-wide on-click rather than blur events.
|
| + if (!BR_hasExcessBlurEvents()) {
|
| + _ac_real_onblur(event);
|
| + }
|
| +}
|
| +
|
| +/** Original autocomplete onblur handler. */
|
| +function _ac_real_onblur(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);
|
| +}
|
| +
|
| +/** This is a document-wide onclick handler that is only registered
|
| + * when using webkit browsers. It calls the real onblur handler
|
| + * when the user clicks away from a text field (or in many other
|
| + * situations where the user clicks, but that is OK). */
|
| +function _ac_fake_onblur(e) {
|
| + var targ;
|
| + if (!e) var e = window.event;
|
| + if (e.target) targ = e.target;
|
| + else if (e.srcElement) targ = e.srcElement;
|
| + if (targ.nodeType == 3) { // Safari
|
| + targ = targ.parentNode;
|
| + }
|
| +
|
| + // If the user clicked anywhere other than one of the
|
| + // form elements that can have its own autocomplete,
|
| + // then close any open autocomplete menu.
|
| + if ('INPUT' != targ.nodeName) {
|
| + _ac_real_onblur(e);
|
| + }
|
| +}
|
| +
|
| +
|
| +/** @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, keycode, 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'));
|
| +};
|
| +/** 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 (code, isDown, isShiftKey) {
|
| + if (!isDown && (ENTER_KEYCODE === code
|
| + || (AC_COMMA_KEYCODE == code && this.commaCompletes))) {
|
| + return true;
|
| + }
|
| + if (TAB_KEYCODE === code && !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;
|
| +};
|
| +
|
| +function _AC_AddItemToFirstCharMap(firstCharMap, ch, s) {
|
| + var 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) {
|
| + this.firstCharMap_ = {};
|
| +
|
| + for (var i = 0; i < strings.length; ++i) {
|
| + var s = strings[i];
|
| + if (!s) { continue; }
|
| +
|
| + _AC_AddItemToFirstCharMap(
|
| + this.firstCharMap_, s.charAt(0).toLowerCase(), s);
|
| +
|
| + var parts = s.split(/\W+/);
|
| + for (var j = 0; j < parts.length; ++j) {
|
| + if (parts[j]) {
|
| + _AC_AddItemToFirstCharMap(
|
| + this.firstCharMap_, parts[j].charAt(0).toLowerCase(), s);
|
| + }
|
| + }
|
| + }
|
| +
|
| + // The maximimum number of results that we are willing to show
|
| + this.countThreshold = 2500;
|
| + this.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
|
| + var start = 0;
|
| + var state = 0;
|
| + for (var i = 0; i < caret; ++i) {
|
| + var 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);
|
| +};
|
| +
|
| +/**
|
| + * 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, toFilter) {
|
| + if (!prefix) {
|
| + return [];
|
| + }
|
| + if ((toFilter == null) || (toFilter.length == 0)) {
|
| + 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 "\-", "\{", "\$".
|
| + var regexForRegexCharacters = /([\^*+\-\$\\\{\}\(\)\[\]\#?\.])/g;
|
| + var 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".
|
| + var patternRegex = '^(.*[-=><:@.,])?(' + modifiedPrefix + ')(.*)';
|
| + var pattern = new RegExp(patternRegex, 'i' /* ignore case */);
|
| +
|
| + var completions = [];
|
| + if (toFilter) {
|
| + var toFilterLength = toFilter.length;
|
| + for (var i = 0; i < toFilterLength; ++i) {
|
| + var matches = toFilter[i].value.match(pattern);
|
| + if (matches) {
|
| + var compSpan = document.createElement('span');
|
| + compSpan.appendChild(document.createTextNode(matches[1] || ''));
|
| + var compBold = document.createElement('b');
|
| + compSpan.appendChild(compBold);
|
| + compBold.appendChild(document.createTextNode(matches[2]));
|
| + compSpan.appendChild(document.createTextNode(matches[3] || ''));
|
| +
|
| + var newCompletion = new _AC_Completion(
|
| + toFilter[i].value,
|
| + compSpan,
|
| + this.docstrings[toFilter[i].value]);
|
| +
|
| + completions.push(newCompletion);
|
| + if (completions.length > this.countThreshold) {
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + return completions;
|
| +};
|
| +
|
| +// 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
|
| + var aval = a.value.toLowerCase().replace(/^\W*/,'');
|
| + var 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, docstr) {
|
| + /** plain text. */
|
| + this.value = value;
|
| + if (typeof compSpan == 'string') compSpan = document.createTextNode(compSpan);
|
| + this.compSpan = compSpan;
|
| + this.docstr = docstr;
|
| +}
|
| +_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 compelte 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
|
| + */
|
| +var 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;
|
| +
|
| +/**
|
| + * handles all the key strokes, updating the completion list, tracking selected
|
| + * element, performing substitutions, etc.
|
| + * @private
|
| + */
|
| +function ac_handleKey_(code, isDown, isShiftKey) {
|
| + // check completions
|
| + ac_checkCompletions();
|
| +
|
| + var show = true;
|
| + var numCompletions = ac_completions ? ac_completions.length : 0;
|
| + // handle enter and tab on key press and the rest on key down
|
| + if (ac_store.isCompletionKey(code, isDown, isShiftKey)) {
|
| + if (ac_selected < 0 && numCompletions >= 1 &&
|
| + ac_store.autoselectFirstRow()) {
|
| + ac_selected = 0;
|
| + }
|
| + if (ac_selected >= 0) {
|
| + var backupInput = ac_focusedInput;
|
| + var completeValue = ac_completions[ac_selected].value;
|
| + ac_complete();
|
| + if (ac_store.oncomplete) {
|
| + ac_store.oncomplete(true, code, backupInput, completeValue);
|
| + }
|
| + }
|
| + } else {
|
| + switch (code) {
|
| + case ESC_KEYCODE: // escape
|
| + //JER?? ac_suppressCompletions = true;
|
| + ac_selected = -1;
|
| + show = false;
|
| + break;
|
| + case UP_KEYCODE: // 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_KEYCODE: // down
|
| + if (isDown) {
|
| + ac_selected = Math.min(numCompletions - 1, ac_selected + 1);
|
| + }
|
| + break;
|
| + }
|
| +
|
| + if (isDown) {
|
| + switch (code) {
|
| + case ESC_KEYCODE:
|
| + case ENTER_KEYCODE:
|
| + case UP_KEYCODE:
|
| + case DOWN_KEYCODE:
|
| + case RIGHT_KEYCODE:
|
| + case LEFT_KEYCODE:
|
| + case TAB_KEYCODE:
|
| + case SHIFT_KEYCODE:
|
| + case BACKSPACE_KEYCODE:
|
| + case DELETE_KEYCODE:
|
| + 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() {
|
| + var caret = ac_getCaretPosition_(ac_focusedInput);
|
| + var 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').
|
| + var 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_suppressCompletions) {
|
| + var caret = ac_getCaretPosition_(ac_focusedInput);
|
| + var completable = ac_store.completable(ac_focusedInput.value, caret);
|
| +
|
| + // If we already have completed, then our work here is done.
|
| + if (completable == ac_lastCompletable) { return; }
|
| + var tofilter;
|
| + if (ac_lastCompletable &&
|
| + ac_lastCompletable.length < completable.length &&
|
| + completable.substring(0, ac_lastCompletable.length) ==
|
| + ac_lastCompletable) {
|
| + tofilter = ac_completions;
|
| + } else {
|
| + ac_completions = null;
|
| + ac_selected = -1;
|
| + }
|
| +
|
| + var oldSelected =
|
| + (ac_selected >= 0) ? ac_completions[ac_selected].value : null;
|
| + ac_completions = ac_store.completions(completable, tofilter);
|
| + ac_selected = -1;
|
| + for (var i = 0; i < ac_completions.length; ++i) {
|
| + if (oldSelected == ac_completions[i].value) {
|
| + ac_selected = i;
|
| + break;
|
| + }
|
| + }
|
| + ac_lastCompletable = completable;
|
| + return;
|
| + }
|
| + ac_lastCompletable = null;
|
| + ac_completions = null;
|
| + ac_selected = -1;
|
| +}
|
| +
|
| +/**
|
| + * maintains the the completion list GUI.
|
| + * @private
|
| + */
|
| +function ac_updateCompletionList(show) {
|
| + var clist = document.getElementById('ac-list');
|
| + 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';
|
| + document.body.appendChild(clist);
|
| + }
|
| +
|
| + // If no choice is selected, then select the first item, if desired.
|
| + if (ac_selected < 0 && ac_store && ac_store.autoselectFirstRow()) {
|
| + ac_selected = 0;
|
| + }
|
| +
|
| + var headerCount= 0;
|
| + var tableEl = document.createElement('table');
|
| + tableEl.setAttribute('cellpadding', 0);
|
| + tableEl.setAttribute('cellspacing', 0);
|
| + for (var i = 0; i < ac_completions.length; ++i) {
|
| + if (ac_completions[i].heading) {
|
| + var rowEl = document.createElement('tr');
|
| + tableEl.appendChild(rowEl);
|
| + var 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');
|
| + tableEl.appendChild(rowEl);
|
| + if (i == ac_selected) {
|
| + rowEl.id = "ac-selected-row";
|
| + rowEl.className = "selected";
|
| + }
|
| + rowEl.setAttribute("data-index", i);
|
| + rowEl.addEventListener("mouseup", function(event) {
|
| + var target = event.target;
|
| + while (target && target.tagName != "TR")
|
| + target = target.parentNode;
|
| + var idx = Number(target.getAttribute("data-index"));
|
| + try {
|
| + _ac_select(idx);
|
| + } finally {
|
| + return false;
|
| + }
|
| + });
|
| + rowEl.addEventListener('mouseover', function(event) {
|
| + var target = event.target;
|
| + while (target && target.tagName != "TR")
|
| + target = target.parentNode;
|
| + var idx = Number(target.getAttribute("data-index"));
|
| + _ac_mouseover(idx);
|
| + });
|
| + var valCellEl = document.createElement('td');
|
| + rowEl.appendChild(valCellEl);
|
| + if (ac_completions[i].compSpan) {
|
| + valCellEl.appendChild(ac_completions[i].compSpan);
|
| + }
|
| + var docCellEl = document.createElement('td');
|
| + rowEl.appendChild(docCellEl);
|
| + if (ac_completions[i].docstr)
|
| + docCellEl.appendChild(document.createTextNode(' = ' + ac_completions[i].docstr));
|
| + }
|
| + }
|
| +
|
| + while (clist.childNodes.length) {
|
| + clist.removeChild(clist.childNodes[0]);
|
| + }
|
| + clist.appendChild(tableEl);
|
| + // position
|
| + var inputBounds = nodeBounds(ac_focusedInput);
|
| + clist.style.left = inputBounds.x + 'px';
|
| + clist.style.top = (inputBounds.y + inputBounds.h) + 'px';
|
| +
|
| + // Note - we use '' instead of 'block', since 'block' has odd effects on
|
| + // the screen in IE, and causes scrollbars to resize
|
| + clist.style.display = '';
|
| +
|
| + window.setTimeout(ac_autoscroll, 100);
|
| +
|
| + } else {
|
| + if (clist) {
|
| + clist.style.display = 'none';
|
| + while (clist.childNodes.length) {
|
| + clist.removeChild(clist.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() {
|
| + var acList = document.getElementById('ac-list');
|
| + var acSelRow = document.getElementById('ac-selected-row');
|
| + var acSelRowTop = acSelRow ? acSelRow.offsetTop : 0;
|
| + var acSelRowHeight = acSelRow ? acSelRow.offsetHeight : 0;
|
| +
|
| +
|
| + var 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) {
|
| + var 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
|
| + var range = document.selection.createRange();
|
| + var 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);
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * on key press, the keycode for comma comes out as 44.
|
| + * on keydown it comes out as 188.
|
| + */
|
| +var AC_COMMA_KEYCODE = ','.charCodeAt(0);
|
| +
|
| +function BR_hasExcessBlurEvents() {
|
| + return navigator.userAgent.toLowerCase().indexOf('webkit') != -1;
|
| +}
|
| +
|
| +function BR_hasUnreliableMouseDown() {
|
| + return navigator.userAgent.toLowerCase().indexOf('webkit') != -1;
|
| +}
|
|
|