author | Michael Ratcliffe <mratcliffe@mozilla.com> |
Wed, 04 Sep 2013 17:43:40 +0200 | |
changeset 145486 | 4b65dc0a16cd164079203213b455d747f1da3273 |
parent 145485 | d1d0a3fdbfe9bb9e2fcc28dd27e30f2937058b37 |
child 145487 | e4ca2c1ba7c3adf9c21195b4f46fcb2aa504181c |
push id | 25214 |
push user | kwierso@gmail.com |
push date | Thu, 05 Sep 2013 00:02:20 +0000 |
treeherder | mozilla-central@99bd249e5a20 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mratcliffe |
bugs | 694019 |
milestone | 26.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
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1070,16 +1070,17 @@ pref("devtools.toolbox.selectedTool", "w pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]'); pref("devtools.toolbox.sideEnabled", true); // Enable the Inspector pref("devtools.inspector.enabled", true); pref("devtools.inspector.activeSidebar", "ruleview"); pref("devtools.inspector.markupPreview", false); pref("devtools.inspector.remote", false); +pref("devtools.inspector.show_pseudo_elements", true); // Enable the Layout View pref("devtools.layoutview.enabled", true); pref("devtools.layoutview.open", false); // Enable the Responsive UI tool pref("devtools.responsiveUI.enabled", true); pref("devtools.responsiveUI.no-reload-notification", false);
--- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -6,17 +6,17 @@ "use strict"; const {Cc, Ci, Cu} = require("chrome"); const promise = require("sdk/core/promise"); let {CssLogic} = require("devtools/styleinspector/css-logic"); let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor"); -let {ELEMENT_STYLE} = require("devtools/server/actors/styles"); +let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; /** @@ -198,29 +198,41 @@ ElementStyle.prototype = { this.rules = []; for (let entry of entries) { this._maybeAddRule(entry); } // Mark overridden computed styles. - this.markOverridden(); + this.markOverriddenAll(); + + this._sortRulesForPseudoElement(); // We're done with the previous list of rules. delete this._refreshRules; return null; }); }).then(null, promiseWarn); this.populated = populated; return this.populated; }, /** + * Put pseudo elements in front of others. + */ + _sortRulesForPseudoElement: function ElementStyle_sortRulesForPseudoElement() + { + this.rules = this.rules.sort((a, b) => { + return (a.pseudoElement || "z") > (b.pseudoElement || "z"); + }); + }, + + /** * Add a rule if it's one we care about. Filters out duplicates and * inherited styles with no inherited properties. * * @param {object} aOptions * Options for creating the Rule, see the Rule constructor. * * @return {bool} true if we added the rule. */ @@ -261,32 +273,48 @@ ElementStyle.prototype = { return false; } this.rules.push(rule); return true; }, /** - * Mark the properties listed in this.rules with an overridden flag - * if an earlier property overrides it. + * Calls markOverridden with all supported pseudo elements */ - markOverridden: function ElementStyle_markOverridden() + markOverriddenAll: function ElementStyle_markOverriddenAll() + { + this.markOverridden(); + for (let pseudo of PSEUDO_ELEMENTS) { + this.markOverridden(pseudo); + } + }, + + /** + * Mark the properties listed in this.rules for a given pseudo element + * with an overridden flag if an earlier property overrides it. + * @param {string} pseudo + * Which pseudo element to flag as overridden. + * Empty string or undefined will default to no pseudo element. + */ + markOverridden: function ElementStyle_markOverridden(pseudo="") { // Gather all the text properties applied by these rules, ordered // from more- to less-specific. let textProps = []; - for each (let rule in this.rules) { - textProps = textProps.concat(rule.textProps.slice(0).reverse()); + for (let rule of this.rules) { + if (rule.pseudoElement == pseudo) { + textProps = textProps.concat(rule.textProps.slice(0).reverse()); + } } // Gather all the computed properties applied by those text // properties. let computedProps = []; - for each (let textProp in textProps) { + for (let textProp of textProps) { computedProps = computedProps.concat(textProp.computed); } // Walk over the computed properties. As we see a property name // for the first time, mark that property's name as taken by this // property. // // If we come across a property whose name is already taken, check @@ -297,17 +325,17 @@ ElementStyle.prototype = { // the new property. // // If the new property is a lower or equal priority, mark it as // overridden. // // _overriddenDirty will be set on each prop, indicating whether its // dirty status changed during this pass. let taken = {}; - for each (let computedProp in computedProps) { + for (let computedProp of computedProps) { let earlier = taken[computedProp.name]; let overridden; if (earlier && computedProp.priority === "important" && earlier.priority !== "important") { // New property is higher priority. Mark the earlier property // overridden (which will reverse its dirty state). earlier._overriddenDirty = !earlier._overriddenDirty; @@ -323,17 +351,17 @@ ElementStyle.prototype = { taken[computedProp.name] = computedProp; } } // For each TextProperty, mark it overridden if all of its // computed properties are marked overridden. Update the text // property's associated editor, if any. This will clear the // _overriddenDirty state on all computed properties. - for each (let textProp in textProps) { + for (let textProp of textProps) { // _updatePropertyOverridden will return true if the // overridden state has changed for the text property. if (this._updatePropertyOverridden(textProp)) { textProp.updateEditor(); } } }, @@ -379,16 +407,17 @@ ElementStyle.prototype = { * @constructor */ function Rule(aElementStyle, aOptions) { this.elementStyle = aElementStyle; this.domRule = aOptions.rule || null; this.style = aOptions.rule; this.matchedSelectors = aOptions.matchedSelectors || []; + this.pseudoElement = aOptions.pseudoElement || ""; this.inherited = aOptions.inherited || null; this._modificationDepth = 0; if (this.domRule) { let parentRule = this.domRule.parentRule; if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) { this.mediaText = parentRule.mediaText; @@ -553,17 +582,17 @@ Rule.prototype = { this.style, textProp.name, null, cssProp.value, textProp.value); } textProp.priority = cssProp.priority; } - this.elementStyle.markOverridden(); + this.elementStyle.markOverriddenAll(); if (promise === this._applyingModifications) { this._applyingModifications = null; } this.elementStyle._changed(); }).then(null, promiseWarn); this._applyingModifications = promise; @@ -1072,16 +1101,17 @@ CssRuleView.prototype = { _populate: function() { let elementStyle = this._elementStyle; return this._elementStyle.populate().then(() => { if (this._elementStyle != elementStyle) { return promise.reject("element changed"); } this._createEditors(); + // Notify anyone that cares that we refreshed. var evt = this.doc.createEvent("Events"); evt.initEvent("CssRuleViewRefreshed", true, false); this.element.dispatchEvent(evt); return undefined; }).then(null, promiseWarn); }, @@ -1127,43 +1157,128 @@ CssRuleView.prototype = { _changed: function CssRuleView_changed() { var evt = this.doc.createEvent("Events"); evt.initEvent("CssRuleViewChanged", true, false); this.element.dispatchEvent(evt); }, /** + * Text for header that shows above rules for this element + */ + get selectedElementLabel () + { + if (this._selectedElementLabel) { + return this._selectedElementLabel; + } + this._selectedElementLabel = CssLogic.l10n("rule.selectedElement"); + return this._selectedElementLabel; + }, + + /** + * Text for header that shows above rules for pseudo elements + */ + get pseudoElementLabel () + { + if (this._pseudoElementLabel) { + return this._pseudoElementLabel; + } + this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement"); + return this._pseudoElementLabel; + }, + + togglePseudoElementVisibility: function(value) + { + this._showPseudoElements = !!value; + let isOpen = this.showPseudoElements; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + isOpen); + + this.element.classList.toggle("show-pseudo-elements", isOpen); + + if (this.pseudoElementTwisty) { + if (isOpen) { + this.pseudoElementTwisty.setAttribute("open", "true"); + } + else { + this.pseudoElementTwisty.removeAttribute("open"); + } + } + }, + + get showPseudoElements () + { + if (this._showPseudoElements === undefined) { + this._showPseudoElements = + Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); + } + return this._showPseudoElements; + }, + + /** * Creates editor UI for each of the rules in _elementStyle. */ _createEditors: function CssRuleView_createEditors() { // Run through the current list of rules, attaching // their editors in order. Create editors if needed. let lastInheritedSource = ""; + let seenPseudoElement = false; + let seenNormalElement = false; + for (let rule of this._elementStyle.rules) { if (rule.domRule.system) { continue; } + // Only print header for this element if there are pseudo elements + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { + seenNormalElement = true; + let div = this.doc.createElementNS(HTML_NS, "div"); + div.className = "theme-gutter ruleview-header"; + div.textContent = this.selectedElementLabel; + this.element.appendChild(div); + } + let inheritedSource = rule.inheritedSource; if (inheritedSource != lastInheritedSource) { - let h2 = this.doc.createElementNS(HTML_NS, "div"); - h2.className = "ruleview-rule-inheritance theme-gutter"; - h2.textContent = inheritedSource; + let div = this.doc.createElementNS(HTML_NS, "div"); + div.className = "theme-gutter ruleview-header"; + div.textContent = inheritedSource; lastInheritedSource = inheritedSource; - this.element.appendChild(h2); + this.element.appendChild(div); + } + + if (!seenPseudoElement && rule.pseudoElement) { + seenPseudoElement = true; + + let div = this.doc.createElementNS(HTML_NS, "div"); + div.className = "theme-gutter ruleview-header"; + div.textContent = this.pseudoElementLabel; + + let twisty = this.pseudoElementTwisty = + this.doc.createElementNS(HTML_NS, "span"); + twisty.className = "ruleview-expander theme-twisty"; + twisty.addEventListener("click", () => { + this.togglePseudoElementVisibility(!this.showPseudoElements); + }, false); + + div.insertBefore(twisty, div.firstChild); + this.element.appendChild(div); } if (!rule.editor) { new RuleEditor(this, rule); } this.element.appendChild(rule.editor.element); } + + this.togglePseudoElementVisibility(this.showPseudoElements); }, /** * Copy selected text from the rule view. * * @param {Event} aEvent * The event object. */ @@ -1221,16 +1336,19 @@ function RuleEditor(aRuleView, aRule) } RuleEditor.prototype = { _create: function RuleEditor_create() { this.element = this.doc.createElementNS(HTML_NS, "div"); this.element.className = "ruleview-rule theme-separator"; this.element._ruleEditor = this; + if (this.rule.pseudoElement) { + this.element.classList.add("ruleview-rule-pseudo-element"); + } // Give a relative position for the inplace editor's measurement // span to be placed absolutely against. this.element.style.position = "relative"; // Add the source link. let source = createChild(this.element, "div", { class: "ruleview-rule-source theme-link" @@ -1352,22 +1470,25 @@ RuleEditor.prototype = { * Programatically add a new property to the rule. * * @param {string} aName * Property name. * @param {string} aValue * Property value. * @param {string} aPriority * Property priority. + * @return {TextProperty} + * The new property */ addProperty: function RuleEditor_addProperty(aName, aValue, aPriority) { let prop = this.rule.createProperty(aName, aValue, aPriority); let editor = new TextPropertyEditor(this, prop); this.propertyList.appendChild(editor.element); + return prop; }, /** * Create a text input for a property name. If a non-empty property * name is given, we'll create a real TextProperty and add it to the * rule. */ newProperty: function RuleEditor_newProperty()
--- a/browser/devtools/styleinspector/ruleview.css +++ b/browser/devtools/styleinspector/ruleview.css @@ -31,8 +31,26 @@ .ruleview-propertycontainer a { cursor: pointer; } .ruleview-computedlist:not(.styleinspector-open), .ruleview-warning[hidden] { display: none; } + +.ruleview-rule-pseudo-element { + display: none; +} + +.show-pseudo-elements .ruleview-rule-pseudo-element { + display: block; +} + +.ruleview .ruleview-expander { + vertical-align: middle; +} + +.ruleview-header { + vertical-align:middle; + height: 1.5em; + line-height: 1.5em; +} \ No newline at end of file
--- a/browser/devtools/styleinspector/test/Makefile.in +++ b/browser/devtools/styleinspector/test/Makefile.in @@ -38,16 +38,17 @@ MOCHITEST_BROWSER_FILES = \ browser_computedview_734259_style_editor_link.js \ browser_computedview_copy.js\ browser_styleinspector_bug_677930_urls_clickable.js \ browser_bug893965_css_property_completion_new_property.js \ browser_bug893965_css_property_completion_existing_property.js \ browser_bug894376_css_value_completion_new_property_value_pair.js \ browser_bug894376_css_value_completion_existing_property_value_pair.js \ browser_ruleview_bug_902966_revert_value_on_ESC.js \ + browser_ruleview_pseudoelement.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 \ @@ -55,11 +56,12 @@ MOCHITEST_BROWSER_FILES += \ 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 \ + browser_ruleview_pseudoelement.html \ $(NULL) include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.html @@ -0,0 +1,115 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> + <head> + <style> + +body { + color: #333; +} + +.box { + float:left; + width: 128px; + height: 128px; + background: #ddd; + padding: 32px; + margin: 32px; + position:relative; +} + +* { + cursor: default; +} + +nothing { + cursor: pointer; +} + +p::-moz-selection { + color: white; + background: black; +} +p::selection { + color: white; + background: black; +} + +p:first-line { + background: blue; +} +p:first-letter { + color: red; + font-size: 130%; +} + +.box:before { + background: green; + content: " "; + position: absolute; + height:32px; + width:32px; +} + +.box:after { + background: red; + content: " "; + position: absolute; + border-radius: 50%; + height:32px; + width:32px; + top: 50%; + left: 50%; + margin-top: -16px; + margin-left: -16px; +} + +.topleft:before { + top:0; + left:0; +} + +.topright:before { + top:0; + right:0; +} + +.bottomright:before { + bottom:10px; + right:10px; + color: red; +} + +.bottomright:before { + bottom:0; + right:0; +} + +.bottomleft:before { + bottom:0; + left:0; +} + + </style> + </head> + <body> + <h1>ruleview pseudoelement($("test"));</h1> + + <div id="topleft" class="box topleft"> + <p>Top Left<br />Position</p> + </div> + + <div id="topright" class="box topright"> + <p>Top Right<br />Position</p> + </div> + + <div id="bottomright" class="box bottomright"> + <p>Bottom Right<br />Position</p> + </div> + + <div id="bottomleft" class="box bottomleft"> + <p>Bottom Left<br />Position</p> + </div> + + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js @@ -0,0 +1,317 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let doc; +let inspector; +let view; + +const TEST_URI = "http://example.com/browser/browser/" + + "devtools/styleinspector/test/" + + "browser_ruleview_pseudoelement.html"; + +function testPseudoElements(aInspector, aRuleView) +{ + inspector = aInspector; + view = aRuleView; + + testTopLeft(); +} + +function testTopLeft() +{ + testNode(doc.querySelector("#topleft"), (element, elementStyle) => { + let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; }); + let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; }); + let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; }); + let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; }); + let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; }); + let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; }); + + is(elementRules.length, 4, "TopLeft has the correct number of non psuedo element rules"); + is(afterRules.length, 1, "TopLeft has the correct number of :after rules"); + is(beforeRules.length, 2, "TopLeft has the correct number of :before rules"); + is(firstLineRules.length, 0, "TopLeft has the correct number of :first-line rules"); + is(firstLetterRules.length, 0, "TopLeft has the correct number of :first-letter rules"); + is(selectionRules.length, 0, "TopLeft has the correct number of :selection rules"); + + let gutters = view.element.querySelectorAll(".theme-gutter"); + is (gutters.length, 3, "There are three gutter headings"); + is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct"); + is (gutters[1].textContent, "This Element", "Gutter heading is correct"); + is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct"); + + // Make sure that clicking on the twisty hides pseudo elements + let expander = gutters[0].querySelector(".ruleview-expander"); + ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded"); + expander.click(); + ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by twisty"); + expander.click(); + ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded again"); + expander.click(); + + let defaultView = element.ownerDocument.defaultView; + let elementRule = elementRules[0]; + let elementRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementRule; + })[0]._ruleEditor; + + let elementAfterRule = afterRules[0]; + let elementAfterRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementAfterRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementAfterRule.textProps), + "background: none repeat scroll 0% 0% red; content: \" \"; position: absolute; " + + "border-radius: 50%; height: 32px; width: 32px; top: 50%; left: 50%; margin-top: -16px; margin-left: -16px", + "TopLeft after properties are correct" + ); + + let elementBeforeRule = beforeRules[0]; + let elementBeforeRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementBeforeRule.textProps), + "top: 0px; left: 0px", + "TopLeft before properties are correct" + ); + + let firstProp = elementAfterRuleView.addProperty("background-color", "rgb(0, 255, 0)", ""); + let secondProp = elementAfterRuleView.addProperty("padding", "100px", ""); + + is (firstProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 2], + "First added property is on back of array"); + is (secondProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 1], + "Second added property is on back of array"); + + promiseDone(elementAfterRule._applyingModifications.then(() => { + is(defaultView.getComputedStyle(element, ":after").getPropertyValue("background-color"), + "rgb(0, 255, 0)", "Added property should have been used."); + is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"), + "100px", "Added property should have been used."); + is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"), + "32px", "Added property should not apply to element"); + + secondProp.setEnabled(false); + + return elementAfterRule._applyingModifications; + }).then(() => { + is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"), "0px", + "Disabled property should have been used."); + is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"), "32px", + "Added property should not apply to element"); + + secondProp.setEnabled(true); + + return elementAfterRule._applyingModifications; + }).then(() => { + is(defaultView.getComputedStyle(element, ":after").getPropertyValue("padding-top"), "100px", + "Enabled property should have been used."); + is(defaultView.getComputedStyle(element).getPropertyValue("padding-top"), "32px", + "Added property should not apply to element"); + + let firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", ""); + + return elementRule._applyingModifications; + }).then(() => { + is(defaultView.getComputedStyle(element).getPropertyValue("background-color"), "rgb(0, 0, 255)", + "Added property should have been used."); + is(defaultView.getComputedStyle(element, ":after").getPropertyValue("background-color"), "rgb(0, 255, 0)", + "Added prop does not apply to pseudo"); + + testTopRight(); + })); + }); +} + +function testTopRight() +{ + testNode(doc.querySelector("#topright"), (element, elementStyle) => { + + let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; }); + let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; }); + let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; }); + let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; }); + let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; }); + let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; }); + + is(elementRules.length, 4, "TopRight has the correct number of non psuedo element rules"); + is(afterRules.length, 1, "TopRight has the correct number of :after rules"); + is(beforeRules.length, 2, "TopRight has the correct number of :before rules"); + is(firstLineRules.length, 0, "TopRight has the correct number of :first-line rules"); + is(firstLetterRules.length, 0, "TopRight has the correct number of :first-letter rules"); + is(selectionRules.length, 0, "TopRight has the correct number of :selection rules"); + + let gutters = view.element.querySelectorAll(".theme-gutter"); + is (gutters.length, 3, "There are three gutter headings"); + is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct"); + is (gutters[1].textContent, "This Element", "Gutter heading is correct"); + is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct"); + + let expander = gutters[0].querySelector(".ruleview-expander"); + ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements remain collapsed after switching element"); + expander.scrollIntoView(); + expander.click(); + ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are shown again after clicking twisty"); + + testBottomRight(); + }); +} + +function testBottomRight() +{ + testNode(doc.querySelector("#bottomright"), (element, elementStyle) => { + + let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; }); + let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; }); + let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; }); + let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; }); + let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; }); + let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; }); + + is(elementRules.length, 4, "BottomRight has the correct number of non psuedo element rules"); + is(afterRules.length, 1, "BottomRight has the correct number of :after rules"); + is(beforeRules.length, 3, "BottomRight has the correct number of :before rules"); + is(firstLineRules.length, 0, "BottomRight has the correct number of :first-line rules"); + is(firstLetterRules.length, 0, "BottomRight has the correct number of :first-letter rules"); + is(selectionRules.length, 0, "BottomRight has the correct number of :selection rules"); + + testBottomLeft(); + }); +} + +function testBottomLeft() +{ + testNode(doc.querySelector("#bottomleft"), (element, elementStyle) => { + + let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; }); + let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; }); + let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; }); + let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; }); + let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; }); + let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; }); + + is(elementRules.length, 4, "BottomLeft has the correct number of non psuedo element rules"); + is(afterRules.length, 1, "BottomLeft has the correct number of :after rules"); + is(beforeRules.length, 2, "BottomLeft has the correct number of :before rules"); + is(firstLineRules.length, 0, "BottomLeft has the correct number of :first-line rules"); + is(firstLetterRules.length, 0, "BottomLeft has the correct number of :first-letter rules"); + is(selectionRules.length, 0, "BottomLeft has the correct number of :selection rules"); + + testParagraph(); + }); +} + +function testParagraph() +{ + testNode(doc.querySelector("#bottomleft p"), (element, elementStyle) => { + + let elementRules = elementStyle.rules.filter((rule) => { return !rule.pseudoElement; }); + let afterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":after"; }); + let beforeRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":before"; }); + let firstLineRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-line"; }); + let firstLetterRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":first-letter"; }); + let selectionRules = elementStyle.rules.filter((rule) => { return rule.pseudoElement === ":-moz-selection"; }); + + is(elementRules.length, 3, "Paragraph has the correct number of non psuedo element rules"); + is(afterRules.length, 0, "Paragraph has the correct number of :after rules"); + is(beforeRules.length, 0, "Paragraph has the correct number of :before rules"); + is(firstLineRules.length, 1, "Paragraph has the correct number of :first-line rules"); + is(firstLetterRules.length, 1, "Paragraph has the correct number of :first-letter rules"); + is(selectionRules.length, 1, "Paragraph has the correct number of :selection rules"); + + let gutters = view.element.querySelectorAll(".theme-gutter"); + is (gutters.length, 3, "There are three gutter headings"); + is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct"); + is (gutters[1].textContent, "This Element", "Gutter heading is correct"); + is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct"); + + let elementFirstLineRule = firstLineRules[0]; + let elementFirstLineRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementFirstLineRule.textProps), + "background: none repeat scroll 0% 0% blue", + "Paragraph first-line properties are correct" + ); + + let elementFirstLetterRule = firstLetterRules[0]; + let elementFirstLetterRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementFirstLetterRule.textProps), + "color: red; font-size: 130%", + "Paragraph first-letter properties are correct" + ); + + let elementSelectionRule = selectionRules[0]; + let elementSelectionRuleView = [].filter.call(view.element.children, (e) => { + return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule; + })[0]._ruleEditor; + + is + ( + convertTextPropsToString(elementSelectionRule.textProps), + "color: white; background: none repeat scroll 0% 0% black", + "Paragraph first-letter properties are correct" + ); + + testBody(); + }); +} + +function testBody() { + + testNode(doc.querySelector("body"), (element, elementStyle) => { + + let gutters = view.element.querySelectorAll(".theme-gutter"); + is (gutters.length, 0, "There are no gutter headings"); + + finishTest(); + }); + +} +function convertTextPropsToString(textProps) { + return textProps.map((t) => { + return t.name + ": " + t.value; + }).join("; "); +} + +function testNode(node, cb) +{ + inspector.once("inspector-updated", () => { + cb(node, view._elementStyle) + }); + inspector.selection.setNode(node); +} + +function finishTest() +{ + doc = null; + gBrowser.removeCurrentTab(); + finish(); +} + +function test() +{ + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true); + doc = content.document; + waitForFocus(() => openRuleView(testPseudoElements), content); + }, true); + + content.location = TEST_URI; +}
--- a/browser/locales/en-US/chrome/browser/devtools/styleinspector.properties +++ b/browser/locales/en-US/chrome/browser/devtools/styleinspector.properties @@ -30,16 +30,24 @@ rule.sourceInline=inline rule.sourceElement=element # LOCALIZATION NOTE (rule.inheritedFrom): Shown for CSS rules # that were inherited from a parent node. Will be passed a node # identifier of the parent node. # e.g "Inherited from body#bodyID" rule.inheritedFrom=Inherited from %S +# LOCALIZATION NOTE (rule.pseudoElement): Shown for CSS rules +# pseudo element header +rule.pseudoElement=Pseudo-elements + +# LOCALIZATION NOTE (rule.pseudoElement): Shown for CSS rules +# pseudo element header +rule.selectedElement=This Element + # LOCALIZATION NOTE (helpLinkTitle): For each style property # the user can hover it and get a help link button which allows one to # quickly jump to the documentation from the Mozilla Developer Network site. # This is the link title shown in the hover tooltip. helpLinkTitle=Read the documentation for this property # LOCALIZATION NOTE (rule.warning.title): When an invalid property value is # entered into the rule view a warning icon is displayed. This text is used for
--- a/browser/themes/linux/devtools/ruleview.css +++ b/browser/themes/linux/devtools/ruleview.css @@ -9,17 +9,17 @@ .ruleview-rule-source { -moz-padding-start: 5px; cursor: pointer; text-align: right; float: right; -moz-user-select: none; } -.ruleview-rule-inheritance { +.ruleview-header { border-top-width: 1px; border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; padding: 1px 4px; margin-top: 4px; -moz-user-select: none; }
--- a/browser/themes/osx/devtools/ruleview.css +++ b/browser/themes/osx/devtools/ruleview.css @@ -9,26 +9,30 @@ .ruleview-rule-source { -moz-padding-start: 5px; cursor: pointer; text-align: right; float: right; -moz-user-select: none; } -.ruleview-rule-inheritance { +.ruleview-header { border-top-width: 1px; border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; padding: 1px 4px; - margin-top: 4px; -moz-user-select: none; } +.ruleview-rule-pseudo-element { + padding-left:20px; + border-left: solid 10px; +} + .ruleview-rule-source:hover { text-decoration: underline; } .ruleview-rule, #noResults { padding: 2px 4px; }
--- a/browser/themes/windows/devtools/ruleview.css +++ b/browser/themes/windows/devtools/ruleview.css @@ -9,17 +9,17 @@ .ruleview-rule-source { -moz-padding-start: 5px; cursor: pointer; text-align: right; float: right; -moz-user-select: none; } -.ruleview-rule-inheritance { +.ruleview-header { border-top-width: 1px; border-bottom-width: 1px; border-top-style: solid; border-bottom-style: solid; padding: 1px 4px; margin-top: 4px; -moz-user-select: none; }
--- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -15,16 +15,19 @@ loader.lazyGetter(this, "CssLogic", () = loader.lazyGetter(this, "DOMUtils", () => Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)); // The PageStyle actor flattens the DOM CSS objects a little bit, merging // Rules and their Styles into one actor. For elements (which have a style // but no associated rule) we fake a rule with the following style id. const ELEMENT_STYLE = 100; exports.ELEMENT_STYLE = ELEMENT_STYLE; +const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"]; +exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS; + // Predeclare the domnode actor type for use in requests. types.addActorType("domnode"); /** * DOM Nodes returned by the style actor will be owned by the DOM walker * for the connection. */ types.addLifetime("walker", "walker"); @@ -355,46 +358,56 @@ var PageStyleActor = protocol.ActorClass if (!inherited || this._hasInheritedProps(element.style)) { rules.push({ rule: elementStyle, inherited: inherited, }); } - // Get the styles that apply to the element. - let domRules = DOMUtils.getCSSStyleRules(element); + let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS]; + for (let pseudo of pseudoElements) { - // getCSSStyleRules returns ordered from least-specific to - // most-specific. - for (let i = domRules.Count() - 1; i >= 0; i--) { - let domRule = domRules.GetElementAt(i); + // Get the styles that apply to the element. + let domRules = DOMUtils.getCSSStyleRules(element, pseudo); - let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet); - - if (isSystem && options.filter != CssLogic.FILTER.UA) { + if (!domRules) { continue; } - if (inherited) { - // Don't include inherited rules if none of its properties - // are inheritable. - let hasInherited = Array.prototype.some.call(domRule.style, prop => { - return DOMUtils.isInheritedProperty(prop); - }); - if (!hasInherited) { + // getCSSStyleRules returns ordered from least-specific to + // most-specific. + for (let i = domRules.Count() - 1; i >= 0; i--) { + let domRule = domRules.GetElementAt(i); + + let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet); + + if (isSystem && options.filter != CssLogic.FILTER.UA) { continue; } + + if (inherited) { + // Don't include inherited rules if none of its properties + // are inheritable. + let hasInherited = Array.prototype.some.call(domRule.style, prop => { + return DOMUtils.isInheritedProperty(prop); + }); + if (!hasInherited) { + continue; + } + } + + let ruleActor = this._styleRef(domRule); + rules.push({ + rule: ruleActor, + inherited: inherited, + pseudoElement: pseudo + }); } - let ruleActor = this._styleRef(domRule); - rules.push({ - rule: ruleActor, - inherited: inherited, - }); } }, /** * Expand Sets of rules and sheets to include all parent rules and sheets. */ expandSets: function(ruleSet, sheetSet) { // Sets include new items in their iteration