зеркало из https://github.com/mozilla/gecko-dev.git
548 строки
18 KiB
JavaScript
548 строки
18 KiB
JavaScript
/* 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 flags = require("devtools/shared/flags");
|
|
const { throttle } = require("devtools/shared/throttle");
|
|
|
|
const {
|
|
clearFlexbox,
|
|
updateFlexbox,
|
|
updateFlexboxColor,
|
|
updateFlexboxHighlighted,
|
|
} = require("./actions/flexbox");
|
|
|
|
loader.lazyRequireGetter(this, "parseURL", "devtools/client/shared/source-utils", true);
|
|
loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage");
|
|
|
|
const FLEXBOX_COLOR = "#9400FF";
|
|
|
|
class FlexboxInspector {
|
|
constructor(inspector, window) {
|
|
this.document = window.document;
|
|
this.inspector = inspector;
|
|
this.selection = inspector.selection;
|
|
this.store = inspector.store;
|
|
this.walker = inspector.walker;
|
|
|
|
this.onHighlighterShown = this.onHighlighterShown.bind(this);
|
|
this.onHighlighterHidden = this.onHighlighterHidden.bind(this);
|
|
this.onNavigate = this.onNavigate.bind(this);
|
|
this.onReflow = throttle(this.onReflow, 500, this);
|
|
this.onSetFlexboxOverlayColor = this.onSetFlexboxOverlayColor.bind(this);
|
|
this.onSidebarSelect = this.onSidebarSelect.bind(this);
|
|
this.onToggleFlexboxHighlighter = this.onToggleFlexboxHighlighter.bind(this);
|
|
this.onUpdatePanel = this.onUpdatePanel.bind(this);
|
|
|
|
this.init();
|
|
}
|
|
|
|
// Get the highlighters overlay from the Inspector.
|
|
get highlighters() {
|
|
if (!this._highlighters) {
|
|
// highlighters is a lazy getter in the inspector.
|
|
this._highlighters = this.inspector.highlighters;
|
|
}
|
|
|
|
return this._highlighters;
|
|
}
|
|
|
|
async init() {
|
|
if (!this.inspector) {
|
|
return;
|
|
}
|
|
try {
|
|
this.hasGetCurrentFlexbox = await this.inspector.target.actorHasMethod("layout",
|
|
"getCurrentFlexbox");
|
|
this.layoutInspector = await this.walker.getLayoutInspector();
|
|
} catch (e) {
|
|
// These calls might fail if called asynchrously after the toolbox is finished
|
|
// closing.
|
|
return;
|
|
}
|
|
|
|
if (flags.testing) {
|
|
// In tests, we start listening immediately to avoid having to simulate a mousemove.
|
|
this.highlighters.on("flexbox-highlighter-hidden", this.onHighlighterHidden);
|
|
this.highlighters.on("flexbox-highlighter-shown", this.onHighlighterShown);
|
|
} else {
|
|
this.document.addEventListener("mousemove", () => {
|
|
this.highlighters.on("flexbox-highlighter-hidden", this.onHighlighterHidden);
|
|
this.highlighters.on("flexbox-highlighter-shown", this.onHighlighterShown);
|
|
}, { once: true });
|
|
}
|
|
|
|
this.inspector.sidebar.on("select", this.onSidebarSelect);
|
|
|
|
this.onSidebarSelect();
|
|
}
|
|
|
|
destroy() {
|
|
if (this._highlighters) {
|
|
this.highlighters.off("flexbox-highlighter-hidden", this.onHighlighterHidden);
|
|
this.highlighters.off("flexbox-highlighter-shown", this.onHighlighterShown);
|
|
}
|
|
|
|
this.selection.off("new-node-front", this.onUpdatePanel);
|
|
this.inspector.sidebar.off("select", this.onSidebarSelect);
|
|
this.inspector.off("new-root", this.onNavigate);
|
|
|
|
this.inspector.reflowTracker.untrackReflows(this, this.onReflow);
|
|
|
|
this._customHostColors = null;
|
|
this._highlighters = null;
|
|
this._overlayColor = null;
|
|
this.document = null;
|
|
this.hasGetCurrentFlexbox = null;
|
|
this.inspector = null;
|
|
this.layoutInspector = null;
|
|
this.selection = null;
|
|
this.store = null;
|
|
this.walker = null;
|
|
}
|
|
|
|
getComponentProps() {
|
|
return {
|
|
onSetFlexboxOverlayColor: this.onSetFlexboxOverlayColor,
|
|
onToggleFlexboxHighlighter: this.onToggleFlexboxHighlighter,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns an object containing the custom flexbox colors for different hosts.
|
|
*
|
|
* @return {Object} that maps a host name to a custom flexbox color for a given host.
|
|
*/
|
|
async getCustomHostColors() {
|
|
if (this._customHostColors) {
|
|
return this._customHostColors;
|
|
}
|
|
|
|
// Cache the custom host colors to avoid refetching from async storage.
|
|
this._customHostColors = await asyncStorage.getItem("flexboxInspectorHostColors")
|
|
|| {};
|
|
return this._customHostColors;
|
|
}
|
|
|
|
/**
|
|
* Returns the flex container properties for a given node. If the given node is a flex
|
|
* item, it attempts to fetch the flex container of the parent node of the given node.
|
|
*
|
|
* @param {NodeFront} nodeFront
|
|
* The NodeFront to fetch the flex container properties.
|
|
* @param {Boolean} onlyLookAtParents
|
|
* Whether or not to only consider the parent node of the given node.
|
|
* @return {Object} consisting of the given node's flex container's properties.
|
|
*/
|
|
async getFlexContainerProps(nodeFront, onlyLookAtParents = false) {
|
|
const flexboxFront = await this.layoutInspector.getCurrentFlexbox(nodeFront,
|
|
onlyLookAtParents);
|
|
|
|
if (!flexboxFront) {
|
|
return null;
|
|
}
|
|
|
|
// If the FlexboxFront doesn't yet have access to the NodeFront for its container,
|
|
// then get it from the walker. This happens when the walker hasn't seen this
|
|
// particular DOM Node in the tree yet or when we are connected to an older server.
|
|
let containerNodeFront = flexboxFront.containerNodeFront;
|
|
if (!containerNodeFront) {
|
|
containerNodeFront = await this.walker.getNodeFromActor(flexboxFront.actorID,
|
|
["containerEl"]);
|
|
}
|
|
|
|
const flexItems = await this.getFlexItems(flexboxFront);
|
|
|
|
// If the current selected node is a flex item, display its flex item sizing
|
|
// properties.
|
|
let flexItemShown = null;
|
|
if (onlyLookAtParents) {
|
|
flexItemShown = this.selection.nodeFront.actorID;
|
|
} else {
|
|
const selectedFlexItem = flexItems.find(item =>
|
|
item.nodeFront === this.selection.nodeFront);
|
|
if (selectedFlexItem) {
|
|
flexItemShown = selectedFlexItem.nodeFront.actorID;
|
|
}
|
|
}
|
|
|
|
return {
|
|
actorID: flexboxFront.actorID,
|
|
flexItems,
|
|
flexItemShown,
|
|
isFlexItemContainer: onlyLookAtParents,
|
|
nodeFront: containerNodeFront,
|
|
properties: flexboxFront.properties,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns an array of flex items object for the given flex container front.
|
|
*
|
|
* @param {FlexboxFront} flexboxFront
|
|
* A flex container FlexboxFront.
|
|
* @return {Array} of objects containing the flex item front properties.
|
|
*/
|
|
async getFlexItems(flexboxFront) {
|
|
const flexItemFronts = await flexboxFront.getFlexItems();
|
|
const flexItems = [];
|
|
|
|
for (const flexItemFront of flexItemFronts) {
|
|
// Fetch the NodeFront of the flex items.
|
|
let itemNodeFront = flexItemFront.nodeFront;
|
|
if (!itemNodeFront) {
|
|
itemNodeFront = await this.walker.getNodeFromActor(flexItemFront.actorID,
|
|
["element"]);
|
|
}
|
|
|
|
flexItems.push({
|
|
actorID: flexItemFront.actorID,
|
|
computedStyle: flexItemFront.computedStyle,
|
|
flexItemSizing: flexItemFront.flexItemSizing,
|
|
nodeFront: itemNodeFront,
|
|
properties: flexItemFront.properties,
|
|
});
|
|
}
|
|
|
|
return flexItems;
|
|
}
|
|
|
|
/**
|
|
* Returns the custom overlay color for the current host or the default flexbox color.
|
|
*
|
|
* @return {String} overlay color.
|
|
*/
|
|
async getOverlayColor() {
|
|
if (this._overlayColor) {
|
|
return this._overlayColor;
|
|
}
|
|
|
|
// Cache the overlay color for the current host to avoid repeatably parsing the host
|
|
// and fetching the custom color from async storage.
|
|
const customColors = await this.getCustomHostColors();
|
|
const currentUrl = this.inspector.target.url;
|
|
// Get the hostname, if there is no hostname, fall back on protocol
|
|
// ex: `data:` uri, and `about:` pages
|
|
const hostname = parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
|
|
this._overlayColor = customColors[hostname] ? customColors[hostname] : FLEXBOX_COLOR;
|
|
return this._overlayColor;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the layout panel is visible, and false otherwise.
|
|
*/
|
|
isPanelVisible() {
|
|
return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
|
|
this.inspector.toolbox.currentToolId === "inspector" &&
|
|
this.inspector.sidebar.getCurrentTabID() === "layoutview";
|
|
}
|
|
/**
|
|
* Handler for "flexbox-highlighter-shown" events emitted from the
|
|
* HighlightersOverlay. Passes nodefront and highlight status to
|
|
* handleHighlighterChange. Required since on and off events need
|
|
* the same reference object.
|
|
*
|
|
* @param {NodeFront} nodeFront
|
|
* The NodeFront of the flex container element for which the flexbox
|
|
* highlighter is shown for.
|
|
*/
|
|
onHighlighterShown(nodeFront) {
|
|
return this.onHighlighterChange(true, nodeFront);
|
|
}
|
|
|
|
/**
|
|
* Handler for "flexbox-highlighter-hidden" events emitted from the
|
|
* HighlightersOverlay. Passes nodefront and highlight status to
|
|
* handleHighlighterChange. Required since on and off events need
|
|
* the same reference object.
|
|
*
|
|
* @param {NodeFront} nodeFront
|
|
* The NodeFront of the flex container element for which the flexbox
|
|
* highlighter is shown for.
|
|
*/
|
|
onHighlighterHidden(nodeFront) {
|
|
return this.onHighlighterChange(false, nodeFront);
|
|
}
|
|
|
|
/**
|
|
* Handler for "flexbox-highlighter-shown" and "flexbox-highlighter-hidden" events
|
|
* emitted from the HighlightersOverlay. Updates the flex container highlighted state
|
|
* only if the provided NodeFront is the current selected flex container.
|
|
*
|
|
* @param {Boolean} highlighted
|
|
* If the change is to highlight or hide the overlay.
|
|
* @param {NodeFront} nodeFront
|
|
* The NodeFront of the flex container element for which the flexbox
|
|
* highlighter is shown for.
|
|
*/
|
|
onHighlighterChange(highlighted, nodeFront) {
|
|
const { flexbox } = this.store.getState();
|
|
|
|
if (flexbox.flexContainer.nodeFront === nodeFront &&
|
|
flexbox.highlighted !== highlighted) {
|
|
this.store.dispatch(updateFlexboxHighlighted(highlighted));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for the "new-root" event fired by the inspector. Clears the cached overlay
|
|
* color for the flexbox highlighter and updates the panel.
|
|
*/
|
|
onNavigate() {
|
|
this._overlayColor = null;
|
|
this.onUpdatePanel();
|
|
}
|
|
|
|
/**
|
|
* Handler for the "reflow" event fired by the inspector's reflow tracker. On reflows,
|
|
* updates the flexbox panel because the shape of the flexbox on the page may have
|
|
* changed.
|
|
*/
|
|
async onReflow() {
|
|
if (!this.isPanelVisible() ||
|
|
!this.store ||
|
|
!this.selection.nodeFront ||
|
|
!this.hasGetCurrentFlexbox ||
|
|
this._isUpdating) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const flexContainer = await this.getFlexContainerProps(this.selection.nodeFront);
|
|
|
|
// Clear the flexbox panel if there is no flex container for the current node
|
|
// selection.
|
|
if (!flexContainer) {
|
|
this.store.dispatch(clearFlexbox());
|
|
return;
|
|
}
|
|
|
|
const { flexbox } = this.store.getState();
|
|
|
|
// Compare the new flexbox state of the current selected nodeFront with the old
|
|
// flexbox state to determine if we need to update.
|
|
if (hasFlexContainerChanged(flexbox.flexContainer, flexContainer)) {
|
|
this.update(flexContainer);
|
|
return;
|
|
}
|
|
|
|
let flexItemContainer = null;
|
|
// If the current selected node is also the flex container node, check if it is
|
|
// a flex item of a parent flex container.
|
|
if (flexContainer.nodeFront === this.selection.nodeFront) {
|
|
flexItemContainer = await this.getFlexContainerProps(this.selection.nodeFront,
|
|
true);
|
|
}
|
|
|
|
// Compare the new and old state of the parent flex container properties.
|
|
if (hasFlexContainerChanged(flexbox.flexItemContainer, flexItemContainer)) {
|
|
this.update(flexContainer, flexItemContainer);
|
|
}
|
|
} catch (e) {
|
|
// This call might fail if called asynchrously after the toolbox is finished
|
|
// closing.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for a change in the flexbox overlay color picker for a flex container.
|
|
*
|
|
* @param {String} color
|
|
* A hex string representing the color to use.
|
|
*/
|
|
async onSetFlexboxOverlayColor(color) {
|
|
this.store.dispatch(updateFlexboxColor(color));
|
|
|
|
const { flexbox } = this.store.getState();
|
|
|
|
if (flexbox.highlighted) {
|
|
this.highlighters.showFlexboxHighlighter(flexbox.flexContainer.nodeFront);
|
|
}
|
|
|
|
this._overlayColor = color;
|
|
|
|
const currentUrl = this.inspector.target.url;
|
|
// Get the hostname, if there is no hostname, fall back on protocol
|
|
// ex: `data:` uri, and `about:` pages
|
|
const hostname = parseURL(currentUrl).hostname || parseURL(currentUrl).protocol;
|
|
const customColors = await this.getCustomHostColors();
|
|
customColors[hostname] = color;
|
|
this._customHostColors = customColors;
|
|
await asyncStorage.setItem("flexboxInspectorHostColors", customColors);
|
|
}
|
|
|
|
/**
|
|
* Handler for the inspector sidebar "select" event. Updates the flexbox panel if it
|
|
* is visible.
|
|
*/
|
|
onSidebarSelect() {
|
|
if (!this.isPanelVisible()) {
|
|
this.inspector.reflowTracker.untrackReflows(this, this.onReflow);
|
|
this.inspector.off("new-root", this.onNavigate);
|
|
this.selection.off("new-node-front", this.onUpdatePanel);
|
|
return;
|
|
}
|
|
|
|
this.inspector.reflowTracker.trackReflows(this, this.onReflow);
|
|
this.inspector.on("new-root", this.onNavigate);
|
|
this.selection.on("new-node-front", this.onUpdatePanel);
|
|
|
|
this.update();
|
|
}
|
|
|
|
/**
|
|
* Handler for a change in the input checkboxes in the FlexboxItem component.
|
|
* Toggles on/off the flexbox highlighter for the provided flex container element.
|
|
*
|
|
* @param {NodeFront} node
|
|
* The NodeFront of the flex container element for which the flexbox
|
|
* highlighter is toggled on/off for.
|
|
*/
|
|
onToggleFlexboxHighlighter(node) {
|
|
this.highlighters.toggleFlexboxHighlighter(node, "layout");
|
|
this.store.dispatch(updateFlexboxHighlighted(node !==
|
|
this.highlighters.flexboxHighlighterShow));
|
|
}
|
|
|
|
/**
|
|
* Handler for "new-root" event fired by the inspector and "new-node-front" event fired
|
|
* by the inspector selection. Updates the flexbox panel if it is visible.
|
|
*
|
|
* @param {Object}
|
|
* This callback is sometimes executed on "new-node-front" events which means
|
|
* that a first param is passed here (the nodeFront), which we don't care about.
|
|
* @param {String} reason
|
|
* On "new-node-front" events, a reason is passed here, and we need it to detect
|
|
* if this update was caused by a node selection from the markup-view.
|
|
*/
|
|
onUpdatePanel(_, reason) {
|
|
if (!this.isPanelVisible()) {
|
|
return;
|
|
}
|
|
|
|
this.update(null, null, reason === "treepanel");
|
|
}
|
|
|
|
/**
|
|
* Updates the flexbox panel by dispatching the new flexbox data. This is called when
|
|
* the layout view becomes visible or a new node is selected and needs to be update
|
|
* with new flexbox data.
|
|
*
|
|
* @param {Object|null} flexContainer
|
|
* An object consisting of the current flex container's flex items and
|
|
* properties.
|
|
* @param {Object|null} flexItemContainer
|
|
* An object consisting of the parent flex container's flex items and
|
|
* properties.
|
|
* @param {Boolean} initiatedByMarkupViewSelection
|
|
* True if the update was due to a node selection in the markup-view.
|
|
*/
|
|
async update(flexContainer, flexItemContainer, initiatedByMarkupViewSelection) {
|
|
this._isUpdating = true;
|
|
|
|
// Stop refreshing if the inspector or store is already destroyed or no node is
|
|
// selected.
|
|
if (!this.inspector ||
|
|
!this.store ||
|
|
!this.selection.nodeFront ||
|
|
!this.hasGetCurrentFlexbox) {
|
|
this._isUpdating = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Fetch the current flexbox if no flexbox front was passed into this update.
|
|
if (!flexContainer) {
|
|
flexContainer = await this.getFlexContainerProps(this.selection.nodeFront);
|
|
}
|
|
|
|
// Clear the flexbox panel if there is no flex container for the current node
|
|
// selection.
|
|
if (!flexContainer) {
|
|
this.store.dispatch(clearFlexbox());
|
|
this._isUpdating = false;
|
|
return;
|
|
}
|
|
|
|
if (!flexItemContainer && flexContainer.nodeFront === this.selection.nodeFront) {
|
|
flexItemContainer = await this.getFlexContainerProps(this.selection.nodeFront,
|
|
true);
|
|
}
|
|
|
|
const highlighted = this.inspector.isHighlighterReady &&
|
|
flexContainer.nodeFront === this.highlighters.flexboxHighlighterShown;
|
|
const color = await this.getOverlayColor();
|
|
|
|
this.store.dispatch(updateFlexbox({
|
|
color,
|
|
flexContainer,
|
|
flexItemContainer,
|
|
highlighted,
|
|
initiatedByMarkupViewSelection,
|
|
}));
|
|
} catch (e) {
|
|
// This call might fail if called asynchrously after the toolbox is finished
|
|
// closing.
|
|
}
|
|
|
|
this._isUpdating = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a given flex container object, returns the flex container properties that can be
|
|
* used to check if 2 flex container objects are the same.
|
|
*
|
|
* @param {Object|null} flexContainer
|
|
* Object consisting of the flex container's properties.
|
|
* @return {Object|null} consisting of the comparable flex container's properties.
|
|
*/
|
|
function getComparableFlexContainerProperties(flexContainer) {
|
|
if (!flexContainer) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
flexItems: getComparableFlexItemsProperties(flexContainer.flexItems),
|
|
nodeFront: flexContainer.nodeFront.actorID,
|
|
properties: flexContainer.properties,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Given an array of flex item objects, returns the relevant flex item properties that can
|
|
* be compared to check if any changes has occurred.
|
|
*
|
|
* @param {Array} flexItems
|
|
* Array of objects containing the flex item properties.
|
|
* @return {Array} of objects consisting of the comparable flex item's properties.
|
|
*/
|
|
function getComparableFlexItemsProperties(flexItems) {
|
|
return flexItems.map(item => {
|
|
return {
|
|
computedStyle: item.computedStyle,
|
|
flexItemSizing: item.flexItemSizing,
|
|
nodeFront: item.nodeFront.actorID,
|
|
properties: item.properties,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compares the old and new flex container properties
|
|
*
|
|
* @param {Object} oldFlexContainer
|
|
* Object consisting of the old flex container's properties.
|
|
* @param {Object} newFlexContainer
|
|
* Object consisting of the new flex container's properties.
|
|
* @return {Boolean} true if the flex container properties are the same, false otherwise.
|
|
*/
|
|
function hasFlexContainerChanged(oldFlexContainer, newFlexContainer) {
|
|
return JSON.stringify(getComparableFlexContainerProperties(oldFlexContainer)) !==
|
|
JSON.stringify(getComparableFlexContainerProperties(newFlexContainer));
|
|
}
|
|
|
|
module.exports = FlexboxInspector;
|