import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'rule-editor.js',
'text-property-editor.js',
)

View file

@ -0,0 +1,620 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {l10n} = require("devtools/shared/inspector/css-logic");
const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
const {Rule} = require("devtools/client/inspector/rules/models/rule");
const {InplaceEditor, editableField, editableItem} =
require("devtools/client/shared/inplace-editor");
const {TextPropertyEditor} =
require("devtools/client/inspector/rules/views/text-property-editor");
const {
createChild,
blurOnMultipleProperties,
promiseWarn
} = require("devtools/client/inspector/shared/utils");
const {
parseDeclarations,
parsePseudoClassesAndAttributes,
SELECTOR_ATTRIBUTE,
SELECTOR_ELEMENT,
SELECTOR_PSEUDO_CLASS
} = require("devtools/shared/css/parsing-utils");
const promise = require("promise");
const Services = require("Services");
const EventEmitter = require("devtools/shared/event-emitter");
const {Task} = require("devtools/shared/task");
const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
const {LocalizationHelper} = require("devtools/shared/l10n");
const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
/**
* RuleEditor is responsible for the following:
* Owns a Rule object and creates a list of TextPropertyEditors
* for its TextProperties.
* Manages creation of new text properties.
*
* One step of a RuleEditor's instantiation is figuring out what's the original
* source link to the parent stylesheet (in case of source maps). This step is
* asynchronous and is triggered as soon as the RuleEditor is instantiated (see
* updateSourceLink). If you need to know when the RuleEditor is done with this,
* you need to listen to the source-link-updated event.
*
* @param {CssRuleView} ruleView
* The CssRuleView containg the document holding this rule editor.
* @param {Rule} rule
* The Rule object we're editing.
*/
function RuleEditor(ruleView, rule) {
EventEmitter.decorate(this);
this.ruleView = ruleView;
this.doc = this.ruleView.styleDocument;
this.toolbox = this.ruleView.inspector.toolbox;
this.rule = rule;
this.isEditable = !rule.isSystem;
// Flag that blocks updates of the selector and properties when it is
// being edited
this.isEditing = false;
this._onNewProperty = this._onNewProperty.bind(this);
this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
this._onSelectorDone = this._onSelectorDone.bind(this);
this._locationChanged = this._locationChanged.bind(this);
this.updateSourceLink = this.updateSourceLink.bind(this);
this.rule.domRule.on("location-changed", this._locationChanged);
this.toolbox.on("tool-registered", this.updateSourceLink);
this.toolbox.on("tool-unregistered", this.updateSourceLink);
this._create();
}
RuleEditor.prototype = {
destroy: function () {
this.rule.domRule.off("location-changed");
this.toolbox.off("tool-registered", this.updateSourceLink);
this.toolbox.off("tool-unregistered", this.updateSourceLink);
},
get isSelectorEditable() {
let trait = this.isEditable &&
this.ruleView.inspector.target.client.traits.selectorEditable &&
this.rule.domRule.type !== ELEMENT_STYLE &&
this.rule.domRule.type !== CSSRule.KEYFRAME_RULE;
// Do not allow editing anonymousselectors until we can
// detect mutations on pseudo elements in Bug 1034110.
return trait && !this.rule.elementStyle.element.isAnonymous;
},
_create: function () {
this.element = this.doc.createElement("div");
this.element.className = "ruleview-rule theme-separator";
this.element.setAttribute("uneditable", !this.isEditable);
this.element.setAttribute("unmatched", this.rule.isUnmatched);
this.element._ruleEditor = this;
// Give a relative position for the inplace editor's measurement
// span to be placed absolutely against.
this.element.style.position = "relative";
// Add the source link.
this.source = createChild(this.element, "div", {
class: "ruleview-rule-source theme-link"
});
this.source.addEventListener("click", function () {
if (this.source.hasAttribute("unselectable")) {
return;
}
let rule = this.rule.domRule;
this.ruleView.emit("ruleview-linked-clicked", rule);
}.bind(this));
let sourceLabel = this.doc.createElement("span");
sourceLabel.classList.add("ruleview-rule-source-label");
this.source.appendChild(sourceLabel);
this.updateSourceLink();
let code = createChild(this.element, "div", {
class: "ruleview-code"
});
let header = createChild(code, "div", {});
this.selectorText = createChild(header, "span", {
class: "ruleview-selectorcontainer theme-fg-color3",
tabindex: this.isSelectorEditable ? "0" : "-1",
});
if (this.isSelectorEditable) {
this.selectorText.addEventListener("click", event => {
// Clicks within the selector shouldn't propagate any further.
event.stopPropagation();
}, false);
editableField({
element: this.selectorText,
done: this._onSelectorDone,
cssProperties: this.rule.cssProperties,
contextMenu: this.ruleView.inspector.onTextBoxContextMenu
});
}
if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
let selector = this.rule.domRule.selectors
? this.rule.domRule.selectors.join(", ")
: this.ruleView.inspector.selectionCssSelector;
let selectorHighlighter = createChild(header, "span", {
class: "ruleview-selectorhighlighter" +
(this.ruleView.highlighters.selectorHighlighterShown === selector ?
" highlighted" : ""),
title: l10n("rule.selectorHighlighter.tooltip")
});
selectorHighlighter.addEventListener("click", () => {
this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
});
}
this.openBrace = createChild(header, "span", {
class: "ruleview-ruleopen",
textContent: " {"
});
this.propertyList = createChild(code, "ul", {
class: "ruleview-propertylist"
});
this.populate();
this.closeBrace = createChild(code, "div", {
class: "ruleview-ruleclose",
tabindex: this.isEditable ? "0" : "-1",
textContent: "}"
});
if (this.isEditable) {
// A newProperty editor should only be created when no editor was
// previously displayed. Since the editors are cleared on blur,
// check this.ruleview.isEditing on mousedown
this._ruleViewIsEditing = false;
code.addEventListener("mousedown", () => {
this._ruleViewIsEditing = this.ruleView.isEditing;
});
code.addEventListener("click", () => {
let selection = this.doc.defaultView.getSelection();
if (selection.isCollapsed && !this._ruleViewIsEditing) {
this.newProperty();
}
// Cleanup the _ruleViewIsEditing flag
this._ruleViewIsEditing = false;
}, false);
this.element.addEventListener("mousedown", () => {
this.doc.defaultView.focus();
}, false);
// Create a property editor when the close brace is clicked.
editableItem({ element: this.closeBrace }, () => {
this.newProperty();
});
}
},
/**
* Event handler called when a property changes on the
* StyleRuleActor.
*/
_locationChanged: function () {
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 : title;
let sourceLine = this.rule.ruleLine > 0 ? ":" + this.rule.ruleLine : "";
sourceLabel.setAttribute("title", sourceHref + sourceLine);
if (this.toolbox.isToolRegistered("styleeditor")) {
this.source.removeAttribute("unselectable");
} else {
this.source.setAttribute("unselectable", true);
}
if (this.rule.isSystem) {
let uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles");
sourceLabel.textContent = uaLabel + " " + title;
// 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
if (sourceHref === "about:PreferenceStyleSheet") {
this.source.setAttribute("unselectable", "true");
sourceLabel.textContent = uaLabel;
sourceLabel.removeAttribute("title");
}
} else {
sourceLabel.textContent = title;
if (this.rule.ruleLine === -1 && this.rule.domRule.parentStyleSheet) {
this.source.setAttribute("unselectable", "true");
}
}
let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
if (showOrig && !this.rule.isSystem &&
this.rule.domRule.type !== ELEMENT_STYLE) {
// Only get the original source link if the right pref is set, if the rule
// isn't a system rule and if it isn't an inline rule.
this.rule.getOriginalSourceStrings().then((strings) => {
sourceLabel.textContent = strings.short;
sourceLabel.setAttribute("title", strings.full);
}, e => console.error(e)).then(() => {
this.emit("source-link-updated");
});
} else {
// If we're not getting the original source link, then we can emit the
// event immediately (but still asynchronously to give consumers a chance
// to register it after having instantiated the RuleEditor).
promise.resolve().then(() => {
this.emit("source-link-updated");
});
}
},
/**
* Update the rule editor with the contents of the rule.
*/
populate: function () {
// Clear out existing viewers.
while (this.selectorText.hasChildNodes()) {
this.selectorText.removeChild(this.selectorText.lastChild);
}
// If selector text comes from a css rule, highlight selectors that
// actually match. For custom selector text (such as for the 'element'
// style, just show the text directly.
if (this.rule.domRule.type === ELEMENT_STYLE) {
this.selectorText.textContent = this.rule.selectorText;
} else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) {
this.selectorText.textContent = this.rule.domRule.keyText;
} else {
this.rule.domRule.selectors.forEach((selector, i) => {
if (i !== 0) {
createChild(this.selectorText, "span", {
class: "ruleview-selector-separator",
textContent: ", "
});
}
let containerClass =
(this.rule.matchedSelectors.indexOf(selector) > -1) ?
"ruleview-selector-matched" : "ruleview-selector-unmatched";
let selectorContainer = createChild(this.selectorText, "span", {
class: containerClass
});
let parsedSelector = parsePseudoClassesAndAttributes(selector);
for (let selectorText of parsedSelector) {
let selectorClass = "";
switch (selectorText.type) {
case SELECTOR_ATTRIBUTE:
selectorClass = "ruleview-selector-attribute";
break;
case SELECTOR_ELEMENT:
selectorClass = "ruleview-selector";
break;
case SELECTOR_PSEUDO_CLASS:
selectorClass = [":active", ":focus", ":hover"].some(
pseudo => selectorText.value === pseudo) ?
"ruleview-selector-pseudo-class-lock" :
"ruleview-selector-pseudo-class";
break;
default:
break;
}
createChild(selectorContainer, "span", {
textContent: selectorText.value,
class: selectorClass
});
}
});
}
for (let prop of this.rule.textProps) {
if (!prop.editor && !prop.invisible) {
let editor = new TextPropertyEditor(this, prop);
this.propertyList.appendChild(editor.element);
}
}
},
/**
* Programatically add a new property to the rule.
*
* @param {String} name
* Property name.
* @param {String} value
* Property value.
* @param {String} priority
* Property priority.
* @param {Boolean} enabled
* True if the property should be enabled.
* @param {TextProperty} siblingProp
* Optional, property next to which the new property will be added.
* @return {TextProperty}
* The new property
*/
addProperty: function (name, value, priority, enabled, siblingProp) {
let prop = this.rule.createProperty(name, value, priority, enabled,
siblingProp);
let index = this.rule.textProps.indexOf(prop);
let editor = new TextPropertyEditor(this, prop);
// Insert this node before the DOM node that is currently at its new index
// in the property list. There is currently one less node in the DOM than
// in the property list, so this causes it to appear after siblingProp.
// If there is no node at its index, as is the case where this is the last
// node being inserted, then this behaves as appendChild.
this.propertyList.insertBefore(editor.element,
this.propertyList.children[index]);
return prop;
},
/**
* Programatically add a list of new properties to the rule. Focus the UI
* to the proper location after adding (either focus the value on the
* last property if it is empty, or create a new property and focus it).
*
* @param {Array} properties
* Array of properties, which are objects with this signature:
* {
* name: {string},
* value: {string},
* priority: {string}
* }
* @param {TextProperty} siblingProp
* Optional, the property next to which all new props should be added.
*/
addProperties: function (properties, siblingProp) {
if (!properties || !properties.length) {
return;
}
let lastProp = siblingProp;
for (let p of properties) {
let isCommented = Boolean(p.commentOffsets);
let enabled = !isCommented;
lastProp = this.addProperty(p.name, p.value, p.priority, enabled,
lastProp);
}
// Either focus on the last value if incomplete, or start a new one.
if (lastProp && lastProp.value.trim() === "") {
lastProp.editor.valueSpan.click();
} else {
this.newProperty();
}
},
/**
* Create a text input for a property name. If a non-empty property
* name is given, we'll create a real TextProperty and add it to the
* rule.
*/
newProperty: function () {
// If we're already creating a new property, ignore this.
if (!this.closeBrace.hasAttribute("tabindex")) {
return;
}
// While we're editing a new property, it doesn't make sense to
// start a second new property editor, so disable focusing the
// close brace for now.
this.closeBrace.removeAttribute("tabindex");
this.newPropItem = createChild(this.propertyList, "li", {
class: "ruleview-property ruleview-newproperty",
});
this.newPropSpan = createChild(this.newPropItem, "span", {
class: "ruleview-propertyname",
tabindex: "0"
});
this.multipleAddedProperties = null;
this.editor = new InplaceEditor({
element: this.newPropSpan,
done: this._onNewProperty,
destroy: this._newPropertyDestroy,
advanceChars: ":",
contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
popup: this.ruleView.popup,
cssProperties: this.rule.cssProperties,
contextMenu: this.ruleView.inspector.onTextBoxContextMenu
});
// Auto-close the input if multiple rules get pasted into new property.
this.editor.input.addEventListener("paste",
blurOnMultipleProperties(this.rule.cssProperties), false);
},
/**
* Called when the new property input has been dismissed.
*
* @param {String} value
* The value in the editor.
* @param {Boolean} commit
* True if the value should be committed.
*/
_onNewProperty: function (value, commit) {
if (!value || !commit) {
return;
}
// parseDeclarations allows for name-less declarations, but in the present
// case, we're creating a new declaration, it doesn't make sense to accept
// these entries
this.multipleAddedProperties =
parseDeclarations(this.rule.cssProperties.isKnown, value, true)
.filter(d => d.name);
// Blur the editor field now and deal with adding declarations later when
// the field gets destroyed (see _newPropertyDestroy)
this.editor.input.blur();
},
/**
* Called when the new property editor is destroyed.
* This is where the properties (type TextProperty) are actually being
* added, since we want to wait until after the inplace editor `destroy`
* event has been fired to keep consistent UI state.
*/
_newPropertyDestroy: function () {
// We're done, make the close brace focusable again.
this.closeBrace.setAttribute("tabindex", "0");
this.propertyList.removeChild(this.newPropItem);
delete this.newPropItem;
delete this.newPropSpan;
// If properties were added, we want to focus the proper element.
// If the last new property has no value, focus the value on it.
// Otherwise, start a new property and focus that field.
if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
this.addProperties(this.multipleAddedProperties);
}
},
/**
* Called when the selector's inplace editor is closed.
* Ignores the change if the user pressed escape, otherwise
* commits it.
*
* @param {String} value
* The value contained in the editor.
* @param {Boolean} commit
* True if the change should be applied.
* @param {Number} direction
* The move focus direction number.
*/
_onSelectorDone: Task.async(function* (value, commit, direction) {
if (!commit || this.isEditing || value === "" ||
value === this.rule.selectorText) {
return;
}
let ruleView = this.ruleView;
let elementStyle = ruleView._elementStyle;
let element = elementStyle.element;
let supportsUnmatchedRules =
this.rule.domRule.supportsModifySelectorUnmatched;
this.isEditing = true;
try {
let response = yield this.rule.domRule.modifySelector(element, value);
if (!supportsUnmatchedRules) {
this.isEditing = false;
if (response) {
this.ruleView.refreshPanel();
}
return;
}
// We recompute the list of applied styles, because editing a
// selector might cause this rule's position to change.
let applied = yield elementStyle.pageStyle.getApplied(element, {
inherited: true,
matchedSelectors: true,
filter: elementStyle.showUserAgentStyles ? "ua" : undefined
});
this.isEditing = false;
let {ruleProps, isMatching} = response;
if (!ruleProps) {
// Notify for changes, even when nothing changes,
// just to allow tests being able to track end of this request.
ruleView.emit("ruleview-invalid-selector");
return;
}
ruleProps.isUnmatched = !isMatching;
let newRule = new Rule(elementStyle, ruleProps);
let editor = new RuleEditor(ruleView, newRule);
let rules = elementStyle.rules;
let newRuleIndex = applied.findIndex((r) => r.rule == ruleProps.rule);
let oldIndex = rules.indexOf(this.rule);
// If the selector no longer matches, then we leave the rule in
// the same relative position.
if (newRuleIndex === -1) {
newRuleIndex = oldIndex;
}
// Remove the old rule and insert the new rule.
rules.splice(oldIndex, 1);
rules.splice(newRuleIndex, 0, newRule);
elementStyle._changed();
elementStyle.markOverriddenAll();
// We install the new editor in place of the old -- you might
// think we would replicate the list-modification logic above,
// but that is complicated due to the way the UI installs
// pseudo-element rules and the like.
this.element.parentNode.replaceChild(editor.element, this.element);
// Remove highlight for modified selector
if (ruleView.highlighters.selectorHighlighterShown) {
ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
ruleView.highlighters.selectorHighlighterShown);
}
editor._moveSelectorFocus(direction);
} catch (err) {
this.isEditing = false;
promiseWarn(err);
}
}),
/**
* Handle moving the focus change after a tab or return keypress in the
* selector inplace editor.
*
* @param {Number} direction
* The move focus direction number.
*/
_moveSelectorFocus: function (direction) {
if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
return;
}
if (this.rule.textProps.length > 0) {
this.rule.textProps[0].editor.nameSpan.click();
} else {
this.propertyList.click();
}
}
};
exports.RuleEditor = RuleEditor;

