Popup.js (8593B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import React, { Component } from "devtools/client/shared/vendor/react"; 6 import { div } from "devtools/client/shared/vendor/react-dom-factories"; 7 import PropTypes from "devtools/client/shared/vendor/react-prop-types"; 8 import { connect } from "devtools/client/shared/vendor/react-redux"; 9 10 const Reps = ChromeUtils.importESModule( 11 "resource://devtools/client/shared/components/reps/index.mjs" 12 ); 13 const { 14 REPS: { Grip }, 15 MODE, 16 } = Reps; 17 import * as objectInspector from "resource://devtools/client/shared/components/object-inspector/index.js"; 18 19 const { ObjectInspector, utils } = objectInspector; 20 21 const { 22 node: { nodeIsPrimitive }, 23 } = utils; 24 25 import ExceptionPopup from "./ExceptionPopup"; 26 27 import actions from "../../../actions/index"; 28 import Popover from "../../shared/Popover"; 29 30 export class Popup extends Component { 31 constructor(props) { 32 super(props); 33 } 34 35 static get propTypes() { 36 return { 37 clearPreview: PropTypes.func.isRequired, 38 editorRef: PropTypes.object.isRequired, 39 highlightDomElement: PropTypes.func.isRequired, 40 openElementInInspector: PropTypes.func.isRequired, 41 openLink: PropTypes.func.isRequired, 42 preview: PropTypes.object.isRequired, 43 selectSourceURL: PropTypes.func.isRequired, 44 unHighlightDomElement: PropTypes.func.isRequired, 45 }; 46 } 47 48 componentDidMount() { 49 this.addHighlightToToken(this.props.preview.target); 50 } 51 52 componentWillUnmount() { 53 this.removeHighlightFromToken(this.props.preview.target); 54 } 55 56 componentDidUpdate(prevProps) { 57 const { target } = this.props.preview; 58 if (prevProps.target == target) { 59 return; 60 } 61 62 this.removeHighlightFromToken(prevProps.preview.target); 63 this.addHighlightToToken(target); 64 } 65 66 addHighlightToToken(target) { 67 if (!target) { 68 return; 69 } 70 71 target.classList.add("preview-token"); 72 addHighlightToTargetSiblings(target, this.props); 73 } 74 75 removeHighlightFromToken(target) { 76 if (!target) { 77 return; 78 } 79 80 target.classList.remove("preview-token"); 81 removeHighlightForTargetSiblings(target); 82 } 83 84 calculateMaxHeight = () => { 85 const { editorRef } = this.props; 86 if (!editorRef) { 87 return "auto"; 88 } 89 90 const { height, top } = editorRef.getBoundingClientRect(); 91 const maxHeight = height + top; 92 if (maxHeight < 250) { 93 return maxHeight; 94 } 95 96 return 250; 97 }; 98 99 createElement(element) { 100 return document.createElement(element); 101 } 102 103 renderExceptionPreview(exception) { 104 return React.createElement(ExceptionPopup, { 105 exception, 106 mouseout: this.props.clearPreview, 107 }); 108 } 109 110 renderPreview() { 111 const { 112 preview: { root, exception, previewType }, 113 } = this.props; 114 115 const usesCustomFormatter = 116 root?.contents?.value?.useCustomFormatter ?? false; 117 118 if (exception) { 119 return this.renderExceptionPreview(exception); 120 } 121 122 return div( 123 { 124 className: `preview-popup preview-type-${previewType}`, 125 style: { 126 maxHeight: this.calculateMaxHeight(), 127 }, 128 }, 129 // Bug 1915610 - JS Tracer isn't localized yet 130 previewType == "tracer" 131 ? div({ className: "preview-tracer-header" }, "Tracer preview") 132 : null, 133 previewType == "tracer" && !nodeIsPrimitive(root) 134 ? div( 135 { className: "preview-tracer-warning" }, 136 "Attribute previews on traced objects are showing the current values and not the value at execution of the selected frame." 137 ) 138 : null, 139 React.createElement(ObjectInspector, { 140 roots: [root], 141 autoExpandDepth: 1, 142 autoReleaseObjectActors: false, 143 mode: usesCustomFormatter ? MODE.LONG : MODE.SHORT, 144 disableWrap: true, 145 displayRootNodeAsHeader: true, 146 focusable: false, 147 openLink: this.props.openLink, 148 defaultRep: Grip, 149 createElement: this.createElement, 150 onDOMNodeClick: grip => this.props.openElementInInspector(grip), 151 onInspectIconClick: grip => this.props.openElementInInspector(grip), 152 onDOMNodeMouseOver: grip => this.props.highlightDomElement(grip), 153 onDOMNodeMouseOut: grip => this.props.unHighlightDomElement(grip), 154 mayUseCustomFormatter: true, 155 onViewSourceInDebugger: ({ url, line, column }) => 156 this.props.selectSourceURL(url, { line, column }), 157 }) 158 ); 159 } 160 161 getPreviewType() { 162 const { 163 preview: { root, exception }, 164 } = this.props; 165 if (exception || nodeIsPrimitive(root)) { 166 return "tooltip"; 167 } 168 169 return "popover"; 170 } 171 172 render() { 173 const { 174 preview: { cursorPos, resultGrip, exception }, 175 editorRef, 176 } = this.props; 177 178 if ( 179 !exception && 180 (typeof resultGrip == "undefined" || resultGrip?.optimizedOut) 181 ) { 182 return null; 183 } 184 185 const type = this.getPreviewType(); 186 return React.createElement( 187 Popover, 188 { 189 targetPosition: cursorPos, 190 type, 191 editorRef, 192 target: this.props.preview.target, 193 mouseout: this.props.clearPreview, 194 }, 195 this.renderPreview() 196 ); 197 } 198 } 199 200 export function addHighlightToTargetSiblings(target, props) { 201 // This function searches for related tokens that should also be highlighted when previewed. 202 // Here is the process: 203 // It conducts a search on the target's next siblings and then another search for the previous siblings. 204 // If a sibling is not an element node (nodeType === 1), the highlight is not added and the search is short-circuited. 205 // 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. 206 207 const tokenType = target.classList.item(0); 208 const previewExpression = props.preview.expression; 209 210 if ( 211 tokenType && 212 previewExpression && 213 target.innerHTML !== previewExpression 214 ) { 215 let nextSibling = target.nextSibling; 216 let nextElementSibling = target.nextElementSibling; 217 218 // Note: Declaring previous/next ELEMENT siblings as well because 219 // properties like innerHTML can't be checked on nextSibling 220 // without creating a flow error even if the node is an element type. 221 while ( 222 nextSibling && 223 nextElementSibling && 224 nextSibling.nodeType === 1 && 225 nextElementSibling.className.includes(tokenType) && 226 previewExpression.includes(nextElementSibling.innerHTML) 227 ) { 228 // All checks passed, add highlight and continue the search. 229 nextElementSibling.classList.add("preview-token"); 230 231 nextSibling = nextSibling.nextSibling; 232 nextElementSibling = nextElementSibling.nextElementSibling; 233 } 234 235 let previousSibling = target.previousSibling; 236 let previousElementSibling = target.previousElementSibling; 237 238 while ( 239 previousSibling && 240 previousElementSibling && 241 previousSibling.nodeType === 1 && 242 previousElementSibling.className.includes(tokenType) && 243 previewExpression.includes(previousElementSibling.innerHTML) 244 ) { 245 // All checks passed, add highlight and continue the search. 246 previousElementSibling.classList.add("preview-token"); 247 248 previousSibling = previousSibling.previousSibling; 249 previousElementSibling = previousElementSibling.previousElementSibling; 250 } 251 } 252 } 253 254 export function removeHighlightForTargetSiblings(target) { 255 // Look at target's previous and next token siblings. 256 // If they also have the highlight class 'preview-token', 257 // remove that class. 258 let nextSibling = target.nextElementSibling; 259 while (nextSibling && nextSibling.className.includes("preview-token")) { 260 nextSibling.classList.remove("preview-token"); 261 nextSibling = nextSibling.nextElementSibling; 262 } 263 let previousSibling = target.previousElementSibling; 264 while ( 265 previousSibling && 266 previousSibling.className.includes("preview-token") 267 ) { 268 previousSibling.classList.remove("preview-token"); 269 previousSibling = previousSibling.previousElementSibling; 270 } 271 } 272 273 const mapDispatchToProps = { 274 addExpression: actions.addExpression, 275 selectSourceURL: actions.selectSourceURL, 276 openLink: actions.openLink, 277 openElementInInspector: actions.openElementInInspectorCommand, 278 highlightDomElement: actions.highlightDomElement, 279 unHighlightDomElement: actions.unHighlightDomElement, 280 }; 281 282 export default connect(null, mapDispatchToProps)(Popup);