summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/flexbox/flexbox.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/flexbox/flexbox.js569
1 files changed, 569 insertions, 0 deletions
diff --git a/devtools/client/inspector/flexbox/flexbox.js b/devtools/client/inspector/flexbox/flexbox.js
new file mode 100644
index 0000000000..3947da3cf9
--- /dev/null
+++ b/devtools/client/inspector/flexbox/flexbox.js
@@ -0,0 +1,569 @@
+/* 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 { throttle } = require("resource://devtools/shared/throttle.js");
+
+const {
+ clearFlexbox,
+ updateFlexbox,
+ updateFlexboxColor,
+ updateFlexboxHighlighted,
+} = require("resource://devtools/client/inspector/flexbox/actions/flexbox.js");
+const flexboxReducer = require("resource://devtools/client/inspector/flexbox/reducers/flexbox.js");
+
+loader.lazyRequireGetter(
+ this,
+ "parseURL",
+ "resource://devtools/client/shared/source-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "asyncStorage",
+ "resource://devtools/shared/async-storage.js"
+);
+
+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.store.injectReducer("flexbox", flexboxReducer);
+
+ 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.onUpdatePanel = this.onUpdatePanel.bind(this);
+
+ this.init();
+ }
+
+ init() {
+ if (!this.inspector) {
+ return;
+ }
+
+ this.inspector.highlighters.on(
+ "highlighter-shown",
+ this.onHighlighterShown
+ );
+ this.inspector.highlighters.on(
+ "highlighter-hidden",
+ this.onHighlighterHidden
+ );
+ this.inspector.sidebar.on("select", this.onSidebarSelect);
+
+ this.onSidebarSelect();
+ }
+
+ destroy() {
+ this.selection.off("new-node-front", this.onUpdatePanel);
+ this.inspector.off("new-root", this.onNavigate);
+ this.inspector.off("reflow-in-selected-target", this.onReflow);
+ this.inspector.highlighters.off(
+ "highlighter-shown",
+ this.onHighlighterShown
+ );
+ this.inspector.highlighters.off(
+ "highlighter-hidden",
+ this.onHighlighterHidden
+ );
+ this.inspector.sidebar.off("select", this.onSidebarSelect);
+
+ this._customHostColors = null;
+ this._overlayColor = null;
+ this.document = null;
+ this.inspector = null;
+ this.selection = null;
+ this.store = null;
+ }
+
+ getComponentProps() {
+ return {
+ onSetFlexboxOverlayColor: this.onSetFlexboxOverlayColor,
+ };
+ }
+
+ /**
+ * 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 layoutFront = await nodeFront.walkerFront.getLayoutInspector();
+ const flexboxFront = await layoutFront.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 flexboxFront.walkerFront.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 flexItemFront.walkerFront.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.currentTarget.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 "highlighter-shown" events emitted by HighlightersOverlay.
+ * If the event is dispatched on behalf of a flex highlighter, toggle the
+ * corresponding flex container's highlighted state in the Redux store.
+ *
+ * @param {Object} data
+ * Object with data associated with the highlighter event.
+ * {NodeFront} data.nodeFront
+ * The NodeFront of the flex container element for which the flexbox
+ * highlighter is shown for.
+ * {String} data.type
+ * Highlighter type
+ */
+ onHighlighterShown(data) {
+ if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) {
+ this.onHighlighterChange(true, data.nodeFront);
+ }
+ }
+
+ /**
+ * Handler for "highlighter-shown" events emitted by HighlightersOverlay.
+ * If the event is dispatched on behalf of a flex highlighter, toggle the
+ * corresponding flex container's highlighted state in the Redux store.
+ *
+ * @param {Object} data
+ * Object with data associated with the highlighter event.
+ * {NodeFront} data.nodeFront
+ * The NodeFront of the flex container element for which the flexbox
+ * highlighter was previously shown for.
+ * {String} data.type
+ * Highlighter type
+ */
+ onHighlighterHidden(data) {
+ if (data.type === this.inspector.highlighters.TYPES.FLEXBOX) {
+ this.onHighlighterChange(false, data.nodeFront);
+ }
+ }
+
+ /**
+ * Updates the flex container highlighted state in the Redux store if the provided
+ * NodeFront is the current selected flex container.
+ *
+ * @param {Boolean} highlighted
+ * Whether 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 reflow events fired by the inspector when a node is selected. On reflows,
+ * update 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._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.inspector.highlighters.showFlexboxHighlighter(
+ flexbox.flexContainer.nodeFront
+ );
+ }
+
+ this._overlayColor = color;
+
+ const currentUrl = this.inspector.currentTarget.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.off("reflow-in-selected-target", this.onReflow);
+ this.inspector.off("new-root", this.onNavigate);
+ this.selection.off("new-node-front", this.onUpdatePanel);
+ return;
+ }
+
+ this.inspector.on("reflow-in-selected-target", this.onReflow);
+ this.inspector.on("new-root", this.onNavigate);
+ this.selection.on("new-node-front", this.onUpdatePanel);
+
+ this.update();
+ }
+
+ /**
+ * 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._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 =
+ flexContainer.nodeFront ===
+ this.inspector.highlighters.getNodeForActiveHighlighter(
+ this.inspector.highlighters.TYPES.FLEXBOX
+ );
+ 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;