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