diff options
Diffstat (limited to 'devtools/client/shared/components/frame.js')
-rw-r--r-- | devtools/client/shared/components/frame.js | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/devtools/client/shared/components/frame.js b/devtools/client/shared/components/frame.js new file mode 100644 index 000000000..5abe5f057 --- /dev/null +++ b/devtools/client/shared/components/frame.js @@ -0,0 +1,239 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react"); +const { getSourceNames, parseURL, + isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const l10n = new LocalizationHelper("devtools/client/locales/components.properties"); +const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties"); + +module.exports = createClass({ + displayName: "Frame", + + propTypes: { + // SavedFrame, or an object containing all the required properties. + frame: PropTypes.shape({ + functionDisplayName: PropTypes.string, + source: PropTypes.string.isRequired, + line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + }).isRequired, + // Clicking on the frame link -- probably should link to the debugger. + onClick: PropTypes.func.isRequired, + // Option to display a function name before the source link. + showFunctionName: PropTypes.bool, + // Option to display a function name even if it's anonymous. + showAnonymousFunctionName: PropTypes.bool, + // Option to display a host name after the source link. + showHost: PropTypes.bool, + // Option to display a host name if the filename is empty or just '/' + showEmptyPathAsHost: PropTypes.bool, + // Option to display a full source instead of just the filename. + showFullSourceUrl: PropTypes.bool, + // Service to enable the source map feature for console. + sourceMapService: PropTypes.object, + }, + + getDefaultProps() { + return { + showFunctionName: false, + showAnonymousFunctionName: false, + showHost: false, + showEmptyPathAsHost: false, + showFullSourceUrl: false, + }; + }, + + componentWillMount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.subscribe(source, this.onSourceUpdated); + } + }, + + componentWillUnmount() { + const sourceMapService = this.props.sourceMapService; + if (sourceMapService) { + const source = this.getSource(); + sourceMapService.unsubscribe(source, this.onSourceUpdated); + } + }, + + /** + * Component method to update the FrameView when a resolved location is available + * @param event + * @param location + */ + onSourceUpdated(event, location, resolvedLocation) { + const frame = this.getFrame(resolvedLocation); + this.setState({ + frame, + isSourceMapped: true, + }); + }, + + /** + * Utility method to convert the Frame object to the + * Source Object model required by SourceMapService + * @param frame + * @returns {{url: *, line: *, column: *}} + */ + getSource(frame) { + frame = frame || this.props.frame; + const { source, line, column } = frame; + return { + url: source, + line, + column, + }; + }, + + /** + * Utility method to convert the Source object model to the + * Frame object model required by FrameView class. + * @param source + * @returns {{source: *, line: *, column: *, functionDisplayName: *}} + */ + getFrame(source) { + const { url, line, column } = source; + return { + source: url, + line, + column, + functionDisplayName: this.props.frame.functionDisplayName, + }; + }, + + render() { + let frame, isSourceMapped; + let { + onClick, + showFunctionName, + showAnonymousFunctionName, + showHost, + showEmptyPathAsHost, + showFullSourceUrl + } = this.props; + + if (this.state && this.state.isSourceMapped) { + frame = this.state.frame; + isSourceMapped = this.state.isSourceMapped; + } else { + frame = this.props.frame; + } + + let source = frame.source ? String(frame.source) : ""; + let line = frame.line != void 0 ? Number(frame.line) : null; + let column = frame.column != void 0 ? Number(frame.column) : null; + + const { short, long, host } = getSourceNames(source); + // Reparse the URL to determine if we should link this; `getSourceNames` + // has already cached this indirectly. We don't want to attempt to + // link to "self-hosted" and "(unknown)". However, we do want to link + // to Scratchpad URIs. + // Source mapped sources might not necessary linkable, but they + // are still valid in the debugger. + const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)) + || isSourceMapped; + const elements = []; + const sourceElements = []; + let sourceEl; + + let tooltip = long; + + // Exclude all falsy values, including `0`, as line numbers start with 1. + if (line) { + tooltip += `:${line}`; + // Intentionally exclude 0 + if (column) { + tooltip += `:${column}`; + } + } + + let attributes = { + "data-url": long, + className: "frame-link", + }; + + if (showFunctionName) { + let functionDisplayName = frame.functionDisplayName; + if (!functionDisplayName && showAnonymousFunctionName) { + functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction"); + } + + if (functionDisplayName) { + elements.push( + dom.span({ className: "frame-link-function-display-name" }, + functionDisplayName), + " " + ); + } + } + + let displaySource = showFullSourceUrl ? long : short; + if (isSourceMapped) { + displaySource = getSourceMappedFile(displaySource); + } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) { + displaySource = host; + } + + sourceElements.push(dom.span({ + className: "frame-link-filename", + }, displaySource)); + + // If we have a line number > 0. + if (line) { + let lineInfo = `:${line}`; + // Add `data-line` attribute for testing + attributes["data-line"] = line; + + // Intentionally exclude 0 + if (column) { + lineInfo += `:${column}`; + // Add `data-column` attribute for testing + attributes["data-column"] = column; + } + + sourceElements.push(dom.span({ className: "frame-link-line" }, lineInfo)); + } + + // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL + // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056. + let sourceInnerEl = dom.span({ + className: "frame-link-source-inner", + title: isLinkable ? + l10n.getFormatStr("frame.viewsourceindebugger", tooltip) : tooltip, + }, sourceElements); + + // If source is not a URL (self-hosted, eval, etc.), don't make + // it an anchor link, as we can't link to it. + if (isLinkable) { + sourceEl = dom.a({ + onClick: e => { + e.preventDefault(); + onClick(this.getSource(frame)); + }, + href: source, + className: "frame-link-source", + draggable: false, + }, sourceInnerEl); + } else { + sourceEl = dom.span({ + className: "frame-link-source", + }, sourceInnerEl); + } + elements.push(sourceEl); + + if (showHost && host) { + elements.push(" ", dom.span({ className: "frame-link-host" }, host)); + } + + return dom.span(attributes, ...elements); + } +}); |