/* 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 . */ import React, { Component } from "devtools/client/shared/vendor/react"; import { div } from "devtools/client/shared/vendor/react-dom-factories"; import PropTypes from "devtools/client/shared/vendor/react-prop-types"; import { connect } from "devtools/client/shared/vendor/react-redux"; const Reps = ChromeUtils.importESModule( "resource://devtools/client/shared/components/reps/index.mjs" ); const { REPS: { Grip }, MODE, } = Reps; import * as objectInspector from "resource://devtools/client/shared/components/object-inspector/index.js"; const { ObjectInspector, utils } = objectInspector; const { node: { nodeIsPrimitive }, } = utils; import ExceptionPopup from "./ExceptionPopup"; import actions from "../../../actions/index"; import Popover from "../../shared/Popover"; export class Popup extends Component { constructor(props) { super(props); } static get propTypes() { return { clearPreview: PropTypes.func.isRequired, editorRef: PropTypes.object.isRequired, highlightDomElement: PropTypes.func.isRequired, openElementInInspector: PropTypes.func.isRequired, openLink: PropTypes.func.isRequired, preview: PropTypes.object.isRequired, selectSourceURL: PropTypes.func.isRequired, unHighlightDomElement: PropTypes.func.isRequired, }; } componentDidMount() { this.addHighlightToToken(this.props.preview.target); } componentWillUnmount() { this.removeHighlightFromToken(this.props.preview.target); } componentDidUpdate(prevProps) { const { target } = this.props.preview; if (prevProps.target == target) { return; } this.removeHighlightFromToken(prevProps.preview.target); this.addHighlightToToken(target); } addHighlightToToken(target) { if (!target) { return; } target.classList.add("preview-token"); addHighlightToTargetSiblings(target, this.props); } removeHighlightFromToken(target) { if (!target) { return; } target.classList.remove("preview-token"); removeHighlightForTargetSiblings(target); } calculateMaxHeight = () => { const { editorRef } = this.props; if (!editorRef) { return "auto"; } const { height, top } = editorRef.getBoundingClientRect(); const maxHeight = height + top; if (maxHeight < 250) { return maxHeight; } return 250; }; createElement(element) { return document.createElement(element); } renderExceptionPreview(exception) { return React.createElement(ExceptionPopup, { exception, mouseout: this.props.clearPreview, }); } renderPreview() { const { preview: { root, exception, previewType }, } = this.props; const usesCustomFormatter = root?.contents?.value?.useCustomFormatter ?? false; if (exception) { return this.renderExceptionPreview(exception); } return div( { className: `preview-popup preview-type-${previewType}`, style: { maxHeight: this.calculateMaxHeight(), }, }, // Bug 1915610 - JS Tracer isn't localized yet previewType == "tracer" ? div({ className: "preview-tracer-header" }, "Tracer preview") : null, previewType == "tracer" && !nodeIsPrimitive(root) ? div( { className: "preview-tracer-warning" }, "Attribute previews on traced objects are showing the current values and not the value at execution of the selected frame." ) : null, React.createElement(ObjectInspector, { roots: [root], autoExpandDepth: 1, autoReleaseObjectActors: false, mode: usesCustomFormatter ? MODE.LONG : MODE.SHORT, disableWrap: true, displayRootNodeAsHeader: true, focusable: false, openLink: this.props.openLink, defaultRep: Grip, createElement: this.createElement, onDOMNodeClick: grip => this.props.openElementInInspector(grip), onInspectIconClick: grip => this.props.openElementInInspector(grip), onDOMNodeMouseOver: grip => this.props.highlightDomElement(grip), onDOMNodeMouseOut: grip => this.props.unHighlightDomElement(grip), mayUseCustomFormatter: true, onViewSourceInDebugger: ({ url, line, column }) => this.props.selectSourceURL(url, { line, column }), }) ); } getPreviewType() { const { preview: { root, exception }, } = this.props; if (exception || nodeIsPrimitive(root)) { return "tooltip"; } return "popover"; } render() { const { preview: { cursorPos, resultGrip, exception }, editorRef, } = this.props; if ( !exception && (typeof resultGrip == "undefined" || resultGrip?.optimizedOut) ) { return null; } const type = this.getPreviewType(); return React.createElement( Popover, { targetPosition: cursorPos, type, editorRef, target: this.props.preview.target, mouseout: this.props.clearPreview, }, this.renderPreview() ); } } export function addHighlightToTargetSiblings(target, props) { // This function searches for related tokens that should also be highlighted when previewed. // Here is the process: // It conducts a search on the target's next siblings and then another search for the previous siblings. // If a sibling is not an element node (nodeType === 1), the highlight is not added and the search is short-circuited. // If the element sibling is the same token type as the target, and is also found in the preview expression, the highlight class is added. const tokenType = target.classList.item(0); const previewExpression = props.preview.expression; if ( tokenType && previewExpression && target.innerHTML !== previewExpression ) { let nextSibling = target.nextSibling; let nextElementSibling = target.nextElementSibling; // Note: Declaring previous/next ELEMENT siblings as well because // properties like innerHTML can't be checked on nextSibling // without creating a flow error even if the node is an element type. while ( nextSibling && nextElementSibling && nextSibling.nodeType === 1 && nextElementSibling.className.includes(tokenType) && previewExpression.includes(nextElementSibling.innerHTML) ) { // All checks passed, add highlight and continue the search. nextElementSibling.classList.add("preview-token"); nextSibling = nextSibling.nextSibling; nextElementSibling = nextElementSibling.nextElementSibling; } let previousSibling = target.previousSibling; let previousElementSibling = target.previousElementSibling; while ( previousSibling && previousElementSibling && previousSibling.nodeType === 1 && previousElementSibling.className.includes(tokenType) && previewExpression.includes(previousElementSibling.innerHTML) ) { // All checks passed, add highlight and continue the search. previousElementSibling.classList.add("preview-token"); previousSibling = previousSibling.previousSibling; previousElementSibling = previousElementSibling.previousElementSibling; } } } export function removeHighlightForTargetSiblings(target) { // Look at target's previous and next token siblings. // If they also have the highlight class 'preview-token', // remove that class. let nextSibling = target.nextElementSibling; while (nextSibling && nextSibling.className.includes("preview-token")) { nextSibling.classList.remove("preview-token"); nextSibling = nextSibling.nextElementSibling; } let previousSibling = target.previousElementSibling; while ( previousSibling && previousSibling.className.includes("preview-token") ) { previousSibling.classList.remove("preview-token"); previousSibling = previousSibling.previousElementSibling; } } const mapDispatchToProps = { addExpression: actions.addExpression, selectSourceURL: actions.selectSourceURL, openLink: actions.openLink, openElementInInspector: actions.openElementInInspectorCommand, highlightDomElement: actions.highlightDomElement, unHighlightDomElement: actions.unHighlightDomElement, }; export default connect(null, mapDispatchToProps)(Popup);