diff --git a/browser/devtools/shared/inplace-editor.js b/browser/devtools/shared/inplace-editor.js index 670af93b2ad..a583d404b8d 100644 --- a/browser/devtools/shared/inplace-editor.js +++ b/browser/devtools/shared/inplace-editor.js @@ -73,6 +73,12 @@ Cu.import("resource://gre/modules/devtools/event-emitter.js"); * {boolean} stopOnReturn: * If true, the return key will not advance the editor to the next * focusable element. + * {boolean} stopOnTab: + * If true, the tab key will not advance the editor to the next + * focusable element. + * {boolean} stopOnShiftTab: + * If true, shift tab will not advance the editor to the previous + * focusable element. * {string} trigger: The DOM event that should trigger editing, * defaults to "click" */ @@ -171,6 +177,8 @@ function InplaceEditor(aOptions, aEvent) this.destroy = aOptions.destroy; this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent; this.multiline = aOptions.multiline || false; + this.stopOnShiftTab = !!aOptions.stopOnShiftTab; + this.stopOnTab = !!aOptions.stopOnTab; this.stopOnReturn = !!aOptions.stopOnReturn; this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT; this.property = aOptions.property; @@ -899,9 +907,15 @@ InplaceEditor.prototype = { let direction = FOCUS_FORWARD; if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB && aEvent.shiftKey) { - direction = FOCUS_BACKWARD; + if (this.stopOnShiftTab) { + direction = null; + } else { + direction = FOCUS_BACKWARD; + } } - if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { + if ((this.stopOnReturn && + aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) || + (this.stopOnTab && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB)) { direction = null; } diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js index ae11bb2175b..ebc87a85e11 100644 --- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -1692,14 +1692,23 @@ function RuleEditor(aRuleView, aRule) { this.doc = this.ruleView.doc; this.rule = aRule; this.isEditable = !aRule.isSystem; + // Flag that blocks updates of the selector and properties when it is + // being edited + this.isEditing = false; this._onNewProperty = this._onNewProperty.bind(this); this._newPropertyDestroy = this._newPropertyDestroy.bind(this); + this._onSelectorDone = this._onSelectorDone.bind(this); this._create(); } RuleEditor.prototype = { + get isSelectorEditable() { + let toolbox = this.ruleView.inspector.toolbox; + return toolbox.target.client.traits.selectorEditable; + }, + _create: function() { this.element = this.doc.createElementNS(HTML_NS, "div"); this.element.className = "ruleview-rule theme-separator"; @@ -1741,10 +1750,30 @@ RuleEditor.prototype = { let header = createChild(code, "div", {}); - this.selectorText = createChild(header, "span", { + this.selectorContainer = createChild(header, "span", { + class: "ruleview-selectorcontainer" + }); + + this.selectorText = createChild(this.selectorContainer, "span", { class: "ruleview-selector theme-fg-color3" }); + if (this.isEditable && this.rule.domRule.type !== ELEMENT_STYLE && + this.isSelectorEditable) { + this.selectorContainer.addEventListener("click", aEvent => { + // Clicks within the selector shouldn't propagate any further. + aEvent.stopPropagation(); + }, false); + + editableField({ + element: this.selectorText, + done: this._onSelectorDone, + stopOnShiftTab: true, + stopOnTab: true, + stopOnReturn: true + }); + } + this.openBrace = createChild(header, "span", { class: "ruleview-ruleopen", textContent: " {" @@ -2019,6 +2048,36 @@ RuleEditor.prototype = { if (this.multipleAddedProperties && this.multipleAddedProperties.length) { this.addProperties(this.multipleAddedProperties); } + }, + + /** + * Called when the selector's inplace editor is closed. + * Ignores the change if the user pressed escape, otherwise + * commits it. + * + * @param {string} aValue + * The value contained in the editor. + * @param {boolean} aCommit + * True if the change should be applied. + */ + _onSelectorDone: function(aValue, aCommit) { + if (!aCommit || this.isEditing || aValue === "" || + aValue === this.rule.selectorText) { + return; + } + + this.isEditing = true; + + this.rule.domRule.modifySelector(aValue).then(isModified => { + this.isEditing = false; + + if (isModified) { + this.ruleView.refreshPanel(); + } + }).then(null, err => { + this.isEditing = false; + promiseWarn(err); + }); } }; @@ -2423,7 +2482,7 @@ TextPropertyEditor.prototype = { * True if the change should be applied. */ _onNameDone: function(aValue, aCommit) { - if (aCommit) { + if (aCommit && !this.ruleEditor.isEditing) { // Unlike the value editor, if a name is empty the entire property // should always be removed. if (aValue.trim() === "") { @@ -2472,7 +2531,7 @@ TextPropertyEditor.prototype = { * True if the change should be applied. */ _onValueDone: function(aValue, aCommit) { - if (!aCommit) { + if (!aCommit && !this.ruleEditor.isEditing) { // A new property should be removed when escape is pressed. if (this.removeOnRevert) { this.remove(); @@ -2568,8 +2627,9 @@ TextPropertyEditor.prototype = { * @param {string} aValue The value to set the current property to. */ _previewValue: function(aValue) { - // Since function call is throttled, we need to make sure we are still editing - if (!this.editing) { + // Since function call is throttled, we need to make sure we are still + // editing, and any selector modifications have been completed + if (!this.editing || this.ruleEditor.isEditing) { return; } diff --git a/browser/devtools/styleinspector/ruleview.css b/browser/devtools/styleinspector/ruleview.css index e963cc3e019..a1ef3b148aa 100644 --- a/browser/devtools/styleinspector/ruleview.css +++ b/browser/devtools/styleinspector/ruleview.css @@ -19,7 +19,8 @@ pointer-events: none; } -.ruleview-namecontainer { +.ruleview-namecontainer, +.ruleview-selectorcontainer { cursor: text; } diff --git a/browser/devtools/styleinspector/test/browser.ini b/browser/devtools/styleinspector/test/browser.ini index 870b151c1a7..4c750e76317 100644 --- a/browser/devtools/styleinspector/test/browser.ini +++ b/browser/devtools/styleinspector/test/browser.ini @@ -62,6 +62,9 @@ support-files = [browser_ruleview_edit-property-order.js] [browser_ruleview_edit-property_01.js] [browser_ruleview_edit-property_02.js] +[browser_ruleview_edit-selector-commit.js] +[browser_ruleview_edit-selector_01.js] +[browser_ruleview_edit-selector_02.js] [browser_ruleview_eyedropper.js] skip-if = os == "win" && debug # bug 963492 [browser_ruleview_inherit.js] diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js index 267131911bc..59328865155 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_01.js @@ -37,7 +37,7 @@ let test = asyncTest(function*() { function* testCancelNew(view) { info("Test adding a new rule to the element's style declaration and leaving it empty."); - let elementRuleEditor = view.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(view, 0); info("Focusing a new property name in the rule-view"); let editor = yield focusEditableField(elementRuleEditor.closeBrace); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js index d19a85217ca..588ca549e7b 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_02.js @@ -39,7 +39,7 @@ let test = asyncTest(function*() { function* testCreateNewEscape(view) { info("Test creating a new property and escaping"); - let elementRuleEditor = view.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(view, 0); info("Focusing a new property name in the rule-view"); let editor = yield focusEditableField(elementRuleEditor.closeBrace); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js index 5947d3d3562..9b44e9e6e66 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property-cancel_03.js @@ -31,7 +31,7 @@ function* testCancelNew(inspector, ruleView) { // Start at the beginning: start to add a rule to the element's style // declaration, but leave it empty. - let elementRuleEditor = ruleView.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); let editor = yield focusEditableField(elementRuleEditor.closeBrace); is(inplaceEditor(elementRuleEditor.newPropSpan), editor, @@ -50,7 +50,7 @@ function* testCancelNewOnEscape(inspector, ruleView) { // Start at the beginning: start to add a rule to the element's style // declaration, add some text, then press escape. - let elementRuleEditor = ruleView.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); let editor = yield focusEditableField(elementRuleEditor.closeBrace); is(inplaceEditor(elementRuleEditor.newPropSpan), editor, "Next focused editor should be the new property editor."); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-property_01.js b/browser/devtools/styleinspector/test/browser_ruleview_add-property_01.js index 7f88597bc7c..0e341c193b2 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-property_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property_01.js @@ -39,7 +39,7 @@ let test = asyncTest(function*() { function* testCreateNew(view) { info("Test creating a new property"); - let elementRuleEditor = view.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(view, 0); info("Focusing a new property name in the rule-view"); let editor = yield focusEditableField(elementRuleEditor.closeBrace); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-property_02.js b/browser/devtools/styleinspector/test/browser_ruleview_add-property_02.js index 3e4ec9c21ff..478d4d2dd89 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-property_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-property_02.js @@ -29,7 +29,7 @@ let test = asyncTest(function*() { function* testCreateNew(inspector, ruleView) { // Create a new property. - let elementRuleEditor = ruleView.element.children[0]._ruleEditor; + let elementRuleEditor = getRuleViewRuleEditor(ruleView, 0); let editor = yield focusEditableField(elementRuleEditor.closeBrace); is(inplaceEditor(elementRuleEditor.newPropSpan), editor, diff --git a/browser/devtools/styleinspector/test/browser_ruleview_content_01.js b/browser/devtools/styleinspector/test/browser_ruleview_content_01.js index 637bf1c69f5..13fc6b77acb 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_content_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_content_01.js @@ -35,7 +35,7 @@ function* testContentAfterNodeSelection(inspector, ruleView) { "After highlighting null, has a no-results element again."); yield selectNode("#testid", inspector); - let classEditor = ruleView.element.children[2]._ruleEditor; + let classEditor = getRuleViewRuleEditor(ruleView, 2); is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent, ".testclass", ".textclass should be matched."); is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent, diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js index 4b9bfb2c577..ee76293758c 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-property-commit.js @@ -56,14 +56,14 @@ function createDocument() { } function* runTestData(view, {value, commitKey, modifiers, expected}) { - let idRuleEditor = view.element.children[1]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 1); let propEditor = idRuleEditor.rule.textProps[0].editor; info("Focusing the inplace editor field"); let editor = yield focusEditableField(propEditor.valueSpan); is(inplaceEditor(propEditor.valueSpan), editor, "Focused editor should be the value span."); - info("Entering test data " + value) + info("Entering test data " + value); for (let ch of value) { EventUtils.sendChar(ch, view.doc.defaultView); } diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js index 491de7a4c64..19798fb7bcb 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-property-increments.js @@ -34,7 +34,7 @@ function createDocument() { function* testMarginIncrements(view) { info("Testing keyboard increments on the margin property"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let marginPropEditor = idRuleEditor.rule.textProps[0].editor; yield runIncrementTest(marginPropEditor, view, { @@ -52,7 +52,7 @@ function* testMarginIncrements(view) { function* testVariousUnitIncrements(view) { info("Testing keyboard increments on values with various units"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; yield runIncrementTest(paddingPropEditor, view, { @@ -71,7 +71,7 @@ function* testVariousUnitIncrements(view) { function* testHexIncrements(view) { info("Testing keyboard increments with hex colors"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let hexColorPropEditor = idRuleEditor.rule.textProps[2].editor; yield runIncrementTest(hexColorPropEditor, view, { @@ -87,7 +87,7 @@ function* testHexIncrements(view) { function* testRgbIncrements(view) { info("Testing keyboard increments with rgb colors"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let rgbColorPropEditor = idRuleEditor.rule.textProps[3].editor; yield runIncrementTest(rgbColorPropEditor, view, { @@ -103,7 +103,7 @@ function* testRgbIncrements(view) { function* testShorthandIncrements(view) { info("Testing keyboard increments within shorthand values"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let paddingPropEditor = idRuleEditor.rule.textProps[1].editor; yield runIncrementTest(paddingPropEditor, view, { @@ -119,7 +119,7 @@ function* testShorthandIncrements(view) { function* testOddCases(view) { info("Testing some more odd cases"); - let idRuleEditor = view.element.children[0]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 0); let marginPropEditor = idRuleEditor.rule.textProps[0].editor; yield runIncrementTest(marginPropEditor, view, { diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-property_01.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-property_01.js index 6feef57957b..a205df27f7b 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-property_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-property_01.js @@ -40,7 +40,7 @@ let test = asyncTest(function*() { function* testEditProperty(view, name, value) { info("Test editing existing property name/value fields"); - let idRuleEditor = view.element.children[1]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(view, 1); let propEditor = idRuleEditor.rule.textProps[0].editor; info("Focusing an existing property name in the rule-view"); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-property_02.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-property_02.js index 995b8f2065d..89e45529b9d 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-property_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-property_02.js @@ -29,7 +29,7 @@ let test = asyncTest(function*() { }); function* testEditProperty(inspector, ruleView) { - let idRuleEditor = ruleView.element.children[1]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(ruleView, 1); let propEditor = idRuleEditor.rule.textProps[0].editor; let editor = yield focusEditableField(propEditor.nameSpan); @@ -77,7 +77,7 @@ function* testEditProperty(inspector, ruleView) { } function* testDisableProperty(inspector, ruleView) { - let idRuleEditor = ruleView.element.children[1]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(ruleView, 1); let propEditor = idRuleEditor.rule.textProps[0].editor; info("Disabling a property"); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js new file mode 100644 index 00000000000..3362a251c75 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector-commit.js @@ -0,0 +1,98 @@ +/* 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"; + +// Test selector value is correctly displayed when committing the inplace editor +// with ENTER, ESC, SHIFT+TAB and TAB + +let PAGE_CONTENT = [ + '', + '
Styled Node
', +].join("\n"); + +const TEST_DATA = [ + { + node: "#testid", + value: ".testclass", + commitKey: "VK_ESCAPE", + modifiers: {}, + expected: "#testid" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_RETURN", + modifiers: {}, + expected: ".testclass" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_TAB", + modifiers: {}, + expected: ".testclass" + }, + { + node: "#testid", + value: ".testclass", + commitKey: "VK_TAB", + modifiers: {shiftKey: true}, + expected: ".testclass" + } +]; + +let test = asyncTest(function*() { + yield addTab("data:text/html,test escaping selector change reverts back to original value"); + + 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, value, commitKey, modifiers, expected} = data; + + info("Updating " + node + " to " + value + " and committing with " + commitKey + ". Expecting: " + expected); + + info("Selecting the test element"); + yield selectNode(node, inspector); + + 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("Enter the new selector value: " + value); + editor.input.value = value; + + info("Entering the commit key " + commitKey + " " + modifiers); + EventUtils.synthesizeKey(commitKey, modifiers); + + if (commitKey === "VK_ESCAPE") { + is(idRuleEditor.rule.selectorText, expected, + "Value is as expected: " + expected); + is(idRuleEditor.isEditing, false, "Selector is not being edited.") + } else { + yield once(view.element, "CssRuleViewRefreshed"); + ok(getRuleViewRule(view, expected), + "Rule with " + name + " selector exists."); + } + + info("Resetting page content"); + content.document.body.innerHTML = PAGE_CONTENT; +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js new file mode 100644 index 00000000000..2234f84aa90 --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_01.js @@ -0,0 +1,66 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view + +let PAGE_CONTENT = [ + '', + '
Styled Node
', + 'This is a span' +].join("\n"); + +let test = asyncTest(function*() { + yield addTab("data:text/html,test rule view selector changes"); + + 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); + yield testEditSelector(view, "span"); + + info("Selecting the modified element"); + yield selectNode("#testid2", inspector); + yield checkModifiedElement(view, "span"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + 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 and committing"); + 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, 1, "Should have 1 rule."); + is(getRuleViewRule(view, name), undefined, + name + " selector has been removed."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js new file mode 100644 index 00000000000..c4df43e8f0f --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js @@ -0,0 +1,80 @@ +/* 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"; + +// Testing selector inplace-editor behaviors in the rule-view with pseudo +// classes and elements + +let PAGE_CONTENT = [ + '', + '
Styled Node
', + 'This is a span', + '
A
', + '
B
' +].join("\n"); + +let test = asyncTest(function*() { + yield addTab("data:text/html,test rule view selector changes"); + + 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(".testclass", inspector); + yield testEditSelector(view, "div:nth-child(2)"); + + info("Selecting the modified element"); + yield selectNode("#testid", inspector); + yield checkModifiedElement(view, "div:nth-child(2)"); + + info("Selecting the test element"); + yield selectNode("#testid3", inspector); + yield testEditSelector(view, ".testclass2:after"); + + info("Selecting the modified element"); + yield selectNode(".testclass2", inspector); + yield checkModifiedElement(view, ".testclass2:after"); +}); + +function* testEditSelector(view, name) { + info("Test editing existing selector fields"); + + 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, 1, "Should have 1 rule."); + is(getRuleViewRule(view, name), undefined, + name + " selector has been removed."); +} + +function* checkModifiedElement(view, name) { + is(view._elementStyle.rules.length, 2, "Should have 2 rules."); + ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists."); +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_livepreview.js b/browser/devtools/styleinspector/test/browser_ruleview_livepreview.js index 5e114970385..d63b1938a74 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_livepreview.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_livepreview.js @@ -40,7 +40,7 @@ let test = asyncTest(function*() { function* testLivePreviewData(data, ruleView, testElement) { - let idRuleEditor = ruleView.element.children[1]._ruleEditor; + let idRuleEditor = getRuleViewRuleEditor(ruleView, 1); let propEditor = idRuleEditor.rule.textProps[0].editor; info("Focusing the property value inplace-editor"); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js index 1edaf2caf93..17f9e837ecc 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-duplicates.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testCreateNewMultiDuplicates(inspector, ruleEditor); }); @@ -51,4 +51,4 @@ function* testCreateNewMultiDuplicates(inspector, ruleEditor) { is(ruleEditor.rule.textProps[6].value, "violet", "Should have correct property value"); yield inspector.once("inspector-updated"); -} \ No newline at end of file +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js index b920f4c2888..0deea1240e1 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-priority.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testCreateNewMultiPriority(inspector, ruleEditor); }); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js index c2c36b6af31..f0b6d8bc070 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_01.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testCreateNewMultiUnfinished(inspector, ruleEditor, view); }); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js index 7bfd429b34f..cb5b46f9620 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple-properties-unfinished_02.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testCreateNewMultiPartialUnfinished(inspector, ruleEditor, view); }); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js index b8fc576dec8..69a3933ae93 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_01.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testCreateNewMulti(inspector, ruleEditor); }); diff --git a/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js b/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js index a33c1c65f7b..93ff89b9363 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_multiple_properties_02.js @@ -17,7 +17,7 @@ let test = asyncTest(function*() { newElement.textContent = "Test Element"; content.document.body.appendChild(newElement); yield selectNode(newElement, inspector); - let ruleEditor = view.element.children[0]._ruleEditor; + let ruleEditor = getRuleViewRuleEditor(view, 0); yield testMultiValues(inspector, ruleEditor, view); }); diff --git a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js index 5c2a9c47dd4..ea3b6de3492 100644 --- a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js +++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js @@ -42,7 +42,7 @@ let test = asyncTest(function*() { ok(!hs.promises[TYPE], "And no highlighter is being initialized either"); info("Disabling the applied property"); - let classRuleEditor = rView.element.children[1]._ruleEditor; + let classRuleEditor = getRuleViewRuleEditor(rView, 1); let propEditor = classRuleEditor.rule.textProps[0].editor; propEditor.enable.click(); yield classRuleEditor.rule._applyingModifications; diff --git a/browser/devtools/styleinspector/test/head.js b/browser/devtools/styleinspector/test/head.js index 571f99843d8..d060197ade0 100644 --- a/browser/devtools/styleinspector/test/head.js +++ b/browser/devtools/styleinspector/test/head.js @@ -595,6 +595,16 @@ function getRuleViewLinkByIndex(view, index) { return links[index]; } +/** + * Get the rule editor from the rule-view given its index + * @param {CssRuleView} view The instance of the rule-view panel + * @param {Number} index The index of the link to get + * @return {DOMNode} The rule editor if any at this index + */ +function getRuleViewRuleEditor(view, index) { + return view.element.children[index]._ruleEditor; +} + /** * Click on a rule-view's close brace to focus a new property name editor * @param {RuleEditor} ruleEditor An instance of RuleEditor that will receive diff --git a/toolkit/devtools/server/actors/root.js b/toolkit/devtools/server/actors/root.js index 6885a173b33..7694be12e08 100644 --- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -121,7 +121,10 @@ RootActor.prototype = { storageInspectorReadOnly: true, // Whether conditional breakpoints are supported conditionalBreakpoints: true, - bulk: true + bulk: true, + // Whether the style rule actor implements the modifySelector method + // that modifies the rule's selector + selectorEditable: true }, /** diff --git a/toolkit/devtools/server/actors/styles.js b/toolkit/devtools/server/actors/styles.js index d293e1e0cdc..fe6e6ee84de 100644 --- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -595,6 +595,18 @@ var StyleRuleActor = protocol.ActorClass({ // to which this rule belongs. get marshallPool() this.pageStyle, + getDocument: function(sheet) { + let document; + + if (sheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + document = sheet.ownerNode; + } else { + document = sheet.ownerNode.ownerDocument; + } + + return document; + }, + toString: function() "[StyleRuleActor for " + this.rawRule + "]", form: function(detail) { @@ -677,11 +689,7 @@ var StyleRuleActor = protocol.ActorClass({ parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; } - if (parentStyleSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { - document = parentStyleSheet.ownerNode; - } else { - document = parentStyleSheet.ownerNode.ownerDocument; - } + document = this.getDocument(parentStyleSheet); } let tempElement = document.createElement("div"); @@ -700,7 +708,63 @@ var StyleRuleActor = protocol.ActorClass({ }, { request: { modifications: Arg(0, "array:json") }, response: { rule: RetVal("domstylerule") } - }) + }), + + /** + * Removes the current rule and inserts a new rule with the new selector + * into the parent style sheet. + * @param string value + * The new selector value + * @returns boolean + * Returns a boolean if the selector in the stylesheet was modified, + * and false otherwise + */ + modifySelector: method(function(value) { + if (this.type === ELEMENT_STYLE) { + return false; + } + + let rule = this.rawRule; + let parentStyleSheet = rule.parentStyleSheet; + let document = this.getDocument(parentStyleSheet); + // Extract the selector, and pseudo elements and classes + let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/); + let selectorElement; + + try { + selectorElement = document.querySelector(selector); + } 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; + + // 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(); + parentStyleSheet.insertRule(value + " " + ruleText, i); + + return true; + } else { + return false; + } + }, { + request: { selector: Arg(0, "string") }, + response: { isModified: RetVal("boolean") }, + }), }); /**