1 /* 2 * Copyright (C) 2008 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 14 * its contributors may be used to endorse or promote products derived 15 * from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 WebInspector.TextPrompt = function(element, completions, stopCharacters, omitHistory) 30 { 31 this.element = element; 32 this.element.addStyleClass("text-prompt"); 33 this.completions = completions; 34 this.completionStopCharacters = stopCharacters; 35 if (!omitHistory) { 36 this.history = []; 37 this.historyOffset = 0; 38 } 39 this._boundOnKeyDown = this._onKeyDown.bind(this); 40 this.element.addEventListener("keydown", this._boundOnKeyDown, true); 41 } 42 43 WebInspector.TextPrompt.prototype = { 44 get text() 45 { 46 return this.element.textContent; 47 }, 48 49 set text(x) 50 { 51 if (!x) { 52 // Append a break element instead of setting textContent to make sure the selection is inside the prompt. 53 this.element.removeChildren(); 54 this.element.appendChild(document.createElement("br")); 55 } else 56 this.element.textContent = x; 57 58 this.moveCaretToEndOfPrompt(); 59 }, 60 61 removeFromElement: function() 62 { 63 this.clearAutoComplete(true); 64 this.element.removeEventListener("keydown", this._boundOnKeyDown, true); 65 }, 66 67 _onKeyDown: function(event) 68 { 69 function defaultAction() 70 { 71 this.clearAutoComplete(); 72 this.autoCompleteSoon(); 73 } 74 75 if (event.handled) 76 return; 77 78 var handled = false; 79 80 switch (event.keyIdentifier) { 81 case "Up": 82 this.upKeyPressed(event); 83 break; 84 case "Down": 85 this.downKeyPressed(event); 86 break; 87 case "U+0009": // Tab 88 this.tabKeyPressed(event); 89 break; 90 case "Right": 91 case "End": 92 if (!this.acceptAutoComplete()) 93 this.autoCompleteSoon(); 94 break; 95 case "Alt": 96 case "Meta": 97 case "Shift": 98 case "Control": 99 break; 100 case "U+0050": // Ctrl+P = Previous 101 if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 102 handled = true; 103 this._moveBackInHistory(); 104 break; 105 } 106 defaultAction.call(this); 107 break; 108 case "U+004E": // Ctrl+N = Next 109 if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 110 handled = true; 111 this._moveForwardInHistory(); 112 break; 113 } 114 defaultAction.call(this); 115 break; 116 default: 117 defaultAction.call(this); 118 break; 119 } 120 121 handled |= event.handled; 122 if (handled) { 123 event.handled = true; 124 event.preventDefault(); 125 event.stopPropagation(); 126 } 127 }, 128 129 acceptAutoComplete: function() 130 { 131 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode) 132 return false; 133 134 var text = this.autoCompleteElement.textContent; 135 var textNode = document.createTextNode(text); 136 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement); 137 delete this.autoCompleteElement; 138 139 var finalSelectionRange = document.createRange(); 140 finalSelectionRange.setStart(textNode, text.length); 141 finalSelectionRange.setEnd(textNode, text.length); 142 143 var selection = window.getSelection(); 144 selection.removeAllRanges(); 145 selection.addRange(finalSelectionRange); 146 147 return true; 148 }, 149 150 clearAutoComplete: function(includeTimeout) 151 { 152 if (includeTimeout && "_completeTimeout" in this) { 153 clearTimeout(this._completeTimeout); 154 delete this._completeTimeout; 155 } 156 157 if (!this.autoCompleteElement) 158 return; 159 160 if (this.autoCompleteElement.parentNode) 161 this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement); 162 delete this.autoCompleteElement; 163 164 if (!this._userEnteredRange || !this._userEnteredText) 165 return; 166 167 this._userEnteredRange.deleteContents(); 168 this.element.pruneEmptyTextNodes(); 169 170 var userTextNode = document.createTextNode(this._userEnteredText); 171 this._userEnteredRange.insertNode(userTextNode); 172 173 var selectionRange = document.createRange(); 174 selectionRange.setStart(userTextNode, this._userEnteredText.length); 175 selectionRange.setEnd(userTextNode, this._userEnteredText.length); 176 177 var selection = window.getSelection(); 178 selection.removeAllRanges(); 179 selection.addRange(selectionRange); 180 181 delete this._userEnteredRange; 182 delete this._userEnteredText; 183 }, 184 185 autoCompleteSoon: function() 186 { 187 if (!("_completeTimeout" in this)) 188 this._completeTimeout = setTimeout(this.complete.bind(this, true), 250); 189 }, 190 191 complete: function(auto, reverse) 192 { 193 this.clearAutoComplete(true); 194 var selection = window.getSelection(); 195 if (!selection.rangeCount) 196 return; 197 198 var selectionRange = selection.getRangeAt(0); 199 var isEmptyInput = selectionRange.commonAncestorContainer === this.element; // this.element has no child Text nodes. 200 201 // Do not attempt to auto-complete an empty input in the auto mode (only on demand). 202 if (auto && isEmptyInput) 203 return; 204 if (!auto && !isEmptyInput && !selectionRange.commonAncestorContainer.isDescendant(this.element)) 205 return; 206 if (auto && !this.isCaretAtEndOfPrompt()) 207 return; 208 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward"); 209 this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange, reverse)); 210 }, 211 212 _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions) 213 { 214 if (!completions || !completions.length) 215 return; 216 217 var selectionRange = selection.getRangeAt(0); 218 219 var fullWordRange = document.createRange(); 220 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); 221 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); 222 223 if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString()) 224 return; 225 226 var wordPrefixLength = originalWordPrefixRange.toString().length; 227 228 if (auto) 229 var completionText = completions[0]; 230 else { 231 if (completions.length === 1) { 232 var completionText = completions[0]; 233 wordPrefixLength = completionText.length; 234 } else { 235 var commonPrefix = completions[0]; 236 for (var i = 0; i < completions.length; ++i) { 237 var completion = completions[i]; 238 var lastIndex = Math.min(commonPrefix.length, completion.length); 239 for (var j = wordPrefixLength; j < lastIndex; ++j) { 240 if (commonPrefix[j] !== completion[j]) { 241 commonPrefix = commonPrefix.substr(0, j); 242 break; 243 } 244 } 245 } 246 wordPrefixLength = commonPrefix.length; 247 248 if (selection.isCollapsed) 249 var completionText = completions[0]; 250 else { 251 var currentText = fullWordRange.toString(); 252 253 var foundIndex = null; 254 for (var i = 0; i < completions.length; ++i) { 255 if (completions[i] === currentText) 256 foundIndex = i; 257 } 258 259 var nextIndex = foundIndex + (reverse ? -1 : 1); 260 if (foundIndex === null || nextIndex >= completions.length) 261 var completionText = completions[0]; 262 else if (nextIndex < 0) 263 var completionText = completions[completions.length - 1]; 264 else 265 var completionText = completions[nextIndex]; 266 } 267 } 268 } 269 270 this._userEnteredRange = fullWordRange; 271 this._userEnteredText = fullWordRange.toString(); 272 273 fullWordRange.deleteContents(); 274 this.element.pruneEmptyTextNodes(); 275 276 var finalSelectionRange = document.createRange(); 277 278 if (auto) { 279 var prefixText = completionText.substring(0, wordPrefixLength); 280 var suffixText = completionText.substring(wordPrefixLength); 281 282 var prefixTextNode = document.createTextNode(prefixText); 283 fullWordRange.insertNode(prefixTextNode); 284 285 this.autoCompleteElement = document.createElement("span"); 286 this.autoCompleteElement.className = "auto-complete-text"; 287 this.autoCompleteElement.textContent = suffixText; 288 289 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling); 290 291 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength); 292 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength); 293 } else { 294 var completionTextNode = document.createTextNode(completionText); 295 fullWordRange.insertNode(completionTextNode); 296 297 if (completions.length > 1) 298 finalSelectionRange.setStart(completionTextNode, wordPrefixLength); 299 else 300 finalSelectionRange.setStart(completionTextNode, completionText.length); 301 302 finalSelectionRange.setEnd(completionTextNode, completionText.length); 303 } 304 305 selection.removeAllRanges(); 306 selection.addRange(finalSelectionRange); 307 }, 308 309 isCaretInsidePrompt: function() 310 { 311 return this.element.isInsertionCaretInside(); 312 }, 313 314 isCaretAtEndOfPrompt: function() 315 { 316 var selection = window.getSelection(); 317 if (!selection.rangeCount || !selection.isCollapsed) 318 return false; 319 320 var selectionRange = selection.getRangeAt(0); 321 var node = selectionRange.startContainer; 322 if (node !== this.element && !node.isDescendant(this.element)) 323 return false; 324 325 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length) 326 return false; 327 328 var foundNextText = false; 329 while (node) { 330 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { 331 if (foundNextText) 332 return false; 333 foundNextText = true; 334 } 335 336 node = node.traverseNextNode(this.element); 337 } 338 339 return true; 340 }, 341 342 isCaretOnFirstLine: function() 343 { 344 var selection = window.getSelection(); 345 var focusNode = selection.focusNode; 346 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) 347 return true; 348 349 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1) 350 return false; 351 focusNode = focusNode.previousSibling; 352 353 while (focusNode) { 354 if (focusNode.nodeType !== Node.TEXT_NODE) 355 return true; 356 if (focusNode.textContent.indexOf("\n") !== -1) 357 return false; 358 focusNode = focusNode.previousSibling; 359 } 360 361 return true; 362 }, 363 364 isCaretOnLastLine: function() 365 { 366 var selection = window.getSelection(); 367 var focusNode = selection.focusNode; 368 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) 369 return true; 370 371 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1) 372 return false; 373 focusNode = focusNode.nextSibling; 374 375 while (focusNode) { 376 if (focusNode.nodeType !== Node.TEXT_NODE) 377 return true; 378 if (focusNode.textContent.indexOf("\n") !== -1) 379 return false; 380 focusNode = focusNode.nextSibling; 381 } 382 383 return true; 384 }, 385 386 moveCaretToEndOfPrompt: function() 387 { 388 var selection = window.getSelection(); 389 var selectionRange = document.createRange(); 390 391 var offset = this.element.childNodes.length; 392 selectionRange.setStart(this.element, offset); 393 selectionRange.setEnd(this.element, offset); 394 395 selection.removeAllRanges(); 396 selection.addRange(selectionRange); 397 }, 398 399 tabKeyPressed: function(event) 400 { 401 event.handled = true; 402 this.complete(false, event.shiftKey); 403 }, 404 405 upKeyPressed: function(event) 406 { 407 if (!this.isCaretOnFirstLine()) 408 return; 409 410 event.handled = true; 411 this._moveBackInHistory(); 412 }, 413 414 downKeyPressed: function(event) 415 { 416 if (!this.isCaretOnLastLine()) 417 return; 418 419 event.handled = true; 420 this._moveForwardInHistory(); 421 }, 422 423 _moveBackInHistory: function() 424 { 425 if (!this.history || this.historyOffset == this.history.length) 426 return; 427 428 this.clearAutoComplete(true); 429 430 if (this.historyOffset === 0) 431 this.tempSavedCommand = this.text; 432 433 ++this.historyOffset; 434 this.text = this.history[this.history.length - this.historyOffset]; 435 436 this.element.scrollIntoView(true); 437 var firstNewlineIndex = this.text.indexOf("\n"); 438 if (firstNewlineIndex === -1) 439 this.moveCaretToEndOfPrompt(); 440 else { 441 var selection = window.getSelection(); 442 var selectionRange = document.createRange(); 443 444 selectionRange.setStart(this.element.firstChild, firstNewlineIndex); 445 selectionRange.setEnd(this.element.firstChild, firstNewlineIndex); 446 447 selection.removeAllRanges(); 448 selection.addRange(selectionRange); 449 } 450 }, 451 452 _moveForwardInHistory: function() 453 { 454 if (!this.history || this.historyOffset === 0) 455 return; 456 457 this.clearAutoComplete(true); 458 459 --this.historyOffset; 460 461 if (this.historyOffset === 0) { 462 this.text = this.tempSavedCommand; 463 delete this.tempSavedCommand; 464 return; 465 } 466 467 this.text = this.history[this.history.length - this.historyOffset]; 468 this.element.scrollIntoView(); 469 } 470 } 471