[GTK] Enable CSS Variables feature in development builds
[WebKit-https.git] / Source / WebCore / inspector / front-end / TextPrompt.js
1 /*
2  * Copyright (C) 2008 Apple Inc.  All rights reserved.
3  * Copyright (C) 2011 Google Inc.  All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer.
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 /**
31  * @constructor
32  * @extends WebInspector.Object
33  * @param {function(Element, Range, boolean, function(Array.<string>, number=))} completions
34  * @param {string=} stopCharacters
35  */
36 WebInspector.TextPrompt = function(completions, stopCharacters)
37 {
38     /**
39      * @type {Element|undefined}
40      */
41     this._proxyElement;
42     this._proxyElementDisplay = "inline-block";
43     this._loadCompletions = completions;
44     this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
45     this._suggestForceable = true;
46 }
47
48 WebInspector.TextPrompt.Events = {
49     ItemApplied: "text-prompt-item-applied",
50     ItemAccepted: "text-prompt-item-accepted"
51 };
52
53 WebInspector.TextPrompt.prototype = {
54     get proxyElement()
55     {
56         return this._proxyElement;
57     },
58
59     setSuggestForceable: function(x)
60     {
61         this._suggestForceable = x;
62     },
63
64     setSuggestBoxEnabled: function(className)
65     {
66         this._suggestBoxClassName = className;
67     },
68
69     renderAsBlock: function()
70     {
71         this._proxyElementDisplay = "block";
72     },
73
74     /**
75      * Clients should never attach any event listeners to the |element|. Instead,
76      * they should use the result of this method to attach listeners for bubbling events.
77      *
78      * @param {Element} element
79      */
80     attach: function(element)
81     {
82         return this._attachInternal(element);
83     },
84
85     /**
86      * Clients should never attach any event listeners to the |element|. Instead,
87      * they should use the result of this method to attach listeners for bubbling events
88      * or the |blurListener| parameter to register a "blur" event listener on the |element|
89      * (since the "blur" event does not bubble.)
90      *
91      * @param {Element} element
92      * @param {function(Event)} blurListener
93      */
94     attachAndStartEditing: function(element, blurListener)
95     {
96         this._attachInternal(element);
97         this._startEditing(blurListener);
98         return this.proxyElement;
99     },
100
101     _attachInternal: function(element)
102     {
103         if (this.proxyElement)
104             throw "Cannot attach an attached TextPrompt";
105         this._element = element;
106
107         this._boundOnKeyDown = this.onKeyDown.bind(this);
108         this._boundOnMouseWheel = this.onMouseWheel.bind(this);
109         this._boundSelectStart = this._selectStart.bind(this);
110         this._proxyElement = element.ownerDocument.createElement("span");
111         this._proxyElement.style.display = this._proxyElementDisplay;
112         element.parentElement.insertBefore(this.proxyElement, element);
113         this.proxyElement.appendChild(element);
114         this._element.addStyleClass("text-prompt");
115         this._element.addEventListener("keydown", this._boundOnKeyDown, false);
116         this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
117         this._element.addEventListener("selectstart", this._boundSelectStart, false);
118
119         if (typeof this._suggestBoxClassName === "string")
120             this._suggestBox = new WebInspector.TextPrompt.SuggestBox(this, this._element, this._suggestBoxClassName);
121
122         return this.proxyElement;
123     },
124
125     detach: function()
126     {
127         this._removeFromElement();
128         this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement);
129         this.proxyElement.parentElement.removeChild(this.proxyElement);
130         this._element.removeStyleClass("text-prompt");
131         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
132         this._element.removeEventListener("mousewheel", this._boundOnMouseWheel, false);
133         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
134         delete this._proxyElement;
135         WebInspector.restoreFocusFromElement(this._element);
136     },
137
138     get text()
139     {
140         return this._element.textContent;
141     },
142
143     set text(x)
144     {
145         this._removeSuggestionAids();
146         if (!x) {
147             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
148             this._element.removeChildren();
149             this._element.appendChild(document.createElement("br"));
150         } else
151             this._element.textContent = x;
152
153         this.moveCaretToEndOfPrompt();
154         this._element.scrollIntoView();
155     },
156
157     _removeFromElement: function()
158     {
159         this.clearAutoComplete(true);
160         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
161         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
162         if (this._isEditing)
163             this._stopEditing();
164         if (this._suggestBox)
165             this._suggestBox.removeFromElement();
166     },
167
168     _startEditing: function(blurListener)
169     {
170         this._isEditing = true;
171         this._element.addStyleClass("editing");
172         if (blurListener) {
173             this._blurListener = blurListener;
174             this._element.addEventListener("blur", this._blurListener, false);
175         }
176         this._oldTabIndex = this._element.tabIndex;
177         if (this._element.tabIndex < 0)
178             this._element.tabIndex = 0;
179         WebInspector.setCurrentFocusElement(this._element);
180     },
181
182     _stopEditing: function()
183     {
184         this._element.tabIndex = this._oldTabIndex;
185         if (this._blurListener)
186             this._element.removeEventListener("blur", this._blurListener, false);
187         this._element.removeStyleClass("editing");
188         delete this._isEditing;
189     },
190
191     _removeSuggestionAids: function()
192     {
193         this.clearAutoComplete();
194         this.hideSuggestBox();
195     },
196
197     _selectStart: function(event)
198     {
199         if (this._selectionTimeout)
200             clearTimeout(this._selectionTimeout);
201
202         this._removeSuggestionAids();
203
204         function moveBackIfOutside()
205         {
206             delete this._selectionTimeout;
207             if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
208                 this.moveCaretToEndOfPrompt();
209                 this.autoCompleteSoon();
210             }
211         }
212
213         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
214     },
215
216     /**
217      * @param {boolean=} force
218      */
219     defaultKeyHandler: function(event, force)
220     {
221         this.clearAutoComplete();
222         this.autoCompleteSoon(force);
223         return false;
224     },
225
226     onMouseWheel: function(event)
227     {
228         // Subclasses can implement. 
229     },
230
231     onKeyDown: function(event)
232     {
233         var handled = false;
234         var invokeDefault = true;
235
236         switch (event.keyIdentifier) {
237         case "Up":
238             handled = this.upKeyPressed(event);
239             break;
240         case "Down":
241             handled = this.downKeyPressed(event);
242             break;
243         case "PageUp":
244             handled = this.pageUpKeyPressed(event);
245             break;
246         case "PageDown":
247             handled = this.pageDownKeyPressed(event);
248             break;
249         case "U+0009": // Tab
250             handled = this.tabKeyPressed(event);
251             break;
252         case "Enter":
253             handled = this.enterKeyPressed(event);
254             break;
255         case "Left":
256         case "Home":
257             this._removeSuggestionAids();
258             invokeDefault = false;
259             break;
260         case "Right":
261         case "End":
262             if (this.isCaretAtEndOfPrompt())
263                 handled = this.acceptAutoComplete();
264             else
265                 this._removeSuggestionAids();
266             invokeDefault = false;
267             break;
268         case "U+001B": // Esc
269             if (this.isSuggestBoxVisible()) {
270                 this._suggestBox.hide();
271                 handled = true;
272             }
273             break;
274         case "U+0020": // Space
275             if (this._suggestForceable && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
276                 this.defaultKeyHandler(event, true);
277                 handled = true;
278             }
279             break;
280         case "Alt":
281         case "Meta":
282         case "Shift":
283         case "Control":
284             invokeDefault = false;
285             break;
286         }
287
288         if (!handled && invokeDefault)
289             handled = this.defaultKeyHandler(event);
290
291         if (handled)
292             event.consume(true);
293
294         return handled;
295     },
296
297     acceptAutoComplete: function()
298     {
299         var result = false;
300         if (this.isSuggestBoxVisible())
301             result = this._suggestBox.acceptSuggestion();
302         if (!result)
303             result = this.acceptSuggestion();
304
305         return result;
306     },
307
308     /**
309      * @param {boolean=} includeTimeout
310      */
311     clearAutoComplete: function(includeTimeout)
312     {
313         if (includeTimeout && this._completeTimeout) {
314             clearTimeout(this._completeTimeout);
315             delete this._completeTimeout;
316         }
317         delete this._waitingForCompletions;
318
319         if (!this.autoCompleteElement)
320             return;
321
322         if (this.autoCompleteElement.parentNode)
323             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
324         delete this.autoCompleteElement;
325
326         if (!this._userEnteredRange || !this._userEnteredText)
327             return;
328
329         this._userEnteredRange.deleteContents();
330         this._element.normalize();
331
332         var userTextNode = document.createTextNode(this._userEnteredText);
333         this._userEnteredRange.insertNode(userTextNode);
334
335         var selectionRange = document.createRange();
336         selectionRange.setStart(userTextNode, this._userEnteredText.length);
337         selectionRange.setEnd(userTextNode, this._userEnteredText.length);
338
339         var selection = window.getSelection();
340         selection.removeAllRanges();
341         selection.addRange(selectionRange);
342
343         delete this._userEnteredRange;
344         delete this._userEnteredText;
345     },
346
347     /**
348      * @param {boolean=} force
349      */
350     autoCompleteSoon: function(force)
351     {
352         var immediately = this.isSuggestBoxVisible() || force;
353         if (!this._completeTimeout)
354             this._completeTimeout = setTimeout(this.complete.bind(this, true, force), immediately ? 0 : 250);
355     },
356
357     /**
358      * @param {boolean=} reverse
359      */
360     complete: function(auto, force, reverse)
361     {
362         this.clearAutoComplete(true);
363         var selection = window.getSelection();
364         if (!selection.rangeCount)
365             return;
366
367         var selectionRange = selection.getRangeAt(0);
368         var isEmptyInput = selectionRange.commonAncestorContainer === this._element; // this._element has no child Text nodes.
369
370         var shouldExit;
371
372         // Do not attempt to auto-complete an empty input in the auto mode (only on demand).
373         if (auto && isEmptyInput && !force)
374             shouldExit = true;
375         else if (!auto && !isEmptyInput && !selectionRange.commonAncestorContainer.isDescendant(this._element))
376             shouldExit = true;
377         else if (auto && !force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
378             shouldExit = true;
379         else if (!selection.isCollapsed)
380             shouldExit = true;
381         else if (!force) {
382             // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
383             var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
384             if (wordSuffixRange.toString().length)
385                 shouldExit = true;
386         }
387         if (shouldExit) {
388             this.hideSuggestBox();
389             return;
390         }
391
392         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
393         this._waitingForCompletions = true;
394         this._loadCompletions(this.proxyElement, wordPrefixRange, force, this._completionsReady.bind(this, selection, auto, wordPrefixRange, !!reverse));
395     },
396
397     _boxForAnchorAtStart: function(selection, textRange)
398     {
399         var rangeCopy = selection.getRangeAt(0).cloneRange();
400         var anchorElement = document.createElement("span");
401         anchorElement.textContent = "\u200B";
402         textRange.insertNode(anchorElement);
403         var box = anchorElement.boxInWindow(window);
404         anchorElement.parentElement.removeChild(anchorElement);
405         selection.removeAllRanges();
406         selection.addRange(rangeCopy);
407         return box;
408     },
409
410     /**
411      * @param {Array.<string>} completions
412      * @param {number} wordPrefixLength
413      */
414     _buildCommonPrefix: function(completions, wordPrefixLength)
415     {
416         var commonPrefix = completions[0];
417         for (var i = 0; i < completions.length; ++i) {
418             var completion = completions[i];
419             var lastIndex = Math.min(commonPrefix.length, completion.length);
420             for (var j = wordPrefixLength; j < lastIndex; ++j) {
421                 if (commonPrefix[j] !== completion[j]) {
422                     commonPrefix = commonPrefix.substr(0, j);
423                     break;
424                 }
425             }
426         }
427         return commonPrefix;
428     },
429
430     /**
431      * @param {Selection} selection
432      * @param {boolean} auto
433      * @param {Range} originalWordPrefixRange
434      * @param {boolean} reverse
435      * @param {Array.<string>=} completions
436      * @param {number=} selectedIndex
437      */
438     _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions, selectedIndex)
439     {
440         if (!this._waitingForCompletions || !completions || !completions.length) {
441             this.hideSuggestBox();
442             return;
443         }
444         delete this._waitingForCompletions;
445
446         var selectionRange = selection.getRangeAt(0);
447
448         var fullWordRange = document.createRange();
449         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
450         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
451
452         if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
453             return;
454
455         selectedIndex = selectedIndex || 0;
456
457         this._userEnteredRange = fullWordRange;
458         this._userEnteredText = fullWordRange.toString();
459
460         if (this._suggestBox)
461             this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt());
462
463         var wordPrefixLength = originalWordPrefixRange.toString().length;
464
465         if (auto) {
466             var completionText = completions[selectedIndex];
467             var commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
468
469             this._commonPrefix = commonPrefix;
470         } else {
471             if (completions.length === 1) {
472                 var completionText = completions[selectedIndex];
473                 wordPrefixLength = completionText.length;
474             } else {
475                 var commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
476                 wordPrefixLength = commonPrefix.length;
477
478                 if (selection.isCollapsed)
479                     var completionText = completions[selectedIndex];
480                 else {
481                     var currentText = fullWordRange.toString();
482
483                     var foundIndex = null;
484                     for (var i = 0; i < completions.length; ++i) {
485                         if (completions[i] === currentText)
486                             foundIndex = i;
487                     }
488
489                     var nextIndex = foundIndex + (reverse ? -1 : 1);
490                     if (foundIndex === null || nextIndex >= completions.length)
491                         var completionText = completions[selectedIndex];
492                     else if (nextIndex < 0)
493                         var completionText = completions[completions.length - 1];
494                     else
495                         var completionText = completions[nextIndex];
496                 }
497             }
498         }
499
500         if (auto) {
501             if (this.isCaretAtEndOfPrompt()) {
502                 this._userEnteredRange.deleteContents();
503                 this._element.normalize();
504                 var finalSelectionRange = document.createRange();
505                 var prefixText = completionText.substring(0, wordPrefixLength);
506                 var suffixText = completionText.substring(wordPrefixLength);
507
508                 var prefixTextNode = document.createTextNode(prefixText);
509                 fullWordRange.insertNode(prefixTextNode);
510
511                 this.autoCompleteElement = document.createElement("span");
512                 this.autoCompleteElement.className = "auto-complete-text";
513                 this.autoCompleteElement.textContent = suffixText;
514
515                 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
516
517                 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
518                 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
519                 selection.removeAllRanges();
520                 selection.addRange(finalSelectionRange);
521             }
522         } else
523             this.applySuggestion(completionText, completions.length > 1, originalWordPrefixRange);
524     },
525
526     _completeCommonPrefix: function()
527     {
528         if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
529             return;
530
531         if (!this.isSuggestBoxVisible()) {
532             this.acceptAutoComplete();
533             return;
534         }
535
536         this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
537         this.acceptSuggestion(true)
538     },
539
540     /**
541      * @param {Range=} originalPrefixRange
542      */
543     applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
544     {
545         var wordPrefixLength;
546         if (originalPrefixRange)
547             wordPrefixLength = originalPrefixRange.toString().length;
548         else
549             wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
550
551         this._userEnteredRange.deleteContents();
552         this._element.normalize();
553         var finalSelectionRange = document.createRange();
554         var completionTextNode = document.createTextNode(completionText);
555         this._userEnteredRange.insertNode(completionTextNode);
556         if (this.autoCompleteElement && this.autoCompleteElement.parentNode) {
557             this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
558             delete this.autoCompleteElement;
559         }
560
561         if (isIntermediateSuggestion)
562             finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
563         else
564             finalSelectionRange.setStart(completionTextNode, completionText.length);
565
566         finalSelectionRange.setEnd(completionTextNode, completionText.length);
567
568         var selection = window.getSelection();
569         selection.removeAllRanges();
570         selection.addRange(finalSelectionRange);
571         if (isIntermediateSuggestion)
572             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
573     },
574
575     /**
576      * @param {boolean=} prefixAccepted
577      */
578     acceptSuggestion: function(prefixAccepted)
579     {
580         if (this._isAcceptingSuggestion)
581             return false;
582
583         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
584             return false;
585
586         var text = this.autoCompleteElement.textContent;
587         var textNode = document.createTextNode(text);
588         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
589         delete this.autoCompleteElement;
590
591         var finalSelectionRange = document.createRange();
592         finalSelectionRange.setStart(textNode, text.length);
593         finalSelectionRange.setEnd(textNode, text.length);
594
595         var selection = window.getSelection();
596         selection.removeAllRanges();
597         selection.addRange(finalSelectionRange);
598
599         if (!prefixAccepted) {
600             this.hideSuggestBox();
601             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
602         } else
603             this.autoCompleteSoon(true);
604
605         return true;
606     },
607
608     hideSuggestBox: function()
609     {
610         if (this.isSuggestBoxVisible())
611             this._suggestBox.hide();
612     },
613
614     isSuggestBoxVisible: function()
615     {
616         return this._suggestBox && this._suggestBox.visible;
617     },
618
619     isCaretInsidePrompt: function()
620     {
621         return this._element.isInsertionCaretInside();
622     },
623
624     isCaretAtEndOfPrompt: function()
625     {
626         var selection = window.getSelection();
627         if (!selection.rangeCount || !selection.isCollapsed)
628             return false;
629
630         var selectionRange = selection.getRangeAt(0);
631         var node = selectionRange.startContainer;
632         if (!node.isSelfOrDescendant(this._element))
633             return false;
634
635         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
636             return false;
637
638         var foundNextText = false;
639         while (node) {
640             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
641                 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
642                     return false;
643                 foundNextText = true;
644             }
645
646             node = node.traverseNextNode(this._element);
647         }
648
649         return true;
650     },
651
652     isCaretOnFirstLine: function()
653     {
654         var selection = window.getSelection();
655         var focusNode = selection.focusNode;
656         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
657             return true;
658
659         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
660             return false;
661         focusNode = focusNode.previousSibling;
662
663         while (focusNode) {
664             if (focusNode.nodeType !== Node.TEXT_NODE)
665                 return true;
666             if (focusNode.textContent.indexOf("\n") !== -1)
667                 return false;
668             focusNode = focusNode.previousSibling;
669         }
670
671         return true;
672     },
673
674     isCaretOnLastLine: function()
675     {
676         var selection = window.getSelection();
677         var focusNode = selection.focusNode;
678         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
679             return true;
680
681         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
682             return false;
683         focusNode = focusNode.nextSibling;
684
685         while (focusNode) {
686             if (focusNode.nodeType !== Node.TEXT_NODE)
687                 return true;
688             if (focusNode.textContent.indexOf("\n") !== -1)
689                 return false;
690             focusNode = focusNode.nextSibling;
691         }
692
693         return true;
694     },
695
696     moveCaretToEndOfPrompt: function()
697     {
698         var selection = window.getSelection();
699         var selectionRange = document.createRange();
700
701         var offset = this._element.childNodes.length;
702         selectionRange.setStart(this._element, offset);
703         selectionRange.setEnd(this._element, offset);
704
705         selection.removeAllRanges();
706         selection.addRange(selectionRange);
707     },
708
709     tabKeyPressed: function(event)
710     {
711         this._completeCommonPrefix();
712
713         // Consume the key.
714         return true;
715     },
716
717     enterKeyPressed: function(event)
718     {
719         if (this.isSuggestBoxVisible())
720             return this._suggestBox.enterKeyPressed(event);
721
722         return false;
723     },
724
725     upKeyPressed: function(event)
726     {
727         if (this.isSuggestBoxVisible())
728             return this._suggestBox.upKeyPressed(event);
729
730         return false;
731     },
732
733     downKeyPressed: function(event)
734     {
735         if (this.isSuggestBoxVisible())
736             return this._suggestBox.downKeyPressed(event);
737
738         return false;
739     },
740
741     pageUpKeyPressed: function(event)
742     {
743         if (this.isSuggestBoxVisible())
744             return this._suggestBox.pageUpKeyPressed(event);
745
746         return false;
747     },
748
749     pageDownKeyPressed: function(event)
750     {
751         if (this.isSuggestBoxVisible())
752             return this._suggestBox.pageDownKeyPressed(event);
753
754         return false;
755     },
756
757     __proto__: WebInspector.Object.prototype
758 }
759
760
761 /**
762  * @constructor
763  * @extends {WebInspector.TextPrompt}
764  * @param {function(Element, Range, boolean, function(Array.<string>,number=))} completions
765  * @param {string=} stopCharacters
766  */
767 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
768 {
769     WebInspector.TextPrompt.call(this, completions, stopCharacters);
770
771     /**
772      * @type {Array.<string>}
773      */
774     this._data = [];
775
776     /**
777      * 1-based entry in the history stack.
778      * @type {number}
779      */
780     this._historyOffset = 1;
781
782     /**
783      * Whether to coalesce duplicate items in the history, default is true.
784      * @type {boolean}
785      */
786     this._coalesceHistoryDupes = true;
787 }
788
789 WebInspector.TextPromptWithHistory.prototype = {
790     get historyData()
791     {
792         // FIXME: do we need to copy this?
793         return this._data;
794     },
795
796     setCoalesceHistoryDupes: function(x)
797     {
798         this._coalesceHistoryDupes = x;
799     },
800
801     /**
802      * @param {Array.<string>} data
803      */
804     setHistoryData: function(data)
805     {
806         this._data = [].concat(data);
807         this._historyOffset = 1;
808     },
809
810     /**
811      * Pushes a committed text into the history.
812      * @param {string} text
813      */
814     pushHistoryItem: function(text)
815     {
816         if (this._uncommittedIsTop) {
817             this._data.pop();
818             delete this._uncommittedIsTop;
819         }
820
821         this._historyOffset = 1;
822         if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
823             return;
824         this._data.push(text);
825     },
826
827     /**
828      * Pushes the current (uncommitted) text into the history.
829      */
830     _pushCurrentText: function()
831     {
832         if (this._uncommittedIsTop)
833             this._data.pop(); // Throw away obsolete uncommitted text.
834         this._uncommittedIsTop = true;
835         this.clearAutoComplete(true);
836         this._data.push(this.text);
837     },
838
839     /**
840      * @return {string|undefined}
841      */
842     _previous: function()
843     {
844         if (this._historyOffset > this._data.length)
845             return undefined;
846         if (this._historyOffset === 1)
847             this._pushCurrentText();
848         ++this._historyOffset;
849         return this._currentHistoryItem();
850     },
851
852     /**
853      * @return {string|undefined}
854      */
855     _next: function()
856     {
857         if (this._historyOffset === 1)
858             return undefined;
859         --this._historyOffset;
860         return this._currentHistoryItem();
861     },
862
863     _currentHistoryItem: function()
864     {
865         return this._data[this._data.length - this._historyOffset];
866     },
867
868     /**
869      * @override
870      */
871     defaultKeyHandler: function(event, force)
872     {
873         var newText;
874         var isPrevious;
875
876         switch (event.keyIdentifier) {
877         case "Up":
878             if (!this.isCaretOnFirstLine())
879                 break;
880             newText = this._previous();
881             isPrevious = true;
882             break;
883         case "Down":
884             if (!this.isCaretOnLastLine())
885                 break;
886             newText = this._next();
887             break;
888         case "U+0050": // Ctrl+P = Previous
889             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
890                 newText = this._previous();
891                 isPrevious = true;
892             }
893             break;
894         case "U+004E": // Ctrl+N = Next
895             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
896                 newText = this._next();
897             break;
898         }
899
900         if (newText !== undefined) {
901             event.consume(true);
902             this.text = newText;
903
904             if (isPrevious) {
905                 var firstNewlineIndex = this.text.indexOf("\n");
906                 if (firstNewlineIndex === -1)
907                     this.moveCaretToEndOfPrompt();
908                 else {
909                     var selection = window.getSelection();
910                     var selectionRange = document.createRange();
911
912                     selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
913                     selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
914
915                     selection.removeAllRanges();
916                     selection.addRange(selectionRange);
917                 }
918             }
919
920             return true;
921         }
922
923         return WebInspector.TextPrompt.prototype.defaultKeyHandler.apply(this, arguments);
924     },
925
926     __proto__: WebInspector.TextPrompt.prototype
927 }
928
929 /**
930  * @constructor
931  */
932 WebInspector.TextPrompt.SuggestBox = function(textPrompt, inputElement, className)
933 {
934     this._textPrompt = textPrompt;
935     this._inputElement = inputElement;
936     this._length = 0;
937     this._selectedIndex = -1;
938     this._selectedElement = null;
939     this._boundOnScroll = this._onscrollresize.bind(this, true);
940     this._boundOnResize = this._onscrollresize.bind(this, false);
941     window.addEventListener("scroll", this._boundOnScroll, true);
942     window.addEventListener("resize", this._boundOnResize, true);
943
944     this._bodyElement = inputElement.ownerDocument.body;
945     this._element = inputElement.ownerDocument.createElement("div");
946     this._element.className = "suggest-box " + (className || "");
947     this._element.addEventListener("mousedown", this._onboxmousedown.bind(this), true);
948     this.containerElement = this._element.createChild("div", "container");
949     this.contentElement = this.containerElement.createChild("div", "content");
950 }
951
952 WebInspector.TextPrompt.SuggestBox.prototype = {
953     get visible()
954     {
955         return !!this._element.parentElement;
956     },
957
958     get hasSelection()
959     {
960         return !!this._selectedElement;
961     },
962
963     _onscrollresize: function(isScroll, event)
964     {
965         if (isScroll && this._element.isAncestor(event.target) || !this.visible)
966             return;
967         this._updateBoxPositionWithExistingAnchor();
968     },
969
970     _updateBoxPositionWithExistingAnchor: function()
971     {
972         this._updateBoxPosition(this._anchorBox);
973     },
974
975     /**
976      * @param {AnchorBox} anchorBox
977      */
978     _updateBoxPosition: function(anchorBox)
979     {
980         // Measure the content element box.
981         this.contentElement.style.display = "inline-block";
982         document.body.appendChild(this.contentElement);
983         this.contentElement.positionAt(0, 0);
984         var contentWidth = this.contentElement.offsetWidth;
985         var contentHeight = this.contentElement.offsetHeight;
986         this.contentElement.style.display = "block";
987         this.containerElement.appendChild(this.contentElement);
988
989         // Lay out the suggest-box relative to the anchorBox.
990         this._anchorBox = anchorBox;
991         const spacer = 6;
992
993         const suggestBoxPaddingX = 21;
994         var maxWidth = document.body.offsetWidth - anchorBox.x - spacer;
995         var width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
996         var paddedWidth = contentWidth + suggestBoxPaddingX;
997         var boxX = anchorBox.x;
998         if (width < paddedWidth) {
999             // Shift the suggest box to the left to accommodate the content without trimming to the BODY edge.
1000             maxWidth = document.body.offsetWidth - spacer;
1001             width = Math.min(contentWidth, maxWidth - suggestBoxPaddingX) + suggestBoxPaddingX;
1002             boxX = document.body.offsetWidth - width;
1003         }
1004
1005         const suggestBoxPaddingY = 2;
1006         var boxY;
1007         var aboveHeight = anchorBox.y;
1008         var underHeight = document.body.offsetHeight - anchorBox.y - anchorBox.height;
1009         var maxHeight = Math.max(underHeight, aboveHeight) - spacer;
1010         var height = Math.min(contentHeight, maxHeight - suggestBoxPaddingY) + suggestBoxPaddingY;
1011         if (underHeight >= aboveHeight) {
1012             // Locate the suggest box under the anchorBox.
1013             boxY = anchorBox.y + anchorBox.height;
1014             this._element.removeStyleClass("above-anchor");
1015             this._element.addStyleClass("under-anchor");
1016         } else {
1017             // Locate the suggest box above the anchorBox.
1018             boxY = anchorBox.y - height;
1019             this._element.removeStyleClass("under-anchor");
1020             this._element.addStyleClass("above-anchor");
1021         }
1022
1023         this._element.positionAt(boxX, boxY);
1024         this._element.style.width = width + "px";
1025         this._element.style.height = height + "px";
1026     },
1027
1028     _onboxmousedown: function(event)
1029     {
1030         event.preventDefault();
1031     },
1032
1033     hide: function()
1034     {
1035         if (!this.visible)
1036             return;
1037
1038         this._element.parentElement.removeChild(this._element);
1039         delete this._selectedElement;
1040     },
1041
1042     removeFromElement: function()
1043     {
1044         window.removeEventListener("scroll", this._boundOnScroll, true);
1045         window.removeEventListener("resize", this._boundOnResize, true);
1046         this.hide();
1047     },
1048
1049     /**
1050      * @param {string=} text
1051      * @param {boolean=} isIntermediateSuggestion
1052      */
1053     _applySuggestion: function(text, isIntermediateSuggestion)
1054     {
1055         if (!this.visible || !(text || this._selectedElement))
1056             return false;
1057
1058         var suggestion = text || this._selectedElement.textContent;
1059         if (!suggestion)
1060             return false;
1061
1062         this._textPrompt.applySuggestion(suggestion, isIntermediateSuggestion);
1063         return true;
1064     },
1065
1066     /**
1067      * @param {string=} text
1068      */
1069     acceptSuggestion: function(text)
1070     {
1071         var result = this._applySuggestion(text, false);
1072         this.hide();
1073         if (!result)
1074             return false;
1075
1076         this._textPrompt.acceptSuggestion();
1077
1078         return true;
1079     },
1080
1081     /**
1082      * @param {number} shift
1083      * @param {boolean=} isCircular
1084      * @return {boolean} is changed
1085      */
1086     _selectClosest: function(shift, isCircular)
1087     {
1088         if (!this._length)
1089             return false;
1090
1091         var index = this._selectedIndex + shift;
1092
1093         if (isCircular)
1094             index = (this._length + index) % this._length;
1095         else
1096             index = Number.constrain(index, 0, this._length - 1);
1097
1098         this._selectItem(index);
1099         this._applySuggestion(undefined, true);
1100         return true;
1101     },
1102
1103     /**
1104      * @param {AnchorBox} anchorBox
1105      * @param {Array.<string>=} completions
1106      * @param {number=} selectedIndex
1107      * @param {boolean=} canShowForSingleItem
1108      */
1109     updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
1110     {
1111         if (this._suggestTimeout) {
1112             clearTimeout(this._suggestTimeout);
1113             delete this._suggestTimeout;
1114         }
1115         this._completionsReady(anchorBox, completions, selectedIndex, canShowForSingleItem);
1116     },
1117
1118     _onItemMouseDown: function(text, event)
1119     {
1120         this.acceptSuggestion(text);
1121         event.consume(true);
1122     },
1123
1124     _createItemElement: function(prefix, text)
1125     {
1126         var element = document.createElement("div");
1127         element.className = "suggest-box-content-item source-code";
1128         element.tabIndex = -1;
1129         if (prefix && prefix.length && !text.indexOf(prefix)) {
1130             var prefixElement = element.createChild("span", "prefix");
1131             prefixElement.textContent = prefix;
1132             var suffixElement = element.createChild("span", "suffix");
1133             suffixElement.textContent = text.substring(prefix.length);
1134         } else {
1135             var suffixElement = element.createChild("span", "suffix");
1136             suffixElement.textContent = text;
1137         }
1138         element.addEventListener("mousedown", this._onItemMouseDown.bind(this, text), false);
1139         return element;
1140     },
1141
1142     /**
1143      * @param {Array.<string>=} items
1144      * @param {number=} selectedIndex
1145      */
1146     _updateItems: function(items, selectedIndex)
1147     {
1148         this._length = items.length;
1149         this.contentElement.removeChildren();
1150
1151         var userEnteredText = this._textPrompt._userEnteredText;
1152         for (var i = 0; i < items.length; ++i) {
1153             var item = items[i];
1154             var currentItemElement = this._createItemElement(userEnteredText, item);
1155             this.contentElement.appendChild(currentItemElement);
1156         }
1157
1158         this._selectedElement = null;
1159         if (typeof selectedIndex === "number")
1160             this._selectItem(selectedIndex);
1161     },
1162
1163     /**
1164      * @param {number} index
1165      */
1166     _selectItem: function(index)
1167     {
1168         if (this._selectedElement)
1169             this._selectedElement.classList.remove("selected");
1170
1171         this._selectedIndex = index;
1172         this._selectedElement = this.contentElement.children[index];
1173         this._selectedElement.classList.add("selected");
1174
1175         this._selectedElement.scrollIntoViewIfNeeded(false);
1176     },
1177
1178     /**
1179      * @param {Array.<string>=} completions
1180      * @param {boolean=} canShowForSingleItem
1181      */
1182     _canShowBox: function(completions, canShowForSingleItem)
1183     {
1184         if (!completions || !completions.length)
1185             return false;
1186
1187         if (completions.length > 1)
1188             return true;
1189
1190         // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
1191         return canShowForSingleItem && completions[0] !== this._textPrompt._userEnteredText;
1192     },
1193
1194     _rememberRowCountPerViewport: function()
1195     {
1196         if (!this.contentElement.firstChild)
1197             return;
1198
1199         this._rowCountPerViewport = Math.floor(this.containerElement.offsetHeight / this.contentElement.firstChild.offsetHeight);
1200     },
1201
1202     /**
1203      * @param {AnchorBox} anchorBox
1204      * @param {Array.<string>=} completions
1205      * @param {number=} selectedIndex
1206      * @param {boolean=} canShowForSingleItem
1207      */
1208     _completionsReady: function(anchorBox, completions, selectedIndex, canShowForSingleItem)
1209     {
1210         if (this._canShowBox(completions, canShowForSingleItem)) {
1211             this._updateItems(completions, selectedIndex);
1212             this._updateBoxPosition(anchorBox);
1213             if (!this.visible)
1214                 this._bodyElement.appendChild(this._element);
1215             this._rememberRowCountPerViewport();
1216         } else
1217             this.hide();
1218     },
1219
1220     upKeyPressed: function(event)
1221     {
1222         return this._selectClosest(-1, true);
1223     },
1224
1225     downKeyPressed: function(event)
1226     {
1227         return this._selectClosest(1, true);
1228     },
1229
1230     pageUpKeyPressed: function(event)
1231     {
1232         return this._selectClosest(-this._rowCountPerViewport, false);
1233     },
1234
1235     pageDownKeyPressed: function(event)
1236     {
1237         return this._selectClosest(this._rowCountPerViewport, false);
1238     },
1239
1240     enterKeyPressed: function(event)
1241     {
1242         var hasSelectedItem = !!this._selectedElement;
1243         this.acceptSuggestion();
1244
1245         // Report the event as non-handled if there is no selected item,
1246         // to commit the input or handle it otherwise.
1247         return hasSelectedItem;
1248     },
1249
1250     tabKeyPressed: function(event)
1251     {
1252         return this.enterKeyPressed(event);
1253     }
1254 }