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