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