Home | History | Annotate | Download | only in front_end
      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