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