2 * Copyright (C) 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2011 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 * its contributors may be used to endorse or promote products derived
16 * from this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 * @extends WebInspector.Object
33 * @param {function(Element, Range, boolean, function(Array.<string>, number=))} completions
34 * @param {string=} stopCharacters
36 WebInspector.TextPrompt = function(completions, stopCharacters)
39 * @type {Element|undefined}
42 this._proxyElementDisplay = "inline-block";
43 this._loadCompletions = completions;
44 this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
45 this._suggestForceable = true;
48 WebInspector.TextPrompt.Events = {
49 ItemApplied: "text-prompt-item-applied",
50 ItemAccepted: "text-prompt-item-accepted"
53 WebInspector.TextPrompt.prototype = {
56 return this._proxyElement;
59 setSuggestForceable: function(x)
61 this._suggestForceable = x;
64 setSuggestBoxEnabled: function(className)
66 this._suggestBoxClassName = className;
69 renderAsBlock: function()
71 this._proxyElementDisplay = "block";
75 * Clients should never attach any event listeners to the |element|. Instead,
76 * they should use the result of this method to attach listeners for bubbling events.
78 * @param {Element} element
80 attach: function(element)
82 return this._attachInternal(element);
86 * Clients should never attach any event listeners to the |element|. Instead,
87 * they should use the result of this method to attach listeners for bubbling events
88 * or the |blurListener| parameter to register a "blur" event listener on the |element|
89 * (since the "blur" event does not bubble.)
91 * @param {Element} element
92 * @param {function(Event)} blurListener
94 attachAndStartEditing: function(element, blurListener)
96 this._attachInternal(element);
97 this._startEditing(blurListener);
98 return this.proxyElement;
101 _attachInternal: function(element)
103 if (this.proxyElement)
104 throw "Cannot attach an attached TextPrompt";
105 this._element = element;
107 this._boundOnKeyDown = this.onKeyDown.bind(this);
108 this._boundOnMouseWheel = this.onMouseWheel.bind(this);
109 this._boundSelectStart = this._selectStart.bind(this);
110 this._proxyElement = element.ownerDocument.createElement("span");
111 this._proxyElement.style.display = this._proxyElementDisplay;
112 element.parentElement.insertBefore(this.proxyElement, element);
113 this.proxyElement.appendChild(element);
114 this._element.addStyleClass("text-prompt");
115 this._element.addEventListener("keydown", this._boundOnKeyDown, false);
116 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
117 this._element.addEventListener("selectstart", this._boundSelectStart, false);
119 if (typeof this._suggestBoxClassName === "string")
120 this._suggestBox = new WebInspector.TextPrompt.SuggestBox(this, this._element, this._suggestBoxClassName);
122 return this.proxyElement;
127 this._removeFromElement();
128 this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement);
129 this.proxyElement.parentElement.removeChild(this.proxyElement);
130 this._element.removeStyleClass("text-prompt");
131 this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
132 this._element.removeEventListener("mousewheel", this._boundOnMouseWheel, false);
133 this._element.removeEventListener("selectstart", this._boundSelectStart, false);
134 delete this._proxyElement;
135 WebInspector.restoreFocusFromElement(this._element);
140 return this._element.textContent;
145 this._removeSuggestionAids();
147 // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
148 this._element.removeChildren();
149 this._element.appendChild(document.createElement("br"));
151 this._element.textContent = x;
153 this.moveCaretToEndOfPrompt();
154 this._element.scrollIntoView();
157 _removeFromElement: function()
159 this.clearAutoComplete(true);
160 this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
161 this._element.removeEventListener("selectstart", this._boundSelectStart, false);
164 if (this._suggestBox)
165 this._suggestBox.removeFromElement();
168 _startEditing: function(blurListener)
170 this._isEditing = true;
171 this._element.addStyleClass("editing");
173 this._blurListener = blurListener;
174 this._element.addEventListener("blur", this._blurListener, false);
176 this._oldTabIndex = this._element.tabIndex;
177 if (this._element.tabIndex < 0)
178 this._element.tabIndex = 0;
179 WebInspector.setCurrentFocusElement(this._element);
182 _stopEditing: function()
184 this._element.tabIndex = this._oldTabIndex;
185 if (this._blurListener)
186 this._element.removeEventListener("blur", this._blurListener, false);
187 this._element.removeStyleClass("editing");
188 delete this._isEditing;
191 _removeSuggestionAids: function()
193 this.clearAutoComplete();
194 this.hideSuggestBox();
197 _selectStart: function(event)
199 if (this._selectionTimeout)
200 clearTimeout(this._selectionTimeout);
202 this._removeSuggestionAids();
204 function moveBackIfOutside()
206 delete this._selectionTimeout;
207 if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
208 this.moveCaretToEndOfPrompt();
209 this.autoCompleteSoon();
213 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
217 * @param {boolean=} force
219 defaultKeyHandler: function(event, force)
221 this.clearAutoComplete();
222 this.autoCompleteSoon(force);
226 onMouseWheel: function(event)
228 // Subclasses can implement.
231 onKeyDown: function(event)
234 var invokeDefault = true;
236 switch (event.keyIdentifier) {
238 handled = this.upKeyPressed(event);
241 handled = this.downKeyPressed(event);
244 handled = this.pageUpKeyPressed(event);
247 handled = this.pageDownKeyPressed(event);
249 case "U+0009": // Tab
250 handled = this.tabKeyPressed(event);
253 handled = this.enterKeyPressed(event);
257 this._removeSuggestionAids();
258 invokeDefault = false;
262 if (this.isCaretAtEndOfPrompt())
263 handled = this.acceptAutoComplete();
265 this._removeSuggestionAids();
266 invokeDefault = false;
268 case "U+001B": // Esc
269 if (this.isSuggestBoxVisible()) {
270 this._suggestBox.hide();
274 case "U+0020": // Space
275 if (this._suggestForceable && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
276 this.defaultKeyHandler(event, true);
284 invokeDefault = false;
288 if (!handled && invokeDefault)
289 handled = this.defaultKeyHandler(event);
297 acceptAutoComplete: function()
300 if (this.isSuggestBoxVisible())
301 result = this._suggestBox.acceptSuggestion();
303 result = this.acceptSuggestion();
309 * @param {boolean=} includeTimeout
311 clearAutoComplete: function(includeTimeout)
313 if (includeTimeout && this._completeTimeout) {
314 clearTimeout(this._completeTimeout);
315 delete this._completeTimeout;
317 delete this._waitingForCompletions;
319 if (!this.autoCompleteElement)
322 if (this.autoCompleteElement.parentNode)
323 this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
324 delete this.autoCompleteElement;
326 if (!this._userEnteredRange || !this._userEnteredText)
329 this._userEnteredRange.deleteContents();
330 this._element.normalize();
332 var userTextNode = document.createTextNode(this._userEnteredText);
333 this._userEnteredRange.insertNode(userTextNode);
335 var selectionRange = document.createRange();
336 selectionRange.setStart(userTextNode, this._userEnteredText.length);
337 selectionRange.setEnd(userTextNode, this._userEnteredText.length);
339 var selection = window.getSelection();
340 selection.removeAllRanges();
341 selection.addRange(selectionRange);
343 delete this._userEnteredRange;
344 delete this._userEnteredText;
348 * @param {boolean=} force
350 autoCompleteSoon: function(force)
352 var immediately = this.isSuggestBoxVisible() || force;
353 if (!this._completeTimeout)
354 this._completeTimeout = setTimeout(this.complete.bind(this, true, force), immediately ? 0 : 250);
358 * @param {boolean=} reverse
360 complete: function(auto, force, reverse)
362 this.clearAutoComplete(true);
363 var selection = window.getSelection();
364 if (!selection.rangeCount)
367 var selectionRange = selection.getRangeAt(0);
368 var isEmptyInput = selectionRange.commonAncestorContainer === this._element; // this._element has no child Text nodes.
372 // Do not attempt to auto-complete an empty input in the auto mode (only on demand).
373 if (auto && isEmptyInput && !force)
375 else if (!auto && !isEmptyInput && !selectionRange.commonAncestorContainer.isDescendant(this._element))
377 else if (auto && !force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
379 else if (!selection.isCollapsed)
382 // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
383 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
384 if (wordSuffixRange.toString().length)
388 this.hideSuggestBox();
392 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
393 this._waitingForCompletions = true;
394 this._loadCompletions(this.proxyElement, wordPrefixRange, force, this._completionsReady.bind(this, selection, auto, wordPrefixRange, !!reverse));
397 _boxForAnchorAtStart: function(selection, textRange)
399 var rangeCopy = selection.getRangeAt(0).cloneRange();
400 var anchorElement = document.createElement("span");
401 anchorElement.textContent = "\u200B";
402 textRange.insertNode(anchorElement);
403 var box = anchorElement.boxInWindow(window);
404 anchorElement.parentElement.removeChild(anchorElement);
405 selection.removeAllRanges();
406 selection.addRange(rangeCopy);
411 * @param {Array.<string>} completions
412 * @param {number} wordPrefixLength
414 _buildCommonPrefix: function(completions, wordPrefixLength)
416 var commonPrefix = completions[0];
417 for (var i = 0; i < completions.length; ++i) {
418 var completion = completions[i];
419 var lastIndex = Math.min(commonPrefix.length, completion.length);
420 for (var j = wordPrefixLength; j < lastIndex; ++j) {
421 if (commonPrefix[j] !== completion[j]) {
422 commonPrefix = commonPrefix.substr(0, j);
431 * @param {Selection} selection
432 * @param {boolean} auto
433 * @param {Range} originalWordPrefixRange
434 * @param {boolean} reverse
435 * @param {Array.<string>=} completions
436 * @param {number=} selectedIndex
438 _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions, selectedIndex)
440 if (!this._waitingForCompletions || !completions || !completions.length) {
441 this.hideSuggestBox();
444 delete this._waitingForCompletions;
446 var selectionRange = selection.getRangeAt(0);
448 var fullWordRange = document.createRange();
449 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
450 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
452 if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
455 selectedIndex = selectedIndex || 0;
457 this._userEnteredRange = fullWordRange;
458 this._userEnteredText = fullWordRange.toString();
460 if (this._suggestBox)
461 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt());
463 var wordPrefixLength = originalWordPrefixRange.toString().length;
466 var completionText = completions[selectedIndex];
467 var commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
469 this._commonPrefix = commonPrefix;
471 if (completions.length === 1) {
472 var completionText = completions[selectedIndex];
473 wordPrefixLength = completionText.length;
475 var commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
476 wordPrefixLength = commonPrefix.length;
478 if (selection.isCollapsed)
479 var completionText = completions[selectedIndex];
481 var currentText = fullWordRange.toString();
483 var foundIndex = null;
484 for (var i = 0; i < completions.length; ++i) {
485 if (completions[i] === currentText)
489 var nextIndex = foundIndex + (reverse ? -1 : 1);
490 if (foundIndex === null || nextIndex >= completions.length)
491 var completionText = completions[selectedIndex];
492 else if (nextIndex < 0)
493 var completionText = completions[completions.length - 1];
495 var completionText = completions[nextIndex];
501 if (this.isCaretAtEndOfPrompt()) {
502 this._userEnteredRange.deleteContents();
503 this._element.normalize();
504 var finalSelectionRange = document.createRange();
505 var prefixText = completionText.substring(0, wordPrefixLength);
506 var suffixText = completionText.substring(wordPrefixLength);
508 var prefixTextNode = document.createTextNode(prefixText);
509 fullWordRange.insertNode(prefixTextNode);
511 this.autoCompleteElement = document.createElement("span");
512 this.autoCompleteElement.className = "auto-complete-text";
513 this.autoCompleteElement.textContent = suffixText;
515 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
517 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
518 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
519 selection.removeAllRanges();
520 selection.addRange(finalSelectionRange);
523 this.applySuggestion(completionText, completions.length > 1, originalWordPrefixRange);
526 _completeCommonPrefix: function()
528 if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
531 if (!this.isSuggestBoxVisible()) {
532 this.acceptAutoComplete();
536 this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
537 this.acceptSuggestion(true)
541 * @param {Range=} originalPrefixRange
543 applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
545 var wordPrefixLength;
546 if (originalPrefixRange)
547 wordPrefixLength = originalPrefixRange.toString().length;
549 wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
551 this._userEnteredRange.deleteContents();
552 this._element.normalize();
553 var finalSelectionRange = document.createRange();
554 var completionTextNode = document.createTextNode(completionText);
555 this._userEnteredRange.insertNode(completionTextNode);
556 if (this.autoCompleteElement && this.autoCompleteElement.parentNode) {
557 this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
558 delete this.autoCompleteElement;
561 if (isIntermediateSuggestion)
562 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
564 finalSelectionRange.setStart(completionTextNode, completionText.length);
566 finalSelectionRange.setEnd(completionTextNode, completionText.length);
568 var selection = window.getSelection();
569 selection.removeAllRanges();
570 selection.addRange(finalSelectionRange);
571 if (isIntermediateSuggestion)
572 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
576 * @param {boolean=} prefixAccepted
578 acceptSuggestion: function(prefixAccepted)
580 if (this._isAcceptingSuggestion)
583 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
586 var text = this.autoCompleteElement.textContent;
587 var textNode = document.createTextNode(text);
588 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
589 delete this.autoCompleteElement;
591 var finalSelectionRange = document.createRange();
592 finalSelectionRange.setStart(textNode, text.length);
593 finalSelectionRange.setEnd(textNode, text.length);
595 var selection = window.getSelection();
596 selection.removeAllRanges();
597 selection.addRange(finalSelectionRange);
599 if (!prefixAccepted) {
600 this.hideSuggestBox();
601 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
603 this.autoCompleteSoon(true);
608 hideSuggestBox: function()
610 if (this.isSuggestBoxVisible())
611 this._suggestBox.hide();
614 isSuggestBoxVisible: function()
616 return this._suggestBox && this._suggestBox.visible;
619 isCaretInsidePrompt: function()
621 return this._element.isInsertionCaretInside();
624 isCaretAtEndOfPrompt: function()
626 var selection = window.getSelection();
627 if (!selection.rangeCount || !selection.isCollapsed)
630 var selectionRange = selection.getRangeAt(0);
631 var node = selectionRange.startContainer;
632 if (!node.isSelfOrDescendant(this._element))
635 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
638 var foundNextText = false;
640 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
641 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
643 foundNextText = true;
646 node = node.traverseNextNode(this._element);
652 isCaretOnFirstLine: function()
654 var selection = window.getSelection();
655 var focusNode = selection.focusNode;
656 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
659 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
661 focusNode = focusNode.previousSibling;
664 if (focusNode.nodeType !== Node.TEXT_NODE)
666 if (focusNode.textContent.indexOf("\n") !== -1)
668 focusNode = focusNode.previousSibling;
674 isCaretOnLastLine: function()
676 var selection = window.getSelection();
677 var focusNode = selection.focusNode;
678 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
681 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
683 focusNode = focusNode.nextSibling;
686 if (focusNode.nodeType !== Node.TEXT_NODE)
688 if (focusNode.textContent.indexOf("\n") !== -1)
690 focusNode = focusNode.nextSibling;
696 moveCaretToEndOfPrompt: function()
698 var selection = window.getSelection();
699 var selectionRange = document.createRange();
701 var offset = this._element.childNodes.length;
702 selectionRange.setStart(this._element, offset);
703 selectionRange.setEnd(this._element, offset);
705 selection.removeAllRanges();
706 selection.addRange(selectionRange);
709 tabKeyPressed: function(event)
711 this._completeCommonPrefix();
717 enterKeyPressed: function(event)
719 if (this.isSuggestBoxVisible())
720 return this._suggestBox.enterKeyPressed(event);
725 upKeyPressed: function(event)
727 if (this.isSuggestBoxVisible())
728 return this._suggestBox.upKeyPressed(event);
733 downKeyPressed: function(event)
735 if (this.isSuggestBoxVisible())
736 return this._suggestBox.downKeyPressed(event);
741 pageUpKeyPressed: function(event)
743 if (this.isSuggestBoxVisible())
744 return this._suggestBox.pageUpKeyPressed(event);
749 pageDownKeyPressed: function(event)
751 if (this.isSuggestBoxVisible())
752 return this._suggestBox.pageDownKeyPressed(event);
757 __proto__: WebInspector.Object.prototype
763 * @extends {WebInspector.TextPrompt}
764 * @param {function(Element, Range, boolean, function(Array.<string>,number=))} completions
765 * @param {string=} stopCharacters
767 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
769 WebInspector.TextPrompt.call(this, completions, stopCharacters);
772 * @type {Array.<string>}
777 * 1-based entry in the history stack.
780 this._historyOffset = 1;
783 * Whether to coalesce duplicate items in the history, default is true.
786 this._coalesceHistoryDupes = true;
789 WebInspector.TextPromptWithHistory.prototype = {
792 // FIXME: do we need to copy this?
796 setCoalesceHistoryDupes: function(x)
798 this._coalesceHistoryDupes = x;
802 * @param {Array.<string>} data
804 setHistoryData: function(data)
806 this._data = [].concat(data);
807 this._historyOffset = 1;
811 * Pushes a committed text into the history.
812 * @param {string} text
814 pushHistoryItem: function(text)
816 if (this._uncommittedIsTop) {
818 delete this._uncommittedIsTop;
821 this._historyOffset = 1;
822 if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
824 this._data.push(text);
828 * Pushes the current (uncommitted) text into the history.
830 _pushCurrentText: function()
832 if (this._uncommittedIsTop)
833 this._data.pop(); // Throw away obsolete uncommitted text.
834 this._uncommittedIsTop = true;
835 this.clearAutoComplete(true);
836 this._data.push(this.text);
840 * @return {string|undefined}
842 _previous: function()
844 if (this._historyOffset > this._data.length)
846 if (this._historyOffset === 1)
847 this._pushCurrentText();
848 ++this._historyOffset;
849 return this._currentHistoryItem();
853 * @return {string|undefined}
857 if (this._historyOffset === 1)
859 --this._historyOffset;
860 return this._currentHistoryItem();
863 _currentHistoryItem: function()
865 return this._data[this._data.length - this._historyOffset];
871 defaultKeyHandler: function(event, force)
876 switch (event.keyIdentifier) {
878 if (!this.isCaretOnFirstLine())
880 newText = this._previous();
884 if (!this.isCaretOnLastLine())
886 newText = this._next();
888 case "U+0050": // Ctrl+P = Previous
889 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
890 newText = this._previous();
894 case "U+004E": // Ctrl+N = Next
895 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
896 newText = this._next();
900 if (newText !== undefined) {
905 var firstNewlineIndex = this.text.indexOf("\n");
906 if (firstNewlineIndex === -1)
907 this.moveCaretToEndOfPrompt();
909 var selection = window.getSelection();
910 var selectionRange = document.createRange();
912 selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
913 selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
915 selection.removeAllRanges();
916 selection.addRange(selectionRange);
923 return WebInspector.TextPrompt.prototype.defaultKeyHandler.apply(this, arguments);
926 __proto__: WebInspector.TextPrompt.prototype
932 WebInspector.TextPrompt.SuggestBox = function(textPrompt, inputElement, className)
934 this._textPrompt = textPrompt;
935 this._inputElement = inputElement;
937 this._selectedIndex = -1;
938 this._selectedElement = null;
939 this._boundOnScroll = this._onscrollresize.bind(this, true);
940 this._boundOnResize = this._onscrollresize.bind(this, false);
941 window.addEventListener("scroll", this._boundOnScroll, true);
942 window.addEventListener("resize", this._boundOnResize, true);
944 this._bodyElement = inputElement.ownerDocument.body;
945 this._element = inputElement.ownerDocument.createElement("div");
946 this._element.className = "suggest-box " + (className || "");
947 this._element.addEventListener("mousedown", this._onboxmousedown.bind(this), true);
948 this.containerElement = this._element.createChild("div", "container");
949 this.contentElement = this.containerElement.createChild("div", "content");
952 WebInspector.TextPrompt.SuggestBox.prototype = {
955 return !!this._element.parentElement;
960 return !!this._selectedElement;
963 _onscrollresize: function(isScroll, event)
965 if (isScroll && this._element.isAncestor(event.target) || !this.visible)
967 this._updateBoxPositionWithExistingAnchor();
970 _updateBoxPositionWithExistingAnchor: function()
972 this._updateBoxPosition(this._anchorBox);
976 * @param {AnchorBox} anchorBox
978 _updateBoxPosition: function(anchorBox)
980 // Measure the content element box.
981 this.contentElement.style.display = "inline-block";
982 document.body.appendChild(this.contentElement);
983 this.contentElement.positionAt(0, 0);
984 var contentWidth = this.contentElement.offsetWidth;
985 var contentHeight = this.contentElement.offsetHeight;
986 this.contentElement.style.display = "block";
987 this.containerElement.appendChild(this.contentElement);
989 // Lay out the suggest-box relative to the anchorBox.
990 this._anchorBox = anchorBox;
993 const suggestBoxPaddingX = 21;
994 var maxWidth = document.body.offsetWidth - anchorBox.x - spacer;
995 var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
996 var paddedWidth = contentWidth + suggestBoxPaddingX;
997 var boxX = anchorBox.x;
998 if (width < paddedWidth) {
999 // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge.
1000 maxWidth = document.body.offsetWidth - spacer;
1001 width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
1002 boxX = document.body.offsetWidth - width;
1005 const suggestBoxPaddingY = 2;
1007 var aboveHeight = anchorBox.y;
1008 var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height;
1009 var maxHeight = Math.max(underHeight, aboveHeight) - spacer;
1010 var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
1011 if (underHeight >= aboveHeight) {
1012 // Locate the suggest box under the anchorBox.
1013 boxY = anchorBox.y + anchorBox.height;
1014 this._element.removeStyleClass("above-anchor");
1015 this._element.addStyleClass("under-anchor");
1017 // Locate the suggest box above the anchorBox.
1018 boxY = anchorBox.y - height;
1019 this._element.removeStyleClass("under-anchor");
1020 this._element.addStyleClass("above-anchor");
1023 this._element.positionAt(boxX, boxY);
1024 this._element.style.width = width + "px";
1025 this._element.style.height = height + "px";
1028 _onboxmousedown: function(event)
1030 event.preventDefault();
1038 this._element.parentElement.removeChild(this._element);
1039 delete this._selectedElement;
1042 removeFromElement: function()
1044 window.removeEventListener("scroll", this._boundOnScroll, true);
1045 window.removeEventListener("resize", this._boundOnResize, true);
1050 * @param {string=} text
1051 * @param {boolean=} isIntermediateSuggestion
1053 _applySuggestion: function(text, isIntermediateSuggestion)
1055 if (!this.visible || !(text || this._selectedElement))
1058 var suggestion = text || this._selectedElement.textContent;
1062 this._textPrompt.applySuggestion(suggestion, isIntermediateSuggestion);
1067 * @param {string=} text
1069 acceptSuggestion: function(text)
1071 var result = this._applySuggestion(text, false);
1076 this._textPrompt.acceptSuggestion();
1082 * @param {number} shift
1083 * @param {boolean=} isCircular
1084 * @return {boolean} is changed
1086 _selectClosest: function(shift, isCircular)
1091 var index = this._selectedIndex + shift;
1094 index = (this._length + index) % this._length;
1096 index = Number.constrain(index, 0, this._length - 1);
1098 this._selectItem(index);
1099 this._applySuggestion(undefined, true);
1104 * @param {AnchorBox} anchorBox
1105 * @param {Array.<string>=} completions
1106 * @param {number=} selectedIndex
1107 * @param {boolean=} canShowForSingleItem
1109 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
1111 if (this._suggestTimeout) {
1112 clearTimeout(this._suggestTimeout);
1113 delete this._suggestTimeout;
1115 this._completionsReady(anchorBox, completions, selectedIndex, canShowForSingleItem);
1118 _onItemMouseDown: function(text, event)
1120 this.acceptSuggestion(text);
1121 event.consume(true);
1124 _createItemElement: function(prefix, text)
1126 var element = document.createElement("div");
1127 element.className = "suggest-box-content-item source-code";
1128 element.tabIndex = -1;
1129 if (prefix && prefix.length && !text.indexOf(prefix)) {
1130 var prefixElement = element.createChild("span", "prefix");
1131 prefixElement.textContent = prefix;
1132 var suffixElement = element.createChild("span", "suffix");
1133 suffixElement.textContent = text.substring(prefix.length);
1135 var suffixElement = element.createChild("span", "suffix");
1136 suffixElement.textContent = text;
1138 element.addEventListener("mousedown", this._onItemMouseDown.bind(this, text), false);
1143 * @param {Array.<string>=} items
1144 * @param {number=} selectedIndex
1146 _updateItems: function(items, selectedIndex)
1148 this._length = items.length;
1149 this.contentElement.removeChildren();
1151 var userEnteredText = this._textPrompt._userEnteredText;
1152 for (var i = 0; i < items.length; ++i) {
1153 var item = items[i];
1154 var currentItemElement = this._createItemElement(userEnteredText, item);
1155 this.contentElement.appendChild(currentItemElement);
1158 this._selectedElement = null;
1159 if (typeof selectedIndex === "number")
1160 this._selectItem(selectedIndex);
1164 * @param {number} index
1166 _selectItem: function(index)
1168 if (this._selectedElement)
1169 this._selectedElement.classList.remove("selected");
1171 this._selectedIndex = index;
1172 this._selectedElement = this.contentElement.children[index];
1173 this._selectedElement.classList.add("selected");
1175 this._selectedElement.scrollIntoViewIfNeeded(false);
1179 * @param {Array.<string>=} completions
1180 * @param {boolean=} canShowForSingleItem
1182 _canShowBox: function(completions, canShowForSingleItem)
1184 if (!completions || !completions.length)
1187 if (completions.length > 1)
1190 // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
1191 return canShowForSingleItem && completions[0] !== this._textPrompt._userEnteredText;
1194 _rememberRowCountPerViewport: function()
1196 if (!this.contentElement.firstChild)
1199 this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
1203 * @param {AnchorBox} anchorBox
1204 * @param {Array.<string>=} completions
1205 * @param {number=} selectedIndex
1206 * @param {boolean=} canShowForSingleItem
1208 _completionsReady: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
1210 if (this._canShowBox(completions, canShowForSingleItem)) {
1211 this._updateItems(completions, selectedIndex);
1212 this._updateBoxPosition(anchorBox);
1214 this._bodyElement.appendChild(this._element);
1215 this._rememberRowCountPerViewport();
1220 upKeyPressed: function(event)
1222 return this._selectClosest(-1, true);
1225 downKeyPressed: function(event)
1227 return this._selectClosest(1, true);
1230 pageUpKeyPressed: function(event)
1232 return this._selectClosest(-this._rowCountPerViewport, false);
1235 pageDownKeyPressed: function(event)
1237 return this._selectClosest(this._rowCountPerViewport, false);
1240 enterKeyPressed: function(event)
1242 var hasSelectedItem = !!this._selectedElement;
1243 this.acceptSuggestion();
1245 // Report the event as non-handled if there is no selected item,
1246 // to commit the input or handle it otherwise.
1247 return hasSelectedItem;
1250 tabKeyPressed: function(event)
1252 return this.enterKeyPressed(event);