1 /* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. 4 * Copyright (C) 2007 Matt Lilek (pewtermoose (at) gmail.com). 5 * Copyright (C) 2009 Joseph Pecoraro 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 17 * its contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 21 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 24 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 29 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 /** 33 * @param {!Element} element 34 * @param {?function(!MouseEvent): boolean} elementDragStart 35 * @param {function(!MouseEvent)} elementDrag 36 * @param {?function(!MouseEvent)} elementDragEnd 37 * @param {!string} cursor 38 * @param {?string=} hoverCursor 39 */ 40 WebInspector.installDragHandle = function(element, elementDragStart, elementDrag, elementDragEnd, cursor, hoverCursor) 41 { 42 element.addEventListener("mousedown", WebInspector.elementDragStart.bind(WebInspector, elementDragStart, elementDrag, elementDragEnd, cursor), false); 43 if (hoverCursor !== null) 44 element.style.cursor = hoverCursor || cursor; 45 } 46 47 /** 48 * @param {?function(!MouseEvent):boolean} elementDragStart 49 * @param {function(!MouseEvent)} elementDrag 50 * @param {?function(!MouseEvent)} elementDragEnd 51 * @param {string} cursor 52 * @param {?Event} event 53 */ 54 WebInspector.elementDragStart = function(elementDragStart, elementDrag, elementDragEnd, cursor, event) 55 { 56 // Only drag upon left button. Right will likely cause a context menu. So will ctrl-click on mac. 57 if (event.button || (WebInspector.isMac() && event.ctrlKey)) 58 return; 59 60 if (WebInspector._elementDraggingEventListener) 61 return; 62 63 if (elementDragStart && !elementDragStart(/** @type {!MouseEvent} */ (event))) 64 return; 65 66 if (WebInspector._elementDraggingGlassPane) { 67 WebInspector._elementDraggingGlassPane.dispose(); 68 delete WebInspector._elementDraggingGlassPane; 69 } 70 71 var targetDocument = event.target.ownerDocument; 72 73 WebInspector._elementDraggingEventListener = elementDrag; 74 WebInspector._elementEndDraggingEventListener = elementDragEnd; 75 WebInspector._mouseOutWhileDraggingTargetDocument = targetDocument; 76 77 targetDocument.addEventListener("mousemove", WebInspector._elementDragMove, true); 78 targetDocument.addEventListener("mouseup", WebInspector._elementDragEnd, true); 79 targetDocument.addEventListener("mouseout", WebInspector._mouseOutWhileDragging, true); 80 81 targetDocument.body.style.cursor = cursor; 82 83 event.preventDefault(); 84 } 85 86 WebInspector._mouseOutWhileDragging = function() 87 { 88 WebInspector._unregisterMouseOutWhileDragging(); 89 WebInspector._elementDraggingGlassPane = new WebInspector.GlassPane(); 90 } 91 92 WebInspector._unregisterMouseOutWhileDragging = function() 93 { 94 if (!WebInspector._mouseOutWhileDraggingTargetDocument) 95 return; 96 WebInspector._mouseOutWhileDraggingTargetDocument.removeEventListener("mouseout", WebInspector._mouseOutWhileDragging, true); 97 delete WebInspector._mouseOutWhileDraggingTargetDocument; 98 } 99 100 /** 101 * @param {!Event} event 102 */ 103 WebInspector._elementDragMove = function(event) 104 { 105 if (WebInspector._elementDraggingEventListener(/** @type {!MouseEvent} */ (event))) 106 WebInspector._cancelDragEvents(event); 107 } 108 109 /** 110 * @param {!Event} event 111 */ 112 WebInspector._cancelDragEvents = function(event) 113 { 114 var targetDocument = event.target.ownerDocument; 115 targetDocument.removeEventListener("mousemove", WebInspector._elementDragMove, true); 116 targetDocument.removeEventListener("mouseup", WebInspector._elementDragEnd, true); 117 WebInspector._unregisterMouseOutWhileDragging(); 118 119 targetDocument.body.style.removeProperty("cursor"); 120 121 if (WebInspector._elementDraggingGlassPane) 122 WebInspector._elementDraggingGlassPane.dispose(); 123 124 delete WebInspector._elementDraggingGlassPane; 125 delete WebInspector._elementDraggingEventListener; 126 delete WebInspector._elementEndDraggingEventListener; 127 } 128 129 /** 130 * @param {!Event} event 131 */ 132 WebInspector._elementDragEnd = function(event) 133 { 134 var elementDragEnd = WebInspector._elementEndDraggingEventListener; 135 136 WebInspector._cancelDragEvents(/** @type {!MouseEvent} */ (event)); 137 138 event.preventDefault(); 139 if (elementDragEnd) 140 elementDragEnd(/** @type {!MouseEvent} */ (event)); 141 } 142 143 /** 144 * @constructor 145 */ 146 WebInspector.GlassPane = function() 147 { 148 this.element = document.createElement("div"); 149 this.element.style.cssText = "position:absolute;top:0;bottom:0;left:0;right:0;background-color:transparent;z-index:1000;"; 150 this.element.id = "glass-pane"; 151 document.body.appendChild(this.element); 152 WebInspector._glassPane = this; 153 } 154 155 WebInspector.GlassPane.prototype = { 156 dispose: function() 157 { 158 delete WebInspector._glassPane; 159 if (WebInspector.HelpScreen.isVisible()) 160 WebInspector.HelpScreen.focus(); 161 else 162 WebInspector.inspectorView.focus(); 163 this.element.remove(); 164 } 165 } 166 167 WebInspector.animateStyle = function(animations, duration, callback) 168 { 169 var startTime = new Date().getTime(); 170 var hasCompleted = false; 171 172 const animationsLength = animations.length; 173 const propertyUnit = {opacity: ""}; 174 const defaultUnit = "px"; 175 176 // Pre-process animations. 177 for (var i = 0; i < animationsLength; ++i) { 178 var animation = animations[i]; 179 var element = null, start = null, end = null, key = null; 180 for (key in animation) { 181 if (key === "element") 182 element = animation[key]; 183 else if (key === "start") 184 start = animation[key]; 185 else if (key === "end") 186 end = animation[key]; 187 } 188 189 if (!element || !end) 190 continue; 191 192 if (!start) { 193 var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element); 194 start = {}; 195 for (key in end) 196 start[key] = parseInt(computedStyle.getPropertyValue(key), 10); 197 animation.start = start; 198 } else 199 for (key in start) 200 element.style.setProperty(key, start[key] + (key in propertyUnit ? propertyUnit[key] : defaultUnit)); 201 } 202 203 function animateLoop() 204 { 205 if (hasCompleted) 206 return; 207 208 var complete = new Date().getTime() - startTime; 209 210 // Make style changes. 211 for (var i = 0; i < animationsLength; ++i) { 212 var animation = animations[i]; 213 var element = animation.element; 214 var start = animation.start; 215 var end = animation.end; 216 if (!element || !end) 217 continue; 218 219 var style = element.style; 220 for (key in end) { 221 var endValue = end[key]; 222 if (complete < duration) { 223 var startValue = start[key]; 224 // Linear animation. 225 var newValue = startValue + (endValue - startValue) * complete / duration; 226 style.setProperty(key, newValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit)); 227 } else 228 style.setProperty(key, endValue + (key in propertyUnit ? propertyUnit[key] : defaultUnit)); 229 } 230 } 231 232 // End condition. 233 if (complete >= duration) 234 hasCompleted = true; 235 if (callback) 236 callback(hasCompleted); 237 if (!hasCompleted) 238 window.requestAnimationFrame(animateLoop); 239 } 240 241 function forceComplete() 242 { 243 if (hasCompleted) 244 return; 245 246 duration = 0; 247 animateLoop(); 248 } 249 250 window.requestAnimationFrame(animateLoop); 251 return { 252 forceComplete: forceComplete 253 }; 254 } 255 256 WebInspector.isBeingEdited = function(element) 257 { 258 if (element.classList.contains("text-prompt") || element.nodeName === "INPUT" || element.nodeName === "TEXTAREA") 259 return true; 260 261 if (!WebInspector.__editingCount) 262 return false; 263 264 while (element) { 265 if (element.__editing) 266 return true; 267 element = element.parentElement; 268 } 269 return false; 270 } 271 272 WebInspector.markBeingEdited = function(element, value) 273 { 274 if (value) { 275 if (element.__editing) 276 return false; 277 element.classList.add("being-edited"); 278 element.__editing = true; 279 WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1; 280 } else { 281 if (!element.__editing) 282 return false; 283 element.classList.remove("being-edited"); 284 delete element.__editing; 285 --WebInspector.__editingCount; 286 } 287 return true; 288 } 289 290 /** 291 * @constructor 292 * @param {function(!Element,string,string,T,string)} commitHandler 293 * @param {function(!Element,T)} cancelHandler 294 * @param {T=} context 295 * @template T 296 */ 297 WebInspector.EditingConfig = function(commitHandler, cancelHandler, context) 298 { 299 this.commitHandler = commitHandler; 300 this.cancelHandler = cancelHandler 301 this.context = context; 302 303 /** 304 * Handles the "paste" event, return values are the same as those for customFinishHandler 305 * @type {function(!Element)|undefined} 306 */ 307 this.pasteHandler; 308 309 /** 310 * Whether the edited element is multiline 311 * @type {boolean|undefined} 312 */ 313 this.multiline; 314 315 /** 316 * Custom finish handler for the editing session (invoked on keydown) 317 * @type {function(!Element,*)|undefined} 318 */ 319 this.customFinishHandler; 320 } 321 322 WebInspector.EditingConfig.prototype = { 323 setPasteHandler: function(pasteHandler) 324 { 325 this.pasteHandler = pasteHandler; 326 }, 327 328 /** 329 * @param {string} initialValue 330 * @param {!Object} mode 331 * @param {string} theme 332 * @param {boolean=} lineWrapping 333 * @param {boolean=} smartIndent 334 */ 335 setMultilineOptions: function(initialValue, mode, theme, lineWrapping, smartIndent) 336 { 337 this.multiline = true; 338 this.initialValue = initialValue; 339 this.mode = mode; 340 this.theme = theme; 341 this.lineWrapping = lineWrapping; 342 this.smartIndent = smartIndent; 343 }, 344 345 setCustomFinishHandler: function(customFinishHandler) 346 { 347 this.customFinishHandler = customFinishHandler; 348 } 349 } 350 351 WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/; 352 353 WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()"; 354 355 356 /** 357 * @param {!Event} event 358 * @return {?string} 359 */ 360 WebInspector._valueModificationDirection = function(event) 361 { 362 var direction = null; 363 if (event.type === "mousewheel") { 364 if (event.wheelDeltaY > 0) 365 direction = "Up"; 366 else if (event.wheelDeltaY < 0) 367 direction = "Down"; 368 } else { 369 if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp") 370 direction = "Up"; 371 else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown") 372 direction = "Down"; 373 } 374 return direction; 375 } 376 377 /** 378 * @param {string} hexString 379 * @param {!Event} event 380 */ 381 WebInspector._modifiedHexValue = function(hexString, event) 382 { 383 var direction = WebInspector._valueModificationDirection(event); 384 if (!direction) 385 return hexString; 386 387 var number = parseInt(hexString, 16); 388 if (isNaN(number) || !isFinite(number)) 389 return hexString; 390 391 var maxValue = Math.pow(16, hexString.length) - 1; 392 var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel"); 393 var delta; 394 395 if (arrowKeyOrMouseWheelEvent) 396 delta = (direction === "Up") ? 1 : -1; 397 else 398 delta = (event.keyIdentifier === "PageUp") ? 16 : -16; 399 400 if (event.shiftKey) 401 delta *= 16; 402 403 var result = number + delta; 404 if (result < 0) 405 result = 0; // Color hex values are never negative, so clamp to 0. 406 else if (result > maxValue) 407 return hexString; 408 409 // Ensure the result length is the same as the original hex value. 410 var resultString = result.toString(16).toUpperCase(); 411 for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i) 412 resultString = "0" + resultString; 413 return resultString; 414 } 415 416 /** 417 * @param {number} number 418 * @param {!Event} event 419 */ 420 WebInspector._modifiedFloatNumber = function(number, event) 421 { 422 var direction = WebInspector._valueModificationDirection(event); 423 if (!direction) 424 return number; 425 426 var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel"); 427 428 // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down. 429 // Also jump by 10 for page up and down, or by 100 if shift is held with a page key. 430 var changeAmount = 1; 431 if (event.shiftKey && !arrowKeyOrMouseWheelEvent) 432 changeAmount = 100; 433 else if (event.shiftKey || !arrowKeyOrMouseWheelEvent) 434 changeAmount = 10; 435 else if (event.altKey) 436 changeAmount = 0.1; 437 438 if (direction === "Down") 439 changeAmount *= -1; 440 441 // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns. 442 // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1. 443 var result = Number((number + changeAmount).toFixed(6)); 444 if (!String(result).match(WebInspector.CSSNumberRegex)) 445 return null; 446 447 return result; 448 } 449 450 /** 451 * @param {?Event} event 452 * @param {!Element} element 453 * @param {function(string,string)=} finishHandler 454 * @param {function(string)=} suggestionHandler 455 * @param {function(number):number=} customNumberHandler 456 * @return {boolean} 457 */ 458 WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler) 459 { 460 var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel"); 461 var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown"); 462 if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed) 463 return false; 464 465 var selection = window.getSelection(); 466 if (!selection.rangeCount) 467 return false; 468 469 var selectionRange = selection.getRangeAt(0); 470 if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element)) 471 return false; 472 473 var originalValue = element.textContent; 474 var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element); 475 var wordString = wordRange.toString(); 476 477 if (suggestionHandler && suggestionHandler(wordString)) 478 return false; 479 480 var replacementString; 481 var prefix, suffix, number; 482 483 var matches; 484 matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString); 485 if (matches && matches.length) { 486 prefix = matches[1]; 487 suffix = matches[3]; 488 number = WebInspector._modifiedHexValue(matches[2], event); 489 490 if (customNumberHandler) 491 number = customNumberHandler(number); 492 493 replacementString = prefix + number + suffix; 494 } else { 495 matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString); 496 if (matches && matches.length) { 497 prefix = matches[1]; 498 suffix = matches[3]; 499 number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event); 500 501 // Need to check for null explicitly. 502 if (number === null) 503 return false; 504 505 if (customNumberHandler) 506 number = customNumberHandler(number); 507 508 replacementString = prefix + number + suffix; 509 } 510 } 511 512 if (replacementString) { 513 var replacementTextNode = document.createTextNode(replacementString); 514 515 wordRange.deleteContents(); 516 wordRange.insertNode(replacementTextNode); 517 518 var finalSelectionRange = document.createRange(); 519 finalSelectionRange.setStart(replacementTextNode, 0); 520 finalSelectionRange.setEnd(replacementTextNode, replacementString.length); 521 522 selection.removeAllRanges(); 523 selection.addRange(finalSelectionRange); 524 525 event.handled = true; 526 event.preventDefault(); 527 528 if (finishHandler) 529 finishHandler(originalValue, replacementString); 530 531 return true; 532 } 533 return false; 534 } 535 536 /** 537 * @param {!Element} element 538 * @param {!WebInspector.EditingConfig=} config 539 * @return {?{cancel: function(), commit: function(), codeMirror: !CodeMirror, setWidth: function(number)}} 540 */ 541 WebInspector.startEditing = function(element, config) 542 { 543 if (!WebInspector.markBeingEdited(element, true)) 544 return null; 545 546 config = config || new WebInspector.EditingConfig(function() {}, function() {}); 547 var committedCallback = config.commitHandler; 548 var cancelledCallback = config.cancelHandler; 549 var pasteCallback = config.pasteHandler; 550 var context = config.context; 551 var isMultiline = config.multiline || false; 552 var oldText = isMultiline ? config.initialValue : getContent(element); 553 var moveDirection = ""; 554 var oldTabIndex; 555 var codeMirror; 556 var cssLoadView; 557 558 /** 559 * @param {?Event} e 560 */ 561 function consumeCopy(e) 562 { 563 e.consume(); 564 } 565 566 if (isMultiline) { 567 loadScript("CodeMirrorTextEditor.js"); 568 cssLoadView = new WebInspector.CodeMirrorCSSLoadView(); 569 cssLoadView.show(element); 570 WebInspector.setCurrentFocusElement(element); 571 element.addEventListener("copy", consumeCopy, false); 572 codeMirror = window.CodeMirror(element, { 573 mode: config.mode, 574 lineWrapping: config.lineWrapping, 575 smartIndent: config.smartIndent, 576 autofocus: true, 577 theme: config.theme, 578 value: oldText 579 }); 580 codeMirror.getWrapperElement().classList.add("source-code"); 581 codeMirror.on("cursorActivity", function(cm) { 582 cm.display.cursor.scrollIntoViewIfNeeded(false); 583 }); 584 } else { 585 element.classList.add("editing"); 586 587 oldTabIndex = element.getAttribute("tabIndex"); 588 if (typeof oldTabIndex !== "number" || oldTabIndex < 0) 589 element.tabIndex = 0; 590 WebInspector.setCurrentFocusElement(element); 591 } 592 593 /** 594 * @param {number} width 595 */ 596 function setWidth(width) 597 { 598 const padding = 30; 599 codeMirror.getWrapperElement().style.width = (width - codeMirror.getWrapperElement().offsetLeft - padding) + "px"; 600 codeMirror.refresh(); 601 } 602 603 /** 604 * @param {?Event=} e 605 */ 606 function blurEventListener(e) { 607 if (!isMultiline || !e || !e.relatedTarget || !e.relatedTarget.isSelfOrDescendant(element)) 608 editingCommitted.call(element); 609 } 610 611 function getContent(element) { 612 if (isMultiline) 613 return codeMirror.getValue(); 614 615 if (element.tagName === "INPUT" && element.type === "text") 616 return element.value; 617 618 return element.textContent; 619 } 620 621 /** @this {Element} */ 622 function cleanUpAfterEditing() 623 { 624 WebInspector.markBeingEdited(element, false); 625 626 element.removeEventListener("blur", blurEventListener, isMultiline); 627 element.removeEventListener("keydown", keyDownEventListener, true); 628 if (pasteCallback) 629 element.removeEventListener("paste", pasteEventListener, true); 630 631 WebInspector.restoreFocusFromElement(element); 632 633 if (isMultiline) { 634 element.removeEventListener("copy", consumeCopy, false); 635 cssLoadView.detach(); 636 return; 637 } 638 639 this.classList.remove("editing"); 640 641 if (typeof oldTabIndex !== "number") 642 element.removeAttribute("tabIndex"); 643 else 644 this.tabIndex = oldTabIndex; 645 this.scrollTop = 0; 646 this.scrollLeft = 0; 647 } 648 649 /** @this {Element} */ 650 function editingCancelled() 651 { 652 if (isMultiline) 653 codeMirror.setValue(oldText); 654 else { 655 if (this.tagName === "INPUT" && this.type === "text") 656 this.value = oldText; 657 else 658 this.textContent = oldText; 659 } 660 661 cleanUpAfterEditing.call(this); 662 663 cancelledCallback(this, context); 664 } 665 666 /** @this {Element} */ 667 function editingCommitted() 668 { 669 cleanUpAfterEditing.call(this); 670 671 committedCallback(this, getContent(this), oldText, context, moveDirection); 672 } 673 674 function defaultFinishHandler(event) 675 { 676 var isMetaOrCtrl = WebInspector.isMac() ? 677 event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey : 678 event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey; 679 if (isEnterKey(event) && (event.isMetaOrCtrlForTest || !isMultiline || isMetaOrCtrl)) 680 return "commit"; 681 else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code || event.keyIdentifier === "U+001B") 682 return "cancel"; 683 else if (!isMultiline && event.keyIdentifier === "U+0009") // Tab key 684 return "move-" + (event.shiftKey ? "backward" : "forward"); 685 } 686 687 function handleEditingResult(result, event) 688 { 689 if (result === "commit") { 690 editingCommitted.call(element); 691 event.consume(true); 692 } else if (result === "cancel") { 693 editingCancelled.call(element); 694 event.consume(true); 695 } else if (result && result.startsWith("move-")) { 696 moveDirection = result.substring(5); 697 if (event.keyIdentifier !== "U+0009") 698 blurEventListener(); 699 } 700 } 701 702 function pasteEventListener(event) 703 { 704 var result = pasteCallback(event); 705 handleEditingResult(result, event); 706 } 707 708 function keyDownEventListener(event) 709 { 710 var handler = config.customFinishHandler || defaultFinishHandler; 711 var result = handler(event); 712 handleEditingResult(result, event); 713 } 714 715 element.addEventListener("blur", blurEventListener, isMultiline); 716 element.addEventListener("keydown", keyDownEventListener, true); 717 if (pasteCallback) 718 element.addEventListener("paste", pasteEventListener, true); 719 720 return { 721 cancel: editingCancelled.bind(element), 722 commit: editingCommitted.bind(element), 723 codeMirror: codeMirror, // For testing. 724 setWidth: setWidth 725 }; 726 } 727 728 /** 729 * @param {number} seconds 730 * @param {boolean=} higherResolution 731 * @return {string} 732 */ 733 Number.secondsToString = function(seconds, higherResolution) 734 { 735 if (!isFinite(seconds)) 736 return "-"; 737 738 if (seconds === 0) 739 return "0"; 740 741 var ms = seconds * 1000; 742 if (higherResolution && ms < 1000) 743 return WebInspector.UIString("%.3f\u2009ms", ms); 744 else if (ms < 1000) 745 return WebInspector.UIString("%.0f\u2009ms", ms); 746 747 if (seconds < 60) 748 return WebInspector.UIString("%.2f\u2009s", seconds); 749 750 var minutes = seconds / 60; 751 if (minutes < 60) 752 return WebInspector.UIString("%.1f\u2009min", minutes); 753 754 var hours = minutes / 60; 755 if (hours < 24) 756 return WebInspector.UIString("%.1f\u2009hrs", hours); 757 758 var days = hours / 24; 759 return WebInspector.UIString("%.1f\u2009days", days); 760 } 761 762 /** 763 * @param {number} bytes 764 * @return {string} 765 */ 766 Number.bytesToString = function(bytes) 767 { 768 if (bytes < 1024) 769 return WebInspector.UIString("%.0f\u2009B", bytes); 770 771 var kilobytes = bytes / 1024; 772 if (kilobytes < 100) 773 return WebInspector.UIString("%.1f\u2009KB", kilobytes); 774 if (kilobytes < 1024) 775 return WebInspector.UIString("%.0f\u2009KB", kilobytes); 776 777 var megabytes = kilobytes / 1024; 778 if (megabytes < 100) 779 return WebInspector.UIString("%.1f\u2009MB", megabytes); 780 else 781 return WebInspector.UIString("%.0f\u2009MB", megabytes); 782 } 783 784 Number.withThousandsSeparator = function(num) 785 { 786 var str = num + ""; 787 var re = /(\d+)(\d{3})/; 788 while (str.match(re)) 789 str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space. 790 return str; 791 } 792 793 WebInspector.useLowerCaseMenuTitles = function() 794 { 795 return WebInspector.platform() === "windows"; 796 } 797 798 WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append) 799 { 800 return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append); 801 } 802 803 WebInspector.openLinkExternallyLabel = function() 804 { 805 return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab"); 806 } 807 808 WebInspector.copyLinkAddressLabel = function() 809 { 810 return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address"); 811 } 812 813 WebInspector.platform = function() 814 { 815 if (!WebInspector._platform) 816 WebInspector._platform = InspectorFrontendHost.platform(); 817 return WebInspector._platform; 818 } 819 820 WebInspector.isMac = function() 821 { 822 if (typeof WebInspector._isMac === "undefined") 823 WebInspector._isMac = WebInspector.platform() === "mac"; 824 825 return WebInspector._isMac; 826 } 827 828 WebInspector.isWin = function() 829 { 830 if (typeof WebInspector._isWin === "undefined") 831 WebInspector._isWin = WebInspector.platform() === "windows"; 832 833 return WebInspector._isWin; 834 } 835 836 WebInspector.PlatformFlavor = { 837 WindowsVista: "windows-vista", 838 MacTiger: "mac-tiger", 839 MacLeopard: "mac-leopard", 840 MacSnowLeopard: "mac-snowleopard", 841 MacLion: "mac-lion", 842 MacMountainLion: "mac-mountain-lion" 843 } 844 845 WebInspector.platformFlavor = function() 846 { 847 function detectFlavor() 848 { 849 const userAgent = navigator.userAgent; 850 851 if (WebInspector.platform() === "windows") { 852 var match = userAgent.match(/Windows NT (\d+)\.(?:\d+)/); 853 if (match && match[1] >= 6) 854 return WebInspector.PlatformFlavor.WindowsVista; 855 return null; 856 } else if (WebInspector.platform() === "mac") { 857 var match = userAgent.match(/Mac OS X\s*(?:(\d+)_(\d+))?/); 858 if (!match || match[1] != 10) 859 return WebInspector.PlatformFlavor.MacSnowLeopard; 860 switch (Number(match[2])) { 861 case 4: 862 return WebInspector.PlatformFlavor.MacTiger; 863 case 5: 864 return WebInspector.PlatformFlavor.MacLeopard; 865 case 6: 866 return WebInspector.PlatformFlavor.MacSnowLeopard; 867 case 7: 868 return WebInspector.PlatformFlavor.MacLion; 869 case 8: 870 return WebInspector.PlatformFlavor.MacMountainLion; 871 default: 872 return ""; 873 } 874 } 875 } 876 877 if (!WebInspector._platformFlavor) 878 WebInspector._platformFlavor = detectFlavor(); 879 880 return WebInspector._platformFlavor; 881 } 882 883 WebInspector.port = function() 884 { 885 if (!WebInspector._port) 886 WebInspector._port = InspectorFrontendHost.port(); 887 888 return WebInspector._port; 889 } 890 891 WebInspector.installPortStyles = function() 892 { 893 var platform = WebInspector.platform(); 894 document.body.classList.add("platform-" + platform); 895 var flavor = WebInspector.platformFlavor(); 896 if (flavor) 897 document.body.classList.add("platform-" + flavor); 898 var port = WebInspector.port(); 899 document.body.classList.add("port-" + port); 900 } 901 902 WebInspector._windowFocused = function(event) 903 { 904 if (event.target.document.nodeType === Node.DOCUMENT_NODE) 905 document.body.classList.remove("inactive"); 906 } 907 908 WebInspector._windowBlurred = function(event) 909 { 910 if (event.target.document.nodeType === Node.DOCUMENT_NODE) 911 document.body.classList.add("inactive"); 912 } 913 914 WebInspector.previousFocusElement = function() 915 { 916 return WebInspector._previousFocusElement; 917 } 918 919 WebInspector.currentFocusElement = function() 920 { 921 return WebInspector._currentFocusElement; 922 } 923 924 WebInspector._focusChanged = function(event) 925 { 926 WebInspector.setCurrentFocusElement(event.target); 927 } 928 929 WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet(); 930 WebInspector._isTextEditingElement = function(element) 931 { 932 if (element instanceof HTMLInputElement) 933 return element.type in WebInspector._textInputTypes; 934 935 if (element instanceof HTMLTextAreaElement) 936 return true; 937 938 return false; 939 } 940 941 WebInspector.setCurrentFocusElement = function(x) 942 { 943 if (WebInspector._glassPane && x && !WebInspector._glassPane.element.isAncestor(x)) 944 return; 945 if (WebInspector._currentFocusElement !== x) 946 WebInspector._previousFocusElement = WebInspector._currentFocusElement; 947 WebInspector._currentFocusElement = x; 948 949 if (WebInspector._currentFocusElement) { 950 WebInspector._currentFocusElement.focus(); 951 952 // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside. 953 // This is needed (at least) to remove caret from console when focus is moved to some element in the panel. 954 // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check. 955 var selection = window.getSelection(); 956 if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) { 957 var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange(); 958 selectionRange.setStart(WebInspector._currentFocusElement, 0); 959 selectionRange.setEnd(WebInspector._currentFocusElement, 0); 960 961 selection.removeAllRanges(); 962 selection.addRange(selectionRange); 963 } 964 } else if (WebInspector._previousFocusElement) 965 WebInspector._previousFocusElement.blur(); 966 } 967 968 WebInspector.restoreFocusFromElement = function(element) 969 { 970 if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement())) 971 WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement()); 972 } 973 974 WebInspector.setToolbarColors = function(backgroundColor, color) 975 { 976 if (!WebInspector._themeStyleElement) { 977 WebInspector._themeStyleElement = document.createElement("style"); 978 document.head.appendChild(WebInspector._themeStyleElement); 979 } 980 var parsedColor = WebInspector.Color.parse(color); 981 var shadowColor = parsedColor ? parsedColor.invert().setAlpha(0.33).toString(WebInspector.Color.Format.RGBA) : "white"; 982 var prefix = WebInspector.isMac() ? "body:not(.undocked)" : ""; 983 WebInspector._themeStyleElement.textContent = 984 String.sprintf( 985 "%s .toolbar-background {\ 986 background-image: none !important;\ 987 background-color: %s !important;\ 988 color: %s !important;\ 989 }", prefix, backgroundColor, color) + 990 String.sprintf( 991 "%s .toolbar-background button.status-bar-item .glyph, %s .toolbar-background button.status-bar-item .long-click-glyph {\ 992 background-color: %s;\ 993 }", prefix, prefix, color) + 994 String.sprintf( 995 "%s .toolbar-background button.status-bar-item .glyph.shadow, %s .toolbar-background button.status-bar-item .long-click-glyph.shadow {\ 996 background-color: %s;\ 997 }", prefix, prefix, shadowColor); 998 } 999 1000 WebInspector.resetToolbarColors = function() 1001 { 1002 if (WebInspector._themeStyleElement) 1003 WebInspector._themeStyleElement.textContent = ""; 1004 } 1005 1006 /** 1007 * @param {!Element} element 1008 * @param {number} offset 1009 * @param {number} length 1010 * @param {!Array.<!Object>=} domChanges 1011 */ 1012 WebInspector.highlightSearchResult = function(element, offset, length, domChanges) 1013 { 1014 var result = WebInspector.highlightSearchResults(element, [new WebInspector.SourceRange(offset, length)], domChanges); 1015 return result.length ? result[0] : null; 1016 } 1017 1018 /** 1019 * @param {!Element} element 1020 * @param {!Array.<!WebInspector.SourceRange>} resultRanges 1021 * @param {!Array.<!Object>=} changes 1022 */ 1023 WebInspector.highlightSearchResults = function(element, resultRanges, changes) 1024 { 1025 return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "highlighted-search-result", changes); 1026 } 1027 1028 /** 1029 * @param {!Element} element 1030 * @param {!Array.<!WebInspector.SourceRange>} resultRanges 1031 * @param {string} styleClass 1032 * @param {!Array.<!Object>=} changes 1033 */ 1034 WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes) 1035 { 1036 changes = changes || []; 1037 var highlightNodes = []; 1038 var lineText = element.textContent; 1039 var ownerDocument = element.ownerDocument; 1040 var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); 1041 1042 var snapshotLength = textNodeSnapshot.snapshotLength; 1043 if (snapshotLength === 0) 1044 return highlightNodes; 1045 1046 var nodeRanges = []; 1047 var rangeEndOffset = 0; 1048 for (var i = 0; i < snapshotLength; ++i) { 1049 var range = {}; 1050 range.offset = rangeEndOffset; 1051 range.length = textNodeSnapshot.snapshotItem(i).textContent.length; 1052 rangeEndOffset = range.offset + range.length; 1053 nodeRanges.push(range); 1054 } 1055 1056 var startIndex = 0; 1057 for (var i = 0; i < resultRanges.length; ++i) { 1058 var startOffset = resultRanges[i].offset; 1059 var endOffset = startOffset + resultRanges[i].length; 1060 1061 while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset) 1062 startIndex++; 1063 var endIndex = startIndex; 1064 while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset) 1065 endIndex++; 1066 if (endIndex === snapshotLength) 1067 break; 1068 1069 var highlightNode = ownerDocument.createElement("span"); 1070 highlightNode.className = styleClass; 1071 highlightNode.textContent = lineText.substring(startOffset, endOffset); 1072 1073 var lastTextNode = textNodeSnapshot.snapshotItem(endIndex); 1074 var lastText = lastTextNode.textContent; 1075 lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset); 1076 changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent }); 1077 1078 if (startIndex === endIndex) { 1079 lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode); 1080 changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement }); 1081 highlightNodes.push(highlightNode); 1082 1083 var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset)); 1084 lastTextNode.parentElement.insertBefore(prefixNode, highlightNode); 1085 changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement }); 1086 } else { 1087 var firstTextNode = textNodeSnapshot.snapshotItem(startIndex); 1088 var firstText = firstTextNode.textContent; 1089 var anchorElement = firstTextNode.nextSibling; 1090 1091 firstTextNode.parentElement.insertBefore(highlightNode, anchorElement); 1092 changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement }); 1093 highlightNodes.push(highlightNode); 1094 1095 firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset); 1096 changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent }); 1097 1098 for (var j = startIndex + 1; j < endIndex; j++) { 1099 var textNode = textNodeSnapshot.snapshotItem(j); 1100 var text = textNode.textContent; 1101 textNode.textContent = ""; 1102 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent }); 1103 } 1104 } 1105 startIndex = endIndex; 1106 nodeRanges[startIndex].offset = endOffset; 1107 nodeRanges[startIndex].length = lastTextNode.textContent.length; 1108 1109 } 1110 return highlightNodes; 1111 } 1112 1113 WebInspector.applyDomChanges = function(domChanges) 1114 { 1115 for (var i = 0, size = domChanges.length; i < size; ++i) { 1116 var entry = domChanges[i]; 1117 switch (entry.type) { 1118 case "added": 1119 entry.parent.insertBefore(entry.node, entry.nextSibling); 1120 break; 1121 case "changed": 1122 entry.node.textContent = entry.newText; 1123 break; 1124 } 1125 } 1126 } 1127 1128 WebInspector.revertDomChanges = function(domChanges) 1129 { 1130 for (var i = domChanges.length - 1; i >= 0; --i) { 1131 var entry = domChanges[i]; 1132 switch (entry.type) { 1133 case "added": 1134 entry.node.remove(); 1135 break; 1136 case "changed": 1137 entry.node.textContent = entry.oldText; 1138 break; 1139 } 1140 } 1141 } 1142 1143 WebInspector._coalescingLevel = 0; 1144 1145 WebInspector.startBatchUpdate = function() 1146 { 1147 if (!WebInspector._coalescingLevel) 1148 WebInspector._postUpdateHandlers = new Map(); 1149 WebInspector._coalescingLevel++; 1150 } 1151 1152 WebInspector.endBatchUpdate = function() 1153 { 1154 if (--WebInspector._coalescingLevel) 1155 return; 1156 1157 var handlers = WebInspector._postUpdateHandlers; 1158 delete WebInspector._postUpdateHandlers; 1159 1160 var keys = handlers.keys(); 1161 for (var i = 0; i < keys.length; ++i) { 1162 var object = keys[i]; 1163 var methods = handlers.get(object).keys(); 1164 for (var j = 0; j < methods.length; ++j) 1165 methods[j].call(object); 1166 } 1167 } 1168 1169 /** 1170 * @param {!Object} object 1171 * @param {function()} method 1172 */ 1173 WebInspector.invokeOnceAfterBatchUpdate = function(object, method) 1174 { 1175 if (!WebInspector._coalescingLevel) { 1176 method.call(object); 1177 return; 1178 } 1179 1180 var methods = WebInspector._postUpdateHandlers.get(object); 1181 if (!methods) { 1182 methods = new Map(); 1183 WebInspector._postUpdateHandlers.put(object, methods); 1184 } 1185 methods.put(method); 1186 } 1187 1188 /** 1189 * This bogus view is needed to load/unload CodeMirror-related CSS on demand. 1190 * 1191 * @constructor 1192 * @extends {WebInspector.View} 1193 */ 1194 WebInspector.CodeMirrorCSSLoadView = function() 1195 { 1196 WebInspector.View.call(this); 1197 this.element.classList.add("hidden"); 1198 this.registerRequiredCSS("cm/codemirror.css"); 1199 this.registerRequiredCSS("cm/cmdevtools.css"); 1200 } 1201 1202 WebInspector.CodeMirrorCSSLoadView.prototype = { 1203 __proto__: WebInspector.View.prototype 1204 } 1205 1206 ;(function() { 1207 1208 /** 1209 * @this {Window} 1210 */ 1211 function windowLoaded() 1212 { 1213 window.addEventListener("focus", WebInspector._windowFocused, false); 1214 window.addEventListener("blur", WebInspector._windowBlurred, false); 1215 document.addEventListener("focus", WebInspector._focusChanged.bind(this), true); 1216 window.removeEventListener("DOMContentLoaded", windowLoaded, false); 1217 } 1218 1219 window.addEventListener("DOMContentLoaded", windowLoaded, false); 1220 1221 })(); 1222