--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -20,17 +20,16 @@ const ClassListPreviewer = require("devt
const {getCssProperties} = require("devtools/shared/fronts/css-properties");
const {
VIEW_NODE_SELECTOR_TYPE,
VIEW_NODE_PROPERTY_TYPE,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_LOCATION_TYPE,
VIEW_NODE_SHAPE_POINT_TYPE,
- VIEW_NODE_SWATCH_TYPE,
VIEW_NODE_VARIABLE_TYPE,
VIEW_NODE_FONT_TYPE,
} = require("devtools/client/inspector/shared/node-types");
const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
const {debounce} = require("devtools/shared/debounce");
const EventEmitter = require("devtools/shared/old-event-emitter");
@@ -348,27 +347,16 @@ CssRuleView.prototype = {
enabled: prop.enabled,
overridden: prop.overridden,
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href,
textProperty: prop,
toggleActive: getShapeToggleActive(node),
point: getShapePoint(node)
};
- } else if (classes.contains("ruleview-swatch") && prop) {
- type = VIEW_NODE_SWATCH_TYPE;
- value = {
- enabled: prop.enabled,
- overridden: prop.overridden,
- property: getPropertyNameAndValue(node).name,
- rule: prop.rule,
- sheetHref: prop.rule.domRule.href,
- textProperty: prop,
- value: node.textContent
- };
} else if ((classes.contains("ruleview-variable") ||
classes.contains("ruleview-unmatched-variable")) && prop) {
type = VIEW_NODE_VARIABLE_TYPE;
value = {
property: getPropertyNameAndValue(node).name,
value: node.textContent,
enabled: prop.enabled,
overridden: prop.overridden,
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -22,17 +22,16 @@ 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";
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
const FONT_FAMILY_CLASS = "ruleview-font-family";
const SHAPE_SWATCH_CLASS = "ruleview-shapeswatch";
/*
* 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 = [
@@ -89,32 +88,32 @@ function TextPropertyEditor(ruleEditor,
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.debounce(this._previewValue, 10, this);
+ this._onInContextEditorCommit = this._onInContextEditorCommit.bind(this);
+ this._onInContextEditorPreview = this._onInContextEditorPreview.bind(this);
this.update = this.update.bind(this);
this.updatePropertyState = this.updatePropertyState.bind(this);
- this._onHoverShapePoint = this._onHoverShapePoint.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.ruleView.highlighters.isEditing)
- || this.popup.isOpen;
+ this.ruleView.tooltips.isEditing) || this.popup.isOpen;
},
/**
* Get the rule to the current text property
*/
get rule() {
return this.prop.rule;
},
@@ -315,18 +314,16 @@ TextPropertyEditor.prototype = {
property: this.prop,
defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
popup: this.popup,
multiline: true,
maxWidth: () => this.container.getBoundingClientRect().width,
cssProperties: this.cssProperties,
cssVariables: this.rule.elementStyle.variables,
});
-
- this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint);
}
},
/**
* Get the path from which to resolve requests for this
* rule's stylesheet.
*
* @return {String} the stylesheet's href.
@@ -505,38 +502,34 @@ TextPropertyEditor.prototype = {
if (gridToggle) {
gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
if (this.ruleView.highlighters.gridHighlighterShown ===
this.ruleView.inspector.selection.nodeFront) {
gridToggle.classList.add("active");
}
}
- this.shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
- if (this.shapeToggle) {
+ let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
+ if (shapeToggle) {
let mode = "css" + name.split("-").map(s => {
return s[0].toUpperCase() + s.slice(1);
}).join("");
- this.shapeToggle.setAttribute("data-mode", mode);
+ shapeToggle.setAttribute("data-mode", mode);
let shapesEditor =
await this.ruleView.highlighters.getInContextEditor("shapesEditor");
let callbacks = {
- onShow: this._onStartEditing,
- onPreview: this._onSwatchPreview,
- onCommit: this._onSwatchCommit,
- onRevert: this._onSwatchRevert
+ onPreview: this._onInContextEditorPreview,
+ onCommit: this._onInContextEditorCommit
};
- if (shapesEditor.activeSwatch) {
- shapesEditor.replaceSwatch(shapesEditor.activeSwatch, this.shapeToggle, callbacks);
- } else {
- shapesEditor.addSwatch(this.shapeToggle, callbacks);
- }
+ shapesEditor.link(this.prop, shapeToggle, callbacks);
+ // Mark this toggle if this property is being currently edited; unmark otherwise.
+ shapeToggle.classList.toggle("active", shapesEditor.activeProperty === this.prop);
}
// 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;
@@ -831,39 +824,30 @@ TextPropertyEditor.prototype = {
/**
* 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: async function (direction) {
+ remove: function (direction) {
if (this._colorSwatchSpans && this._colorSwatchSpans.length) {
for (let span of this._colorSwatchSpans) {
this.ruleView.tooltips.getTooltip("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);
}
}
- if (this.shapeToggle) {
- let shapesEditor =
- await this.ruleView.highlighters.getInContextEditor("shapesEditor");
- if (shapesEditor) {
- shapesEditor.removeSwatch(this.shapeToggle);
- }
- // shapesEditor.off("hover-shape-point")
- }
-
this.element.remove();
this.ruleEditor.rule.editClosestTextProperty(this.prop, direction);
this.nameSpan.textProperty = null;
this.valueSpan.textProperty = null;
this.prop.remove();
},
/**
@@ -1049,70 +1033,29 @@ TextPropertyEditor.prototype = {
* @return {Boolean} true if the property is a `display: [inline-]grid` declaration.
*/
isDisplayGrid: function () {
return this.prop.name === "display" &&
(this.prop.value === "grid" || this.prop.value === "inline-grid");
},
/**
- * Highlight the given shape point in the rule view. Called when "hover-shape-point"
- * event is emitted.
+ * Live preview this property, without committing changes.
*
- * @param {Event} event
- * The "hover-shape-point" event.
- * @param {String} point
- * The point to highlight.
+ * @param {String} value
+ * The value to set the current property to.
*/
- _onHoverShapePoint: function (event, point) {
- // If there is no shape toggle, or it is not active, return.
- let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch.active");
- if (!shapeToggle) {
- return;
- }
-
- let view = this.ruleView;
- let { highlighters } = view;
- let ruleViewEl = view.element;
- let selector = `.ruleview-shape-point.active`;
- for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
- this._toggleShapePointActive(pointNode, false);
- }
-
- if (typeof point === "string") {
- if (point.includes(",")) {
- point = point.split(",")[0];
- }
- // Because one inset value can represent multiple points, inset points use classes
- // instead of data.
- selector = (INSET_POINT_TYPES.includes(point)) ?
- `.ruleview-shape-point.${point}` :
- `.ruleview-shape-point[data-point='${point}']`;
- for (let pointNode of this.valueSpan.querySelectorAll(selector)) {
- let nodeInfo = view.getNodeInfo(pointNode);
- if (highlighters.isRuleViewShapePoint(nodeInfo)) {
- this._toggleShapePointActive(pointNode, true);
- }
- }
- }
+ _onInContextEditorPreview(value) {
+ this.ruleEditor.rule.previewPropertyValue(this.prop, value);
},
/**
- * Toggle the class "active" on the given shape point in the rule view if the current
- * inspector selection is highlighted by the shapes highlighter.
+ * Commit this property value. Triggers editor update.
*
- * @param {NodeFront} node
- * The NodeFront of the shape point to toggle
- * @param {Boolean} active
- * Whether the shape point should be active
+ * @param {String} value
+ * The value to set the current property to.
*/
- _toggleShapePointActive: function (node, active) {
- let { highlighters } = this.ruleView;
- if (highlighters.inspector.selection.nodeFront !=
- highlighters.shapesHighlighterShown) {
- return;
- }
-
- node.classList.toggle("active", active);
- },
+ _onInContextEditorCommit(value) {
+ this.prop.setValue(value);
+ }
};
module.exports = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -49,50 +49,35 @@ class HighlightersOverlay {
grid: {},
shapes: {},
};
this.onClick = this.onClick.bind(this);
this.onMarkupMutation = this.onMarkupMutation.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
- this.onShapeHover = this.onShapeHover.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.hideFlexboxHighlighter = this.hideFlexboxHighlighter.bind(this);
this.hideGridHighlighter = this.hideGridHighlighter.bind(this);
this.hideShapesHighlighter = this.hideShapesHighlighter.bind(this);
this.showFlexboxHighlighter = this.showFlexboxHighlighter.bind(this);
this.showGridHighlighter = this.showGridHighlighter.bind(this);
this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
this._handleRejection = this._handleRejection.bind(this);
- this._onHighlighterEvent = this._onHighlighterEvent.bind(this);
this._onShapesHighlighterShown = this._onShapesHighlighterShown.bind(this);
this._onShapesHighlighterHidden = this._onShapesHighlighterHidden.bind(this);
// Add inspector events, not specific to a given view.
this.inspector.on("markupmutation", this.onMarkupMutation);
this.inspector.target.on("will-navigate", this.onWillNavigate);
- // Store for callbacks for events emitted by highlighters
- this.highligherEventHandlers = {};
EventEmitter.decorate(this);
}
/**
- * Cycle though all registered in-context editors and return true as soon as _any one_
- * of them is found to be in editing mode. Called by TextPropertyEditor.editing
- */
- get isEditing() {
- return Object.keys(this.editors).some((key) => {
- let editor = this.editors[key];
- return (typeof (editor.isEditing) === "function" && editor.isEditing());
- });
- }
-
- /**
* Returns whether `node` is somewhere inside the DOM of the rule view.
*
* @param {DOMNode} node
* @return {Boolean}
*/
isRuleView(node) {
return !!node.closest("#ruleview-panel");
}
@@ -109,18 +94,16 @@ class HighlightersOverlay {
return;
}
let el = view.element;
el.addEventListener("click", this.onClick, true);
el.addEventListener("mousemove", this.onMouseMove);
el.addEventListener("mouseout", this.onMouseOut);
el.ownerDocument.defaultView.addEventListener("mouseout", this.onMouseOut);
- this.highligherEventHandlers["shape-hover-on"] = this.onShapeHover;
- this.highligherEventHandlers["shape-hover-off"] = this.onShapeHover;
}
/**
* Remove the overlay from the given view. This will stop tracking mouse movement and
* showing highlighters.
*
* @param {CssRuleView|CssComputedView|LayoutView} view
* Either the rule-view or computed-view panel to remove the highlighters
@@ -130,17 +113,16 @@ class HighlightersOverlay {
if (!this.supportsHighlighters) {
return;
}
let el = view.element;
el.removeEventListener("click", this.onClick, true);
el.removeEventListener("mousemove", this.onMouseMove);
el.removeEventListener("mouseout", this.onMouseOut);
- this.highligherEventHandlers = {};
}
/**
* Show the shapes highlighter for the given element with a shape.
* This method delegates to the in-context shapes editor which wraps the
* shapes highlighter with additional functionality required for passing changes
* back to the rule view.
*
@@ -180,42 +162,37 @@ class HighlightersOverlay {
let selector = await node.getUniqueSelector();
this.state.shapes = { selector, options, url };
} catch (e) {
this._handleRejection(e);
}
}
/**
- * Hide the shapes highlighter for the given element.
+ * Hide the shapes highlighter if visible.
* This method delegates the to the in-context shapes editor which wraps
* the shapes highlighter with additional functionality.
- *
- * @param {NodeFront} node
- * The NodeFront of the element with a shape to unhighlight.
*/
- async hideShapesHighlighter(node) {
+ async hideShapesHighlighter() {
let shapesEditor = await this.getInContextEditor("shapesEditor");
if (!shapesEditor) {
return;
}
- shapesEditor.hide(node);
+ shapesEditor.hide();
}
/**
* Called after the shapes highlighter was hidden.
*
* @param {Object} data
* Data associated with the event.
* Contains:
* - {NodeFront} node: The NodeFront of the element that was highlighted.
*/
_onShapesHighlighterHidden(data) {
- let { node } = data;
- this._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown,
this.state.shapes.options);
this.shapesHighlighterShown = null;
this.state.shapes = {};
}
/**
* Show the shapes highlighter for the given element, with the given point highlighted.
@@ -444,33 +421,16 @@ class HighlightersOverlay {
await this.highlighters.GeometryEditorHighlighter.hide();
this.emit("geometry-editor-highlighter-hidden");
this.geometryEditorHighlighterShown = null;
}
/**
- * Handle events emitted by the highlighter.
- * Find any callback assigned to the event type and call it with the given data object.
- *
- * @param {Object} data
- * The data object sent in the event.
- */
- _onHighlighterEvent(data) {
- const handler = this.highligherEventHandlers[data.type];
- if (!handler || typeof handler !== "function") {
- return;
- }
- handler.call(this, data);
-
- this.emit("highlighter-event-handled");
- }
-
- /**
* Restores the saved flexbox highlighter state.
*/
async restoreFlexboxState() {
try {
await this.restoreState("flexbox", this.state.flexbox, this.showFlexboxHighlighter);
} catch (e) {
this._handleRejection(e);
}
@@ -486,21 +446,21 @@ class HighlightersOverlay {
this._handleRejection(e);
}
}
/**
* Restores the saved shape highlighter state.
*/
async restoreShapeState() {
- try {
- await this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter);
- } catch (e) {
- this._handleRejection(e);
- }
+ // try {
+ // await this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter);
+ // } catch (e) {
+ // this._handleRejection(e);
+ // }
}
/**
* Helper function called by restoreFlexboxState, restoreGridState and
* restoreShapeState. Restores the saved highlighter state for the given highlighter
* and their state.
*
* @param {String} name
@@ -547,17 +507,17 @@ class HighlightersOverlay {
switch (type) {
case "shapesEditor":
let highlighter = await this._getHighlighter("ShapesHighlighter");
if (!highlighter) {
return null;
}
const ShapesInContextEditor = require("devtools/client/shared/widgets/ShapesInContextEditor");
- editor = new ShapesInContextEditor(highlighter, this.inspector);
+ editor = new ShapesInContextEditor(highlighter, this.inspector, this.state);
editor.on("show", this._onShapesHighlighterShown);
editor.on("hide", this._onShapesHighlighterHidden);
break;
default:
throw new Error(`Unsupported in-context editor '${name}'`);
}
this.editors[type] = editor;
@@ -586,49 +546,27 @@ class HighlightersOverlay {
} catch (e) {
// Ignore any error
}
if (!highlighter) {
return null;
}
- highlighter.on("highlighter-event", this._onHighlighterEvent);
this.highlighters[type] = highlighter;
return highlighter;
}
_handleRejection(error) {
if (!this.destroyed) {
console.error(error);
}
}
/**
- * Handler for the "shape-change" event of the ShapesHighlighter.
- * Preivews the new shape live on the page as events come in and persists the shape
- * value to the Rule view in a throttled manner because this is an expensive operation.
- *
- * @param {Object} data
- * Data associated with the "shape-change" event.
- * Contains:
- * - {String} value: the new shape value
- * - {String} type: the event type ("shape-change")
- */
- onShapeHover(data) {
- if (data.type === "shape-hover-on") {
- this.state.shapes.hoverPoint = data.point;
- this.emit("hover-shape-point", data.point);
- } else if (data.type === "shape-hover-off") {
- this.state.shapes.hoverPoint = null;
- this.emit("hover-shape-point", null);
- }
- }
-
- /**
* Toggle all the icons with the given selector in the rule view if the current
* inspector selection is the highlighted node.
*
* @param {NodeFront} node
* The NodeFront of the element with a shape to highlight.
* @param {Boolean} active
* Whether or not the shape icon should be active.
* @param {String} selector
@@ -832,17 +770,16 @@ class HighlightersOverlay {
let nodeInfo = view.getNodeInfo(event.target);
if (!nodeInfo) {
return;
}
if (this.isRuleViewShapePoint(nodeInfo)) {
let { point } = nodeInfo.value;
this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, point);
- this.emit("hover-shape-point", point);
return;
}
// Choose the type of highlighter required for the hovered node.
let type;
if (this._isRuleViewTransform(nodeInfo) ||
this._isComputedViewTransform(nodeInfo)) {
type = "CssTransformHighlighter";
@@ -870,17 +807,16 @@ class HighlightersOverlay {
// Otherwise, hide the highlighter.
let view = this.isRuleView(this._lastHovered) ?
this.inspector.getPanel("ruleview").view :
this.inspector.getPanel("computedview").computedView;
let nodeInfo = view.getNodeInfo(this._lastHovered);
if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) {
this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, null);
- this.emit("hover-shape-point", null);
}
this._lastHovered = null;
this._hideHoveredHighlighter();
}
/**
* Handler function for "markupmutation" events. Hides the flexbox/grid/shapes
* highlighter if the flexbox/grid/shapes container is no longer in the DOM tree.
@@ -928,19 +864,16 @@ class HighlightersOverlay {
* Destroy this overlay instance, removing it from the view and destroying
* all initialized highlighters.
*/
destroy() {
this.destroyEditors();
for (let type in this.highlighters) {
if (this.highlighters[type]) {
- if (this.highlighters[type].off) {
- this.highlighters[type].off("highlighter-event", this._onHighlighterEvent);
- }
this.highlighters[type].finalize();
this.highlighters[type] = null;
}
}
// Remove inspector events.
this.inspector.off("markupmutation", this.onMarkupMutation);
this.inspector.target.off("will-navigate", this.onWillNavigate);
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -13,9 +13,8 @@
exports.VIEW_NODE_SELECTOR_TYPE = 1;
exports.VIEW_NODE_PROPERTY_TYPE = 2;
exports.VIEW_NODE_VALUE_TYPE = 3;
exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
exports.VIEW_NODE_LOCATION_TYPE = 5;
exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
exports.VIEW_NODE_VARIABLE_TYPE = 7;
exports.VIEW_NODE_FONT_TYPE = 8;
-exports.VIEW_NODE_SWATCH_TYPE = 9;
--- a/devtools/client/shared/widgets/ShapesInContextEditor.js
+++ b/devtools/client/shared/widgets/ShapesInContextEditor.js
@@ -3,262 +3,300 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const { debounce } = require("devtools/shared/debounce");
class ShapesInContextEditor {
- constructor(highlighter, inspector) {
+ constructor(highlighter, inspector, state) {
EventEmitter.decorate(this);
- this._isEditing = false;
this._highligherEventHandlers = {};
this._highligherEventHandlers["shape-change"] = this._onShapeChange;
this._highligherEventHandlers["shape-hover-on"] = this._onShapeHover;
this._highligherEventHandlers["shape-hover-off"] = this._onShapeHover;
this._onHighlighterEvent = this._onHighlighterEvent.bind(this);
+ this._onNodeFrontChanged = this._onNodeFrontChanged.bind(this);
+ this._onRuleViewChanged = this._onRuleViewChanged.bind(this);
this._onSwatchClick = this._onSwatchClick.bind(this);
- this._onNodeFrontChanged = this._onNodeFrontChanged.bind(this);
- // When a swatch is clicked, and for as long as the highlighter is shown,
- // activeSwatch will hold the reference to the swatch DOM element
- // that was clicked. It is a key in the this.swatches Map which holds
- // callbacks that map changes back to the original CSS declaration.
this.activeSwatch = null;
- // Commit causes expensive changes in Rule view so we debounce it.
- this.commit = debounce(this.commit, 150, this);
+ this.activeProperty = null;
+
+ // Commit triggers expensive changes so we debounce it.
+ this.commit = debounce(this.commit, 200, this);
this.inspector = inspector;
this.highlighter = highlighter;
this.highlighter.on("highlighter-event", this._onHighlighterEvent);
// Refence to the NodeFront currently being highlighted.
this.highlighterTargetNode = null;
- // All target swatches are kept in a map, indexed by swatch DOM elements
- this.swatches = new Map();
+ // Map with data required to preview and persist shape value changes to the Rule view.
+ // keys - TextProperty instances relevant for shape editor (clip-path, shape-outside).
+ // values - objects with references to swatch elements that trigger the shape editor
+ // and callbacks used to preview and persist shape value changes.
+ this.links = new Map();
+
+ // Reference to Rule view used to listen for changes
+ this.ruleView = this.inspector.getPanel("ruleview").view;
+ this.ruleView.on("ruleview-changed", this._onRuleViewChanged);
+
+ // Reference of |state| from HighlightersOverlay.
+ this.state = state;
}
- /**
- * Add a new swatch DOM element to the list of swatch elements this editor
- * knows about. That means from now on, clicking on that swatch will
- * toggle the editor.
- *
- * @param {node} swatchEl
- * The element to add
- * @param {object} callbacks
- * Callbacks that will be executed when the editor wants to preview a
- * value change, or revert a change, or commit a change.
- * - onShow: called when the highlighter is shown.
- * - onPreview: called when there is a shape change event from the highlighter.
- * - onRevert:
- * - onCommit: called when the highlighter is closed.
- */
- addSwatch(swatchEl, callbacks = {}) {
- if (!callbacks.onShow) {
- callbacks.onShow = function () {};
+ link(prop, swatch, callbacks = {}) {
+ if (this.links.has(prop)) {
+ this.replaceSwatch(prop, swatch);
+ return;
}
if (!callbacks.onPreview) {
callbacks.onPreview = function () {};
}
- if (!callbacks.onRevert) {
- callbacks.onRevert = function () {};
- }
if (!callbacks.onCommit) {
callbacks.onCommit = function () {};
}
- this.swatches.set(swatchEl, { callbacks });
- swatchEl.addEventListener("click", this._onSwatchClick);
+ swatch.addEventListener("click", this._onSwatchClick);
+ this.links.set(prop, { swatch, callbacks });
}
- removeSwatch(swatchEl) {
- if (this.swatches.has(swatchEl)) {
- if (this.activeSwatch === swatchEl) {
- this.hide();
- this.activeSwatch = null;
- }
- swatchEl.removeEventListener("click", this._onSwatchClick);
- this.swatches.delete(swatchEl);
+ unlink(prop) {
+ let data = this.links.get(prop);
+ if (!data || !data.swatch) {
+ return;
+ }
+ if (this.activeProperty === prop) {
+ this.hide();
+ }
+
+ data.swatch.classList.remove("active");
+ data.swatch.removeEventListener("click", this._onSwatchClick);
+ this.links.delete(prop);
+ }
+
+ replaceSwatch(prop, swatch) {
+ let data = this.links.get(prop);
+ if (data.swatch) {
+ // Cleanup old
+ data.swatch.removeEventListener("click", this._onSwatchClick);
+ data.swatch = undefined;
+ // Setup new
+ swatch.addEventListener("click", this._onSwatchClick);
+ data.swatch = swatch;
}
}
- replaceSwatch(oldSwatchEl, newSwatchEl, callbacks = {}) {
- if (!oldSwatchEl || !newSwatchEl) {
- return;
+ /**
+ * Called when the element style changes from the Rule view.
+ * If the TextProperty we're acting on isn't enabled anymore or overridden,
+ * turn off the shapes highlighter.
+ */
+ _onRuleViewChanged() {
+ if (this.activeProperty &&
+ (!this.activeProperty.enabled || this.activeProperty.overridden)) {
+ this.hide();
}
+ }
- this.addSwatch(newSwatchEl, callbacks);
-
- // Replace old swatch with new one if it was active
- if (this.activeSwatch === oldSwatchEl) {
- this.activeSwatch = newSwatchEl;
- this.activeSwatch.classList.add("active");
+ _onSwatchClick(event) {
+ event.stopPropagation();
+ for (let [prop, data] of this.links) {
+ if (data.swatch == event.target) {
+ this.activeSwatch = data.swatch;
+ this.activeSwatch.classList.add("active");
+ this.activeProperty = prop;
+ }
}
- oldSwatchEl.removeEventListener("click", this._onSwatchClick);
- this.swatches.delete(oldSwatchEl);
- }
+ let nodeFront = this.inspector.selection.nodeFront;
+ let options = {
+ mode: event.target.dataset.mode,
+ transformMode: event.metaKey || event.ctrlKey
+ };
- async _onSwatchClick(event) {
- event.stopPropagation();
- let swatch = this.swatches.get(event.target);
- if (swatch) {
- this.activeSwatch = event.target;
-
- let nodeFront = this.inspector.selection.nodeFront;
- let options = {
- mode: event.target.dataset.mode,
- transformMode: event.metaKey || event.ctrlKey
- };
-
- await this.toggle(nodeFront, options);
- }
+ this.toggle(nodeFront, options);
}
/**
* Toggle the shapes highlighter for the given element.
*
- * @param {NodeFront} node
- * The NodeFront of the element with a shape to highlight.
- * @param {Object} options
- * Object used for passing options to the shapes highlighter.
+ * @param {NodeFront} node
+ * The NodeFront of the element with a shape to highlight.
+ * @param {Object} options
+ * Object used for passing options to the shapes highlighter.
*/
async toggle(node, options) {
if (node == this.highlighterTargetNode) {
if (!options.transformMode) {
- await this.hide(node);
+ await this.hide();
return;
}
- options.transformMode =
- !this.inspector.highlighters.state.shapes.options.transformMode;
+ options.transformMode = !this.state.shapes.options.transformMode;
}
await this.show(node, options);
}
async show(node, options) {
let isShown = await this.highlighter.show(node, options);
if (!isShown) {
return;
}
- // When the shapes highlighter is shown as a result of restore operation on page
- // refresh, it means editing didn't originate in the Rule view. We must identify
- // the swatch associated with it so it updates values in the right CSS declaration.
- if (!this.activeSwatch) {
- this.activeSwatch = this.findActiveSwatch(`.ruleview-shapeswatch`);
- }
-
- let swatch = this.swatches.get(this.activeSwatch);
- if (swatch) {
- swatch.callbacks.onShow();
- }
-
this.inspector.selection.on("detached-front", this._onNodeFrontChanged);
this.inspector.selection.on("new-node-front", this._onNodeFrontChanged);
- this.activeSwatch.classList.add("active");
this.highlighterTargetNode = node;
this.emit("show", { node, options });
}
- async hide(node) {
+ async hide() {
await this.highlighter.hide();
+
this.activeSwatch.classList.remove("active");
this.activeSwatch = null;
+ this.activeProperty = null;
+
this.emit("hide", { node: this.highlighterTargetNode });
this.inspector.selection.off("detached-front", this._onNodeFrontChanged);
this.inspector.selection.off("new-node-front", this._onNodeFrontChanged);
this.highlighterTargetNode = null;
}
- findActiveSwatch(selector) {
- for (let [swatch] of this.swatches) {
- if (swatch.matches(selector)) {
- return swatch;
- }
- }
-
- return null;
- }
-
- isEditing() {
- return this._isEditing;
- }
-
+ /**
+ * Handle events emitted by the highlighter.
+ * Find any callback assigned to the event type and call it with the given data object.
+ *
+ * @param {Object} data
+ * The data object sent in the event.
+ */
_onHighlighterEvent(data) {
const handler = this._highligherEventHandlers[data.type];
if (!handler || typeof handler !== "function") {
return;
}
handler.call(this, data);
+ this.inspector.highlighters.emit("highlighter-event-handled");
}
/**
- * Clean up when node selection changes because Rule view and
- * TextPropertyEditor instances are not automatically destroyed when selection
- * changes.
+ * Clean up when node selection changes because Rule view and TextPropertyEditor
+ * instances are not automatically destroyed when selection changes.
*/
_onNodeFrontChanged() {
this.hide();
- this.swatches.clear();
+ this.links.clear();
}
+ /**
+ * Called when there's an updated shape value from the highlighter.
+ *
+ * @param {Object} data
+ * Data associated with the "shape-change" event.
+ * Contains:
+ * - {String} value: the new shape value.
+ * - {String} type: the event type ("shape-change").
+ */
_onShapeChange(data) {
this.preview(data.value);
- }
-
- _onShapeHover(data) {
+ this.commit(data.value);
}
- preview(value) {
- if (this.activeSwatch) {
- // Update the text of CSS value in the rule view. This makes it inert.
- // When .commit() is called, the value is parsed and its the DOM structure rebuilt.
- this.activeSwatch.nextSibling.textContent = value;
+ /**
+ * Called when the mouse moves over or away from a coordinate point inside the shape
+ * highlighter.
+ * Highlights the corresponding coordinate node in the shape value from the Rule view.
+ *
+ * @param {Object} data
+ * Data associated with the "shape-hover" event.
+ * Contains:
+ * - {String|null} point: coordinate to highlight or null if nothing to highlight
+ * - {String} type: the event type ("shape-hover-on" or "shape-hover-on").
+ */
+ _onShapeHover(data) {
+ if (!this.activeProperty) {
+ console.warn("SHAPEHOVER: no active property");
+ return;
+ }
- /**
- * TextPropertyEditor._previewValue() pings all subscribed editors
- * (mostly tooltips and inline editors) to check if they are in
- * "editing on" mode when allowing to preview the value.
- *
- * Here we're toggling the perceived "editing on" state of the shape
- * highlighter just for preview. Once shown, the shapes highlighter is,
- * technically, always in "editing on" state, but that messes with logic
- * in TextPropertyEditor which expects all editors to be off in order to
- * correctly handle behavirous like disabling new properties that overwrite
- * existing properties.
- */
- this._isEditing = true;
- let swatch = this.swatches.get(this.activeSwatch);
- swatch.callbacks.onPreview(value);
- this.commit();
- // Pretend not to be in editing mode anymore.
- this._isEditing = false;
+ let shapeValueEl = this.links.get(this.activeProperty).swatch.nextSibling;
+ if (!shapeValueEl) {
+ return;
+ }
+ let pointSelector = ".ruleview-shape-point";
+ // First, unmark all highlighted coordinate nodes from Rule view
+ for (let node of shapeValueEl.querySelectorAll(`${pointSelector}.active`)) {
+ node.classList.remove("active");
+ }
+
+ // Exit if there's no coordinate to highlight. For example, if it was just a mouseout.
+ if (typeof data.point !== "string") {
+ return;
+ }
+
+ let point = (data.point.includes(",")) ? data.point.split(",")[0] : data.point;
+
+ /**
+ * Build selector for coordinate nodes in shape value that must be highlighted.
+ * Coordinate values for inset use class names instead of data attributes because
+ * a single node may represent multiple coordinates in shorthand notation.
+ * Example: inset(50px); The node wrapping 50px represents all four inset coordinates.
+ */
+ const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+ let selector = INSET_POINT_TYPES.includes(point) ?
+ `${pointSelector}.${point}` :
+ `${pointSelector}[data-point='${point}']`;
+
+ for (let node of shapeValueEl.querySelectorAll(selector)) {
+ node.classList.add("active");
}
}
- revert() {
+ /**
+ * Preview this shape value on the element but do commit the changes to the Rule view.
+ *
+ * @param {String} value
+ * The shape value to set the current property to
+ */
+ preview(value) {
+ if (!this.activeProperty) {
+ console.warn("PREVIEW: no active property");
+ return;
+ }
+ let data = this.links.get(this.activeProperty);
+ // Update the element's styles to see live results.
+ data.callbacks.onPreview(value);
+ // Update the text of CSS value in the Rule view. This makes it inert.
+ // When .commit() is called, the value is reparsed and its DOM structure rebuilt.
+ data.swatch.nextSibling.textContent = value;
}
/**
- * Commits changes to the Rule view. This triggers an expensive operation which
- * rebuilds the DOM of the TextPropertyEditor and attaches events, therefore
- * it is called in a debounced manner.
+ * Commit this shape value change which triggers an expensive operation that rebuilds
+ * part of the DOM of the TextPropertyEditor. Called in a debounced manner.
+ *
+ * @param {String} value
+ * The shape value to set the current property to
*/
- commit() {
- if (this.activeSwatch) {
- let swatch = this.swatches.get(this.activeSwatch);
- swatch.callbacks.onCommit();
+ commit(value) {
+ if (!this.activeProperty) {
+ console.warn("COMMIT: no active property");
+ return;
}
+ let data = this.links.get(this.activeProperty);
+ data.callbacks.onCommit(value);
}
destroy() {
- this.swatches.clear();
- this.activeSwatch = null;
- this.highlighter.off("highlighter-event");
+ this.hide();
+ // TODO remove links one by one.
+ this.links.clear();
+ this.highlighter.off("highlighter-event", this._onHighlighterEvent);
+ this.ruleView.off("ruleview-changed", this._onRuleViewChanged);
this._highligherEventHandlers = {};
}
}
module.exports = ShapesInContextEditor;
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -477,17 +477,18 @@
background-size: 1em;
}
.ruleview-grid {
background: url("chrome://devtools/skin/images/grid.svg");
border-radius: 0;
}
-.ruleview-shape-point.active {
+.ruleview-shape-point.active,
+.ruleview-shapeswatch.active + .ruleview-shape > .ruleview-shape-point:hover {
background-color: var(--rule-highlight-background-color);
}
.ruleview-colorswatch::before {
content: '';
background-color: #eee;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);