Home | History | Annotate | Download | only in components
      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.createTextChild(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.createTextChild(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 /**
     94  * @param {?WebInspector.DOMNode} node
     95  * @return {!Node}
     96  */
     97 WebInspector.DOMPresentationUtils.linkifyNodeReference = function(node)
     98 {
     99     if (!node)
    100         return document.createTextNode(WebInspector.UIString("<node>"));
    101 
    102     var link = document.createElement("span");
    103     link.className = "node-link";
    104     WebInspector.DOMPresentationUtils.decorateNodeLabel(node, link);
    105 
    106     link.addEventListener("click", WebInspector.Revealer.reveal.bind(WebInspector.Revealer, node, undefined), false);
    107     link.addEventListener("mouseover", node.highlight.bind(node, undefined, undefined), false);
    108     link.addEventListener("mouseout", node.domModel().hideDOMNodeHighlight.bind(node.domModel()), false);
    109 
    110     return link;
    111 }
    112 
    113 /**
    114  * @param {string} imageURL
    115  * @param {!WebInspector.Target} target
    116  * @param {boolean} showDimensions
    117  * @param {function(!Element=)} userCallback
    118  * @param {!Object=} precomputedDimensions
    119  */
    120 WebInspector.DOMPresentationUtils.buildImagePreviewContents = function(target, imageURL, showDimensions, userCallback, precomputedDimensions)
    121 {
    122     var resource = target.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.fullQualifiedSelector = function(node, justSelector)
    168 {
    169     if (node.nodeType() !== Node.ELEMENT_NODE)
    170         return node.localName() || node.nodeName().toLowerCase();
    171     return WebInspector.DOMPresentationUtils.cssPath(node, justSelector);
    172 }
    173 
    174 /**
    175  * @param {!WebInspector.DOMNode} node
    176  * @return {string}
    177  */
    178 WebInspector.DOMPresentationUtils.simpleSelector = function(node)
    179 {
    180     var lowerCaseName = node.localName() || node.nodeName().toLowerCase();
    181     if (node.nodeType() !== Node.ELEMENT_NODE)
    182         return lowerCaseName;
    183     if (lowerCaseName === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
    184         return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]";
    185     if (node.getAttribute("id"))
    186         return lowerCaseName + "#" + node.getAttribute("id");
    187     if (node.getAttribute("class"))
    188         return (lowerCaseName === "div" ? "" : lowerCaseName) + "." + node.getAttribute("class").trim().replace(/\s+/g, ".");
    189     return lowerCaseName;
    190 }
    191 
    192 /**
    193  * @param {!WebInspector.DOMNode} node
    194  * @param {boolean=} optimized
    195  * @return {string}
    196  */
    197 WebInspector.DOMPresentationUtils.cssPath = function(node, optimized)
    198 {
    199     if (node.nodeType() !== Node.ELEMENT_NODE)
    200         return "";
    201 
    202     var steps = [];
    203     var contextNode = node;
    204     while (contextNode) {
    205         var step = WebInspector.DOMPresentationUtils._cssPathStep(contextNode, !!optimized, contextNode === node);
    206         if (!step)
    207             break; // Error - bail out early.
    208         steps.push(step);
    209         if (step.optimized)
    210             break;
    211         contextNode = contextNode.parentNode;
    212     }
    213 
    214     steps.reverse();
    215     return steps.join(" > ");
    216 }
    217 
    218 /**
    219  * @param {!WebInspector.DOMNode} node
    220  * @param {boolean} optimized
    221  * @param {boolean} isTargetNode
    222  * @return {?WebInspector.DOMNodePathStep}
    223  */
    224 WebInspector.DOMPresentationUtils._cssPathStep = function(node, optimized, isTargetNode)
    225 {
    226     if (node.nodeType() !== Node.ELEMENT_NODE)
    227         return null;
    228 
    229     var id = node.getAttribute("id");
    230     if (optimized) {
    231         if (id)
    232             return new WebInspector.DOMNodePathStep(idSelector(id), true);
    233         var nodeNameLower = node.nodeName().toLowerCase();
    234         if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
    235             return new WebInspector.DOMNodePathStep(node.nodeNameInCorrectCase(), true);
    236     }
    237     var nodeName = node.nodeNameInCorrectCase();
    238 
    239     if (id)
    240         return new WebInspector.DOMNodePathStep(nodeName + idSelector(id), true);
    241     var parent = node.parentNode;
    242     if (!parent || parent.nodeType() === Node.DOCUMENT_NODE)
    243         return new WebInspector.DOMNodePathStep(nodeName, true);
    244 
    245     /**
    246      * @param {!WebInspector.DOMNode} node
    247      * @return {!Array.<string>}
    248      */
    249     function prefixedElementClassNames(node)
    250     {
    251         var classAttribute = node.getAttribute("class");
    252         if (!classAttribute)
    253             return [];
    254 
    255         return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) {
    256             // The prefix is required to store "__proto__" in a object-based map.
    257             return "$" + name;
    258         });
    259     }
    260 
    261     /**
    262      * @param {string} id
    263      * @return {string}
    264      */
    265     function idSelector(id)
    266     {
    267         return "#" + escapeIdentifierIfNeeded(id);
    268     }
    269 
    270     /**
    271      * @param {string} ident
    272      * @return {string}
    273      */
    274     function escapeIdentifierIfNeeded(ident)
    275     {
    276         if (isCSSIdentifier(ident))
    277             return ident;
    278         var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);
    279         var lastIndex = ident.length - 1;
    280         return ident.replace(/./g, function(c, i) {
    281             return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
    282         });
    283     }
    284 
    285     /**
    286      * @param {string} c
    287      * @param {boolean} isLast
    288      * @return {string}
    289      */
    290     function escapeAsciiChar(c, isLast)
    291     {
    292         return "\\" + toHexByte(c) + (isLast ? "" : " ");
    293     }
    294 
    295     /**
    296      * @param {string} c
    297      */
    298     function toHexByte(c)
    299     {
    300         var hexByte = c.charCodeAt(0).toString(16);
    301         if (hexByte.length === 1)
    302           hexByte = "0" + hexByte;
    303         return hexByte;
    304     }
    305 
    306     /**
    307      * @param {string} c
    308      * @return {boolean}
    309      */
    310     function isCSSIdentChar(c)
    311     {
    312         if (/[a-zA-Z0-9_-]/.test(c))
    313             return true;
    314         return c.charCodeAt(0) >= 0xA0;
    315     }
    316 
    317     /**
    318      * @param {string} value
    319      * @return {boolean}
    320      */
    321     function isCSSIdentifier(value)
    322     {
    323         return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
    324     }
    325 
    326     var prefixedOwnClassNamesArray = prefixedElementClassNames(node);
    327     var needsClassNames = false;
    328     var needsNthChild = false;
    329     var ownIndex = -1;
    330     var elementIndex = -1;
    331     var siblings = parent.children();
    332     for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
    333         var sibling = siblings[i];
    334         if (sibling.nodeType() !== Node.ELEMENT_NODE)
    335             continue;
    336         elementIndex += 1;
    337         if (sibling === node) {
    338             ownIndex = elementIndex;
    339             continue;
    340         }
    341         if (needsNthChild)
    342             continue;
    343         if (sibling.nodeNameInCorrectCase() !== nodeName)
    344             continue;
    345 
    346         needsClassNames = true;
    347         var ownClassNames = prefixedOwnClassNamesArray.keySet();
    348         var ownClassNameCount = 0;
    349         for (var name in ownClassNames)
    350             ++ownClassNameCount;
    351         if (ownClassNameCount === 0) {
    352             needsNthChild = true;
    353             continue;
    354         }
    355         var siblingClassNamesArray = prefixedElementClassNames(sibling);
    356         for (var j = 0; j < siblingClassNamesArray.length; ++j) {
    357             var siblingClass = siblingClassNamesArray[j];
    358             if (!ownClassNames.hasOwnProperty(siblingClass))
    359                 continue;
    360             delete ownClassNames[siblingClass];
    361             if (!--ownClassNameCount) {
    362                 needsNthChild = true;
    363                 break;
    364             }
    365         }
    366     }
    367 
    368     var result = nodeName;
    369     if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
    370         result += "[type=\"" + node.getAttribute("type") + "\"]";
    371     if (needsNthChild) {
    372         result += ":nth-child(" + (ownIndex + 1) + ")";
    373     } else if (needsClassNames) {
    374         for (var prefixedName in prefixedOwnClassNamesArray.keySet())
    375             result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));
    376     }
    377 
    378     return new WebInspector.DOMNodePathStep(result, false);
    379 }
    380 
    381 /**
    382  * @param {!WebInspector.DOMNode} node
    383  * @param {boolean=} optimized
    384  * @return {string}
    385  */
    386 WebInspector.DOMPresentationUtils.xPath = function(node, optimized)
    387 {
    388     if (node.nodeType() === Node.DOCUMENT_NODE)
    389         return "/";
    390 
    391     var steps = [];
    392     var contextNode = node;
    393     while (contextNode) {
    394         var step = WebInspector.DOMPresentationUtils._xPathValue(contextNode, optimized);
    395         if (!step)
    396             break; // Error - bail out early.
    397         steps.push(step);
    398         if (step.optimized)
    399             break;
    400         contextNode = contextNode.parentNode;
    401     }
    402 
    403     steps.reverse();
    404     return (steps.length && steps[0].optimized ? "" : "/") + steps.join("/");
    405 }
    406 
    407 /**
    408  * @param {!WebInspector.DOMNode} node
    409  * @param {boolean=} optimized
    410  * @return {?WebInspector.DOMNodePathStep}
    411  */
    412 WebInspector.DOMPresentationUtils._xPathValue = function(node, optimized)
    413 {
    414     var ownValue;
    415     var ownIndex = WebInspector.DOMPresentationUtils._xPathIndex(node);
    416     if (ownIndex === -1)
    417         return null; // Error.
    418 
    419     switch (node.nodeType()) {
    420     case Node.ELEMENT_NODE:
    421         if (optimized && node.getAttribute("id"))
    422             return new WebInspector.DOMNodePathStep("//*[@id=\"" + node.getAttribute("id") + "\"]", true);
    423         ownValue = node.localName();
    424         break;
    425     case Node.ATTRIBUTE_NODE:
    426         ownValue = "@" + node.nodeName();
    427         break;
    428     case Node.TEXT_NODE:
    429     case Node.CDATA_SECTION_NODE:
    430         ownValue = "text()";
    431         break;
    432     case Node.PROCESSING_INSTRUCTION_NODE:
    433         ownValue = "processing-instruction()";
    434         break;
    435     case Node.COMMENT_NODE:
    436         ownValue = "comment()";
    437         break;
    438     case Node.DOCUMENT_NODE:
    439         ownValue = "";
    440         break;
    441     default:
    442         ownValue = "";
    443         break;
    444     }
    445 
    446     if (ownIndex > 0)
    447         ownValue += "[" + ownIndex + "]";
    448 
    449     return new WebInspector.DOMNodePathStep(ownValue, node.nodeType() === Node.DOCUMENT_NODE);
    450 },
    451 
    452 /**
    453  * @param {!WebInspector.DOMNode} node
    454  * @return {number}
    455  */
    456 WebInspector.DOMPresentationUtils._xPathIndex = function(node)
    457 {
    458     // Returns -1 in case of error, 0 if no siblings matching the same expression, <XPath index among the same expression-matching sibling nodes> otherwise.
    459     function areNodesSimilar(left, right)
    460     {
    461         if (left === right)
    462             return true;
    463 
    464         if (left.nodeType() === Node.ELEMENT_NODE && right.nodeType() === Node.ELEMENT_NODE)
    465             return left.localName() === right.localName();
    466 
    467         if (left.nodeType() === right.nodeType())
    468             return true;
    469 
    470         // XPath treats CDATA as text nodes.
    471         var leftType = left.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : left.nodeType();
    472         var rightType = right.nodeType() === Node.CDATA_SECTION_NODE ? Node.TEXT_NODE : right.nodeType();
    473         return leftType === rightType;
    474     }
    475 
    476     var siblings = node.parentNode ? node.parentNode.children() : null;
    477     if (!siblings)
    478         return 0; // Root node - no siblings.
    479     var hasSameNamedElements;
    480     for (var i = 0; i < siblings.length; ++i) {
    481         if (areNodesSimilar(node, siblings[i]) && siblings[i] !== node) {
    482             hasSameNamedElements = true;
    483             break;
    484         }
    485     }
    486     if (!hasSameNamedElements)
    487         return 0;
    488     var ownIndex = 1; // XPath indices start with 1.
    489     for (var i = 0; i < siblings.length; ++i) {
    490         if (areNodesSimilar(node, siblings[i])) {
    491             if (siblings[i] === node)
    492                 return ownIndex;
    493             ++ownIndex;
    494         }
    495     }
    496     return -1; // An error occurred: |node| not found in parent's children.
    497 }
    498 
    499 /**
    500  * @constructor
    501  * @param {string} value
    502  * @param {boolean} optimized
    503  */
    504 WebInspector.DOMNodePathStep = function(value, optimized)
    505 {
    506     this.value = value;
    507     this.optimized = optimized || false;
    508 }
    509 
    510 WebInspector.DOMNodePathStep.prototype = {
    511     /**
    512      * @return {string}
    513      */
    514     toString: function()
    515     {
    516         return this.value;
    517     }
    518 }
    519