author | manas <manas.khurana20@gmail.com> |
Sat, 22 Aug 2020 09:12:12 +0000 | |
changeset 545733 | 0fc6c987a4ba54316df080c65bcc5c4102cc6479 |
parent 545732 | 9fb870ca7b1e5f9bea92a67ea9ced7f0772d2eee |
child 545734 | c089d805db488ef5c19f298d5227374ae3605f49 |
push id | 124769 |
push user | gluong@mozilla.com |
push date | Sat, 22 Aug 2020 15:11:58 +0000 |
treeherder | autoland@0fc6c987a4ba [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | gl, jdescottes, bradwerth |
bugs | 1657680 |
milestone | 81.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/inspector/markup/views/element-editor.js +++ b/devtools/client/inspector/markup/views/element-editor.js @@ -88,28 +88,35 @@ function ElementEditor(container, node) this.inspector = this.markup.inspector; this.highlighters = this.markup.highlighters; this._cssProperties = this.inspector.cssProperties; this.isOverflowDebuggingEnabled = Services.prefs.getBoolPref( "devtools.overflow.debugging.enabled" ); + // If this is a scrollable element, this specifies whether or not its overflow causing + // elements are highlighted. Otherwise, it is null if the element is not scrollable. + this.highlightingOverflowCausingElements = this.node.isScrollable + ? false + : null; + this.attrElements = new Map(); this.animationTimers = {}; this.elt = null; this.tag = null; this.closeTag = null; this.attrList = null; this.newAttr = null; this.closeElt = null; this.onCustomBadgeClick = this.onCustomBadgeClick.bind(this); this.onDisplayBadgeClick = this.onDisplayBadgeClick.bind(this); + this.onScrollableBadgeClick = this.onScrollableBadgeClick.bind(this); this.onExpandBadgeClick = this.onExpandBadgeClick.bind(this); this.onFlexboxHighlighterChange = this.onFlexboxHighlighterChange.bind(this); this.onGridHighlighterChange = this.onGridHighlighterChange.bind(this); this.onTagEdit = this.onTagEdit.bind(this); // Create the main editor this.buildMarkup(); @@ -312,16 +319,17 @@ ElementEditor.prototype = { } this.updateEventBadge(); this.updateDisplayBadge(); this.updateCustomBadge(); this.updateScrollableBadge(); this.updateTextEditor(); this.updateOverflowBadge(); + this.updateOverflowHighlight(); }, updateEventBadge: function() { const showEventBadge = this.node.hasEventListeners; if (this._eventBadge && !showEventBadge) { this._eventBadge.remove(); this._eventBadge = null; } else if (showEventBadge && !this._eventBadge) { @@ -350,24 +358,43 @@ ElementEditor.prototype = { this._createScrollableBadge(); } else if (this._scrollableBadge && !this.node.isScrollable) { this._scrollableBadge.remove(); this._scrollableBadge = null; } }, _createScrollableBadge: function() { + const isInteractive = + this.isOverflowDebuggingEnabled && + this.node.walkerFront.traits.supportsOverflowDebugging && + // Document elements cannot have interative scrollable badges since retrieval of their + // overflow causing elements is not supported. + !this.node.isDocumentElement; + this._scrollableBadge = this.doc.createElement("div"); - this._scrollableBadge.className = "inspector-badge scrollable-badge"; + this._scrollableBadge.className = `inspector-badge scrollable-badge ${ + isInteractive ? "interactive" : "" + }`; + this._scrollableBadge.textContent = INSPECTOR_L10N.getStr( "markupView.scrollableBadge.label" ); this._scrollableBadge.title = INSPECTOR_L10N.getStr( - "markupView.scrollableBadge.tooltip" + isInteractive + ? "markupView.scrollableBadge.interactive.tooltip" + : "markupView.scrollableBadge.tooltip" ); + + if (isInteractive) { + this._scrollableBadge.addEventListener( + "click", + this.onScrollableBadgeClick + ); + } this.elt.insertBefore(this._scrollableBadge, this._customBadge); }, /** * Update the markup display badge. */ updateDisplayBadge: function() { const displayType = this.node.displayType; @@ -467,16 +494,63 @@ ElementEditor.prototype = { "markupView.custom.tooltiptext" ); this._customBadge.addEventListener("click", this.onCustomBadgeClick); // Badges order is [event][display][custom], insert custom badge at the end. this.elt.appendChild(this._customBadge); }, /** + * If node causes overflow, toggle its overflow highlight if its scrollable ancestor's + * scrollable badge is active/inactive. + */ + updateOverflowHighlight: async function() { + if ( + !this.isOverflowDebuggingEnabled || + !this.node.walkerFront.traits.supportsOverflowDebugging + ) { + return; + } + + let showOverflowHighlight = false; + + if (this.node.causesOverflow) { + try { + const scrollableAncestor = await this.node.walkerFront.getScrollableAncestorNode( + this.node + ); + const markupContainer = scrollableAncestor + ? this.markup.getContainer(scrollableAncestor) + : null; + + showOverflowHighlight = !!markupContainer?.editor + .highlightingOverflowCausingElements; + } catch (e) { + // This call might fail if called asynchrously after the toolbox is finished + // closing. + return; + } + } + + this.setOverflowHighlight(showOverflowHighlight); + }, + + /** + * Show overflow highlight if showOverflowHighlight is true, otherwise hide it. + * + * @param {Boolean} showOverflowHighlight + */ + setOverflowHighlight: function(showOverflowHighlight) { + this.container.tagState.classList.toggle( + "overflow-causing-highlighted", + showOverflowHighlight + ); + }, + + /** * Update the inline text editor in case of a single text child node. */ updateTextEditor: function() { const node = this.node.inlineTextChild; if (this.textEditor && this.textEditor.node != node) { this.elt.removeChild(this.textEditor.elt); this.textEditor.destroy(); @@ -936,16 +1010,43 @@ ElementEditor.prototype = { ); }, onExpandBadgeClick: function() { this.container.expandContainer(); }, /** + * Called when the scrollable badge is clicked. Shows the overflow causing elements and + * highlights their container if the scroll badge is active. + */ + onScrollableBadgeClick: async function() { + this.highlightingOverflowCausingElements = this._scrollableBadge.classList.toggle( + "active" + ); + + const overflowCausingElements = await this.node.walkerFront.getOverflowCausingElements( + this.node + ); + const overflowCausingElementsList = await overflowCausingElements.items(); + + for (const element of overflowCausingElementsList) { + if (this.highlightingOverflowCausingElements) { + await this.markup.showNode(element); + } + + const markupContainer = this.markup.getContainer(element); + + markupContainer.editor.setOverflowHighlight( + this.highlightingOverflowCausingElements + ); + } + }, + + /** * Handler for "flexbox-highlighter-hidden" and "flexbox-highlighter-shown" event * emitted from the HighlightersOverlay. Toggles the active state of the display badge * if it matches the highlighted flex container node. */ onFlexboxHighlighterChange: function() { if (!this._displayBadge) { return; } @@ -1004,16 +1105,23 @@ ElementEditor.prototype = { this.stopTrackingFlexboxHighlighterEvents(); this.stopTrackingGridHighlighterEvents(); } if (this._customBadge) { this._customBadge.removeEventListener("click", this.onCustomBadgeClick); } + if (this._scrollableBadge) { + this._scrollableBadge.removeEventListener( + "click", + this.onScrollableBadgeClick + ); + } + this.expandBadge.removeEventListener("click", this.onExpandBadgeClick); for (const key in this.animationTimers) { clearTimeout(this.animationTimers[key]); } this.animationTimers = null; }, };
--- a/devtools/client/locales/en-US/inspector.properties +++ b/devtools/client/locales/en-US/inspector.properties @@ -502,16 +502,20 @@ inspector.colorSchemeSimulation.tooltip= # LOCALIZATION NOTE (markupView.scrollableBadge.label): This is the text displayed inside a # badge, in the inspector, next to nodes that are scrollable in the page. markupView.scrollableBadge.label=scroll # LOCALIZATION NOTE (markupView.scrollableBadge.tooltip): This is the tooltip that is displayed # when hovering over badges next to scrollable elements in the inspector. markupView.scrollableBadge.tooltip=This element has scrollable overflow. +# LOCALIZATION NOTE (markupView.scrollableBadge.interactive.tooltip): This is the tooltip that is displayed +# when hovering over interactive badges next to scrollable elements in the inspector. +markupView.scrollableBadge.interactive.tooltip=This element has scrollable overflow. Click to reveal elements that are causing the overflow. + # LOCALIZATION NOTE (markupView.overflowBadge.label): This is the text displayed inside a # badge, in the inspector, next to nodes that are causing overflow in other elements. markupView.overflowBadge.label=overflow # LOCALIZATION NOTE (markupView.overflowBadge.tooltip): This is the tooltip that is displayed # when hovering over badges next to overflow causing elements in the inspector. markupView.overflowBadge.tooltip=This element is causing an element to overflow.
--- a/devtools/client/themes/badge.css +++ b/devtools/client/themes/badge.css @@ -6,27 +6,32 @@ --badge-active-background-color: var(--blue-50); --badge-active-border-color: #FFFFFFB3; --badge-background-color: white; --badge-border-color: #CACAD1; --badge-color: var(--grey-60); --badge-hover-background-color: #DFDFE8; --badge-interactive-background-color: var(--grey-20); --badge-interactive-color: var(--grey-90); + --badge-scrollable-color: #8000D7; + --badge-scrollable-background-color: #FFFFFF; + --badge-scrollable-active-background-color: #8000D7; } .theme-dark:root { --badge-active-background-color: var(--blue-60); --badge-active-border-color: #FFF6; --badge-background-color: var(--grey-80); --badge-border-color: var(--grey-50); --badge-color: var(--grey-40); --badge-hover-background-color: var(--grey-80); --badge-interactive-background-color: var(--grey-70); --badge-interactive-color: var(--grey-30); + --badge-scrollable-color: #B98EFF; + --badge-scrollable-background-color: transparent; } /* Inspector badge */ .inspector-badge, .editor.text .whitespace::before { display: inline-block; /* 9px text is too blurry on low-resolution screens */ font-size: 10px; @@ -70,8 +75,19 @@ } .inspector-badge.active, .inspector-badge.interactive.active { background-color: var(--badge-active-background-color); border-color: var(--badge-active-border-color); color: var(--theme-selection-color); } + +.inspector-badge.interactive.scrollable-badge { + background-color: var(--badge-scrollable-background-color); + border-color: var(--badge-scrollable-color); + color: var(--badge-scrollable-color); +} + +.inspector-badge.interactive.scrollable-badge.active { + background-color: var(--badge-scrollable-active-background-color); + color: var(--theme-selection-color); +}
--- a/devtools/client/themes/markup.css +++ b/devtools/client/themes/markup.css @@ -6,26 +6,28 @@ --markup-hidden-attr-name-color: var(--grey-43); --markup-hidden-attr-value-color: var(--grey-55); --markup-hidden-punctuation-color: var(--grey-43); --markup-pseudoclass-disk-color: #e9c600; --markup-hidden-tag-color: var(--grey-50); --markup-outline: var(--theme-splitter-color); --markup-drag-line: var(--grey-40); --markup-drop-line: var(--blue-55); + --markup-overflow-causing-background-color: rgba(128, 0, 215, 0.15); } .theme-dark:root { --markup-hidden-attr-name-color: #787878; --markup-hidden-attr-value-color: #a4a4a4; --markup-hidden-punctuation-color: #787878; --markup-hidden-tag-color: var(--grey-45); --markup-outline: var(--theme-selection-background); --markup-drag-line: var(--grey-55); --markup-drop-line: var(--blue-50); + --markup-overflow-causing-background-color: rgba(148, 0, 255, 0.38); } * { padding: 0; margin: 0; } :root { @@ -304,16 +306,20 @@ ul.children + .tag-line::before { } /* Hide HTML void elements (img, hr, br, …) closing tag when the element is not * expanded (it can be if it has pseudo-elements attached) */ .child.collapsed > .tag-line .void-element .close { display: none; } +.tag-line .tag-state.overflow-causing-highlighted:not(.theme-selected) { + background-color: var(--markup-overflow-causing-background-color); +} + .closing-bracket { pointer-events: none; } .newattr { margin-right: -13px; }
--- a/devtools/server/actors/inspector/node.js +++ b/devtools/server/actors/inspector/node.js @@ -103,21 +103,22 @@ const NodeActor = protocol.ActorClassWit this._eventCollector = new EventCollector(this.walker.targetActor); // Store the original display type and scrollable state and whether or not the node is // displayed to track changes when reflows occur. this.currentDisplayType = this.displayType; this.wasDisplayed = this.isDisplayed; this.wasScrollable = this.isScrollable; - this.walker.updateOverflowCausingElements( - this.rawNode, - this, - this.walker.overflowCausingElementsSet - ); + if (this.isScrollable) { + this.walker.updateOverflowCausingElements( + this, + this.walker.overflowCausingElementsMap + ); + } }, toString: function() { return ( "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]" ); }, @@ -178,17 +179,17 @@ const NodeActor = protocol.ActorClassWit nodeName: this.rawNode.nodeName, nodeValue: this.rawNode.nodeValue, displayName: getNodeDisplayName(this.rawNode), numChildren: this.numChildren, inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined, displayType: this.displayType, isScrollable: this.isScrollable, isTopLevelDocument: this.isTopLevelDocument, - causesOverflow: this.walker.overflowCausingElementsSet.has(this.rawNode), + causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode), // doctype attributes name: this.rawNode.name, publicId: this.rawNode.publicId, systemId: this.rawNode.systemId, attrs: this.writeAttrs(), customElementLocation: this.getCustomElementLocation(),
--- a/devtools/server/actors/inspector/walker.js +++ b/devtools/server/actors/inspector/walker.js @@ -201,17 +201,20 @@ var WalkerActor = protocol.ActorClassWit this._nodeActorsMap = new Map(); this._pendingMutations = []; this._activePseudoClassLocks = new Set(); this._mutationBreakpoints = new WeakMap(); this.customElementWatcher = new CustomElementWatcher( targetActor.chromeEventHandler ); - this.overflowCausingElementsSet = new Set(); + + // In this map, the key-value pairs are the overflow causing elements and their + // respective ancestor scrollable node actor. + this.overflowCausingElementsMap = new Map(); this.showAllAnonymousContent = options.showAllAnonymousContent; this.walkerSearch = new WalkerSearch(this); // Nodes which have been removed from the client's known // ownership tree are considered "orphaned", and stored in // this set. @@ -359,16 +362,18 @@ var WalkerActor = protocol.ActorClassWit // Returns the JSON representation of this object over the wire. form: function() { return { actor: this.actorID, root: this.rootNode.form(), traits: { // Walker implements node picker starting with Firefox 80 supportsNodePicker: true, + // Walker implements overflow debugging support starting with Firefox 81 + supportsOverflowDebugging: true, }, }; }, toString: function() { return "[WalkerActor " + this.actorID + "]"; }, @@ -414,18 +419,18 @@ var WalkerActor = protocol.ActorClassWit return; } this._destroyed = true; protocol.Actor.prototype.destroy.call(this); try { this.clearPseudoClassLocks(); this._activePseudoClassLocks = null; - this.overflowCausingElementsSet.clear(); - this.overflowCausingElementsSet = null; + this.overflowCausingElementsMap.clear(); + this.overflowCausingElementsMap = null; this._hoveredNode = null; this.rootWin = null; this.rootDoc = null; this.rootNode = null; this.layoutHelpers = null; this._orphaned = null; this._retainedOrphans = null; @@ -569,17 +574,17 @@ var WalkerActor = protocol.ActorClassWit }, _onReflows: function(reflows) { // Going through the nodes the walker knows about, see which ones have had their // display, scrollable or overflow state changed and send events if any. const displayTypeChanges = []; const scrollableStateChanges = []; - const currentOverflowCausingElementsSet = new Set(); + const currentOverflowCausingElementsMap = new Map(); for (const [node, actor] of this._nodeActorsMap) { if (Cu.isDeadWrapper(node)) { continue; } const displayType = actor.displayType; const isDisplayed = actor.isDisplayed; @@ -596,36 +601,37 @@ var WalkerActor = protocol.ActorClassWit } const isScrollable = actor.isScrollable; if (isScrollable !== actor.wasScrollable) { scrollableStateChanges.push(actor); actor.wasScrollable = isScrollable; } - this.updateOverflowCausingElements( - node, - actor, - currentOverflowCausingElementsSet - ); + if (isScrollable) { + this.updateOverflowCausingElements( + actor, + currentOverflowCausingElementsMap + ); + } } // Get the NodeActor for each node in the symmetric difference of - // currentOverflowCausingElementsSet and this.overflowCausingElementsSet - const overflowStateChanges = [...currentOverflowCausingElementsSet] - .filter(node => !this.overflowCausingElementsSet.has(node)) + // currentOverflowCausingElementsMap and this.overflowCausingElementsMap + const overflowStateChanges = [...currentOverflowCausingElementsMap.keys()] + .filter(node => !this.overflowCausingElementsMap.has(node)) .concat( - [...this.overflowCausingElementsSet].filter( - node => !currentOverflowCausingElementsSet.has(node) + [...this.overflowCausingElementsMap.keys()].filter( + node => !currentOverflowCausingElementsMap.has(node) ) ) .filter(node => this.hasNode(node)) .map(node => this.getNode(node)); - this.overflowCausingElementsSet = currentOverflowCausingElementsSet; + this.overflowCausingElementsMap = currentOverflowCausingElementsMap; if (displayTypeChanges.length) { this.emit("display-change", displayTypeChanges); } if (scrollableStateChanges.length) { this.emit("scrollable-change", scrollableStateChanges); } @@ -2858,35 +2864,83 @@ var WalkerActor = protocol.ActorClassWit this.nodePicker.pick(doFocus); }, cancelPick() { this.nodePicker.cancelPick(); }, /** - * If element has a scrollbar, find the children causing the overflow and - * add them to the set. - * @param {DOMNode} node The node whose overflow causing elements are returned. - * @param {NodeActor} actor The actor of the node. - * @param {Set} set The set to which the overflow causing elements are added. + * Given a scrollable node, find its descendants which are causing overflow in it and + * add their raw nodes to the map as keys with the scrollable element as the values. + * + * @param {NodeActor} scrollableNode A scrollable node. + * @param {Map} map The map to which the overflow causing elements are added. */ - updateOverflowCausingElements: function(node, actor, set) { - if (node.nodeType !== Node.ELEMENT_NODE || !actor.isScrollable) { + updateOverflowCausingElements: function(scrollableNode, map) { + if ( + isNodeDead(scrollableNode) || + scrollableNode.rawNode.nodeType !== Node.ELEMENT_NODE + ) { return; } const overflowCausingChildren = [ - ...InspectorUtils.getOverflowingChildrenOfElement(node), + ...InspectorUtils.getOverflowingChildrenOfElement(scrollableNode.rawNode), ]; - for (let child of overflowCausingChildren) { - // child is a Node, but not necessarily an Element. + for (let overflowCausingChild of overflowCausingChildren) { + // overflowCausingChild is a Node, but not necessarily an Element. // So, get the containing Element - if (child.nodeType !== Node.ELEMENT_NODE) { - child = child.parentElement; + if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) { + overflowCausingChild = overflowCausingChild.parentElement; } - set.add(child); + map.set(overflowCausingChild, scrollableNode); + } + }, + + /** + * Return the overflow causing elements for the given node. + * + * @param {NodeActor} node The scrollable node. + */ + getOverflowCausingElements: function(node) { + if ( + isNodeDead(node) || + node.rawNode.nodeType !== Node.ELEMENT_NODE || + !node.isScrollable + ) { + return []; } + + const overflowCausingElements = [ + ...InspectorUtils.getOverflowingChildrenOfElement(node.rawNode), + ].map(overflowCausingChild => { + if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) { + overflowCausingChild = overflowCausingChild.parentElement; + } + + this.attachElement(overflowCausingChild); + + return overflowCausingChild; + }); + + return new NodeListActor(this, overflowCausingElements); + }, + + /** + * Return the scrollable ancestor node which has overflow because of the given node. + * + * @param {NodeActor} overflowCausingNode + */ + getScrollableAncestorNode: function(overflowCausingNode) { + if ( + isNodeDead(overflowCausingNode) || + !this.overflowCausingElementsMap.has(overflowCausingNode.rawNode) + ) { + return null; + } + + return this.overflowCausingElementsMap.get(overflowCausingNode.rawNode); }, }); exports.WalkerActor = WalkerActor;
--- a/devtools/shared/specs/walker.js +++ b/devtools/shared/specs/walker.js @@ -379,12 +379,28 @@ const walkerSpec = generateActorSpec({ watchRootNode: { request: {}, response: {}, }, unwatchRootNode: { request: {}, oneway: true, }, + getOverflowCausingElements: { + request: { + node: Arg(0, "domnode"), + }, + response: { + list: RetVal("domnodelist"), + }, + }, + getScrollableAncestorNode: { + request: { + node: Arg(0, "domnode"), + }, + response: { + node: RetVal("nullable:domnode"), + }, + }, }, }); exports.walkerSpec = walkerSpec;