• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, 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
43WebInspector.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