Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2008 Apple Inc.  All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  *
      8  * 1.  Redistributions of source code must retain the above copyright
      9  *     notice, this list of conditions and the following disclaimer.
     10  * 2.  Redistributions in binary form must reproduce the above copyright
     11  *     notice, this list of conditions and the following disclaimer in the
     12  *     documentation and/or other materials provided with the distribution.
     13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     14  *     its contributors may be used to endorse or promote products derived
     15  *     from this software without specific prior written permission.
     16  *
     17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     27  */
     28 
     29 WebInspector.TextPrompt = function(element, completions, stopCharacters, omitHistory)
     30 {
     31     this.element = element;
     32     this.element.addStyleClass("text-prompt");
     33     this.completions = completions;
     34     this.completionStopCharacters = stopCharacters;
     35     if (!omitHistory) {
     36         this.history = [];
     37         this.historyOffset = 0;
     38     }
     39     this._boundOnKeyDown = this._onKeyDown.bind(this);
     40     this.element.addEventListener("keydown", this._boundOnKeyDown, true);
     41 }
     42 
     43 WebInspector.TextPrompt.prototype = {
     44     get text()
     45     {
     46         return this.element.textContent;
     47     },
     48 
     49     set text(x)
     50     {
     51         if (!x) {
     52             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
     53             this.element.removeChildren();
     54             this.element.appendChild(document.createElement("br"));
     55         } else
     56             this.element.textContent = x;
     57 
     58         this.moveCaretToEndOfPrompt();
     59     },
     60 
     61     removeFromElement: function()
     62     {
     63         this.clearAutoComplete(true);
     64         this.element.removeEventListener("keydown", this._boundOnKeyDown, true);
     65     },
     66 
     67     _onKeyDown: function(event)
     68     {
     69         function defaultAction()
     70         {
     71             this.clearAutoComplete();
     72             this.autoCompleteSoon();
     73         }
     74 
     75         if (event.handled)
     76             return;
     77 
     78         var handled = false;
     79 
     80         switch (event.keyIdentifier) {
     81             case "Up":
     82                 this.upKeyPressed(event);
     83                 break;
     84             case "Down":
     85                 this.downKeyPressed(event);
     86                 break;
     87             case "U+0009": // Tab
     88                 this.tabKeyPressed(event);
     89                 break;
     90             case "Right":
     91             case "End":
     92                 if (!this.acceptAutoComplete())
     93                     this.autoCompleteSoon();
     94                 break;
     95             case "Alt":
     96             case "Meta":
     97             case "Shift":
     98             case "Control":
     99                 break;
    100             case "U+0050": // Ctrl+P = Previous
    101                 if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    102                     handled = true;
    103                     this._moveBackInHistory();
    104                     break;
    105                 }
    106                 defaultAction.call(this);
    107                 break;
    108             case "U+004E": // Ctrl+N = Next
    109                 if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    110                     handled = true;
    111                     this._moveForwardInHistory();
    112                     break;
    113                 }
    114                 defaultAction.call(this);
    115                 break;
    116             default:
    117                 defaultAction.call(this);
    118                 break;
    119         }
    120 
    121         handled |= event.handled;
    122         if (handled) {
    123             event.handled = true;
    124             event.preventDefault();
    125             event.stopPropagation();
    126         }
    127     },
    128 
    129     acceptAutoComplete: function()
    130     {
    131         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
    132             return false;
    133 
    134         var text = this.autoCompleteElement.textContent;
    135         var textNode = document.createTextNode(text);
    136         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
    137         delete this.autoCompleteElement;
    138 
    139         var finalSelectionRange = document.createRange();
    140         finalSelectionRange.setStart(textNode, text.length);
    141         finalSelectionRange.setEnd(textNode, text.length);
    142 
    143         var selection = window.getSelection();
    144         selection.removeAllRanges();
    145         selection.addRange(finalSelectionRange);
    146 
    147         return true;
    148     },
    149 
    150     clearAutoComplete: function(includeTimeout)
    151     {
    152         if (includeTimeout && "_completeTimeout" in this) {
    153             clearTimeout(this._completeTimeout);
    154             delete this._completeTimeout;
    155         }
    156 
    157         if (!this.autoCompleteElement)
    158             return;
    159 
    160         if (this.autoCompleteElement.parentNode)
    161             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
    162         delete this.autoCompleteElement;
    163 
    164         if (!this._userEnteredRange || !this._userEnteredText)
    165             return;
    166 
    167         this._userEnteredRange.deleteContents();
    168         this.element.pruneEmptyTextNodes();
    169 
    170         var userTextNode = document.createTextNode(this._userEnteredText);
    171         this._userEnteredRange.insertNode(userTextNode);
    172 
    173         var selectionRange = document.createRange();
    174         selectionRange.setStart(userTextNode, this._userEnteredText.length);
    175         selectionRange.setEnd(userTextNode, this._userEnteredText.length);
    176 
    177         var selection = window.getSelection();
    178         selection.removeAllRanges();
    179         selection.addRange(selectionRange);
    180 
    181         delete this._userEnteredRange;
    182         delete this._userEnteredText;
    183     },
    184 
    185     autoCompleteSoon: function()
    186     {
    187         if (!("_completeTimeout" in this))
    188             this._completeTimeout = setTimeout(this.complete.bind(this, true), 250);
    189     },
    190 
    191     complete: function(auto, reverse)
    192     {
    193         this.clearAutoComplete(true);
    194         var selection = window.getSelection();
    195         if (!selection.rangeCount)
    196             return;
    197 
    198         var selectionRange = selection.getRangeAt(0);
    199         var isEmptyInput = selectionRange.commonAncestorContainer === this.element; // this.element has no child Text nodes.
    200 
    201         // Do not attempt to auto-complete an empty input in the auto mode (only on demand).
    202         if (auto && isEmptyInput)
    203             return;
    204         if (!auto && !isEmptyInput && !selectionRange.commonAncestorContainer.isDescendant(this.element))
    205             return;
    206         if (auto && !this.isCaretAtEndOfPrompt())
    207             return;
    208         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward");
    209         this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange, reverse));
    210     },
    211 
    212     _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions)
    213     {
    214         if (!completions || !completions.length)
    215             return;
    216 
    217         var selectionRange = selection.getRangeAt(0);
    218 
    219         var fullWordRange = document.createRange();
    220         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
    221         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
    222 
    223         if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
    224             return;
    225 
    226         var wordPrefixLength = originalWordPrefixRange.toString().length;
    227 
    228         if (auto)
    229             var completionText = completions[0];
    230         else {
    231             if (completions.length === 1) {
    232                 var completionText = completions[0];
    233                 wordPrefixLength = completionText.length;
    234             } else {
    235                 var commonPrefix = completions[0];
    236                 for (var i = 0; i < completions.length; ++i) {
    237                     var completion = completions[i];
    238                     var lastIndex = Math.min(commonPrefix.length, completion.length);
    239                     for (var j = wordPrefixLength; j < lastIndex; ++j) {
    240                         if (commonPrefix[j] !== completion[j]) {
    241                             commonPrefix = commonPrefix.substr(0, j);
    242                             break;
    243                         }
    244                     }
    245                 }
    246                 wordPrefixLength = commonPrefix.length;
    247 
    248                 if (selection.isCollapsed)
    249                     var completionText = completions[0];
    250                 else {
    251                     var currentText = fullWordRange.toString();
    252 
    253                     var foundIndex = null;
    254                     for (var i = 0; i < completions.length; ++i) {
    255                         if (completions[i] === currentText)
    256                             foundIndex = i;
    257                     }
    258 
    259                     var nextIndex = foundIndex + (reverse ? -1 : 1);
    260                     if (foundIndex === null || nextIndex >= completions.length)
    261                         var completionText = completions[0];
    262                     else if (nextIndex < 0)
    263                         var completionText = completions[completions.length - 1];
    264                     else
    265                         var completionText = completions[nextIndex];
    266                 }
    267             }
    268         }
    269 
    270         this._userEnteredRange = fullWordRange;
    271         this._userEnteredText = fullWordRange.toString();
    272 
    273         fullWordRange.deleteContents();
    274         this.element.pruneEmptyTextNodes();
    275 
    276         var finalSelectionRange = document.createRange();
    277 
    278         if (auto) {
    279             var prefixText = completionText.substring(0, wordPrefixLength);
    280             var suffixText = completionText.substring(wordPrefixLength);
    281 
    282             var prefixTextNode = document.createTextNode(prefixText);
    283             fullWordRange.insertNode(prefixTextNode);
    284 
    285             this.autoCompleteElement = document.createElement("span");
    286             this.autoCompleteElement.className = "auto-complete-text";
    287             this.autoCompleteElement.textContent = suffixText;
    288 
    289             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
    290 
    291             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
    292             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
    293         } else {
    294             var completionTextNode = document.createTextNode(completionText);
    295             fullWordRange.insertNode(completionTextNode);
    296 
    297             if (completions.length > 1)
    298                 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
    299             else
    300                 finalSelectionRange.setStart(completionTextNode, completionText.length);
    301 
    302             finalSelectionRange.setEnd(completionTextNode, completionText.length);
    303         }
    304 
    305         selection.removeAllRanges();
    306         selection.addRange(finalSelectionRange);
    307     },
    308 
    309     isCaretInsidePrompt: function()
    310     {
    311         return this.element.isInsertionCaretInside();
    312     },
    313 
    314     isCaretAtEndOfPrompt: function()
    315     {
    316         var selection = window.getSelection();
    317         if (!selection.rangeCount || !selection.isCollapsed)
    318             return false;
    319 
    320         var selectionRange = selection.getRangeAt(0);
    321         var node = selectionRange.startContainer;
    322         if (node !== this.element && !node.isDescendant(this.element))
    323             return false;
    324 
    325         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
    326             return false;
    327 
    328         var foundNextText = false;
    329         while (node) {
    330             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
    331                 if (foundNextText)
    332                     return false;
    333                 foundNextText = true;
    334             }
    335 
    336             node = node.traverseNextNode(this.element);
    337         }
    338 
    339         return true;
    340     },
    341 
    342     isCaretOnFirstLine: function()
    343     {
    344         var selection = window.getSelection();
    345         var focusNode = selection.focusNode;
    346         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element)
    347             return true;
    348 
    349         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
    350             return false;
    351         focusNode = focusNode.previousSibling;
    352 
    353         while (focusNode) {
    354             if (focusNode.nodeType !== Node.TEXT_NODE)
    355                 return true;
    356             if (focusNode.textContent.indexOf("\n") !== -1)
    357                 return false;
    358             focusNode = focusNode.previousSibling;
    359         }
    360 
    361         return true;
    362     },
    363 
    364     isCaretOnLastLine: function()
    365     {
    366         var selection = window.getSelection();
    367         var focusNode = selection.focusNode;
    368         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element)
    369             return true;
    370 
    371         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
    372             return false;
    373         focusNode = focusNode.nextSibling;
    374 
    375         while (focusNode) {
    376             if (focusNode.nodeType !== Node.TEXT_NODE)
    377                 return true;
    378             if (focusNode.textContent.indexOf("\n") !== -1)
    379                 return false;
    380             focusNode = focusNode.nextSibling;
    381         }
    382 
    383         return true;
    384     },
    385 
    386     moveCaretToEndOfPrompt: function()
    387     {
    388         var selection = window.getSelection();
    389         var selectionRange = document.createRange();
    390 
    391         var offset = this.element.childNodes.length;
    392         selectionRange.setStart(this.element, offset);
    393         selectionRange.setEnd(this.element, offset);
    394 
    395         selection.removeAllRanges();
    396         selection.addRange(selectionRange);
    397     },
    398 
    399     tabKeyPressed: function(event)
    400     {
    401         event.handled = true;
    402         this.complete(false, event.shiftKey);
    403     },
    404 
    405     upKeyPressed: function(event)
    406     {
    407         if (!this.isCaretOnFirstLine())
    408             return;
    409 
    410         event.handled = true;
    411         this._moveBackInHistory();
    412     },
    413 
    414     downKeyPressed: function(event)
    415     {
    416         if (!this.isCaretOnLastLine())
    417             return;
    418 
    419         event.handled = true;
    420         this._moveForwardInHistory();
    421     },
    422 
    423     _moveBackInHistory: function()
    424     {
    425         if (!this.history || this.historyOffset == this.history.length)
    426             return;
    427 
    428         this.clearAutoComplete(true);
    429 
    430         if (this.historyOffset === 0)
    431             this.tempSavedCommand = this.text;
    432 
    433         ++this.historyOffset;
    434         this.text = this.history[this.history.length - this.historyOffset];
    435 
    436         this.element.scrollIntoView(true);
    437         var firstNewlineIndex = this.text.indexOf("\n");
    438         if (firstNewlineIndex === -1)
    439             this.moveCaretToEndOfPrompt();
    440         else {
    441             var selection = window.getSelection();
    442             var selectionRange = document.createRange();
    443 
    444             selectionRange.setStart(this.element.firstChild, firstNewlineIndex);
    445             selectionRange.setEnd(this.element.firstChild, firstNewlineIndex);
    446 
    447             selection.removeAllRanges();
    448             selection.addRange(selectionRange);
    449         }
    450     },
    451 
    452     _moveForwardInHistory: function()
    453     {
    454         if (!this.history || this.historyOffset === 0)
    455             return;
    456 
    457         this.clearAutoComplete(true);
    458 
    459         --this.historyOffset;
    460 
    461         if (this.historyOffset === 0) {
    462             this.text = this.tempSavedCommand;
    463             delete this.tempSavedCommand;
    464             return;
    465         }
    466 
    467         this.text = this.history[this.history.length - this.historyOffset];
    468         this.element.scrollIntoView();
    469     }
    470 }
    471