summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/Frame.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/Frame.js')
-rw-r--r--devtools/client/shared/components/Frame.js401
1 files changed, 401 insertions, 0 deletions
diff --git a/devtools/client/shared/components/Frame.js b/devtools/client/shared/components/Frame.js
new file mode 100644
index 0000000000..4efc7d3bd6
--- /dev/null
+++ b/devtools/client/shared/components/Frame.js
@@ -0,0 +1,401 @@
+/* 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 {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ getUnicodeUrl,
+ getUnicodeUrlPath,
+ getUnicodeHostname,
+} = require("resource://devtools/client/shared/unicode-url.js");
+const {
+ getSourceNames,
+ parseURL,
+ getSourceMappedFile,
+} = require("resource://devtools/client/shared/source-utils.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const {
+ MESSAGE_SOURCE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+);
+const webl10n = new LocalizationHelper(
+ "devtools/client/locales/webconsole.properties"
+);
+
+function savedFrameToLocation(frame) {
+ const { source: url, line, column, sourceId } = frame;
+ return {
+ url,
+ line,
+ column,
+ // The sourceId will be a string if it's a source actor ID, otherwise
+ // it is either a Spidermonkey-internal ID from a SavedFrame or missing,
+ // and in either case we can't use the ID for anything useful.
+ id: typeof sourceId === "string" ? sourceId : null,
+ };
+}
+
+/**
+ * Get the tooltip message.
+ * @param {string|undefined} messageSource
+ * @param {string} url
+ * @returns {string}
+ */
+function getTooltipMessage(messageSource, url) {
+ if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
+ return l10n.getFormatStr("frame.viewsourceinstyleeditor", url);
+ }
+ return l10n.getFormatStr("frame.viewsourceindebugger", url);
+}
+
+class Frame extends Component {
+ static get propTypes() {
+ return {
+ // Optional className that will be put into the element.
+ className: PropTypes.string,
+ // SavedFrame, or an object containing all the required properties.
+ frame: PropTypes.shape({
+ functionDisplayName: PropTypes.string,
+ // This could be a SavedFrame with a numeric sourceId, or it could
+ // be a SavedFrame-like client-side object, in which case the
+ // "sourceId" will be a source actor ID.
+ sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ 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,
+ // 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.
+ sourceMapURLService: PropTypes.object,
+ // The source of the message
+ messageSource: PropTypes.string,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ showFunctionName: false,
+ showAnonymousFunctionName: false,
+ showHost: false,
+ showEmptyPathAsHost: false,
+ showFullSourceUrl: false,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ originalLocation: null,
+ };
+ this._locationChanged = this._locationChanged.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ if (this.props.sourceMapURLService) {
+ const location = savedFrameToLocation(this.props.frame);
+ // Many things that make use of this component either:
+ // a) Pass in no sourceId because they have no way to know.
+ // b) Pass in no sourceId because the actor wasn't created when the
+ // server sent its response.
+ //
+ // and due to that, we need to use subscribeByLocation in order to
+ // handle both cases with an without an ID.
+ this.unsubscribeSourceMapURLService =
+ this.props.sourceMapURLService.subscribeByLocation(
+ location,
+ this._locationChanged
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.unsubscribeSourceMapURLService) {
+ this.unsubscribeSourceMapURLService();
+ }
+ }
+
+ _locationChanged(originalLocation) {
+ this.setState({ originalLocation });
+ }
+
+ /**
+ * Get current location's source, line, and column.
+ * @returns {{source: string, line: number|null, column: number|null}}
+ */
+ #getCurrentLocationInfo = () => {
+ const { frame } = this.props;
+ const { originalLocation } = this.state;
+
+ const generatedLocation = savedFrameToLocation(frame);
+ const currentLocation = originalLocation || generatedLocation;
+
+ const source = currentLocation.url || "";
+ const line =
+ currentLocation.line != void 0 ? Number(currentLocation.line) : null;
+ const column =
+ currentLocation.column != void 0 ? Number(currentLocation.column) : null;
+ return {
+ source,
+ line,
+ column,
+ };
+ };
+
+ /**
+ * Get unicode hostname of the source link.
+ * @returns {string}
+ */
+ #getCurrentLocationUnicodeHostName = () => {
+ const { source } = this.#getCurrentLocationInfo();
+
+ const { host } = getSourceNames(source);
+ return host ? getUnicodeHostname(host) : "";
+ };
+
+ /**
+ * Check if the current location is linkable.
+ * @returns {boolean}
+ */
+ #isCurrentLocationLinkable = () => {
+ const { frame } = this.props;
+ const { originalLocation } = this.state;
+
+ const generatedLocation = savedFrameToLocation(frame);
+
+ // 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)".
+ // Source mapped sources might not necessary linkable, but they
+ // are still valid in the debugger.
+ // If we have a source ID then we can show the source in the debugger.
+ return !!(
+ originalLocation ||
+ generatedLocation.id ||
+ !!parseURL(generatedLocation.url)
+ );
+ };
+
+ /**
+ * Get the props of the top element.
+ */
+ #getTopElementProps = () => {
+ const { className } = this.props;
+
+ const { source, line, column } = this.#getCurrentLocationInfo();
+ const { long } = getSourceNames(source);
+ const props = {
+ "data-url": long,
+ className: "frame-link" + (className ? ` ${className}` : ""),
+ };
+
+ // If we have a line number > 0.
+ if (line) {
+ // Add `data-line` attribute for testing
+ props["data-line"] = line;
+
+ // Intentionally exclude 0
+ if (column) {
+ // Add `data-column` attribute for testing
+ props["data-column"] = column;
+ }
+ }
+ return props;
+ };
+
+ /**
+ * Get the props of the source element.
+ */
+ #getSourceElementsProps = () => {
+ const { frame, onClick, messageSource } = this.props;
+
+ const generatedLocation = savedFrameToLocation(frame);
+ const { source, line, column } = this.#getCurrentLocationInfo();
+ const { long } = getSourceNames(source);
+ let url = getUnicodeUrl(long);
+
+ // Exclude all falsy values, including `0`, as line numbers start with 1.
+ if (line) {
+ url += `:${line}`;
+ // Intentionally exclude 0
+ if (column) {
+ url += `:${column}`;
+ }
+ }
+
+ const isLinkable = this.#isCurrentLocationLinkable();
+
+ // 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.
+ const tooltipMessage = getTooltipMessage(messageSource, url);
+
+ const sourceElConfig = {
+ key: "source",
+ className: "frame-link-source",
+ title: isLinkable ? tooltipMessage : url,
+ };
+
+ if (isLinkable) {
+ return {
+ ...sourceElConfig,
+ onClick: e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ onClick(generatedLocation);
+ },
+ href: source,
+ draggable: false,
+ };
+ }
+
+ return sourceElConfig;
+ };
+
+ /**
+ * Render the source elements.
+ * @returns {React.ReactNode}
+ */
+ #renderSourceElements = () => {
+ const { line, column } = this.#getCurrentLocationInfo();
+
+ const sourceElements = [this.#renderDisplaySource()];
+
+ if (line) {
+ let lineInfo = `:${line}`;
+
+ // Intentionally exclude 0
+ if (column) {
+ lineInfo += `:${column}`;
+ }
+
+ sourceElements.push(
+ dom.span(
+ {
+ key: "line",
+ className: "frame-link-line",
+ },
+ lineInfo
+ )
+ );
+ }
+
+ if (this.#isCurrentLocationLinkable()) {
+ return dom.a(this.#getSourceElementsProps(), 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.
+ return dom.span(this.#getSourceElementsProps(), sourceElements);
+ };
+
+ /**
+ * Render the display source.
+ * @returns {React.ReactNode}
+ */
+ #renderDisplaySource = () => {
+ const { showEmptyPathAsHost, showFullSourceUrl } = this.props;
+ const { originalLocation } = this.state;
+
+ const { source } = this.#getCurrentLocationInfo();
+ const { short, long, host } = getSourceNames(source);
+ const unicodeShort = getUnicodeUrlPath(short);
+ const unicodeLong = getUnicodeUrl(long);
+ let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort;
+ if (originalLocation) {
+ displaySource = getSourceMappedFile(displaySource);
+
+ // In case of pretty-printed HTML file, we would only get the formatted suffix; replace
+ // it with the full URL instead
+ if (showEmptyPathAsHost && displaySource == ":formatted") {
+ displaySource = host + displaySource;
+ }
+ } else if (
+ showEmptyPathAsHost &&
+ (displaySource === "" || displaySource === "/")
+ ) {
+ displaySource = host;
+ }
+
+ return dom.span(
+ {
+ key: "filename",
+ className: "frame-link-filename",
+ },
+ displaySource
+ );
+ };
+
+ /**
+ * Render the function display name.
+ * @returns {React.ReactNode}
+ */
+ #renderFunctionDisplayName = () => {
+ const { frame, showFunctionName, showAnonymousFunctionName } = this.props;
+ if (!showFunctionName) {
+ return null;
+ }
+ const functionDisplayName = frame.functionDisplayName;
+ if (functionDisplayName || showAnonymousFunctionName) {
+ return [
+ dom.span(
+ {
+ key: "function-display-name",
+ className: "frame-link-function-display-name",
+ },
+ functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction")
+ ),
+ " ",
+ ];
+ }
+ return null;
+ };
+
+ render() {
+ const { showHost } = this.props;
+
+ const elements = [
+ this.#renderFunctionDisplayName(),
+ this.#renderSourceElements(),
+ ];
+
+ const unicodeHost = showHost
+ ? this.#getCurrentLocationUnicodeHostName()
+ : null;
+ if (unicodeHost) {
+ elements.push(" ");
+ elements.push(
+ dom.span(
+ {
+ key: "host",
+ className: "frame-link-host",
+ },
+ unicodeHost
+ )
+ );
+ }
+
+ return dom.span(this.#getTopElementProps(), ...elements);
+ }
+}
+
+module.exports = Frame;