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