Bug 984880 - as-authored styles in the rule view; r=pbrosset,r=bgrins

This commit is contained in:
Tom Tromey 2015-10-06 09:35:31 -07:00
parent a821c5000b
commit 86b4213d48
55 changed files with 1713 additions and 323 deletions

View File

@ -1379,7 +1379,7 @@ pref("devtools.inspector.showAllAnonymousContent", false);
pref("devtools.inspector.mdnDocsTooltip.enabled", true); pref("devtools.inspector.mdnDocsTooltip.enabled", true);
// DevTools default color unit // DevTools default color unit
pref("devtools.defaultColorUnit", "hex"); pref("devtools.defaultColorUnit", "authored");
// Enable the Responsive UI tool // Enable the Responsive UI tool
pref("devtools.responsiveUI.no-reload-notification", false); pref("devtools.responsiveUI.no-reload-notification", false);

View File

@ -69,6 +69,10 @@ values from browser.dtd. -->
- inspector. This is visible in the options panel. --> - inspector. This is visible in the options panel. -->
<!ENTITY options.defaultColorUnit.accesskey "U"> <!ENTITY options.defaultColorUnit.accesskey "U">
<!-- LOCALIZATION NOTE (options.defaultColorUnit.authored): This is used in the
- 'Default color unit' dropdown list and is visible in the options panel. -->
<!ENTITY options.defaultColorUnit.authored "As Authored">
<!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the <!-- LOCALIZATION NOTE (options.defaultColorUnit.hex): This is used in the
- 'Default color unit' dropdown list and is visible in the options panel. --> - 'Default color unit' dropdown list and is visible in the options panel. -->
<!ENTITY options.defaultColorUnit.hex "Hex"> <!ENTITY options.defaultColorUnit.hex "Hex">

View File

@ -55,6 +55,7 @@
<menulist id="defaultColorUnitMenuList" <menulist id="defaultColorUnitMenuList"
data-pref="devtools.defaultColorUnit"> data-pref="devtools.defaultColorUnit">
<menupopup> <menupopup>
<menuitem label="&options.defaultColorUnit.authored;" value="authored"/>
<menuitem label="&options.defaultColorUnit.hex;" value="hex"/> <menuitem label="&options.defaultColorUnit.hex;" value="hex"/>
<menuitem label="&options.defaultColorUnit.hsl;" value="hsl"/> <menuitem label="&options.defaultColorUnit.hsl;" value="hsl"/>
<menuitem label="&options.defaultColorUnit.rgb;" value="rgb"/> <menuitem label="&options.defaultColorUnit.rgb;" value="rgb"/>

View File

