Bug 677930 - Style Inspector: make URLs clickable.
authorBrian Grinstead <briangrinstead@gmail.com>
Fri, 19 Apr 2013 16:30:33 -0500
changeset 141060 8911b764dc1e9b65d94facac590a737f10e6f958
parent 141059 229cd2ebe225bc6d6aa4038d491f04fcd958ae6d
child 141061 40dafc3767942c72413c842a84b10a756b071ae8
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs677930
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 677930 - Style Inspector: make URLs clickable.
browser/devtools/styleinspector/CssLogic.jsm
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/ruleview.css
browser/devtools/styleinspector/test/Makefile.in
browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html
browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js
browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css
browser/devtools/styleinspector/test/test-image.png
--- a/browser/devtools/styleinspector/CssLogic.jsm
+++ b/browser/devtools/styleinspector/CssLogic.jsm
@@ -740,16 +740,34 @@ CssLogic.isContentStylesheet = function 
   if (aSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
     return CssLogic.isContentStylesheet(aSheet.parentStyleSheet);
   }
 
   return false;
 };
 
 /**
+ * Get a source for a stylesheet, taking into account embedded stylesheets
+ * for which we need to use document.defaultView.location.href rather than
+ * sheet.href
+ *
+ * @param {CSSStyleSheet} aSheet the DOM object for the style sheet.
+ * @return {string} the address of the stylesheet.
+ */
+CssLogic.href = function CssLogic_href(aSheet)
+{
+  let href = aSheet.href;
+  if (!href) {
+    href = aSheet.ownerNode.ownerDocument.location;
+  }
+
+  return href;
+};
+
+/**
  * Return a shortened version of a style sheet's source.
  *
  * @param {CSSStyleSheet} aSheet the DOM object for the style sheet.
  */
 CssLogic.shortSource = function CssLogic_shortSource(aSheet)
 {
   // Use a string like "inline" if there is no source href
   if (!aSheet || !aSheet.href) {
@@ -922,31 +940,27 @@ CssSheet.prototype = {
   {
     if (this._mediaMatches === null) {
       this._mediaMatches = this._cssLogic.mediaMatches(this.domSheet);
     }
     return this._mediaMatches;
   },
 
   /**
-   * Get a source for a stylesheet, taking into account embedded stylesheets
-   * for which we need to use document.defaultView.location.href rather than
-   * sheet.href
+   * Get a source for a stylesheet, using CssLogic.href
    *
    * @return {string} the address of the stylesheet.
    */
   get href()
   {
-    if (!this._href) {
-      this._href = this.domSheet.href;
-      if (!this._href) {
-        this._href = this.domSheet.ownerNode.ownerDocument.location;
-      }
+    if (this._href) {
+      return this._href;
     }
 
+    this._href = CssLogic.href(this.domSheet);
     return this._href;
   },
 
   /**
    * Create a shorthand version of the href of a stylesheet.
    *
    * @return {string} the shorthand source of the stylesheet.
    */
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -19,16 +19,22 @@ const XUL_NS = "http://www.mozilla.org/k
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
 
 // Used to parse a single property line.
 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
 
+// Used to parse an external resource from a property value
+const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
+
+const IOService = Components.classes["@mozilla.org/network/io-service;1"]
+                  .getService(Components.interfaces.nsIIOService);
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/CssLogic.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
 
 this.EXPORTED_SYMBOLS = ["CssRuleView",
                          "_ElementStyle"];
 
@@ -1316,16 +1322,22 @@ RuleEditor.prototype = {
  * @constructor
  */
 function TextPropertyEditor(aRuleEditor, aProperty)
 {
   this.doc = aRuleEditor.doc;
   this.prop = aProperty;
   this.prop.editor = this;
 
+  let sheet = this.prop.rule.sheet;
+  let href = sheet ? CssLogic.href(sheet) : null;
+  if (href) {
+    this.sheetURI = IOService.newURI(href, null, null);
+  }
+
   this._onEnableClicked = this._onEnableClicked.bind(this);
   this._onExpandClicked = this._onExpandClicked.bind(this);
   this._onStartEditing = this._onStartEditing.bind(this);
   this._onNameDone = this._onNameDone.bind(this);
   this._onValueDone = this._onValueDone.bind(this);
 
   this._create();
   this.update();
@@ -1432,16 +1444,46 @@ TextPropertyEditor.prototype = {
       done: this._onValueDone,
       validate: this._validate.bind(this),
       warning: this.warning,
       advanceChars: ';'
     });
   },
 
   /**
+   * Resolve a URI based on the rule stylesheet
+   * @param {string} relativePath the path to resolve
+   * @return {string} the resolved path.
+   */
+  resolveURI: function(relativePath)
+  {
+    if (this.sheetURI) {
+      relativePath = this.sheetURI.resolve(relativePath);
+    }
+    return relativePath;
+  },
+
+  /**
+   * Check the property value to find an external resource (if any).
+   * @return {string} the URI in the property value, or null if there is no match.
+   */
+  getResourceURI: function()
+  {
+    let val = this.prop.value;
+    let uriMatch = CSS_RESOURCE_RE.exec(val);
+    let uri = null;
+
+    if (uriMatch && uriMatch[1]) {
+      uri = uriMatch[1];
+    }
+
+    return uri;
+  },
+
+  /**
    * Populate the span based on changes to the TextProperty.
    */
   update: function TextPropertyEditor_update()
   {
     if (this.prop.enabled) {
       this.enable.style.removeProperty("visibility");
       this.enable.setAttribute("checked", "");
     } else {
@@ -1459,17 +1501,42 @@ TextPropertyEditor.prototype = {
     this.nameSpan.textContent = name;
 
     // Combine the property's value and priority into one string for
     // the value.
     let val = this.prop.value;
     if (this.prop.priority) {
       val += " !" + this.prop.priority;
     }
-    this.valueSpan.textContent = val;
+
+    // Treat URLs differently than other properties.
+    // Allow the user to click a link to the resource and open it.
+    let resourceURI = this.getResourceURI();
+    if (resourceURI) {
+      this.valueSpan.textContent = "";
+
+      appendText(this.valueSpan, val.split(resourceURI)[0]);
+
+      let a = createChild(this.valueSpan, "a",  {
+        target: "_blank",
+        class: "theme-link",
+        textContent: resourceURI,
+        href: this.resolveURI(resourceURI)
+      });
+
+      a.addEventListener("click", function(aEvent) {
+        // Clicks within the link shouldn't trigger editing.
+        aEvent.stopPropagation();
+      }, false);
+
+      appendText(this.valueSpan, val.split(resourceURI)[1]);
+    } else {
+      this.valueSpan.textContent = val;
+    }
+
     this.warning.hidden = this._validate();
 
     let store = this.prop.rule.elementStyle.store;
     let propDirty = store.userProperties.contains(this.prop.rule.style, name);
     if (propDirty) {
       this.element.setAttribute("dirty", "");
     } else {
       this.element.removeAttribute("dirty");
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -23,12 +23,16 @@
   cursor: text;
 }
 
 .ruleview-propertycontainer {
   cursor: text;
   padding-right: 15px;
 }
 
+.ruleview-propertycontainer a {
+  cursor: pointer;
+}
+
 .ruleview-computedlist:not(.styleinspector-open),
 .ruleview-warning[hidden] {
   display: none;
 }
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -32,24 +32,29 @@ MOCHITEST_BROWSER_FILES = \
   browser_ruleview_update.js \
   browser_bug705707_is_content_stylesheet.js \
   browser_bug722196_property_view_media_queries.js \
   browser_bug722196_rule_view_media_queries.js \
   browser_bug_592743_specificity.js \
   browser_bug722691_rule_view_increment.js \
   browser_computedview_734259_style_editor_link.js \
   browser_computedview_copy.js\
+  browser_styleinspector_bug_677930_urls_clickable.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
   browser_bug705707_is_content_stylesheet_linked.css \
   browser_bug705707_is_content_stylesheet_script.css \
   browser_bug705707_is_content_stylesheet.xul \
   browser_bug705707_is_content_stylesheet_xul.css \
   browser_bug722196_identify_media_queries.html \
+  browser_styleinspector_bug_677930_urls_clickable.html \
+  browser_styleinspector_bug_677930_urls_clickable \
+  browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css \
+  test-image.png \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+  <head>
+
+    <link href="./browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css" rel="stylesheet" type="text/css">
+
+  </head>
+  <body>
+
+    <div class="relative">Background image with relative path (loaded from external css)</div>
+
+    <div class="absolute">Background image with absolute path (loaded from external css)</div>
+
+    <div class="base64">Background image with base64 url (loaded from external css)</div>
+
+    <div class="inline" style="background: url(test-image.png);">Background image with relative path (loaded from style attribute)</div>';
+
+    <div class="noimage">No background image :(</div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable.js
@@ -0,0 +1,97 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests to make sure that URLs are clickable in the rule view
+
+let doc;
+let computedView;
+let inspector;
+
+const BASE_URL = "http://example.com/browser/browser/" +
+                 "devtools/styleinspector/test/";
+const TEST_URI = BASE_URL +
+                 "browser_styleinspector_bug_677930_urls_clickable.html";
+const TEST_IMAGE = BASE_URL + "test-image.png";
+const BASE_64_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+function createDocument()
+{
+  doc.title = "Style Inspector URL Clickable test";
+
+  openInspector(function(aInspector) {
+    inspector = aInspector;
+    executeSoon(selectNode);
+  });
+}
+
+
+function selectNode(aInspector)
+{
+  let sidebar = inspector.sidebar;
+  let iframe = sidebar._tabbox.querySelector(".iframe-ruleview");
+  let contentDoc = iframe.contentWindow.document;
+
+  let relative = doc.querySelector(".relative");
+  let absolute = doc.querySelector(".absolute");
+  let inline = doc.querySelector(".inline");
+  let base64 = doc.querySelector(".base64");
+  let noimage = doc.querySelector(".noimage");
+
+  ok(relative, "captain, we have the relative div");
+  ok(absolute, "captain, we have the absolute div");
+  ok(inline, "captain, we have the inline div");
+  ok(base64, "captain, we have the base64 div");
+  ok(noimage, "captain, we have the noimage div");
+
+  inspector.selection.setNode(relative);
+  is(inspector.selection.node, relative, "selection matches the relative element");
+  let relativeLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+  ok (relativeLink, "Link exists for relative node");
+  ok (relativeLink.getAttribute("href"), TEST_IMAGE);
+
+  inspector.selection.setNode(absolute);
+  is(inspector.selection.node, absolute, "selection matches the absolute element");
+  let absoluteLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+  ok (absoluteLink, "Link exists for absolute node");
+  ok (absoluteLink.getAttribute("href"), TEST_IMAGE);
+
+  inspector.selection.setNode(inline);
+  is(inspector.selection.node, inline, "selection matches the inline element");
+  let inlineLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+  ok (inlineLink, "Link exists for inline node");
+  ok (inlineLink.getAttribute("href"), TEST_IMAGE);
+
+  inspector.selection.setNode(base64);
+  is(inspector.selection.node, base64, "selection matches the base64 element");
+  let base64Link = contentDoc.querySelector(".ruleview-propertycontainer a");
+  ok (base64Link, "Link exists for base64 node");
+  ok (base64Link.getAttribute("href"), BASE_64_URL);
+
+  inspector.selection.setNode(noimage);
+  is(inspector.selection.node, noimage, "selection matches the inline element");
+  let noimageLink = contentDoc.querySelector(".ruleview-propertycontainer a");
+  ok (!noimageLink, "There is no link for the node with no background image");
+
+  finishUp();
+}
+
+function finishUp()
+{
+  doc = computedView = inspector = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = TEST_URI;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_bug_677930_urls_clickable/browser_styleinspector_bug_677930_urls_clickable.css
@@ -0,0 +1,9 @@
+.relative {
+    background-image: url(../test-image.png);
+}
+.absolute {
+    background: url("http://example.com/browser/browser/devtools/styleinspector/test/test-image.png");
+}
+.base64 {
+    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
+}
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..769c636340e11f9d2a0b7eb6a84d574dd9563f0c
GIT binary patch
literal 580
zc$@)50=xZ*P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz)=5M`RCwBA
z{Qv(y10?_;fS8au@87>?@9gaSt*os4-<6T^zln*-e<1(Y>eZ{a-@A8D7MlS80mJ}u
z0SKQtbH>fs*!aH-1H=C_K)ecw?*efe5W7I}s#U9Y!PLVrKmdV>nKNfT1{(H16si$K
zmjm&CV+al6D=8`c7X*o+82}JK4A3z64>JJB_`e(E3S)?2<zN>X{|^lf1sei(+1<Me
zFarPr2&}oIqvIC?)OL^o?-&p^!wh-{#k-;2f_VoZfS{%bLi}zFF<>UtAY{W<LBj?n
zz6$CsfB=HI;P*^44eyZn#$YcBg1rF~2L~`P&;bI75#$0;v@zVf$It;Z4d`qJS0Dzu
zh@l)BQx!mb7Roj*FK2kaXAgtR*|Q9LfP8=e0=od{pY0$UI-sVXf!f>w#jt?wfcn3@
zy!=0d5|Evi_8%aC7~Z{m#}1AKK|~<_M{=g1px}QcP=ErR57Iaj7$e5OumVLr$n^jL
z1djz!sJcLHd57c@W2lWFi_p^m2m=HVoB>kgf@C|$pqa*ykjAAMgaHBwo)>`0c=vmt
z+g3yQpuk*x7A(#H^u|wInF%0(FiZq_r2`t3pnwGh6fWCA7$ATcDb3CR0R{jJCzQv)
SYsoAC0000<MNUMnLSTYrIq9PS