View file

@ -0,0 +1,880 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {l10n} = require("devtools/shared/inspector/css-logic");
const {getCssProperties} = require("devtools/shared/fronts/css-properties");
const {InplaceEditor, editableField} =
require("devtools/client/shared/inplace-editor");
const {
createChild,
appendText,
advanceValidate,
blurOnMultipleProperties
} = require("devtools/client/inspector/shared/utils");
const {
parseDeclarations,
parseSingleValue,
} = require("devtools/shared/css/parsing-utils");
const Services = require("Services");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const SHARED_SWATCH_CLASS = "ruleview-swatch";
const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
/*
* An actionable element is an element which on click triggers a specific action
* (e.g. shows a color tooltip, opens a link, …).
*/
const ACTIONABLE_ELEMENTS_SELECTORS = [
`.${COLOR_SWATCH_CLASS}`,
`.${BEZIER_SWATCH_CLASS}`,
`.${FILTER_SWATCH_CLASS}`,
`.${ANGLE_SWATCH_CLASS}`,
"a"
];
/**
* TextPropertyEditor is responsible for the following:
* Owns a TextProperty object.
* Manages changes to the TextProperty.
* Can be expanded to display computed properties.
* Can mark a property disabled or enabled.
*
* @param {RuleEditor} ruleEditor
* The rule editor that owns this TextPropertyEditor.
* @param {TextProperty} property
* The text property to edit.
*/
function TextPropertyEditor(ruleEditor, property) {
this.ruleEditor = ruleEditor;
this.ruleView = this.ruleEditor.ruleView;
this.doc = this.ruleEditor.doc;
this.popup = this.ruleView.popup;
this.prop = property;
this.prop.editor = this;
this.browserWindow = this.doc.defaultView.top;
this._populatedComputed = false;
this._hasPendingClick = false;
this._clickedElementOptions = null;
const toolbox = this.ruleView.inspector.toolbox;
this.cssProperties = getCssProperties(toolbox);
this._onEnableClicked = this._onEnableClicked.bind(this);
this._onExpandClicked = this._onExpandClicked.bind(this);
this._onStartEditing = this._onStartEditing.bind(this);
this._onNameDone = this._onNameDone.bind(this);
this._onValueDone = this._onValueDone.bind(this);
this._onSwatchCommit = this._onSwatchCommit.bind(this);
this._onSwatchPreview = this._onSwatchPreview.bind(this);
this._onSwatchRevert = this._onSwatchRevert.bind(this);
this._onValidate = this.ruleView.throttle(this._previewValue, 10, this);
this.update = this.update.bind(this);
this.updatePropertyState = this.updatePropertyState.bind(this);
this._create();
this.update();
}
TextPropertyEditor.prototype = {
/**
* Boolean indicating if the name or value is being currently edited.
*/
get editing() {
return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
this.ruleView.tooltips.isEditing) || this.popup.isOpen;
},
/**
* Get the rule to the current text property
*/
get rule() {
return this.prop.rule;
},
/**
* Create the property editor's DOM.
*/
_create: function () {
this.element = this.doc.createElementNS(HTML_NS, "li");
this.element.classList.add("ruleview-property");
this.element._textPropertyEditor = this;
this.container = createChild(this.element, "div", {
class: "ruleview-propertycontainer"
});
// The enable checkbox will disable or enable the rule.
this.enable = createChild(this.container, "div", {
class: "ruleview-enableproperty theme-checkbox",
tabindex: "-1"
});
// Click to expand the computed properties of the text property.
this.expander = createChild(this.container, "span", {
class: "ruleview-expander theme-twisty"
});
this.expander.addEventListener("click", this._onExpandClicked, true);
this.nameContainer = createChild(this.container, "span", {
class: "ruleview-namecontainer"
});
// Property name, editable when focused. Property name
// is committed when the editor is unfocused.
this.nameSpan = createChild(this.nameContainer, "span", {
class: "ruleview-propertyname theme-fg-color5",
tabindex: this.ruleEditor.isEditable ? "0" : "-1",
});
appendText(this.nameContainer, ": ");
// Create a span that will hold the property and semicolon.
// Use this span to create a slightly larger click target
// for the value.
this.valueContainer = createChild(this.container, "span", {
class: "ruleview-propertyvaluecontainer"
});
// Property value, editable when focused. Changes to the
// property value are applied as they are typed, and reverted
// if the user presses escape.
this.valueSpan = createChild(this.valueContainer, "span", {
class: "ruleview-propertyvalue theme-fg-color1",
tabindex: this.ruleEditor.isEditable ? "0" : "-1",
});
// Storing the TextProperty on the elements for easy access
// (for instance by the tooltip)
this.valueSpan.textProperty = this.prop;
this.nameSpan.textProperty = this.prop;
// If the value is a color property we need to put it through the parser
// so that colors can be coerced into the default color type. This prevents
// us from thinking that when colors are coerced they have been changed by
// the user.
let outputParser = this.ruleView._outputParser;
let frag = outputParser.parseCssProperty(this.prop.name, this.prop.value);
let parsedValue = frag.textContent;
// Save the initial value as the last committed value,
// for restoring after pressing escape.
this.committed = { name: this.prop.name,
value: parsedValue,
priority: this.prop.priority };
appendText(this.valueContainer, ";");
this.warning = createChild(this.container, "div", {
class: "ruleview-warning",
hidden: "",
title: l10n("rule.warning.title"),
});
// Filter button that filters for the current property name and is
// displayed when the property is overridden by another rule.
this.filterProperty = createChild(this.container, "div", {
class: "ruleview-overridden-rule-filter",
hidden: "",
title: l10n("rule.filterProperty.title"),
});
this.filterProperty.addEventListener("click", event => {
this.ruleEditor.ruleView.setFilterStyles("`" + this.prop.name + "`");
event.stopPropagation();
}, false);
// Holds the viewers for the computed properties.
// will be populated in |_updateComputed|.
this.computed = createChild(this.element, "ul", {
class: "ruleview-computedlist",
});
// Only bind event handlers if the rule is editable.
if (this.ruleEditor.isEditable) {
this.enable.addEventListener("click", this._onEnableClicked, true);
this.nameContainer.addEventListener("click", (event) => {
// Clicks within the name shouldn't propagate any further.
event.stopPropagation();
// Forward clicks on nameContainer to the editable nameSpan
if (event.target === this.nameContainer) {
this.nameSpan.click();
}
}, false);
editableField({
start: this._onStartEditing,
element: this.nameSpan,
done: this._onNameDone,
destroy: this.updatePropertyState,
advanceChars: ":",
contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
popup: this.popup,
cssProperties: this.cssProperties,
contextMenu: this.ruleView.inspector.onTextBoxContextMenu
});
// Auto blur name field on multiple CSS rules get pasted in.
this.nameContainer.addEventListener("paste",
blurOnMultipleProperties(this.cssProperties), false);
this.valueContainer.addEventListener("click", (event) => {
// Clicks within the value shouldn't propagate any further.
event.stopPropagation();
// Forward clicks on valueContainer to the editable valueSpan
if (event.target === this.valueContainer) {
this.valueSpan.click();
}
}, false);
// The mousedown event could trigger a blur event on nameContainer, which
// will trigger a call to the update function. The update function clears
// valueSpan's markup. Thus the regular click event does not bubble up, and
// listener's callbacks are not called.
// So we need to remember where the user clicks in order to re-trigger the click
// after the valueSpan's markup is re-populated. We only need to track this for
// valueSpan's child elements, because direct click on valueSpan will always
// trigger a click event.
this.valueSpan.addEventListener("mousedown", (event) => {
let clickedEl = event.target;
if (clickedEl === this.valueSpan) {
return;
}
this._hasPendingClick = true;
let matchedSelector = ACTIONABLE_ELEMENTS_SELECTORS.find(
(selector) => clickedEl.matches(selector));
if (matchedSelector) {
let similarElements = [...this.valueSpan.querySelectorAll(matchedSelector)];
this._clickedElementOptions = {
selector: matchedSelector,
index: similarElements.indexOf(clickedEl)
};
}
}, false);
this.valueSpan.addEventListener("mouseup", (event) => {
this._clickedElementOptions = null;
this._hasPendingClick = false;
}, false);
this.valueSpan.addEventListener("click", (event) => {
let target = event.target;
if (target.nodeName === "a") {
event.stopPropagation();
event.preventDefault();
this.browserWindow.openUILinkIn(target.href, "tab");
}
}, false);
editableField({
start: this._onStartEditing,
element: this.valueSpan,
done: this._onValueDone,
destroy: this.update,
validate: this._onValidate,
advanceChars: advanceValidate,
contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
property: this.prop,
popup: this.popup,
multiline: true,
maxWidth: () => this.container.getBoundingClientRect().width,
cssProperties: this.cssProperties,
contextMenu: this.ruleView.inspector.onTextBoxContextMenu
});
}
},
/**
* Get the path from which to resolve requests for this
* rule's stylesheet.
*
* @return {String} the stylesheet's href.
*/
get sheetHref() {
let domRule = this.rule.domRule;
if (domRule) {
return domRule.href || domRule.nodeHref;
}
return undefined;
},
/**
* Populate the span based on changes to the TextProperty.
*/
update: function () {
if (this.ruleView.isDestroyed) {
return;
}
this.updatePropertyState();
let name = this.prop.name;
this.nameSpan.textContent = name;
// Combine the property's value and priority into one string for
// the value.
let store = this.rule.elementStyle.store;
let val = store.userProperties.getProperty(this.rule.style, name,
this.prop.value);
if (this.prop.priority) {
val += " !" + this.prop.priority;
}
let propDirty = store.userProperties.contains(this.rule.style, name);
if (propDirty) {
this.element.setAttribute("dirty", "");
} else {
this.element.removeAttribute("dirty");
}
let outputParser = this.ruleView._outputParser;
let parserOptions = {
angleClass: "ruleview-angle",
angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
bezierClass: "ruleview-bezier",
bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
colorClass: "ruleview-color",
colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
filterClass: "ruleview-filter",
filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
gridClass: "ruleview-grid",
defaultColorType: !propDirty,
urlClass: "theme-link",
baseURI: this.sheetHref
};
let frag = outputParser.parseCssProperty(name, val, parserOptions);
this.valueSpan.innerHTML = "";
this.valueSpan.appendChild(frag);
this.ruleView.emit("property-value-updated", this.valueSpan);
// Attach the color picker tooltip to the color swatches
this._colorSwatchSpans =
this.valueSpan.querySelectorAll("." + COLOR_SWATCH_CLASS);
if (this.ruleEditor.isEditable) {
for (let span of this._colorSwatchSpans) {
// Adding this swatch to the list of swatches our colorpicker
// knows about
this.ruleView.tooltips.colorPicker.addSwatch(span, {
onShow: this._onStartEditing,
onPreview: this._onSwatchPreview,
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
});
span.on("unit-change", this._onSwatchCommit);
let title = l10n("rule.colorSwatch.tooltip");
span.setAttribute("title", title);
}
}
// Attach the cubic-bezier tooltip to the bezier swatches
this._bezierSwatchSpans =
this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
if (this.ruleEditor.isEditable) {
for (let span of this._bezierSwatchSpans) {
// Adding this swatch to the list of swatches our colorpicker
// knows about
this.ruleView.tooltips.cubicBezier.addSwatch(span, {
onShow: this._onStartEditing,
onPreview: this._onSwatchPreview,
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
});
let title = l10n("rule.bezierSwatch.tooltip");
span.setAttribute("title", title);
}
}
// Attach the filter editor tooltip to the filter swatch
let span = this.valueSpan.querySelector("." + FILTER_SWATCH_CLASS);
if (this.ruleEditor.isEditable) {
if (span) {
parserOptions.filterSwatch = true;
this.ruleView.tooltips.filterEditor.addSwatch(span, {
onShow: this._onStartEditing,
onPreview: this._onSwatchPreview,
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
}, outputParser, parserOptions);
let title = l10n("rule.filterSwatch.tooltip");
span.setAttribute("title", title);
}
}
this.angleSwatchSpans =
this.valueSpan.querySelectorAll("." + ANGLE_SWATCH_CLASS);
if (this.ruleEditor.isEditable) {
for (let angleSpan of this.angleSwatchSpans) {
angleSpan.on("unit-change", this._onSwatchCommit);
let title = l10n("rule.angleSwatch.tooltip");
angleSpan.setAttribute("title", title);
}
}
let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
if (gridToggle) {
gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
if (this.ruleView.highlighters.gridHighlighterShown ===
this.ruleView.inspector.selection.nodeFront) {
gridToggle.classList.add("active");
}
}
// Now that we have updated the property's value, we might have a pending
// click on the value container. If we do, we have to trigger a click event
// on the right element.
if (this._hasPendingClick) {
this._hasPendingClick = false;
let elToClick;
if (this._clickedElementOptions !== null) {
let {selector, index} = this._clickedElementOptions;
elToClick = this.valueSpan.querySelectorAll(selector)[index];
this._clickedElementOptions = null;
}
if (!elToClick) {
elToClick = this.valueSpan;
}
elToClick.click();
}
// Populate the computed styles.
this._updateComputed();
// Update the rule property highlight.
this.ruleView._updatePropertyHighlight(this);
},
_onStartEditing: function () {
this.element.classList.remove("ruleview-overridden");
this.filterProperty.hidden = true;
this.enable.style.visibility = "hidden";
},
/**
* Update the visibility of the enable checkbox, the warning indicator and
* the filter property, as well as the overriden state of the property.
*/
updatePropertyState: function () {
if (this.prop.enabled) {
this.enable.style.removeProperty("visibility");
this.enable.setAttribute("checked", "");
} else {
this.enable.style.visibility = "visible";
this.enable.removeAttribute("checked");
}
this.warning.hidden = this.editing || this.isValid();
this.filterProperty.hidden = this.editing ||
!this.isValid() ||
!this.prop.overridden ||
this.ruleEditor.rule.isUnmatched;
if (!this.editing &&
(this.prop.overridden || !this.prop.enabled ||
!this.prop.isKnownProperty())) {
this.element.classList.add("ruleview-overridden");
} else {
this.element.classList.remove("ruleview-overridden");
}
},
/**
* Update the indicator for computed styles. The computed styles themselves
* are populated on demand, when they become visible.
*/
_updateComputed: function () {
this.computed.innerHTML = "";
let showExpander = this.prop.computed.some(c => c.name !== this.prop.name);
this.expander.style.visibility = showExpander ? "visible" : "hidden";
this._populatedComputed = false;
if (this.expander.hasAttribute("open")) {
this._populateComputed();
}
},
/**
* Populate the list of computed styles.
*/
_populateComputed: function () {
if (this._populatedComputed) {
return;
}
this._populatedComputed = true;
for (let computed of this.prop.computed) {
// Don't bother to duplicate information already
// shown in the text property.
if (computed.name === this.prop.name) {
continue;
}
let li = createChild(this.computed, "li", {
class: "ruleview-computed"
});
if (computed.overridden) {
li.classList.add("ruleview-overridden");
}
createChild(li, "span", {
class: "ruleview-propertyname theme-fg-color5",
textContent: computed.name
});
appendText(li, ": ");
let outputParser = this.ruleView._outputParser;
let frag = outputParser.parseCssProperty(
computed.name, computed.value, {
colorSwatchClass: "ruleview-swatch ruleview-colorswatch",
urlClass: "theme-link",
baseURI: this.sheetHref
}
);
// Store the computed property value that was parsed for output
computed.parsedValue = frag.textContent;
createChild(li, "span", {
class: "ruleview-propertyvalue theme-fg-color1",
child: frag
});
appendText(li, ";");
// Store the computed style element for easy access when highlighting
// styles
computed.element = li;
}
},
/**
* Handles clicks on the disabled property.
*/
_onEnableClicked: function (event) {
let checked = this.enable.hasAttribute("checked");
if (checked) {
this.enable.removeAttribute("checked");
} else {
this.enable.setAttribute("checked", "");
}
this.prop.setEnabled(!checked);
event.stopPropagation();
},
/**
* Handles clicks on the computed property expander. If the computed list is
* open due to user expanding or style filtering, collapse the computed list
* and close the expander. Otherwise, add user-open attribute which is used to
* expand the computed list and tracks whether or not the computed list is
* expanded by manually by the user.
*/
_onExpandClicked: function (event) {
if (this.computed.hasAttribute("filter-open") ||
this.computed.hasAttribute("user-open")) {
this.expander.removeAttribute("open");
this.computed.removeAttribute("filter-open");
this.computed.removeAttribute("user-open");
} else {
this.expander.setAttribute("open", "true");
this.computed.setAttribute("user-open", "");
this._populateComputed();
}
event.stopPropagation();
},
/**
* Expands the computed list when a computed property is matched by the style
* filtering. The filter-open attribute is used to track whether or not the
* computed list was toggled opened by the filter.
*/
expandForFilter: function () {
if (!this.computed.hasAttribute("user-open")) {
this.expander.setAttribute("open", "true");
this.computed.setAttribute("filter-open", "");
this._populateComputed();
}
},
/**
* Collapses the computed list that was expanded by style filtering.
*/
collapseForFilter: function () {
this.computed.removeAttribute("filter-open");
if (!this.computed.hasAttribute("user-open")) {
this.expander.removeAttribute("open");
}
},
/**
* Called when the property name's inplace editor is closed.
* Ignores the change if the user pressed escape, otherwise
* commits it.
*
* @param {String} value
* The value contained in the editor.
* @param {Boolean} commit
* True if the change should be applied.
* @param {Number} direction
* The move focus direction number.
*/
_onNameDone: function (value, commit, direction) {
let isNameUnchanged = (!commit && !this.ruleEditor.isEditing) ||
this.committed.name === value;
if (this.prop.value && isNameUnchanged) {
return;
}
// Remove a property if the name is empty
if (!value.trim()) {
this.remove(direction);
return;
}
// Remove a property if the property value is empty and the property
// value is not about to be focused
if (!this.prop.value &&
direction !== Services.focus.MOVEFOCUS_FORWARD) {
this.remove(direction);
return;
}
// Adding multiple rules inside of name field overwrites the current
// property with the first, then adds any more onto the property list.
let properties = parseDeclarations(this.cssProperties.isKnown, value);
if (properties.length) {
this.prop.setName(properties[0].name);
this.committed.name = this.prop.name;
if (!this.prop.enabled) {
this.prop.setEnabled(true);
}
if (properties.length > 1) {
this.prop.setValue(properties[0].value, properties[0].priority);
this.ruleEditor.addProperties(properties.slice(1), this.prop);
}
}
},
/**
* Remove property from style and the editors from DOM.
* Begin editing next or previous available property given the focus
* direction.
*
* @param {Number} direction
* The move focus direction number.
*/
remove: function (direction) {
if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
for (let span of this._colorSwatchSpans) {
this.ruleView.tooltips.colorPicker.removeSwatch(span);
span.off("unit-change", this._onSwatchCommit);
}
}
if (this.angleSwatchSpans && this.angleSwatchSpans.length) {
for (let span of this.angleSwatchSpans) {
span.off("unit-change", this._onSwatchCommit);
}
}
this.element.parentNode.removeChild(this.element);
this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
this.nameSpan.textProperty = null;
this.valueSpan.textProperty = null;
this.prop.remove();
},
/**
* Called when a value editor closes. If the user pressed escape,
* revert to the value this property had before editing.
*
* @param {String} value
* The value contained in the editor.
* @param {Boolean} commit
* True if the change should be applied.
* @param {Number} direction
* The move focus direction number.
*/
_onValueDone: function (value = "", commit, direction) {
let parsedProperties = this._getValueAndExtraProperties(value);
let val = parseSingleValue(this.cssProperties.isKnown,
parsedProperties.firstValue);
let isValueUnchanged = (!commit && !this.ruleEditor.isEditing) ||
!parsedProperties.propertiesToAdd.length &&
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 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;
}
if (this.isDisplayGrid()) {
this.ruleView.highlighters._hideGridHighlighter();
}
// First, set this property value (common case, only modified a property)
this.prop.setValue(val.value, val.priority);
if (!this.prop.enabled) {
this.prop.setEnabled(true);
}
this.committed.value = this.prop.value;
this.committed.priority = this.prop.priority;
// If needed, add any new properties after this.prop.
this.ruleEditor.addProperties(parsedProperties.propertiesToAdd, this.prop);
// If the input value is empty and the focus is moving forward to the next
// editable field, then remove the whole property.
// A timeout is used here to accurately check the state, since the inplace
// editor `done` and `destroy` events fire before the next editor
// is focused.
if (!value.trim() && direction !== Services.focus.MOVEFOCUS_BACKWARD) {
setTimeout(() => {
if (!this.editing) {
this.remove(direction);
}
}, 0);
}
},
/**
* Called when the swatch editor wants to commit a value change.
*/
_onSwatchCommit: function () {
this._onValueDone(this.valueSpan.textContent, true);
this.update();
},
/**
* Called when the swatch editor wants to preview a value change.
*/
_onSwatchPreview: function () {
this._previewValue(this.valueSpan.textContent);
},
/**
* Called when the swatch editor closes from an ESC. Revert to the original
* value of this property before editing.
*/
_onSwatchRevert: function () {
this._previewValue(this.prop.value, true);
this.update();
},
/**
* Parse a value string and break it into pieces, starting with the
* first value, and into an array of additional properties (if any).
*
* Example: Calling with "red; width: 100px" would return
* { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
*
* @param {String} value
* The string to parse
* @return {Object} An object with the following properties:
* firstValue: A string containing a simple value, like
* "red" or "100px!important"
* propertiesToAdd: An array with additional properties, following the
* parseDeclarations format of {name,value,priority}
*/
_getValueAndExtraProperties: function (value) {
// The inplace editor will prevent manual typing of multiple properties,
// but we need to deal with the case during a paste event.
// Adding multiple properties inside of value editor sets value with the
// first, then adds any more onto the property list (below this property).
let firstValue = value;
let propertiesToAdd = [];
let properties = parseDeclarations(this.cssProperties.isKnown, value);
// Check to see if the input string can be parsed as multiple properties
if (properties.length) {
// Get the first property value (if any), and any remaining
// properties (if any)
if (!properties[0].name && properties[0].value) {
firstValue = properties[0].value;
propertiesToAdd = properties.slice(1);
} else if (properties[0].name && properties[0].value) {
// In some cases, the value could be a property:value pair
// itself. Join them as one value string and append
// potentially following properties
firstValue = properties[0].name + ": " + properties[0].value;
propertiesToAdd = properties.slice(1);
}
}
return {
propertiesToAdd: propertiesToAdd,
firstValue: firstValue
};
},
/**
* Live preview this property, without committing changes.
*
* @param {String} value
* The value to set the current property to.
* @param {Boolean} reverting
* True if we're reverting the previously previewed value
*/
_previewValue: function (value, reverting = false) {
// Since function call is throttled, we need to make sure we are still
// editing, and any selector modifications have been completed
if (!reverting && (!this.editing || this.ruleEditor.isEditing)) {
return;
}
let val = parseSingleValue(this.cssProperties.isKnown, value);
this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
val.priority);
},
/**
* 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 this.prop.isValid();
},
/**
* Returns true if the property is a `display: grid` declaration.
*
* @return {Boolean} true if the property is a `display: grid` declaration.
*/
isDisplayGrid: function () {
return this.prop.name === "display" && this.prop.value === "grid";
}
};
exports.TextPropertyEditor = TextPropertyEditor;