From 1ebbb30be21f07d6a4d3ccd3b8dc7bd0dd37b41d Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Mon, 20 Apr 2020 16:54:46 +0200 Subject: [PATCH] Enable image preview in styles panel This change adds image preview functionality to the styles panel including the computed styles view. The preview functionality within ElementsPanel is removed because it seems unused (equivalent code in ElementsTreeOutline handles the previews). Screenshot: https://i.imgur.com/TIU3Rg3.png Fixed: 1040019 Change-Id: I6e387826de908df74ea6e5d7a89e2293291e692b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2156347 Commit-Queue: Alex Rudenko Reviewed-by: Mathias Bynens Reviewed-by: Peter Marshall --- BUILD.gn | 1 + front_end/elements/ComputedStyleWidget.js | 10 ++++ front_end/elements/ElementsPanel.js | 41 +------------ front_end/elements/ElementsTreeElement.js | 5 +- front_end/elements/ElementsTreeOutline.js | 59 +++++++------------ front_end/elements/ImagePreviewPopover.js | 71 +++++++++++++++++++++++ front_end/elements/StylesSidebarPane.js | 31 +++++++--- front_end/elements/module.json | 3 +- 8 files changed, 129 insertions(+), 92 deletions(-) create mode 100644 front_end/elements/ImagePreviewPopover.js diff --git a/BUILD.gn b/BUILD.gn index 18ad800c0e..aacae848bb 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -1167,6 +1167,7 @@ all_devtools_modules = [ "elements/ElementsTreeElementHighlighter.js", "elements/ElementsTreeOutline.js", "elements/EventListenersWidget.js", + "elements/ImagePreviewPopover.js", "elements/InspectElementModeController.js", "elements/MarkerDecorator.js", "elements/MetricsSidebarPane.js", diff --git a/front_end/elements/ComputedStyleWidget.js b/front_end/elements/ComputedStyleWidget.js index 0e7eb89c1d..fb79423725 100644 --- a/front_end/elements/ComputedStyleWidget.js +++ b/front_end/elements/ComputedStyleWidget.js @@ -34,6 +34,7 @@ import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {ComputedStyle, ComputedStyleModel, Events} from './ComputedStyleModel.js'; // eslint-disable-line no-unused-vars +import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {PlatformFontsWidget} from './PlatformFontsWidget.js'; import {StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js'; @@ -78,6 +79,14 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { this._linkifier = new Components.Linkifier.Linkifier(_maxLinkLength); + this._imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => { + const link = event.composedPath()[0]; + if (link instanceof Element) { + return link; + } + return null; + }, () => this._computedStyleModel.node()); + /** * @param {?RegExp} regex * @this {ComputedStyleWidget} @@ -164,6 +173,7 @@ export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { expandedProperties.add(propertyName); } const hadFocus = this._propertiesOutline.element.hasFocus(); + this._imagePreviewPopover.hide(); this._propertiesOutline.removeChildren(); this._linkifier.reset(); const cssModel = this._computedStyleModel.cssModel(); diff --git a/front_end/elements/ElementsPanel.js b/front_end/elements/ElementsPanel.js index f22a128f85..f01473e666 100644 --- a/front_end/elements/ElementsPanel.js +++ b/front_end/elements/ElementsPanel.js @@ -37,7 +37,7 @@ import * as UI from '../ui/ui.js'; import {ComputedStyleWidget} from './ComputedStyleWidget.js'; import {ElementsBreadcrumbs, Events} from './ElementsBreadcrumbs.js'; -import {ElementsTreeElement, HrefSymbol} from './ElementsTreeElement.js'; // eslint-disable-line no-unused-vars +import {ElementsTreeElement} from './ElementsTreeElement.js'; // eslint-disable-line no-unused-vars import {ElementsTreeElementHighlighter} from './ElementsTreeElementHighlighter.js'; import {ElementsTreeOutline} from './ElementsTreeOutline.js'; import {MarkerDecorator} from './MarkerDecorator.js'; // eslint-disable-line no-unused-vars @@ -306,9 +306,6 @@ export class ElementsPanel extends UI.Panel.Panel { this._contentElement.removeChild(header); } } - if (this._popoverHelper) { - this._popoverHelper.hidePopover(); - } super.willHide(); self.UI.context.setFlavor(ElementsPanel, null); } @@ -523,36 +520,6 @@ export class ElementsPanel extends UI.Panel.Panel { UI.ViewManager.ViewManager.instance().showView('elements').then(() => this.selectDOMNode(node, true)); } - /** - * @param {!Event} event - * @return {?UI.PopoverRequest} - */ - _getPopoverRequest(event) { - let link = event.target; - while (link && !link[HrefSymbol]) { - link = link.parentElementOrShadowHost(); - } - if (!link) { - return null; - } - - return { - box: link.boxInWindow(), - show: async popover => { - const node = this.selectedDOMNode(); - if (!node) { - return false; - } - const preview = - await Components.ImagePreview.ImagePreview.build(node.domModel().target(), link[HrefSymbol], true); - if (preview) { - popover.contentElement.appendChild(preview); - } - return !!preview; - } - }; - } - _jumpToSearchResult(index) { if (!this._searchResults) { return; @@ -880,12 +847,6 @@ export class ElementsPanel extends UI.Panel.Panel { this.sidebarPaneView = UI.ViewManager.ViewManager.instance().createTabbedLocation( () => UI.ViewManager.ViewManager.instance().showView('elements')); const tabbedPane = this.sidebarPaneView.tabbedPane(); - if (this._popoverHelper) { - this._popoverHelper.hidePopover(); - } - this._popoverHelper = new UI.PopoverHelper.PopoverHelper(tabbedPane.element, this._getPopoverRequest.bind(this)); - this._popoverHelper.setHasPadding(true); - this._popoverHelper.setTimeout(0); if (this._splitMode !== _splitMode.Vertical) { this._splitWidget.installResizer(tabbedPane.headerElement()); diff --git a/front_end/elements/ElementsTreeElement.js b/front_end/elements/ElementsTreeElement.js index 04237c4f8b..2314d72a87 100644 --- a/front_end/elements/ElementsTreeElement.js +++ b/front_end/elements/ElementsTreeElement.js @@ -39,6 +39,7 @@ import * as UI from '../ui/ui.js'; import {canGetJSPath, cssPath, jsPath, xPath} from './DOMPath.js'; import {MappedCharToEntity, UpdateRecord} from './ElementsTreeOutline.js'; // eslint-disable-line no-unused-vars +import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {MarkerDecorator} from './MarkerDecorator.js'; /** @@ -1410,8 +1411,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement { const link = node.nodeName().toLowerCase() === 'a' ? UI.XLink.XLink.create(rewrittenHref, value, '', true /* preventClick */) : Components.Linkifier.Linkifier.linkifyURL(rewrittenHref, {text: value, preventClick: true}); - link[HrefSymbol] = rewrittenHref; - return link; + return ImagePreviewPopover.setImageUrl(link, rewrittenHref); } const nodeName = node ? node.nodeName().toLowerCase() : ''; @@ -1838,7 +1838,6 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement { } } -export const HrefSymbol = Symbol('ElementsTreeElement.Href'); export const InitialChildrenLimit = 500; // A union of HTML4 and HTML5-Draft elements that explicitly diff --git a/front_end/elements/ElementsTreeOutline.js b/front_end/elements/ElementsTreeOutline.js index d935f6bc2f..ddd97cef0f 100644 --- a/front_end/elements/ElementsTreeOutline.js +++ b/front_end/elements/ElementsTreeOutline.js @@ -29,13 +29,13 @@ */ import * as Common from '../common/common.js'; -import * as Components from '../components/components.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; // eslint-disable-line no-unused-vars import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {linkifyDeferredNodeReference} from './DOMLinkifier.js'; -import {ElementsTreeElement, HrefSymbol, InitialChildrenLimit} from './ElementsTreeElement.js'; +import {ElementsTreeElement, InitialChildrenLimit} from './ElementsTreeElement.js'; +import {HrefSymbol, ImagePreviewPopover} from './ImagePreviewPopover.js'; /** * @unrestricted @@ -88,9 +88,22 @@ export class ElementsTreeOutline extends UI.TreeOutline.TreeOutline { this._visible = false; - this._popoverHelper = new UI.PopoverHelper.PopoverHelper(this._element, this._getPopoverRequest.bind(this)); - this._popoverHelper.setHasPadding(true); - this._popoverHelper.setTimeout(0, 100); + this._imagePreviewPopover = new ImagePreviewPopover( + this.contentElement, + event => { + let link = event.target; + while (link && !link[HrefSymbol]) { + link = link.parentElementOrShadowHost(); + } + return link; + }, + link => { + const listItem = link.enclosingNodeOrSelfWithNodeName('li'); + if (!listItem) { + return null; + } + return /** @type {!ElementsTreeElement} */ (listItem.treeElement).node(); + }); /** @type {!Map} */ this._updateRecords = new Map(); @@ -324,7 +337,7 @@ export class ElementsTreeOutline extends UI.TreeOutline.TreeOutline { } this._visible = visible; if (!this._visible) { - this._popoverHelper.hidePopover(); + this._imagePreviewPopover.hide(); if (this._multilineEditing) { this._multilineEditing.cancel(); } @@ -574,38 +587,6 @@ export class ElementsTreeOutline extends UI.TreeOutline.TreeOutline { return element; } - /** - * @param {!Event} event - * @return {?UI.PopoverRequest} - */ - _getPopoverRequest(event) { - let link = event.target; - while (link && !link[HrefSymbol]) { - link = link.parentElementOrShadowHost(); - } - if (!link) { - return null; - } - - return { - box: link.boxInWindow(), - show: async popover => { - const listItem = link.enclosingNodeOrSelfWithNodeName('li'); - if (!listItem) { - return false; - } - const node = /** @type {!ElementsTreeElement} */ (listItem.treeElement).node(); - const precomputedFeatures = await Components.ImagePreview.ImagePreview.loadDimensionsForNode(node); - const preview = await Components.ImagePreview.ImagePreview.build( - node.domModel().target(), link[HrefSymbol], true, {precomputedFeatures}); - if (preview) { - popover.contentElement.appendChild(preview); - } - return !!preview; - } - }; - } - /** * @param {!Event} event */ @@ -1047,7 +1028,7 @@ export class ElementsTreeOutline extends UI.TreeOutline.TreeOutline { _reset() { this.rootDOMNode = null; this.selectDOMNode(null, false); - this._popoverHelper.hidePopover(); + this._imagePreviewPopover.hide(); delete this._clipboardNodeData; SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); this._updateRecords.clear(); diff --git a/front_end/elements/ImagePreviewPopover.js b/front_end/elements/ImagePreviewPopover.js new file mode 100644 index 0000000000..309468d143 --- /dev/null +++ b/front_end/elements/ImagePreviewPopover.js @@ -0,0 +1,71 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import * as Components from '../components/components.js'; +import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars +import * as UI from '../ui/ui.js'; + +/** + * ImagePreviewPopover sets listeners on the container element to display + * an image preview if needed. The image URL comes from the event (mouseover) target + * in a propery identified by HrefSymbol. To enable preview for any child element + * set the property HrefSymbol. + */ +export class ImagePreviewPopover { + /** + * @param {!Element} container + * @param {function(!Event):?Element} getLinkElement + * @param {function(!Element):?SDK.DOMModel.DOMNode} getDOMNode + */ + constructor(container, getLinkElement, getDOMNode) { + this._getLinkElement = getLinkElement; + this._getDOMNode = getDOMNode; + this._popover = new UI.PopoverHelper.PopoverHelper(container, this._handleRequest.bind(this)); + this._popover.setHasPadding(true); + this._popover.setTimeout(0, 100); + } + + /** + * @param {!Event} event + * @return {?UI.PopoverHelper.PopoverRequest} + */ + _handleRequest(event) { + const link = this._getLinkElement(event); + if (!link || !link[HrefSymbol]) { + return null; + } + return { + box: link.boxInWindow(), + hide: undefined, + show: async popover => { + const node = this._getDOMNode(/** @type {!Element} */ (link)); + if (!node) { + return false; + } + const precomputedFeatures = await Components.ImagePreview.ImagePreview.loadDimensionsForNode(node); + const preview = await Components.ImagePreview.ImagePreview.build( + node.domModel().target(), link[HrefSymbol], true, {precomputedFeatures}); + if (preview) { + popover.contentElement.appendChild(preview); + } + return !!preview; + } + }; + } + + hide() { + this._popover.hidePopover(); + } + + /** + * @param {!Element} element + * @param {string} url + */ + static setImageUrl(element, url) { + element[HrefSymbol] = url; + return element; + } +} + +export const HrefSymbol = Symbol('ImagePreviewPopover.Href'); diff --git a/front_end/elements/StylesSidebarPane.js b/front_end/elements/StylesSidebarPane.js index 0e2015ca86..81d90ace9a 100644 --- a/front_end/elements/StylesSidebarPane.js +++ b/front_end/elements/StylesSidebarPane.js @@ -39,6 +39,7 @@ import * as UI from '../ui/ui.js'; import {ColorSwatchPopoverIcon, ShadowSwatchPopoverHelper} from './ColorSwatchPopoverIcon.js'; import {linkifyDeferredNodeReference} from './DOMLinkifier.js'; import {ElementsSidebarPane} from './ElementsSidebarPane.js'; +import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {StylePropertyHighlighter} from './StylePropertyHighlighter.js'; import {StylePropertyTreeElement} from './StylePropertyTreeElement.js'; import {Context} from './StylePropertyTreeElement.js'; // eslint-disable-line no-unused-vars @@ -90,6 +91,14 @@ export class StylesSidebarPane extends ElementsSidebarPane { self.UI.context.addFlavorChangeListener(SDK.DOMModel.DOMNode, this.forceUpdate, this); this.contentElement.addEventListener('copy', this._clipboardCopy.bind(this)); this._resizeThrottler = new Common.Throttler.Throttler(100); + + this._imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => { + const link = event.composedPath()[0]; + if (link instanceof Element) { + return link; + } + return null; + }, () => this.node()); } /** @@ -707,6 +716,7 @@ export class StylesSidebarPane extends ElementsSidebarPane { */ willHide() { this._swatchPopoverHelper.hide(); + this._imagePreviewPopover.hide(); super.willHide(); } @@ -2704,15 +2714,18 @@ export class StylesSidebarPropertyRenderer { } else if (this._node) { hrefUrl = this._node.resolveURL(url); } - container.appendChild(Components.Linkifier.Linkifier.linkifyURL(hrefUrl || url, { - text: url, - preventClick: true, - // crbug.com/1027168 - // We rely on CSS text-overflow: ellipsis to hide long URLs in the Style panel, - // so that we don't have to keep two versions (original vs. trimmed) of URL - // at the same time, which complicates both StylesSidebarPane and StylePropertyTreeElement. - bypassURLTrimming: true, - })); + const link = ImagePreviewPopover.setImageUrl( + Components.Linkifier.Linkifier.linkifyURL(hrefUrl || url, { + text: url, + preventClick: true, + // crbug.com/1027168 + // We rely on CSS text-overflow: ellipsis to hide long URLs in the Style panel, + // so that we don't have to keep two versions (original vs. trimmed) of URL + // at the same time, which complicates both StylesSidebarPane and StylePropertyTreeElement. + bypassURLTrimming: true, + }), + url); + container.appendChild(link); container.createTextChild(')'); return container; } diff --git a/front_end/elements/module.json b/front_end/elements/module.json index 11d98c297f..3a1c49b2d5 100644 --- a/front_end/elements/module.json +++ b/front_end/elements/module.json @@ -323,7 +323,8 @@ "ElementsPanel.js", "ClassesPaneWidget.js", "ElementStatePaneWidget.js", - "ElementsTreeElementHighlighter.js" + "ElementsTreeElementHighlighter.js", + "ImagePreviewPopover.js" ], "resources": [ "breadcrumbs.css",