mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-18 05:07:31 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
8
devtools/client/inspector/rules/views/moz.build
Normal file
8
devtools/client/inspector/rules/views/moz.build
Normal 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',
|
||||
)
|
||||
620
devtools/client/inspector/rules/views/rule-editor.js
Normal file
620
devtools/client/inspector/rules/views/rule-editor.js
Normal 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;
|
||||
880
devtools/client/inspector/rules/views/text-property-editor.js
Normal file
880
devtools/client/inspector/rules/views/text-property-editor.js
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue