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 * @implements {WebInspector.SuggestBoxDelegate} 34 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions 35 * @param {string=} stopCharacters 36 */ 37 WebInspector.TextPrompt = function(completions, stopCharacters) 38 { 39 /** 40 * @type {!Element|undefined} 41 */ 42 this._proxyElement; 43 this._proxyElementDisplay = "inline-block"; 44 this._loadCompletions = completions; 45 this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>."; 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 /** 60 * @param {string} className 61 */ 62 setSuggestBoxEnabled: function(className) 63 { 64 this._suggestBoxClassName = className; 65 }, 66 67 renderAsBlock: function() 68 { 69 this._proxyElementDisplay = "block"; 70 }, 71 72 /** 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. 75 * 76 * @param {!Element} element 77 */ 78 attach: function(element) 79 { 80 return this._attachInternal(element); 81 }, 82 83 /** 84 * Clients should never attach any event listeners to the |element|. Instead, 85 * they should use the result of this method to attach listeners for bubbling events 86 * or the |blurListener| parameter to register a "blur" event listener on the |element| 87 * (since the "blur" event does not bubble.) 88 * 89 * @param {!Element} element 90 * @param {function(!Event)} blurListener 91 */ 92 attachAndStartEditing: function(element, blurListener) 93 { 94 this._attachInternal(element); 95 this._startEditing(blurListener); 96 return this.proxyElement; 97 }, 98 99 /** 100 * @param {!Element} element 101 */ 102 _attachInternal: function(element) 103 { 104 if (this.proxyElement) 105 throw "Cannot attach an attached TextPrompt"; 106 this._element = element; 107 108 this._boundOnKeyDown = this.onKeyDown.bind(this); 109 this._boundOnMouseWheel = this.onMouseWheel.bind(this); 110 this._boundSelectStart = this._selectStart.bind(this); 111 this._proxyElement = element.ownerDocument.createElement("span"); 112 this._proxyElement.style.display = this._proxyElementDisplay; 113 element.parentElement.insertBefore(this.proxyElement, element); 114 this.proxyElement.appendChild(element); 115 this._element.classList.add("text-prompt"); 116 this._element.addEventListener("keydown", this._boundOnKeyDown, false); 117 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false); 118 this._element.addEventListener("selectstart", this._boundSelectStart, false); 119 120 if (typeof this._suggestBoxClassName === "string") 121 this._suggestBox = new WebInspector.SuggestBox(this, this._element, this._suggestBoxClassName); 122 123 return this.proxyElement; 124 }, 125 126 detach: function() 127 { 128 this._removeFromElement(); 129 this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement); 130 this.proxyElement.remove(); 131 delete this._proxyElement; 132 this._element.classList.remove("text-prompt"); 133 this._element.removeEventListener("keydown", this._boundOnKeyDown, false); 134 this._element.removeEventListener("mousewheel", this._boundOnMouseWheel, false); 135 this._element.removeEventListener("selectstart", this._boundSelectStart, false); 136 WebInspector.restoreFocusFromElement(this._element); 137 }, 138 139 /** 140 * @type {string} 141 */ 142 get text() 143 { 144 return this._element.textContent; 145 }, 146 147 /** 148 * @param {string} x 149 */ 150 set text(x) 151 { 152 this._removeSuggestionAids(); 153 if (!x) { 154 // Append a break element instead of setting textContent to make sure the selection is inside the prompt. 155 this._element.removeChildren(); 156 this._element.appendChild(document.createElement("br")); 157 } else 158 this._element.textContent = x; 159 160 this.moveCaretToEndOfPrompt(); 161 this._element.scrollIntoView(); 162 }, 163 164 _removeFromElement: function() 165 { 166 this.clearAutoComplete(true); 167 this._element.removeEventListener("keydown", this._boundOnKeyDown, false); 168 this._element.removeEventListener("selectstart", this._boundSelectStart, false); 169 if (this._isEditing) 170 this._stopEditing(); 171 if (this._suggestBox) 172 this._suggestBox.removeFromElement(); 173 }, 174 175 /** 176 * @param {function(!Event)=} blurListener 177 */ 178 _startEditing: function(blurListener) 179 { 180 this._isEditing = true; 181 this._element.classList.add("editing"); 182 if (blurListener) { 183 this._blurListener = blurListener; 184 this._element.addEventListener("blur", this._blurListener, false); 185 } 186 this._oldTabIndex = this._element.tabIndex; 187 if (this._element.tabIndex < 0) 188 this._element.tabIndex = 0; 189 WebInspector.setCurrentFocusElement(this._element); 190 if (!this.text) 191 this._updateAutoComplete(); 192 }, 193 194 _stopEditing: function() 195 { 196 this._element.tabIndex = this._oldTabIndex; 197 if (this._blurListener) 198 this._element.removeEventListener("blur", this._blurListener, false); 199 this._element.classList.remove("editing"); 200 delete this._isEditing; 201 }, 202 203 _removeSuggestionAids: function() 204 { 205 this.clearAutoComplete(); 206 this.hideSuggestBox(); 207 }, 208 209 _selectStart: function() 210 { 211 if (this._selectionTimeout) 212 clearTimeout(this._selectionTimeout); 213 214 this._removeSuggestionAids(); 215 216 /** 217 * @this {WebInspector.TextPrompt} 218 */ 219 function moveBackIfOutside() 220 { 221 delete this._selectionTimeout; 222 if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) { 223 this.moveCaretToEndOfPrompt(); 224 this.autoCompleteSoon(); 225 } 226 } 227 228 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100); 229 }, 230 231 /** 232 * @param {boolean=} force 233 * @return {boolean} 234 */ 235 defaultKeyHandler: function(event, force) 236 { 237 this._updateAutoComplete(force); 238 return false; 239 }, 240 241 /** 242 * @param {boolean=} force 243 */ 244 _updateAutoComplete: function(force) 245 { 246 this.clearAutoComplete(); 247 this.autoCompleteSoon(force); 248 }, 249 250 /** 251 * @param {?Event} event 252 */ 253 onMouseWheel: function(event) 254 { 255 // Subclasses can implement. 256 }, 257 258 /** 259 * @param {?Event} event 260 */ 261 onKeyDown: function(event) 262 { 263 var handled = false; 264 var invokeDefault = true; 265 266 switch (event.keyIdentifier) { 267 case "U+0009": // Tab 268 handled = this.tabKeyPressed(event); 269 break; 270 case "Left": 271 case "Home": 272 this._removeSuggestionAids(); 273 invokeDefault = false; 274 break; 275 case "Right": 276 case "End": 277 if (this.isCaretAtEndOfPrompt()) 278 handled = this.acceptAutoComplete(); 279 else 280 this._removeSuggestionAids(); 281 invokeDefault = false; 282 break; 283 case "U+001B": // Esc 284 if (this.isSuggestBoxVisible()) { 285 this._removeSuggestionAids(); 286 handled = true; 287 } 288 break; 289 case "U+0020": // Space 290 if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 291 this.defaultKeyHandler(event, true); 292 handled = true; 293 } 294 break; 295 case "Alt": 296 case "Meta": 297 case "Shift": 298 case "Control": 299 invokeDefault = false; 300 break; 301 } 302 303 if (!handled && this.isSuggestBoxVisible()) 304 handled = this._suggestBox.keyPressed(event); 305 306 if (!handled && invokeDefault) 307 handled = this.defaultKeyHandler(event); 308 309 if (handled) 310 event.consume(true); 311 312 return handled; 313 }, 314 315 /** 316 * @return {boolean} 317 */ 318 acceptAutoComplete: function() 319 { 320 var result = false; 321 if (this.isSuggestBoxVisible()) 322 result = this._suggestBox.acceptSuggestion(); 323 if (!result) 324 result = this._acceptSuggestionInternal(); 325 326 return result; 327 }, 328 329 /** 330 * @param {boolean=} includeTimeout 331 */ 332 clearAutoComplete: function(includeTimeout) 333 { 334 if (includeTimeout && this._completeTimeout) { 335 clearTimeout(this._completeTimeout); 336 delete this._completeTimeout; 337 } 338 delete this._waitingForCompletions; 339 340 if (!this.autoCompleteElement) 341 return; 342 343 this.autoCompleteElement.remove(); 344 delete this.autoCompleteElement; 345 346 if (!this._userEnteredRange || !this._userEnteredText) 347 return; 348 349 this._userEnteredRange.deleteContents(); 350 this._element.normalize(); 351 352 var userTextNode = document.createTextNode(this._userEnteredText); 353 this._userEnteredRange.insertNode(userTextNode); 354 355 var selectionRange = document.createRange(); 356 selectionRange.setStart(userTextNode, this._userEnteredText.length); 357 selectionRange.setEnd(userTextNode, this._userEnteredText.length); 358 359 var selection = window.getSelection(); 360 selection.removeAllRanges(); 361 selection.addRange(selectionRange); 362 363 delete this._userEnteredRange; 364 delete this._userEnteredText; 365 }, 366 367 /** 368 * @param {boolean=} force 369 */ 370 autoCompleteSoon: function(force) 371 { 372 var immediately = this.isSuggestBoxVisible() || force; 373 if (!this._completeTimeout) 374 this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250); 375 }, 376 377 /** 378 * @param {boolean=} reverse 379 */ 380 complete: function(force, reverse) 381 { 382 this.clearAutoComplete(true); 383 var selection = window.getSelection(); 384 if (!selection.rangeCount) 385 return; 386 387 var selectionRange = selection.getRangeAt(0); 388 var shouldExit; 389 390 if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible()) 391 shouldExit = true; 392 else if (!selection.isCollapsed) 393 shouldExit = true; 394 else if (!force) { 395 // BUG72018: Do not show suggest box if caret is followed by a non-stop character. 396 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward"); 397 if (wordSuffixRange.toString().length) 398 shouldExit = true; 399 } 400 if (shouldExit) { 401 this.hideSuggestBox(); 402 return; 403 } 404 405 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward"); 406 this._waitingForCompletions = true; 407 this._loadCompletions(this.proxyElement, wordPrefixRange, force, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse)); 408 }, 409 410 disableDefaultSuggestionForEmptyInput: function() 411 { 412 this._disableDefaultSuggestionForEmptyInput = true; 413 }, 414 415 /** 416 * @param {!Selection} selection 417 * @param {!Range} textRange 418 */ 419 _boxForAnchorAtStart: function(selection, textRange) 420 { 421 var rangeCopy = selection.getRangeAt(0).cloneRange(); 422 var anchorElement = document.createElement("span"); 423 anchorElement.textContent = "\u200B"; 424 textRange.insertNode(anchorElement); 425 var box = anchorElement.boxInWindow(window); 426 anchorElement.remove(); 427 selection.removeAllRanges(); 428 selection.addRange(rangeCopy); 429 return box; 430 }, 431 432 /** 433 * @param {!Array.<string>} completions 434 * @param {number} wordPrefixLength 435 */ 436 _buildCommonPrefix: function(completions, wordPrefixLength) 437 { 438 var commonPrefix = completions[0]; 439 for (var i = 0; i < completions.length; ++i) { 440 var completion = completions[i]; 441 var lastIndex = Math.min(commonPrefix.length, completion.length); 442 for (var j = wordPrefixLength; j < lastIndex; ++j) { 443 if (commonPrefix[j] !== completion[j]) { 444 commonPrefix = commonPrefix.substr(0, j); 445 break; 446 } 447 } 448 } 449 return commonPrefix; 450 }, 451 452 /** 453 * @param {!Selection} selection 454 * @param {!Range} originalWordPrefixRange 455 * @param {boolean} reverse 456 * @param {!Array.<string>} completions 457 * @param {number=} selectedIndex 458 */ 459 _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex) 460 { 461 if (!this._waitingForCompletions || !completions.length) { 462 this.hideSuggestBox(); 463 return; 464 } 465 delete this._waitingForCompletions; 466 467 var selectionRange = selection.getRangeAt(0); 468 469 var fullWordRange = document.createRange(); 470 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); 471 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); 472 473 if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString()) 474 return; 475 476 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0); 477 478 this._userEnteredRange = fullWordRange; 479 this._userEnteredText = fullWordRange.toString(); 480 481 if (this._suggestBox) 482 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText); 483 484 if (selectedIndex === -1) 485 return; 486 487 var wordPrefixLength = originalWordPrefixRange.toString().length; 488 this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength); 489 490 if (this.isCaretAtEndOfPrompt()) { 491 this._userEnteredRange.deleteContents(); 492 this._element.normalize(); 493 var finalSelectionRange = document.createRange(); 494 var completionText = completions[selectedIndex]; 495 var prefixText = completionText.substring(0, wordPrefixLength); 496 var suffixText = completionText.substring(wordPrefixLength); 497 498 var prefixTextNode = document.createTextNode(prefixText); 499 fullWordRange.insertNode(prefixTextNode); 500 501 this.autoCompleteElement = document.createElement("span"); 502 this.autoCompleteElement.className = "auto-complete-text"; 503 this.autoCompleteElement.textContent = suffixText; 504 505 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling); 506 507 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength); 508 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength); 509 selection.removeAllRanges(); 510 selection.addRange(finalSelectionRange); 511 } 512 }, 513 514 _completeCommonPrefix: function() 515 { 516 if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText)) 517 return; 518 519 if (!this.isSuggestBoxVisible()) { 520 this.acceptAutoComplete(); 521 return; 522 } 523 524 this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length); 525 this._acceptSuggestionInternal(true); 526 }, 527 528 /** 529 * @param {string} completionText 530 * @param {boolean=} isIntermediateSuggestion 531 */ 532 applySuggestion: function(completionText, isIntermediateSuggestion) 533 { 534 this._applySuggestion(completionText, isIntermediateSuggestion); 535 }, 536 537 /** 538 * @param {string} completionText 539 * @param {boolean=} isIntermediateSuggestion 540 * @param {!Range=} originalPrefixRange 541 */ 542 _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange) 543 { 544 var wordPrefixLength; 545 if (originalPrefixRange) 546 wordPrefixLength = originalPrefixRange.toString().length; 547 else 548 wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0; 549 550 this._userEnteredRange.deleteContents(); 551 this._element.normalize(); 552 var finalSelectionRange = document.createRange(); 553 var completionTextNode = document.createTextNode(completionText); 554 this._userEnteredRange.insertNode(completionTextNode); 555 if (this.autoCompleteElement) { 556 this.autoCompleteElement.remove(); 557 delete this.autoCompleteElement; 558 } 559 560 if (isIntermediateSuggestion) 561 finalSelectionRange.setStart(completionTextNode, wordPrefixLength); 562 else 563 finalSelectionRange.setStart(completionTextNode, completionText.length); 564 565 finalSelectionRange.setEnd(completionTextNode, completionText.length); 566 567 var selection = window.getSelection(); 568 selection.removeAllRanges(); 569 selection.addRange(finalSelectionRange); 570 if (isIntermediateSuggestion) 571 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText }); 572 }, 573 574 /** 575 * @override 576 */ 577 acceptSuggestion: function() 578 { 579 this._acceptSuggestionInternal(); 580 }, 581 582 /** 583 * @param {boolean=} prefixAccepted 584 * @return {boolean} 585 */ 586 _acceptSuggestionInternal: function(prefixAccepted) 587 { 588 if (this._isAcceptingSuggestion) 589 return false; 590 591 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode) 592 return false; 593 594 var text = this.autoCompleteElement.textContent; 595 var textNode = document.createTextNode(text); 596 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement); 597 delete this.autoCompleteElement; 598 599 var finalSelectionRange = document.createRange(); 600 finalSelectionRange.setStart(textNode, text.length); 601 finalSelectionRange.setEnd(textNode, text.length); 602 603 var selection = window.getSelection(); 604 selection.removeAllRanges(); 605 selection.addRange(finalSelectionRange); 606 607 if (!prefixAccepted) { 608 this.hideSuggestBox(); 609 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted); 610 } else 611 this.autoCompleteSoon(true); 612 613 return true; 614 }, 615 616 hideSuggestBox: function() 617 { 618 if (this.isSuggestBoxVisible()) 619 this._suggestBox.hide(); 620 }, 621 622 /** 623 * @return {boolean} 624 */ 625 isSuggestBoxVisible: function() 626 { 627 return this._suggestBox && this._suggestBox.visible(); 628 }, 629 630 /** 631 * @return {boolean} 632 */ 633 isCaretInsidePrompt: function() 634 { 635 return this._element.isInsertionCaretInside(); 636 }, 637 638 /** 639 * @return {boolean} 640 */ 641 isCaretAtEndOfPrompt: function() 642 { 643 var selection = window.getSelection(); 644 if (!selection.rangeCount || !selection.isCollapsed) 645 return false; 646 647 var selectionRange = selection.getRangeAt(0); 648 var node = selectionRange.startContainer; 649 if (!node.isSelfOrDescendant(this._element)) 650 return false; 651 652 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length) 653 return false; 654 655 var foundNextText = false; 656 while (node) { 657 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { 658 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node))) 659 return false; 660 foundNextText = true; 661 } 662 663 node = node.traverseNextNode(this._element); 664 } 665 666 return true; 667 }, 668 669 /** 670 * @return {boolean} 671 */ 672 isCaretOnFirstLine: function() 673 { 674 var selection = window.getSelection(); 675 var focusNode = selection.focusNode; 676 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element) 677 return true; 678 679 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1) 680 return false; 681 focusNode = focusNode.previousSibling; 682 683 while (focusNode) { 684 if (focusNode.nodeType !== Node.TEXT_NODE) 685 return true; 686 if (focusNode.textContent.indexOf("\n") !== -1) 687 return false; 688 focusNode = focusNode.previousSibling; 689 } 690 691 return true; 692 }, 693 694 /** 695 * @return {boolean} 696 */ 697 isCaretOnLastLine: function() 698 { 699 var selection = window.getSelection(); 700 var focusNode = selection.focusNode; 701 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element) 702 return true; 703 704 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1) 705 return false; 706 focusNode = focusNode.nextSibling; 707 708 while (focusNode) { 709 if (focusNode.nodeType !== Node.TEXT_NODE) 710 return true; 711 if (focusNode.textContent.indexOf("\n") !== -1) 712 return false; 713 focusNode = focusNode.nextSibling; 714 } 715 716 return true; 717 }, 718 719 moveCaretToEndOfPrompt: function() 720 { 721 var selection = window.getSelection(); 722 var selectionRange = document.createRange(); 723 724 var offset = this._element.childNodes.length; 725 selectionRange.setStart(this._element, offset); 726 selectionRange.setEnd(this._element, offset); 727 728 selection.removeAllRanges(); 729 selection.addRange(selectionRange); 730 }, 731 732 /** 733 * @param {!Event} event 734 * @return {boolean} 735 */ 736 tabKeyPressed: function(event) 737 { 738 this._completeCommonPrefix(); 739 740 // Consume the key. 741 return true; 742 }, 743 744 __proto__: WebInspector.Object.prototype 745 } 746 747 748 /** 749 * @constructor 750 * @extends {WebInspector.TextPrompt} 751 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions 752 * @param {string=} stopCharacters 753 */ 754 WebInspector.TextPromptWithHistory = function(completions, stopCharacters) 755 { 756 WebInspector.TextPrompt.call(this, completions, stopCharacters); 757 758 /** 759 * @type {!Array.<string>} 760 */ 761 this._data = []; 762 763 /** 764 * 1-based entry in the history stack. 765 * @type {number} 766 */ 767 this._historyOffset = 1; 768 769 /** 770 * Whether to coalesce duplicate items in the history, default is true. 771 * @type {boolean} 772 */ 773 this._coalesceHistoryDupes = true; 774 } 775 776 WebInspector.TextPromptWithHistory.prototype = { 777 /** 778 * @return {!Array.<string>} 779 */ 780 get historyData() 781 { 782 // FIXME: do we need to copy this? 783 return this._data; 784 }, 785 786 /** 787 * @param {boolean} x 788 */ 789 setCoalesceHistoryDupes: function(x) 790 { 791 this._coalesceHistoryDupes = x; 792 }, 793 794 /** 795 * @param {!Array.<string>} data 796 */ 797 setHistoryData: function(data) 798 { 799 this._data = [].concat(data); 800 this._historyOffset = 1; 801 }, 802 803 /** 804 * Pushes a committed text into the history. 805 * @param {string} text 806 */ 807 pushHistoryItem: function(text) 808 { 809 if (this._uncommittedIsTop) { 810 this._data.pop(); 811 delete this._uncommittedIsTop; 812 } 813 814 this._historyOffset = 1; 815 if (this._coalesceHistoryDupes && text === this._currentHistoryItem()) 816 return; 817 this._data.push(text); 818 }, 819 820 /** 821 * Pushes the current (uncommitted) text into the history. 822 */ 823 _pushCurrentText: function() 824 { 825 if (this._uncommittedIsTop) 826 this._data.pop(); // Throw away obsolete uncommitted text. 827 this._uncommittedIsTop = true; 828 this.clearAutoComplete(true); 829 this._data.push(this.text); 830 }, 831 832 /** 833 * @return {string|undefined} 834 */ 835 _previous: function() 836 { 837 if (this._historyOffset > this._data.length) 838 return undefined; 839 if (this._historyOffset === 1) 840 this._pushCurrentText(); 841 ++this._historyOffset; 842 return this._currentHistoryItem(); 843 }, 844 845 /** 846 * @return {string|undefined} 847 */ 848 _next: function() 849 { 850 if (this._historyOffset === 1) 851 return undefined; 852 --this._historyOffset; 853 return this._currentHistoryItem(); 854 }, 855 856 /** 857 * @return {string|undefined} 858 */ 859 _currentHistoryItem: function() 860 { 861 return this._data[this._data.length - this._historyOffset]; 862 }, 863 864 /** 865 * @override 866 */ 867 defaultKeyHandler: function(event, force) 868 { 869 var newText; 870 var isPrevious; 871 872 switch (event.keyIdentifier) { 873 case "Up": 874 if (!this.isCaretOnFirstLine()) 875 break; 876 newText = this._previous(); 877 isPrevious = true; 878 break; 879 case "Down": 880 if (!this.isCaretOnLastLine()) 881 break; 882 newText = this._next(); 883 break; 884 case "U+0050": // Ctrl+P = Previous 885 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 886 newText = this._previous(); 887 isPrevious = true; 888 } 889 break; 890 case "U+004E": // Ctrl+N = Next 891 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) 892 newText = this._next(); 893 break; 894 } 895 896 if (newText !== undefined) { 897 event.consume(true); 898 this.text = newText; 899 900 if (isPrevious) { 901 var firstNewlineIndex = this.text.indexOf("\n"); 902 if (firstNewlineIndex === -1) 903 this.moveCaretToEndOfPrompt(); 904 else { 905 var selection = window.getSelection(); 906 var selectionRange = document.createRange(); 907 908 selectionRange.setStart(this._element.firstChild, firstNewlineIndex); 909 selectionRange.setEnd(this._element.firstChild, firstNewlineIndex); 910 911 selection.removeAllRanges(); 912 selection.addRange(selectionRange); 913 } 914 } 915 916 return true; 917 } 918 919 return WebInspector.TextPrompt.prototype.defaultKeyHandler.apply(this, arguments); 920 }, 921 922 __proto__: WebInspector.TextPrompt.prototype 923 } 924 925