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.appendChild(document.createElement("br"));
    161         } else
    162             this._element.textContent = x;
    163 
    164         this.moveCaretToEndOfPrompt();
    165         this._element.scrollIntoView();
    166     },
    167 
    168     _removeFromElement: function()
    169     {
    170         this.clearAutoComplete(true);
    171         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
    172         this._element.removeEventListener("input", this._boundOnInput, false);
    173         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
    174         this._element.removeEventListener("blur", this._boundRemoveSuggestionAids, false);
    175         if (this._isEditing)
    176             this._stopEditing();
    177         if (this._suggestBox)
    178             this._suggestBox.removeFromElement();
    179     },
    180 
    181     /**
    182      * @param {function(!Event)=} blurListener
    183      */
    184     _startEditing: function(blurListener)
    185     {
    186         this._isEditing = true;
    187         this._element.classList.add("editing");
    188         if (blurListener) {
    189             this._blurListener = blurListener;
    190             this._element.addEventListener("blur", this._blurListener, false);
    191         }
    192         this._oldTabIndex = this._element.tabIndex;
    193         if (this._element.tabIndex < 0)
    194             this._element.tabIndex = 0;
    195         WebInspector.setCurrentFocusElement(this._element);
    196         if (!this.text)
    197             this._updateAutoComplete();
    198     },
    199 
    200     _stopEditing: function()
    201     {
    202         this._element.tabIndex = this._oldTabIndex;
    203         if (this._blurListener)
    204             this._element.removeEventListener("blur", this._blurListener, false);
    205         this._element.classList.remove("editing");
    206         delete this._isEditing;
    207     },
    208 
    209     _removeSuggestionAids: function()
    210     {
    211         this.clearAutoComplete();
    212         this.hideSuggestBox();
    213     },
    214 
    215     _selectStart: function()
    216     {
    217         if (this._selectionTimeout)
    218             clearTimeout(this._selectionTimeout);
    219 
    220         this._removeSuggestionAids();
    221 
    222         /**
    223          * @this {WebInspector.TextPrompt}
    224          */
    225         function moveBackIfOutside()
    226         {
    227             delete this._selectionTimeout;
    228             if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
    229                 this.moveCaretToEndOfPrompt();
    230                 this.autoCompleteSoon();
    231             }
    232         }
    233 
    234         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
    235     },
    236 
    237     /**
    238      * @param {boolean=} force
    239      */
    240     _updateAutoComplete: function(force)
    241     {
    242         this.clearAutoComplete();
    243         this.autoCompleteSoon(force);
    244     },
    245 
    246     /**
    247      * @param {?Event} event
    248      */
    249     onMouseWheel: function(event)
    250     {
    251         // Subclasses can implement.
    252     },
    253 
    254     /**
    255      * @param {?Event} event
    256      */
    257     onKeyDown: function(event)
    258     {
    259         var handled = false;
    260         delete this._needUpdateAutocomplete;
    261 
    262         switch (event.keyIdentifier) {
    263         case "U+0009": // Tab
    264             handled = this.tabKeyPressed(event);
    265             break;
    266         case "Left":
    267         case "Home":
    268             this._removeSuggestionAids();
    269             break;
    270         case "Right":
    271         case "End":
    272             if (this.isCaretAtEndOfPrompt())
    273                 handled = this.acceptAutoComplete();
    274             else
    275                 this._removeSuggestionAids();
    276             break;
    277         case "U+001B": // Esc
    278             if (this.isSuggestBoxVisible()) {
    279                 this._removeSuggestionAids();
    280                 handled = true;
    281             }
    282             break;
    283         case "U+0020": // Space
    284             if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    285                 this._updateAutoComplete(true);
    286                 handled = true;
    287             }
    288             break;
    289         case "Alt":
    290         case "Meta":
    291         case "Shift":
    292         case "Control":
    293             break;
    294         }
    295 
    296         if (!handled && this.isSuggestBoxVisible())
    297             handled = this._suggestBox.keyPressed(event);
    298 
    299         if (!handled)
    300             this._needUpdateAutocomplete = true;
    301 
    302         if (handled)
    303             event.consume(true);
    304     },
    305 
    306     /**
    307      * @param {?Event} event
    308      */
    309     onInput: function(event)
    310     {
    311         if (this._needUpdateAutocomplete)
    312             this._updateAutoComplete();
    313     },
    314 
    315     /**
    316      * @return {boolean}
    317      */
    318     acceptAutoComplete: function()
    319     {
    320         var result = false;
    321         if (this.isSuggestBoxVisible())
    322             result = this._suggestBox.acceptSuggestion();
    323         if (!result)
    324             result = this._acceptSuggestionInternal();
    325 
    326         return result;
    327     },
    328 
    329     /**
    330      * @param {boolean=} includeTimeout
    331      */
    332     clearAutoComplete: function(includeTimeout)
    333     {
    334         if (includeTimeout && this._completeTimeout) {
    335             clearTimeout(this._completeTimeout);
    336             delete this._completeTimeout;
    337         }
    338         delete this._waitingForCompletions;
    339 
    340         if (!this.autoCompleteElement)
    341             return;
    342 
    343         this.autoCompleteElement.remove();
    344         delete this.autoCompleteElement;
    345         delete this._userEnteredRange;
    346         delete this._userEnteredText;
    347     },
    348 
    349     /**
    350      * @param {boolean=} force
    351      */
    352     autoCompleteSoon: function(force)
    353     {
    354         var immediately = this.isSuggestBoxVisible() || force;
    355         if (!this._completeTimeout)
    356             this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
    357     },
    358 
    359     /**
    360      * @param {boolean=} force
    361      * @param {boolean=} reverse
    362      */
    363     complete: function(force, reverse)
    364     {
    365         this.clearAutoComplete(true);
    366         var selection = window.getSelection();
    367         if (!selection.rangeCount)
    368             return;
    369 
    370         var selectionRange = selection.getRangeAt(0);
    371         var shouldExit;
    372 
    373         if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
    374             shouldExit = true;
    375         else if (!selection.isCollapsed)
    376             shouldExit = true;
    377         else if (!force) {
    378             // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
    379             var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
    380             if (wordSuffixRange.toString().length)
    381                 shouldExit = true;
    382         }
    383         if (shouldExit) {
    384             this.hideSuggestBox();
    385             return;
    386         }
    387 
    388         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
    389         this._waitingForCompletions = true;
    390         this._loadCompletions(this.proxyElement, wordPrefixRange, force || false, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse));
    391     },
    392 
    393     disableDefaultSuggestionForEmptyInput: function()
    394     {
    395         this._disableDefaultSuggestionForEmptyInput = true;
    396     },
    397 
    398     /**
    399      * @param {!Selection} selection
    400      * @param {!Range} textRange
    401      */
    402     _boxForAnchorAtStart: function(selection, textRange)
    403     {
    404         var rangeCopy = selection.getRangeAt(0).cloneRange();
    405         var anchorElement = document.createElement("span");
    406         anchorElement.textContent = "\u200B";
    407         textRange.insertNode(anchorElement);
    408         var box = anchorElement.boxInWindow(window);
    409         anchorElement.remove();
    410         selection.removeAllRanges();
    411         selection.addRange(rangeCopy);
    412         return box;
    413     },
    414 
    415     /**
    416      * @param {!Array.<string>} completions
    417      * @param {number} wordPrefixLength
    418      */
    419     _buildCommonPrefix: function(completions, wordPrefixLength)
    420     {
    421         var commonPrefix = completions[0];
    422         for (var i = 0; i < completions.length; ++i) {
    423             var completion = completions[i];
    424             var lastIndex = Math.min(commonPrefix.length, completion.length);
    425             for (var j = wordPrefixLength; j < lastIndex; ++j) {
    426                 if (commonPrefix[j] !== completion[j]) {
    427                     commonPrefix = commonPrefix.substr(0, j);
    428                     break;
    429                 }
    430             }
    431         }
    432         return commonPrefix;
    433     },
    434 
    435     /**
    436      * @param {!Selection} selection
    437      * @param {!Range} originalWordPrefixRange
    438      * @param {boolean} reverse
    439      * @param {!Array.<string>} completions
    440      * @param {number=} selectedIndex
    441      */
    442     _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
    443     {
    444         if (!this._waitingForCompletions || !completions.length) {
    445             this.hideSuggestBox();
    446             return;
    447         }
    448         delete this._waitingForCompletions;
    449 
    450         var selectionRange = selection.getRangeAt(0);
    451 
    452         var fullWordRange = document.createRange();
    453         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
    454         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
    455 
    456         if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString())
    457             return;
    458 
    459         selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0);
    460 
    461         this._userEnteredRange = fullWordRange;
    462         this._userEnteredText = fullWordRange.toString();
    463 
    464         if (this._suggestBox)
    465             this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
    466 
    467         if (selectedIndex === -1)
    468             return;
    469 
    470         var wordPrefixLength = originalWordPrefixRange.toString().length;
    471         this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
    472 
    473         if (this.isCaretAtEndOfPrompt()) {
    474             this._userEnteredRange.deleteContents();
    475             this._element.normalize();
    476             var finalSelectionRange = document.createRange();
    477             var completionText = completions[selectedIndex];
    478             var prefixText = completionText.substring(0, wordPrefixLength);
    479             var suffixText = completionText.substring(wordPrefixLength);
    480 
    481             var prefixTextNode = document.createTextNode(prefixText);
    482             fullWordRange.insertNode(prefixTextNode);
    483 
    484             this.autoCompleteElement = document.createElement("span");
    485             this.autoCompleteElement.className = "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._isAcceptingSuggestion)
    573             return false;
    574 
    575         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
    576             return false;
    577 
    578         var text = this.autoCompleteElement.textContent;
    579         var textNode = document.createTextNode(text);
    580         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
    581         delete this.autoCompleteElement;
    582 
    583         var finalSelectionRange = document.createRange();
    584         finalSelectionRange.setStart(textNode, text.length);
    585         finalSelectionRange.setEnd(textNode, text.length);
    586 
    587         var selection = window.getSelection();
    588         selection.removeAllRanges();
    589         selection.addRange(finalSelectionRange);
    590 
    591         if (!prefixAccepted) {
    592             this.hideSuggestBox();
    593             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
    594         } else
    595             this.autoCompleteSoon(true);
    596 
    597         return true;
    598     },
    599 
    600     hideSuggestBox: function()
    601     {
    602         if (this.isSuggestBoxVisible())
    603             this._suggestBox.hide();
    604     },
    605 
    606     /**
    607      * @return {boolean}
    608      */
    609     isSuggestBoxVisible: function()
    610     {
    611         return this._suggestBox && this._suggestBox.visible();
    612     },
    613 
    614     /**
    615      * @return {boolean}
    616      */
    617     isCaretInsidePrompt: function()
    618     {
    619         return this._element.isInsertionCaretInside();
    620     },
    621 
    622     /**
    623      * @return {boolean}
    624      */
    625     isCaretAtEndOfPrompt: function()
    626     {
    627         var selection = window.getSelection();
    628         if (!selection.rangeCount || !selection.isCollapsed)
    629             return false;
    630 
    631         var selectionRange = selection.getRangeAt(0);
    632         var node = selectionRange.startContainer;
    633         if (!node.isSelfOrDescendant(this._element))
    634             return false;
    635 
    636         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
    637             return false;
    638 
    639         var foundNextText = false;
    640         while (node) {
    641             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
    642                 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
    643                     return false;
    644                 foundNextText = true;
    645             }
    646 
    647             node = node.traverseNextNode(this._element);
    648         }
    649 
    650         return true;
    651     },
    652 
    653     /**
    654      * @return {boolean}
    655      */
    656     isCaretOnFirstLine: function()
    657     {
    658         var selection = window.getSelection();
    659         var focusNode = selection.focusNode;
    660         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    661             return true;
    662 
    663         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
    664             return false;
    665         focusNode = focusNode.previousSibling;
    666 
    667         while (focusNode) {
    668             if (focusNode.nodeType !== Node.TEXT_NODE)
    669                 return true;
    670             if (focusNode.textContent.indexOf("\n") !== -1)
    671                 return false;
    672             focusNode = focusNode.previousSibling;
    673         }
    674 
    675         return true;
    676     },
    677 
    678     /**
    679      * @return {boolean}
    680      */
    681     isCaretOnLastLine: function()
    682     {
    683         var selection = window.getSelection();
    684         var focusNode = selection.focusNode;
    685         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    686             return true;
    687 
    688         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
    689             return false;
    690         focusNode = focusNode.nextSibling;
    691 
    692         while (focusNode) {
    693             if (focusNode.nodeType !== Node.TEXT_NODE)
    694                 return true;
    695             if (focusNode.textContent.indexOf("\n") !== -1)
    696                 return false;
    697             focusNode = focusNode.nextSibling;
    698         }
    699 
    700         return true;
    701     },
    702 
    703     moveCaretToEndOfPrompt: function()
    704     {
    705         var selection = window.getSelection();
    706         var selectionRange = document.createRange();
    707 
    708         var offset = this._element.childNodes.length;
    709         selectionRange.setStart(this._element, offset);
    710         selectionRange.setEnd(this._element, offset);
    711 
    712         selection.removeAllRanges();
    713         selection.addRange(selectionRange);
    714     },
    715 
    716     /**
    717      * @param {!Event} event
    718      * @return {boolean}
    719      */
    720     tabKeyPressed: function(event)
    721     {
    722         this._completeCommonPrefix();
    723 
    724         // Consume the key.
    725         return true;
    726     },
    727 
    728     __proto__: WebInspector.Object.prototype
    729 }
    730 
    731 
    732 /**
    733  * @constructor
    734  * @extends {WebInspector.TextPrompt}
    735  * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
    736  * @param {string=} stopCharacters
    737  */
    738 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
    739 {
    740     WebInspector.TextPrompt.call(this, completions, stopCharacters);
    741 
    742     /**
    743      * @type {!Array.<string>}
    744      */
    745     this._data = [];
    746 
    747     /**
    748      * 1-based entry in the history stack.
    749      * @type {number}
    750      */
    751     this._historyOffset = 1;
    752 
    753     /**
    754      * Whether to coalesce duplicate items in the history, default is true.
    755      * @type {boolean}
    756      */
    757     this._coalesceHistoryDupes = true;
    758 }
    759 
    760 WebInspector.TextPromptWithHistory.prototype = {
    761     /**
    762      * @return {!Array.<string>}
    763      */
    764     get historyData()
    765     {
    766         // FIXME: do we need to copy this?
    767         return this._data;
    768     },
    769 
    770     /**
    771      * @param {boolean} x
    772      */
    773     setCoalesceHistoryDupes: function(x)
    774     {
    775         this._coalesceHistoryDupes = x;
    776     },
    777 
    778     /**
    779      * @param {!Array.<string>} data
    780      */
    781     setHistoryData: function(data)
    782     {
    783         this._data = [].concat(data);
    784         this._historyOffset = 1;
    785     },
    786 
    787     /**
    788      * Pushes a committed text into the history.
    789      * @param {string} text
    790      */
    791     pushHistoryItem: function(text)
    792     {
    793         if (this._uncommittedIsTop) {
    794             this._data.pop();
    795             delete this._uncommittedIsTop;
    796         }
    797 
    798         this._historyOffset = 1;
    799         if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
    800             return;
    801         this._data.push(text);
    802     },
    803 
    804     /**
    805      * Pushes the current (uncommitted) text into the history.
    806      */
    807     _pushCurrentText: function()
    808     {
    809         if (this._uncommittedIsTop)
    810             this._data.pop(); // Throw away obsolete uncommitted text.
    811         this._uncommittedIsTop = true;
    812         this.clearAutoComplete(true);
    813         this._data.push(this.text);
    814     },
    815 
    816     /**
    817      * @return {string|undefined}
    818      */
    819     _previous: function()
    820     {
    821         if (this._historyOffset > this._data.length)
    822             return undefined;
    823         if (this._historyOffset === 1)
    824             this._pushCurrentText();
    825         ++this._historyOffset;
    826         return this._currentHistoryItem();
    827     },
    828 
    829     /**
    830      * @return {string|undefined}
    831      */
    832     _next: function()
    833     {
    834         if (this._historyOffset === 1)
    835             return undefined;
    836         --this._historyOffset;
    837         return this._currentHistoryItem();
    838     },
    839 
    840     /**
    841      * @return {string|undefined}
    842      */
    843     _currentHistoryItem: function()
    844     {
    845         return this._data[this._data.length - this._historyOffset];
    846     },
    847 
    848     /**
    849      * @override
    850      */
    851     onKeyDown: function(event)
    852     {
    853         var newText;
    854         var isPrevious;
    855 
    856         switch (event.keyIdentifier) {
    857         case "Up":
    858             if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
    859                 break;
    860             newText = this._previous();
    861             isPrevious = true;
    862             break;
    863         case "Down":
    864             if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
    865                 break;
    866             newText = this._next();
    867             break;
    868         case "U+0050": // Ctrl+P = Previous
    869             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    870                 newText = this._previous();
    871                 isPrevious = true;
    872             }
    873             break;
    874         case "U+004E": // Ctrl+N = Next
    875             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
    876                 newText = this._next();
    877             break;
    878         }
    879 
    880         if (newText !== undefined) {
    881             event.consume(true);
    882             this.text = newText;
    883 
    884             if (isPrevious) {
    885                 var firstNewlineIndex = this.text.indexOf("\n");
    886                 if (firstNewlineIndex === -1)
    887                     this.moveCaretToEndOfPrompt();
    888                 else {
    889                     var selection = window.getSelection();
    890                     var selectionRange = document.createRange();
    891 
    892                     selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
    893                     selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
    894 
    895                     selection.removeAllRanges();
    896                     selection.addRange(selectionRange);
    897                 }
    898             }
    899 
    900             return;
    901         }
    902 
    903         WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
    904     },
    905 
    906     __proto__: WebInspector.TextPrompt.prototype
    907 }
    908 
    909