2 * Copyright (C) 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2011 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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.
32 * @extends {WebInspector.Object}
33 * @implements {WebInspector.SuggestBoxDelegate}
34 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
35 * @param {string=} stopCharacters
37 WebInspector.TextPrompt = function(completions, stopCharacters)
40 * @type {!Element|undefined}
43 this._proxyElementDisplay = "inline-block";
44 this._loadCompletions = completions;
45 this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
48 WebInspector.TextPrompt.Events = {
49 ItemApplied: "text-prompt-item-applied",
50 ItemAccepted: "text-prompt-item-accepted"
53 WebInspector.TextPrompt.prototype = {
56 return this._proxyElement;
60 * @param {string} className
62 setSuggestBoxEnabled: function(className)
64 this._suggestBoxClassName = className;
67 renderAsBlock: function()
69 this._proxyElementDisplay = "block";
73 * Clients should never attach any event listeners to the |element|. Instead,
74 * they should use the result of this method to attach listeners for bubbling events.
76 * @param {!Element} element
79 attach: function(element)
81 return this._attachInternal(element);
85 * Clients should never attach any event listeners to the |element|. Instead,
86 * they should use the result of this method to attach listeners for bubbling events
87 * or the |blurListener| parameter to register a "blur" event listener on the |element|
88 * (since the "blur" event does not bubble.)
90 * @param {!Element} element
91 * @param {function(!Event)} blurListener
94 attachAndStartEditing: function(element, blurListener)
96 this._attachInternal(element);
97 this._startEditing(blurListener);
98 return this.proxyElement;
102 * @param {!Element} element
105 _attachInternal: function(element)
107 if (this.proxyElement)
108 throw "Cannot attach an attached TextPrompt";
109 this._element = element;
111 this._boundOnKeyDown = this.onKeyDown.bind(this);
112 this._boundOnMouseWheel = this.onMouseWheel.bind(this);
113 this._boundSelectStart = this._selectStart.bind(this);
114 this._boundHideSuggestBox = this.hideSuggestBox.bind(this);
115 this._proxyElement = element.ownerDocument.createElement("span");
116 this._proxyElement.style.display = this._proxyElementDisplay;
117 element.parentElement.insertBefore(this.proxyElement, element);
118 this.proxyElement.appendChild(element);
119 this._element.classList.add("text-prompt");
120 this._element.addEventListener("keydown", this._boundOnKeyDown, false);
121 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
122 this._element.addEventListener("selectstart", this._boundSelectStart, false);
123 this._element.addEventListener("blur", this._boundHideSuggestBox, false);
125 if (typeof this._suggestBoxClassName === "string")
126 this._suggestBox = new WebInspector.SuggestBox(this, this._element, this._suggestBoxClassName);
128 return this.proxyElement;
133 this._removeFromElement();
134 this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement);
135 this.proxyElement.remove();
136 delete this._proxyElement;
137 this._element.classList.remove("text-prompt");
138 this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
139 this._element.removeEventListener("mousewheel", this._boundOnMouseWheel, false);
140 this._element.removeEventListener("selectstart", this._boundSelectStart, false);
141 WebInspector.restoreFocusFromElement(this._element);
149 return this._element.textContent;
157 this._removeSuggestionAids();
159 // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
160 this._element.removeChildren();
161 this._element.appendChild(document.createElement("br"));
163 this._element.textContent = x;
165 this.moveCaretToEndOfPrompt();
166 this._element.scrollIntoView();
169 _removeFromElement: function()
171 this.clearAutoComplete(true);
172 this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
173 this._element.removeEventListener("selectstart", this._boundSelectStart, false);
174 this._element.removeEventListener("blur", this._boundHideSuggestBox, false);
177 if (this._suggestBox)
178 this._suggestBox.removeFromElement();
182 * @param {function(!Event)=} blurListener
184 _startEditing: function(blurListener)
186 this._isEditing = true;
187 this._element.classList.add("editing");
189 this._blurListener = blurListener;
190 this._element.addEventListener("blur", this._blurListener, false);
192 this._oldTabIndex = this._element.tabIndex;
193 if (this._element.tabIndex < 0)
194 this._element.tabIndex = 0;
195 WebInspector.setCurrentFocusElement(this._element);
197 this._updateAutoComplete();
200 _stopEditing: function()
202 this._element.tabIndex = this._oldTabIndex;
203 if (this._blurListener)
204 this._element.removeEventListener("blur", this._blurListener, false);
205 this._element.classList.remove("editing");
206 delete this._isEditing;
209 _removeSuggestionAids: function()
211 this.clearAutoComplete();
212 this.hideSuggestBox();
215 _selectStart: function()
217 if (this._selectionTimeout)
218 clearTimeout(this._selectionTimeout);
220 this._removeSuggestionAids();
223 * @this {WebInspector.TextPrompt}
225 function moveBackIfOutside()
227 delete this._selectionTimeout;
228 if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
229 this.moveCaretToEndOfPrompt();
230 this.autoCompleteSoon();
234 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
238 * @param {boolean=} force
241 defaultKeyHandler: function(event, force)
243 this._updateAutoComplete(force);
248 * @param {boolean=} force
250 _updateAutoComplete: function(force)
252 this.clearAutoComplete();
253 this.autoCompleteSoon(force);
257 * @param {?Event} event
259 onMouseWheel: function(event)
261 // Subclasses can implement.
265 * @param {?Event} event
268 onKeyDown: function(event)
271 var invokeDefault = true;
273 switch (event.keyIdentifier) {
274 case "U+0009": // Tab
275 handled = this.tabKeyPressed(event);
279 this._removeSuggestionAids();
280 invokeDefault = false;
284 if (this.isCaretAtEndOfPrompt())
285 handled = this.acceptAutoComplete();
287 this._removeSuggestionAids();
288 invokeDefault = false;
290 case "U+001B": // Esc
291 if (this.isSuggestBoxVisible()) {
292 this._removeSuggestionAids();
296 case "U+0020": // Space
297 if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
298 this.defaultKeyHandler(event, true);
306 invokeDefault = false;
310 if (!handled && this.isSuggestBoxVisible())
311 handled = this._suggestBox.keyPressed(event);
313 if (!handled && invokeDefault)
314 handled = this.defaultKeyHandler(event);
325 acceptAutoComplete: function()
328 if (this.isSuggestBoxVisible())
329 result = this._suggestBox.acceptSuggestion();
331 result = this._acceptSuggestionInternal();
337 * @param {boolean=} includeTimeout
339 clearAutoComplete: function(includeTimeout)
341 if (includeTimeout && this._completeTimeout) {
342 clearTimeout(this._completeTimeout);
343 delete this._completeTimeout;
345 delete this._waitingForCompletions;
347 if (!this.autoCompleteElement)
350 this.autoCompleteElement.remove();
351 delete this.autoCompleteElement;
353 if (!this._userEnteredRange || !this._userEnteredText)
356 this._userEnteredRange.deleteContents();
357 this._element.normalize();
359 var userTextNode = document.createTextNode(this._userEnteredText);
360 this._userEnteredRange.insertNode(userTextNode);
362 var selectionRange = document.createRange();
363 selectionRange.setStart(userTextNode, this._userEnteredText.length);
364 selectionRange.setEnd(userTextNode, this._userEnteredText.length);
366 var selection = window.getSelection();
367 selection.removeAllRanges();
368 selection.addRange(selectionRange);
370 delete this._userEnteredRange;
371 delete this._userEnteredText;
375 * @param {boolean=} force
377 autoCompleteSoon: function(force)
379 var immediately = this.isSuggestBoxVisible() || force;
380 if (!this._completeTimeout)
381 this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
385 * @param {boolean=} reverse
387 complete: function(force, reverse)
389 this.clearAutoComplete(true);
390 var selection = window.getSelection();
391 if (!selection.rangeCount)
394 var selectionRange = selection.getRangeAt(0);
397 if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
399 else if (!selection.isCollapsed)
402 // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
403 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
404 if (wordSuffixRange.toString().length)
408 this.hideSuggestBox();
412 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
413 this._waitingForCompletions = true;
414 this._loadCompletions(this.proxyElement, wordPrefixRange, force, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse));
417 disableDefaultSuggestionForEmptyInput: function()
419 this._disableDefaultSuggestionForEmptyInput = true;
423 * @param {!Selection} selection
424 * @param {!Range} textRange
426 _boxForAnchorAtStart: function(selection, textRange)
428 var rangeCopy = selection.getRangeAt(0).cloneRange();
429 var anchorElement = document.createElement("span");
430 anchorElement.textContent = "\u200B";
431 textRange.insertNode(anchorElement);
432 var box = anchorElement.boxInWindow(window);
433 anchorElement.remove();
434 selection.removeAllRanges();
435 selection.addRange(rangeCopy);
440 * @param {!Array.<string>} completions
441 * @param {number} wordPrefixLength
443 _buildCommonPrefix: function(completions, wordPrefixLength)
445 var commonPrefix = completions[0];
446 for (var i = 0; i < completions.length; ++i) {
447 var completion = completions[i];
448 var lastIndex = Math.min(commonPrefix.length, completion.length);
449 for (var j = wordPrefixLength; j < lastIndex; ++j) {
450 if (commonPrefix[j] !== completion[j]) {
451 commonPrefix = commonPrefix.substr(0, j);
460 * @param {!Selection} selection
461 * @param {!Range} originalWordPrefixRange
462 * @param {boolean} reverse
463 * @param {!Array.<string>} completions
464 * @param {number=} selectedIndex
466 _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
468 if (!this._waitingForCompletions || !completions.length) {
469 this.hideSuggestBox();
472 delete this._waitingForCompletions;
474 var selectionRange = selection.getRangeAt(0);
476 var fullWordRange = document.createRange();
477 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
478 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
480 if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString())
483 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0);
485 this._userEnteredRange = fullWordRange;
486 this._userEnteredText = fullWordRange.toString();
488 if (this._suggestBox)
489 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
491 if (selectedIndex === -1)
494 var wordPrefixLength = originalWordPrefixRange.toString().length;
495 this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
497 if (this.isCaretAtEndOfPrompt()) {
498 this._userEnteredRange.deleteContents();
499 this._element.normalize();
500 var finalSelectionRange = document.createRange();
501 var completionText = completions[selectedIndex];
502 var prefixText = completionText.substring(0, wordPrefixLength);
503 var suffixText = completionText.substring(wordPrefixLength);
505 var prefixTextNode = document.createTextNode(prefixText);
506 fullWordRange.insertNode(prefixTextNode);
508 this.autoCompleteElement = document.createElement("span");
509 this.autoCompleteElement.className = "auto-complete-text";
510 this.autoCompleteElement.textContent = suffixText;
512 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
514 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
515 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
516 selection.removeAllRanges();
517 selection.addRange(finalSelectionRange);
518 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied);
522 _completeCommonPrefix: function()
524 if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
527 if (!this.isSuggestBoxVisible()) {
528 this.acceptAutoComplete();
532 this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
533 this._acceptSuggestionInternal(true);
537 * @param {string} completionText
538 * @param {boolean=} isIntermediateSuggestion
540 applySuggestion: function(completionText, isIntermediateSuggestion)
542 this._applySuggestion(completionText, isIntermediateSuggestion);
546 * @param {string} completionText
547 * @param {boolean=} isIntermediateSuggestion
548 * @param {!Range=} originalPrefixRange
550 _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
552 var wordPrefixLength;
553 if (originalPrefixRange)
554 wordPrefixLength = originalPrefixRange.toString().length;
556 wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
558 this._userEnteredRange.deleteContents();
559 this._element.normalize();
560 var finalSelectionRange = document.createRange();
561 var completionTextNode = document.createTextNode(completionText);
562 this._userEnteredRange.insertNode(completionTextNode);
563 if (this.autoCompleteElement) {
564 this.autoCompleteElement.remove();
565 delete this.autoCompleteElement;
568 if (isIntermediateSuggestion)
569 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
571 finalSelectionRange.setStart(completionTextNode, completionText.length);
573 finalSelectionRange.setEnd(completionTextNode, completionText.length);
575 var selection = window.getSelection();
576 selection.removeAllRanges();
577 selection.addRange(finalSelectionRange);
578 if (isIntermediateSuggestion)
579 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
585 acceptSuggestion: function()
587 this._acceptSuggestionInternal();
591 * @param {boolean=} prefixAccepted
594 _acceptSuggestionInternal: function(prefixAccepted)
596 if (this._isAcceptingSuggestion)
599 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
602 var text = this.autoCompleteElement.textContent;
603 var textNode = document.createTextNode(text);
604 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
605 delete this.autoCompleteElement;
607 var finalSelectionRange = document.createRange();
608 finalSelectionRange.setStart(textNode, text.length);
609 finalSelectionRange.setEnd(textNode, text.length);
611 var selection = window.getSelection();
612 selection.removeAllRanges();
613 selection.addRange(finalSelectionRange);
615 if (!prefixAccepted) {
616 this.hideSuggestBox();
617 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
619 this.autoCompleteSoon(true);
624 hideSuggestBox: function()
626 if (this.isSuggestBoxVisible())
627 this._suggestBox.hide();
633 isSuggestBoxVisible: function()
635 return this._suggestBox && this._suggestBox.visible();
641 isCaretInsidePrompt: function()
643 return this._element.isInsertionCaretInside();
649 isCaretAtEndOfPrompt: function()
651 var selection = window.getSelection();
652 if (!selection.rangeCount || !selection.isCollapsed)
655 var selectionRange = selection.getRangeAt(0);
656 var node = selectionRange.startContainer;
657 if (!node.isSelfOrDescendant(this._element))
660 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
663 var foundNextText = false;
665 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
666 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
668 foundNextText = true;
671 node = node.traverseNextNode(this._element);
680 isCaretOnFirstLine: function()
682 var selection = window.getSelection();
683 var focusNode = selection.focusNode;
684 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
687 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
689 focusNode = focusNode.previousSibling;
692 if (focusNode.nodeType !== Node.TEXT_NODE)
694 if (focusNode.textContent.indexOf("\n") !== -1)
696 focusNode = focusNode.previousSibling;
705 isCaretOnLastLine: function()
707 var selection = window.getSelection();
708 var focusNode = selection.focusNode;
709 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
712 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
714 focusNode = focusNode.nextSibling;
717 if (focusNode.nodeType !== Node.TEXT_NODE)
719 if (focusNode.textContent.indexOf("\n") !== -1)
721 focusNode = focusNode.nextSibling;
727 moveCaretToEndOfPrompt: function()
729 var selection = window.getSelection();
730 var selectionRange = document.createRange();
732 var offset = this._element.childNodes.length;
733 selectionRange.setStart(this._element, offset);
734 selectionRange.setEnd(this._element, offset);
736 selection.removeAllRanges();
737 selection.addRange(selectionRange);
741 * @param {!Event} event
744 tabKeyPressed: function(event)
746 this._completeCommonPrefix();
752 __proto__: WebInspector.Object.prototype
758 * @extends {WebInspector.TextPrompt}
759 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
760 * @param {string=} stopCharacters
762 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
764 WebInspector.TextPrompt.call(this, completions, stopCharacters);
767 * @type {!Array.<string>}
772 * 1-based entry in the history stack.
775 this._historyOffset = 1;
778 * Whether to coalesce duplicate items in the history, default is true.
781 this._coalesceHistoryDupes = true;
784 WebInspector.TextPromptWithHistory.prototype = {
786 * @return {!Array.<string>}
790 // FIXME: do we need to copy this?
797 setCoalesceHistoryDupes: function(x)
799 this._coalesceHistoryDupes = x;
803 * @param {!Array.<string>} data
805 setHistoryData: function(data)
807 this._data = [].concat(data);
808 this._historyOffset = 1;
812 * Pushes a committed text into the history.
813 * @param {string} text
815 pushHistoryItem: function(text)
817 if (this._uncommittedIsTop) {
819 delete this._uncommittedIsTop;
822 this._historyOffset = 1;
823 if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
825 this._data.push(text);
829 * Pushes the current (uncommitted) text into the history.
831 _pushCurrentText: function()
833 if (this._uncommittedIsTop)
834 this._data.pop(); // Throw away obsolete uncommitted text.
835 this._uncommittedIsTop = true;
836 this.clearAutoComplete(true);
837 this._data.push(this.text);
841 * @return {string|undefined}
843 _previous: function()
845 if (this._historyOffset > this._data.length)
847 if (this._historyOffset === 1)
848 this._pushCurrentText();
849 ++this._historyOffset;
850 return this._currentHistoryItem();
854 * @return {string|undefined}
858 if (this._historyOffset === 1)
860 --this._historyOffset;
861 return this._currentHistoryItem();
865 * @return {string|undefined}
867 _currentHistoryItem: function()
869 return this._data[this._data.length - this._historyOffset];
876 defaultKeyHandler: function(event, force)
881 switch (event.keyIdentifier) {
883 if (!this.isCaretOnFirstLine())
885 newText = this._previous();
889 if (!this.isCaretOnLastLine())
891 newText = this._next();
893 case "U+0050": // Ctrl+P = Previous
894 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
895 newText = this._previous();
899 case "U+004E": // Ctrl+N = Next
900 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
901 newText = this._next();
905 if (newText !== undefined) {
910 var firstNewlineIndex = this.text.indexOf("\n");
911 if (firstNewlineIndex === -1)
912 this.moveCaretToEndOfPrompt();
914 var selection = window.getSelection();
915 var selectionRange = document.createRange();
917 selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
918 selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
920 selection.removeAllRanges();
921 selection.addRange(selectionRange);
928 return WebInspector.TextPrompt.prototype.defaultKeyHandler.apply(this, arguments);
931 __proto__: WebInspector.TextPrompt.prototype