• 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)
30{
31    this.element = element;
32    this.completions = completions;
33    this.completionStopCharacters = stopCharacters;
34    this.history = [];
35    this.historyOffset = 0;
36}
37
38WebInspector.TextPrompt.prototype = {
39    get text()
40    {
41        return this.element.textContent;
42    },
43
44    set text(x)
45    {
46        if (!x) {
47            // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
48            this.element.removeChildren();
49            this.element.appendChild(document.createElement("br"));
50        } else
51            this.element.textContent = x;
52
53        this.moveCaretToEndOfPrompt();
54    },
55
56    handleKeyEvent: function(event)
57    {
58        switch (event.keyIdentifier) {
59            case "Up":
60                this._upKeyPressed(event);
61                break;
62            case "Down":
63                this._downKeyPressed(event);
64                break;
65            case "U+0009": // Tab
66                this._tabKeyPressed(event);
67                break;
68            case "Right":
69            case "End":
70                if (!this.acceptAutoComplete())
71                    this.autoCompleteSoon();
72                break;
73            default:
74                this.clearAutoComplete();
75                this.autoCompleteSoon();
76                break;
77        }
78    },
79
80    acceptAutoComplete: function()
81    {
82        if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
83            return false;
84
85        var text = this.autoCompleteElement.textContent;
86        var textNode = document.createTextNode(text);
87        this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
88        delete this.autoCompleteElement;
89
90        var finalSelectionRange = document.createRange();
91        finalSelectionRange.setStart(textNode, text.length);
92        finalSelectionRange.setEnd(textNode, text.length);
93
94        var selection = window.getSelection();
95        selection.removeAllRanges();
96        selection.addRange(finalSelectionRange);
97
98        return true;
99    },
100
101    clearAutoComplete: function(includeTimeout)
102    {
103        if (includeTimeout && "_completeTimeout" in this) {
104            clearTimeout(this._completeTimeout);
105            delete this._completeTimeout;
106        }
107
108        if (!this.autoCompleteElement)
109            return;
110
111        if (this.autoCompleteElement.parentNode)
112            this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
113        delete this.autoCompleteElement;
114
115        if (!this._userEnteredRange || !this._userEnteredText)
116            return;
117
118        this._userEnteredRange.deleteContents();
119
120        var userTextNode = document.createTextNode(this._userEnteredText);
121        this._userEnteredRange.insertNode(userTextNode);
122
123        var selectionRange = document.createRange();
124        selectionRange.setStart(userTextNode, this._userEnteredText.length);
125        selectionRange.setEnd(userTextNode, this._userEnteredText.length);
126
127        var selection = window.getSelection();
128        selection.removeAllRanges();
129        selection.addRange(selectionRange);
130
131        delete this._userEnteredRange;
132        delete this._userEnteredText;
133    },
134
135    autoCompleteSoon: function()
136    {
137        if (!("_completeTimeout" in this))
138            this._completeTimeout = setTimeout(this.complete.bind(this, true), 250);
139    },
140
141    complete: function(auto)
142    {
143        this.clearAutoComplete(true);
144        var selection = window.getSelection();
145        if (!selection.rangeCount)
146            return;
147
148        var selectionRange = selection.getRangeAt(0);
149        if (!selectionRange.commonAncestorContainer.isDescendant(this.element))
150            return;
151        if (auto && !this.isCaretAtEndOfPrompt())
152            return;
153        var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward");
154        this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange));
155    },
156
157    _completionsReady: function(selection, auto, originalWordPrefixRange, completions)
158    {
159        if (!completions || !completions.length)
160            return;
161
162        var selectionRange = selection.getRangeAt(0);
163
164        var fullWordRange = document.createRange();
165        fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
166        fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
167
168        if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
169            return;
170
171        if (completions.length === 1 || selection.isCollapsed || auto) {
172            var completionText = completions[0];
173        } else {
174            var currentText = fullWordRange.toString();
175
176            var foundIndex = null;
177            for (var i = 0; i < completions.length; ++i) {
178                if (completions[i] === currentText)
179                    foundIndex = i;
180            }
181
182            if (foundIndex === null || (foundIndex + 1) >= completions.length)
183                var completionText = completions[0];
184            else
185                var completionText = completions[foundIndex + 1];
186        }
187
188        var wordPrefixLength = originalWordPrefixRange.toString().length;
189
190        this._userEnteredRange = fullWordRange;
191        this._userEnteredText = fullWordRange.toString();
192
193        fullWordRange.deleteContents();
194
195        var finalSelectionRange = document.createRange();
196
197        if (auto) {
198            var prefixText = completionText.substring(0, wordPrefixLength);
199            var suffixText = completionText.substring(wordPrefixLength);
200
201            var prefixTextNode = document.createTextNode(prefixText);
202            fullWordRange.insertNode(prefixTextNode);
203
204            this.autoCompleteElement = document.createElement("span");
205            this.autoCompleteElement.className = "auto-complete-text";
206            this.autoCompleteElement.textContent = suffixText;
207
208            prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
209
210            finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
211            finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
212        } else {
213            var completionTextNode = document.createTextNode(completionText);
214            fullWordRange.insertNode(completionTextNode);
215
216            if (completions.length > 1)
217                finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
218            else
219                finalSelectionRange.setStart(completionTextNode, completionText.length);
220
221            finalSelectionRange.setEnd(completionTextNode, completionText.length);
222        }
223
224        selection.removeAllRanges();
225        selection.addRange(finalSelectionRange);
226    },
227
228    isCaretInsidePrompt: function()
229    {
230        return this.element.isInsertionCaretInside();
231    },
232
233    isCaretAtEndOfPrompt: function()
234    {
235        var selection = window.getSelection();
236        if (!selection.rangeCount || !selection.isCollapsed)
237            return false;
238
239        var selectionRange = selection.getRangeAt(0);
240        var node = selectionRange.startContainer;
241        if (node !== this.element && !node.isDescendant(this.element))
242            return false;
243
244        if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
245            return false;
246
247        var foundNextText = false;
248        while (node) {
249            if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
250                if (foundNextText)
251                    return false;
252                foundNextText = true;
253            }
254
255            node = node.traverseNextNode(false, this.element);
256        }
257
258        return true;
259    },
260
261    moveCaretToEndOfPrompt: function()
262    {
263        var selection = window.getSelection();
264        var selectionRange = document.createRange();
265
266        var offset = this.element.childNodes.length;
267        selectionRange.setStart(this.element, offset);
268        selectionRange.setEnd(this.element, offset);
269
270        selection.removeAllRanges();
271        selection.addRange(selectionRange);
272    },
273
274    _tabKeyPressed: function(event)
275    {
276        event.preventDefault();
277        event.stopPropagation();
278
279        this.complete();
280    },
281
282    _upKeyPressed: function(event)
283    {
284        event.preventDefault();
285        event.stopPropagation();
286
287        if (this.historyOffset == this.history.length)
288            return;
289
290        this.clearAutoComplete(true);
291
292        if (this.historyOffset == 0)
293            this.tempSavedCommand = this.text;
294
295        ++this.historyOffset;
296        this.text = this.history[this.history.length - this.historyOffset];
297    },
298
299    _downKeyPressed: function(event)
300    {
301        event.preventDefault();
302        event.stopPropagation();
303
304        if (this.historyOffset == 0)
305            return;
306
307        this.clearAutoComplete(true);
308
309        --this.historyOffset;
310
311        if (this.historyOffset == 0) {
312            this.text = this.tempSavedCommand;
313            delete this.tempSavedCommand;
314            return;
315        }
316
317        this.text = this.history[this.history.length - this.historyOffset];
318    }
319}
320