Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2011 Google Inc.  All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions
      7  * are met:
      8  *
      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.
     17  *
     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.
     28  */
     29 
     30 /**
     31  * @constructor
     32  * @extends {WebInspector.Object}
     33  * @implements {WebInspector.SuggestBoxDelegate}
     34  * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
     35  * @param {string=} stopCharacters
     36  */
     37 WebInspector.TextPrompt = function(completions, stopCharacters)
     38 {
     39     /**
     40      * @type {!Element|undefined}
     41      */
     42     this._proxyElement;
     43     this._proxyElementDisplay = "inline-block";
     44     this._loadCompletions = completions;
     45     this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
     46 }
     47 
     48 WebInspector.TextPrompt.Events = {
     49     ItemApplied: "text-prompt-item-applied",
     50     ItemAccepted: "text-prompt-item-accepted"
     51 };
     52 
     53 WebInspector.TextPrompt.prototype = {
     54     get proxyElement()
     55     {
     56         return this._proxyElement;
     57     },
     58 
     59     /**
     60      * @param {boolean} suggestBoxEnabled
     61      */
     62     setSuggestBoxEnabled: function(suggestBoxEnabled)
     63     {
     64         this._suggestBoxEnabled = suggestBoxEnabled;
     65     },
     66 
     67     renderAsBlock: function()
     68     {
     69         this._proxyElementDisplay = "block";
     70     },
     71 
     72     /**
     73      * Clients should never attach any event listeners to the |element|. Instead,
     74      * they should use the result of this method to attach listeners for bubbling events.
     75      *
     76      * @param {!Element} element
     77      * @return {!Element}
     78      */
     79     attach: function(element)
     80     {
     81         return this._attachInternal(element);
     82     },
     83 
     84     /**
     85      * Clients should never attach any event listeners to the |element|. Instead,
     86      * they should use the result of this method to attach listeners for bubbling events
     87      * or the |blurListener| parameter to register a "blur" event listener on the |element|
     88      * (since the "blur" event does not bubble.)
     89      *
     90      * @param {!Element} element
     91      * @param {function(!Event)} blurListener
     92      * @return {!Element}
     93      */
     94     attachAndStartEditing: function(element, blurListener)
     95     {
     96         this._attachInternal(element);
     97         this._startEditing(blurListener);
     98         return this.proxyElement;
     99     },
    100 
    101     /**
    102      * @param {!Element} element
    103      * @return {!Element}
    104      */
    105     _attachInternal: function(element)
    106     {
    107         if (this.proxyElement)
    108             throw "Cannot attach an attached TextPrompt";
    109         this._element = element;
    110 
    111         this._boundOnKeyDown = this.onKeyDown.bind(this);
    112         this._boundOnInput = this.onInput.bind(this);
    113         this._boundOnMouseWheel = this.onMouseWheel.bind(this);
    114         this._boundSelectStart = this._selectStart.bind(this);
    115         this._boundRemoveSuggestionAids = this._removeSuggestionAids.bind(this);
    116         this._proxyElement = element.ownerDocument.createElement("span");
    117         this._proxyElement.style.display = this._proxyElementDisplay;
    118         element.parentElement.insertBefore(this.proxyElement, element);
    119         this.proxyElement.appendChild(element);
    120         this._element.classList.add("text-prompt");
    121         this._element.addEventListener("keydown", this._boundOnKeyDown, false);
    122         this._element.addEventListener("input", this._boundOnInput, false);
    123         this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
    124         this._element.addEventListener("selectstart", this._boundSelectStart, false);
    125         this._element.addEventListener("blur", this._boundRemoveSuggestionAids, false);
    126 
    127         if (this._suggestBoxEnabled)
    128             this._suggestBox = new WebInspector.SuggestBox(this);
    129 
    130         return this.proxyElement;
    131     },
    132 
    133     detach: function()
    134     {
    135         this._removeFromElement();
    136         this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement);
    137         this.proxyElement.remove();
    138         delete this._proxyElement;
    139         this._element.classList.remove("text-prompt");
    140         WebInspector.restoreFocusFromElement(this._element);
    141     },
    142 
    143     /**
    144      * @type {string}
    145      */
    146     get text()
    147     {
    148         return this._element.textContent;
    149     },
    150 
    151     /**
    152      * @param {string} x
    153      */
    154     set text(x)
    155     {
    156         this._removeSuggestionAids();
    157         if (!x) {
    158             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
    159             this._element.removeChildren();
    160             this._element.createChild("br");
    161         } else {
    162             this._element.textContent = x;
    163         }
    164 
    165         this.moveCaretToEndOfPrompt();
    166         this._element.scrollIntoView();
    167     },
    168 
    169     _removeFromElement: function()
    170     {
    171         this.clearAutoComplete(true);
    172         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
    173         this._element.removeEventListener("input", this._boundOnInput, false);
    174         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
    175         this._element.removeEventListener("blur", this._boundRemoveSuggestionAids, false);
    176         if (this._isEditing)
    177             this._stopEditing();
    178         if (this._suggestBox)
    179             this._suggestBox.removeFromElement();
    180     },
    181 
    182     /**
    183      * @param {function(!Event)=} blurListener
    184      */
    185     _startEditing: function(blurListener)
    186     {
    187         this._isEditing = true;
    188         this._element.classList.add("editing");
    189         if (blurListener) {
    190             this._blurListener = blurListener;
    191             this._element.addEventListener("blur", this._blurListener, false);
    192         }
    193         this._oldTabIndex = this._element.tabIndex;
    194         if (this._element.tabIndex < 0)
    195             this._element.tabIndex = 0;
    196         WebInspector.setCurrentFocusElement(this._element);
    197         if (!this.text)
    198             this._updateAutoComplete();
    199     },
    200 
    201     _stopEditing: function()
    202     {
    203         this._element.tabIndex = this._oldTabIndex;
    204         if (this._blurListener)
    205             this._element.removeEventListener("blur", this._blurListener, false);
    206         this._element.classList.remove("editing");
    207         delete this._isEditing;
    208     },
    209 
    210     _removeSuggestionAids: function()
    211     {
    212         this.clearAutoComplete();
    213         this.hideSuggestBox();
    214     },
    215 
    216     _selectStart: function()
    217     {
    218         if (this._selectionTimeout)
    219             clearTimeout(this._selectionTimeout);
    220 
    221         this._removeSuggestionAids();
    222 
    223         /**
    224          * @this {WebInspector.TextPrompt}
    225          */
    226         function moveBackIfOutside()
    227         {
    228             delete this._selectionTimeout;
    229             if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
    230                 this.moveCaretToEndOfPrompt();
    231                 this.autoCompleteSoon();
    232             }
    233         }
    234 
    235         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
    236     },
    237 
    238     /**
    239      * @param {boolean=} force
    240      */
    241     _updateAutoComplete: function(force)
    242     {
    243         this.clearAutoComplete();
    244         this.autoCompleteSoon(force);
    245     },
    246 
    247     /**
    248      * @param {!Event} event
    249      */
    250     onMouseWheel: function(event)
    251     {
    252         // Subclasses can implement.
    253     },
    254 
    255     /**
    256      * @param {!Event} event
    257      */
    258     onKeyDown: function(event)
    259     {
    260         var handled = false;
    261         delete this._needUpdateAutocomplete;
    262 
    263         switch (event.keyIdentifier) {
    264         case "U+0009": // Tab
    265             handled = this.tabKeyPressed(event);
    266             break;
    267         case "Left":
    268         case "Home":
    269             this._removeSuggestionAids();
    270             break;
    271         case "Right":
    272         case "End":
    273             if (this.isCaretAtEndOfPrompt())
    274                 handled = this.acceptAutoComplete();
    275             else
    276                 this._removeSuggestionAids();
    277             break;
    278         case "U+001B": // Esc
    279             if (this.isSuggestBoxVisible()) {
    280                 this._removeSuggestionAids();
    281                 handled = true;
    282             }
    283             break;
    284         case "U+0020": // Space
    285             if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    286                 this._updateAutoComplete(true);
    287                 handled = true;
    288             }
    289             break;
    290         case "Alt":
    291         case "Meta":
    292         case "Shift":
    293         case "Control":
    294             break;
    295         }
    296 
    297         if (!handled && this.isSuggestBoxVisible())
    298             handled = this._suggestBox.keyPressed(event);
    299 
    300         if (!handled)
    301             this._needUpdateAutocomplete = true;
    302 
    303         if (handled)
    304             event.consume(true);
    305     },
    306 
    307     /**
    308      * @param {!Event} event
    309      */
    310     onInput: function(event)
    311     {
    312         if (this._needUpdateAutocomplete)
    313             this._updateAutoComplete();
    314     },
    315 
    316     /**
    317      * @return {boolean}
    318      */
    319     acceptAutoComplete: function()
    320     {
    321         var result = false;
    322         if (this.isSuggestBoxVisible())
    323             result = this._suggestBox.acceptSuggestion();
    324         if (!result)
    325             result = this._acceptSuggestionInternal();
    326 
    327         return result;
    328     },
    329 
    330     /**
    331      * @param {boolean=} includeTimeout
    332      */
    333     clearAutoComplete: function(includeTimeout)
    334     {
    335         if (includeTimeout && this._completeTimeout) {
    336             clearTimeout(this._completeTimeout);
    337             delete this._completeTimeout;
    338         }
    339         delete this._waitingForCompletions;
    340 
    341         if (!this.autoCompleteElement)
    342             return;
    343 
    344         this.autoCompleteElement.remove();
    345         delete this.autoCompleteElement;
    346         delete this._userEnteredRange;
    347         delete this._userEnteredText;
    348     },
    349 
    350     /**
    351      * @param {boolean=} force
    352      */
    353     autoCompleteSoon: function(force)
    354     {
    355         var immediately = this.isSuggestBoxVisible() || force;
    356         if (!this._completeTimeout)
    357             this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
    358     },
    359 
    360     /**
    361      * @param {boolean=} force
    362      * @param {boolean=} reverse
    363      */
    364     complete: function(force, reverse)
    365     {
    366         this.clearAutoComplete(true);
    367         var selection = window.getSelection();
    368         if (!selection.rangeCount)
    369             return;
    370 
    371         var selectionRange = selection.getRangeAt(0);
    372         var shouldExit;
    373 
    374         if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
    375             shouldExit = true;
    376         else if (!selection.isCollapsed)
    377             shouldExit = true;
    378         else if (!force) {
    379             // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
    380             var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
    381             if (wordSuffixRange.toString().length)
    382                 shouldExit = true;
    383         }
    384         if (shouldExit) {
    385             this.hideSuggestBox();
    386             return;
    387         }
    388 
    389         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
    390         this._waitingForCompletions = true;
    391         this._loadCompletions(this.proxyElement, wordPrefixRange, force || false, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse));
    392     },
    393 
    394     disableDefaultSuggestionForEmptyInput: function()
    395     {
    396         this._disableDefaultSuggestionForEmptyInput = true;
    397     },
    398 
    399     /**
    400      * @param {!Selection} selection
    401      * @param {!Range} textRange
    402      */
    403     _boxForAnchorAtStart: function(selection, textRange)
    404     {
    405         var rangeCopy = selection.getRangeAt(0).cloneRange();
    406         var anchorElement = document.createElement("span");
    407         anchorElement.textContent = "\u200B";
    408         textRange.insertNode(anchorElement);
    409         var box = anchorElement.boxInWindow(window);
    410         anchorElement.remove();
    411         selection.removeAllRanges();
    412         selection.addRange(rangeCopy);
    413         return box;
    414     },
    415 
    416     /**
    417      * @param {!Array.<string>} completions
    418      * @param {number} wordPrefixLength
    419      */
    420     _buildCommonPrefix: function(completions, wordPrefixLength)
    421     {
    422         var commonPrefix = completions[0];
    423         for (var i = 0; i < completions.length; ++i) {
    424             var completion = completions[i];
    425             var lastIndex = Math.min(commonPrefix.length, completion.length);
    426             for (var j = wordPrefixLength; j < lastIndex; ++j) {
    427                 if (commonPrefix[j] !== completion[j]) {
    428                     commonPrefix = commonPrefix.substr(0, j);
    429                     break;
    430                 }
    431             }
    432         }
    433         return commonPrefix;
    434     },
    435 
    436     /**
    437      * @param {!Selection} selection
    438      * @param {!Range} originalWordPrefixRange
    439      * @param {boolean} reverse
    440      * @param {!Array.<string>} completions
    441      * @param {number=} selectedIndex
    442      */
    443     _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
    444     {
    445         if (!this._waitingForCompletions || !completions.length) {
    446             this.hideSuggestBox();
    447             return;
    448         }
    449         delete this._waitingForCompletions;
    450 
    451         var selectionRange = selection.getRangeAt(0);
    452 
    453         var fullWordRange = document.createRange();
    454         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
    455         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
    456 
    457         if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString())
    458             return;
    459 
    460         selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0);
    461 
    462         this._userEnteredRange = fullWordRange;
    463         this._userEnteredText = fullWordRange.toString();
    464 
    465         if (this._suggestBox)
    466             this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
    467 
    468         if (selectedIndex === -1)
    469             return;
    470 
    471         var wordPrefixLength = originalWordPrefixRange.toString().length;
    472         this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
    473 
    474         if (this.isCaretAtEndOfPrompt()) {
    475             var completionText = completions[selectedIndex];
    476             var prefixText = this._userEnteredRange.toString();
    477             var suffixText = completionText.substring(wordPrefixLength);
    478             this._userEnteredRange.deleteContents();
    479             this._element.normalize();
    480             var finalSelectionRange = document.createRange();
    481 
    482             var prefixTextNode = document.createTextNode(prefixText);
    483             fullWordRange.insertNode(prefixTextNode);
    484 
    485             this.autoCompleteElement = document.createElementWithClass("span", "auto-complete-text");
    486             this.autoCompleteElement.textContent = suffixText;
    487 
    488             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
    489 
    490             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
    491             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
    492             selection.removeAllRanges();
    493             selection.addRange(finalSelectionRange);
    494             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied);
    495         }
    496     },
    497 
    498     _completeCommonPrefix: function()
    499     {
    500         if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
    501             return;
    502 
    503         if (!this.isSuggestBoxVisible()) {
    504             this.acceptAutoComplete();
    505             return;
    506         }
    507 
    508         this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
    509         this._acceptSuggestionInternal(true);
    510     },
    511 
    512     /**
    513      * @param {string} completionText
    514      * @param {boolean=} isIntermediateSuggestion
    515      */
    516     applySuggestion: function(completionText, isIntermediateSuggestion)
    517     {
    518         this._applySuggestion(completionText, isIntermediateSuggestion);
    519     },
    520 
    521     /**
    522      * @param {string} completionText
    523      * @param {boolean=} isIntermediateSuggestion
    524      * @param {!Range=} originalPrefixRange
    525      */
    526     _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
    527     {
    528         var wordPrefixLength;
    529         if (originalPrefixRange)
    530             wordPrefixLength = originalPrefixRange.toString().length;
    531         else
    532             wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
    533 
    534         this._userEnteredRange.deleteContents();
    535         this._element.normalize();
    536         var finalSelectionRange = document.createRange();
    537         var completionTextNode = document.createTextNode(completionText);
    538         this._userEnteredRange.insertNode(completionTextNode);
    539         if (this.autoCompleteElement) {
    540             this.autoCompleteElement.remove();
    541             delete this.autoCompleteElement;
    542         }
    543 
    544         if (isIntermediateSuggestion)
    545             finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
    546         else
    547             finalSelectionRange.setStart(completionTextNode, completionText.length);
    548 
    549         finalSelectionRange.setEnd(completionTextNode, completionText.length);
    550 
    551         var selection = window.getSelection();
    552         selection.removeAllRanges();
    553         selection.addRange(finalSelectionRange);
    554         if (isIntermediateSuggestion)
    555             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
    556     },
    557 
    558     /**
    559      * @override
    560      */
    561     acceptSuggestion: function()
    562     {
    563         this._acceptSuggestionInternal();
    564     },
    565 
    566     /**
    567      * @param {boolean=} prefixAccepted
    568      * @return {boolean}
    569      */
    570     _acceptSuggestionInternal: function(prefixAccepted)
    571     {
    572         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
    573             return false;
    574 
    575         var text = this.autoCompleteElement.textContent;
    576         var textNode = document.createTextNode(text);
    577         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
    578         delete this.autoCompleteElement;
    579 
    580         var finalSelectionRange = document.createRange();
    581         finalSelectionRange.setStart(textNode, text.length);
    582         finalSelectionRange.setEnd(textNode, text.length);
    583 
    584         var selection = window.getSelection();
    585         selection.removeAllRanges();
    586         selection.addRange(finalSelectionRange);
    587 
    588         if (!prefixAccepted) {
    589             this.hideSuggestBox();
    590             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
    591         } else
    592             this.autoCompleteSoon(true);
    593 
    594         return true;
    595     },
    596 
    597     hideSuggestBox: function()
    598     {
    599         if (this.isSuggestBoxVisible())
    600             this._suggestBox.hide();
    601     },
    602 
    603     /**
    604      * @return {boolean}
    605      */
    606     isSuggestBoxVisible: function()
    607     {
    608         return this._suggestBox && this._suggestBox.visible();
    609     },
    610 
    611     /**
    612      * @return {boolean}
    613      */
    614     isCaretInsidePrompt: function()
    615     {
    616         return this._element.isInsertionCaretInside();
    617     },
    618 
    619     /**
    620      * @return {boolean}
    621      */
    622     isCaretAtEndOfPrompt: function()
    623     {
    624         var selection = window.getSelection();
    625         if (!selection.rangeCount || !selection.isCollapsed)
    626             return false;
    627 
    628         var selectionRange = selection.getRangeAt(0);
    629         var node = selectionRange.startContainer;
    630         if (!node.isSelfOrDescendant(this._element))
    631             return false;
    632 
    633         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
    634             return false;
    635 
    636         var foundNextText = false;
    637         while (node) {
    638             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
    639                 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
    640                     return false;
    641                 foundNextText = true;
    642             }
    643 
    644             node = node.traverseNextNode(this._element);
    645         }
    646 
    647         return true;
    648     },
    649 
    650     /**
    651      * @return {boolean}
    652      */
    653     isCaretOnFirstLine: function()
    654     {
    655         var selection = window.getSelection();
    656         var focusNode = selection.focusNode;
    657         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    658             return true;
    659 
    660         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
    661             return false;
    662         focusNode = focusNode.previousSibling;
    663 
    664         while (focusNode) {
    665             if (focusNode.nodeType !== Node.TEXT_NODE)
    666                 return true;
    667             if (focusNode.textContent.indexOf("\n") !== -1)
    668                 return false;
    669             focusNode = focusNode.previousSibling;
    670         }
    671 
    672         return true;
    673     },
    674 
    675     /**
    676      * @return {boolean}
    677      */
    678     isCaretOnLastLine: function()
    679     {
    680         var selection = window.getSelection();
    681         var focusNode = selection.focusNode;
    682         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    683             return true;
    684 
    685         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
    686             return false;
    687         focusNode = focusNode.nextSibling;
    688 
    689         while (focusNode) {
    690             if (focusNode.nodeType !== Node.TEXT_NODE)
    691                 return true;
    692             if (focusNode.textContent.indexOf("\n") !== -1)
    693                 return false;
    694             focusNode = focusNode.nextSibling;
    695         }
    696 
    697         return true;
    698     },
    699 
    700     moveCaretToEndOfPrompt: function()
    701     {
    702         var selection = window.getSelection();
    703         var selectionRange = document.createRange();
    704 
    705         var offset = this._element.childNodes.length;
    706         selectionRange.setStart(this._element, offset);
    707         selectionRange.setEnd(this._element, offset);
    708 
    709         selection.removeAllRanges();
    710         selection.addRange(selectionRange);
    711     },
    712 
    713     /**
    714      * @param {!Event} event
    715      * @return {boolean}
    716      */
    717     tabKeyPressed: function(event)
    718     {
    719         this._completeCommonPrefix();
    720 
    721         // Consume the key.
    722         return true;
    723     },
    724 
    725     __proto__: WebInspector.Object.prototype
    726 }
    727 
    728 
    729 /**
    730  * @constructor
    731  * @extends {WebInspector.TextPrompt}
    732  * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
    733  * @param {string=} stopCharacters
    734  */
    735 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
    736 {
    737     WebInspector.TextPrompt.call(this, completions, stopCharacters);
    738 
    739     /**
    740      * @type {!Array.<string>}
    741      */
    742     this._data = [];
    743 
    744     /**
    745      * 1-based entry in the history stack.
    746      * @type {number}
    747      */
    748     this._historyOffset = 1;
    749 
    750     /**
    751      * Whether to coalesce duplicate items in the history, default is true.
    752      * @type {boolean}
    753      */
    754     this._coalesceHistoryDupes = true;
    755 }
    756 
    757 WebInspector.TextPromptWithHistory.prototype = {
    758     /**
    759      * @return {!Array.<string>}
    760      */
    761     get historyData()
    762     {
    763         // FIXME: do we need to copy this?
    764         return this._data;
    765     },
    766 
    767     /**
    768      * @param {boolean} x
    769      */
    770     setCoalesceHistoryDupes: function(x)
    771     {
    772         this._coalesceHistoryDupes = x;
    773     },
    774 
    775     /**
    776      * @param {!Array.<string>} data
    777      */
    778     setHistoryData: function(data)
    779     {
    780         this._data = [].concat(data);
    781         this._historyOffset = 1;
    782     },
    783 
    784     /**
    785      * Pushes a committed text into the history.
    786      * @param {string} text
    787      */
    788     pushHistoryItem: function(text)
    789     {
    790         if (this._uncommittedIsTop) {
    791             this._data.pop();
    792             delete this._uncommittedIsTop;
    793         }
    794 
    795         this._historyOffset = 1;
    796         if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
    797             return;
    798         this._data.push(text);
    799     },
    800 
    801     /**
    802      * Pushes the current (uncommitted) text into the history.
    803      */
    804     _pushCurrentText: function()
    805     {
    806         if (this._uncommittedIsTop)
    807             this._data.pop(); // Throw away obsolete uncommitted text.
    808         this._uncommittedIsTop = true;
    809         this.clearAutoComplete(true);
    810         this._data.push(this.text);
    811     },
    812 
    813     /**
    814      * @return {string|undefined}
    815      */
    816     _previous: function()
    817     {
    818         if (this._historyOffset > this._data.length)
    819             return undefined;
    820         if (this._historyOffset === 1)
    821             this._pushCurrentText();
    822         ++this._historyOffset;
    823         return this._currentHistoryItem();
    824     },
    825 
    826     /**
    827      * @return {string|undefined}
    828      */
    829     _next: function()
    830     {
    831         if (this._historyOffset === 1)
    832             return undefined;
    833         --this._historyOffset;
    834         return this._currentHistoryItem();
    835     },
    836 
    837     /**
    838      * @return {string|undefined}
    839      */
    840     _currentHistoryItem: function()
    841     {
    842         return this._data[this._data.length - this._historyOffset];
    843     },
    844 
    845     /**
    846      * @override
    847      */
    848     onKeyDown: function(event)
    849     {
    850         var newText;
    851         var isPrevious;
    852 
    853         switch (event.keyIdentifier) {
    854         case "Up":
    855             if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
    856                 break;
    857             newText = this._previous();
    858             isPrevious = true;
    859             break;
    860         case "Down":
    861             if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
    862                 break;
    863             newText = this._next();
    864             break;
    865         case "U+0050": // Ctrl+P = Previous
    866             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    867                 newText = this._previous();
    868                 isPrevious = true;
    869             }
    870             break;
    871         case "U+004E": // Ctrl+N = Next
    872             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
    873                 newText = this._next();
    874             break;
    875         }
    876 
    877         if (newText !== undefined) {
    878             event.consume(true);
    879             this.text = newText;
    880 
    881             if (isPrevious) {
    882                 var firstNewlineIndex = this.text.indexOf("\n");
    883                 if (firstNewlineIndex === -1)
    884                     this.moveCaretToEndOfPrompt();
    885                 else {
    886                     var selection = window.getSelection();
    887                     var selectionRange = document.createRange();
    888 
    889                     selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
    890                     selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
    891 
    892                     selection.removeAllRanges();
    893                     selection.addRange(selectionRange);
    894                 }
    895             }
    896 
    897             return;
    898         }
    899 
    900         WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
    901     },
    902 
    903     __proto__: WebInspector.TextPrompt.prototype
    904 }
    905 
    906