@ -95,9 +95,9 @@ EditingSession.prototype = {
} }
if (property.value == "") { if (property.value == "") {
modifications.removeProperty(property.name); modifications.removeProperty(-1, property.name);
} else { } else {
modifications.setProperty(property.name, property.value, ""); modifications.setProperty(-1, property.name, property.value, "");
} }
} }
@ -113,9 +113,9 @@ EditingSession.prototype = {
for (let [property, value] of this._modifications) { for (let [property, value] of this._modifications) {
if (value != "") { if (value != "") {
modifications.setProperty(property, value, ""); modifications.setProperty(-1, property, value, "");
} else { } else {
modifications.removeProperty(property); modifications.removeProperty(-1, property);
} }
} }

View File

@ -33,12 +33,8 @@ var COLOR_TEST_CLASS = "test-class";
// property. |value| is the CSS text to use. |segments| is an array // property. |value| is the CSS text to use. |segments| is an array
// describing the expected result. If an element of |segments| is a // describing the expected result. If an element of |segments| is a
// string, it is simply appended to the expected string. Otherwise, // string, it is simply appended to the expected string. Otherwise,
// it must be an object with a |value| property and a |name| property. // it must be an object with a |value| property, which is the color
// These describe the color and are both used in the generated // name as it appears in the input.
// expected output -- |name| is the color name as it appears in the
// input (e.g., "red"); and |value| is the hash-style numeric value
// for the color, which parseCssProperty emits in some spots (e.g.,
// "#F00").
// //
// This approach is taken to reduce boilerplate and to make it simpler // This approach is taken to reduce boilerplate and to make it simpler
// to modify the test when the parseCssProperty output changes. // to modify the test when the parseCssProperty output changes.
@ -53,10 +49,10 @@ function makeColorTest(name, value, segments) {
if (typeof (segment) === "string") { if (typeof (segment) === "string") {
result.expected += segment; result.expected += segment;
} else { } else {
result.expected += "<span data-color=\"" + segment.value + "\">" + result.expected += "<span data-color=\"" + segment.name + "\">" +
"<span style=\"background-color:" + segment.name + "<span style=\"background-color:" + segment.name +
"\" class=\"" + COLOR_TEST_CLASS + "\"></span><span>" + "\" class=\"" + COLOR_TEST_CLASS + "\"></span><span>" +
segment.value + "</span></span>"; segment.name + "</span></span>";
} }
} }
@ -68,25 +64,25 @@ function makeColorTest(name, value, segments) {
function testParseCssProperty(doc, parser) { function testParseCssProperty(doc, parser) {
let tests = [ let tests = [
makeColorTest("border", "1px solid red", makeColorTest("border", "1px solid red",
["1px solid ", {name: "red", value: "#F00"}]), ["1px solid ", {name: "red"}]),
makeColorTest("background-image", makeColorTest("background-image",
"linear-gradient(to right, #F60 10%, rgba(0,0,0,1))", "linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
["linear-gradient(to right, ", {name: "#F60", value: "#F60"}, ["linear-gradient(to right, ", {name: "#F60"},
" 10%, ", {name: "rgba(0,0,0,1)", value: "#000"}, " 10%, ", {name: "rgba(0,0,0,1)"},
")"]), ")"]),
// In "arial black", "black" is a font, not a color. // In "arial black", "black" is a font, not a color.
makeColorTest("font-family", "arial black", ["arial black"]), makeColorTest("font-family", "arial black", ["arial black"]),
makeColorTest("box-shadow", "0 0 1em red", makeColorTest("box-shadow", "0 0 1em red",
["0 0 1em ", {name: "red", value: "#F00"}]), ["0 0 1em ", {name: "red"}]),
makeColorTest("box-shadow", makeColorTest("box-shadow",
"0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)", "0 0 1em red, 2px 2px 0 0 rgba(0,0,0,.5)",
["0 0 1em ", {name: "red", value: "#F00"}, ["0 0 1em ", {name: "red"},
", 2px 2px 0 0 ", ", 2px 2px 0 0 ",
{name: "rgba(0,0,0,.5)", value: "rgba(0,0,0,.5)"}]), {name: "rgba(0,0,0,.5)"}]),
makeColorTest("content", "\"red\"", ["\"red\""]), makeColorTest("content", "\"red\"", ["\"red\""]),
@ -98,7 +94,7 @@ function testParseCssProperty(doc, parser) {
["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ", ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
"url(red.svg#blue)\"><span>", "url(red.svg#blue)\"><span>",
"blur(1px) drop-shadow(0 0 0 ", "blur(1px) drop-shadow(0 0 0 ",
{name: "blue", value: "#00F"}, {name: "blue"},
") url(red.svg#blue)</span></span>"]), ") url(red.svg#blue)</span></span>"]),
makeColorTest("color", "currentColor", ["currentColor"]), makeColorTest("color", "currentColor", ["currentColor"]),

View File

@ -1145,6 +1145,8 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
// Then set spectrum's color and listen to color changes to preview them // Then set spectrum's color and listen to color changes to preview them
if (this.activeSwatch) { if (this.activeSwatch) {
this.currentSwatchColor = this.activeSwatch.nextSibling; this.currentSwatchColor = this.activeSwatch.nextSibling;
this._colorUnit =
colorUtils.classifyColor(this.currentSwatchColor.textContent);
let color = this.activeSwatch.style.backgroundColor; let color = this.activeSwatch.style.backgroundColor;
this.spectrum.then(spectrum => { this.spectrum.then(spectrum => {
spectrum.off("changed", this._onSpectrumColorChange); spectrum.off("changed", this._onSpectrumColorChange);
@ -1222,6 +1224,7 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
_toDefaultType: function(color) { _toDefaultType: function(color) {
let colorObj = new colorUtils.CssColor(color); let colorObj = new colorUtils.CssColor(color);
colorObj.colorUnit = this._colorUnit;
return colorObj.toString(); return colorObj.toString();
}, },

View File

@ -86,6 +86,12 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
this.walker = walker; this.walker = walker;
this.highlighter = highlighter; this.highlighter = highlighter;
// True when we've called update() on the style sheet.
this._isUpdating = false;
// True when we've just set the editor text based on a style-applied
// event from the StyleSheetActor.
this._justSetText = false;
this._state = { // state to use when inputElement attaches this._state = { // state to use when inputElement attaches
text: "", text: "",
selection: { selection: {
@ -103,7 +109,8 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
this._onPropertyChange = this._onPropertyChange.bind(this); this._onPropertyChange = this._onPropertyChange.bind(this);
this._onError = this._onError.bind(this); this._onError = this._onError.bind(this);
this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this); this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this);
this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this) this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this);
this._onStyleApplied = this._onStyleApplied.bind(this);
this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
this.saveToFile = this.saveToFile.bind(this); this.saveToFile = this.saveToFile.bind(this);
@ -119,6 +126,7 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
this.cssSheet.getMediaRules().then(this._onMediaRulesChanged, Cu.reportError); this.cssSheet.getMediaRules().then(this._onMediaRulesChanged, Cu.reportError);
} }
this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged); this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
this.cssSheet.on("style-applied", this._onStyleApplied);
this.savedFile = file; this.savedFile = file;
this.linkCSSFile(); this.linkCSSFile();
} }
@ -244,6 +252,27 @@ StyleSheetEditor.prototype = {
this.emit("linked-css-file"); this.emit("linked-css-file");
}, },
/**
* A helper function that fetches the source text from the style
* sheet. The text is possibly prettified using
* CssLogic.prettifyCSS. This also sets |this._state.text| to the
* new text.
*
* @return {Promise} a promise that resolves to the new text
*/
_getSourceTextAndPrettify: function() {
return this.styleSheet.getText().then((longStr) => {
return longStr.string();
}).then((source) => {
let ruleCount = this.styleSheet.ruleCount;
if (!this.styleSheet.isOriginalSource) {
source = CssLogic.prettifyCSS(source, ruleCount);
}
this._state.text = source;
return source;
});
},
/** /**
* Start fetching the full text source for this editor's sheet. * Start fetching the full text source for this editor's sheet.
* *
@ -251,19 +280,11 @@ StyleSheetEditor.prototype = {
* A promise that'll resolve with the source text once the source * A promise that'll resolve with the source text once the source
* has been loaded or reject on unexpected error. * has been loaded or reject on unexpected error.
*/ */
fetchSource: function () { fetchSource: function() {
return Task.spawn(function* () { return this._getSourceTextAndPrettify().then((source) => {
let longStr = yield this.styleSheet.getText();
let source = yield longStr.string();
let ruleCount = this.styleSheet.ruleCount;
if (!this.styleSheet.isOriginalSource) {
source = CssLogic.prettifyCSS(source, ruleCount);
}
this._state.text = source;
this.sourceLoaded = true; this.sourceLoaded = true;
return source; return source;
}.bind(this)).then(null, e => { }).then(null, e => {
if (this._isDestroyed) { if (this._isDestroyed) {
console.warn("Could not fetch the source for " + console.warn("Could not fetch the source for " +
this.styleSheet.href + this.styleSheet.href +
@ -323,6 +344,26 @@ StyleSheetEditor.prototype = {
this.emit("property-change", property, value); this.emit("property-change", property, value);
}, },
/**
* Called when the stylesheet text changes.
*/
_onStyleApplied: function() {
if (this._isUpdating) {
// We just applied an edit in the editor, so we can drop this
// notification.
this._isUpdating = false;
} else if (this.sourceEditor) {
this._getSourceTextAndPrettify().then((newText) => {
this._justSetText = true;
let firstLine = this.sourceEditor.getFirstVisibleLine();
let pos = this.sourceEditor.getCursor();
this.sourceEditor.setText(newText);
this.sourceEditor.setFirstVisibleLine(firstLine);
this.sourceEditor.setCursor(pos);
});
}
},
/** /**
* Handles changes to the list of @media rules in the stylesheet. * Handles changes to the list of @media rules in the stylesheet.
* Emits 'media-rules-changed' if the list has changed. * Emits 'media-rules-changed' if the list has changed.
@ -489,6 +530,11 @@ StyleSheetEditor.prototype = {
return; // TODO: do we want to do this? return; // TODO: do we want to do this?
} }
if (this._justSetText) {
this._justSetText = false;
return;
}
this._updateTask = null; // reset only if we actually perform an update this._updateTask = null; // reset only if we actually perform an update
// (stylesheet is enabled) so that 'missed' updates // (stylesheet is enabled) so that 'missed' updates
// while the stylesheet is disabled can be performed // while the stylesheet is disabled can be performed
@ -498,6 +544,7 @@ StyleSheetEditor.prototype = {
this._state.text = this.sourceEditor.getText(); this._state.text = this.sourceEditor.getText();
} }
this._isUpdating = true;
this.styleSheet.update(this._state.text, this.transitionsEnabled) this.styleSheet.update(this._state.text, this.transitionsEnabled)
.then(null, Cu.reportError); .then(null, Cu.reportError);
}, },
@ -726,10 +773,11 @@ StyleSheetEditor.prototype = {
} }
this.cssSheet.off("property-change", this._onPropertyChange); this.cssSheet.off("property-change", this._onPropertyChange);
this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged); this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
this.cssSheet.off("style-applied", this._onStyleApplied);
this.styleSheet.off("error", this._onError); this.styleSheet.off("error", this._onError);
this._isDestroyed = true; this._isDestroyed = true;
} }
} };
/** /**
* Find a path on disk for a file given it's hosted uri, the uri of the * Find a path on disk for a file given it's hosted uri, the uri of the

View File

@ -48,6 +48,7 @@ support-files =
doc_uncached.css doc_uncached.css
doc_uncached.html doc_uncached.html
doc_xulpage.xul doc_xulpage.xul
sync.html
[browser_styleeditor_autocomplete.js] [browser_styleeditor_autocomplete.js]
[browser_styleeditor_autocomplete-disabled.js] [browser_styleeditor_autocomplete-disabled.js]
@ -83,5 +84,10 @@ skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
[browser_styleeditor_sourcemap_large.js] [browser_styleeditor_sourcemap_large.js]
[browser_styleeditor_sourcemap_watching.js] [browser_styleeditor_sourcemap_watching.js]
skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
[browser_styleeditor_sync.js]
[browser_styleeditor_syncAddRule.js]
[browser_styleeditor_syncAlreadyOpen.js]
[browser_styleeditor_syncEditSelector.js]
[browser_styleeditor_syncIntoRuleView.js]
[browser_styleeditor_transition_rule.js] [browser_styleeditor_transition_rule.js]
[browser_styleeditor_xul.js] [browser_styleeditor_xul.js]

View File

@ -0,0 +1,74 @@
/* vim: set 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 that changes in the style inspector are synchronized into the
// style editor.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
const expectedText = `
body {
border-width: 15px;
/*! color: red; */
}
#testid {
/*! font-size: 4em; */
}
`;
function* closeAndReopenToolbox() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
let { ui: newui } = yield openStyleEditor();
return newui;
}
add_task(function*() {
yield addTab(TESTCASE_URI);
let { inspector, view } = yield openRuleView();
yield selectNode("#testid", inspector);
let ruleEditor = getRuleViewRuleEditor(view, 1);
// Disable the "font-size" property.
let propEditor = ruleEditor.rule.textProps[0].editor;
let onModification = view.once("ruleview-changed");
propEditor.enable.click();
yield onModification;
// Disable the "color" property. Note that this property is in a
// rule that also contains a non-inherited property -- so this test
// is also testing that property editing works properly in this
// situation.
ruleEditor = getRuleViewRuleEditor(view, 3);
propEditor = ruleEditor.rule.textProps[1].editor;
onModification = view.once("ruleview-changed");
propEditor.enable.click();
yield onModification;
let { ui } = yield openStyleEditor();
let editor = yield ui.editors[0].getSourceEditor();
let text = editor.sourceEditor.getText();
is(text, expectedText, "style inspector changes are synced");
// Close and reopen the toolbox, to see that the edited text remains
// available.
ui = yield closeAndReopenToolbox();
editor = yield ui.editors[0].getSourceEditor();
text = editor.sourceEditor.getText();
is(text, expectedText, "changes remain after close and reopen");
// For the time being, the actor does not update the style's owning
// node's textContent. See bug 1205380.
yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
let style = content.document.querySelector("style");
return style.textContent;
}).then((textContent) => {
isnot(textContent, expectedText, "changes not written back to style node");
});
});

View File

@ -0,0 +1,33 @@
/* vim: set 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 that adding a new rule is synced to the style editor.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
const expectedText = `
#testid {
}`;
add_task(function*() {
yield addTab(TESTCASE_URI);
let { inspector, view } = yield openRuleView();
yield selectNode("#testid", inspector);
let onRuleViewChanged = once(view, "ruleview-changed");
view.addRuleButton.click();
yield onRuleViewChanged;
let { ui } = yield openStyleEditor();
info("Selecting the second editor");
yield ui.selectStyleSheet(ui.editors[1].styleSheet);
let editor = ui.editors[1];
let text = editor.sourceEditor.getText();
is(text, expectedText, "selector edits are synced");
});

View File

@ -0,0 +1,52 @@
/* vim: set 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 that changes in the style inspector are synchronized into the
// style editor.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
const expectedText = `
body {
border-width: 15px;
color: red;
}
#testid {
/*! font-size: 4em; */
}
`;
add_task(function*() {
yield addTab(TESTCASE_URI);
let { inspector, view } = yield openRuleView();
// In this test, make sure the style editor is open before making
// changes in the inspector.
let { ui } = yield openStyleEditor();
let editor = yield ui.editors[0].getSourceEditor();
let onEditorChange = promise.defer();
editor.sourceEditor.on("change", onEditorChange.resolve);
yield openRuleView();
yield selectNode("#testid", inspector);
let ruleEditor = getRuleViewRuleEditor(view, 1);
// Disable the "font-size" property.
let propEditor = ruleEditor.rule.textProps[0].editor;
let onModification = view.once("ruleview-changed");
propEditor.enable.click();
yield onModification;
yield openStyleEditor();
yield onEditorChange.promise;
let text = editor.sourceEditor.getText();
is(text, expectedText, "style inspector changes are synced");
});

View File

@ -0,0 +1,41 @@
/* vim: set 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 that changes in the style inspector are synchronized into the
// style editor.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
const expectedText = `
body {
border-width: 15px;
color: red;
}
#testid, span {
font-size: 4em;
}
`;
add_task(function*() {
yield addTab(TESTCASE_URI);
let { inspector, view } = yield openRuleView();
yield selectNode("#testid", inspector);
let ruleEditor = getRuleViewRuleEditor(view, 1);
let editor = yield focusEditableField(view, ruleEditor.selectorText);
editor.input.value = "#testid, span";
let onRuleViewChanged = once(view, "ruleview-changed");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewChanged;
let { ui } = yield openStyleEditor();
editor = yield ui.editors[0].getSourceEditor();
let text = editor.sourceEditor.getText();
is(text, expectedText, "selector edits are synced");
});

View File

@ -0,0 +1,51 @@
/* vim: set 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 that changes in the style editor are synchronized into the
// style inspector.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleinspector/test/head.js", this);
const TEST_URI = `
<style type='text/css'>
div { background-color: seagreen; }
</style>
<div id='testid' class='testclass'>Styled Node</div>
`;
const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }";
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
let { panel, ui } = yield openStyleEditor();
let editor = yield ui.editors[0].getSourceEditor();
let waitForRuleView = view.once("ruleview-refreshed");
yield typeInEditor(editor, panel.panelWindow);
yield waitForRuleView;
let value = getRuleViewPropertyValue(view, "#testid", "color");
is(value, "chartreuse", "check that edits were synced to rule view");
});
function typeInEditor(aEditor, panelWindow) {
let deferred = promise.defer();
waitForFocus(function() {
for (let c of TESTCASE_CSS_SOURCE) {
EventUtils.synthesizeKey(c, {}, panelWindow);
}
ok(aEditor.unsaved, "new editor has unsaved flag");
deferred.resolve();
}, panelWindow);
return deferred.promise;
}

View File

@ -71,17 +71,29 @@ function* cleanup()
} }
/** /**
* Creates a new tab in specified window navigates it to the given URL and * Open the style editor for the current tab.
* opens style editor in it.
*/ */
var openStyleEditorForURL = Task.async(function* (url, win) { var openStyleEditor = Task.async(function*(tab) {
let tab = yield addTab(url, win); if (!tab) {
tab = gBrowser.selectedTab;
}
let target = TargetFactory.forTab(tab); let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "styleeditor"); let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
let panel = toolbox.getPanel("styleeditor"); let panel = toolbox.getPanel("styleeditor");
let ui = panel.UI; let ui = panel.UI;
return { tab, toolbox, panel, ui }; return { toolbox, panel, ui };
});
/**
* Creates a new tab in specified window navigates it to the given URL and
* opens style editor in it.
*/
var openStyleEditorForURL = Task.async(function* (url, win) {
let tab = yield addTab(url, win);
let result = yield openStyleEditor(tab);
result.tab = tab;
return result;
}); });
/** /**

View File

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>simple testcase</title>
<style type="text/css">
body {
border-width: 15px;
color: red;
}
#testid {
font-size: 4em;
}
</style>
</head>
<body>
<div id="testid">simple testcase</div>
</body>
</html>

View File

@ -109,7 +109,7 @@ function createDummyDocument() {
* Responsible for applying changes to the properties in a rule. * Responsible for applying changes to the properties in a rule.
* Maintains a list of TextProperty objects. * Maintains a list of TextProperty objects.
* TextProperty: * TextProperty:
* Manages a single property from the cssText attribute of the * Manages a single property from the authoredText attribute of the
* relevant declaration. * relevant declaration.
* Maintains a list of computed properties that come from this * Maintains a list of computed properties that come from this
* property declaration. * property declaration.
@ -183,6 +183,12 @@ ElementStyle.prototype = {
} }
this.destroyed = true; this.destroyed = true;
for (let rule of this.rules) {
if (rule.editor) {
rule.editor.destroy();
}
}
this.dummyElement = null; this.dummyElement = null;
this.dummyElementPromise.then(dummyElement => { this.dummyElementPromise.then(dummyElement => {
dummyElement.remove(); dummyElement.remove();
@ -226,12 +232,12 @@ ElementStyle.prototype = {
// Store the current list of rules (if any) during the population // Store the current list of rules (if any) during the population
// process. They will be reused if possible. // process. They will be reused if possible.
this._refreshRules = this.rules; let existingRules = this.rules;
this.rules = []; this.rules = [];
for (let entry of entries) { for (let entry of entries) {
this._maybeAddRule(entry); this._maybeAddRule(entry, existingRules);
} }
// Mark overridden computed styles. // Mark overridden computed styles.
@ -240,7 +246,11 @@ ElementStyle.prototype = {
this._sortRulesForPseudoElement(); this._sortRulesForPseudoElement();
// We're done with the previous list of rules. // We're done with the previous list of rules.
delete this._refreshRules; for (let r of existingRules) {
if (r && r.editor) {
r.editor.destroy();
}
}
}); });
}).then(null, e => { }).then(null, e => {
// populate is often called after a setTimeout, // populate is often called after a setTimeout,
@ -269,9 +279,12 @@ ElementStyle.prototype = {
* *
* @param {Object} options * @param {Object} options
* Options for creating the Rule, see the Rule constructor. * Options for creating the Rule, see the Rule constructor.
* @param {Array} existingRules
* Rules to reuse if possible. If a rule is reused, then it
* it will be deleted from this array.
* @return {Boolean} true if we added the rule. * @return {Boolean} true if we added the rule.
*/ */
_maybeAddRule: function(options) { _maybeAddRule: function(options, existingRules) {
// If we've already included this domRule (for example, when a // If we've already included this domRule (for example, when a
// common selector is inherited), ignore it. // common selector is inherited), ignore it.
if (options.rule && if (options.rule &&
@ -287,13 +300,12 @@ ElementStyle.prototype = {
// If we're refreshing and the rule previously existed, reuse the // If we're refreshing and the rule previously existed, reuse the
// Rule object. // Rule object.
if (this._refreshRules) { if (existingRules) {
for (let r of this._refreshRules) { let ruleIndex = existingRules.findIndex((r) => r.matches(options));
if (r.matches(options)) { if (ruleIndex >= 0) {
rule = r; rule = existingRules[ruleIndex];
rule.refresh(options); rule.refresh(options);
break; existingRules.splice(ruleIndex, 1);
}
} }
} }
@ -302,8 +314,8 @@ ElementStyle.prototype = {
rule = new Rule(this, options); rule = new Rule(this, options);
} }
// Ignore inherited rules with no properties. // Ignore inherited rules with no visible properties.
if (options.inherited && rule.textProps.length === 0) { if (options.inherited && !rule.hasAnyVisibleProperties()) {
return false; return false;
} }
@ -372,6 +384,16 @@ ElementStyle.prototype = {
let taken = {}; let taken = {};
for (let computedProp of computedProps) { for (let computedProp of computedProps) {
let earlier = taken[computedProp.name]; let earlier = taken[computedProp.name];
// Prevent -webkit-gradient from being selected after unchecking
// linear-gradient in this case:
// -moz-linear-gradient: ...;
// -webkit-linear-gradient: ...;
// linear-gradient: ...;
if (!computedProp.textProp.isValid()) {
computedProp.overridden = true;
continue;
}
let overridden; let overridden;
if (earlier && if (earlier &&
computedProp.priority === "important" && computedProp.priority === "important" &&
@ -463,7 +485,7 @@ function Rule(elementStyle, options) {
this.mediaText = this.domRule.mediaText; this.mediaText = this.domRule.mediaText;
} }
// Populate the text properties with the style's current cssText // Populate the text properties with the style's current authoredText
// value, and add in any disabled properties from the store. // value, and add in any disabled properties from the store.
this.textProps = this._getTextProperties(); this.textProps = this._getTextProperties();
this.textProps = this.textProps.concat(this._getDisabledProperties()); this.textProps = this.textProps.concat(this._getDisabledProperties());
@ -473,17 +495,12 @@ Rule.prototype = {
mediaText: "", mediaText: "",
get title() { get title() {
if (this._title) { let title = CssLogic.shortSource(this.sheet);
return this._title;
}
this._title = CssLogic.shortSource(this.sheet);
if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
this._title += ":" + this.ruleLine; title += ":" + this.ruleLine;
} }
this._title = this._title + return title + (this.mediaText ? " @media " + this.mediaText : "");
(this.mediaText ? " @media " + this.mediaText : "");
return this._title;
}, },
get inheritedSource() { get inheritedSource() {
@ -551,10 +568,6 @@ Rule.prototype = {
* both the full and short version of the source string. * both the full and short version of the source string.
*/ */
getOriginalSourceStrings: function() { getOriginalSourceStrings: function() {
if (this._originalSourceStrings) {
return promise.resolve(this._originalSourceStrings);
}
return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => { return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => {
let mediaString = mediaText ? " @" + mediaText : ""; let mediaString = mediaText ? " @" + mediaText : "";
@ -564,7 +577,6 @@ Rule.prototype = {
short: CssLogic.shortSource({href: href}) + ":" + line + mediaString short: CssLogic.shortSource({href: href}) + ":" + line + mediaString
}; };
this._originalSourceStrings = sourceStrings;
return sourceStrings; return sourceStrings;
}); });
}, },
@ -595,31 +607,35 @@ Rule.prototype = {
createProperty: function(name, value, priority, siblingProp) { createProperty: function(name, value, priority, siblingProp) {
let prop = new TextProperty(this, name, value, priority); let prop = new TextProperty(this, name, value, priority);
let ind;
if (siblingProp) { if (siblingProp) {
let ind = this.textProps.indexOf(siblingProp); ind = this.textProps.indexOf(siblingProp) + 1;
this.textProps.splice(ind + 1, 0, prop); this.textProps.splice(ind, 0, prop);
} else { } else {
ind = this.textProps.length;
this.textProps.push(prop); this.textProps.push(prop);
} }
this.applyProperties(); this.applyProperties((modifications) => {
modifications.createProperty(ind, name, value, priority);
});
return prop; return prop;
}, },
/** /**
* Reapply all the properties in this rule, and update their * Helper function for applyProperties that is called when the actor
* computed styles. Store disabled properties in the element * does not support as-authored styles. Store disabled properties
* style's store. Will re-mark overridden properties. * in the element style's store.
*/ */
applyProperties: function(modifications) { _applyPropertiesNoAuthored: function(modifications) {
this.elementStyle.markOverriddenAll(); this.elementStyle.markOverriddenAll();
if (!modifications) {
modifications = this.style.startModifyingProperties();
}
let disabledProps = []; let disabledProps = [];
for (let prop of this.textProps) { for (let prop of this.textProps) {
if (prop.invisible) {
continue;
}
if (!prop.enabled) { if (!prop.enabled) {
disabledProps.push({ disabledProps.push({
name: prop.name, name: prop.name,
@ -632,7 +648,7 @@ Rule.prototype = {
continue; continue;
} }
modifications.setProperty(prop.name, prop.value, prop.priority); modifications.setProperty(-1, prop.name, prop.value, prop.priority);
prop.updateComputed(); prop.updateComputed();
} }
@ -645,9 +661,9 @@ Rule.prototype = {
disabled.delete(this.style); disabled.delete(this.style);
} }
let modificationsPromise = modifications.apply().then(() => { return modifications.apply().then(() => {
let cssProps = {}; let cssProps = {};
for (let cssProp of parseDeclarations(this.style.cssText)) { for (let cssProp of parseDeclarations(this.style.authoredText)) {
cssProps[cssProp.name] = cssProp; cssProps[cssProp.name] = cssProp;
} }
@ -667,18 +683,67 @@ Rule.prototype = {
textProp.priority = cssProp.priority; textProp.priority = cssProp.priority;
} }
});
},
this.elementStyle.markOverriddenAll(); /**
* A helper for applyProperties that applies properties in the "as
if (modificationsPromise === this._applyingModifications) { * authored" case; that is, when the StyleRuleActor supports
this._applyingModifications = null; * setRuleText.
*/
_applyPropertiesAuthored: function(modifications) {
return modifications.apply().then(() => {
// The rewriting may have required some other property values to
// change, e.g., to insert some needed terminators. Update the
// relevant properties here.
for (let index in modifications.changedDeclarations) {
let newValue = modifications.changedDeclarations[index];
this.textProps[index].noticeNewValue(newValue);
} }
// Recompute and redisplay the computed properties.
for (let prop of this.textProps) {
if (!prop.invisible && prop.enabled) {
prop.updateComputed();
prop.updateEditor();
}
}
});
},
this.elementStyle._changed(); /**
}).then(null, promiseWarn); * Reapply all the properties in this rule, and update their
* computed styles. Will re-mark overridden properties. Sets the
* |_applyingModifications| property to a promise which will resolve
* when the edit has completed.
*
* @param {Function} modifier a function that takes a RuleModificationList
* (or RuleRewriter) as an argument and that modifies it
* to apply the desired edit
* @return {Promise} a promise which will resolve when the edit
* is complete
*/
applyProperties: function(modifier) {
// If there is already a pending modification, we have to wait
// until it settles before applying the next modification.
let resultPromise =
promise.resolve(this._applyingModifications).then(() => {
let modifications = this.style.startModifyingProperties();
modifier(modifications);
if (this.style.canSetRuleText) {
return this._applyPropertiesAuthored(modifications);
}
return this._applyPropertiesNoAuthored(modifications);
}).then(() => {
this.elementStyle.markOverriddenAll();
this._applyingModifications = modificationsPromise; if (resultPromise === this._applyingModifications) {
return modificationsPromise; this._applyingModifications = null;
this.elementStyle._changed();
}
}).catch(promiseWarn);
this._applyingModifications = resultPromise;
return resultPromise;
}, },
/** /**
@ -693,10 +758,12 @@ Rule.prototype = {
if (name === property.name) { if (name === property.name) {
return; return;
} }
let modifications = this.style.startModifyingProperties();
modifications.removeProperty(property.name);
property.name = name; property.name = name;
this.applyProperties(modifications, name); let index = this.textProps.indexOf(property);
this.applyProperties((modifications) => {
modifications.renameProperty(index, property.name, name);
});
}, },
/** /**
@ -716,7 +783,11 @@ Rule.prototype = {
property.value = value; property.value = value;
property.priority = priority; property.priority = priority;
this.applyProperties(null, property.name);
let index = this.textProps.indexOf(property);
this.applyProperties((modifications) => {
modifications.setProperty(index, property.name, value, priority);
});
}, },
/** /**
@ -732,7 +803,8 @@ Rule.prototype = {
*/ */
previewPropertyValue: function(property, value, priority) { previewPropertyValue: function(property, value, priority) {
let modifications = this.style.startModifyingProperties(); let modifications = this.style.startModifyingProperties();
modifications.setProperty(property.name, value, priority); modifications.setProperty(this.textProps.indexOf(property),
property.name, value, priority);
modifications.apply().then(() => { modifications.apply().then(() => {
// Ensure dispatching a ruleview-changed event // Ensure dispatching a ruleview-changed event
// also for previews // also for previews
@ -748,12 +820,14 @@ Rule.prototype = {
* @param {Boolean} value * @param {Boolean} value
*/ */
setPropertyEnabled: function(property, value) { setPropertyEnabled: function(property, value) {
property.enabled = !!value; if (property.enabled === !!value) {
let modifications = this.style.startModifyingProperties(); return;
if (!property.enabled) {
modifications.removeProperty(property.name);
} }
this.applyProperties(modifications); property.enabled = !!value;
let index = this.textProps.indexOf(property);
this.applyProperties((modifications) => {
modifications.setPropertyEnabled(index, property.name, property.enabled);
});
}, },
/** /**
@ -764,30 +838,35 @@ Rule.prototype = {
* The property to be removed * The property to be removed
*/ */
removeProperty: function(property) { removeProperty: function(property) {
this.textProps = this.textProps.filter(prop => prop !== property); let index = this.textProps.indexOf(property);
let modifications = this.style.startModifyingProperties(); this.textProps.splice(index, 1);
modifications.removeProperty(property.name);
// Need to re-apply properties in case removing this TextProperty // Need to re-apply properties in case removing this TextProperty
// exposes another one. // exposes another one.
this.applyProperties(modifications); this.applyProperties((modifications) => {
modifications.removeProperty(index, property.name);
});
}, },
/** /**
* Get the list of TextProperties from the style. Needs * Get the list of TextProperties from the style. Needs
* to parse the style's cssText. * to parse the style's authoredText.
*/ */
_getTextProperties: function() { _getTextProperties: function() {
let textProps = []; let textProps = [];
let store = this.elementStyle.store; let store = this.elementStyle.store;
let props = parseDeclarations(this.style.cssText); let props = parseDeclarations(this.style.authoredText, true);
for (let prop of props) { for (let prop of props) {
let name = prop.name; let name = prop.name;
if (this.inherited && !domUtils.isInheritedProperty(name)) { // In an inherited rule, we only show inherited properties.
continue; // However, we must keep all properties in order for rule
} // rewriting to work properly. So, compute the "invisible"
// property here.
let invisible = this.inherited && !domUtils.isInheritedProperty(name);
let value = store.userProperties.getProperty(this.style, name, let value = store.userProperties.getProperty(this.style, name,
prop.value); prop.value);
let textProp = new TextProperty(this, name, value, prop.priority); let textProp = new TextProperty(this, name, value, prop.priority,
!("commentOffsets" in prop),
invisible);
textProps.push(textProp); textProps.push(textProp);
} }
@ -864,7 +943,7 @@ Rule.prototype = {
/** /**
* Update the current TextProperties that match a given property * Update the current TextProperties that match a given property
* from the cssText. Will choose one existing TextProperty to update * from the authoredText. Will choose one existing TextProperty to update
* with the new property's value, and will disable all others. * with the new property's value, and will disable all others.
* *
* When choosing the best match to reuse, properties will be chosen * When choosing the best match to reuse, properties will be chosen
@ -880,7 +959,7 @@ Rule.prototype = {
* *
* @param {TextProperty} newProp * @param {TextProperty} newProp
* The current version of the property, as parsed from the * The current version of the property, as parsed from the
* cssText in Rule._getTextProperties(). * authoredText in Rule._getTextProperties().
* @return {Boolean} true if a property was updated, false if no properties * @return {Boolean} true if a property was updated, false if no properties
* were updated. * were updated.
*/ */
@ -953,18 +1032,26 @@ Rule.prototype = {
let index = this.textProps.indexOf(textProperty); let index = this.textProps.indexOf(textProperty);
if (direction === Ci.nsIFocusManager.MOVEFOCUS_FORWARD) { if (direction === Ci.nsIFocusManager.MOVEFOCUS_FORWARD) {
if (index === this.textProps.length - 1) { for (++index; index < this.textProps.length; ++index) {
if (!this.textProps[index].invisible) {
break;
}
}
if (index === this.textProps.length) {
textProperty.rule.editor.closeBrace.click(); textProperty.rule.editor.closeBrace.click();
} else { } else {
let nextProp = this.textProps[index + 1]; this.textProps[index].editor.nameSpan.click();
nextProp.editor.nameSpan.click();
} }
} else if (direction === Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) { } else if (direction === Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) {
if (index === 0) { for (--index; index >= 0; --index) {
if (!this.textProps[index].invisible) {
break;
}
}
if (index < 0) {
textProperty.editor.ruleEditor.selectorText.click(); textProperty.editor.ruleEditor.selectorText.click();
} else { } else {
let prevProp = this.textProps[index - 1]; this.textProps[index].editor.valueSpan.click();
prevProp.editor.valueSpan.click();
} }
} }
}, },
@ -978,15 +1065,31 @@ Rule.prototype = {
let terminator = osString === "WINNT" ? "\r\n" : "\n"; let terminator = osString === "WINNT" ? "\r\n" : "\n";
for (let textProp of this.textProps) { for (let textProp of this.textProps) {
cssText += "\t" + textProp.stringifyProperty() + terminator; if (!textProp.invisible) {
cssText += "\t" + textProp.stringifyProperty() + terminator;
}
} }
return selectorText + " {" + terminator + cssText + "}"; return selectorText + " {" + terminator + cssText + "}";
},
/**
* See whether this rule has any non-invisible properties.
* @return {Boolean} true if there is any visible property, or false
* if all properties are invisible
*/
hasAnyVisibleProperties: function() {
for (let prop of this.textProps) {
if (!prop.invisible) {
return true;
}
}
return false;
} }
}; };
/** /**
* A single property in a rule's cssText. * A single property in a rule's authoredText.
* *
* @param {Rule} rule * @param {Rule} rule
* The rule this TextProperty came from. * The rule this TextProperty came from.
@ -996,13 +1099,22 @@ Rule.prototype = {
* The property's value (not including priority). * The property's value (not including priority).
* @param {String} priority * @param {String} priority
* The property's priority (either "important" or an empty string). * The property's priority (either "important" or an empty string).
* @param {Boolean} enabled
* Whether the property is enabled.
* @param {Boolean} invisible
* Whether the property is invisible. An invisible property
* does not show up in the UI; these are needed so that the
* index of a property in Rule.textProps is the same as the index
* coming from parseDeclarations.
*/ */
function TextProperty(rule, name, value, priority) { function TextProperty(rule, name, value, priority, enabled = true,
invisible = false) {
this.rule = rule; this.rule = rule;
this.name = name; this.name = name;
this.value = value; this.value = value;
this.priority = priority; this.priority = priority;
this.enabled = true; this.enabled = !!enabled;
this.invisible = invisible;
this.updateComputed(); this.updateComputed();
} }
@ -1087,6 +1199,17 @@ TextProperty.prototype = {
this.updateEditor(); this.updateEditor();
}, },
/**
* Called when the property's value has been updated externally, and
* the property and editor should update.
*/
noticeNewValue: function(value) {
if (value !== this.value) {
this.value = value;
this.updateEditor();
}
},
setName: function(name) { setName: function(name) {
let store = this.rule.elementStyle.store; let store = this.rule.elementStyle.store;
@ -1122,6 +1245,33 @@ TextProperty.prototype = {
} }
return declaration; return declaration;
},
/**
* See whether this property's name is known.
*
* @return {Boolean} true if the property name is known, false otherwise.
*/
isKnownProperty: function() {
try {
// If the property name is invalid, the cssPropertyIsShorthand
// will throw an exception. But if it is valid, no exception will
// be thrown; so we just ignore the return value.
domUtils.cssPropertyIsShorthand(this.name);
return true;
} catch (e) {
return false;
}
},
/**
* Validate this property. Does it make sense for this value to be assigned
* to this property name? This does not apply the property value
*
* @return {Boolean} true if the property value is valid, false otherwise.
*/
isValid: function() {
return domUtils.cssPropertyIsValid(this.name, this.value);
} }
}; };
@ -1485,19 +1635,15 @@ CssRuleView.prototype = {
}, },
/** /**
* Add a new rule to the current element. * A helper for _onAddRule that handles the case where the actor
* does not support as-authored styles.
*/ */
_onAddRule: function() { _onAddNewRuleNonAuthored: function() {
let elementStyle = this._elementStyle; let elementStyle = this._elementStyle;
let element = elementStyle.element; let element = elementStyle.element;
let rules = elementStyle.rules; let rules = elementStyle.rules;
let client = this.inspector.toolbox._target.client;
let pseudoClasses = element.pseudoClassLocks; let pseudoClasses = element.pseudoClassLocks;
if (!client.traits.addNewRule) {
return;
}
this.pageStyle.addNewRule(element, pseudoClasses).then(options => { this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
let newRule = new Rule(elementStyle, options); let newRule = new Rule(elementStyle, options);
rules.push(newRule); rules.push(newRule);
@ -1523,6 +1669,44 @@ CssRuleView.prototype = {
}); });
}, },
/**
* Add a new rule to the current element.
*/
_onAddRule: function() {
let elementStyle = this._elementStyle;
let element = elementStyle.element;
let client = this.inspector.toolbox._target.client;
let pseudoClasses = element.pseudoClassLocks;
if (!client.traits.addNewRule) {
return;
}
if (!this.pageStyle.supportsAuthoredStyles) {
// We're talking to an old server.
this._onAddNewRuleNonAuthored();
return;
}
// Adding a new rule with authored styles will cause the actor to
// emit an event, which will in turn cause the rule view to be
// updated. So, we wait for this update and for the rule creation
// request to complete, and then focus the new rule's selector.
let eventPromise = this.once("ruleview-refreshed");
let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
promise.all([eventPromise, newRulePromise]).then((values) => {
let options = values[1];
// Be sure the reference the correct |rules| here.
for (let rule of this._elementStyle.rules) {
if (options.rule === rule.domRule) {
rule.editor.selectorText.click();
elementStyle._changed();
break;
}
}
});
},
/** /**
* Disables add rule button when needed * Disables add rule button when needed
*/ */
@ -2146,7 +2330,7 @@ CssRuleView.prototype = {
// Highlight search matches in the rule properties // Highlight search matches in the rule properties
for (let textProp of rule.textProps) { for (let textProp of rule.textProps) {
if (this._highlightProperty(textProp.editor)) { if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
isHighlighted = true; isHighlighted = true;
} }
} }
@ -2435,11 +2619,18 @@ function RuleEditor(aRuleView, aRule) {
this._onNewProperty = this._onNewProperty.bind(this); this._onNewProperty = this._onNewProperty.bind(this);
this._newPropertyDestroy = this._newPropertyDestroy.bind(this); this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
this._onSelectorDone = this._onSelectorDone.bind(this); this._onSelectorDone = this._onSelectorDone.bind(this);
this._locationChanged = this._locationChanged.bind(this);
this.rule.domRule.on("location-changed", this._locationChanged);
this._create(); this._create();
} }
RuleEditor.prototype = { RuleEditor.prototype = {
destroy: function() {
this.rule.domRule.off("location-changed");
},
get isSelectorEditable() { get isSelectorEditable() {
let toolbox = this.ruleView.inspector.toolbox; let toolbox = this.ruleView.inspector.toolbox;
let trait = this.isEditable && let trait = this.isEditable &&
@ -2554,17 +2745,26 @@ RuleEditor.prototype = {
} }
}, },
/**
* Event handler called when a property changes on the
* StyleRuleActor.
*/
_locationChanged: function(line, column) {
this.updateSourceLink();
},
updateSourceLink: function() { updateSourceLink: function() {
let sourceLabel = this.element.querySelector(".ruleview-rule-source-label"); let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
let title = this.rule.title;
let sourceHref = (this.rule.sheet && this.rule.sheet.href) ? let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
this.rule.sheet.href : this.rule.title; this.rule.sheet.href : title;
let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : ""; let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine); sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine);
if (this.rule.isSystem) { if (this.rule.isSystem) {
let uaLabel = _strings.GetStringFromName("rule.userAgentStyles"); let uaLabel = _strings.GetStringFromName("rule.userAgentStyles");
sourceLabel.setAttribute("value", uaLabel + " " + this.rule.title); sourceLabel.setAttribute("value", uaLabel + " " + title);
// Special case about:PreferenceStyleSheet, as it is generated on the // Special case about:PreferenceStyleSheet, as it is generated on the
// fly and the URI is not registered with the about: handler. // fly and the URI is not registered with the about: handler.
@ -2575,7 +2775,7 @@ RuleEditor.prototype = {
sourceLabel.removeAttribute("tooltiptext"); sourceLabel.removeAttribute("tooltiptext");
} }
} else { } else {
sourceLabel.setAttribute("value", this.rule.title); sourceLabel.setAttribute("value", title);
if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) { if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
sourceLabel.parentNode.setAttribute("unselectable", "true"); sourceLabel.parentNode.setAttribute("unselectable", "true");
} }
@ -2654,7 +2854,7 @@ RuleEditor.prototype = {
} }
for (let prop of this.rule.textProps) { for (let prop of this.rule.textProps) {
if (!prop.editor) { if (!prop.editor && !prop.invisible) {
let editor = new TextPropertyEditor(this, prop); let editor = new TextPropertyEditor(this, prop);
this.propertyList.appendChild(editor.element); this.propertyList.appendChild(editor.element);
} }
@ -2862,13 +3062,13 @@ RuleEditor.prototype = {
rules.splice(rules.indexOf(this.rule), 1); rules.splice(rules.indexOf(this.rule), 1);
rules.push(newRule); rules.push(newRule);
elementStyle._changed(); elementStyle._changed();
elementStyle.markOverriddenAll();
editor.element.setAttribute("unmatched", !isMatching); editor.element.setAttribute("unmatched", !isMatching);
this.element.parentNode.replaceChild(editor.element, this.element); this.element.parentNode.replaceChild(editor.element, this.element);
// Remove highlight for modified selector // Remove highlight for modified selector
if (ruleView.highlightedSelector && if (ruleView.highlightedSelector) {
ruleView.highlightedSelector === this.rule.selectorText) {
ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon, ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
ruleView.highlightedSelector); ruleView.highlightedSelector);
} }
@ -3172,7 +3372,8 @@ TextPropertyEditor.prototype = {
!this.isValid() || !this.isValid() ||
!this.prop.overridden; !this.prop.overridden;
if (this.prop.overridden || !this.prop.enabled) { if (this.prop.overridden || !this.prop.enabled ||
!this.prop.isKnownProperty()) {
this.element.classList.add("ruleview-overridden"); this.element.classList.add("ruleview-overridden");
} else { } else {
this.element.classList.remove("ruleview-overridden"); this.element.classList.remove("ruleview-overridden");
@ -3502,8 +3703,10 @@ TextPropertyEditor.prototype = {
this.committed.value === val.value && this.committed.value === val.value &&
this.committed.priority === val.priority; this.committed.priority === val.priority;
// If the value is not empty and unchanged, revert the property back to // If the value is not empty and unchanged, revert the property back to
// its original enabled or disabled state // its original value and enabled or disabled state
if (value.trim() && isValueUnchanged) { if (value.trim() && isValueUnchanged) {
this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
val.priority);
this.rule.setPropertyEnabled(this.prop, this.prop.enabled); this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
return; return;
} }
@ -3555,7 +3758,7 @@ TextPropertyEditor.prototype = {
* value of this property before editing. * value of this property before editing.
*/ */
_onSwatchRevert: function() { _onSwatchRevert: function() {
this.rule.setPropertyEnabled(this.prop, this.prop.enabled); this._previewValue(this.prop.value);
this.update(); this.update();
}, },
@ -3631,7 +3834,7 @@ TextPropertyEditor.prototype = {
* @return {Boolean} true if the property value is valid, false otherwise. * @return {Boolean} true if the property value is valid, false otherwise.
*/ */
isValid: function() { isValid: function() {
return domUtils.cssPropertyIsValid(this.prop.name, this.prop.value); return this.prop.isValid();
} }
}; };

View File

@ -48,6 +48,7 @@ function RuleViewTool(inspector, window) {
this.inspector.selection.on("pseudoclass", this.refresh); this.inspector.selection.on("pseudoclass", this.refresh);
this.inspector.target.on("navigate", this.clearUserProperties); this.inspector.target.on("navigate", this.clearUserProperties);
this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.onSelected(); this.onSelected();
} }
@ -152,6 +153,9 @@ RuleViewTool.prototype = {
this.inspector.selection.off("new-node-front", this.onSelected); this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.target.off("navigate", this.clearUserProperties); this.inspector.target.off("navigate", this.clearUserProperties);
this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected); this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
if (this.inspector.pageStyle) {
this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
}
this.view.off("ruleview-linked-clicked", this.onLinkClicked); this.view.off("ruleview-linked-clicked", this.onLinkClicked);
this.view.off("ruleview-changed", this.onPropertyChanged); this.view.off("ruleview-changed", this.onPropertyChanged);
@ -179,6 +183,7 @@ function ComputedViewTool(inspector, window) {
this.inspector.on("layout-change", this.refresh); this.inspector.on("layout-change", this.refresh);
this.inspector.selection.on("pseudoclass", this.refresh); this.inspector.selection.on("pseudoclass", this.refresh);
this.inspector.sidebar.on("computedview-selected", this.onPanelSelected); this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.view.selectElement(null); this.view.selectElement(null);
@ -244,6 +249,9 @@ ComputedViewTool.prototype = {
this.inspector.selection.off("pseudoclass", this.refresh); this.inspector.selection.off("pseudoclass", this.refresh);
this.inspector.selection.off("new-node-front", this.onSelected); this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.sidebar.off("computedview-selected", this.onPanelSelected); this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
if (this.inspector.pageStyle) {
this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
}
this.view.destroy(); this.view.destroy();

View File

@ -19,6 +19,7 @@ support-files =
doc_matched_selectors.html doc_matched_selectors.html
doc_media_queries.html doc_media_queries.html
doc_pseudoelement.html doc_pseudoelement.html
doc_ruleLineNumbers.html
doc_sourcemaps.css doc_sourcemaps.css
doc_sourcemaps.css.map doc_sourcemaps.css.map
doc_sourcemaps.html doc_sourcemaps.html
@ -60,6 +61,7 @@ support-files =
[browser_ruleview_add-rule_03.js] [browser_ruleview_add-rule_03.js]
[browser_ruleview_add-rule_04.js] [browser_ruleview_add-rule_04.js]
[browser_ruleview_add-rule_pseudo_class.js] [browser_ruleview_add-rule_pseudo_class.js]
[browser_ruleview_authored.js]
[browser_ruleview_colorpicker-and-image-tooltip_01.js] [browser_ruleview_colorpicker-and-image-tooltip_01.js]
[browser_ruleview_colorpicker-and-image-tooltip_02.js] [browser_ruleview_colorpicker-and-image-tooltip_02.js]
[browser_ruleview_colorpicker-appears-on-swatch-click.js] [browser_ruleview_colorpicker-appears-on-swatch-click.js]
@ -118,18 +120,21 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_ruleview_filtereditor-commit-on-ENTER.js] [browser_ruleview_filtereditor-commit-on-ENTER.js]
[browser_ruleview_filtereditor-revert-on-ESC.js] [browser_ruleview_filtereditor-revert-on-ESC.js]
skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s. skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
[browser_ruleview_guessIndentation.js]
[browser_ruleview_inherited-properties_01.js] [browser_ruleview_inherited-properties_01.js]
[browser_ruleview_inherited-properties_02.js] [browser_ruleview_inherited-properties_02.js]
[browser_ruleview_inherited-properties_03.js] [browser_ruleview_inherited-properties_03.js]
[browser_ruleview_keybindings.js] [browser_ruleview_keybindings.js]
[browser_ruleview_keyframes-rule_01.js] [browser_ruleview_keyframes-rule_01.js]
[browser_ruleview_keyframes-rule_02.js] [browser_ruleview_keyframes-rule_02.js]
[browser_ruleview_lineNumbers.js]
[browser_ruleview_livepreview.js] [browser_ruleview_livepreview.js]
[browser_ruleview_mark_overridden_01.js] [browser_ruleview_mark_overridden_01.js]
[browser_ruleview_mark_overridden_02.js] [browser_ruleview_mark_overridden_02.js]
[browser_ruleview_mark_overridden_03.js] [browser_ruleview_mark_overridden_03.js]
[browser_ruleview_mark_overridden_04.js] [browser_ruleview_mark_overridden_04.js]
[browser_ruleview_mark_overridden_05.js] [browser_ruleview_mark_overridden_05.js]
[browser_ruleview_mark_overridden_06.js]
[browser_ruleview_mark_overridden_07.js] [browser_ruleview_mark_overridden_07.js]
[browser_ruleview_mathml-element.js] [browser_ruleview_mathml-element.js]
[browser_ruleview_media-queries.js] [browser_ruleview_media-queries.js]

View File

@ -34,7 +34,13 @@ function checkColorCycling(container, inspector) {
let valueNode = container.querySelector(".computedview-color"); let valueNode = container.querySelector(".computedview-color");
let win = inspector.sidebar.getWindowForTab("computedview"); let win = inspector.sidebar.getWindowForTab("computedview");
// Hex (default) // "Authored" (default; currently the computed value)
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Hex
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00", "Color displayed as a hex value."); is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
// HSL // HSL
@ -55,15 +61,9 @@ function checkColorCycling(container, inspector) {
is(valueNode.textContent, "red", is(valueNode.textContent, "red",
"Color displayed as a color name."); "Color displayed as a color name.");
// "Authored" (currently the computed value) // Back to "Authored"
EventUtils.synthesizeMouseAtCenter(swatch, EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win); {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)", is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value."); "Color displayed as an RGB value.");
// Back to hex
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00",
"Color displayed as hex again.");
} }

View File

@ -75,7 +75,7 @@ const TEST_DATA = [
ok("property" in nodeInfo.value); ok("property" in nodeInfo.value);
ok("value" in nodeInfo.value); ok("value" in nodeInfo.value);
is(nodeInfo.value.property, "color"); is(nodeInfo.value.property, "color");
is(nodeInfo.value.value, "#F00"); is(nodeInfo.value.value, "rgb(255, 0, 0)");
} }
}, },
{ {
@ -88,7 +88,7 @@ const TEST_DATA = [
ok("property" in nodeInfo.value); ok("property" in nodeInfo.value);
ok("value" in nodeInfo.value); ok("value" in nodeInfo.value);
is(nodeInfo.value.property, "color"); is(nodeInfo.value.property, "color");
is(nodeInfo.value.value, "#F00"); is(nodeInfo.value.value, "rgb(255, 0, 0)");
} }
}, },
{ {
@ -149,7 +149,7 @@ const TEST_DATA = [
assertNodeInfo: function(nodeInfo) { assertNodeInfo: function(nodeInfo) {
is(nodeInfo.type, VIEW_NODE_VALUE_TYPE); is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
is(nodeInfo.value.property, "color"); is(nodeInfo.value.property, "color");
is(nodeInfo.value.value, "#F00"); is(nodeInfo.value.value, "red");
} }
}, },
{ {

View File

@ -25,5 +25,5 @@ add_task(function*() {
fontSize = getComputedViewPropertyValue(view, "font-size"); fontSize = getComputedViewPropertyValue(view, "font-size");
is(fontSize, "15px", "The computed view shows the updated font-size"); is(fontSize, "15px", "The computed view shows the updated font-size");
let color = getComputedViewPropertyValue(view, "color"); let color = getComputedViewPropertyValue(view, "color");
is(color, "#F00", "The computed view also shows the color now"); is(color, "rgb(255, 0, 0)", "The computed view also shows the color now");
}); });

View File

@ -81,7 +81,7 @@ function checkSelectAll(view) {
info("Checking that _onSelectAll() then copy returns the correct " + info("Checking that _onSelectAll() then copy returns the correct " +
"clipboard value"); "clipboard value");
view._contextmenu._onSelectAll(); view._contextmenu._onSelectAll();
let expectedPattern = "color: #FF0;[\\r\\n]+" + let expectedPattern = "color: rgb\\(255, 255, 0\\);[\\r\\n]+" +
"font-family: helvetica,sans-serif;[\\r\\n]+" + "font-family: helvetica,sans-serif;[\\r\\n]+" +
"font-size: 16px;[\\r\\n]+" + "font-size: 16px;[\\r\\n]+" +
"font-variant-caps: small-caps;[\\r\\n]*"; "font-variant-caps: small-caps;[\\r\\n]*";

View File

@ -69,6 +69,6 @@ function* testCreateNew(view) {
yield onModifications; yield onModifications;
is(textProp.value, "#XYZ", "Text prop should have been changed."); is(textProp.value, "#XYZ", "Text prop should have been changed.");
is(textProp.overridden, false, "Property should not be overridden"); is(textProp.overridden, true, "Property should be overridden");
is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry"); is(textProp.editor.isValid(), false, "#XYZ should not be a valid entry");
} }

View File

@ -0,0 +1,127 @@
/* 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 for as-authored styles.
add_task(function*() {
yield basicTest();
yield overrideTest();
yield colorEditingTest();
});
function* createTestContent(style) {
let content = `<style type="text/css">
${style}
</style>
<div id="testid" class="testclass">Styled Node</div>`;
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(content));
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
return view;
}
function* basicTest() {
let view = yield createTestContent("#testid {" +
// Invalid property.
" something: random;" +
// Invalid value.
" color: orang;" +
// Override.
" background-color: blue;" +
" background-color: #f0c;" +
"} ");
let elementStyle = view._elementStyle;
let expected = [
{name: "something", overridden: true},
{name: "color", overridden: true},
{name: "background-color", overridden: true},
{name: "background-color", overridden: false}
];
let rule = elementStyle.rules[1];
for (let i = 0; i < expected.length; ++i) {
let prop = rule.textProps[i];
is(prop.name, expected[i].name, "test name for prop " + i);
is(prop.overridden, expected[i].overridden,
"test overridden for prop " + i);
}
}
function* overrideTest() {
let gradientText = "(45deg, rgba(255,255,255,0.2) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.2) 75%, transparent 75%, transparent);";
let view =
yield createTestContent("#testid {" +
" background-image: -moz-linear-gradient" +
gradientText +
" background-image: -webkit-linear-gradient" +
gradientText +
" background-image: linear-gradient" +
gradientText +
"} ");
let elementStyle = view._elementStyle;
let rule = elementStyle.rules[1];
// Initially the last property should be active.
for (let i = 0; i < 3; ++i) {
let prop = rule.textProps[i];
is(prop.name, "background-image", "check the property name");
is(prop.overridden, i !== 2, "check overridden for " + i);
}
rule.textProps[2].setEnabled(false);
yield rule._applyingModifications;
// Now the first property should be active.
for (let i = 0; i < 3; ++i) {
let prop = rule.textProps[i];
is(prop.overridden || !prop.enabled, i !== 0,
"post-change check overridden for " + i);
}
}
function* colorEditingTest() {
let colors = [
{name: "hex", text: "#f0c", result: "#0F0"},
{name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}
];
Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");
for (let color of colors) {
let view = yield createTestContent("#testid {" +
" color: " + color.text + ";" +
"} ");
let cPicker = view.tooltips.colorPicker;
let swatch = getRuleViewProperty(view, "#testid", "color").valueSpan
.querySelector(".ruleview-colorswatch");
let onShown = cPicker.tooltip.once("shown");
swatch.click();
yield onShown;
let testNode = yield getNode("#testid");
yield simulateColorPickerChange(view, cPicker, [0, 255, 0, 1], {
element: testNode,
name: "color",
value: "rgb(0, 255, 0)"
});
let spectrum = yield cPicker.spectrum;
let onHidden = cPicker.tooltip.once("hidden");
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
yield onHidden;
is(getRuleViewPropertyValue(view, "#testid", "color"), color.result,
"changing the color preserved the unit for " + color.name);
}
}

View File

@ -21,7 +21,7 @@ add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {view} = yield openRuleView(); let {view} = yield openRuleView();
let value = getRuleViewProperty(view, "body", "background").valueSpan; let value = getRuleViewProperty(view, "body", "background").valueSpan;
let swatch = value.querySelectorAll(".ruleview-colorswatch")[1]; let swatch = value.querySelectorAll(".ruleview-colorswatch")[0];
let url = value.querySelector(".theme-link"); let url = value.querySelector(".theme-link");
yield testImageTooltipAfterColorChange(swatch, url, view); yield testImageTooltipAfterColorChange(swatch, url, view);
}); });

View File

@ -61,7 +61,7 @@ function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
info("Image tooltip is shown, verify that the swatch is still correct"); info("Image tooltip is shown, verify that the swatch is still correct");
swatch = value.querySelector(".ruleview-colorswatch"); swatch = value.querySelector(".ruleview-colorswatch");
is(swatch.style.backgroundColor, "rgb(0, 0, 0)", is(swatch.style.backgroundColor, "black",
"The swatch's color is correct"); "The swatch's color is correct");
is(swatch.nextSibling.textContent, "#000", "The color name is correct"); is(swatch.nextSibling.textContent, "black", "The color name is correct");
} }

View File

@ -39,7 +39,7 @@ function testColorParsing(view) {
ok(colorEls, "The color elements were found"); ok(colorEls, "The color elements were found");
is(colorEls.length, 3, "There are 3 color values"); is(colorEls.length, 3, "There are 3 color values");
let colors = ["#F06", "#333", "#000"]; let colors = ["#f06", "#333", "#000"];
for (let i = 0; i < colors.length; i++) { for (let i = 0; i < colors.length; i++) {
is(colorEls[i].textContent, colors[i], "The right color value was found"); is(colorEls[i].textContent, colors[i], "The right color value was found");
} }

View File

@ -27,7 +27,7 @@ function checkColorCycling(container, inspector) {
let valueNode = container.querySelector(".ruleview-color"); let valueNode = container.querySelector(".ruleview-color");
let win = inspector.sidebar.getWindowForTab("ruleview"); let win = inspector.sidebar.getWindowForTab("ruleview");
// Hex (default) // Hex
is(valueNode.textContent, "#F00", "Color displayed as a hex value."); is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
// HSL // HSL
@ -48,15 +48,16 @@ function checkColorCycling(container, inspector) {
is(valueNode.textContent, "red", is(valueNode.textContent, "red",
"Color displayed as a color name."); "Color displayed as a color name.");
// "Authored" (currently the computed value) // "Authored"
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Back to hex
EventUtils.synthesizeMouseAtCenter(swatch, EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win); {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00", is(valueNode.textContent, "#F00",
"Color displayed as hex again."); "Color displayed as an authored value.");
// One more click skips hex, because it is the same as authored, and
// instead goes back to HSL.
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "hsl(0, 100%, 50%)",
"Color displayed as an HSL value again.");
} }

View File

@ -48,9 +48,10 @@ function* testEditPropertyAndRemove(inspector, view) {
"Focus should have moved to the next property name"); "Focus should have moved to the next property name");
info("Focus the property value and remove the property"); info("Focus the property value and remove the property");
let onChanged = view.once("ruleview-changed");
yield sendKeysAndWaitForFocus(view, ruleEditor.element, yield sendKeysAndWaitForFocus(view, ruleEditor.element,
["VK_TAB", "VK_DELETE", "VK_RETURN"]); ["VK_TAB", "VK_DELETE", "VK_RETURN"]);
yield ruleEditor.rule._applyingModifications; yield onChanged;
newValue = yield executeInContent("Test:GetRulePropertyValue", { newValue = yield executeInContent("Test:GetRulePropertyValue", {
styleSheetIndex: 0, styleSheetIndex: 0,

View File

@ -59,7 +59,7 @@ function* testEditProperty(ruleEditor, name, value, isValid) {
info("Entering a new property name, including : to commit and " + info("Entering a new property name, including : to commit and " +
"focus the value"); "focus the value");
let onValueFocus = once(ruleEditor.element, "focus", true); let onValueFocus = once(ruleEditor.element, "focus", true);
let onModifications = ruleEditor.rule._applyingModifications; let onModifications = ruleEditor.ruleView.once("ruleview-changed");
EventUtils.sendString(name + ":", doc.defaultView); EventUtils.sendString(name + ":", doc.defaultView);
yield onValueFocus; yield onValueFocus;
yield onModifications; yield onModifications;
@ -71,10 +71,9 @@ function* testEditProperty(ruleEditor, name, value, isValid) {
info("Entering a new value, including ; to commit and blur the value"); info("Entering a new value, including ; to commit and blur the value");
let onBlur = once(input, "blur"); let onBlur = once(input, "blur");
onModifications = ruleEditor.rule._applyingModifications;
EventUtils.sendString(value + ";", doc.defaultView); EventUtils.sendString(value + ";", doc.defaultView);
yield onBlur; yield onBlur;
yield onModifications; yield ruleEditor.rule._applyingModifications;
is(propEditor.isValid(), isValid, is(propEditor.isValid(), isValid,
value + " is " + isValid ? "valid" : "invalid"); value + " is " + isValid ? "valid" : "invalid");

View File

@ -33,8 +33,10 @@ add_task(function*() {
yield focusEditableField(view, propEditor.valueSpan); yield focusEditableField(view, propEditor.valueSpan);
info("Deleting all the text out of a value field"); info("Deleting all the text out of a value field");
let waitForUpdates = view.once("ruleview-changed");
yield sendCharsAndWaitForFocus(view, ruleEditor.element, yield sendCharsAndWaitForFocus(view, ruleEditor.element,
["VK_DELETE", "VK_RETURN"]); ["VK_DELETE", "VK_RETURN"]);
yield waitForUpdates;
info("Pressing enter a couple times to cycle through editors"); info("Pressing enter a couple times to cycle through editors");
yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]); yield sendCharsAndWaitForFocus(view, ruleEditor.element, ["VK_RETURN"]);
@ -48,9 +50,7 @@ add_task(function*() {
function* sendCharsAndWaitForFocus(view, element, chars) { function* sendCharsAndWaitForFocus(view, element, chars) {
let onFocus = once(element, "focus", true); let onFocus = once(element, "focus", true);
for (let ch of chars) { for (let ch of chars) {
let onRuleViewChanged = view.once("ruleview-changed");
EventUtils.sendChar(ch, element.ownerDocument.defaultView); EventUtils.sendChar(ch, element.ownerDocument.defaultView);
yield onRuleViewChanged;
} }
yield onFocus; yield onFocus;
} }

View File

@ -10,7 +10,7 @@
const TEST_URI = ` const TEST_URI = `
<style type='text/css'> <style type='text/css'>
#testid { #testid {
background-color: red; background-color: #f00;
} }
</style> </style>
<div id='testid'>Styled Node</div> <div id='testid'>Styled Node</div>

View File

@ -98,7 +98,7 @@ function* testAddProperty(view) {
info("Entering a value and bluring the field to expect a rule change"); info("Entering a value and bluring the field to expect a rule change");
editor.input.value = "center"; editor.input.value = "center";
let onBlur = once(editor.input, "blur"); let onBlur = once(editor.input, "blur");
onRuleViewChanged = view.once("ruleview-changed"); onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
editor.input.blur(); editor.input.blur();
yield onBlur; yield onBlur;
yield onRuleViewChanged; yield onRuleViewChanged;

View File

@ -0,0 +1,76 @@
/* 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 that we can guess indentation from a style sheet, not just a
// rule.
// Needed for openStyleEditor.
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/styleeditor/test/head.js", this);
// Use a weird indentation depth to avoid accidental success.
const TEST_URI = `
<style type='text/css'>
div {
background-color: blue;
}
* {
}
</style>
<div id='testid' class='testclass'>Styled Node</div>
`;
const expectedText = `
div {
background-color: blue;
}
* {
color: chartreuse;
}
`;
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
yield testIndentation(inspector, view);
});
function* testIndentation(inspector, view) {
let ruleEditor = getRuleViewRuleEditor(view, 2);
info("Focusing a new property name in the rule-view");
let editor = yield focusEditableField(view, ruleEditor.closeBrace);
let input = editor.input;
info("Entering color in the property name editor");
input.value = "color";
info("Pressing return to commit and focus the new value field");
let onValueFocus = once(ruleEditor.element, "focus", true);
let onModifications = ruleEditor.rule._applyingModifications;
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onValueFocus;
yield onModifications;
// Getting the new value editor after focus
editor = inplaceEditor(view.styleDocument.activeElement);
info("Entering a value and bluring the field to expect a rule change");
editor.input.value = "chartreuse";
let onBlur = once(editor.input, "blur");
onModifications = ruleEditor.rule._applyingModifications;
editor.input.blur();
yield onBlur;
yield onModifications;
let { ui } = yield openStyleEditor();
let styleEditor = yield ui.editors[0].getSourceEditor();
let text = styleEditor.sourceEditor.getText();
is(text, expectedText, "style inspector changes are synced");
}

View File

@ -38,8 +38,12 @@ function* simpleInherit(inspector, view) {
is(inheritRule.selectorText, "#test2", is(inheritRule.selectorText, "#test2",
"Inherited rule should be the one that includes inheritable properties."); "Inherited rule should be the one that includes inheritable properties.");
ok(!!inheritRule.inherited, "Rule should consider itself inherited."); ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
is(inheritRule.textProps.length, 1, is(inheritRule.textProps.length, 2,
"Should only display one inherited style"); "Rule should have two styles");
let inheritProp = inheritRule.textProps[0]; let bgcProp = inheritRule.textProps[0];
is(bgcProp.name, "background-color",
"background-color property should exist");
ok(bgcProp.invisible, "background-color property should be invisible");
let inheritProp = inheritRule.textProps[1];
is(inheritProp.name, "color", "color should have been inherited."); is(inheritProp.name, "color", "color should have been inherited.");
} }

View File

@ -0,0 +1,52 @@
/* 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 that editing a rule will update the line numbers of subsequent
// rules in the rule view.
const TESTCASE_URI = TEST_URL_ROOT + "doc_ruleLineNumbers.html";
add_task(function*() {
yield addTab(TESTCASE_URI);
let { inspector, view } = yield openRuleView();
yield selectNode("#testid", inspector);
let elementRuleEditor = getRuleViewRuleEditor(view, 1);
let bodyRuleEditor = getRuleViewRuleEditor(view, 3);
let value = getRuleViewLinkTextByIndex(view, 2);
// Note that this is relative to the <style>.
is(value.slice(-2), ":6", "initial rule line number is 6");
info("Focusing a new property name in the rule-view");
let editor = yield focusEditableField(view, elementRuleEditor.closeBrace);
is(inplaceEditor(elementRuleEditor.newPropSpan), editor,
"The new property editor got focused");
let input = editor.input;
info("Entering font-size in the property name editor");
input.value = "font-size";
info("Pressing return to commit and focus the new value field");
let onLocationChanged = once(bodyRuleEditor.rule.domRule, "location-changed");
let onValueFocus = once(elementRuleEditor.element, "focus", true);
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onValueFocus;
yield elementRuleEditor.rule._applyingModifications;
// Getting the new value editor after focus
editor = inplaceEditor(view.styleDocument.activeElement);
info("Entering a value and bluring the field to expect a rule change");
editor.input.value = "23px";
editor.input.blur();
yield elementRuleEditor.rule._applyingModifications;
yield onLocationChanged;
let newBodyTitle = getRuleViewLinkTextByIndex(view, 2);
// Note that this is relative to the <style>.
is(newBodyTitle.slice(-2), ":7", "updated rule line number is 7");
});

View File

@ -27,6 +27,7 @@ function* testMarkOverridden(inspector, view) {
let ruleEditor = getRuleViewRuleEditor(view, 1); let ruleEditor = getRuleViewRuleEditor(view, 1);
yield createNewRuleViewProperty(ruleEditor, "background-color: red;"); yield createNewRuleViewProperty(ruleEditor, "background-color: red;");
yield ruleEditor.rule._applyingModifications;
let firstProp = ruleEditor.rule.textProps[0]; let firstProp = ruleEditor.rule.textProps[0];
let secondProp = ruleEditor.rule.textProps[1]; let secondProp = ruleEditor.rule.textProps[1];

View File

@ -0,0 +1,60 @@
/* 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 that the rule view marks overridden rules correctly after
// editing the selector.
const TEST_URI = `
<style type='text/css'>
div {
background-color: blue;
background-color: chartreuse;
}
</style>
<div id='testid' class='testclass'>Styled Node</div>
`;
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
yield testMarkOverridden(inspector, view);
});
function* testMarkOverridden(inspector, view) {
let elementStyle = view._elementStyle;
let rule = elementStyle.rules[1];
checkProperties(rule);
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(view, ruleEditor.selectorText);
info("Entering a new selector name and committing");
editor.input.value = "div[class]";
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewChanged;
view.searchField.focus();
checkProperties(rule);
}
// A helper to perform a repeated set of checks.
function checkProperties(rule) {
let prop = rule.textProps[0];
is(prop.name, "background-color",
"First property should be background-color");
is(prop.value, "blue", "First property value should be blue");
ok(prop.overridden, "prop should be overridden.");
prop = rule.textProps[1];
is(prop.name, "background-color",
"Second property should be background-color");
is(prop.value, "chartreuse", "First property value should be chartreuse");
ok(!prop.overridden, "prop should not be overridden.");
}

View File

@ -51,7 +51,8 @@ function* testMarkOverridden(inspector, view) {
[{name: "margin-right", value: "23px", overridden: false}, [{name: "margin-right", value: "23px", overridden: false},
{name: "margin-left", value: "1px", overridden: false}], {name: "margin-left", value: "1px", overridden: false}],
[{name: "font-size", value: "12px", overridden: false}], [{name: "font-size", value: "12px", overridden: false}],
[{name: "font-size", value: "79px", overridden: true}] [{name: "margin-right", value: "1px", overridden: true},
{name: "font-size", value: "79px", overridden: true}]
]; ];
for (let i = 1; i < RESULTS.length; ++i) { for (let i = 1; i < RESULTS.length; ++i) {

View File

@ -24,7 +24,6 @@ add_task(function*() {
is(elementStyle.rules.length, 3, "Should have 3 rules."); is(elementStyle.rules.length, 3, "Should have 3 rules.");
is(elementStyle.rules[0].title, inline, "check rule 0 title"); is(elementStyle.rules[0].title, inline, "check rule 0 title");
is(elementStyle.rules[1].title, inline + is(elementStyle.rules[1].title, inline +
":15 @media screen and (min-width: 1px)", "check rule 1 title"); ":9 @media screen and (min-width: 1px)", "check rule 1 title");
is(elementStyle.rules[2].title, inline + ":8", "check rule 2 title"); is(elementStyle.rules[2].title, inline + ":2", "check rule 2 title");
}); });

View File

@ -4,7 +4,7 @@
"use strict"; "use strict";
// Test that the rule-view behaves correctly when entering mutliple and/or // Test that the rule-view behaves correctly when entering multiple and/or
// unfinished properties/values in inplace-editors // unfinished properties/values in inplace-editors
const TEST_URI = "<div>Test Element</div>"; const TEST_URI = "<div>Test Element</div>";
@ -16,24 +16,12 @@ add_task(function*() {
yield testCreateNewMultiUnfinished(inspector, view); yield testCreateNewMultiUnfinished(inspector, view);
}); });
function waitRuleViewChanged(view, n) {
let deferred = promise.defer();
let count = 0;
let listener = function() {
if (++count == n) {
view.off("ruleview-changed", listener);
deferred.resolve();
}
};
view.on("ruleview-changed", listener);
return deferred.promise;
}
function* testCreateNewMultiUnfinished(inspector, view) { function* testCreateNewMultiUnfinished(inspector, view) {
let ruleEditor = getRuleViewRuleEditor(view, 0); let ruleEditor = getRuleViewRuleEditor(view, 0);
let onMutation = inspector.once("markupmutation"); let onMutation = inspector.once("markupmutation");
// There is 5 rule-view updates, one for the rule view creation, // There are 2 rule-view updates: one for the preview and one for
// one for each new property // the final commit.
let onRuleViewChanged = waitRuleViewChanged(view, 5); let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
yield createNewRuleViewProperty(ruleEditor, yield createNewRuleViewProperty(ruleEditor,
"color:blue;background : orange ; text-align:center; border-color: "); "color:blue;background : orange ; text-align:center; border-color: ");
yield onMutation; yield onMutation;

View File

@ -166,7 +166,7 @@ function* testParagraph(inspector, view) {
let elementFirstLineRule = rules.firstLineRules[0]; let elementFirstLineRule = rules.firstLineRules[0];
is(convertTextPropsToString(elementFirstLineRule.textProps), is(convertTextPropsToString(elementFirstLineRule.textProps),
"background: blue none repeat scroll 0% 0%", "background: blue",
"Paragraph first-line properties are correct"); "Paragraph first-line properties are correct");
let elementFirstLetterRule = rules.firstLetterRules[0]; let elementFirstLetterRule = rules.firstLetterRules[0];
@ -176,7 +176,7 @@ function* testParagraph(inspector, view) {
let elementSelectionRule = rules.selectionRules[0]; let elementSelectionRule = rules.selectionRules[0];
is(convertTextPropsToString(elementSelectionRule.textProps), is(convertTextPropsToString(elementSelectionRule.textProps),
"color: white; background: black none repeat scroll 0% 0%", "color: white; background: black",
"Paragraph first-letter properties are correct"); "Paragraph first-letter properties are correct");
} }

View File

@ -131,8 +131,7 @@ function* testPropertyChange6(inspector, ruleView, testElement) {
"Added a property"); "Added a property");
validateTextProp(rule.textProps[4], true, "background", validateTextProp(rule.textProps[4], true, "background",
"red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%", "red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
"shortcut property correctly set", "shortcut property correctly set");
"#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%");
} }
function* changeElementStyle(testElement, style, inspector) { function* changeElementStyle(testElement, style, inspector) {
@ -141,8 +140,7 @@ function* changeElementStyle(testElement, style, inspector) {
yield onRefreshed; yield onRefreshed;
} }
function validateTextProp(aProp, aEnabled, aName, aValue, aDesc, function validateTextProp(aProp, aEnabled, aName, aValue, aDesc) {
valueSpanText) {
is(aProp.enabled, aEnabled, aDesc + ": enabled."); is(aProp.enabled, aEnabled, aDesc + ": enabled.");
is(aProp.name, aName, aDesc + ": name."); is(aProp.name, aName, aDesc + ": name.");
is(aProp.value, aValue, aDesc + ": value."); is(aProp.value, aValue, aDesc + ": value.");
@ -151,5 +149,5 @@ function validateTextProp(aProp, aEnabled, aName, aValue, aDesc,
aDesc + ": enabled checkbox."); aDesc + ": enabled checkbox.");
is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span."); is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span.");
is(aProp.editor.valueSpan.textContent, is(aProp.editor.valueSpan.textContent,
valueSpanText || aValue, aDesc + ": value span."); aValue, aDesc + ": value span.");
} }

View File

@ -7,12 +7,14 @@
// Tests that the rule view search filter works properly in the computed list // Tests that the rule view search filter works properly in the computed list
// for color values. // for color values.
const SEARCH = "background-color: #F3F3F3"; // The color format here is chosen to match the default returned by
// CssColor.toString.
const SEARCH = "background-color: rgb(243, 243, 243)";
const TEST_URI = ` const TEST_URI = `
<style type="text/css"> <style type="text/css">
.testclass { .testclass {
background: #F3F3F3 none repeat scroll 0% 0%; background: rgb(243, 243, 243) none repeat scroll 0% 0%;
} }
</style> </style>
<div class="testclass">Styled Node</h1> <div class="testclass">Styled Node</h1>

View File

@ -43,7 +43,7 @@ function* testModifyPropertyValueFilter(inspector, view) {
"top text property is correctly highlighted."); "top text property is correctly highlighted.");
let onBlur = once(editor.input, "blur"); let onBlur = once(editor.input, "blur");
let onModification = rule._applyingModifications; let onModification = view.once("ruleview-changed");
EventUtils.sendString("4px 0px", view.styleWindow); EventUtils.sendString("4px 0px", view.styleWindow);
EventUtils.synthesizeKey("VK_RETURN", {}); EventUtils.synthesizeKey("VK_RETURN", {});
yield onBlur; yield onBlur;

View File

@ -64,10 +64,10 @@ function* checkCopySelection(view) {
let expectedPattern = " margin: 10em;[\\r\\n]+" + let expectedPattern = " margin: 10em;[\\r\\n]+" +
" font-size: 14pt;[\\r\\n]+" + " font-size: 14pt;[\\r\\n]+" +
" font-family: helvetica,sans-serif;[\\r\\n]+" + " font-family: helvetica,sans-serif;[\\r\\n]+" +
" color: #AAA;[\\r\\n]+" + " color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
"}[\\r\\n]+" + "}[\\r\\n]+" +
"html {[\\r\\n]+" + "html {[\\r\\n]+" +
" color: #000;[\\r\\n]*"; " color: #000000;[\\r\\n]*";
let onPopup = once(view._contextmenu._menupopup, "popupshown"); let onPopup = once(view._contextmenu._menupopup, "popupshown");
EventUtils.synthesizeMouseAtCenter(prop, EventUtils.synthesizeMouseAtCenter(prop,
@ -102,10 +102,10 @@ function* checkSelectAll(view) {
" margin: 10em;[\\r\\n]+" + " margin: 10em;[\\r\\n]+" +
" font-size: 14pt;[\\r\\n]+" + " font-size: 14pt;[\\r\\n]+" +
" font-family: helvetica,sans-serif;[\\r\\n]+" + " font-family: helvetica,sans-serif;[\\r\\n]+" +
" color: #AAA;[\\r\\n]+" + " color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
"}[\\r\\n]+" + "}[\\r\\n]+" +
"html {[\\r\\n]+" + "html {[\\r\\n]+" +
" color: #000;[\\r\\n]+" + " color: #000000;[\\r\\n]+" +
"}[\\r\\n]*"; "}[\\r\\n]*";
let onPopup = once(view._contextmenu._menupopup, "popupshown"); let onPopup = once(view._contextmenu._menupopup, "popupshown");

View File

@ -40,9 +40,11 @@ function* userAgentStylesUneditable(inspector, view) {
ok(rule.editor.element.hasAttribute("uneditable"), ok(rule.editor.element.hasAttribute("uneditable"),
"UA rules have uneditable attribute"); "UA rules have uneditable attribute");
ok(!rule.textProps[0].editor.nameSpan._editable, let firstProp = rule.textProps.filter(p => !p.invisible)[0];
ok(!firstProp.editor.nameSpan._editable,
"nameSpan is not editable"); "nameSpan is not editable");
ok(!rule.textProps[0].editor.valueSpan._editable, ok(!firstProp.editor.valueSpan._editable,
"valueSpan is not editable"); "valueSpan is not editable");
ok(!rule.editor.closeBrace._editable, "closeBrace is not editable"); ok(!rule.editor.closeBrace._editable, "closeBrace is not editable");

View File

@ -85,7 +85,7 @@ function testIsColorPopupOnNode(view, node) {
let correct = isColorValueNode(node); let correct = isColorValueNode(node);
is(result, correct, "_isColorPopup returned the expected value " + correct); is(result, correct, "_isColorPopup returned the expected value " + correct);
is(view._contextmenu._colorToCopy, (correct) ? "#123ABC" : "", is(view._contextmenu._colorToCopy, (correct) ? "rgb(18, 58, 188)" : "",
"_colorToCopy was set to the expected value"); "_colorToCopy was set to the expected value");
} }

View File

@ -16,7 +16,7 @@ add_task(function*() {
let {inspector, view} = yield openRuleView(); let {inspector, view} = yield openRuleView();
yield selectNode("#one", inspector); yield selectNode("#one", inspector);
is(getRuleViewPropertyValue(view, "element", "color"), "#F00", is(getRuleViewPropertyValue(view, "element", "color"), "red",
"The rule-view shows the properties for test node one"); "The rule-view shows the properties for test node one");
let cView = inspector.sidebar.getWindowForTab("computedview") let cView = inspector.sidebar.getWindowForTab("computedview")
@ -38,6 +38,6 @@ add_task(function*() {
ok(getComputedViewPropertyValue(cView, "color"), "#00F", ok(getComputedViewPropertyValue(cView, "color"), "#00F",
"The computed-view shows the properties for test node two"); "The computed-view shows the properties for test node two");
is(getRuleViewPropertyValue(view, "element", "color"), "#F00", is(getRuleViewPropertyValue(view, "element", "color"), "red",
"The rule-view doesn't the properties for test node two"); "The rule-view doesn't the properties for test node two");
}); });

View File

@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>simple testcase</title>
<style type="text/css">
#testid {
background-color: seagreen;
}
body {
color: chartreuse;
}
</style>
</head>
<body>
<div id="testid">simple testcase</div>
</body>
</html>

View File

@ -306,6 +306,44 @@ function openRuleView() {
return openInspectorSideBar("ruleview"); return openInspectorSideBar("ruleview");
} }
/**
* Wait for eventName on target to be delivered a number of times.
*
* @param {Object} target
* An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Number} numTimes
* Number of deliveries to wait for.
* @param {Boolean} useCapture
* Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function waitForNEvents(target, eventName, numTimes, useCapture = false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
let count = 0;
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
if (++count == numTimes) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}
}, useCapture);
break;
}
}
return deferred.promise;
}
/** /**
* Wait for eventName on target. * Wait for eventName on target.
* *
@ -318,25 +356,7 @@ function openRuleView() {
* @return A promise that resolves when the event has been handled * @return A promise that resolves when the event has been handled
*/ */
function once(target, eventName, useCapture=false) { function once(target, eventName, useCapture=false) {
info("Waiting for event: '" + eventName + "' on " + target + "."); return waitForNEvents(target, eventName, 1, useCapture);
let deferred = promise.defer();
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}, useCapture);
break;
}
}
return deferred.promise;
} }
/** /**

View File

@ -14,8 +14,9 @@ const {Class} = require("sdk/core/heritage");
const {LongStringActor} = require("devtools/server/actors/string"); const {LongStringActor} = require("devtools/server/actors/string");
const {PSEUDO_ELEMENT_SET} = require("devtools/shared/styleinspector/css-logic"); const {PSEUDO_ELEMENT_SET} = require("devtools/shared/styleinspector/css-logic");
// This will add the "stylesheet" actor type for protocol.js to recognize // This will also add the "stylesheet" actor type for protocol.js to recognize
require("devtools/server/actors/stylesheets"); const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} =
require("devtools/server/actors/stylesheets");
loader.lazyGetter(this, "CssLogic", () => { loader.lazyGetter(this, "CssLogic", () => {
return require("devtools/shared/styleinspector/css-logic").CssLogic; return require("devtools/shared/styleinspector/css-logic").CssLogic;
@ -24,6 +25,10 @@ loader.lazyGetter(this, "DOMUtils", () => {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
}); });
loader.lazyGetter(this, "RuleRewriter", () => {
return require("devtools/client/styleinspector/css-parsing-utils").RuleRewriter;
});
// The PageStyle actor flattens the DOM CSS objects a little bit, merging // 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 // 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. // but no associated rule) we fake a rule with the following style id.
@ -128,6 +133,13 @@ types.addDictType("fontface", {
var PageStyleActor = protocol.ActorClass({ var PageStyleActor = protocol.ActorClass({
typeName: "pagestyle", typeName: "pagestyle",
events: {
"stylesheet-updated": {
type: "styleSheetUpdated",
styleSheet: Arg(0, "stylesheet")
}
},
/** /**
* Create a PageStyleActor. * Create a PageStyleActor.
* *
@ -151,9 +163,12 @@ var PageStyleActor = protocol.ActorClass({
this.onFrameUnload = this.onFrameUnload.bind(this); this.onFrameUnload = this.onFrameUnload.bind(this);
events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload); events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
this._styleApplied = this._styleApplied.bind(this);
this._watchedSheets = new Set();
}, },
destroy: function () { destroy: function() {
if (!this.walker) { if (!this.walker) {
return; return;
} }
@ -164,6 +179,11 @@ var PageStyleActor = protocol.ActorClass({
this.refMap = null; this.refMap = null;
this.cssLogic = null; this.cssLogic = null;
this._styleElement = null; this._styleElement = null;
for (let sheet of this._watchedSheets) {
sheet.off("style-applied", this._styleApplied);
}
this._watchedSheets.clear();
}, },
get conn() { get conn() {
@ -182,11 +202,22 @@ var PageStyleActor = protocol.ActorClass({
// getApplied method calls cssLogic.highlight(node) to recreate the // getApplied method calls cssLogic.highlight(node) to recreate the
// style cache. Clients requesting getApplied from actors that have not // style cache. Clients requesting getApplied from actors that have not
// been fixed must make sure cssLogic.highlight(node) was called before. // been fixed must make sure cssLogic.highlight(node) was called before.
getAppliedCreatesStyleCache: true getAppliedCreatesStyleCache: true,
// Whether addNewRule accepts the editAuthored argument.
authoredStyles: true
} }
}; };
}, },
/**
* Called when a style sheet is updated.
*/
_styleApplied: function(kind, styleSheet) {
if (kind === UPDATE_GENERAL) {
events.emit(this, "stylesheet-updated", styleSheet);
}
},
/** /**
* Return or create a StyleRuleActor for the given item. * Return or create a StyleRuleActor for the given item.
* @param item Either a CSSStyleRule or a DOM element. * @param item Either a CSSStyleRule or a DOM element.
@ -202,6 +233,21 @@ var PageStyleActor = protocol.ActorClass({
return actor; return actor;
}, },
/**
* Update the association between a StyleRuleActor and its
* corresponding item. This is used when a StyleRuleActor updates
* as style sheet and starts using a new rule.
*
* @param oldItem The old association; either a CSSStyleRule or a
* DOM element.
* @param item Either a CSSStyleRule or a DOM element.
* @param actor a StyleRuleActor
*/
updateStyleRef: function(oldItem, item, actor) {
this.refMap.delete(oldItem);
this.refMap.set(item, actor);
},
/** /**
* Return or create a StyleSheetActor for the given nsIDOMCSSStyleSheet. * Return or create a StyleSheetActor for the given nsIDOMCSSStyleSheet.
* @param {DOMStyleSheet} sheet * @param {DOMStyleSheet} sheet
@ -212,6 +258,10 @@ var PageStyleActor = protocol.ActorClass({
_sheetRef: function(sheet) { _sheetRef: function(sheet) {
let tabActor = this.inspector.tabActor; let tabActor = this.inspector.tabActor;
let actor = tabActor.createStyleSheetActor(sheet); let actor = tabActor.createStyleSheetActor(sheet);
if (!this._watchedSheets.has(actor)) {
this._watchedSheets.add(actor);
actor.on("style-applied", this._styleApplied);
}
return actor; return actor;
}, },
@ -524,7 +574,7 @@ var PageStyleActor = protocol.ActorClass({
* `matchedSelectors`: Include an array of specific selectors that * `matchedSelectors`: Include an array of specific selectors that
* caused this rule to match its node. * caused this rule to match its node.
*/ */
getApplied: method(function(node, options) { getApplied: method(Task.async(function*(node, options) {
if (!node) { if (!node) {
return {entries: [], rules: [], sheets: []}; return {entries: [], rules: [], sheets: []};
} }
@ -532,8 +582,14 @@ var PageStyleActor = protocol.ActorClass({
this.cssLogic.highlight(node.rawNode); this.cssLogic.highlight(node.rawNode);
let entries = []; let entries = [];
entries = entries.concat(this._getAllElementRules(node, undefined, options)); entries = entries.concat(this._getAllElementRules(node, undefined, options));
return this.getAppliedProps(node, entries, options);
}, { let result = this.getAppliedProps(node, entries, options);
for (let rule of result.rules) {
// See the comment in |form| to understand this.
yield rule.getAuthoredCssText();
}
return result;
}), {
request: { request: {
node: Arg(0, "domnode"), node: Arg(0, "domnode"),
inherited: Option(1, "boolean"), inherited: Option(1, "boolean"),
@ -905,12 +961,17 @@ var PageStyleActor = protocol.ActorClass({
/** /**
* Adds a new rule, and returns the new StyleRuleActor. * Adds a new rule, and returns the new StyleRuleActor.
* @param NodeActor node * @param {NodeActor} node
* @param [string] pseudoClasses The list of pseudo classes to append to the * @param {String} pseudoClasses The list of pseudo classes to append to the
* new selector. * new selector.
* @returns StyleRuleActor of the new rule * @param {Boolean} editAuthored
* True if the selector should be updated by editing the
* authored text; false if the selector should be updated via
* CSSOM.
* @returns {StyleRuleActor} the new rule
*/ */
addNewRule: method(function(node, pseudoClasses) { addNewRule: method(Task.async(function*(node, pseudoClasses,
editAuthored = false) {
let style = this.styleElement; let style = this.styleElement;
let sheet = style.sheet; let sheet = style.sheet;
let cssRules = sheet.cssRules; let cssRules = sheet.cssRules;
@ -930,11 +991,22 @@ var PageStyleActor = protocol.ActorClass({
} }
let index = sheet.insertRule(selector + " {}", cssRules.length); let index = sheet.insertRule(selector + " {}", cssRules.length);
return this.getNewAppliedProps(node, cssRules.item(index));
}, { // If inserting the rule succeeded, go ahead and edit the source
// text if requested.
if (editAuthored) {
let sheetActor = this._sheetRef(sheet);
let {str: authoredText} = yield sheetActor.getText();
authoredText += "\n" + selector + " {\n" + "}";
yield sheetActor.update(authoredText, false);
}
return this.getNewAppliedProps(node, sheet.cssRules.item(index));
}), {
request: { request: {
node: Arg(0, "domnode"), node: Arg(0, "domnode"),
pseudoClasses: Arg(1, "nullable:array:string") pseudoClasses: Arg(1, "nullable:array:string"),
editAuthored: Arg(2, "boolean")
}, },
response: RetVal("appliedStylesReturn") response: RetVal("appliedStylesReturn")
}), }),
@ -966,6 +1038,10 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
return this.inspector.walker; return this.inspector.walker;
}, },
get supportsAuthoredStyles() {
return this._form.traits && this._form.traits.authoredStyles;
},
getMatchedSelectors: protocol.custom(function(node, property, options) { getMatchedSelectors: protocol.custom(function(node, property, options) {
return this._getMatchedSelectors(node, property, options).then(ret => { return this._getMatchedSelectors(node, property, options).then(ret => {
return ret.matched; return ret.matched;
@ -989,7 +1065,13 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
}), }),
addNewRule: protocol.custom(function(node, pseudoClasses) { addNewRule: protocol.custom(function(node, pseudoClasses) {
return this._addNewRule(node, pseudoClasses).then(ret => { let addPromise;
if (this.supportsAuthoredStyles) {
addPromise = this._addNewRule(node, pseudoClasses, true);
} else {
addPromise = this._addNewRule(node, pseudoClasses);
}
return addPromise.then(ret => {
return ret.entries[0]; return ret.entries[0];
}); });
}, { }, {
@ -1007,19 +1089,34 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
*/ */
var StyleRuleActor = protocol.ActorClass({ var StyleRuleActor = protocol.ActorClass({
typeName: "domstylerule", typeName: "domstylerule",
events: {
"location-changed": {
type: "locationChanged",
line: Arg(0, "number"),
column: Arg(1, "number")
},
},
initialize: function(pageStyle, item) { initialize: function(pageStyle, item) {
protocol.Actor.prototype.initialize.call(this, null); protocol.Actor.prototype.initialize.call(this, null);
this.pageStyle = pageStyle; this.pageStyle = pageStyle;
this.rawStyle = item.style; this.rawStyle = item.style;
this._parentSheet = null;
this._onStyleApplied = this._onStyleApplied.bind(this);
if (item instanceof (Ci.nsIDOMCSSRule)) { if (item instanceof (Ci.nsIDOMCSSRule)) {
this.type = item.type; this.type = item.type;
this.rawRule = item; this.rawRule = item;
if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule || if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule ||
this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) && this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) &&
this.rawRule.parentStyleSheet) { this.rawRule.parentStyleSheet) {
this.line = DOMUtils.getRuleLine(this.rawRule); this.line = DOMUtils.getRelativeRuleLine(this.rawRule);
this.column = DOMUtils.getRuleColumn(this.rawRule); this.column = DOMUtils.getRuleColumn(this.rawRule);
this._parentSheet = this.rawRule.parentStyleSheet;
this._computeRuleIndex();
this.sheetActor = this.pageStyle._sheetRef(this._parentSheet);
this.sheetActor.on("style-applied", this._onStyleApplied);
} }
} else { } else {
// Fake a rule // Fake a rule
@ -1047,6 +1144,9 @@ var StyleRuleActor = protocol.ActorClass({
this.pageStyle = null; this.pageStyle = null;
this.rawNode = null; this.rawNode = null;
this.rawRule = null; this.rawRule = null;
if (this.sheetActor) {
this.sheetActor.off("style-applied", this._onStyleApplied);
}
}, },
// Objects returned by this actor are owned by the PageStyleActor // Objects returned by this actor are owned by the PageStyleActor
@ -1055,6 +1155,17 @@ var StyleRuleActor = protocol.ActorClass({
return this.pageStyle; return this.pageStyle;
}, },
// True if this rule supports as-authored styles, meaning that the
// rule text can be rewritten using setRuleText.
get canSetRuleText() {
// Special case about:PreferenceStyleSheet, as it is
// generated on the fly and the URI is not registered with the
// about: handler.
// https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
return !!(this._parentSheet &&
this._parentSheet.href !== "about:PreferenceStyleSheet");
},
getDocument: function(sheet) { getDocument: function(sheet) {
let document; let document;
@ -1085,6 +1196,9 @@ var StyleRuleActor = protocol.ActorClass({
// Whether the style rule actor implements the modifySelector2 method // Whether the style rule actor implements the modifySelector2 method
// that allows for unmatched rule to be added // that allows for unmatched rule to be added
modifySelectorUnmatched: true, modifySelectorUnmatched: true,
// Whether the style rule actor implements the setRuleText
// method.
canSetRuleText: this.canSetRuleText,
} }
}; };
@ -1102,10 +1216,17 @@ var StyleRuleActor = protocol.ActorClass({
} }
} }
} }
if (this.rawRule.parentStyleSheet) { if (this._parentSheet) {
form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID; form.parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet).actorID;
} }
// One tricky thing here is that other methods in this actor must
// ensure that authoredText has been set before |form| is called.
// This has to be treated specially, for now, because we cannot
// synchronously compute the authored text, but |form| also cannot
// return a promise. See bug 1205868.
form.authoredText = this.authoredText;
switch (this.type) { switch (this.type) {
case Ci.nsIDOMCSSRule.STYLE_RULE: case Ci.nsIDOMCSSRule.STYLE_RULE:
form.selectors = CssLogic.getSelectors(this.rawRule); form.selectors = CssLogic.getSelectors(this.rawRule);
@ -1139,7 +1260,125 @@ var StyleRuleActor = protocol.ActorClass({
}, },
/** /**
* Modify a rule's properties. Passed an array of modifications: * Send an event notifying that the location of the rule has
* changed.
*
* @param {Number} line the new line number
* @param {Number} column the new column number
*/
_notifyLocationChanged: function(line, column) {
events.emit(this, "location-changed", line, column);
},
/**
* Compute the index of this actor's raw rule in its parent style
* sheet.
*/
_computeRuleIndex: function() {
let rule = this.rawRule;
let cssRules = this._parentSheet.cssRules;
this._ruleIndex = -1;
for (let i = 0; i < cssRules.length; i++) {
if (rule === cssRules.item(i)) {
this._ruleIndex = i;
break;
}
}
},
/**
* This is attached to the parent style sheet actor's
* "style-applied" event.
*/
_onStyleApplied: function(kind) {
if (kind === UPDATE_GENERAL) {
// A general change means that the rule actors are invalidated,
// so stop listening to events now.
if (this.sheetActor) {
this.sheetActor.off("style-applied", this._onStyleApplied);
}
} else if (this._ruleIndex >= 0) {
// The sheet was updated by this actor, in a way that preserves
// the rules. Now, recompute our new rule from the style sheet,
// so that we aren't left with a reference to a dangling rule.
let oldRule = this.rawRule;
this.rawRule = this._parentSheet.cssRules[this._ruleIndex];
// Also tell the page style so that future calls to _styleRef
// return the same StyleRuleActor.
this.pageStyle.updateStyleRef(oldRule, this.rawRule, this);
let line = DOMUtils.getRelativeRuleLine(this.rawRule);
let column = DOMUtils.getRuleColumn(this.rawRule);
if (line !== this.line || column !== this.column) {
this._notifyLocationChanged(line, column);
}
this.line = line;
this.column = column;
}
},
/**
* Return a promise that resolves to the authored form of a rule's
* text, if available. If the authored form is not available, the
* returned promise simply resolves to the empty string. If the
* authored form is available, this also sets |this.authoredText|.
* The authored text will include invalid and otherwise ignored
* properties.
*/
getAuthoredCssText: function() {
if (!this.canSetRuleText ||
(this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
return promise.resolve("");
}
if (typeof this.authoredText === "string") {
return promise.resolve(this.authoredText);
}
let parentStyleSheet =
this.pageStyle._sheetRef(this._parentSheet);
return parentStyleSheet.getText().then((longStr) => {
let cssText = longStr.str;
let {text} = getRuleText(cssText, this.line, this.column);
// Cache the result on the rule actor to avoid parsing again next time
this.authoredText = text;
return this.authoredText;
});
},
/**
* Set the contents of the rule. This rewrites the rule in the
* stylesheet and causes it to be re-evaluated.
*
* @param {String} newText the new text of the rule
* @returns the rule with updated properties
*/
setRuleText: method(Task.async(function*(newText) {
if (!this.canSetRuleText ||
(this.type !== Ci.nsIDOMCSSRule.STYLE_RULE &&
this.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE)) {
throw new Error("invalid call to setRuleText");
}
let parentStyleSheet = this.pageStyle._sheetRef(this._parentSheet);
let {str: cssText} = yield parentStyleSheet.getText();
let {offset, text} = getRuleText(cssText, this.line, this.column);
cssText = cssText.substring(0, offset) + newText +
cssText.substring(offset + text.length);
this.authoredText = newText;
yield parentStyleSheet.update(cssText, false, UPDATE_PRESERVING_RULES);
return this;
}), {
request: { modification: Arg(0, "string") },
response: { rule: RetVal("domstylerule") }
}),
/**
* Modify a rule's properties. Passed an array of modifications:
* { * {
* type: "set", * type: "set",
* name: <string>, * name: <string>,
@ -1163,7 +1402,7 @@ var StyleRuleActor = protocol.ActorClass({
if (this.rawNode) { if (this.rawNode) {
document = this.rawNode.ownerDocument; document = this.rawNode.ownerDocument;
} else { } else {
let parentStyleSheet = this.rawRule.parentStyleSheet; let parentStyleSheet = this._parentSheet;
while (parentStyleSheet.ownerRule && while (parentStyleSheet.ownerRule &&
parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) { parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet; parentStyleSheet = parentStyleSheet.ownerRule.parentStyleSheet;
@ -1196,39 +1435,61 @@ var StyleRuleActor = protocol.ActorClass({
* current rule. Returns the newly inserted css rule or null if the rule is * current rule. Returns the newly inserted css rule or null if the rule is
* unsuccessfully inserted to the parent style sheet. * unsuccessfully inserted to the parent style sheet.
* *
* @param string value * @param {String} value
* The new selector value * The new selector value
* @param {Boolean} editAuthored
* True if the selector should be updated by editing the
* authored text; false if the selector should be updated via
* CSSOM.
* *
* @returns CSSRule * @returns {CSSRule}
* The new CSS rule added * The new CSS rule added
*/ */
_addNewSelector: function(value) { _addNewSelector: Task.async(function*(value, editAuthored) {
let rule = this.rawRule; let rule = this.rawRule;
let parentStyleSheet = rule.parentStyleSheet; let parentStyleSheet = this._parentSheet;
let cssRules = parentStyleSheet.cssRules;
let cssText = rule.cssText;
let selectorText = rule.selectorText;
for (let i = 0; i < cssRules.length; i++) { // We know the selector modification is ok, so if the client asked
if (rule === cssRules.item(i)) { // for the authored text to be edited, do it now.
try { if (editAuthored) {
// Inserts the new style rule into the current style sheet and let document = this.getDocument(this._parentSheet);
// delete the current rule try {
let ruleText = cssText.slice(selectorText.length).trim(); document.querySelector(value);
parentStyleSheet.insertRule(value + " " + ruleText, i); } catch (e) {
parentStyleSheet.deleteRule(i + 1); return null;
return cssRules.item(i); }
} catch(e) {
// The selector could be invalid, or the rule could fail to insert. let sheetActor = this.pageStyle._sheetRef(parentStyleSheet);
// If that happens, the method returns null. let {str: authoredText} = yield sheetActor.getText();
let [startOffset, endOffset] = getSelectorOffsets(authoredText, this.line,
this.column);
authoredText = authoredText.substring(0, startOffset) + value +
authoredText.substring(endOffset);
yield sheetActor.update(authoredText, false, UPDATE_PRESERVING_RULES);
} else {
let cssRules = parentStyleSheet.cssRules;
let cssText = rule.cssText;
let selectorText = rule.selectorText;
for (let i = 0; i < cssRules.length; i++) {
if (rule === cssRules.item(i)) {
try {
// Inserts the new style rule into the current style sheet and
// delete the current rule
let ruleText = cssText.slice(selectorText.length).trim();
parentStyleSheet.insertRule(value + " " + ruleText, i);
parentStyleSheet.deleteRule(i + 1);
break;
} catch(e) {
// The selector could be invalid, or the rule could fail to insert.
return null;
}
} }
break;
} }
} }
return null; return parentStyleSheet.cssRules[this._ruleIndex];
}, }),
/** /**
* Modify the current rule's selector by inserting a new rule with the new * Modify the current rule's selector by inserting a new rule with the new
@ -1243,12 +1504,12 @@ var StyleRuleActor = protocol.ActorClass({
* Returns a boolean if the selector in the stylesheet was modified, * Returns a boolean if the selector in the stylesheet was modified,
* and false otherwise * and false otherwise
*/ */
modifySelector: method(function(value) { modifySelector: method(Task.async(function*(value) {
if (this.type === ELEMENT_STYLE) { if (this.type === ELEMENT_STYLE) {
return false; return false;
} }
let document = this.getDocument(this.rawRule.parentStyleSheet); let document = this.getDocument(this._parentSheet);
// Extract the selector, and pseudo elements and classes // Extract the selector, and pseudo elements and classes
let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/); let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/);
let selectorElement; let selectorElement;
@ -1262,11 +1523,11 @@ var StyleRuleActor = protocol.ActorClass({
// Check if the selector is valid and not the same as the original // Check if the selector is valid and not the same as the original
// selector // selector
if (selectorElement && this.rawRule.selectorText !== value) { if (selectorElement && this.rawRule.selectorText !== value) {
this._addNewSelector(value); yield this._addNewSelector(value, false);
return true; return true;
} }
return false; return false;
}, { }), {
request: { selector: Arg(0, "string") }, request: { selector: Arg(0, "string") },
response: { isModified: RetVal("boolean") }, response: { isModified: RetVal("boolean") },
}), }),
@ -1281,17 +1542,20 @@ var StyleRuleActor = protocol.ActorClass({
* selector matches the current element without having to refresh the whole * selector matches the current element without having to refresh the whole
* list. * list.
* *
* @param DOMNode node * @param {DOMNode} node
* The current selected element * The current selected element
* @param string value * @param {String} value
* The new selector value * The new selector value
* @returns Object * @param {Boolean} editAuthored
* True if the selector should be updated by editing the
* authored text; false if the selector should be updated via
* CSSOM.
* @returns {Object}
* Returns an object that contains the applied style properties of the * Returns an object that contains the applied style properties of the
* new rule and a boolean indicating whether or not the new selector * new rule and a boolean indicating whether or not the new selector
* matches the current selected element * matches the current selected element
*/ */
modifySelector2: method(function(node, value) { modifySelector2: method(function(node, value, editAuthored = false) {
let isMatching = false;
let ruleProps = null; let ruleProps = null;
if (this.type === ELEMENT_STYLE || if (this.type === ELEMENT_STYLE ||
@ -1299,23 +1563,40 @@ var StyleRuleActor = protocol.ActorClass({
return { ruleProps, isMatching: true }; return { ruleProps, isMatching: true };
} }
let newCssRule = this._addNewSelector(value); let selectorPromise = this._addNewSelector(value, editAuthored);
if (newCssRule) {
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule); if (editAuthored) {
selectorPromise = selectorPromise.then((newCssRule) => {
if (newCssRule) {
let style = this.pageStyle._styleRef(newCssRule);
// See the comment in |form| to understand this.
return style.getAuthoredCssText().then(() => newCssRule);
}
return newCssRule;
});
} }
// Determine if the new selector value matches the current selected element return selectorPromise.then((newCssRule) => {
try { if (newCssRule) {
isMatching = node.rawNode.matches(value); ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
} catch(e) { }
// This fails when value is an invalid selector.
}
return { ruleProps, isMatching }; // Determine if the new selector value matches the current
// selected element
let isMatching = false;
try {
isMatching = node.rawNode.matches(value);
} catch(e) {
// This fails when value is an invalid selector.
}
return { ruleProps, isMatching };
});
}, { }, {
request: { request: {
node: Arg(0, "domnode"), node: Arg(0, "domnode"),
value: Arg(1, "string") value: Arg(1, "string"),
editAuthored: Arg(2, "boolean")
}, },
response: RetVal("modifiedStylesReturn") response: RetVal("modifiedStylesReturn")
}) })
@ -1346,9 +1627,25 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
}, },
/** /**
* Return a new RuleModificationList for this node. * Ensure _form is updated when location-changed is emitted.
*/
_locationChangedPre: protocol.preEvent("location-changed", function(line,
column) {
this._clearOriginalLocation();
this._form.line = line;
this._form.column = column;
}),
/**
* Return a new RuleModificationList or RuleRewriter for this node.
* A RuleRewriter will be returned when the rule's canSetRuleText
* trait is true; otherwise a RuleModificationList will be
* returned.
*/ */
startModifyingProperties: function() { startModifyingProperties: function() {
if (this.canSetRuleText) {
return new RuleRewriter(this, this.authoredText);
}
return new RuleModificationList(this); return new RuleModificationList(this);
}, },
@ -1364,6 +1661,9 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
get cssText() { get cssText() {
return this._form.cssText; return this._form.cssText;
}, },
get authoredText() {
return this._form.authoredText || this._form.cssText;
},
get keyText() { get keyText() {
return this._form.keyText; return this._form.keyText;
}, },
@ -1416,6 +1716,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
return this._form.traits && this._form.traits.modifySelectorUnmatched; return this._form.traits && this._form.traits.modifySelectorUnmatched;
}, },
get canSetRuleText() {
return this._form.traits && this._form.traits.canSetRuleText;
},
get location() { get location() {
return { return {
source: this.parentStyleSheet, source: this.parentStyleSheet,
@ -1425,6 +1729,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
}; };
}, },
_clearOriginalLocation: function() {
this._originalLocation = null;
},
getOriginalLocation: function() { getOriginalLocation: function() {
if (this._originalLocation) { if (this._originalLocation) {
return promise.resolve(this._originalLocation); return promise.resolve(this._originalLocation);
@ -1458,7 +1766,11 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
let response; let response;
if (this.supportsModifySelectorUnmatched) { if (this.supportsModifySelectorUnmatched) {
// If the debugee supports adding unmatched rules (post FF41) // If the debugee supports adding unmatched rules (post FF41)
response = yield this.modifySelector2(node, value); if (this.canSetRuleText) {
response = yield this.modifySelector2(node, value, true);
} else {
response = yield this.modifySelector2(node, value);
}
} else { } else {
response = yield this._modifySelector(value); response = yield this._modifySelector(value);
} }
@ -1469,6 +1781,13 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
return response; return response;
}), { }), {
impl: "_modifySelector" impl: "_modifySelector"
}),
setRuleText: protocol.custom(function(newText) {
this._form.authoredText = newText;
return this._setRuleText(newText);
}, {
impl: "_setRuleText"
}) })
}); });
@ -1478,6 +1797,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
* list of modifications that will be applied to a StyleRuleActor. * list of modifications that will be applied to a StyleRuleActor.
* The modifications are processed in the order in which they are * The modifications are processed in the order in which they are
* added to the RuleModificationList. * added to the RuleModificationList.
*
* Objects of this type expose the same API as @see RuleRewriter.
* This lets the inspector use (mostly) the same code, regardless of
* whether the server implements setRuleText.
*/ */
var RuleModificationList = Class({ var RuleModificationList = Class({
/** /**
@ -1502,12 +1825,17 @@ var RuleModificationList = Class({
/** /**
* Add a "set" entry to the modification list. * Add a "set" entry to the modification list.
* *
* @param {string} name the property's name * @param {Number} index index of the property in the rule.
* @param {string} value the property's value * This can be -1 in the case where
* @param {string} priority the property's priority, either the empty * the rule does not support setRuleText;
* generally for setting properties
* on an element's style.
* @param {String} name the property's name
* @param {String} value the property's value
* @param {String} priority the property's priority, either the empty
* string or "important" * string or "important"
*/ */
setProperty: function(name, value, priority) { setProperty: function(index, name, value, priority) {
this.modifications.push({ this.modifications.push({
type: "set", type: "set",
name: name, name: name,
@ -1519,14 +1847,73 @@ var RuleModificationList = Class({
/** /**
* Add a "remove" entry to the modification list. * Add a "remove" entry to the modification list.
* *
* @param {string} name the name of the property to remove * @param {Number} index index of the property in the rule.
* This can be -1 in the case where
* the rule does not support setRuleText;
* generally for setting properties
* on an element's style.
* @param {String} name the name of the property to remove
*/ */
removeProperty: function(name) { removeProperty: function(index, name) {
this.modifications.push({ this.modifications.push({
type: "remove", type: "remove",
name: name name: name
}); });
} },
/**
* Rename a property. This implementation acts like
* |removeProperty|, because |setRuleText| is not available.
*
* @param {Number} index index of the property in the rule.
* This can be -1 in the case where
* the rule does not support setRuleText;
* generally for setting properties
* on an element's style.
* @param {String} name current name of the property
* @param {String} newName new name of the property
*/
renameProperty: function(index, name, newName) {
this.removeProperty(index, name);
},
/**
* Enable or disable a property. This implementation acts like
* |removeProperty| when disabling, or a no-op when enabling,
* because |setRuleText| is not available.
*
* @param {Number} index index of the property in the rule.
* This can be -1 in the case where
* the rule does not support setRuleText;
* generally for setting properties
* on an element's style.
* @param {String} name current name of the property
* @param {Boolean} isEnabled true if the property should be enabled;
* false if it should be disabled
*/
setPropertyEnabled: function(index, name, isEnabled) {
if (!isEnabled) {
this.removeProperty(index, name);
}
},
/**
* Create a new property. This implementation does nothing, because
* |setRuleText| is not available.
*
* @param {Number} index index of the property in the rule.
* This can be -1 in the case where
* the rule does not support setRuleText;
* generally for setting properties
* on an element's style.
* @param {String} name name of the new property
* @param {String} value value of the new property
* @param {String} priority priority of the new property; either
* the empty string or "important"
*/
createProperty: function(index, name, value, priority) {
// Nothing.
},
}); });
/** /**
@ -1659,6 +2046,47 @@ function getRuleText(initialText, line, column) {
exports.getRuleText = getRuleText; exports.getRuleText = getRuleText;
/**
* Compute the start and end offsets of a rule's selector text, given
* the CSS text and the line and column at which the rule begins.
* @param {String} initialText
* @param {Number} line (1-indexed)
* @param {Number} column (1-indexed)
* @return {array} An array with two elements: [startOffset, endOffset].
* The elements mark the bounds in |initialText| of
* the CSS rule's selector.
*/
function getSelectorOffsets(initialText, line, column) {
if (typeof line === "undefined" || typeof column === "undefined") {
throw new Error("Location information is missing");
}
let {offset: textOffset, text} =
getTextAtLineColumn(initialText, line, column);
let lexer = DOMUtils.getCSSLexer(text);
// Search forward for the opening brace.
let endOffset;
while (true) {
let token = lexer.nextToken();
if (!token) {
break;
}
if (token.tokenType === "symbol" && token.text === "{") {
if (endOffset === undefined) {
break;
}
return [textOffset, textOffset + endOffset];
}
// Preserve comments and whitespace just before the "{".
if (token.tokenType !== "comment" && token.tokenType !== "whitespace") {
endOffset = token.endOffset;
}
}
throw new Error("could not find bounds of rule");
}
/** /**
* Return the offset and substring of |text| that starts at the given * Return the offset and substring of |text| that starts at the given
* line and column. * line and column.

View File

@ -23,6 +23,11 @@ const {SourceMapConsumer} = require("source-map");
loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/styleinspector/css-logic").CssLogic); loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/styleinspector/css-logic").CssLogic);
const {
getIndentationFromPrefs,
getIndentationFromString
} = require("devtools/shared/shared/indentation");
var TRANSITION_CLASS = "moz-styleeditor-transitioning"; var TRANSITION_CLASS = "moz-styleeditor-transitioning";
var TRANSITION_DURATION_MS = 500; var TRANSITION_DURATION_MS = 500;
var TRANSITION_BUFFER_MS = 1000; var TRANSITION_BUFFER_MS = 1000;
@ -40,6 +45,22 @@ var LOAD_ERROR = "error-load";
types.addActorType("stylesheet"); types.addActorType("stylesheet");
types.addActorType("originalsource"); types.addActorType("originalsource");
// The possible kinds of style-applied events.
// UPDATE_PRESERVING_RULES means that the update is guaranteed to
// preserve the number and order of rules on the style sheet.
// UPDATE_GENERAL covers any other kind of change to the style sheet.
const UPDATE_PRESERVING_RULES = 0;
exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES;
const UPDATE_GENERAL = 1;
exports.UPDATE_GENERAL = UPDATE_GENERAL;
// If the user edits a style sheet, we stash a copy of the edited text
// here, keyed by the style sheet. This way, if the tools are closed
// and then reopened, the edited text will be available. A weak map
// is used so that navigation by the user will eventually cause the
// edited text to be collected.
let modifiedStyleSheets = new WeakMap();
/** /**
* Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
* stylesheets of a document. * stylesheets of a document.
@ -385,7 +406,9 @@ var StyleSheetActor = protocol.ActorClass({
value: Arg(1, "json") value: Arg(1, "json")
}, },
"style-applied" : { "style-applied" : {
type: "styleApplied" type: "styleApplied",
kind: Arg(0, "number"),
styleSheet: Arg(1, "stylesheet")
}, },
"media-rules-changed" : { "media-rules-changed" : {
type: "mediaRulesChanged", type: "mediaRulesChanged",
@ -597,6 +620,12 @@ var StyleSheetActor = protocol.ActorClass({
return promise.resolve(this.text); return promise.resolve(this.text);
} }
let cssText = modifiedStyleSheets.get(this.rawSheet);
if (cssText !== undefined) {
this.text = cssText;
return promise.resolve(cssText);
}
if (!this.href) { if (!this.href) {
// this is an inline <style> sheet // this is an inline <style> sheet
let content = this.ownerNode.textContent; let content = this.ownerNode.textContent;
@ -872,19 +901,22 @@ var StyleSheetActor = protocol.ActorClass({
* @param {object} request * @param {object} request
* 'text' - new text * 'text' - new text
* 'transition' - whether to do CSS transition for change. * 'transition' - whether to do CSS transition for change.
* 'kind' - either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
*/ */
update: method(function(text, transition) { update: method(function(text, transition, kind = UPDATE_GENERAL) {
DOMUtils.parseStyleSheet(this.rawSheet, text); DOMUtils.parseStyleSheet(this.rawSheet, text);
modifiedStyleSheets.set(this.rawSheet, text);
this.text = text; this.text = text;
this._notifyPropertyChanged("ruleCount"); this._notifyPropertyChanged("ruleCount");
if (transition) { if (transition) {
this._insertTransistionRule(); this._insertTransistionRule(kind);
} }
else { else {
events.emit(this, "style-applied"); events.emit(this, "style-applied", kind, this);
} }
this._getMediaRules().then((rules) => { this._getMediaRules().then((rules) => {
@ -901,7 +933,7 @@ var StyleSheetActor = protocol.ActorClass({
* Insert a catch-all transition rule into the document. Set a timeout * Insert a catch-all transition rule into the document. Set a timeout
* to remove the rule after a certain time. * to remove the rule after a certain time.
*/ */
_insertTransistionRule: function() { _insertTransistionRule: function(kind) {
this.document.documentElement.classList.add(TRANSITION_CLASS); this.document.documentElement.classList.add(TRANSITION_CLASS);
// We always add the rule since we've just reset all the rules // We always add the rule since we've just reset all the rules
@ -910,7 +942,7 @@ var StyleSheetActor = protocol.ActorClass({
// Set up clean up and commit after transition duration (+buffer) // Set up clean up and commit after transition duration (+buffer)
// @see _onTransitionEnd // @see _onTransitionEnd
this.window.clearTimeout(this._transitionTimeout); this.window.clearTimeout(this._transitionTimeout);
this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this), this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this, kind),
TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS); TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
}, },
@ -918,7 +950,7 @@ var StyleSheetActor = protocol.ActorClass({
* This cleans up class and rule added for transition effect and then * This cleans up class and rule added for transition effect and then
* notifies that the style has been applied. * notifies that the style has been applied.
*/ */
_onTransitionEnd: function() _onTransitionEnd: function(kind)
{ {
this.document.documentElement.classList.remove(TRANSITION_CLASS); this.document.documentElement.classList.remove(TRANSITION_CLASS);
@ -928,7 +960,7 @@ var StyleSheetActor = protocol.ActorClass({
this.rawSheet.deleteRule(index); this.rawSheet.deleteRule(index);
} }
events.emit(this, "style-applied"); events.emit(this, "style-applied", kind, this);
} }
}) })
@ -981,6 +1013,29 @@ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
}, },
get ruleCount() { get ruleCount() {
return this._form.ruleCount; return this._form.ruleCount;
},
/**
* Get the indentation to use for edits to this style sheet.
*
* @return {Promise} A promise that will resolve to a string that
* should be used to indent a block in this style sheet.
*/
guessIndentation: function() {
let prefIndent = getIndentationFromPrefs();
if (prefIndent) {
let {indentUnit, indentWithTabs} = prefIndent;
return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit));
}
return Task.spawn(function*() {
let longStr = yield this.getText();
let source = yield longStr.string();
let {indentUnit, indentWithTabs} = getIndentationFromString(source);
return indentWithTabs ? "\t" : " ".repeat(indentUnit);
}.bind(this));
} }
}); });

View File

@ -52,13 +52,13 @@ addTest(function modifyProperties() {
let changes = elementStyle.startModifyingProperties(); let changes = elementStyle.startModifyingProperties();
// Change an existing property... // Change an existing property...
changes.setProperty("color", "black"); changes.setProperty(-1, "color", "black");
// Create a new property // Create a new property
changes.setProperty("background-color", "green"); changes.setProperty(-1, "background-color", "green");
// Create a new property and then change it immediately. // Create a new property and then change it immediately.
changes.setProperty("border", "1px solid black"); changes.setProperty(-1, "border", "1px solid black");
changes.setProperty("border", "2px solid black"); changes.setProperty(-1, "border", "2px solid black");
return changes.apply(); return changes.apply();
}).then(() => { }).then(() => {
@ -67,9 +67,9 @@ addTest(function modifyProperties() {
// Remove all the properties // Remove all the properties
let changes = elementStyle.startModifyingProperties(); let changes = elementStyle.startModifyingProperties();
changes.removeProperty("color"); changes.removeProperty(-1, "color");
changes.removeProperty("background-color"); changes.removeProperty(-1, "background-color");
changes.removeProperty("border"); changes.removeProperty(-1, "border");
return changes.apply(); return changes.apply();
}).then(() => { }).then(() => {

View File

@ -853,7 +853,7 @@ pref("devtools.debugger.forbid-certified-apps", true);
pref("devtools.apps.forbidden-permissions", "embed-apps,engineering-mode,embed-widgets"); pref("devtools.apps.forbidden-permissions", "embed-apps,engineering-mode,embed-widgets");
// DevTools default color unit // DevTools default color unit
pref("devtools.defaultColorUnit", "hex"); pref("devtools.defaultColorUnit", "authored");
// Used for devtools debugging // Used for devtools debugging
pref("devtools.dump.emit", false); pref("devtools.dump.emit", false);