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)
     30 {
     31     this.element = element;
     32     this.completions = completions;
     33     this.completionStopCharacters = stopCharacters;
     34     this.history = [];
     35     this.historyOffset = 0;
     36     this.element.addEventListener("keydown", this._onKeyDown.bind(this), true);
     37 }
     38 
     39 WebInspector.TextPrompt.prototype = {
     40     get text()
     41     {
     42         return this.element.textContent;
     43     },
     44 
     45     set text(x)
     46     {
     47         if (!x) {
     48             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
     49             this.element.removeChildren();
     50             this.element.appendChild(document.createElement("br"));
     51         } else
     52             this.element.textContent = x;
     53 
     54         this.moveCaretToEndOfPrompt();
     55     },
     56 
     57     _onKeyDown: function(event)
     58     {
     59         function defaultAction()
     60         {
     61             this.clearAutoComplete();
     62             this.autoCompleteSoon();
     63         }
     64 
     65         var handled = false;
     66         switch (event.keyIdentifier) {
     67             case "Up":
     68                 this._upKeyPressed(event);
     69                 break;
     70             case "Down":
     71                 this._downKeyPressed(event);
     72                 break;
     73             case "U+0009": // Tab
     74                 this._tabKeyPressed(event);
     75                 break;
     76             case "Right":
     77             case "End":
     78                 if (!this.acceptAutoComplete())
     79                     this.autoCompleteSoon();
     80                 break;
     81             case "Alt":
     82             case "Meta":
     83             case "Shift":
     84             case "Control":
     85                 break;
     86             case "U+0050": // Ctrl+P = Previous
     87                 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
     88                     handled = true;
     89                     this._moveBackInHistory();
     90                     break;
     91                 }
     92                 defaultAction.call(this);
     93                 break;
     94             case "U+004E": // Ctrl+N = Next
     95                 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
     96                     handled = true;
     97                     this._moveForwardInHistory();
     98                     break;
     99                 }
    100                 defaultAction.call(this);
    101                 break;
    102             default:
    103                 defaultAction.call(this);
    104                 break;
    105         }
    106 
    107         if (handled) {
    108             event.preventDefault();
    109             event.stopPropagation();
    110         }
    111     },
    112 
    113     acceptAutoComplete: function()
    114     {
    115         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
    116             return false;
    117 
    118         var text = this.autoCompleteElement.textContent;
    119         var textNode = document.createTextNode(text);
    120         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
    121         delete this.autoCompleteElement;
    122 
    123         var finalSelectionRange = document.createRange();
    124         finalSelectionRange.setStart(textNode, text.length);
    125         finalSelectionRange.setEnd(textNode, text.length);
    126 
    127         var selection = window.getSelection();
    128         selection.removeAllRanges();
    129         selection.addRange(finalSelectionRange);
    130 
    131         return true;
    132     },
    133 
    134     clearAutoComplete: function(includeTimeout)
    135     {
    136         if (includeTimeout && "_completeTimeout" in this) {
    137             clearTimeout(this._completeTimeout);
    138             delete this._completeTimeout;
    139         }
    140 
    141         if (!this.autoCompleteElement)
    142             return;
    143 
    144         if (this.autoCompleteElement.parentNode)
    145             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
    146         delete this.autoCompleteElement;
    147 
    148         if (!this._userEnteredRange || !this._userEnteredText)
    149             return;
    150 
    151         this._userEnteredRange.deleteContents();
    152 
    153         var userTextNode = document.createTextNode(this._userEnteredText);
    154         this._userEnteredRange.insertNode(userTextNode);
    155 
    156         var selectionRange = document.createRange();
    157         selectionRange.setStart(userTextNode, this._userEnteredText.length);
    158         selectionRange.setEnd(userTextNode, this._userEnteredText.length);
    159 
    160         var selection = window.getSelection();
    161         selection.removeAllRanges();
    162         selection.addRange(selectionRange);
    163 
    164         delete this._userEnteredRange;
    165         delete this._userEnteredText;
    166     },
    167 
    168     autoCompleteSoon: function()
    169     {
    170         if (!("_completeTimeout" in this))
    171             this._completeTimeout = setTimeout(this.complete.bind(this, true), 250);
    172     },
    173 
    174     complete: function(auto)
    175     {
    176         this.clearAutoComplete(true);
    177         var selection = window.getSelection();
    178         if (!selection.rangeCount)
    179             return;
    180 
    181         var selectionRange = selection.getRangeAt(0);
    182         if (!selectionRange.commonAncestorContainer.isDescendant(this.element))
    183             return;
    184         if (auto && !this.isCaretAtEndOfPrompt())
    185             return;
    186         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward");
    187         this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange));
    188     },
    189 
    190     _completionsReady: function(selection, auto, originalWordPrefixRange, completions)
    191     {
    192         if (!completions || !completions.length)
    193             return;
    194 
    195         var selectionRange = selection.getRangeAt(0);
    196 
    197         var fullWordRange = document.createRange();
    198         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
    199         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
    200 
    201         if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
    202             return;
    203 
    204         if (completions.length === 1 || selection.isCollapsed || auto) {
    205             var completionText = completions[0];
    206         } else {
    207             var currentText = fullWordRange.toString();
    208 
    209             var foundIndex = null;
    210             for (var i = 0; i < completions.length; ++i)
    211                 if (completions[i] === currentText)
    212                     foundIndex = i;
    213 
    214             if (foundIndex === null || (foundIndex + 1) >= completions.length)
    215                 var completionText = completions[0];
    216             else
    217                 var completionText = completions[foundIndex + 1];
    218         }
    219 
    220         var wordPrefixLength = originalWordPrefixRange.toString().length;
    221 
    222         this._userEnteredRange = fullWordRange;
    223         this._userEnteredText = fullWordRange.toString();
    224 
    225         fullWordRange.deleteContents();
    226 
    227         var finalSelectionRange = document.createRange();
    228 
    229         if (auto) {
    230             var prefixText = completionText.substring(0, wordPrefixLength);
    231             var suffixText = completionText.substring(wordPrefixLength);
    232 
    233             var prefixTextNode = document.createTextNode(prefixText);
    234             fullWordRange.insertNode(prefixTextNode);
    235 
    236             this.autoCompleteElement = document.createElement("span");
    237             this.autoCompleteElement.className = "auto-complete-text";
    238             this.autoCompleteElement.textContent = suffixText;
    239 
    240             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
    241 
    242             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
    243             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
    244         } else {
    245             var completionTextNode = document.createTextNode(completionText);
    246             fullWordRange.insertNode(completionTextNode);
    247 
    248             if (completions.length > 1)
    249                 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
    250             else
    251                 finalSelectionRange.setStart(completionTextNode, completionText.length);
    252 
    253             finalSelectionRange.setEnd(completionTextNode, completionText.length);
    254         }
    255 
    256         selection.removeAllRanges();
    257         selection.addRange(finalSelectionRange);
    258     },
    259 
    260     isCaretInsidePrompt: function()
    261     {
    262         return this.element.isInsertionCaretInside();
    263     },
    264 
    265     isCaretAtEndOfPrompt: function()
    266     {
    267         var selection = window.getSelection();
    268         if (!selection.rangeCount || !selection.isCollapsed)
    269             return false;
    270 
    271         var selectionRange = selection.getRangeAt(0);
    272         var node = selectionRange.startContainer;
    273         if (node !== this.element && !node.isDescendant(this.element))
    274             return false;
    275 
    276         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
    277             return false;
    278 
    279         var foundNextText = false;
    280         while (node) {
    281             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
    282                 if (foundNextText)
    283                     return false;
    284                 foundNextText = true;
    285             }
    286 
    287             node = node.traverseNextNode(this.element);
    288         }
    289 
    290         return true;
    291     },
    292 
    293     isCaretOnFirstLine: function()
    294     {
    295         var selection = window.getSelection();
    296         var focusNode = selection.focusNode;
    297         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element)
    298             return true;
    299 
    300         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
    301             return false;
    302         focusNode = focusNode.previousSibling;
    303 
    304         while (focusNode) {
    305             if (focusNode.nodeType !== Node.TEXT_NODE)
    306                 return true;
    307             if (focusNode.textContent.indexOf("\n") !== -1)
    308                 return false;
    309             focusNode = focusNode.previousSibling;
    310         }
    311 
    312         return true;
    313     },
    314 
    315     isCaretOnLastLine: function()
    316     {
    317         var selection = window.getSelection();
    318         var focusNode = selection.focusNode;
    319         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element)
    320             return true;
    321 
    322         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
    323             return false;
    324         focusNode = focusNode.nextSibling;
    325 
    326         while (focusNode) {
    327             if (focusNode.nodeType !== Node.TEXT_NODE)
    328                 return true;
    329             if (focusNode.textContent.indexOf("\n") !== -1)
    330                 return false;
    331             focusNode = focusNode.nextSibling;
    332         }
    333 
    334         return true;
    335     },
    336 
    337     moveCaretToEndOfPrompt: function()
    338     {
    339         var selection = window.getSelection();
    340         var selectionRange = document.createRange();
    341 
    342         var offset = this.element.childNodes.length;
    343         selectionRange.setStart(this.element, offset);
    344         selectionRange.setEnd(this.element, offset);
    345 
    346         selection.removeAllRanges();
    347         selection.addRange(selectionRange);
    348     },
    349 
    350     _tabKeyPressed: function(event)
    351     {
    352         event.preventDefault();
    353         event.stopPropagation();
    354 
    355         this.complete();
    356     },
    357 
    358     _upKeyPressed: function(event)
    359     {
    360         if (!this.isCaretOnFirstLine())
    361             return;
    362 
    363         event.preventDefault();
    364         event.stopPropagation();
    365 
    366         this._moveBackInHistory();
    367     },
    368 
    369     _downKeyPressed: function(event)
    370     {
    371         if (!this.isCaretOnLastLine())
    372             return;
    373 
    374         event.preventDefault();
    375         event.stopPropagation();
    376 
    377         this._moveForwardInHistory();
    378     },
    379 
    380     _moveBackInHistory: function()
    381     {
    382         if (this.historyOffset == this.history.length)
    383             return;
    384 
    385         this.clearAutoComplete(true);
    386 
    387         if (this.historyOffset === 0)
    388             this.tempSavedCommand = this.text;
    389 
    390         ++this.historyOffset;
    391         this.text = this.history[this.history.length - this.historyOffset];
    392 
    393         this.element.scrollIntoViewIfNeeded();
    394         var firstNewlineIndex = this.text.indexOf("\n");
    395         if (firstNewlineIndex === -1)
    396             this.moveCaretToEndOfPrompt();
    397         else {
    398             var selection = window.getSelection();
    399             var selectionRange = document.createRange();
    400 
    401             selectionRange.setStart(this.element.firstChild, firstNewlineIndex);
    402             selectionRange.setEnd(this.element.firstChild, firstNewlineIndex);
    403 
    404             selection.removeAllRanges();
    405             selection.addRange(selectionRange);
    406         }
    407     },
    408 
    409     _moveForwardInHistory: function()
    410     {
    411         if (this.historyOffset === 0)
    412             return;
    413 
    414         this.clearAutoComplete(true);
    415 
    416         --this.historyOffset;
    417 
    418         if (this.historyOffset === 0) {
    419             this.text = this.tempSavedCommand;
    420             delete this.tempSavedCommand;
    421             return;
    422         }
    423 
    424         this.text = this.history[this.history.length - this.historyOffset];
    425         this.element.scrollIntoViewIfNeeded();
    426     }
    427 }
    428