Restore the user entered text when clearing the auto-completion.
[WebKit-https.git] / WebCore / page / inspector / TextPrompt.js
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 }
37
38 WebInspector.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                 if (!this.acceptAutoComplete())
70                     this.autoCompleteSoon();
71                 break;
72             default:
73                 this.clearAutoComplete();
74                 this.autoCompleteSoon();
75                 break;
76         }
77     },
78
79     acceptAutoComplete: function()
80     {
81         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
82             return false;
83
84         var text = this.autoCompleteElement.textContent;
85         var textNode = document.createTextNode(text);
86         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
87         delete this.autoCompleteElement;
88
89         var finalSelectionRange = document.createRange();
90         finalSelectionRange.setStart(textNode, text.length);
91         finalSelectionRange.setEnd(textNode, text.length);
92
93         var selection = window.getSelection();
94         selection.removeAllRanges();
95         selection.addRange(finalSelectionRange);
96
97         return true;
98     },
99
100     clearAutoComplete: function(includeTimeout)
101     {
102         if (includeTimeout && "_completeTimeout" in this) {
103             clearTimeout(this._completeTimeout);
104             delete this._completeTimeout;
105         }
106
107         if (!this.autoCompleteElement)
108             return;
109
110         if (this.autoCompleteElement.parentNode)
111             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
112         delete this.autoCompleteElement;
113
114         if (!this._userEnteredRange || !this._userEnteredText)
115             return;
116
117         this._userEnteredRange.deleteContents();
118
119         var userTextNode = document.createTextNode(this._userEnteredText);
120         this._userEnteredRange.insertNode(userTextNode);           
121
122         var selectionRange = document.createRange();
123         selectionRange.setStart(userTextNode, this._userEnteredText.length);
124         selectionRange.setEnd(userTextNode, this._userEnteredText.length);
125
126         var selection = window.getSelection();
127         selection.removeAllRanges();
128         selection.addRange(selectionRange);
129
130         delete this._userEnteredRange;
131         delete this._userEnteredText;
132     },
133
134     autoCompleteSoon: function()
135     {
136         if (!("_completeTimeout" in this))
137             this._completeTimeout = setTimeout(this.complete.bind(this, true), 250);
138     },
139
140     complete: function(auto)
141     {
142         this.clearAutoComplete(true);
143
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
154         var wordPrefixRange = this.scanBackwards(this.completionStopCharacters, selectionRange.startContainer, selectionRange.startOffset, this.element);
155
156         var completions = this.completions(wordPrefixRange, auto);
157
158         if (!completions || !completions.length)
159             return;
160
161         var fullWordRange = document.createRange();
162         fullWordRange.setStart(wordPrefixRange.startContainer, wordPrefixRange.startOffset);
163         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
164
165         if (completions.length === 1 || selection.isCollapsed || auto) {
166             var completionText = completions[0];
167         } else {
168             var currentText = fullWordRange.toString();
169
170             var foundIndex = null;
171             for (var i = 0; i < completions.length; ++i) {
172                 if (completions[i] === currentText)
173                     foundIndex = i;
174             }
175
176             if (foundIndex === null || (foundIndex + 1) >= completions.length)
177                 var completionText = completions[0];
178             else
179                 var completionText = completions[foundIndex + 1];
180         }
181
182         var wordPrefixLength = wordPrefixRange.toString().length;
183
184         this._userEnteredRange = fullWordRange;
185         this._userEnteredText = fullWordRange.toString();
186
187         fullWordRange.deleteContents();
188
189         var finalSelectionRange = document.createRange();
190
191         if (auto) {
192             var prefixText = completionText.substring(0, wordPrefixLength);
193             var suffixText = completionText.substring(wordPrefixLength);
194
195             var prefixTextNode = document.createTextNode(prefixText);
196             fullWordRange.insertNode(prefixTextNode);           
197
198             this.autoCompleteElement = document.createElement("span");
199             this.autoCompleteElement.className = "auto-complete-text";
200             this.autoCompleteElement.textContent = suffixText;
201
202             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
203
204             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
205             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
206         } else {
207             var completionTextNode = document.createTextNode(completionText);
208             fullWordRange.insertNode(completionTextNode);           
209
210             if (completions.length > 1)
211                 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
212             else
213                 finalSelectionRange.setStart(completionTextNode, completionText.length);
214
215             finalSelectionRange.setEnd(completionTextNode, completionText.length);
216         }
217
218         selection.removeAllRanges();
219         selection.addRange(finalSelectionRange);
220     },
221
222     scanBackwards: function(stopCharacters, endNode, endOffset, stayWithinElement)
223     {
224         var startNode;
225         var startOffset = 0;
226         var node = endNode;
227
228         if (!stayWithinElement)
229             stayWithinElement = this.element;
230
231         while (node) {
232             if (node === stayWithinElement) {
233                 if (!startNode)
234                     startNode = stayWithinElement;
235                 break;
236             }
237
238             if (node.nodeType === Node.TEXT_NODE) {
239                 var start = (node === endNode ? endOffset : node.nodeValue.length);
240                 for (var i = (start - 1); i >= 0; --i) {
241                     var character = node.nodeValue[i];
242                     if (stopCharacters.indexOf(character) !== -1) {
243                         startNode = node;
244                         startOffset = i + 1;
245                         break;
246                     }
247                 }
248             }
249
250             if (startNode)
251                 break;
252
253             node = node.traversePreviousNode();
254         }
255
256         var result = document.createRange();
257         result.setStart(startNode, startOffset);
258         result.setEnd(endNode, endOffset);
259
260         return result;
261     },
262
263     isCaretInsidePrompt: function()
264     {
265         var selection = window.getSelection();
266         if (!selection.rangeCount || !selection.isCollapsed)
267             return false;
268         var selectionRange = selection.getRangeAt(0);
269         return selectionRange.startContainer === this.element || selectionRange.startContainer.isDescendant(this.element);
270     },
271
272     isCaretAtEndOfPrompt: function()
273     {
274         var selection = window.getSelection();
275         if (!selection.rangeCount || !selection.isCollapsed)
276             return false;
277
278         var selectionRange = selection.getRangeAt(0);
279         var node = selectionRange.startContainer;
280         if (node !== this.element && !node.isDescendant(this.element))
281             return false;
282
283         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
284             return false;
285
286         var foundNextText = false;
287         while (node) {
288             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
289                 if (foundNextText)
290                     return false;
291                 foundNextText = true;
292             }
293
294             node = node.traverseNextNode(false, this.element);
295         }
296
297         return true;
298     },
299
300     moveCaretToEndOfPrompt: function()
301     {
302         var selection = window.getSelection();
303         var selectionRange = document.createRange();
304
305         var offset = this.element.childNodes.length;
306         selectionRange.setStart(this.element, offset);
307         selectionRange.setEnd(this.element, offset);
308
309         selection.removeAllRanges();
310         selection.addRange(selectionRange);
311     },
312
313     _tabKeyPressed: function(event)
314     {
315         event.preventDefault();
316         event.stopPropagation();
317
318         this.complete();
319     },
320
321     _upKeyPressed: function(event)
322     {
323         event.preventDefault();
324         event.stopPropagation();
325
326         if (this.historyOffset == this.history.length)
327             return;
328
329         this.clearAutoComplete(true);
330
331         if (this.historyOffset == 0)
332             this.tempSavedCommand = this.text;
333
334         ++this.historyOffset;
335         this.text = this.history[this.history.length - this.historyOffset];
336     },
337
338     _downKeyPressed: function(event)
339     {
340         event.preventDefault();
341         event.stopPropagation();
342
343         if (this.historyOffset == 0)
344             return;
345
346         this.clearAutoComplete(true);
347
348         --this.historyOffset;
349
350         if (this.historyOffset == 0) {
351             this.text = this.tempSavedCommand;
352             delete this.tempSavedCommand;
353             return;
354         }
355
356         this.text = this.history[this.history.length - this.historyOffset];
357     }
358 }