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",