Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1329)

Unified Diff: Source/devtools/front_end/DOMAgent.js

Issue 75253002: DevTools: [Elements] Implement "Copy CSS Path" context menu item for elements (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Implement identifier escaping for id and class attribute values Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: Source/devtools/front_end/DOMAgent.js
diff --git a/Source/devtools/front_end/DOMAgent.js b/Source/devtools/front_end/DOMAgent.js
index 320e88c01c7322119317b9cb53ee4c0e334ee025..671bfe8cec3aadb42211433a2a9211286a0bfa8d 100644
--- a/Source/devtools/front_end/DOMAgent.js
+++ b/Source/devtools/front_end/DOMAgent.js
@@ -125,13 +125,13 @@ WebInspector.DOMNode.ShadowRootTypes = {
* @param {string} value
* @param {boolean} optimized
*/
-WebInspector.DOMNode.XPathStep = function(value, optimized)
+WebInspector.DOMNode.PathStep = function(value, optimized)
{
this.value = value;
this.optimized = optimized;
}
-WebInspector.DOMNode.XPathStep.prototype = {
+WebInspector.DOMNode.PathStep.prototype = {
toString: function()
{
return this.value;
@@ -430,6 +430,14 @@ WebInspector.DOMNode.prototype = {
/**
* @param {boolean} optimized
*/
+ copyCSSPath: function(optimized)
+ {
+ InspectorFrontendHost.copyText(this.cssPath(optimized));
+ },
+
+ /**
+ * @param {boolean} optimized
+ */
copyXPath: function(optimized)
{
InspectorFrontendHost.copyText(this.xPath(optimized));
@@ -685,6 +693,177 @@ WebInspector.DOMNode.prototype = {
* @param {boolean} optimized
* @return {string}
*/
+ cssPath: function(optimized)
pfeldman 2013/11/20 14:08:48 This interesting little feature is about to become
apavlov 2013/11/20 14:23:19 Sounds good.
+ {
+ if (this._nodeType !== Node.ELEMENT_NODE)
+ return "";
+
+ var steps = [];
+ var contextNode = this;
+ while (contextNode) {
+ var step = contextNode._cssPathValue(optimized);
+ if (!step)
+ break; // Error - bail out early.
+ steps.push(step);
+ if (step.optimized)
+ break;
+ contextNode = contextNode.parentNode;
+ }
+
+ steps.reverse();
+ return steps.join(" > ");
+ },
+
+ /**
+ * @param {boolean} optimized
+ * @return {WebInspector.DOMNode.PathStep}
+ */
+ _cssPathValue: function(optimized)
+ {
+ if (this._nodeType !== Node.ELEMENT_NODE)
+ return null;
+ if (optimized) {
+ var id = this.getAttribute("id");
+ if (id)
+ return new WebInspector.DOMNode.PathStep(idSelector(id), true);
+ var nodeNameLower = this._nodeName.toLowerCase();
+ if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
+ return new WebInspector.DOMNode.PathStep(this.nodeNameInCorrectCase(), true);
+ }
+ var nodeName = this.nodeNameInCorrectCase();
+ var parent = this.parentNode;
+ if (!parent || parent._nodeType === Node.DOCUMENT_NODE)
+ return new WebInspector.DOMNode.PathStep(nodeName, true);
+
+ /**
+ * @param {WebInspector.DOMNode} node
+ * @return {Object.<string, boolean>}
+ */
+ function elementClassNames(node)
+ {
+ var classAttribute = node.getAttribute("class");
+ if (!classAttribute)
+ return {};
+
+ return classAttribute.split(/\s+/g).filter(Boolean).keySet();
+ }
+
+ /**
+ * @param {string} id
+ * @return {string}
+ */
+ function idSelector(id)
+ {
+ return "#" + escapeIdentifierIfNeeded(id);
+ }
+
+ /**
+ * @param {string} ident
+ * @return {string}
+ */
+ function escapeIdentifierIfNeeded(ident)
+ {
+ if (isCSSIdentifier(ident))
+ return ident;
+ var result = "";
+ var shouldEscapeFirst = /^(?:[0-9]|-[0-9-]?)/.test(ident);
aandrey 2013/11/20 12:49:02 let's use similar regex to the one you already use
apavlov 2013/11/20 13:15:28 This regex does not cover the case of an identifie
aandrey 2013/11/20 16:20:49 in console: /^(?:[0-9]|-[0-9-]?)/.test("--a") !/^
apavlov 2013/11/21 07:31:35 Right. But I know what breaks. Valid unicode chars
+ var lastIndex = ident.length - 1;
+ for (var i = 0; i <= lastIndex; ++i) {
+ var c = ident[i];
+ result += ((!i && shouldEscapeFirst) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
aandrey 2013/11/20 12:49:02 nit: hard to read, but I could not simplify this n
apavlov 2013/11/20 13:15:28 Fine with that, even though running this code migh
+ }
+ return result;
+ }
+
+ /**
+ * @param {string} c
+ * @param {boolean} isLast
+ * @return {string}
+ */
+ function escapeAsciiChar(c, isLast)
+ {
+ return "\\" + toHexByte(c) + (isLast ? "" : " ");
+ }
+
+ /**
+ * @param {string} c
+ */
+ function toHexByte(c)
+ {
+ var hexByte = c.charCodeAt(0).toString(16);
+ if (hexByte.length === 1)
+ hexByte = "0" + hexByte;
+ return hexByte;
+ }
+
+ /**
+ * @param {string} c
+ * @return {boolean}
+ */
+ function isCSSIdentChar(c)
+ {
+ if (/[a-zA-Z0-9_-]/.test(c))
aandrey 2013/11/20 12:49:02 /^[a-zA-Z0-9_-]$/
apavlov 2013/11/20 13:15:28 Does it make much sense given that |c| is guarante
aandrey 2013/11/20 16:20:49 I think adding ^ and $ restrictions should always
apavlov 2013/11/21 07:31:35 That's the case for multichar strings. In our case
+ return true;
+ return c.charCodeAt(0) >= 0xA0;
+ }
+
+ /**
+ * @param {string} value
+ * @return {boolean}
+ */
+ function isCSSIdentifier(value)
pfeldman 2013/11/20 14:08:48 Also, DOM should not know this much about CSS
apavlov 2013/11/20 14:23:19 I'm fine with adding a lazy-loaded class. Or will
+ {
+ return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
+ }
+
+ var uniqueClassNames = elementClassNames(this);
+ var uniqueClassNamesLeft = 0;
+ for (var name in uniqueClassNames)
+ ++uniqueClassNamesLeft;
+ var needsClassNames = false;
+ var needsNthChild = false;
+ var ownIndex = -1;
+ var siblings = parent.children();
+ for (var i = 0; (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {
+ var sibling = siblings[i];
+ if (sibling === this) {
+ ownIndex = i;
+ continue;
+ }
+ if (sibling.nodeNameInCorrectCase() !== nodeName)
+ continue;
+ if (!uniqueClassNamesLeft) {
+ needsNthChild = true;
+ continue;
+ }
+
+ needsClassNames = true;
+ var siblingClassNames = elementClassNames(sibling);
+ for (var siblingClass in siblingClassNames) {
+ if (!uniqueClassNames.hasOwnProperty(siblingClass))
+ continue;
+ delete uniqueClassNames[siblingClass];
+ if (!--uniqueClassNamesLeft) {
+ needsNthChild = true;
+ break;
+ }
+ }
+ }
+
+ var result = nodeName;
+ if (needsClassNames) {
+ for (var className in uniqueClassNames)
+ result += "." + escapeIdentifierIfNeeded(className);
+ }
+ if (needsNthChild)
+ result += ":nth-child(" + (ownIndex + 1) + ")";
+ return new WebInspector.DOMNode.PathStep(result, false);
+ },
+
+ /**
+ * @param {boolean} optimized
+ * @return {string}
+ */
xPath: function(optimized)
{
if (this._nodeType === Node.DOCUMENT_NODE)
@@ -708,7 +887,7 @@ WebInspector.DOMNode.prototype = {
/**
* @param {boolean} optimized
- * @return {WebInspector.DOMNode.XPathStep}
+ * @return {WebInspector.DOMNode.PathStep}
*/
_xPathValue: function(optimized)
{
@@ -720,7 +899,7 @@ WebInspector.DOMNode.prototype = {
switch (this._nodeType) {
case Node.ELEMENT_NODE:
if (optimized && this.getAttribute("id"))
- return new WebInspector.DOMNode.XPathStep("//*[@id=\"" + this.getAttribute("id") + "\"]", true);
+ return new WebInspector.DOMNode.PathStep("//*[@id=\"" + this.getAttribute("id") + "\"]", true);
ownValue = this._localName;
break;
case Node.ATTRIBUTE_NODE:
@@ -747,7 +926,7 @@ WebInspector.DOMNode.prototype = {
if (ownIndex > 0)
ownValue += "[" + ownIndex + "]";
- return new WebInspector.DOMNode.XPathStep(ownValue, this._nodeType === Node.DOCUMENT_NODE);
+ return new WebInspector.DOMNode.PathStep(ownValue, this._nodeType === Node.DOCUMENT_NODE);
},
/**

Powered by Google App Engine
This is Rietveld 408576698