Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(302)

Side by Side Diff: appengine/monorail/static/js/tracker/ac.js

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

Powered by Google App Engine
This is Rietveld 408576698