author | Gabriel Luong <gabriel.luong@gmail.com> |
Mon, 07 Jul 2014 09:18:00 +0200 | |
changeset 193121 | a7e7bafd6de9a127ca8d871cbfbf488dc7d30128 |
parent 193120 | 38625d13b84006be5f526c7d9fd8ae9dd09eca05 |
child 193122 | 8252ef80638981dd6bedb99cae714512981323e4 |
push id | 27109 |
push user | ryanvm@gmail.com |
push date | Wed, 09 Jul 2014 20:12:42 +0000 |
treeherder | mozilla-central@fc35681b0a87 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | harth |
bugs | 966895 |
milestone | 33.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/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -437,17 +437,17 @@ function Rule(aElementStyle, aOptions) { Rule.prototype = { mediaText: "", get title() { if (this._title) { return this._title; } this._title = CssLogic.shortSource(this.sheet); - if (this.domRule.type !== ELEMENT_STYLE) { + if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { this._title += ":" + this.ruleLine; } this._title = this._title + (this.mediaText ? " @media " + this.mediaText : ""); return this._title; }, get inheritedSource() { @@ -1071,16 +1071,17 @@ function CssRuleView(aInspector, aDoc, a this.element = this.doc.createElementNS(HTML_NS, "div"); this.element.className = "ruleview devtools-monospace"; this.element.flex = 1; this._outputParser = new OutputParser(); this._buildContextMenu = this._buildContextMenu.bind(this); this._contextMenuUpdate = this._contextMenuUpdate.bind(this); + this._onAddRule = this._onAddRule.bind(this); this._onSelectAll = this._onSelectAll.bind(this); this._onCopy = this._onCopy.bind(this); this._onCopyColor = this._onCopyColor.bind(this); this._onToggleOrigSources = this._onToggleOrigSources.bind(this); this.element.addEventListener("copy", this._onCopy); this._handlePrefChange = this._handlePrefChange.bind(this); @@ -1120,32 +1121,37 @@ CssRuleView.prototype = { */ _buildContextMenu: function() { let doc = this.doc.defaultView.parent.document; this._contextmenu = doc.createElementNS(XUL_NS, "menupopup"); this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); this._contextmenu.id = "rule-view-context-menu"; + this.menuitemAddRule = createMenuItem(this._contextmenu, { + label: "ruleView.contextmenu.addRule", + accesskey: "ruleView.contextmenu.addRule.accessKey", + command: this._onAddRule + }); this.menuitemSelectAll = createMenuItem(this._contextmenu, { label: "ruleView.contextmenu.selectAll", accesskey: "ruleView.contextmenu.selectAll.accessKey", command: this._onSelectAll }); this.menuitemCopy = createMenuItem(this._contextmenu, { label: "ruleView.contextmenu.copy", accesskey: "ruleView.contextmenu.copy.accessKey", command: this._onCopy }); this.menuitemCopyColor = createMenuItem(this._contextmenu, { label: "ruleView.contextmenu.copyColor", accesskey: "ruleView.contextmenu.copyColor.accessKey", command: this._onCopyColor }); - this.menuitemSources= createMenuItem(this._contextmenu, { + this.menuitemSources = createMenuItem(this._contextmenu, { label: "ruleView.contextmenu.showOrigSources", accesskey: "ruleView.contextmenu.showOrigSources.accessKey", command: this._onToggleOrigSources }); let popupset = doc.documentElement.querySelector("popupset"); if (!popupset) { popupset = doc.createElementNS(XUL_NS, "popupset"); @@ -1349,16 +1355,53 @@ CssRuleView.prototype = { /** * Toggle the original sources pref. */ _onToggleOrigSources: function() { let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); }, + /** + * Add a new rule to the current element. + */ + _onAddRule: function() { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let rules = elementStyle.rules; + let client = this.inspector.toolbox._target.client; + + if (!client.traits.addNewRule) { + return; + } + + this.pageStyle.addNewRule(element).then(options => { + let newRule = new Rule(elementStyle, options); + rules.push(newRule); + let editor = new RuleEditor(this, newRule); + + // Insert the new rule editor after the inline element rule + if (rules.length <= 1) { + this.element.appendChild(editor.element); + } else { + for (let rule of rules) { + if (rule.selectorText === "element") { + let referenceElement = rule.editor.element.nextSibling; + this.element.insertBefore(editor.element, referenceElement); + break; + } + } + } + + // Focus and make the new rule's selector editable + editor.selectorText.click(); + elementStyle._changed(); + }); + }, + setPageStyle: function(aPageStyle) { this.pageStyle = aPageStyle; }, /** * Return {bool} true if the rule view currently has an input editor visible. */ get isEditing() { @@ -1847,16 +1890,19 @@ RuleEditor.prototype = { // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37 if (sourceHref === "about:PreferenceStyleSheet") { sourceLabel.parentNode.setAttribute("unselectable", "true"); sourceLabel.setAttribute("value", uaLabel); sourceLabel.removeAttribute("tooltiptext"); } } else { sourceLabel.setAttribute("value", this.rule.title); + if (this.rule.ruleLine == -1 && this.rule.domRule.parentStyleSheet) { + sourceLabel.parentNode.setAttribute("unselectable", "true"); + } } let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); if (showOrig && !this.rule.isSystem && this.rule.domRule.type != ELEMENT_STYLE) { this.rule.getOriginalSourceStrings().then((strings) => { sourceLabel.setAttribute("value", strings.short); sourceLabel.setAttribute("tooltiptext", strings.full); }, console.error);
--- a/browser/devtools/styleinspector/test/browser.ini +++ b/browser/devtools/styleinspector/test/browser.ini @@ -37,16 +37,19 @@ support-files = [browser_computedview_select-and-copy-styles.js] [browser_computedview_style-editor-link.js] [browser_ruleview_add-property-and-reselect.js] [browser_ruleview_add-property-cancel_01.js] [browser_ruleview_add-property-cancel_02.js] [browser_ruleview_add-property-cancel_03.js] [browser_ruleview_add-property_01.js] [browser_ruleview_add-property_02.js] +[browser_ruleview_add-rule_01.js] +[browser_ruleview_add-rule_02.js] +[browser_ruleview_add-rule_03.js] [browser_ruleview_colorpicker-and-image-tooltip_01.js] [browser_ruleview_colorpicker-and-image-tooltip_02.js] [browser_ruleview_colorpicker-appears-on-swatch-click.js] [browser_ruleview_colorpicker-commit-on-ENTER.js] [browser_ruleview_colorpicker-edit-gradient.js] [browser_ruleview_colorpicker-hides-on-tooltip.js] [browser_ruleview_colorpicker-multiple-changes.js] [browser_ruleview_colorpicker-revert-on-ESC.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js @@ -0,0 +1,89 @@ +/* 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/ */ + +"use strict"; + +// Tests the behaviour of adding a new rule to the rule view and the +// various inplace-editor behaviours in the new rule editor + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' .testclass {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid" class="testclass">Styled Node</div>', + '<span class="testclass2">This is a span</span>', + '<p>Empty<p>' +].join("\n"); + +const TEST_DATA = [ + { node: "#testid", expected: "#testid" }, + { node: ".testclass2", expected: ".testclass2" }, + { node: "p", expected: "p" } +]; + +let test = asyncTest(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Iterating over the test data"); + for (let data of TEST_DATA) { + yield runTestData(inspector, view, data); + } +}); + +function* runTestData(inspector, view, data) { + let {node, expected} = data; + info("Selecting the test element"); + yield selectNode(node, inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + yield testNewRule(view, expected, 1); + + info("Resetting page content"); + content.document.body.innerHTML = PAGE_CONTENT; +} + +function* testNewRule(view, expected, index) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expected, + "Selector editor value is as expected: " + expected); + + info("Entering the escape key"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(idRuleEditor.selectorText.textContent, expected, + "Selector text value is as expected: " + expected); + + info("Adding new properties to new rule: " + expected) + idRuleEditor.addProperty("font-weight", "bold", ""); + let textProps = idRuleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js @@ -0,0 +1,78 @@ +/* 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/ */ + +"use strict"; + +// Tests the behaviour of adding a new rule to the rule view and editing +// its selector + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid">Styled Node</div>', + '<span>This is a span</span>' +].join("\n"); + +let test = asyncTest(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("span", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector field"); + let idRuleEditor = getRuleViewRuleEditor(view, 1); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + + info("Entering a new selector name and committing"); + editor.value = name; + + info("Waiting for rule view to refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js @@ -0,0 +1,114 @@ +/* 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/ */ + +"use strict"; + +// Tests the behaviour of adding a new rule to the rule view, adding a new +// property and editing the selector + +let PAGE_CONTENT = [ + '<style type="text/css">', + ' #testid {', + ' text-align: center;', + ' }', + '</style>', + '<div id="testid">Styled Node</div>', + '<span>This is a span</span>' +].join("\n"); + +let test = asyncTest(function*() { + yield addTab("data:text/html;charset=utf-8,test rule view add rule"); + + info("Creating the test document"); + content.document.body.innerHTML = PAGE_CONTENT; + + info("Opening the rule-view"); + let {toolbox, inspector, view} = yield openRuleView(); + + info("Selecting the test element"); + yield selectNode("#testid", inspector); + + info("Waiting for context menu to be shown"); + let onPopup = once(view._contextmenu, "popupshown"); + let win = view.doc.defaultView; + + EventUtils.synthesizeMouseAtCenter(view.element, + {button: 2, type: "contextmenu"}, win); + yield onPopup; + + ok(!view.menuitemAddRule.hidden, "Add rule is visible"); + + info("Waiting for rule view to change"); + let onRuleViewChanged = once(view.element, "CssRuleViewChanged"); + + info("Adding the new rule"); + view.menuitemAddRule.click(); + yield onRuleViewChanged; + view._contextmenu.hidePopup(); + + info("Adding new properties to the new rule"); + yield testNewRule(view, "#testid", 1); + + info("Editing existing selector field"); + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("span", inspector); + + info("Check new rule and property exist in the modified element"); + yield checkModifiedElement(view, "span", 1); +}); + +function* testNewRule(view, expected, index) { + let idRuleEditor = getRuleViewRuleEditor(view, index); + let editor = idRuleEditor.selectorText.ownerDocument.activeElement; + is(editor.value, expected, + "Selector editor value is as expected: " + expected); + + info("Entering the escape key"); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + + is(idRuleEditor.selectorText.textContent, expected, + "Selector text value is as expected: " + expected); + + info("Adding new properties to new rule: " + expected) + idRuleEditor.addProperty("font-weight", "bold", ""); + let textProps = idRuleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +} + +function* testEditSelector(view, name) { + let idRuleEditor = getRuleViewRuleEditor(view, 1); + + info("Focusing an existing selector name in the rule-view"); + let editor = yield focusEditableField(idRuleEditor.selectorText); + + is(inplaceEditor(idRuleEditor.selectorText), editor, + "The selector editor got focused"); + + info("Entering a new selector name: " + name); + editor.input.value = name; + + info("Waiting for rule view to refresh"); + let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed"); + + info("Entering the commit key"); + EventUtils.synthesizeKey("VK_RETURN", {}); + yield onRuleViewRefresh; + + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); +} + +function* checkModifiedElement(view, name, index) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); + + let idRuleEditor = getRuleViewRuleEditor(view, index); + let textProps = idRuleEditor.rule.textProps; + let lastRule = textProps[textProps.length - 1]; + is(lastRule.name, "font-weight", "Last rule name is font-weight"); + is(lastRule.value, "bold", "Last rule value is bold"); +}
--- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -119,17 +119,20 @@ RootActor.prototype = { storageInspector: true, // Whether storage inspector is read only storageInspectorReadOnly: true, // Whether conditional breakpoints are supported conditionalBreakpoints: true, bulk: true, // Whether the style rule actor implements the modifySelector method // that modifies the rule's selector - selectorEditable: true + selectorEditable: true, + // Whether the page style actor implements the addNewRule method that + // adds new rules to the page + addNewRule: true }, /** * Return a 'hello' packet as specified by the Remote Debugging Protocol. */ sayHello: function() { return { from: this.actorID,
--- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -24,16 +24,19 @@ 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"); +// Predeclare the domstylerule actor type +types.addActorType("domstylerule"); + /** * DOM Nodes returned by the style actor will be owned by the DOM walker * for the connection. */ types.addLifetime("walker", "walker"); /** * When asking for the styles applied to a node, we return a list of @@ -47,16 +50,22 @@ types.addDictType("appliedstyle", { types.addDictType("matchedselector", { rule: "domstylerule#actorid", selector: "string", value: "string", status: "number" }); +types.addDictType("appliedStylesReturn", { + entries: "array:appliedstyle", + rules: "array:domstylerule", + sheets: "array:stylesheet" +}); + /** * The PageStyle actor lets the client look at the styles on a page, as * they are applied to a given node. */ var PageStyleActor = protocol.ActorClass({ typeName: "pagestyle", /** @@ -74,16 +83,19 @@ var PageStyleActor = protocol.ActorClass throw Error("The inspector's WalkerActor must be created before " + "creating a PageStyleActor."); } this.walker = inspector.walker; this.cssLogic = new CssLogic; // Stores the association of DOM objects -> actors this.refMap = new Map; + + this.onFrameUnload = this.onFrameUnload.bind(this); + events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload); }, get conn() this.inspector.conn, /** * Return or create a StyleRuleActor for the given item. * @param item Either a CSSStyleRule or a DOM element. */ @@ -274,80 +286,37 @@ var PageStyleActor = protocol.ActorClass result += ".style" } return result; }, /** * Get the set of styles that apply to a given node. * @param NodeActor node - * @param string property * @param object options * `filter`: A string filter that affects the "matched" handling. * 'user': Include properties from user style sheets. * 'ua': Include properties from user and user-agent sheets. * Default value is 'ua' * `inherited`: Include styles inherited from parent nodes. * `matchedSeletors`: Include an array of specific selectors that * caused this rule to match its node. */ getApplied: method(function(node, options) { let entries = []; - this.addElementRules(node.rawNode, undefined, options, entries); - - if (options.inherited) { - let parent = this.walker.parentNode(node); - while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) { - this.addElementRules(parent.rawNode, parent, options, entries); - parent = this.walker.parentNode(parent); - } - } - - if (options.matchedSelectors) { - for (let entry of entries) { - if (entry.rule.type === ELEMENT_STYLE) { - continue; - } - - let domRule = entry.rule.rawRule; - let selectors = CssLogic.getSelectors(domRule); - let element = entry.inherited ? entry.inherited.rawNode : node.rawNode; - entry.matchedSelectors = []; - for (let i = 0; i < selectors.length; i++) { - if (DOMUtils.selectorMatchesElement(element, domRule, i)) { - entry.matchedSelectors.push(selectors[i]); - } - } - - } - } - - let rules = new Set; - let sheets = new Set; - entries.forEach(entry => rules.add(entry.rule)); - this.expandSets(rules, sheets); - - return { - entries: entries, - rules: [...rules], - sheets: [...sheets] - } + return this.getAppliedProps(node, entries, options); }, { request: { node: Arg(0, "domnode"), inherited: Option(1, "boolean"), matchedSelectors: Option(1, "boolean"), filter: Option(1, "string") }, - response: RetVal(types.addDictType("appliedStylesReturn", { - entries: "array:appliedstyle", - rules: "array:domstylerule", - sheets: "array:stylesheet" - })) + response: RetVal("appliedStylesReturn") }), _hasInheritedProps: function(style) { return Array.prototype.some.call(style, prop => { return DOMUtils.isInheritedProperty(prop); }); }, @@ -410,16 +379,76 @@ var PageStyleActor = protocol.ActorClass isSystem: isSystem }); } } }, /** + * Helper function for getApplied and addNewRule that fetches a set of + * style properties that apply to the given node and associated rules + * @param NodeActor node + * @param object options + * `filter`: A string filter that affects the "matched" handling. + * 'user': Include properties from user style sheets. + * 'ua': Include properties from user and user-agent sheets. + * Default value is 'ua' + * `inherited`: Include styles inherited from parent nodes. + * `matchedSeletors`: Include an array of specific selectors that + * caused this rule to match its node. + * @param array entries + * List of appliedstyle objects that lists the rules that apply to the + * node. If adding a new rule to the stylesheet, only the new rule entry + * is provided and only the style properties that apply to the new + * rule is fetched. + * @returns Object containing the list of rule entries, rule actors and + * stylesheet actors that applies to the given node and its associated + * rules. + */ + getAppliedProps: function(node, entries, options) { + if (options.inherited) { + let parent = this.walker.parentNode(node); + while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) { + this.addElementRules(parent.rawNode, parent, options, entries); + parent = this.walker.parentNode(parent); + } + } + + if (options.matchedSelectors) { + for (let entry of entries) { + if (entry.rule.type === ELEMENT_STYLE) { + continue; + } + + let domRule = entry.rule.rawRule; + let selectors = CssLogic.getSelectors(domRule); + let element = entry.inherited ? entry.inherited.rawNode : node.rawNode; + entry.matchedSelectors = []; + for (let i = 0; i < selectors.length; i++) { + if (DOMUtils.selectorMatchesElement(element, domRule, i)) { + entry.matchedSelectors.push(selectors[i]); + } + } + } + } + + let rules = new Set; + let sheets = new Set; + entries.forEach(entry => rules.add(entry.rule)); + this.expandSets(rules, sheets); + + return { + entries: entries, + rules: [...rules], + sheets: [...sheets] + } + }, + + /** * Expand Sets of rules and sheets to include all parent rules and sheets. */ expandSets: function(ruleSet, sheetSet) { // Sets include new items in their iteration for (let rule of ruleSet) { if (rule.rawRule.parentRule) { let parent = this._styleRef(rule.rawRule.parentRule); if (!ruleSet.has(parent)) { @@ -511,16 +540,69 @@ var PageStyleActor = protocol.ActorClass if (selectors && selectors.length > 0 && selectors[0].value == "auto") { margins[prop] = "auto"; } } return margins; }, + /** + * On page navigation, tidy up remaining objects. + */ + onFrameUnload: function() { + this._styleElement = null; + }, + + /** + * Helper function to addNewRule to construct a new style tag in the document. + * @returns DOMElement of the style tag + */ + get styleElement() { + if (!this._styleElement) { + let document = this.inspector.window.document; + let style = document.createElement("style"); + style.setAttribute("type", "text/css"); + document.head.appendChild(style); + this._styleElement = style; + } + + return this._styleElement; + }, + + /** + * Adds a new rule, and returns the new StyleRuleActor. + * @param NodeActor node + * @returns StyleRuleActor of the new rule + */ + addNewRule: method(function(node) { + let style = this.styleElement; + let sheet = style.sheet; + let rawNode = node.rawNode; + + let selector; + if (rawNode.id) { + selector = "#" + rawNode.id; + } else if (rawNode.className) { + selector = "." + rawNode.className; + } else { + selector = rawNode.tagName.toLowerCase(); + } + + let index = sheet.insertRule(selector + " {}", sheet.cssRules.length); + let ruleActor = this._styleRef(sheet.cssRules[index]); + return this.getAppliedProps(node, [{ rule: ruleActor }], + { matchedSelectors: true }); + }, { + request: { + node: Arg(0, "domnode") + }, + response: RetVal("appliedStylesReturn") + }), + }); exports.PageStyleActor = PageStyleActor; /** * Front object for the PageStyleActor */ var PageStyleFront = protocol.FrontClass(PageStyleActor, { initialize: function(conn, form, ctx, detail) { @@ -545,22 +627,27 @@ var PageStyleFront = protocol.FrontClass }), getApplied: protocol.custom(function(node, options={}) { return this._getApplied(node, options).then(ret => { return ret.entries; }); }, { impl: "_getApplied" + }), + + addNewRule: protocol.custom(function(node) { + return this._addNewRule(node).then(ret => { + return ret.entries[0]; + }); + }, { + impl: "_addNewRule" }) }); -// Predeclare the domstylerule actor type -types.addActorType("domstylerule"); - /** * An actor that represents a CSS style object on the protocol. * * We slightly flatten the CSSOM for this actor, it represents * both the CSSRule and CSSStyle objects in one actor. For nodes * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor * with a special rule type (100). */ @@ -736,30 +823,32 @@ var StyleRuleActor = protocol.ActorClass } catch (e) { return false; } // Check if the selector is valid and not the same as the original // selector if (selectorElement && rule.selectorText !== value) { let cssRules = parentStyleSheet.cssRules; + let cssText = rule.cssText; + let selectorText = rule.selectorText; // Delete the currently selected rule let i = 0; for (let cssRule of cssRules) { if (rule === cssRule) { parentStyleSheet.deleteRule(i); break; } i++; } // Inserts the new style rule into the current style sheet - let ruleText = rule.cssText.slice(rule.selectorText.length).trim(); + let ruleText = cssText.slice(selectorText.length).trim(); parentStyleSheet.insertRule(value + " " + ruleText, i); return true; } else { return false; } }, { request: { selector: Arg(0, "string") },
--- a/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties +++ b/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties @@ -99,16 +99,24 @@ ruleView.contextmenu.showOrigSources.acc # LOCALIZATION NOTE (ruleView.contextmenu.showCSSSources): Text displayed in the rule view # context menu. ruleView.contextmenu.showCSSSources=Show CSS sources # LOCALIZATION NOTE (ruleView.contextmenu.showCSSSources.accessKey): Access key for # the rule view context menu "Show CSS sources" entry. ruleView.contextmenu.showCSSSources.accessKey=S +# LOCALIZATION NOTE (ruleView.contextmenu.addRule): Text displayed in the +# rule view context menu for adding a new rule to the element. +ruleView.contextmenu.addRule=Add rule + +# LOCALIZATION NOTE (ruleView.contextmenu.addRule.accessKey): Access key for +# the rule view context menu "Add rule" entry. +ruleView.contextmenu.addRule.accessKey=R + # LOCALIZATION NOTE (computedView.contextmenu.selectAll): Text displayed in the # computed view context menu. computedView.contextmenu.selectAll=Select all # LOCALIZATION NOTE (computedView.contextmenu.selectAll.accessKey): Access key for # the computed view context menu "Select all" entry. computedView.contextmenu.selectAll.accessKey=A