chrome-devtools-frontend
Version:
Chrome DevTools UI
1,196 lines (1,097 loc) • 37.5 kB
JavaScript
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Bindings from '../bindings/bindings.js';
import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as UI from '../ui/ui.js';
import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars
export const UIStrings = {
/**
*@description Text in Linkifier
*/
unknown: '(unknown)',
/**
*@description Text short for automatic
*/
auto: 'auto',
/**
*@description Text in Linkifier
*@example {Sources panel} PH1
*/
revealInS: 'Reveal in {PH1}',
/**
*@description Text for revealing an item in its destination
*/
reveal: 'Reveal',
/**
*@description A context menu item in the Linkifier
*@example {Extension} PH1
*/
openUsingS: 'Open using {PH1}',
/**
* @description The name of a setting which controls how links are handled in the UI. 'Handling'
* refers to the ability of extensions to DevTools to be able to intercept link clicks so that they
* can react to them.
*/
linkHandling: 'Link handling:',
};
const str_ = i18n.i18n.registerUIStrings('components/Linkifier.js', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
/** @type {!Set<!Linkifier>} */
const instances = new Set();
/** @type {?LinkDecorator} */
let decorator = null;
/** @type {!WeakMap<!Workspace.UISourceCode.UISourceCode, !Set<!Element>>} */
const anchorsByUISourceCode = new WeakMap();
/** @type {!WeakMap<!Node, _LinkInfo>} */
const infoByAnchor = new WeakMap();
/** @type {!WeakMap<!Node, string>} */
const textByAnchor = new WeakMap();
/** @type {!Map<string, !LinkHandler>} */
const linkHandlers = new Map();
/** @type {!Common.Settings.Setting<string>} */
let linkHandlerSettingInstance;
/**
* @implements {SDK.SDKModel.Observer}
*/
export class Linkifier {
/**
* @param {number=} maxLengthForDisplayedURLs
* @param {boolean=} useLinkDecorator
* @param {function():void=} onLiveLocationUpdate
*/
constructor(maxLengthForDisplayedURLs, useLinkDecorator, onLiveLocationUpdate = () => {}) {
this._maxLength = maxLengthForDisplayedURLs || UI.UIUtils.MaxLengthForDisplayedURLs;
/** @type {!Map<!SDK.SDKModel.Target, !Array<!Element>>} */
this._anchorsByTarget = new Map();
/** @type {!Map<!SDK.SDKModel.Target, !Bindings.LiveLocation.LiveLocationPool>} */
this._locationPoolByTarget = new Map();
this._onLiveLocationUpdate = onLiveLocationUpdate;
this._useLinkDecorator = Boolean(useLinkDecorator);
instances.add(this);
SDK.SDKModel.TargetManager.instance().observeTargets(this);
}
/**
* @param {!LinkDecorator} linkDecorator
*/
static setLinkDecorator(linkDecorator) {
console.assert(!decorator, 'Cannot re-register link decorator.');
decorator = linkDecorator;
linkDecorator.addEventListener(LinkDecorator.Events.LinkIconChanged, onLinkIconChanged);
for (const linkifier of instances) {
linkifier._updateAllAnchorDecorations();
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
function onLinkIconChanged(event) {
const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data);
const links = anchorsByUISourceCode.get(uiSourceCode) || [];
for (const link of links) {
Linkifier._updateLinkDecorations(link);
}
}
}
_updateAllAnchorDecorations() {
for (const anchors of this._anchorsByTarget.values()) {
for (const anchor of anchors) {
Linkifier._updateLinkDecorations(anchor);
}
}
}
/**
* @param {!Element} anchor
* @param {!Workspace.UISourceCode.UILocation} uiLocation
*/
static _bindUILocation(anchor, uiLocation) {
const linkInfo = Linkifier.linkInfo(anchor);
if (!linkInfo) {
return;
}
linkInfo.uiLocation = uiLocation;
if (!uiLocation) {
return;
}
const uiSourceCode = uiLocation.uiSourceCode;
let sourceCodeAnchors = anchorsByUISourceCode.get(uiSourceCode);
if (!sourceCodeAnchors) {
sourceCodeAnchors = new Set();
anchorsByUISourceCode.set(uiSourceCode, sourceCodeAnchors);
}
sourceCodeAnchors.add(anchor);
}
/**
* @param {!Element} anchor
*/
static _unbindUILocation(anchor) {
const info = Linkifier.linkInfo(anchor);
if (!info || !info.uiLocation) {
return;
}
const uiSourceCode = info.uiLocation.uiSourceCode;
info.uiLocation = null;
const sourceCodeAnchors = anchorsByUISourceCode.get(uiSourceCode);
if (sourceCodeAnchors) {
sourceCodeAnchors.delete(anchor);
}
}
/**
* @override
* @param {!SDK.SDKModel.Target} target
*/
targetAdded(target) {
this._anchorsByTarget.set(target, []);
this._locationPoolByTarget.set(target, new Bindings.LiveLocation.LiveLocationPool());
}
/**
* @override
* @param {!SDK.SDKModel.Target} target
*/
targetRemoved(target) {
const locationPool = this._locationPoolByTarget.get(target);
this._locationPoolByTarget.delete(target);
if (!locationPool) {
return;
}
locationPool.disposeAll();
const anchors = /** @type {?Array<!HTMLElement>} */ (this._anchorsByTarget.get(target));
if (!anchors) {
return;
}
this._anchorsByTarget.delete(target);
for (const anchor of anchors) {
const info = Linkifier.linkInfo(anchor);
if (!info) {
continue;
}
info.liveLocation = null;
Linkifier._unbindUILocation(anchor);
const fallback = /** @type {?HTMLElement} */ (info.fallback);
if (fallback) {
// @ts-ignore
anchor.href = fallback.href;
UI.Tooltip.Tooltip.install(anchor, UI.Tooltip.Tooltip.getContent(fallback));
anchor.className = fallback.className;
anchor.textContent = fallback.textContent;
const fallbackInfo = infoByAnchor.get(fallback);
if (fallbackInfo) {
infoByAnchor.set(anchor, fallbackInfo);
}
}
}
}
/**
* @param {?SDK.SDKModel.Target} target
* @param {?string} scriptId
* @param {string} sourceURL
* @param {number|undefined} lineNumber
* @param {!LinkifyOptions=} options
* @return {?HTMLElement}
*/
maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, options) {
let fallbackAnchor = null;
const linkifyURLOptions = {
lineNumber,
maxLength: this._maxLength,
columnNumber: options ? options.columnNumber : undefined,
className: options ? options.className : undefined,
tabStop: options ? options.tabStop : undefined,
text: undefined,
preventClick: undefined,
bypassURLTrimming: undefined
};
const {columnNumber = 0, className = ''} = linkifyURLOptions;
if (sourceURL) {
fallbackAnchor = Linkifier.linkifyURL(sourceURL, linkifyURLOptions);
}
if (!target || target.isDisposed()) {
return fallbackAnchor;
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return fallbackAnchor;
}
let rawLocation;
if (scriptId) {
rawLocation = debuggerModel.createRawLocationByScriptId(scriptId, lineNumber || 0, columnNumber);
}
// The function createRawLocationByScriptId will always return a raw location. Normally
// we rely on the live location that is created from it to update missing information
// to create the link. If we, however, already have a similar script with the same source url,
// use that one.
if (!rawLocation?.script()) {
rawLocation = debuggerModel.createRawLocationByURL(sourceURL, lineNumber || 0, columnNumber) || rawLocation;
}
if (!rawLocation) {
return fallbackAnchor;
}
const createLinkOptions = {
maxLength: undefined,
title: undefined,
href: undefined,
preventClick: undefined,
bypassURLTrimming: undefined,
tabStop: options ? options.tabStop : undefined,
};
// Not initialising the anchor element with 'zero width space' (\u200b) causes a crash
// in the layout engine.
// TODO(szuend): Remove comment and workaround once the crash is fixed.
const anchor = Linkifier._createLink(
fallbackAnchor && fallbackAnchor.textContent ? fallbackAnchor.textContent : '\u200b', className,
createLinkOptions);
const info = Linkifier.linkInfo(anchor);
if (!info) {
return fallbackAnchor;
}
info.enableDecorator = this._useLinkDecorator;
info.fallback = fallbackAnchor;
const pool = this._locationPoolByTarget.get(rawLocation.debuggerModel.target());
if (!pool) {
return fallbackAnchor;
}
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
.createLiveLocation(rawLocation, this._updateAnchor.bind(this, anchor), pool)
.then(liveLocation => {
if (liveLocation) {
info.liveLocation = liveLocation;
this._onLiveLocationUpdate();
}
});
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.debuggerModel.target()));
anchors.push(anchor);
return anchor;
}
/**
* @param {?SDK.SDKModel.Target} target
* @param {?string} scriptId
* @param {string} sourceURL
* @param {number|undefined} lineNumber
* @param {!LinkifyOptions=} options
* @return {!HTMLElement}
*/
linkifyScriptLocation(target, scriptId, sourceURL, lineNumber, options) {
const scriptLink = this.maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, options);
const linkifyURLOptions = {
lineNumber,
maxLength: this._maxLength,
className: options ? options.className : undefined,
columnNumber: options ? options.columnNumber : undefined,
tabStop: options ? options.tabStop : undefined,
text: undefined,
preventClick: undefined,
bypassURLTrimming: undefined
};
return scriptLink || Linkifier.linkifyURL(sourceURL, linkifyURLOptions);
}
/**
* @param {!SDK.DebuggerModel.Location} rawLocation
* @param {string} fallbackUrl
* @param {string=} className
* @return {!Element}
*/
linkifyRawLocation(rawLocation, fallbackUrl, className) {
return this.linkifyScriptLocation(
rawLocation.debuggerModel.target(), rawLocation.scriptId, fallbackUrl, rawLocation.lineNumber,
{columnNumber: rawLocation.columnNumber, className, tabStop: undefined});
}
/**
* @param {?SDK.SDKModel.Target} target
* @param {!Protocol.Runtime.CallFrame} callFrame
* @param {!LinkifyOptions=} options
* @return {?HTMLElement}
*/
maybeLinkifyConsoleCallFrame(target, callFrame, options) {
const linkifyOptions = {
columnNumber: callFrame.columnNumber,
tabStop: options ? options.tabStop : undefined,
className: options ? options.className : undefined
};
return this.maybeLinkifyScriptLocation(
target, callFrame.scriptId, callFrame.url, callFrame.lineNumber, linkifyOptions);
}
/**
* @param {!SDK.SDKModel.Target} target
* @param {!Protocol.Runtime.StackTrace} stackTrace
* @param {string=} classes
* @return {!HTMLElement}
*/
linkifyStackTraceTopFrame(target, stackTrace, classes) {
console.assert(Boolean(stackTrace.callFrames) && Boolean(stackTrace.callFrames.length));
const topFrame = stackTrace.callFrames[0];
const fallbackAnchor = Linkifier.linkifyURL(topFrame.url, {
className: classes,
lineNumber: topFrame.lineNumber,
columnNumber: topFrame.columnNumber,
maxLength: this._maxLength,
text: undefined,
preventClick: undefined,
tabStop: undefined,
bypassURLTrimming: undefined
});
if (target.isDisposed()) {
return fallbackAnchor;
}
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
return fallbackAnchor;
}
const rawLocations = debuggerModel.createRawLocationsByStackTrace(stackTrace);
if (rawLocations.length === 0) {
return fallbackAnchor;
}
// Not initialising the anchor element with 'zero width space' (\u200b) causes a crash
// in the layout engine.
// TODO(szuend): Remove comment and workaround once the crash is fixed.
const anchor = Linkifier._createLink('\u200b', classes || '');
const info = Linkifier.linkInfo(anchor);
if (!info) {
return fallbackAnchor;
}
info.enableDecorator = this._useLinkDecorator;
info.fallback = fallbackAnchor;
const pool = this._locationPoolByTarget.get(target);
if (!pool) {
return fallbackAnchor;
}
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
.createStackTraceTopFrameLiveLocation(rawLocations, this._updateAnchor.bind(this, anchor), pool)
.then(liveLocation => {
info.liveLocation = liveLocation;
this._onLiveLocationUpdate();
});
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(target));
anchors.push(anchor);
return anchor;
}
/**
* @param {!SDK.CSSModel.CSSLocation} rawLocation
* @param {string=} classes
* @return {!Element}
*/
linkifyCSSLocation(rawLocation, classes) {
// Not initialising the anchor element with 'zero width space' (\u200b) causes a crash
// in the layout engine.
// TODO(szuend): Remove comment and workaround once the crash is fixed.
const anchor = /** @type {!HTMLElement} */ (Linkifier._createLink('\u200b', classes || ''));
const info = Linkifier.linkInfo(anchor);
if (!info) {
return anchor;
}
info.enableDecorator = this._useLinkDecorator;
const pool = this._locationPoolByTarget.get(rawLocation.cssModel().target());
if (!pool) {
return anchor;
}
Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance()
.createLiveLocation(rawLocation, this._updateAnchor.bind(this, anchor), pool)
.then(liveLocation => {
info.liveLocation = liveLocation;
this._onLiveLocationUpdate();
});
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.cssModel().target()));
anchors.push(anchor);
return anchor;
}
reset() {
// Create a copy of {keys} so {targetRemoved} can safely modify the map.
for (const target of [...this._anchorsByTarget.keys()]) {
this.targetRemoved(target);
this.targetAdded(target);
}
}
dispose() {
// Create a copy of {keys} so {targetRemoved} can safely modify the map.
for (const target of [...this._anchorsByTarget.keys()]) {
this.targetRemoved(target);
}
SDK.SDKModel.TargetManager.instance().unobserveTargets(this);
instances.delete(this);
}
/**
* @param {!HTMLElement} anchor
* @param {!Bindings.LiveLocation.LiveLocation} liveLocation
*/
async _updateAnchor(anchor, liveLocation) {
Linkifier._unbindUILocation(anchor);
const uiLocation = await liveLocation.uiLocation();
if (!uiLocation) {
if (liveLocation instanceof Bindings.CSSWorkspaceBinding.LiveLocation) {
const header = (/** @type {!Bindings.CSSWorkspaceBinding.LiveLocation} */ (liveLocation)).header();
if (header && header.ownerNode) {
anchor.addEventListener('click', event => {
event.consume(true);
Common.Revealer.reveal(header.ownerNode || null);
}, false);
Linkifier._setTrimmedText(anchor, '<style>');
}
}
return;
}
Linkifier._bindUILocation(anchor, uiLocation);
const text = uiLocation.linkText(true /* skipTrim */);
Linkifier._setTrimmedText(anchor, text, this._maxLength);
let titleText = uiLocation.uiSourceCode.url();
if (uiLocation.uiSourceCode.mimeType() === 'application/wasm') {
// For WebAssembly locations, we follow the conventions described in
// github.com/WebAssembly/design/blob/master/Web.md#developer-facing-display-conventions
if (typeof uiLocation.columnNumber === 'number') {
titleText += `:0x${uiLocation.columnNumber.toString(16)}`;
}
} else if (typeof uiLocation.lineNumber === 'number') {
titleText += ':' + (uiLocation.lineNumber + 1);
}
UI.Tooltip.Tooltip.install(anchor, titleText);
anchor.classList.toggle('webkit-html-ignore-list-link', await liveLocation.isIgnoreListed());
Linkifier._updateLinkDecorations(anchor);
}
/**
* @param {function():void} callback
*/
setLiveLocationUpdateCallback(callback) {
this._onLiveLocationUpdate = callback;
}
/**
* @param {!Element} anchor
*/
static _updateLinkDecorations(anchor) {
const info = Linkifier.linkInfo(anchor);
if (!info || !info.enableDecorator) {
return;
}
if (!decorator || !info.uiLocation) {
return;
}
if (info.icon && info.icon.parentElement) {
anchor.removeChild(info.icon);
}
const icon = decorator.linkIcon(info.uiLocation.uiSourceCode);
if (icon) {
icon.style.setProperty('margin-right', '2px');
anchor.insertBefore(icon, anchor.firstChild);
}
info.icon = icon;
}
/**
* @param {string} url
* @param {!LinkifyURLOptions=} options
* @return {!HTMLElement}
*/
static linkifyURL(url, options) {
options = options || {
text: undefined,
className: undefined,
lineNumber: undefined,
columnNumber: undefined,
preventClick: undefined,
maxLength: undefined,
tabStop: undefined,
bypassURLTrimming: undefined
};
const text = options.text;
const className = options.className || '';
const lineNumber = options.lineNumber;
const columnNumber = options.columnNumber;
const preventClick = options.preventClick;
const maxLength = options.maxLength || UI.UIUtils.MaxLengthForDisplayedURLs;
const bypassURLTrimming = options.bypassURLTrimming;
if (!url || url.trim().toLowerCase().startsWith('javascript:')) {
const element = /** @type {!HTMLElement} */ (document.createElement('span'));
if (className) {
element.className = className;
}
element.textContent = text || url || i18nString(UIStrings.unknown);
return element;
}
let linkText = text || Bindings.ResourceUtils.displayNameForURL(url);
if (typeof lineNumber === 'number' && !text) {
linkText += ':' + (lineNumber + 1);
}
const title = linkText !== url ? url : '';
const linkOptions = {maxLength, title, href: url, preventClick, tabStop: options.tabStop, bypassURLTrimming};
const link = Linkifier._createLink(linkText, className, linkOptions);
const info = Linkifier.linkInfo(link);
if (!info) {
return link;
}
if (lineNumber) {
info.lineNumber = lineNumber;
}
if (columnNumber) {
info.columnNumber = columnNumber;
}
return link;
}
/**
* @param {!Object} revealable
* @param {string|!HTMLElement} text
* @param {string=} fallbackHref
* @param {string=} title
* @param {string=} className
* @return {!HTMLElement}
*/
static linkifyRevealable(revealable, text, fallbackHref, title, className) {
const createLinkOptions = {
maxLength: UI.UIUtils.MaxLengthForDisplayedURLs,
href: fallbackHref,
title,
preventClick: undefined,
tabStop: undefined,
bypassURLTrimming: undefined
};
const link = Linkifier._createLink(text, className || '', createLinkOptions);
const linkInfo = Linkifier.linkInfo(link);
if (!linkInfo) {
return link;
}
linkInfo.revealable = revealable;
return link;
}
/**
* @param {string|!HTMLElement} text
* @param {string} className
* @param {!_CreateLinkOptions=} options
* @returns {!HTMLElement}
*/
static _createLink(text, className, options) {
options = options || {
maxLength: undefined,
title: undefined,
href: undefined,
preventClick: undefined,
tabStop: undefined,
bypassURLTrimming: undefined
};
const {maxLength, title, href, preventClick, tabStop, bypassURLTrimming} = options;
const link = /** @type {!HTMLElement}*/ (document.createElement('span'));
if (className) {
link.className = className;
}
link.classList.add('devtools-link');
if (title) {
UI.Tooltip.Tooltip.install(link, title);
}
if (href) {
// @ts-ignore
link.href = href;
}
if (text instanceof HTMLElement) {
link.appendChild(text);
} else {
if (bypassURLTrimming) {
link.classList.add('devtools-link-styled-trim');
Linkifier._appendTextWithoutHashes(link, text);
} else {
Linkifier._setTrimmedText(link, text, maxLength);
}
}
const linkInfo = {
icon: null,
enableDecorator: false,
uiLocation: null,
liveLocation: null,
url: href || null,
lineNumber: null,
columnNumber: null,
revealable: null,
fallback: null
};
infoByAnchor.set(link, linkInfo);
if (!preventClick) {
link.addEventListener('click', event => {
if (Linkifier._handleClick(event)) {
event.consume(true);
}
}, false);
link.addEventListener('keydown', event => {
if (event.key === 'Enter' && Linkifier._handleClick(event)) {
event.consume(true);
}
}, false);
} else {
link.classList.add('devtools-link-prevent-click');
}
UI.ARIAUtils.markAsLink(link);
link.tabIndex = tabStop ? 0 : -1;
return link;
}
/**
* @param {!Element} link
* @param {string} text
* @param {number=} maxLength
*/
static _setTrimmedText(link, text, maxLength) {
link.removeChildren();
if (maxLength && text.length > maxLength) {
const middleSplit = splitMiddle(text, maxLength);
Linkifier._appendTextWithoutHashes(link, middleSplit[0]);
Linkifier._appendHiddenText(link, middleSplit[1]);
Linkifier._appendTextWithoutHashes(link, middleSplit[2]);
} else {
Linkifier._appendTextWithoutHashes(link, text);
}
/**
* @param {string} string
* @param {number} maxLength
* @return {!Array<string>}
*/
function splitMiddle(string, maxLength) {
let leftIndex = Math.floor(maxLength / 2);
let rightIndex = string.length - Math.ceil(maxLength / 2) + 1;
const codePointAtRightIndex = string.codePointAt(rightIndex - 1);
// Do not truncate between characters that use multiple code points (emojis).
if (typeof codePointAtRightIndex !== 'undefined' && codePointAtRightIndex >= 0x10000) {
rightIndex++;
leftIndex++;
}
const codePointAtLeftIndex = string.codePointAt(leftIndex - 1);
if (typeof codePointAtLeftIndex !== 'undefined' && leftIndex > 0 && codePointAtLeftIndex >= 0x10000) {
leftIndex--;
}
return [string.substring(0, leftIndex), string.substring(leftIndex, rightIndex), string.substring(rightIndex)];
}
}
/**
* @param {!Element} link
* @param {string} string
*/
static _appendTextWithoutHashes(link, string) {
const hashSplit = TextUtils.TextUtils.Utils.splitStringByRegexes(string, [/[a-f0-9]{20,}/g]);
for (const match of hashSplit) {
if (match.regexIndex === -1) {
UI.UIUtils.createTextChild(link, match.value);
} else {
UI.UIUtils.createTextChild(link, match.value.substring(0, 7));
Linkifier._appendHiddenText(link, match.value.substring(7));
}
}
}
/**
* @param {!Element} link
* @param {string} string
*/
static _appendHiddenText(link, string) {
const ellipsisNode = UI.UIUtils.createTextChild(link.createChild('span', 'devtools-link-ellipsis'), '…');
textByAnchor.set(ellipsisNode, string);
}
/**
* @param {!Node} node
* @return {string}
*/
static untruncatedNodeText(node) {
return textByAnchor.get(node) || node.textContent || '';
}
/**
* @param {?Element} link
* @return {?_LinkInfo}
*/
static linkInfo(link) {
return /** @type {?_LinkInfo} */ (link ? infoByAnchor.get(link) || null : null);
}
/**
* @param {!Event} event
* @return {boolean}
*/
static _handleClick(event) {
const link = /** @type {!Element} */ (event.currentTarget);
if (UI.UIUtils.isBeingEdited(/** @type {!Node} */ (event.target)) || link.hasSelection()) {
return false;
}
const linkInfo = Linkifier.linkInfo(link);
if (!linkInfo) {
return false;
}
return Linkifier.invokeFirstAction(linkInfo);
}
/**
*
* @param {!_LinkInfo} linkInfo
*/
static _handleClickFromNewComponentLand(linkInfo) {
Linkifier.invokeFirstAction(linkInfo);
}
/**
* @param {!_LinkInfo} linkInfo
* @return {boolean}
*/
static invokeFirstAction(linkInfo) {
const actions = Linkifier._linkActions(linkInfo);
if (actions.length) {
actions[0].handler.call(null);
return true;
}
return false;
}
/**
* @return {!Common.Settings.Setting<string>}
*/
static _linkHandlerSetting() {
if (!linkHandlerSettingInstance) {
linkHandlerSettingInstance =
Common.Settings.Settings.instance().createSetting('openLinkHandler', i18nString(UIStrings.auto));
}
return linkHandlerSettingInstance;
}
/**
* @param {string} title
* @param {!LinkHandler} handler
*/
static registerLinkHandler(title, handler) {
linkHandlers.set(title, handler);
LinkHandlerSettingUI.instance()._update();
}
/**
* @param {string} title
*/
static unregisterLinkHandler(title) {
linkHandlers.delete(title);
LinkHandlerSettingUI.instance()._update();
}
/**
* @param {!Element} link
* @return {?Workspace.UISourceCode.UILocation}
*/
static uiLocation(link) {
const info = Linkifier.linkInfo(link);
return info ? info.uiLocation : null;
}
/**
* @param {!_LinkInfo} info
* @return {!Array<{section: string, title: string, handler: function()}>}
*/
static _linkActions(info) {
/** @type {!Array<{section: string, title: string, handler: function():*}>} */
const result = [];
if (!info) {
return result;
}
let url = '';
let uiLocation = null;
if (info.uiLocation) {
uiLocation = info.uiLocation;
url = uiLocation.uiSourceCode.contentURL();
} else if (info.url) {
url = info.url;
const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url) ||
Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(
Common.ParsedURL.ParsedURL.urlWithoutHash(url));
uiLocation = uiSourceCode ? uiSourceCode.uiLocation(info.lineNumber || 0, info.columnNumber || 0) : null;
}
const resource = url ? Bindings.ResourceUtils.resourceForURL(url) : null;
const contentProvider = uiLocation ? uiLocation.uiSourceCode : resource;
const revealable = info.revealable || uiLocation || resource;
if (revealable) {
const destination = Common.Revealer.revealDestination(revealable);
result.push({
section: 'reveal',
title: destination ? i18nString(UIStrings.revealInS, {PH1: destination}) : i18nString(UIStrings.reveal),
handler: () => Common.Revealer.reveal(revealable)
});
}
if (contentProvider) {
const lineNumber = uiLocation ? uiLocation.lineNumber : info.lineNumber || 0;
for (const title of linkHandlers.keys()) {
const handler = linkHandlers.get(title);
if (!handler) {
continue;
}
const action = {
section: 'reveal',
title: i18nString(UIStrings.openUsingS, {PH1: title}),
handler: handler.bind(null, contentProvider, lineNumber)
};
if (title === Linkifier._linkHandlerSetting().get()) {
result.unshift(action);
} else {
result.push(action);
}
}
}
if (resource || info.url) {
result.push({
section: 'reveal',
title: UI.UIUtils.openLinkExternallyLabel(),
handler: () => Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(url)
});
result.push({
section: 'clipboard',
title: UI.UIUtils.copyLinkAddressLabel(),
handler: () => Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(url)
});
}
if (uiLocation && uiLocation.uiSourceCode) {
const contentProvider = /** @type {!Workspace.UISourceCode.UISourceCode} */ uiLocation.uiSourceCode;
result.push({
section: 'clipboard',
title: UI.UIUtils.copyFileNameLabel(),
handler: () => Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(contentProvider.displayName())
});
}
return result;
}
}
/**
* @extends {Common.EventTarget.EventTarget}
* @interface
*/
export class LinkDecorator extends Common.EventTarget.EventTarget {
/**
* @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode
* @return {?UI.Icon.Icon}
*/
linkIcon(uiSourceCode) {
throw new Error('Not implemented yet');
}
}
LinkDecorator.Events = {
LinkIconChanged: Symbol('LinkIconChanged')
};
/**
* @type {LinkContextMenuProvider}
*/
let linkContextMenuProviderInstance;
/**
* @implements {UI.ContextMenu.Provider}
*/
export class LinkContextMenuProvider {
/**
* @param {{forceNew: ?boolean}} opts
*/
static instance(opts = {forceNew: null}) {
const {forceNew} = opts;
if (!linkContextMenuProviderInstance || forceNew) {
linkContextMenuProviderInstance = new LinkContextMenuProvider();
}
return linkContextMenuProviderInstance;
}
/**
* @override
* @param {!Event} event
* @param {!UI.ContextMenu.ContextMenu} contextMenu
* @param {!Object} target
*/
appendApplicableItems(event, contextMenu, target) {
let targetNode = /** @type {?Node} */ (target);
while (targetNode && !infoByAnchor.get(targetNode)) {
targetNode = targetNode.parentNodeOrShadowHost();
}
const link = /** @type {?Element} */ (targetNode);
const linkInfo = Linkifier.linkInfo(link);
if (!linkInfo) {
return;
}
const actions = Linkifier._linkActions(linkInfo);
for (const action of actions) {
contextMenu.section(action.section).appendItem(action.title, action.handler);
}
}
}
/** @type {!LinkHandlerSettingUI} */
let linkHandlerSettingUIInstance;
/**
* @implements {UI.SettingsUI.SettingUI}
*/
export class LinkHandlerSettingUI {
/**
* @private
*/
constructor() {
this._element = document.createElement('select');
this._element.classList.add('chrome-select');
this._element.addEventListener('change', this._onChange.bind(this), false);
this._update();
}
/**
* @param {{forceNew: ?boolean}} opts
*/
static instance(opts = {forceNew: null}) {
const {forceNew} = opts;
if (!linkHandlerSettingUIInstance || forceNew) {
linkHandlerSettingUIInstance = new LinkHandlerSettingUI();
}
return linkHandlerSettingUIInstance;
}
_update() {
this._element.removeChildren();
const names = [...linkHandlers.keys()];
names.unshift(i18nString(UIStrings.auto));
for (const name of names) {
const option = document.createElement('option');
option.textContent = name;
option.selected = name === Linkifier._linkHandlerSetting().get();
this._element.appendChild(option);
}
this._element.disabled = names.length <= 1;
}
/**
* @param {!Event} event
*/
_onChange(event) {
if (!event.target) {
return;
}
const value = (/** @type {!HTMLSelectElement} */ (event.target)).value;
Linkifier._linkHandlerSetting().set(value);
}
/**
* @override
* @return {?Element}
*/
settingElement() {
return UI.SettingsUI.createCustomSetting(i18nString(UIStrings.linkHandling), this._element);
}
}
let listeningToNewEvents = false;
function listenForNewComponentLinkifierEvents() {
if (listeningToNewEvents) {
return;
}
listeningToNewEvents = true;
window.addEventListener(
'linkifier-activated',
/**
*
* @param {!Event} event
*/
function(event) {
const unknownEvent = /** @type {?} */ (event);
const eventWithData = /** @type {!{data: !_LinkInfo}} */ (unknownEvent);
Linkifier._handleClickFromNewComponentLand(eventWithData.data);
});
}
listenForNewComponentLinkifierEvents();
/**
* @type {ContentProviderContextMenuProvider}
*/
let contentProviderContextMenuProviderInstance;
/**
* @implements {UI.ContextMenu.Provider}
*/
export class ContentProviderContextMenuProvider {
/**
* @param {{forceNew: ?boolean}} opts
*/
static instance(opts = {forceNew: null}) {
const {forceNew} = opts;
if (!contentProviderContextMenuProviderInstance || forceNew) {
contentProviderContextMenuProviderInstance = new ContentProviderContextMenuProvider();
}
return contentProviderContextMenuProviderInstance;
}
/**
* @override
* @param {!Event} event
* @param {!UI.ContextMenu.ContextMenu} contextMenu
* @param {!Object} target
*/
appendApplicableItems(event, contextMenu, target) {
const contentProvider = /** @type {!Workspace.UISourceCode.UISourceCode} */ (target);
if (!contentProvider.contentURL()) {
return;
}
contextMenu.revealSection().appendItem(
UI.UIUtils.openLinkExternallyLabel(),
() => Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(contentProvider.contentURL()));
for (const title of linkHandlers.keys()) {
const handler = linkHandlers.get(title);
if (!handler) {
continue;
}
contextMenu.revealSection().appendItem(
i18nString(UIStrings.openUsingS, {PH1: title}), handler.bind(null, contentProvider, 0));
}
if (contentProvider instanceof SDK.NetworkRequest.NetworkRequest) {
return;
}
contextMenu.clipboardSection().appendItem(
UI.UIUtils.copyLinkAddressLabel(),
() => Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(contentProvider.contentURL()));
contextMenu.clipboardSection().appendItem(
UI.UIUtils.copyFileNameLabel(),
() => Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(contentProvider.displayName()));
}
}
/**
* @typedef {{
* icon: ?UI.Icon.Icon,
* enableDecorator: boolean,
* uiLocation: ?Workspace.UISourceCode.UILocation,
* liveLocation: ?Bindings.LiveLocation.LiveLocation,
* url: ?string,
* lineNumber: ?number,
* columnNumber: ?number,
* revealable: ?Object,
* fallback: ?Element
* }}
*/
// @ts-ignore typedef
export let _LinkInfo;
/**
* @typedef {{
* text: (string|undefined),
* className: (string|undefined),
* lineNumber: (number|undefined),
* columnNumber: (number|undefined),
* preventClick: (boolean|undefined),
* maxLength: (number|undefined),
* tabStop: (boolean|undefined),
* bypassURLTrimming: (boolean|undefined)
* }}
*/
// @ts-ignore typedef
export let LinkifyURLOptions;
/**
* @typedef {{
* className: (string|undefined),
* columnNumber: (number|undefined),
* tabStop: (boolean|undefined)
* }}
*/
// @ts-ignore typedef
export let LinkifyOptions;
/**
* @typedef {{
* maxLength: (number|undefined),
* title: (string|undefined),
* href: (string|undefined),
* preventClick: (boolean|undefined),
* tabStop: (boolean|undefined),
* bypassURLTrimming: (boolean|undefined)
* }}
*/
// @ts-ignore typedef
export let _CreateLinkOptions;
/**
* @typedef {function(!TextUtils.ContentProvider.ContentProvider, number):void}
*/
// @ts-ignore typedef
export let LinkHandler;