TextPrompt.js revision cad810f21b803229eb11403f9209855525a25d57
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 29WebInspector.TextPrompt = function(element, completions, stopCharacters) 30{ 31 this.element = element; 32 this.element.addStyleClass("text-prompt"); 33 this.completions = completions; 34 this.completionStopCharacters = stopCharacters; 35 this.history = []; 36 this.historyOffset = 0; 37 this.element.addEventListener("keydown", this._onKeyDown.bind(this), true); 38} 39 40WebInspector.TextPrompt.prototype = { 41 get text() 42 { 43 return this.element.textContent; 44 }, 45 46 set text(x) 47 { 48 if (!x) { 49 // Append a break element instead of setting textContent to make sure the selection is inside the prompt. 50 this.element.removeChildren(); 51 this.element.appendChild(document.createElement("br")); 52 } else 53 this.element.textContent = x; 54 55 this.moveCaretToEndOfPrompt(); 56 }, 57 58 _onKeyDown: function(event) 59 { 60 function defaultAction() 61 { 62 this.clearAutoComplete(); 63 this.autoCompleteSoon(); 64 } 65 66 var handled = false; 67 switch (event.keyIdentifier) { 68 case "Up": 69 this._upKeyPressed(event); 70 break; 71 case "Down": 72 this._downKeyPressed(event); 73 break; 74 case "U+0009": // Tab 75 this._tabKeyPressed(event); 76 break; 77 case "Right": 78 case "End": 79 if (!this.acceptAutoComplete()) 80 this.autoCompleteSoon(); 81 break; 82 case "Alt": 83 case "Meta": 84 case "Shift": 85 case "Control": 86 break; 87 case "U+0050": // Ctrl+P = Previous 88 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 89 handled = true; 90 this._moveBackInHistory(); 91 break; 92 } 93 defaultAction.call(this); 94 break; 95 case "U+004E": // Ctrl+N = Next 96 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { 97 handled = true; 98 this._moveForwardInHistory(); 99 break; 100 } 101 defaultAction.call(this); 102 break; 103 default: 104 defaultAction.call(this); 105 break; 106 } 107 108 if (handled) { 109 event.preventDefault(); 110 event.stopPropagation(); 111 } 112 }, 113 114 acceptAutoComplete: function() 115 { 116 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode) 117 return false; 118 119 var text = this.autoCompleteElement.textContent; 120 var textNode = document.createTextNode(text); 121 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement); 122 delete this.autoCompleteElement; 123 124 var finalSelectionRange = document.createRange(); 125 finalSelectionRange.setStart(textNode, text.length); 126 finalSelectionRange.setEnd(textNode, text.length); 127 128 var selection = window.getSelection(); 129 selection.removeAllRanges(); 130 selection.addRange(finalSelectionRange); 131 132 return true; 133 }, 134 135 clearAutoComplete: function(includeTimeout) 136 { 137 if (includeTimeout && "_completeTimeout" in this) { 138 clearTimeout(this._completeTimeout); 139 delete this._completeTimeout; 140 } 141 142 if (!this.autoCompleteElement) 143 return; 144 145 if (this.autoCompleteElement.parentNode) 146 this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement); 147 delete this.autoCompleteElement; 148 149 if (!this._userEnteredRange || !this._userEnteredText) 150 return; 151 152 this._userEnteredRange.deleteContents(); 153 this.element.pruneEmptyTextNodes(); 154 155 var userTextNode = document.createTextNode(this._userEnteredText); 156 this._userEnteredRange.insertNode(userTextNode); 157 158 var selectionRange = document.createRange(); 159 selectionRange.setStart(userTextNode, this._userEnteredText.length); 160 selectionRange.setEnd(userTextNode, this._userEnteredText.length); 161 162 var selection = window.getSelection(); 163 selection.removeAllRanges(); 164 selection.addRange(selectionRange); 165 166 delete this._userEnteredRange; 167 delete this._userEnteredText; 168 }, 169 170 autoCompleteSoon: function() 171 { 172 if (!("_completeTimeout" in this)) 173 this._completeTimeout = setTimeout(this.complete.bind(this, true), 250); 174 }, 175 176 complete: function(auto, reverse) 177 { 178 this.clearAutoComplete(true); 179 var selection = window.getSelection(); 180 if (!selection.rangeCount) 181 return; 182 183 var selectionRange = selection.getRangeAt(0); 184 if (!selectionRange.commonAncestorContainer.isDescendant(this.element)) 185 return; 186 if (auto && !this.isCaretAtEndOfPrompt()) 187 return; 188 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward"); 189 this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange, reverse)); 190 }, 191 192 _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions) 193 { 194 if (!completions || !completions.length) 195 return; 196 197 var selectionRange = selection.getRangeAt(0); 198 199 var fullWordRange = document.createRange(); 200 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); 201 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); 202 203 if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString()) 204 return; 205 206 var wordPrefixLength = originalWordPrefixRange.toString().length; 207 208 if (auto) 209 var completionText = completions[0]; 210 else { 211 if (completions.length === 1) { 212 var completionText = completions[0]; 213 wordPrefixLength = completionText.length; 214 } else { 215 var commonPrefix = completions[0]; 216 for (var i = 0; i < completions.length; ++i) { 217 var completion = completions[i]; 218 var lastIndex = Math.min(commonPrefix.length, completion.length); 219 for (var j = wordPrefixLength; j < lastIndex; ++j) { 220 if (commonPrefix[j] !== completion[j]) { 221 commonPrefix = commonPrefix.substr(0, j); 222 break; 223 } 224 } 225 } 226 wordPrefixLength = commonPrefix.length; 227 228 if (selection.isCollapsed) 229 var completionText = completions[0]; 230 else { 231 var currentText = fullWordRange.toString(); 232 233 var foundIndex = null; 234 for (var i = 0; i < completions.length; ++i) { 235 if (completions[i] === currentText) 236 foundIndex = i; 237 } 238 239 var nextIndex = foundIndex + (reverse ? -1 : 1); 240 if (foundIndex === null || nextIndex >= completions.length) 241 var completionText = completions[0]; 242 else if (nextIndex < 0) 243 var completionText = completions[completions.length - 1]; 244 else 245 var completionText = completions[nextIndex]; 246 } 247 } 248 } 249 250 this._userEnteredRange = fullWordRange; 251 this._userEnteredText = fullWordRange.toString(); 252 253 fullWordRange.deleteContents(); 254 this.element.pruneEmptyTextNodes(); 255 256 var finalSelectionRange = document.createRange(); 257 258 if (auto) { 259 var prefixText = completionText.substring(0, wordPrefixLength); 260 var suffixText = completionText.substring(wordPrefixLength); 261 262 var prefixTextNode = document.createTextNode(prefixText); 263 fullWordRange.insertNode(prefixTextNode); 264 265 this.autoCompleteElement = document.createElement("span"); 266 this.autoCompleteElement.className = "auto-complete-text"; 267 this.autoCompleteElement.textContent = suffixText; 268 269 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling); 270 271 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength); 272 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength); 273 } else { 274 var completionTextNode = document.createTextNode(completionText); 275 fullWordRange.insertNode(completionTextNode); 276 277 if (completions.length > 1) 278 finalSelectionRange.setStart(completionTextNode, wordPrefixLength); 279 else 280 finalSelectionRange.setStart(completionTextNode, completionText.length); 281 282 finalSelectionRange.setEnd(completionTextNode, completionText.length); 283 } 284 285 selection.removeAllRanges(); 286 selection.addRange(finalSelectionRange); 287 }, 288 289 isCaretInsidePrompt: function() 290 { 291 return this.element.isInsertionCaretInside(); 292 }, 293 294 isCaretAtEndOfPrompt: function() 295 { 296 var selection = window.getSelection(); 297 if (!selection.rangeCount || !selection.isCollapsed) 298 return false; 299 300 var selectionRange = selection.getRangeAt(0); 301 var node = selectionRange.startContainer; 302 if (node !== this.element && !node.isDescendant(this.element)) 303 return false; 304 305 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length) 306 return false; 307 308 var foundNextText = false; 309 while (node) { 310 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { 311 if (foundNextText) 312 return false; 313 foundNextText = true; 314 } 315 316 node = node.traverseNextNode(this.element); 317 } 318 319 return true; 320 }, 321 322 isCaretOnFirstLine: function() 323 { 324 var selection = window.getSelection(); 325 var focusNode = selection.focusNode; 326 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) 327 return true; 328 329 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1) 330 return false; 331 focusNode = focusNode.previousSibling; 332 333 while (focusNode) { 334 if (focusNode.nodeType !== Node.TEXT_NODE) 335 return true; 336 if (focusNode.textContent.indexOf("\n") !== -1) 337 return false; 338 focusNode = focusNode.previousSibling; 339 } 340 341 return true; 342 }, 343 344 isCaretOnLastLine: function() 345 { 346 var selection = window.getSelection(); 347 var focusNode = selection.focusNode; 348 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) 349 return true; 350 351 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1) 352 return false; 353 focusNode = focusNode.nextSibling; 354 355 while (focusNode) { 356 if (focusNode.nodeType !== Node.TEXT_NODE) 357 return true; 358 if (focusNode.textContent.indexOf("\n") !== -1) 359 return false; 360 focusNode = focusNode.nextSibling; 361 } 362 363 return true; 364 }, 365 366 moveCaretToEndOfPrompt: function() 367 { 368 var selection = window.getSelection(); 369 var selectionRange = document.createRange(); 370 371 var offset = this.element.childNodes.length; 372 selectionRange.setStart(this.element, offset); 373 selectionRange.setEnd(this.element, offset); 374 375 selection.removeAllRanges(); 376 selection.addRange(selectionRange); 377 }, 378 379 _tabKeyPressed: function(event) 380 { 381 event.preventDefault(); 382 event.stopPropagation(); 383 384 this.complete(false, event.shiftKey); 385 }, 386 387 _upKeyPressed: function(event) 388 { 389 if (!this.isCaretOnFirstLine()) 390 return; 391 392 event.preventDefault(); 393 event.stopPropagation(); 394 395 this._moveBackInHistory(); 396 }, 397 398 _downKeyPressed: function(event) 399 { 400 if (!this.isCaretOnLastLine()) 401 return; 402 403 event.preventDefault(); 404 event.stopPropagation(); 405 406 this._moveForwardInHistory(); 407 }, 408 409 _moveBackInHistory: function() 410 { 411 if (this.historyOffset == this.history.length) 412 return; 413 414 this.clearAutoComplete(true); 415 416 if (this.historyOffset === 0) 417 this.tempSavedCommand = this.text; 418 419 ++this.historyOffset; 420 this.text = this.history[this.history.length - this.historyOffset]; 421 422 this.element.scrollIntoView(true); 423 var firstNewlineIndex = this.text.indexOf("\n"); 424 if (firstNewlineIndex === -1) 425 this.moveCaretToEndOfPrompt(); 426 else { 427 var selection = window.getSelection(); 428 var selectionRange = document.createRange(); 429 430 selectionRange.setStart(this.element.firstChild, firstNewlineIndex); 431 selectionRange.setEnd(this.element.firstChild, firstNewlineIndex); 432 433 selection.removeAllRanges(); 434 selection.addRange(selectionRange); 435 } 436 }, 437 438 _moveForwardInHistory: function() 439 { 440 if (this.historyOffset === 0) 441 return; 442 443 this.clearAutoComplete(true); 444 445 --this.historyOffset; 446 447 if (this.historyOffset === 0) { 448 this.text = this.tempSavedCommand; 449 delete this.tempSavedCommand; 450 return; 451 } 452 453 this.text = this.history[this.history.length - this.historyOffset]; 454 this.element.scrollIntoView(); 455 } 456} 457