Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2011 Google Inc.  All rights reserved.
      3  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      4  * Copyright (C) 2008 Matt Lilek <webkit (at) mattlilek.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 WebInspector.DOMPresentationUtils = {}
     33 
     34 WebInspector.DOMPresentationUtils.decorateNodeLabel = function(node, parentElement)
     35 {
     36     var title = node.nodeNameInCorrectCase();
     37 
     38     var nameElement = document.createElement("span");
     39     nameElement.textContent = title;
     40     parentElement.appendChild(nameElement);
     41 
     42     var idAttribute = node.getAttribute("id");
     43     if (idAttribute) {
     44         var idElement = document.createElement("span");
     45         parentElement.appendChild(idElement);
     46 
     47         var part = "#" + idAttribute;
     48         title += part;
     49         idElement.appendChild(document.createTextNode(part));
     50 
     51         // Mark the name as extra, since the ID is more important.
     52         nameElement.className = "extra";
     53     }
     54 
     55     var classAttribute = node.getAttribute("class");
     56     if (classAttribute) {
     57         var classes = classAttribute.split(/\s+/);
     58         var foundClasses = {};
     59 
     60         if (classes.length) {
     61             var classesElement = document.createElement("span");
     62             classesElement.className = "extra";
     63             parentElement.appendChild(classesElement);
     64 
     65             for (var i = 0; i < classes.length; ++i) {
     66                 var className = classes[i];
     67                 if (className && !(className in foundClasses)) {
     68                     var part = "." + className;
     69                     title += part;
     70                     classesElement.appendChild(document.createTextNode(part));
     71                     foundClasses[className] = true;
     72                 }
     73             }
     74         }
     75     }
     76     parentElement.title = title;
     77 }
     78 
     79 /**
     80  * @param {!Element} container
     81  * @param {string} nodeTitle
     82  */
     83 WebInspector.DOMPresentationUtils.createSpansForNodeTitle = function(container, nodeTitle)
     84 {
     85     var match = nodeTitle.match(/([^#.]+)(#[^.]+)?(\..*)?/);
     86     container.createChild("span", "webkit-html-tag-name").textContent = match[1];
     87     if (match[2])
     88         container.createChild("span", "webkit-html-attribute-value").textContent = match[2];
     89     if (match[3])
     90         container.createChild("span", "webkit-html-attribute-name").textContent = match[3];
     91 }
     92 
     93 WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node)
     94 {
     95     var link = document.createElement("span");
     96     link.className = "node-link";
     97     WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link);
     98 
     99     link.addEventListener("click", WebInspector.domAgent.inspectElement.bind(WebInspector.domAgent, node.id), false);
    100     link.addEventListener("mouseover", WebInspector.domAgent.highlightDOMNode.bind(WebInspector.domAgent, node.id, "", undefined), false);
    101     link.addEventListener("mouseout", WebInspector.domAgent.hideDOMNodeHighlight.bind(WebInspector.domAgent), false);
    102 
    103     return link;
    104 }
    105 
    106 WebInspector.DOMPresentationUtils.linkifyNodeById = function(nodeId)
    107 {
    108     var node = WebInspector.domAgent.nodeForId(nodeId);
    109     if (!node)
    110         return document.createTextNode(WebInspector.UIString("<node>"));
    111     return WebInspector.DOMPresentationUtils.linkifyNodeReference(node);
    112 }
    113 
    114 /**
    115  * @param {string} imageURL
    116  * @param {boolean} showDimensions
    117  * @param {function(!Element=)} userCallback
    118  * @param {!Object=} precomputedDimensions
    119  */
    120 WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(imageURL, showDimensions, userCallback, precomputedDimensions)
    121 {
    122     var resource = WebInspector.resourceTreeModel.resourceForURL(imageURL);
    123     if (!resource) {
    124         userCallback();
    125         return;
    126     }
    127 
    128     var imageElement = document.createElement("img");
    129     imageElement.addEventListener("load", buildContent, false);
    130     imageElement.addEventListener("error", errorCallback, false);
    131     resource.populateImageSource(imageElement);
    132 
    133     function errorCallback()
    134     {
    135         // Drop the event parameter when invoking userCallback.
    136         userCallback();
    137     }
    138 
    139     function buildContent()
    140     {
    141         var container = document.createElement("table");
    142         container.className = "image-preview-container";
    143         var naturalWidth = precomputedDimensions ? precomputedDimensions.naturalWidth : imageElement.naturalWidth;
    144         var naturalHeight = precomputedDimensions ? precomputedDimensions.naturalHeight : imageElement.naturalHeight;
    145         var offsetWidth = precomputedDimensions ? precomputedDimensions.offsetWidth : naturalWidth;
    146         var offsetHeight = precomputedDimensions ? precomputedDimensions.offsetHeight : naturalHeight;
    147         var description;
    148         if (showDimensions) {
    149             if (offsetHeight === naturalHeight && offsetWidth === naturalWidth)
    150                 description = WebInspector.UIString("%d \xd7 %d pixels", offsetWidth, offsetHeight);
    151             else
    152                 description = WebInspector.UIString("%d \xd7 %d pixels (Natural: %d \xd7 %d pixels)", offsetWidth, offsetHeight, naturalWidth, naturalHeight);
    153         }
    154 
    155         container.createChild("tr").createChild("td", "image-container").appendChild(imageElement);
    156         if (description)
    157             container.createChild("tr").createChild("td").createChild("span", "description").textContent = description;
    158         userCallback(container);
    159     }
    160 }
    161 
    162 /**
    163  * @param {!WebInspector.DOMNode} node
    164  * @param {boolean=} justSelector
    165  * @return {string}
    166  */
    167 WebInspector.DOMPresentationUtils.appropriateSelectorFor = function(node, justSelector)
    168 {
    169     var lowerCaseName = node.localName() || node.nodeName().toLowerCase();
    170     if (node.nodeType() !== Node.ELEMENT_NODE)
    171         return lowerCaseName;
    172     if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
    173         return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
    174 
    175     return WebInspector.DOMPresentationUtils.cssPath(node, justSelector);
    176 }
    177 
    178 /**
    179  * @param {!WebInspector.DOMNode} node
    180  * @param {boolean=} optimized
    181  * @return {string}
    182  */
    183 WebInspector.DOMPresentationUtils.cssPath = function(node, optimized)
    184 {
    185     if (node.nodeType() !== Node.ELEMENT_NODE)
    186         return "";
    187 
    188     var steps = [];
    189     var contextNode = node;
    190     while (contextNode) {
    191         var step = WebInspector.DOMPresentationUtils._cssPathValue(contextNode, optimized);
    192         if (!step)
    193             break; // Error - bail out early.
    194         steps.push(step);
    195         if (step.optimized)
    196             break;
    197         contextNode = contextNode.parentNode;
    198     }
    199 
    200     steps.reverse();
    201     return steps.join(" > ");
    202 }
    203 
    204 /**
    205  * @param {!WebInspector.DOMNode} node
    206  * @param {boolean=} optimized
    207  * @return {?WebInspector.DOMNodePathStep}
    208  */
    209 WebInspector.DOMPresentationUtils._cssPathValue = function(node, optimized)
    210 {
    211     if (node.nodeType() !== Node.ELEMENT_NODE)
    212         return null;
    213 
    214     var id = node.getAttribute("id");
    215     if (optimized) {
    216         if (id)
    217             return new WebInspector.DOMNodePathStep(idSelector(id), true);
    218         var nodeNameLower = node.nodeName().toLowerCase();
    219         if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
    220             return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true);
    221     }
    222     var nodeName = node.nodeNameInCorrectCase();
    223 
    224     if (id)
    225         return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true);
    226     var parent = node.parentNode;
    227     if (!parent || parent.nodeType() === Node.DOCUMENT_NODE)
    228         return new WebInspector.DOMNodePathStep(nodeName, true);
    229 
    230     /**
    231      * @param {!WebInspector.DOMNode} node
    232      * @return {!Array.<string>}
    233      */
    234     function prefixedElementClassNames(node)
    235     {
    236         var classAttribute = node.getAttribute("class");
    237         if (!classAttribute)
    238             return [];
    239 
    240         return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) {
    241             // The prefix is required to store "__proto__" in a object-based map.
    242             return "$" + name;
    243         });
    244     }
    245 
    246     /**
    247      * @param {string} id
    248      * @return {string}
    249      */
    250     function idSelector(id)
    251     {
    252         return "#" + escapeIdentifierIfNeeded(id);
    253     }
    254 
    255     /**
    256      * @param {string} ident
    257      * @return {string}
    258      */
    259     function escapeIdentifierIfNeeded(ident)
    260     {
    261         if (isCSSIdentifier(ident))
    262             return ident;
    263         var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);
    264         var lastIndex = ident.length - 1;
    265         return ident.replace(/./g, function(c, i) {
    266             return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
    267         });
    268     }
    269 
    270     /**
    271      * @param {string} c
    272      * @param {boolean} isLast
    273      * @return {string}
    274      */
    275     function escapeAsciiChar(c, isLast)
    276     {
    277         return "\\" + toHexByte(c) + (isLast ? "" : " ");
    278     }
    279 
    280     /**
    281      * @param {string} c
    282      */
    283     function toHexByte(c)
    284     {
    285         var hexByte = c.charCodeAt(0).toString(16);
    286         if (hexByte.length === 1)
    287           hexByte = "0" + hexByte;
    288         return hexByte;
    289     }
    290 
    291     /**
    292      * @param {string} c
    293      * @return {boolean}
    294      */
    295     function isCSSIdentChar(c)
    296     {
    297         if (/[a-zA-Z0-9_-]/.test(c))
    298             return true;
    299         return c.charCodeAt(0) >= 0xA0;
    300     }
    301 
    302     /**
    303      * @param {string} value
    304      * @return {boolean}
    305      */
    306     function isCSSIdentifier(value)
    307     {
    308         return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
    309     }
    310 
    311     var prefixedOwnClassNamesArray = prefixedElementClassNames(node);
    312     var needsClassNames = false;
    313     var needsNthChild = false;
    314     var ownIndex = -1;
    315     var siblings = parent.children();
    316     for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
    317         var sibling = siblings[i];
    318         if (sibling === node) {
    319             ownIndex = i;
    320             continue;
    321         }
    322         if (needsNthChild)
    323             continue;
    324         if (sibling.nodeNameInCorrectCase() !== nodeName)
    325             continue;
    326 
    327         needsClassNames = true;
    328         var ownClassNames = prefixedOwnClassNamesArray.keySet();
    329         var ownClassNameCount = 0;
    330         for (var name in ownClassNames)
    331             ++ownClassNameCount;
    332         if (ownClassNameCount === 0) {
    333             needsNthChild = true;
    334             continue;
    335         }
    336         var siblingClassNamesArray = prefixedElementClassNames(sibling);
    337         for (var j = 0; j < siblingClassNamesArray.length; ++j) {
    338             var siblingClass = siblingClassNamesArray[j];
    339             if (!ownClassNames.hasOwnProperty(siblingClass))
    340                 continue;
    341             delete ownClassNames[siblingClass];
    342             if (!--ownClassNameCount) {
    343                 needsNthChild = true;
    344                 break;
    345             }
    346         }
    347     }
    348 
    349     var result = nodeName;
    350     if (needsNthChild) {
    351         result += ":nth-child(" + (ownIndex + 1) + ")";
    352     } else if (needsClassNames) {
    353         for (var prefixedName in prefixedOwnClassNamesArray.keySet())
    354             result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));
    355     }
    356 
    357     return new WebInspector.DOMNodePathStep(result, false);
    358 }
    359 
    360 /**
    361  * @param {!WebInspector.DOMNode} node
    362  * @param {boolean=} optimized
    363  * @return {string}
    364  */
    365 WebInspector.DOMPresentationUtils.xPath = function(node, optimized)
    366 {
    367     if (node.nodeType() === Node.DOCUMENT_NODE)
    368         return "/";
    369 
    370     var steps = [];
    371     var contextNode = node;
    372     while (contextNode) {
    373         var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized);
    374         if (!step)
    375             break; // Error - bail out early.
    376         steps.push(step);
    377         if (step.optimized)
    378             break;
    379         contextNode = contextNode.parentNode;
    380     }
    381 
    382     steps.reverse();
    383     return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/");
    384 }
    385 
    386 /**
    387  * @param {!WebInspector.DOMNode} node
    388  * @param {boolean=} optimized
    389  * @return {?WebInspector.DOMNodePathStep}
    390  */
    391 WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized)
    392 {
    393     var ownValue;
    394     var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node);
    395     if (ownIndex === -1)
    396         return null; // Error.
    397 
    398     switch (node.nodeType()) {
    399     case Node.ELEMENT_NODE:
    400         if (optimized && node.getAttribute("id"))
    401             return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true);
    402         ownValue = node.localName();
    403         break;
    404     case Node.ATTRIBUTE_NODE:
    405         ownValue = "@" + node.nodeName();
    406         break;
    407     case Node.TEXT_NODE:
    408     case Node.CDATA_SECTION_NODE:
    409         ownValue = "text()";
    410         break;
    411     case Node.PROCESSING_INSTRUCTION_NODE:
    412         ownValue = "processing-instruction()";
    413         break;
    414     case Node.COMMENT_NODE:
    415         ownValue = "comment()";
    416         break;
    417     case Node.DOCUMENT_NODE:
    418         ownValue = "";
    419         break;
    420     default:
    421         ownValue = "";
    422         break;
    423     }
    424 
    425     if (ownIndex > 0)
    426         ownValue += "[" + ownIndex + "]";
    427 
    428     return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE);
    429 },
    430 
    431 /**
    432  * @param {!WebInspector.DOMNode} node
    433  * @return {number}
    434  */
    435 WebInspector.DOMPresentationUtils._xPathIndex = function(node)
    436 {
    437     // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise.
    438     function areNodesSimilar(left, right)
    439     {
    440         if (left === right)
    441             return true;
    442 
    443         if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE)
    444             return left.localName() === right.localName();
    445 
    446         if (left.nodeType() === right.nodeType())
    447             return true;
    448 
    449         // XPath treats CDATA as text nodes.
    450         var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType();
    451         var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType();
    452         return leftType === rightType;
    453     }
    454 
    455     var siblings = node.parentNode ? node.parentNode.children() : null;
    456     if (!siblings)
    457         return 0; // Root node - no siblings.
    458     var hasSameNamedElements;
    459     for (var i = 0; i < siblings.length; ++i) {
    460         if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
    461             hasSameNamedElements = true;
    462             break;
    463         }
    464     }
    465     if (!hasSameNamedElements)
    466         return 0;
    467     var ownIndex = 1; // XPath indices start with 1.
    468     for (var i = 0; i < siblings.length; ++i) {
    469         if (areNodesSimilar(node, siblings[i])) {
    470             if (siblings[i] === node)
    471                 return ownIndex;
    472             ++ownIndex;
    473         }
    474     }
    475     return -1; // An error occurred: |node| not found in parent's children.
    476 }
    477 
    478 /**
    479  * @constructor
    480  * @param {string} value
    481  * @param {boolean} optimized
    482  */
    483 WebInspector.DOMNodePathStep = function(value, optimized)
    484 {
    485     this.value = value;
    486     this.optimized = optimized || false;
    487 }
    488 
    489 WebInspector.DOMNodePathStep.prototype = {
    490     /**
    491      * @return {string}
    492      */
    493     toString: function()
    494     {
    495         return this.value;
    496     }
    497 }
    498