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);
// DevTools default color unit
pref("devtools.defaultColorUnit", "hex");
pref("devtools.defaultColorUnit", "authored");
// Enable the Responsive UI tool
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. -->
<!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
- 'Default color unit' dropdown list and is visible in the options panel. -->
<!ENTITY options.defaultColorUnit.hex "Hex">

View File

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

View File

@ -95,9 +95,9 @@ EditingSession.prototype = {
}
if (property.value == "") {
modifications.removeProperty(property.name);
modifications.removeProperty(-1, property.name);
} 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) {
if (value != "") {
modifications.setProperty(property, value, "");
modifications.setProperty(-1, property, value, "");
} 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
// describing the expected result. If an element of |segments| is a
// string, it is simply appended to the expected string. Otherwise,
// it must be an object with a |value| property and a |name| property.
// These describe the color and are both used in the generated
// 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").
// it must be an object with a |value| property, which is the color
// name as it appears in the input.
//
// This approach is taken to reduce boilerplate and to make it simpler
// to modify the test when the parseCssProperty output changes.
@ -53,10 +49,10 @@ function makeColorTest(name, value, segments) {
if (typeof (segment) === "string") {
result.expected += segment;
} else {
result.expected += "<span data-color=\"" + segment.value + "\">" +
result.expected += "<span data-color=\"" + segment.name + "\">" +
"<span style=\"background-color:" + segment.name +
"\" 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) {
let tests = [
makeColorTest("border", "1px solid red",
["1px solid ", {name: "red", value: "#F00"}]),
["1px solid ", {name: "red"}]),
makeColorTest("background-image",
"linear-gradient(to right, #F60 10%, rgba(0,0,0,1))",
["linear-gradient(to right, ", {name: "#F60", value: "#F60"},
" 10%, ", {name: "rgba(0,0,0,1)", value: "#000"},
["linear-gradient(to right, ", {name: "#F60"},
" 10%, ", {name: "rgba(0,0,0,1)"},
")"]),
// In "arial black", "black" is a font, not a color.
makeColorTest("font-family", "arial black", ["arial black"]),
makeColorTest("box-shadow", "0 0 1em red",
["0 0 1em ", {name: "red", value: "#F00"}]),
["0 0 1em ", {name: "red"}]),
makeColorTest("box-shadow",
"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 ",
{name: "rgba(0,0,0,.5)", value: "rgba(0,0,0,.5)"}]),
{name: "rgba(0,0,0,.5)"}]),
makeColorTest("content", "\"red\"", ["\"red\""]),
@ -98,7 +94,7 @@ function testParseCssProperty(doc, parser) {
["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
"url(red.svg#blue)\"><span>",
"blur(1px) drop-shadow(0 0 0 ",
{name: "blue", value: "#00F"},
{name: "blue"},
") url(red.svg#blue)</span></span>"]),
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
if (this.activeSwatch) {
this.currentSwatchColor = this.activeSwatch.nextSibling;
this._colorUnit =
colorUtils.classifyColor(this.currentSwatchColor.textContent);
let color = this.activeSwatch.style.backgroundColor;
this.spectrum.then(spectrum => {
spectrum.off("changed", this._onSpectrumColorChange);
@ -1222,6 +1224,7 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
_toDefaultType: function(color) {
let colorObj = new colorUtils.CssColor(color);
colorObj.colorUnit = this._colorUnit;
return colorObj.toString();
},

View File

@ -86,6 +86,12 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
this.walker = walker;
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
text: "",
selection: {
@ -103,7 +109,8 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
this._onPropertyChange = this._onPropertyChange.bind(this);
this._onError = this._onError.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.markLinkedFileBroken = this.markLinkedFileBroken.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.on("media-rules-changed", this._onMediaRulesChanged);
this.cssSheet.on("style-applied", this._onStyleApplied);
this.savedFile = file;
this.linkCSSFile();
}
@ -244,6 +252,27 @@ StyleSheetEditor.prototype = {
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.
*
@ -251,19 +280,11 @@ StyleSheetEditor.prototype = {
* A promise that'll resolve with the source text once the source
* has been loaded or reject on unexpected error.
*/
fetchSource: function () {
return Task.spawn(function* () {
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;
fetchSource: function() {
return this._getSourceTextAndPrettify().then((source) => {
this.sourceLoaded = true;
return source;
}.bind(this)).then(null, e => {
}).then(null, e => {
if (this._isDestroyed) {
console.warn("Could not fetch the source for " +
this.styleSheet.href +
@ -323,6 +344,26 @@ StyleSheetEditor.prototype = {
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.
* Emits 'media-rules-changed' if the list has changed.
@ -489,6 +530,11 @@ StyleSheetEditor.prototype = {
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
// (stylesheet is enabled) so that 'missed' updates
// while the stylesheet is disabled can be performed
@ -498,6 +544,7 @@ StyleSheetEditor.prototype = {
this._state.text = this.sourceEditor.getText();
}
this._isUpdating = true;
this.styleSheet.update(this._state.text, this.transitionsEnabled)
.then(null, Cu.reportError);
},
@ -726,10 +773,11 @@ StyleSheetEditor.prototype = {
}
this.cssSheet.off("property-change", this._onPropertyChange);
this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
this.cssSheet.off("style-applied", this._onStyleApplied);
this.styleSheet.off("error", this._onError);
this._isDestroyed = true;
}
}
};
/**
* 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.html
doc_xulpage.xul
sync.html
[browser_styleeditor_autocomplete.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_watching.js]
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_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
* opens style editor in it.
* Open the style editor for the current tab.
*/
var openStyleEditorForURL = Task.async(function* (url, win) {
let tab = yield addTab(url, win);
var openStyleEditor = Task.async(function*(tab) {
if (!tab) {
tab = gBrowser.selectedTab;
}
let target = TargetFactory.forTab(tab);
let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
let panel = toolbox.getPanel("styleeditor");
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.
* Maintains a list of TextProperty objects.
* TextProperty:
* Manages a single property from the cssText attribute of the
* Manages a single property from the authoredText attribute of the
* relevant declaration.
* Maintains a list of computed properties that come from this
* property declaration.
@ -183,6 +183,12 @@ ElementStyle.prototype = {
}
this.destroyed = true;
for (let rule of this.rules) {
if (rule.editor) {
rule.editor.destroy();
}
}
this.dummyElement = null;
this.dummyElementPromise.then(dummyElement => {
dummyElement.remove();
@ -226,12 +232,12 @@ ElementStyle.prototype = {
// Store the current list of rules (if any) during the population
// process. They will be reused if possible.
this._refreshRules = this.rules;
let existingRules = this.rules;
this.rules = [];
for (let entry of entries) {
this._maybeAddRule(entry);
this._maybeAddRule(entry, existingRules);
}
// Mark overridden computed styles.
@ -240,7 +246,11 @@ ElementStyle.prototype = {
this._sortRulesForPseudoElement();
// 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 => {
// populate is often called after a setTimeout,
@ -269,9 +279,12 @@ ElementStyle.prototype = {
*
* @param {Object} options
* 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.
*/
_maybeAddRule: function(options) {
_maybeAddRule: function(options, existingRules) {
// If we've already included this domRule (for example, when a
// common selector is inherited), ignore it.
if (options.rule &&
@ -287,13 +300,12 @@ ElementStyle.prototype = {
// If we're refreshing and the rule previously existed, reuse the
// Rule object.
if (this._refreshRules) {
for (let r of this._refreshRules) {
if (r.matches(options)) {
rule = r;
rule.refresh(options);
break;
}
if (existingRules) {
let ruleIndex = existingRules.findIndex((r) => r.matches(options));
if (ruleIndex >= 0) {
rule = existingRules[ruleIndex];
rule.refresh(options);
existingRules.splice(ruleIndex, 1);
}
}
@ -302,8 +314,8 @@ ElementStyle.prototype = {
rule = new Rule(this, options);
}
// Ignore inherited rules with no properties.
if (options.inherited && rule.textProps.length === 0) {
// Ignore inherited rules with no visible properties.
if (options.inherited && !rule.hasAnyVisibleProperties()) {
return false;
}
@ -372,6 +384,16 @@ ElementStyle.prototype = {
let taken = {};
for (let computedProp of computedProps) {
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;
if (earlier &&
computedProp.priority === "important" &&
@ -463,7 +485,7 @@ function Rule(elementStyle, options) {
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.
this.textProps = this._getTextProperties();
this.textProps = this.textProps.concat(this._getDisabledProperties());
@ -473,17 +495,12 @@ Rule.prototype = {
mediaText: "",
get title() {
if (this._title) {
return this._title;
}
this._title = CssLogic.shortSource(this.sheet);
let title = CssLogic.shortSource(this.sheet);
if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
this._title += ":" + this.ruleLine;
title += ":" + this.ruleLine;
}
this._title = this._title +
(this.mediaText ? " @media " + this.mediaText : "");
return this._title;
return title + (this.mediaText ? " @media " + this.mediaText : "");
},
get inheritedSource() {
@ -551,10 +568,6 @@ Rule.prototype = {
* both the full and short version of the source string.
*/
getOriginalSourceStrings: function() {
if (this._originalSourceStrings) {
return promise.resolve(this._originalSourceStrings);
}
return this.domRule.getOriginalLocation().then(({href, line, mediaText}) => {
let mediaString = mediaText ? " @" + mediaText : "";
@ -564,7 +577,6 @@ Rule.prototype = {
short: CssLogic.shortSource({href: href}) + ":" + line + mediaString
};
this._originalSourceStrings = sourceStrings;
return sourceStrings;
});
},
@ -595,31 +607,35 @@ Rule.prototype = {
createProperty: function(name, value, priority, siblingProp) {
let prop = new TextProperty(this, name, value, priority);
let ind;
if (siblingProp) {
let ind = this.textProps.indexOf(siblingProp);
this.textProps.splice(ind + 1, 0, prop);
ind = this.textProps.indexOf(siblingProp) + 1;
this.textProps.splice(ind, 0, prop);
} else {
ind = this.textProps.length;
this.textProps.push(prop);
}
this.applyProperties();
this.applyProperties((modifications) => {
modifications.createProperty(ind, name, value, priority);
});
return prop;
},
/**
* Reapply all the properties in this rule, and update their
* computed styles. Store disabled properties in the element
* style's store. Will re-mark overridden properties.
* Helper function for applyProperties that is called when the actor
* does not support as-authored styles. Store disabled properties
* in the element style's store.
*/
applyProperties: function(modifications) {
_applyPropertiesNoAuthored: function(modifications) {
this.elementStyle.markOverriddenAll();
if (!modifications) {
modifications = this.style.startModifyingProperties();
}
let disabledProps = [];
for (let prop of this.textProps) {
if (prop.invisible) {
continue;
}
if (!prop.enabled) {
disabledProps.push({
name: prop.name,
@ -632,7 +648,7 @@ Rule.prototype = {
continue;
}
modifications.setProperty(prop.name, prop.value, prop.priority);
modifications.setProperty(-1, prop.name, prop.value, prop.priority);
prop.updateComputed();
}
@ -645,9 +661,9 @@ Rule.prototype = {
disabled.delete(this.style);
}
let modificationsPromise = modifications.apply().then(() => {
return modifications.apply().then(() => {
let cssProps = {};
for (let cssProp of parseDeclarations(this.style.cssText)) {
for (let cssProp of parseDeclarations(this.style.authoredText)) {
cssProps[cssProp.name] = cssProp;
}
@ -667,18 +683,67 @@ Rule.prototype = {
textProp.priority = cssProp.priority;
}
});
},
this.elementStyle.markOverriddenAll();
if (modificationsPromise === this._applyingModifications) {
this._applyingModifications = null;
/**
* A helper for applyProperties that applies properties in the "as
* authored" case; that is, when the StyleRuleActor supports
* 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;
return modificationsPromise;
if (resultPromise === this._applyingModifications) {
this._applyingModifications = null;
this.elementStyle._changed();
}
}).catch(promiseWarn);
this._applyingModifications = resultPromise;
return resultPromise;
},
/**
@ -693,10 +758,12 @@ Rule.prototype = {
if (name === property.name) {
return;
}
let modifications = this.style.startModifyingProperties();
modifications.removeProperty(property.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.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) {
let modifications = this.style.startModifyingProperties();
modifications.setProperty(property.name, value, priority);
modifications.setProperty(this.textProps.indexOf(property),
property.name, value, priority);
modifications.apply().then(() => {
// Ensure dispatching a ruleview-changed event
// also for previews
@ -748,12 +820,14 @@ Rule.prototype = {
* @param {Boolean} value
*/
setPropertyEnabled: function(property, value) {
property.enabled = !!value;
let modifications = this.style.startModifyingProperties();
if (!property.enabled) {
modifications.removeProperty(property.name);
if (property.enabled === !!value) {
return;
}
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
*/
removeProperty: function(property) {
this.textProps = this.textProps.filter(prop => prop !== property);
let modifications = this.style.startModifyingProperties();
modifications.removeProperty(property.name);
let index = this.textProps.indexOf(property);
this.textProps.splice(index, 1);
// Need to re-apply properties in case removing this TextProperty
// exposes another one.
this.applyProperties(modifications);
this.applyProperties((modifications) => {
modifications.removeProperty(index, property.name);
});
},
/**
* Get the list of TextProperties from the style. Needs
* to parse the style's cssText.
* to parse the style's authoredText.
*/
_getTextProperties: function() {
let textProps = [];
let store = this.elementStyle.store;
let props = parseDeclarations(this.style.cssText);
let props = parseDeclarations(this.style.authoredText, true);
for (let prop of props) {
let name = prop.name;
if (this.inherited && !domUtils.isInheritedProperty(name)) {
continue;
}
// In an inherited rule, we only show inherited properties.
// 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,
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);
}
@ -864,7 +943,7 @@ Rule.prototype = {
/**
* 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.
*
* When choosing the best match to reuse, properties will be chosen
@ -880,7 +959,7 @@ Rule.prototype = {
*
* @param {TextProperty} newProp
* 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
* were updated.
*/
@ -953,18 +1032,26 @@ Rule.prototype = {
let index = this.textProps.indexOf(textProperty);
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();
} else {
let nextProp = this.textProps[index + 1];
nextProp.editor.nameSpan.click();
this.textProps[index].editor.nameSpan.click();
}
} 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();
} else {
let prevProp = this.textProps[index - 1];
prevProp.editor.valueSpan.click();
this.textProps[index].editor.valueSpan.click();
}
}
},
@ -978,15 +1065,31 @@ Rule.prototype = {
let terminator = osString === "WINNT" ? "\r\n" : "\n";
for (let textProp of this.textProps) {
cssText += "\t" + textProp.stringifyProperty() + terminator;
if (!textProp.invisible) {
cssText += "\t" + textProp.stringifyProperty() + terminator;
}
}
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
* The rule this TextProperty came from.
@ -996,13 +1099,22 @@ Rule.prototype = {
* The property's value (not including priority).
* @param {String} priority
* 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.name = name;
this.value = value;
this.priority = priority;
this.enabled = true;
this.enabled = !!enabled;
this.invisible = invisible;
this.updateComputed();
}
@ -1087,6 +1199,17 @@ TextProperty.prototype = {
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) {
let store = this.rule.elementStyle.store;
@ -1122,6 +1245,33 @@ TextProperty.prototype = {
}
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 element = elementStyle.element;
let rules = elementStyle.rules;
let client = this.inspector.toolbox._target.client;
let pseudoClasses = element.pseudoClassLocks;
if (!client.traits.addNewRule) {
return;
}
this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
let newRule = new Rule(elementStyle, options);
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
*/
@ -2146,7 +2330,7 @@ CssRuleView.prototype = {
// Highlight search matches in the rule properties
for (let textProp of rule.textProps) {
if (this._highlightProperty(textProp.editor)) {
if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
isHighlighted = true;
}
}
@ -2435,11 +2619,18 @@ function RuleEditor(aRuleView, aRule) {
this._onNewProperty = this._onNewProperty.bind(this);
this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
this._onSelectorDone = this._onSelectorDone.bind(this);
this._locationChanged = this._locationChanged.bind(this);
this.rule.domRule.on("location-changed", this._locationChanged);
this._create();
}
RuleEditor.prototype = {
destroy: function() {
this.rule.domRule.off("location-changed");
},
get isSelectorEditable() {
let toolbox = this.ruleView.inspector.toolbox;
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() {
let sourceLabel = this.element.querySelector(".ruleview-rule-source-label");
let title = this.rule.title;
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 : "";
sourceLabel.setAttribute("tooltiptext", sourceHref + sourceLine);
if (this.rule.isSystem) {
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
// fly and the URI is not registered with the about: handler.
@ -2575,7 +2775,7 @@ RuleEditor.prototype = {
sourceLabel.removeAttribute("tooltiptext");
}
} else {
sourceLabel.setAttribute("value", this.rule.title);
sourceLabel.setAttribute("value", title);
if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
sourceLabel.parentNode.setAttribute("unselectable", "true");
}
@ -2654,7 +2854,7 @@ RuleEditor.prototype = {
}
for (let prop of this.rule.textProps) {
if (!prop.editor) {
if (!prop.editor && !prop.invisible) {
let editor = new TextPropertyEditor(this, prop);
this.propertyList.appendChild(editor.element);
}
@ -2862,13 +3062,13 @@ RuleEditor.prototype = {
rules.splice(rules.indexOf(this.rule), 1);
rules.push(newRule);
elementStyle._changed();
elementStyle.markOverriddenAll();
editor.element.setAttribute("unmatched", !isMatching);
this.element.parentNode.replaceChild(editor.element, this.element);
// Remove highlight for modified selector
if (ruleView.highlightedSelector &&
ruleView.highlightedSelector === this.rule.selectorText) {
if (ruleView.highlightedSelector) {
ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
ruleView.highlightedSelector);
}
@ -3172,7 +3372,8 @@ TextPropertyEditor.prototype = {
!this.isValid() ||
!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");
} else {
this.element.classList.remove("ruleview-overridden");
@ -3502,8 +3703,10 @@ TextPropertyEditor.prototype = {
this.committed.value === val.value &&
this.committed.priority === val.priority;
// 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) {
this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
val.priority);
this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
return;
}
@ -3555,7 +3758,7 @@ TextPropertyEditor.prototype = {
* value of this property before editing.
*/
_onSwatchRevert: function() {
this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
this._previewValue(this.prop.value);
this.update();
},
@ -3631,7 +3834,7 @@ TextPropertyEditor.prototype = {
* @return {Boolean} true if the property value is valid, false otherwise.
*/
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.target.on("navigate", this.clearUserProperties);
this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.onSelected();
}
@ -152,6 +153,9 @@ RuleViewTool.prototype = {
this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.target.off("navigate", this.clearUserProperties);
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-changed", this.onPropertyChanged);
@ -179,6 +183,7 @@ function ComputedViewTool(inspector, window) {
this.inspector.on("layout-change", this.refresh);
this.inspector.selection.on("pseudoclass", this.refresh);
this.inspector.sidebar.on("computedview-selected", this.onPanelSelected);
this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
this.view.selectElement(null);
@ -244,6 +249,9 @@ ComputedViewTool.prototype = {
this.inspector.selection.off("pseudoclass", this.refresh);
this.inspector.selection.off("new-node-front", this.onSelected);
this.inspector.sidebar.off("computedview-selected", this.onPanelSelected);
if (this.inspector.pageStyle) {
this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
}
this.view.destroy();

View File

@ -19,6 +19,7 @@ support-files =
doc_matched_selectors.html
doc_media_queries.html
doc_pseudoelement.html
doc_ruleLineNumbers.html
doc_sourcemaps.css
doc_sourcemaps.css.map
doc_sourcemaps.html
@ -60,6 +61,7 @@ support-files =
[browser_ruleview_add-rule_03.js]
[browser_ruleview_add-rule_04.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_02.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-revert-on-ESC.js]
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_02.js]
[browser_ruleview_inherited-properties_03.js]
[browser_ruleview_keybindings.js]
[browser_ruleview_keyframes-rule_01.js]
[browser_ruleview_keyframes-rule_02.js]
[browser_ruleview_lineNumbers.js]
[browser_ruleview_livepreview.js]
[browser_ruleview_mark_overridden_01.js]
[browser_ruleview_mark_overridden_02.js]
[browser_ruleview_mark_overridden_03.js]
[browser_ruleview_mark_overridden_04.js]
[browser_ruleview_mark_overridden_05.js]
[browser_ruleview_mark_overridden_06.js]
[browser_ruleview_mark_overridden_07.js]
[browser_ruleview_mathml-element.js]
[browser_ruleview_media-queries.js]

View File

@ -34,7 +34,13 @@ function checkColorCycling(container, inspector) {
let valueNode = container.querySelector(".computedview-color");
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.");
// HSL
@ -55,15 +61,9 @@ function checkColorCycling(container, inspector) {
is(valueNode.textContent, "red",
"Color displayed as a color name.");
// "Authored" (currently the computed value)
// Back to "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,
{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("value" in nodeInfo.value);
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("value" in nodeInfo.value);
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) {
is(nodeInfo.type, VIEW_NODE_VALUE_TYPE);
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");
is(fontSize, "15px", "The computed view shows the updated font-size");
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 " +
"clipboard value");
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-size: 16px;[\\r\\n]+" +
"font-variant-caps: small-caps;[\\r\\n]*";

View File

@ -69,6 +69,6 @@ function* testCreateNew(view) {
yield onModifications;
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");
}

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));
let {view} = yield openRuleView();
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");
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");
swatch = value.querySelector(".ruleview-colorswatch");
is(swatch.style.backgroundColor, "rgb(0, 0, 0)",
is(swatch.style.backgroundColor, "black",
"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");
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++) {
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 win = inspector.sidebar.getWindowForTab("ruleview");
// Hex (default)
// Hex
is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
// HSL
@ -48,15 +48,16 @@ function checkColorCycling(container, inspector) {
is(valueNode.textContent, "red",
"Color displayed as a color name.");
// "Authored" (currently the computed value)
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Back to hex
// "Authored"
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
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");
info("Focus the property value and remove the property");
let onChanged = view.once("ruleview-changed");
yield sendKeysAndWaitForFocus(view, ruleEditor.element,
["VK_TAB", "VK_DELETE", "VK_RETURN"]);
yield ruleEditor.rule._applyingModifications;
yield onChanged;
newValue = yield executeInContent("Test:GetRulePropertyValue", {
styleSheetIndex: 0,

View File

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

View File

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

View File

@ -10,7 +10,7 @@
const TEST_URI = `
<style type='text/css'>
#testid {
background-color: red;
background-color: #f00;
}
</style>
<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");
editor.input.value = "center";
let onBlur = once(editor.input, "blur");
onRuleViewChanged = view.once("ruleview-changed");
onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
editor.input.blur();
yield onBlur;
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",
"Inherited rule should be the one that includes inheritable properties.");
ok(!!inheritRule.inherited, "Rule should consider itself inherited.");
is(inheritRule.textProps.length, 1,
"Should only display one inherited style");
let inheritProp = inheritRule.textProps[0];
is(inheritRule.textProps.length, 2,
"Rule should have two styles");
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.");
}

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);
yield createNewRuleViewProperty(ruleEditor, "background-color: red;");
yield ruleEditor.rule._applyingModifications;
let firstProp = ruleEditor.rule.textProps[0];
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-left", value: "1px", 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) {

View File

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

View File

@ -4,7 +4,7 @@
"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
const TEST_URI = "<div>Test Element</div>";
@ -16,24 +16,12 @@ add_task(function*() {
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) {
let ruleEditor = getRuleViewRuleEditor(view, 0);
let onMutation = inspector.once("markupmutation");
// There is 5 rule-view updates, one for the rule view creation,
// one for each new property
let onRuleViewChanged = waitRuleViewChanged(view, 5);
// There are 2 rule-view updates: one for the preview and one for
// the final commit.
let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
yield createNewRuleViewProperty(ruleEditor,
"color:blue;background : orange ; text-align:center; border-color: ");
yield onMutation;

View File

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

View File

@ -131,8 +131,7 @@ function* testPropertyChange6(inspector, ruleView, testElement) {
"Added a property");
validateTextProp(rule.textProps[4], true, "background",
"red url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%",
"shortcut property correctly set",
"#F00 url(\"chrome://branding/content/about-logo.png\") repeat scroll 0% 0%");
"shortcut property correctly set");
}
function* changeElementStyle(testElement, style, inspector) {
@ -141,8 +140,7 @@ function* changeElementStyle(testElement, style, inspector) {
yield onRefreshed;
}
function validateTextProp(aProp, aEnabled, aName, aValue, aDesc,
valueSpanText) {
function validateTextProp(aProp, aEnabled, aName, aValue, aDesc) {
is(aProp.enabled, aEnabled, aDesc + ": enabled.");
is(aProp.name, aName, aDesc + ": name.");
is(aProp.value, aValue, aDesc + ": value.");
@ -151,5 +149,5 @@ function validateTextProp(aProp, aEnabled, aName, aValue, aDesc,
aDesc + ": enabled checkbox.");
is(aProp.editor.nameSpan.textContent, aName, aDesc + ": name span.");
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
// 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 = `
<style type="text/css">
.testclass {
background: #F3F3F3 none repeat scroll 0% 0%;
background: rgb(243, 243, 243) none repeat scroll 0% 0%;
}
</style>
<div class="testclass">Styled Node</h1>

View File

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

View File

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

View File

@ -40,9 +40,11 @@ function* userAgentStylesUneditable(inspector, view) {
ok(rule.editor.element.hasAttribute("uneditable"),
"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");
ok(!rule.textProps[0].editor.valueSpan._editable,
ok(!firstProp.editor.valueSpan._editable,
"valueSpan 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);
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");
}

View File

@ -16,7 +16,7 @@ add_task(function*() {
let {inspector, view} = yield openRuleView();
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");
let cView = inspector.sidebar.getWindowForTab("computedview")
@ -38,6 +38,6 @@ add_task(function*() {
ok(getComputedViewPropertyValue(cView, "color"), "#00F",
"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");
});

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");
}
/**
* 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.
*
@ -318,25 +356,7 @@ function openRuleView() {
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture=false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
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;
return waitForNEvents(target, eventName, 1, useCapture);
}
/**

View File

@ -14,8 +14,9 @@ const {Class} = require("sdk/core/heritage");
const {LongStringActor} = require("devtools/server/actors/string");
const {PSEUDO_ELEMENT_SET} = require("devtools/shared/styleinspector/css-logic");
// This will add the "stylesheet" actor type for protocol.js to recognize
require("devtools/server/actors/stylesheets");
// This will also add the "stylesheet" actor type for protocol.js to recognize
const {UPDATE_PRESERVING_RULES, UPDATE_GENERAL} =
require("devtools/server/actors/stylesheets");
loader.lazyGetter(this, "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);
});
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
// 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.
@ -128,6 +133,13 @@ types.addDictType("fontface", {
var PageStyleActor = protocol.ActorClass({
typeName: "pagestyle",
events: {
"stylesheet-updated": {
type: "styleSheetUpdated",
styleSheet: Arg(0, "stylesheet")
}
},
/**
* Create a PageStyleActor.
*
@ -151,9 +163,12 @@ var PageStyleActor = protocol.ActorClass({
this.onFrameUnload = this.onFrameUnload.bind(this);
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) {
return;
}
@ -164,6 +179,11 @@ var PageStyleActor = protocol.ActorClass({
this.refMap = null;
this.cssLogic = null;
this._styleElement = null;
for (let sheet of this._watchedSheets) {
sheet.off("style-applied", this._styleApplied);
}
this._watchedSheets.clear();
},
get conn() {
@ -182,11 +202,22 @@ var PageStyleActor = protocol.ActorClass({
// getApplied method calls cssLogic.highlight(node) to recreate the
// style cache. Clients requesting getApplied from actors that have not
// 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.
* @param item Either a CSSStyleRule or a DOM element.
@ -202,6 +233,21 @@ var PageStyleActor = protocol.ActorClass({
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.
* @param {DOMStyleSheet} sheet
@ -212,6 +258,10 @@ var PageStyleActor = protocol.ActorClass({
_sheetRef: function(sheet) {
let tabActor = this.inspector.tabActor;
let actor = tabActor.createStyleSheetActor(sheet);
if (!this._watchedSheets.has(actor)) {
this._watchedSheets.add(actor);
actor.on("style-applied", this._styleApplied);
}
return actor;
},
@ -524,7 +574,7 @@ var PageStyleActor = protocol.ActorClass({
* `matchedSelectors`: Include an array of specific selectors that
* caused this rule to match its node.
*/
getApplied: method(function(node, options) {
getApplied: method(Task.async(function*(node, options) {
if (!node) {
return {entries: [], rules: [], sheets: []};
}
@ -532,8 +582,14 @@ var PageStyleActor = protocol.ActorClass({
this.cssLogic.highlight(node.rawNode);
let entries = [];
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: {
node: Arg(0, "domnode"),
inherited: Option(1, "boolean"),
@ -905,12 +961,17 @@ var PageStyleActor = protocol.ActorClass({
/**
* Adds a new rule, and returns the new StyleRuleActor.
* @param NodeActor node
* @param [string] pseudoClasses The list of pseudo classes to append to the
* new selector.
* @returns StyleRuleActor of the new rule
* @param {NodeActor} node
* @param {String} pseudoClasses The list of pseudo classes to append to the
* new selector.
* @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 sheet = style.sheet;
let cssRules = sheet.cssRules;
@ -930,11 +991,22 @@ var PageStyleActor = protocol.ActorClass({
}
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: {
node: Arg(0, "domnode"),
pseudoClasses: Arg(1, "nullable:array:string")
pseudoClasses: Arg(1, "nullable:array:string"),
editAuthored: Arg(2, "boolean")
},
response: RetVal("appliedStylesReturn")
}),
@ -966,6 +1038,10 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
return this.inspector.walker;
},
get supportsAuthoredStyles() {
return this._form.traits && this._form.traits.authoredStyles;
},
getMatchedSelectors: protocol.custom(function(node, property, options) {
return this._getMatchedSelectors(node, property, options).then(ret => {
return ret.matched;
@ -989,7 +1065,13 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
}),
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];
});
}, {
@ -1007,19 +1089,34 @@ var PageStyleFront = protocol.FrontClass(PageStyleActor, {
*/
var StyleRuleActor = protocol.ActorClass({
typeName: "domstylerule",
events: {
"location-changed": {
type: "locationChanged",
line: Arg(0, "number"),
column: Arg(1, "number")
},
},
initialize: function(pageStyle, item) {
protocol.Actor.prototype.initialize.call(this, null);
this.pageStyle = pageStyle;
this.rawStyle = item.style;
this._parentSheet = null;
this._onStyleApplied = this._onStyleApplied.bind(this);
if (item instanceof (Ci.nsIDOMCSSRule)) {
this.type = item.type;
this.rawRule = item;
if ((this.rawRule instanceof Ci.nsIDOMCSSStyleRule ||
this.rawRule instanceof Ci.nsIDOMMozCSSKeyframeRule) &&
this.rawRule.parentStyleSheet) {
this.line = DOMUtils.getRuleLine(this.rawRule);
this.rawRule.parentStyleSheet) {
this.line = DOMUtils.getRelativeRuleLine(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 {
// Fake a rule
@ -1047,6 +1144,9 @@ var StyleRuleActor = protocol.ActorClass({
this.pageStyle = null;
this.rawNode = null;
this.rawRule = null;
if (this.sheetActor) {
this.sheetActor.off("style-applied", this._onStyleApplied);
}
},
// Objects returned by this actor are owned by the PageStyleActor
@ -1055,6 +1155,17 @@ var StyleRuleActor = protocol.ActorClass({
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) {
let document;
@ -1085,6 +1196,9 @@ var StyleRuleActor = protocol.ActorClass({
// Whether the style rule actor implements the modifySelector2 method
// that allows for unmatched rule to be added
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) {
form.parentStyleSheet = this.pageStyle._sheetRef(this.rawRule.parentStyleSheet).actorID;
if (this._parentSheet) {
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) {
case Ci.nsIDOMCSSRule.STYLE_RULE:
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",
* name: <string>,
@ -1163,7 +1402,7 @@ var StyleRuleActor = protocol.ActorClass({
if (this.rawNode) {
document = this.rawNode.ownerDocument;
} else {
let parentStyleSheet = this.rawRule.parentStyleSheet;
let parentStyleSheet = this._parentSheet;
while (parentStyleSheet.ownerRule &&
parentStyleSheet.ownerRule instanceof Ci.nsIDOMCSSImportRule) {
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
* unsuccessfully inserted to the parent style sheet.
*
* @param string value
* @param {String} 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
*/
_addNewSelector: function(value) {
_addNewSelector: Task.async(function*(value, editAuthored) {
let rule = this.rawRule;
let parentStyleSheet = rule.parentStyleSheet;
let cssRules = parentStyleSheet.cssRules;
let cssText = rule.cssText;
let selectorText = rule.selectorText;
let parentStyleSheet = this._parentSheet;
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);
return cssRules.item(i);
} catch(e) {
// The selector could be invalid, or the rule could fail to insert.
// If that happens, the method returns null.
// We know the selector modification is ok, so if the client asked
// for the authored text to be edited, do it now.
if (editAuthored) {
let document = this.getDocument(this._parentSheet);
try {
document.querySelector(value);
} catch (e) {
return null;
}
let sheetActor = this.pageStyle._sheetRef(parentStyleSheet);
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
@ -1243,12 +1504,12 @@ var StyleRuleActor = protocol.ActorClass({
* Returns a boolean if the selector in the stylesheet was modified,
* and false otherwise
*/
modifySelector: method(function(value) {
modifySelector: method(Task.async(function*(value) {
if (this.type === ELEMENT_STYLE) {
return false;
}
let document = this.getDocument(this.rawRule.parentStyleSheet);
let document = this.getDocument(this._parentSheet);
// Extract the selector, and pseudo elements and classes
let [selector, pseudoProp] = value.split(/(:{1,2}.+$)/);
let selectorElement;
@ -1262,11 +1523,11 @@ var StyleRuleActor = protocol.ActorClass({
// Check if the selector is valid and not the same as the original
// selector
if (selectorElement && this.rawRule.selectorText !== value) {
this._addNewSelector(value);
yield this._addNewSelector(value, false);
return true;
}
return false;
}, {
}), {
request: { selector: Arg(0, "string") },
response: { isModified: RetVal("boolean") },
}),
@ -1281,17 +1542,20 @@ var StyleRuleActor = protocol.ActorClass({
* selector matches the current element without having to refresh the whole
* list.
*
* @param DOMNode node
* @param {DOMNode} node
* The current selected element
* @param string value
* @param {String} 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
* new rule and a boolean indicating whether or not the new selector
* matches the current selected element
*/
modifySelector2: method(function(node, value) {
let isMatching = false;
modifySelector2: method(function(node, value, editAuthored = false) {
let ruleProps = null;
if (this.type === ELEMENT_STYLE ||
@ -1299,23 +1563,40 @@ var StyleRuleActor = protocol.ActorClass({
return { ruleProps, isMatching: true };
}
let newCssRule = this._addNewSelector(value);
if (newCssRule) {
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
let selectorPromise = this._addNewSelector(value, editAuthored);
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
try {
isMatching = node.rawNode.matches(value);
} catch(e) {
// This fails when value is an invalid selector.
}
return selectorPromise.then((newCssRule) => {
if (newCssRule) {
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
}
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: {
node: Arg(0, "domnode"),
value: Arg(1, "string")
value: Arg(1, "string"),
editAuthored: Arg(2, "boolean")
},
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() {
if (this.canSetRuleText) {
return new RuleRewriter(this, this.authoredText);
}
return new RuleModificationList(this);
},
@ -1364,6 +1661,9 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
get cssText() {
return this._form.cssText;
},
get authoredText() {
return this._form.authoredText || this._form.cssText;
},
get keyText() {
return this._form.keyText;
},
@ -1416,6 +1716,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
return this._form.traits && this._form.traits.modifySelectorUnmatched;
},
get canSetRuleText() {
return this._form.traits && this._form.traits.canSetRuleText;
},
get location() {
return {
source: this.parentStyleSheet,
@ -1425,6 +1729,10 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
};
},
_clearOriginalLocation: function() {
this._originalLocation = null;
},
getOriginalLocation: function() {
if (this._originalLocation) {
return promise.resolve(this._originalLocation);
@ -1458,7 +1766,11 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
let response;
if (this.supportsModifySelectorUnmatched) {
// 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 {
response = yield this._modifySelector(value);
}
@ -1469,6 +1781,13 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, {
return response;
}), {
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.
* The modifications are processed in the order in which they are
* 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({
/**
@ -1502,12 +1825,17 @@ var RuleModificationList = Class({
/**
* Add a "set" entry to the modification list.
*
* @param {string} name the property's name
* @param {string} value the property's value
* @param {string} priority the property's priority, either the empty
* @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 property's name
* @param {String} value the property's value
* @param {String} priority the property's priority, either the empty
* string or "important"
*/
setProperty: function(name, value, priority) {
setProperty: function(index, name, value, priority) {
this.modifications.push({
type: "set",
name: name,
@ -1519,14 +1847,73 @@ var RuleModificationList = Class({
/**
* 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({
type: "remove",
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;
/**
* 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
* 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);
const {
getIndentationFromPrefs,
getIndentationFromString
} = require("devtools/shared/shared/indentation");
var TRANSITION_CLASS = "moz-styleeditor-transitioning";
var TRANSITION_DURATION_MS = 500;
var TRANSITION_BUFFER_MS = 1000;
@ -40,6 +45,22 @@ var LOAD_ERROR = "error-load";
types.addActorType("stylesheet");
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
* stylesheets of a document.
@ -385,7 +406,9 @@ var StyleSheetActor = protocol.ActorClass({
value: Arg(1, "json")
},
"style-applied" : {
type: "styleApplied"
type: "styleApplied",
kind: Arg(0, "number"),
styleSheet: Arg(1, "stylesheet")
},
"media-rules-changed" : {
type: "mediaRulesChanged",
@ -597,6 +620,12 @@ var StyleSheetActor = protocol.ActorClass({
return promise.resolve(this.text);
}
let cssText = modifiedStyleSheets.get(this.rawSheet);
if (cssText !== undefined) {
this.text = cssText;
return promise.resolve(cssText);
}
if (!this.href) {
// this is an inline <style> sheet
let content = this.ownerNode.textContent;
@ -872,19 +901,22 @@ var StyleSheetActor = protocol.ActorClass({
* @param {object} request
* 'text' - new text
* '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);
modifiedStyleSheets.set(this.rawSheet, text);
this.text = text;
this._notifyPropertyChanged("ruleCount");
if (transition) {
this._insertTransistionRule();
this._insertTransistionRule(kind);
}
else {
events.emit(this, "style-applied");
events.emit(this, "style-applied", kind, this);
}
this._getMediaRules().then((rules) => {
@ -901,7 +933,7 @@ var StyleSheetActor = protocol.ActorClass({
* Insert a catch-all transition rule into the document. Set a timeout
* to remove the rule after a certain time.
*/
_insertTransistionRule: function() {
_insertTransistionRule: function(kind) {
this.document.documentElement.classList.add(TRANSITION_CLASS);
// 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)
// @see _onTransitionEnd
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);
},
@ -918,7 +950,7 @@ var StyleSheetActor = protocol.ActorClass({
* This cleans up class and rule added for transition effect and then
* notifies that the style has been applied.
*/
_onTransitionEnd: function()
_onTransitionEnd: function(kind)
{
this.document.documentElement.classList.remove(TRANSITION_CLASS);
@ -928,7 +960,7 @@ var StyleSheetActor = protocol.ActorClass({
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() {
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();
// Change an existing property...
changes.setProperty("color", "black");
changes.setProperty(-1, "color", "black");
// Create a new property
changes.setProperty("background-color", "green");
changes.setProperty(-1, "background-color", "green");
// Create a new property and then change it immediately.
changes.setProperty("border", "1px solid black");
changes.setProperty("border", "2px solid black");
changes.setProperty(-1, "border", "1px solid black");
changes.setProperty(-1, "border", "2px solid black");
return changes.apply();
}).then(() => {
@ -67,9 +67,9 @@ addTest(function modifyProperties() {
// Remove all the properties
let changes = elementStyle.startModifyingProperties();
changes.removeProperty("color");
changes.removeProperty("background-color");
changes.removeProperty("border");
changes.removeProperty(-1, "color");
changes.removeProperty(-1, "background-color");
changes.removeProperty(-1, "border");
return changes.apply();
}).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");
// DevTools default color unit
pref("devtools.defaultColorUnit", "hex");
pref("devtools.defaultColorUnit", "authored");
// Used for devtools debugging
pref("devtools.dump.emit", false);