• 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    this.element.addEventListener("keydown", this._onKeyDown.bind(this), true);
37}
38
39WebInspector.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