2019-01-22 19:06:39 +03:00
|
|
|
/* 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 EventEmitter = require("devtools/shared/event-emitter");
|
|
|
|
|
|
|
|
// This serves as a local cache for the classes applied to each of the node we care about
|
|
|
|
// here.
|
|
|
|
// The map is indexed by NodeFront. Any time a new node is selected in the inspector, an
|
|
|
|
// entry is added here, indexed by the corresponding NodeFront.
|
|
|
|
// The value for each entry is an array of each of the class this node has. Items of this
|
|
|
|
// array are objects like: { name, isApplied } where the name is the class itself, and
|
|
|
|
// isApplied is a Boolean indicating if the class is applied on the node or not.
|
|
|
|
const CLASSES = new WeakMap();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manages the list classes per DOM elements we care about.
|
|
|
|
* The actual list is stored in the CLASSES const, indexed by NodeFront objects.
|
|
|
|
* The responsibility of this class is to be the source of truth for anyone who wants to
|
|
|
|
* know which classes a given NodeFront has, and which of these are enabled and which are
|
|
|
|
* disabled.
|
|
|
|
* It also reacts to DOM mutations so the list of classes is up to date with what is in
|
|
|
|
* the DOM.
|
|
|
|
* It can also be used to enable/disable a given class, or add classes.
|
|
|
|
*
|
|
|
|
* @param {Inspector} inspector
|
|
|
|
* The current inspector instance.
|
|
|
|
*/
|
2019-02-08 05:13:43 +03:00
|
|
|
class ClassList {
|
|
|
|
constructor(inspector) {
|
|
|
|
EventEmitter.decorate(this);
|
2019-01-22 19:06:39 +03:00
|
|
|
|
2019-02-08 05:13:43 +03:00
|
|
|
this.inspector = inspector;
|
2019-01-22 19:06:39 +03:00
|
|
|
|
2019-02-08 05:13:43 +03:00
|
|
|
this.onMutations = this.onMutations.bind(this);
|
|
|
|
this.inspector.on("markupmutation", this.onMutations);
|
2019-01-22 19:06:39 +03:00
|
|
|
|
2019-02-08 05:13:43 +03:00
|
|
|
this.classListProxyNode = this.inspector.panelDoc.createElement("div");
|
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.inspector.off("markupmutation", this.onMutations);
|
|
|
|
this.inspector = null;
|
|
|
|
this.classListProxyNode = null;
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The current node selection (which only returns if the node is an ELEMENT_NODE type
|
|
|
|
* since that's the only type this model can work with.)
|
|
|
|
*/
|
|
|
|
get currentNode() {
|
|
|
|
if (
|
|
|
|
this.inspector.selection.isElementNode() &&
|
|
|
|
!this.inspector.selection.isPseudoElementNode()
|
|
|
|
) {
|
|
|
|
return this.inspector.selection.nodeFront;
|
|
|
|
}
|
|
|
|
return null;
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The class states for the current node selection. See the documentation of the CLASSES
|
|
|
|
* constant.
|
|
|
|
*/
|
|
|
|
get currentClasses() {
|
|
|
|
if (!this.currentNode) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!CLASSES.has(this.currentNode)) {
|
|
|
|
// Use the proxy node to get a clean list of classes.
|
|
|
|
this.classListProxyNode.className = this.currentNode.className;
|
|
|
|
const nodeClasses = [
|
|
|
|
...new Set([...this.classListProxyNode.classList]),
|
|
|
|
].map(name => {
|
|
|
|
return { name, isApplied: true };
|
|
|
|
});
|
|
|
|
|
|
|
|
CLASSES.set(this.currentNode, nodeClasses);
|
|
|
|
}
|
|
|
|
|
|
|
|
return CLASSES.get(this.currentNode);
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Same as currentClasses, but returns it in the form of a className string, where only
|
|
|
|
* enabled classes are added.
|
|
|
|
*/
|
|
|
|
get currentClassesPreview() {
|
|
|
|
return this.currentClasses
|
|
|
|
.filter(({ isApplied }) => isApplied)
|
|
|
|
.map(({ name }) => name)
|
|
|
|
.join(" ");
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the state for a given class on the current node.
|
|
|
|
*
|
|
|
|
* @param {String} name
|
|
|
|
* The class which state should be changed.
|
|
|
|
* @param {Boolean} isApplied
|
|
|
|
* True if the class should be enabled, false otherwise.
|
|
|
|
* @return {Promise} Resolves when the change has been made in the DOM.
|
|
|
|
*/
|
|
|
|
setClassState(name, isApplied) {
|
|
|
|
// Do the change in our local model.
|
|
|
|
const nodeClasses = this.currentClasses;
|
|
|
|
nodeClasses.find(({ name: cName }) => cName === name).isApplied = isApplied;
|
|
|
|
|
|
|
|
return this.applyClassState();
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add several classes to the current node at once.
|
|
|
|
*
|
|
|
|
* @param {String} classNameString
|
|
|
|
* The string that contains all classes.
|
|
|
|
* @return {Promise} Resolves when the change has been made in the DOM.
|
|
|
|
*/
|
|
|
|
addClassName(classNameString) {
|
|
|
|
this.classListProxyNode.className = classNameString;
|
|
|
|
return Promise.all(
|
|
|
|
[...new Set([...this.classListProxyNode.classList])].map(name => {
|
|
|
|
return this.addClass(name);
|
|
|
|
})
|
|
|
|
);
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a class to the current node at once.
|
|
|
|
*
|
|
|
|
* @param {String} name
|
|
|
|
* The class to be added.
|
|
|
|
* @return {Promise} Resolves when the change has been made in the DOM.
|
|
|
|
*/
|
|
|
|
addClass(name) {
|
|
|
|
// Avoid adding the same class again.
|
|
|
|
if (this.currentClasses.some(({ name: cName }) => cName === name)) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Change the local model, so we retain the state of the existing classes.
|
|
|
|
this.currentClasses.push({ name, isApplied: true });
|
|
|
|
|
|
|
|
return this.applyClassState();
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used internally by other functions like addClass or setClassState. Actually applies
|
|
|
|
* the class change to the DOM.
|
|
|
|
*
|
|
|
|
* @return {Promise} Resolves when the change has been made in the DOM.
|
|
|
|
*/
|
|
|
|
applyClassState() {
|
|
|
|
// If there is no valid inspector selection, bail out silently. No need to report an
|
|
|
|
// error here.
|
|
|
|
if (!this.currentNode) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remember which node we changed and the className we applied, so we can filter out
|
|
|
|
// dom mutations that are caused by us in onMutations.
|
|
|
|
this.lastStateChange = {
|
|
|
|
node: this.currentNode,
|
|
|
|
className: this.currentClassesPreview,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Apply the change to the node.
|
|
|
|
const mod = this.currentNode.startModifyingAttributes();
|
|
|
|
mod.setAttribute("class", this.currentClassesPreview);
|
|
|
|
return mod.apply();
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
onMutations(mutations) {
|
|
|
|
for (const { type, target, attributeName } of mutations) {
|
|
|
|
// Only care if this mutation is for the class attribute.
|
|
|
|
if (type !== "attributes" || attributeName !== "class") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isMutationForOurChange =
|
|
|
|
this.lastStateChange &&
|
|
|
|
target === this.lastStateChange.node &&
|
|
|
|
target.className === this.lastStateChange.className;
|
|
|
|
|
|
|
|
if (!isMutationForOurChange) {
|
|
|
|
CLASSES.delete(target);
|
|
|
|
if (target === this.currentNode) {
|
|
|
|
this.emit("current-node-class-changed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-02-08 05:13:43 +03:00
|
|
|
}
|
|
|
|
}
|
2019-01-22 19:06:39 +03:00
|
|
|
|
|
|
|
module.exports = ClassList;
|