Home | History | Annotate | Download | only in ui
      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.GlassPane.DefaultFocusedViewStack.length)
    160             WebInspector.GlassPane.DefaultFocusedViewStack.peekLast().focus();
    161         this.element.remove();
    162     }
    163 }
    164 
    165 /**
    166  * @type {!Array.<!WebInspector.View|!WebInspector.Dialog>}
    167  */
    168 WebInspector.GlassPane.DefaultFocusedViewStack = [];
    169 
    170 /**
    171  * @param {?Node=} node
    172  * @return {boolean}
    173  */
    174 WebInspector.isBeingEdited = function(node)
    175 {
    176     if (!node || node.nodeType !== Node.ELEMENT_NODE)
    177         return false;
    178     var element = /** {!Element} */ (node);
    179     if (element.classList.contains("text-prompt") || element.nodeName === "INPUT" || element.nodeName === "TEXTAREA")
    180         return true;
    181 
    182     if (!WebInspector.__editingCount)
    183         return false;
    184 
    185     while (element) {
    186         if (element.__editing)
    187             return true;
    188         element = element.parentElement;
    189     }
    190     return false;
    191 }
    192 
    193 /**
    194  * @param {!Element} element
    195  * @param {boolean} value
    196  * @return {boolean}
    197  */
    198 WebInspector.markBeingEdited = function(element, value)
    199 {
    200     if (value) {
    201         if (element.__editing)
    202             return false;
    203         element.classList.add("being-edited");
    204         element.__editing = true;
    205         WebInspector.__editingCount = (WebInspector.__editingCount || 0) + 1;
    206     } else {
    207         if (!element.__editing)
    208             return false;
    209         element.classList.remove("being-edited");
    210         delete element.__editing;
    211         --WebInspector.__editingCount;
    212     }
    213     return true;
    214 }
    215 
    216 WebInspector.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/;
    217 
    218 WebInspector.StyleValueDelimiters = " \xA0\t\n\"':;,/()";
    219 
    220 
    221 /**
    222   * @param {!Event} event
    223   * @return {?string}
    224   */
    225 WebInspector._valueModificationDirection = function(event)
    226 {
    227     var direction = null;
    228     if (event.type === "mousewheel") {
    229         if (event.wheelDeltaY > 0)
    230             direction = "Up";
    231         else if (event.wheelDeltaY < 0)
    232             direction = "Down";
    233     } else {
    234         if (event.keyIdentifier === "Up" || event.keyIdentifier === "PageUp")
    235             direction = "Up";
    236         else if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
    237             direction = "Down";
    238     }
    239     return direction;
    240 }
    241 
    242 /**
    243  * @param {string} hexString
    244  * @param {!Event} event
    245  */
    246 WebInspector._modifiedHexValue = function(hexString, event)
    247 {
    248     var direction = WebInspector._valueModificationDirection(event);
    249     if (!direction)
    250         return hexString;
    251 
    252     var number = parseInt(hexString, 16);
    253     if (isNaN(number) || !isFinite(number))
    254         return hexString;
    255 
    256     var maxValue = Math.pow(16, hexString.length) - 1;
    257     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
    258     var delta;
    259 
    260     if (arrowKeyOrMouseWheelEvent)
    261         delta = (direction === "Up") ? 1 : -1;
    262     else
    263         delta = (event.keyIdentifier === "PageUp") ? 16 : -16;
    264 
    265     if (event.shiftKey)
    266         delta *= 16;
    267 
    268     var result = number + delta;
    269     if (result < 0)
    270         result = 0; // Color hex values are never negative, so clamp to 0.
    271     else if (result > maxValue)
    272         return hexString;
    273 
    274     // Ensure the result length is the same as the original hex value.
    275     var resultString = result.toString(16).toUpperCase();
    276     for (var i = 0, lengthDelta = hexString.length - resultString.length; i < lengthDelta; ++i)
    277         resultString = "0" + resultString;
    278     return resultString;
    279 }
    280 
    281 /**
    282  * @param {number} number
    283  * @param {!Event} event
    284  */
    285 WebInspector._modifiedFloatNumber = function(number, event)
    286 {
    287     var direction = WebInspector._valueModificationDirection(event);
    288     if (!direction)
    289         return number;
    290 
    291     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
    292 
    293     // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
    294     // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
    295     var changeAmount = 1;
    296     if (event.shiftKey && !arrowKeyOrMouseWheelEvent)
    297         changeAmount = 100;
    298     else if (event.shiftKey || !arrowKeyOrMouseWheelEvent)
    299         changeAmount = 10;
    300     else if (event.altKey)
    301         changeAmount = 0.1;
    302 
    303     if (direction === "Down")
    304         changeAmount *= -1;
    305 
    306     // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
    307     // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
    308     var result = Number((number + changeAmount).toFixed(6));
    309     if (!String(result).match(WebInspector.CSSNumberRegex))
    310         return null;
    311 
    312     return result;
    313 }
    314 
    315 /**
    316   * @param {!Event} event
    317   * @param {!Element} element
    318   * @param {function(string,string)=} finishHandler
    319   * @param {function(string)=} suggestionHandler
    320   * @param {function(string, number, string):string=} customNumberHandler
    321   * @return {boolean}
    322  */
    323 WebInspector.handleElementValueModifications = function(event, element, finishHandler, suggestionHandler, customNumberHandler)
    324 {
    325     var arrowKeyOrMouseWheelEvent = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down" || event.type === "mousewheel");
    326     var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown");
    327     if (!arrowKeyOrMouseWheelEvent && !pageKeyPressed)
    328         return false;
    329 
    330     var selection = window.getSelection();
    331     if (!selection.rangeCount)
    332         return false;
    333 
    334     var selectionRange = selection.getRangeAt(0);
    335     if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element))
    336         return false;
    337 
    338     var originalValue = element.textContent;
    339     var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.StyleValueDelimiters, element);
    340     var wordString = wordRange.toString();
    341 
    342     if (suggestionHandler && suggestionHandler(wordString))
    343         return false;
    344 
    345     var replacementString;
    346     var prefix, suffix, number;
    347 
    348     var matches;
    349     matches = /(.*#)([\da-fA-F]+)(.*)/.exec(wordString);
    350     if (matches && matches.length) {
    351         prefix = matches[1];
    352         suffix = matches[3];
    353         number = WebInspector._modifiedHexValue(matches[2], event);
    354 
    355         replacementString = customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix;
    356     } else {
    357         matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString);
    358         if (matches && matches.length) {
    359             prefix = matches[1];
    360             suffix = matches[3];
    361             number = WebInspector._modifiedFloatNumber(parseFloat(matches[2]), event);
    362 
    363             // Need to check for null explicitly.
    364             if (number === null)
    365                 return false;
    366 
    367             replacementString = customNumberHandler ? customNumberHandler(prefix, number, suffix) : prefix + number + suffix;
    368         }
    369     }
    370 
    371     if (replacementString) {
    372         var replacementTextNode = document.createTextNode(replacementString);
    373 
    374         wordRange.deleteContents();
    375         wordRange.insertNode(replacementTextNode);
    376 
    377         var finalSelectionRange = document.createRange();
    378         finalSelectionRange.setStart(replacementTextNode, 0);
    379         finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
    380 
    381         selection.removeAllRanges();
    382         selection.addRange(finalSelectionRange);
    383 
    384         event.handled = true;
    385         event.preventDefault();
    386 
    387         if (finishHandler)
    388             finishHandler(originalValue, replacementString);
    389 
    390         return true;
    391     }
    392     return false;
    393 }
    394 
    395 /**
    396  * @param {number} ms
    397  * @param {number=} precision
    398  * @return {string}
    399  */
    400 Number.preciseMillisToString = function(ms, precision)
    401 {
    402   precision = precision || 0;
    403   var format = "%." + precision + "f\u2009ms";
    404   return WebInspector.UIString(format, ms);
    405 }
    406 
    407 /** @type {!WebInspector.UIStringFormat} */
    408 WebInspector._subMillisFormat = new WebInspector.UIStringFormat("%.3f\u2009ms");
    409 
    410 /** @type {!WebInspector.UIStringFormat} */
    411 WebInspector._millisFormat = new WebInspector.UIStringFormat("%.0f\u2009ms");
    412 
    413 /** @type {!WebInspector.UIStringFormat} */
    414 WebInspector._secondsFormat = new WebInspector.UIStringFormat("%.2f\u2009s");
    415 
    416 /** @type {!WebInspector.UIStringFormat} */
    417 WebInspector._minutesFormat = new WebInspector.UIStringFormat("%.1f\u2009min");
    418 
    419 /** @type {!WebInspector.UIStringFormat} */
    420 WebInspector._hoursFormat = new WebInspector.UIStringFormat("%.1f\u2009hrs");
    421 
    422 /** @type {!WebInspector.UIStringFormat} */
    423 WebInspector._daysFormat = new WebInspector.UIStringFormat("%.1f\u2009days");
    424 
    425 /**
    426  * @param {number} ms
    427  * @param {boolean=} higherResolution
    428  * @return {string}
    429  */
    430 Number.millisToString = function(ms, higherResolution)
    431 {
    432     if (!isFinite(ms))
    433         return "-";
    434 
    435     if (ms === 0)
    436         return "0";
    437 
    438     if (higherResolution && ms < 1000)
    439         return WebInspector._subMillisFormat.format(ms);
    440     else if (ms < 1000)
    441         return WebInspector._millisFormat.format(ms);
    442 
    443     var seconds = ms / 1000;
    444     if (seconds < 60)
    445         return WebInspector._secondsFormat.format(seconds);
    446 
    447     var minutes = seconds / 60;
    448     if (minutes < 60)
    449         return WebInspector._minutesFormat.format(minutes);
    450 
    451     var hours = minutes / 60;
    452     if (hours < 24)
    453         return WebInspector._hoursFormat.format(hours);
    454 
    455     var days = hours / 24;
    456     return WebInspector._daysFormat.format(days);
    457 }
    458 
    459 /**
    460  * @param {number} seconds
    461  * @param {boolean=} higherResolution
    462  * @return {string}
    463  */
    464 Number.secondsToString = function(seconds, higherResolution)
    465 {
    466     if (!isFinite(seconds))
    467         return "-";
    468     return Number.millisToString(seconds * 1000, higherResolution);
    469 }
    470 
    471 /**
    472  * @param {number} bytes
    473  * @return {string}
    474  */
    475 Number.bytesToString = function(bytes)
    476 {
    477     if (bytes < 1024)
    478         return WebInspector.UIString("%.0f\u2009B", bytes);
    479 
    480     var kilobytes = bytes / 1024;
    481     if (kilobytes < 100)
    482         return WebInspector.UIString("%.1f\u2009KB", kilobytes);
    483     if (kilobytes < 1024)
    484         return WebInspector.UIString("%.0f\u2009KB", kilobytes);
    485 
    486     var megabytes = kilobytes / 1024;
    487     if (megabytes < 100)
    488         return WebInspector.UIString("%.1f\u2009MB", megabytes);
    489     else
    490         return WebInspector.UIString("%.0f\u2009MB", megabytes);
    491 }
    492 
    493 /**
    494  * @param {number} num
    495  * @return {string}
    496  */
    497 Number.withThousandsSeparator = function(num)
    498 {
    499     var str = num + "";
    500     var re = /(\d+)(\d{3})/;
    501     while (str.match(re))
    502         str = str.replace(re, "$1\u2009$2"); // \u2009 is a thin space.
    503     return str;
    504 }
    505 
    506 /**
    507  * @return {boolean}
    508  */
    509 WebInspector.useLowerCaseMenuTitles = function()
    510 {
    511     return WebInspector.platform() === "windows";
    512 }
    513 
    514 /**
    515  * @param {string} format
    516  * @param {?ArrayLike} substitutions
    517  * @param {!Object.<string, function(string, ...):*>} formatters
    518  * @param {string} initialValue
    519  * @param {function(string, string): ?} append
    520  * @return {!{formattedResult: string, unusedSubstitutions: ?ArrayLike}};
    521  */
    522 WebInspector.formatLocalized = function(format, substitutions, formatters, initialValue, append)
    523 {
    524     return String.format(WebInspector.UIString(format), substitutions, formatters, initialValue, append);
    525 }
    526 
    527 /**
    528  * @return {string}
    529  */
    530 WebInspector.openLinkExternallyLabel = function()
    531 {
    532     return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Open link in new tab" : "Open Link in New Tab");
    533 }
    534 
    535 /**
    536  * @return {string}
    537  */
    538 WebInspector.copyLinkAddressLabel = function()
    539 {
    540     return WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Copy link address" : "Copy Link Address");
    541 }
    542 
    543 /**
    544  * @return {string}
    545  */
    546 WebInspector.anotherProfilerActiveLabel = function()
    547 {
    548     return WebInspector.UIString("Another profiler is already active");
    549 }
    550 
    551 /**
    552  * @param {string|undefined} description
    553  * @return {string}
    554  */
    555 WebInspector.asyncStackTraceLabel = function(description)
    556 {
    557     if (description)
    558         return description + " " + WebInspector.UIString("(async)");
    559     return WebInspector.UIString("Async Call");
    560 }
    561 
    562 /**
    563  * @return {string}
    564  */
    565 WebInspector.manageBlackboxingButtonLabel = function()
    566 {
    567     return WebInspector.UIString("Manage framework blackboxing...");
    568 }
    569 
    570 WebInspector.installPortStyles = function()
    571 {
    572     var platform = WebInspector.platform();
    573     document.body.classList.add("platform-" + platform);
    574     var flavor = WebInspector.platformFlavor();
    575     if (flavor)
    576         document.body.classList.add("platform-" + flavor);
    577     var port = WebInspector.port();
    578     document.body.classList.add("port-" + port);
    579 }
    580 
    581 WebInspector._windowFocused = function(event)
    582 {
    583     if (event.target.document.nodeType === Node.DOCUMENT_NODE)
    584         document.body.classList.remove("inactive");
    585 }
    586 
    587 WebInspector._windowBlurred = function(event)
    588 {
    589     if (event.target.document.nodeType === Node.DOCUMENT_NODE)
    590         document.body.classList.add("inactive");
    591 }
    592 
    593 /**
    594  * @return {!Element}
    595  */
    596 WebInspector.previousFocusElement = function()
    597 {
    598     return WebInspector._previousFocusElement;
    599 }
    600 
    601 /**
    602  * @return {!Element}
    603  */
    604 WebInspector.currentFocusElement = function()
    605 {
    606     return WebInspector._currentFocusElement;
    607 }
    608 
    609 WebInspector._focusChanged = function(event)
    610 {
    611     WebInspector.setCurrentFocusElement(event.target);
    612 }
    613 
    614 WebInspector._documentBlurred = function(event)
    615 {
    616     // We want to know when currentFocusElement loses focus to nowhere.
    617     // This is the case when event.relatedTarget is null (no element is being focused)
    618     // and document.activeElement is reset to default (this is not a window blur).
    619     if (!event.relatedTarget && document.activeElement === document.body)
    620       WebInspector.setCurrentFocusElement(null);
    621 }
    622 
    623 WebInspector._textInputTypes = ["text", "search", "tel", "url", "email", "password"].keySet();
    624 WebInspector._isTextEditingElement = function(element)
    625 {
    626     if (element instanceof HTMLInputElement)
    627         return element.type in WebInspector._textInputTypes;
    628 
    629     if (element instanceof HTMLTextAreaElement)
    630         return true;
    631 
    632     return false;
    633 }
    634 
    635 WebInspector.setCurrentFocusElement = function(x)
    636 {
    637     if (WebInspector._glassPane && x && !WebInspector._glassPane.element.isAncestor(x))
    638         return;
    639     if (WebInspector._currentFocusElement !== x)
    640         WebInspector._previousFocusElement = WebInspector._currentFocusElement;
    641     WebInspector._currentFocusElement = x;
    642 
    643     if (WebInspector._currentFocusElement) {
    644         WebInspector._currentFocusElement.focus();
    645 
    646         // Make a caret selection inside the new element if there isn't a range selection and there isn't already a caret selection inside.
    647         // This is needed (at least) to remove caret from console when focus is moved to some element in the panel.
    648         // The code below should not be applied to text fields and text areas, hence _isTextEditingElement check.
    649         var selection = window.getSelection();
    650         if (!WebInspector._isTextEditingElement(WebInspector._currentFocusElement) && selection.isCollapsed && !WebInspector._currentFocusElement.isInsertionCaretInside()) {
    651             var selectionRange = WebInspector._currentFocusElement.ownerDocument.createRange();
    652             selectionRange.setStart(WebInspector._currentFocusElement, 0);
    653             selectionRange.setEnd(WebInspector._currentFocusElement, 0);
    654 
    655             selection.removeAllRanges();
    656             selection.addRange(selectionRange);
    657         }
    658     } else if (WebInspector._previousFocusElement)
    659         WebInspector._previousFocusElement.blur();
    660 }
    661 
    662 WebInspector.restoreFocusFromElement = function(element)
    663 {
    664     if (element && element.isSelfOrAncestor(WebInspector.currentFocusElement()))
    665         WebInspector.setCurrentFocusElement(WebInspector.previousFocusElement());
    666 }
    667 
    668 WebInspector.setToolbarColors = function(backgroundColor, color)
    669 {
    670     if (!WebInspector._themeStyleElement) {
    671         WebInspector._themeStyleElement = document.createElement("style");
    672         document.head.appendChild(WebInspector._themeStyleElement);
    673     }
    674     var parsedColor = WebInspector.Color.parse(color);
    675     var shadowColor = parsedColor ? parsedColor.invert().setAlpha(0.33).toString(WebInspector.Color.Format.RGBA) : "white";
    676     var prefix = WebInspector.isMac() ? "body:not(.undocked)" : "";
    677     WebInspector._themeStyleElement.textContent =
    678         String.sprintf(
    679             "%s .toolbar-colors {\
    680                  background-image: none !important;\
    681                  background-color: %s !important;\
    682                  color: %s !important;\
    683              }", prefix, backgroundColor, color) +
    684         String.sprintf(
    685              "%s .toolbar-colors button.status-bar-item .glyph, %s .toolbar-colors button.status-bar-item .long-click-glyph {\
    686                  background-color: %s;\
    687              }", prefix, prefix, color) +
    688         String.sprintf(
    689              "%s .toolbar-colors button.status-bar-item .glyph.shadow, %s .toolbar-colors button.status-bar-item .long-click-glyph.shadow {\
    690                  background-color: %s;\
    691              }", prefix, prefix, shadowColor);
    692 }
    693 
    694 WebInspector.resetToolbarColors = function()
    695 {
    696     if (WebInspector._themeStyleElement)
    697         WebInspector._themeStyleElement.textContent = "";
    698 }
    699 
    700 /**
    701  * @param {!Element} element
    702  * @param {number} offset
    703  * @param {number} length
    704  * @param {!Array.<!Object>=} domChanges
    705  * @return {?Element}
    706  */
    707 WebInspector.highlightSearchResult = function(element, offset, length, domChanges)
    708 {
    709     var result = WebInspector.highlightSearchResults(element, [new WebInspector.SourceRange(offset, length)], domChanges);
    710     return result.length ? result[0] : null;
    711 }
    712 
    713 /**
    714  * @param {!Element} element
    715  * @param {!Array.<!WebInspector.SourceRange>} resultRanges
    716  * @param {!Array.<!Object>=} changes
    717  * @return {!Array.<!Element>}
    718  */
    719 WebInspector.highlightSearchResults = function(element, resultRanges, changes)
    720 {
    721     return WebInspector.highlightRangesWithStyleClass(element, resultRanges, "highlighted-search-result", changes);
    722 }
    723 
    724 /**
    725  * @param {!Element} element
    726  * @param {string} className
    727  */
    728 WebInspector.runCSSAnimationOnce = function(element, className)
    729 {
    730     function animationEndCallback()
    731     {
    732         element.classList.remove(className);
    733         element.removeEventListener("animationend", animationEndCallback, false);
    734     }
    735 
    736     if (element.classList.contains(className))
    737         element.classList.remove(className);
    738 
    739     element.addEventListener("animationend", animationEndCallback, false);
    740     element.classList.add(className);
    741 }
    742 
    743 /**
    744  * @param {!Element} element
    745  * @param {!Array.<!WebInspector.SourceRange>} resultRanges
    746  * @param {string} styleClass
    747  * @param {!Array.<!Object>=} changes
    748  * @return {!Array.<!Element>}
    749  */
    750 WebInspector.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes)
    751 {
    752     changes = changes || [];
    753     var highlightNodes = [];
    754     var lineText = element.textContent;
    755     var ownerDocument = element.ownerDocument;
    756     var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    757 
    758     var snapshotLength = textNodeSnapshot.snapshotLength;
    759     if (snapshotLength === 0)
    760         return highlightNodes;
    761 
    762     var nodeRanges = [];
    763     var rangeEndOffset = 0;
    764     for (var i = 0; i < snapshotLength; ++i) {
    765         var range = {};
    766         range.offset = rangeEndOffset;
    767         range.length = textNodeSnapshot.snapshotItem(i).textContent.length;
    768         rangeEndOffset = range.offset + range.length;
    769         nodeRanges.push(range);
    770     }
    771 
    772     var startIndex = 0;
    773     for (var i = 0; i < resultRanges.length; ++i) {
    774         var startOffset = resultRanges[i].offset;
    775         var endOffset = startOffset + resultRanges[i].length;
    776 
    777         while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset)
    778             startIndex++;
    779         var endIndex = startIndex;
    780         while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset)
    781             endIndex++;
    782         if (endIndex === snapshotLength)
    783             break;
    784 
    785         var highlightNode = ownerDocument.createElement("span");
    786         highlightNode.className = styleClass;
    787         highlightNode.textContent = lineText.substring(startOffset, endOffset);
    788 
    789         var lastTextNode = textNodeSnapshot.snapshotItem(endIndex);
    790         var lastText = lastTextNode.textContent;
    791         lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset);
    792         changes.push({ node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent });
    793 
    794         if (startIndex === endIndex) {
    795             lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode);
    796             changes.push({ node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement });
    797             highlightNodes.push(highlightNode);
    798 
    799             var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset));
    800             lastTextNode.parentElement.insertBefore(prefixNode, highlightNode);
    801             changes.push({ node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement });
    802         } else {
    803             var firstTextNode = textNodeSnapshot.snapshotItem(startIndex);
    804             var firstText = firstTextNode.textContent;
    805             var anchorElement = firstTextNode.nextSibling;
    806 
    807             firstTextNode.parentElement.insertBefore(highlightNode, anchorElement);
    808             changes.push({ node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement });
    809             highlightNodes.push(highlightNode);
    810 
    811             firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset);
    812             changes.push({ node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent });
    813 
    814             for (var j = startIndex + 1; j < endIndex; j++) {
    815                 var textNode = textNodeSnapshot.snapshotItem(j);
    816                 var text = textNode.textContent;
    817                 textNode.textContent = "";
    818                 changes.push({ node: textNode, type: "changed", oldText: text, newText: textNode.textContent });
    819             }
    820         }
    821         startIndex = endIndex;
    822         nodeRanges[startIndex].offset = endOffset;
    823         nodeRanges[startIndex].length = lastTextNode.textContent.length;
    824 
    825     }
    826     return highlightNodes;
    827 }
    828 
    829 WebInspector.applyDomChanges = function(domChanges)
    830 {
    831     for (var i = 0, size = domChanges.length; i < size; ++i) {
    832         var entry = domChanges[i];
    833         switch (entry.type) {
    834         case "added":
    835             entry.parent.insertBefore(entry.node, entry.nextSibling);
    836             break;
    837         case "changed":
    838             entry.node.textContent = entry.newText;
    839             break;
    840         }
    841     }
    842 }
    843 
    844 WebInspector.revertDomChanges = function(domChanges)
    845 {
    846     for (var i = domChanges.length - 1; i >= 0; --i) {
    847         var entry = domChanges[i];
    848         switch (entry.type) {
    849         case "added":
    850             entry.node.remove();
    851             break;
    852         case "changed":
    853             entry.node.textContent = entry.oldText;
    854             break;
    855         }
    856     }
    857 }
    858 
    859 /**
    860  * @constructor
    861  * @param {boolean} autoInvoke
    862  */
    863 WebInspector.InvokeOnceHandlers = function(autoInvoke)
    864 {
    865     this._handlers = null;
    866     this._autoInvoke = autoInvoke;
    867 }
    868 
    869 WebInspector.InvokeOnceHandlers.prototype = {
    870     /**
    871      * @param {!Object} object
    872      * @param {function()} method
    873      */
    874     add: function(object, method)
    875     {
    876         if (!this._handlers) {
    877             this._handlers = new Map();
    878             if (this._autoInvoke)
    879                 this.scheduleInvoke();
    880         }
    881         var methods = this._handlers.get(object);
    882         if (!methods) {
    883             methods = new Set();
    884             this._handlers.set(object, methods);
    885         }
    886         methods.add(method);
    887     },
    888 
    889     scheduleInvoke: function()
    890     {
    891         if (this._handlers)
    892             requestAnimationFrame(this._invoke.bind(this));
    893     },
    894 
    895     _invoke: function()
    896     {
    897         var handlers = this._handlers;
    898         this._handlers = null;
    899         var keys = handlers.keys();
    900         for (var i = 0; i < keys.length; ++i) {
    901             var object = keys[i];
    902             var methods = handlers.get(object).values();
    903             for (var j = 0; j < methods.length; ++j)
    904                 methods[j].call(object);
    905         }
    906     }
    907 }
    908 
    909 WebInspector._coalescingLevel = 0;
    910 WebInspector._postUpdateHandlers = null;
    911 
    912 WebInspector.startBatchUpdate = function()
    913 {
    914     if (!WebInspector._coalescingLevel++)
    915         WebInspector._postUpdateHandlers = new WebInspector.InvokeOnceHandlers(false);
    916 }
    917 
    918 WebInspector.endBatchUpdate = function()
    919 {
    920     if (--WebInspector._coalescingLevel)
    921         return;
    922     WebInspector._postUpdateHandlers.scheduleInvoke();
    923     WebInspector._postUpdateHandlers = null;
    924 }
    925 
    926 /**
    927  * @param {!Object} object
    928  * @param {function()} method
    929  */
    930 WebInspector.invokeOnceAfterBatchUpdate = function(object, method)
    931 {
    932     if (!WebInspector._postUpdateHandlers)
    933         WebInspector._postUpdateHandlers = new WebInspector.InvokeOnceHandlers(true);
    934     WebInspector._postUpdateHandlers.add(object, method);
    935 }
    936 
    937 /**
    938  * @param {!Function} func
    939  * @param {!Array.<{from:number, to:number}>} params
    940  * @param {number} frames
    941  * @param {function()=} animationComplete
    942  * @return {function()}
    943  */
    944 WebInspector.animateFunction = function(func, params, frames, animationComplete)
    945 {
    946     var values = new Array(params.length);
    947     var deltas = new Array(params.length);
    948     for (var i = 0; i < params.length; ++i) {
    949         values[i] = params[i].from;
    950         deltas[i] = (params[i].to - params[i].from) / frames;
    951     }
    952 
    953     var raf = requestAnimationFrame(animationStep);
    954 
    955     var framesLeft = frames;
    956 
    957     function animationStep()
    958     {
    959         if (--framesLeft < 0) {
    960             if (animationComplete)
    961                 animationComplete();
    962             return;
    963         }
    964         for (var i = 0; i < params.length; ++i) {
    965             if (params[i].to > params[i].from)
    966                 values[i] = Number.constrain(values[i] + deltas[i], params[i].from, params[i].to);
    967             else
    968                 values[i] = Number.constrain(values[i] + deltas[i], params[i].to, params[i].from);
    969         }
    970         func.apply(null, values);
    971         raf = window.requestAnimationFrame(animationStep);
    972     }
    973 
    974     function cancelAnimation()
    975     {
    976         window.cancelAnimationFrame(raf);
    977     }
    978 
    979     return cancelAnimation;
    980 }
    981 
    982 /**
    983  * @constructor
    984  * @extends {WebInspector.Object}
    985  * @param {!Element} element
    986  */
    987 WebInspector.LongClickController = function(element)
    988 {
    989     this._element = element;
    990 }
    991 
    992 /**
    993  * @enum {string}
    994  */
    995 WebInspector.LongClickController.Events = {
    996     LongClick: "LongClick",
    997     LongPress: "LongPress"
    998 };
    999 
   1000 WebInspector.LongClickController.prototype = {
   1001     reset: function()
   1002     {
   1003         if (this._longClickInterval) {
   1004             clearInterval(this._longClickInterval);
   1005             delete this._longClickInterval;
   1006         }
   1007     },
   1008 
   1009     enable: function()
   1010     {
   1011         if (this._longClickData)
   1012             return;
   1013         var boundMouseDown = mouseDown.bind(this);
   1014         var boundMouseUp = mouseUp.bind(this);
   1015         var boundReset = this.reset.bind(this);
   1016 
   1017         this._element.addEventListener("mousedown", boundMouseDown, false);
   1018         this._element.addEventListener("mouseout", boundReset, false);
   1019         this._element.addEventListener("mouseup", boundMouseUp, false);
   1020         this._element.addEventListener("click", boundReset, true);
   1021 
   1022         var longClicks = 0;
   1023 
   1024         this._longClickData = { mouseUp: boundMouseUp, mouseDown: boundMouseDown, reset: boundReset };
   1025 
   1026         /**
   1027          * @param {!Event} e
   1028          * @this {WebInspector.LongClickController}
   1029          */
   1030         function mouseDown(e)
   1031         {
   1032             if (e.which !== 1)
   1033                 return;
   1034             longClicks = 0;
   1035             this._longClickInterval = setInterval(longClicked.bind(this, e), 200);
   1036         }
   1037 
   1038         /**
   1039          * @param {!Event} e
   1040          * @this {WebInspector.LongClickController}
   1041          */
   1042         function mouseUp(e)
   1043         {
   1044             if (e.which !== 1)
   1045                 return;
   1046             this.reset();
   1047         }
   1048 
   1049         /**
   1050          * @param {!Event} e
   1051          * @this {WebInspector.LongClickController}
   1052          */
   1053         function longClicked(e)
   1054         {
   1055             ++longClicks;
   1056             this.dispatchEventToListeners(longClicks === 1 ? WebInspector.LongClickController.Events.LongClick : WebInspector.LongClickController.Events.LongPress, e);
   1057         }
   1058     },
   1059 
   1060     disable: function()
   1061     {
   1062         if (!this._longClickData)
   1063             return;
   1064         this._element.removeEventListener("mousedown", this._longClickData.mouseDown, false);
   1065         this._element.removeEventListener("mouseout", this._longClickData.reset, false);
   1066         this._element.removeEventListener("mouseup", this._longClickData.mouseUp, false);
   1067         this._element.addEventListener("click", this._longClickData.reset, true);
   1068         delete this._longClickData;
   1069     },
   1070 
   1071     __proto__: WebInspector.Object.prototype
   1072 }
   1073 
   1074 ;(function() {
   1075 
   1076 function windowLoaded()
   1077 {
   1078     window.addEventListener("focus", WebInspector._windowFocused, false);
   1079     window.addEventListener("blur", WebInspector._windowBlurred, false);
   1080     document.addEventListener("focus", WebInspector._focusChanged, true);
   1081     document.addEventListener("blur", WebInspector._documentBlurred, true);
   1082     window.removeEventListener("DOMContentLoaded", windowLoaded, false);
   1083 }
   1084 
   1085 window.addEventListener("DOMContentLoaded", windowLoaded, false);
   1086 
   1087 })();
   1088