chrome-devtools-frontend
Version:
Chrome DevTools UI
418 lines (373 loc) • 16.8 kB
text/typescript
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
/* eslint-disable rulesdir/es-modules-import */
import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {frameworkEventListeners, type FrameworkEventListenersObject} from './EventListenersUtils.js';
import eventListenersViewStyles from './eventListenersView.css.js';
const UIStrings = {
/**
*@description Empty holder text content in Event Listeners View of the Event Listener Debugging pane in the Sources panel
*/
noEventListeners: 'No event listeners',
/**
*@description Empty holder text content in Event Listeners View of the Event Listener Debugging pane in the Elements panel
*/
eventListenersExplanation: 'On this page you will find registered event listeners',
/**
*@description Delete button title in Event Listeners View of the Event Listener Debugging pane in the Sources panel
*/
deleteEventListener: 'Delete event listener',
/**
*@description Passive button text content in Event Listeners View of the Event Listener Debugging pane in the Sources panel
*/
togglePassive: 'Toggle Passive',
/**
*@description Passive button title in Event Listeners View of the Event Listener Debugging pane in the Sources panel
*/
toggleWhetherEventListenerIs: 'Toggle whether event listener is passive or blocking',
/**
*@description A context menu item to reveal a node in the DOM tree of the Elements Panel
*/
openInElementsPanel: 'Open in Elements panel',
/**
*@description Text in Event Listeners Widget of the Elements panel
*/
passive: 'Passive',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/event_listeners/EventListenersView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class EventListenersView extends UI.Widget.VBox {
private changeCallback: () => void;
private enableDefaultTreeFocus: boolean;
treeOutline: UI.TreeOutline.TreeOutlineInShadow;
private emptyHolder: HTMLDivElement;
private linkifier: Components.Linkifier.Linkifier;
private readonly treeItemMap: Map<string, EventListenersTreeElement>;
constructor(changeCallback: () => void, enableDefaultTreeFocus: boolean|undefined = false) {
super();
this.registerRequiredCSS(eventListenersViewStyles);
this.changeCallback = changeCallback;
this.enableDefaultTreeFocus = enableDefaultTreeFocus;
this.emptyHolder = this.element.createChild('div', 'placeholder hidden');
this.emptyHolder.createChild('span', 'gray-info-message').textContent = i18nString(UIStrings.noEventListeners);
const emptyWidget = new UI.EmptyWidget.EmptyWidget(
i18nString(UIStrings.noEventListeners), i18nString(UIStrings.eventListenersExplanation));
emptyWidget.show(this.emptyHolder);
this.treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
this.treeOutline.setComparator(EventListenersTreeElement.comparator);
this.treeOutline.element.classList.add('event-listener-tree', 'monospace');
this.treeOutline.setShowSelectionOnKeyboardFocus(true);
this.treeOutline.setFocusable(true);
this.treeOutline.registerRequiredCSS(eventListenersViewStyles, objectValueStyles);
this.element.appendChild(this.treeOutline.element);
this.linkifier = new Components.Linkifier.Linkifier();
this.treeItemMap = new Map();
}
override focus(): void {
if (!this.enableDefaultTreeFocus) {
return;
}
if (!this.emptyHolder.classList.contains('hidden')) {
this.treeOutline.forceSelect();
} else {
this.emptyHolder.focus();
}
}
async addObjects(objects: Array<SDK.RemoteObject.RemoteObject|null>): Promise<void> {
this.reset();
await Promise.all(objects.map(obj => obj ? this.addObject(obj) : Promise.resolve()));
this.addEmptyHolderIfNeeded();
this.eventListenersArrivedForTest();
}
private addObject(object: SDK.RemoteObject.RemoteObject): Promise<void> {
let eventListeners: SDK.DOMDebuggerModel.EventListener[];
let frameworkEventListenersObject: (FrameworkEventListenersObject|null)|null = null;
const promises = [];
const domDebuggerModel = object.runtimeModel().target().model(SDK.DOMDebuggerModel.DOMDebuggerModel);
// TODO(kozyatinskiy): figure out how this should work for |window| when there is no DOMDebugger.
if (domDebuggerModel) {
promises.push(domDebuggerModel.eventListeners(object).then(storeEventListeners));
}
promises.push(frameworkEventListeners(object).then(storeFrameworkEventListenersObject));
return Promise.all(promises).then(markInternalEventListeners).then(addEventListeners.bind(this));
function storeEventListeners(result: SDK.DOMDebuggerModel.EventListener[]): void {
eventListeners = result;
}
function storeFrameworkEventListenersObject(result: FrameworkEventListenersObject|null): void {
frameworkEventListenersObject = result;
}
async function markInternalEventListeners(): Promise<void> {
if (!frameworkEventListenersObject) {
return;
}
if (!frameworkEventListenersObject.internalHandlers) {
return;
}
return await frameworkEventListenersObject.internalHandlers.object()
.callFunctionJSON(isInternalEventListener as (this: Object) => boolean[], eventListeners.map(handlerArgument))
.then(setIsInternal);
function handlerArgument(listener: SDK.DOMDebuggerModel.EventListener): Protocol.Runtime.CallArgument {
return SDK.RemoteObject.RemoteObject.toCallArgument(listener.handler());
}
function isInternalEventListener(this: Array<Platform.Constructor.Constructor<unknown>>): boolean[] {
const isInternal = [];
const internalHandlersSet = new Set<Platform.Constructor.Constructor<unknown>>(this);
for (const handler of arguments) {
isInternal.push(internalHandlersSet.has(handler));
}
return isInternal;
}
function setIsInternal(isInternal: boolean[]|null): void {
if (!isInternal) {
return;
}
for (let i = 0; i < eventListeners.length; ++i) {
if (isInternal[i]) {
eventListeners[i].markAsFramework();
}
}
}
}
function addEventListeners(this: EventListenersView): void {
this.addObjectEventListeners(object, eventListeners);
if (frameworkEventListenersObject) {
this.addObjectEventListeners(object, frameworkEventListenersObject.eventListeners);
}
}
}
private addObjectEventListeners(
object: SDK.RemoteObject.RemoteObject, eventListeners: SDK.DOMDebuggerModel.EventListener[]|null): void {
if (!eventListeners) {
return;
}
for (const eventListener of eventListeners) {
const treeItem = this.getOrCreateTreeElementForType(eventListener.type());
treeItem.addObjectEventListener(eventListener, object);
}
}
showFrameworkListeners(showFramework: boolean, showPassive: boolean, showBlocking: boolean): void {
const eventTypes = this.treeOutline.rootElement().children();
for (const eventType of eventTypes) {
let hiddenEventType = true;
for (const listenerElement of eventType.children()) {
const objectListenerElement = listenerElement as ObjectEventListenerBar;
const listenerOrigin = objectListenerElement.eventListener().origin();
let hidden = false;
if (listenerOrigin === SDK.DOMDebuggerModel.EventListener.Origin.FRAMEWORK_USER && !showFramework) {
hidden = true;
}
if (listenerOrigin === SDK.DOMDebuggerModel.EventListener.Origin.FRAMEWORK && showFramework) {
hidden = true;
}
if (!showPassive && objectListenerElement.eventListener().passive()) {
hidden = true;
}
if (!showBlocking && !objectListenerElement.eventListener().passive()) {
hidden = true;
}
objectListenerElement.hidden = hidden;
hiddenEventType = hiddenEventType && hidden;
}
eventType.hidden = hiddenEventType;
}
}
private getOrCreateTreeElementForType(type: string): EventListenersTreeElement {
let treeItem = this.treeItemMap.get(type);
if (!treeItem) {
treeItem = new EventListenersTreeElement(type, this.linkifier, this.changeCallback);
this.treeItemMap.set(type, treeItem);
treeItem.hidden = true;
this.treeOutline.appendChild(treeItem);
}
this.emptyHolder.classList.add('hidden');
return treeItem;
}
addEmptyHolderIfNeeded(): void {
let allHidden = true;
let firstVisibleChild: UI.TreeOutline.TreeElement|null = null;
for (const eventType of this.treeOutline.rootElement().children()) {
eventType.hidden = !eventType.firstChild();
allHidden = allHidden && eventType.hidden;
if (!firstVisibleChild && !eventType.hidden) {
firstVisibleChild = eventType;
}
}
if (allHidden && this.emptyHolder.classList.contains('hidden')) {
this.emptyHolder.classList.remove('hidden');
}
if (firstVisibleChild) {
firstVisibleChild.select(true /* omitFocus */);
}
this.treeOutline.setFocusable(Boolean(firstVisibleChild));
}
reset(): void {
const eventTypes = this.treeOutline.rootElement().children();
for (const eventType of eventTypes) {
eventType.removeChildren();
}
this.linkifier.reset();
}
private eventListenersArrivedForTest(): void {
}
}
export class EventListenersTreeElement extends UI.TreeOutline.TreeElement {
override toggleOnClick: boolean;
private readonly linkifier: Components.Linkifier.Linkifier;
private readonly changeCallback: () => void;
constructor(type: string, linkifier: Components.Linkifier.Linkifier, changeCallback: () => void) {
super(type);
this.toggleOnClick = true;
this.linkifier = linkifier;
this.changeCallback = changeCallback;
UI.ARIAUtils.setLabel(this.listItemElement, `${type}, event listener`);
}
static comparator(element1: UI.TreeOutline.TreeElement, element2: UI.TreeOutline.TreeElement): number {
if (element1.title === element2.title) {
return 0;
}
return element1.title > element2.title ? 1 : -1;
}
addObjectEventListener(eventListener: SDK.DOMDebuggerModel.EventListener, object: SDK.RemoteObject.RemoteObject):
void {
const treeElement = new ObjectEventListenerBar(eventListener, object, this.linkifier, this.changeCallback);
this.appendChild(treeElement as UI.TreeOutline.TreeElement);
}
}
export class ObjectEventListenerBar extends UI.TreeOutline.TreeElement {
private eventListenerInternal: SDK.DOMDebuggerModel.EventListener;
editable: boolean;
private readonly changeCallback: () => void;
private valueTitle?: Element;
constructor(
eventListener: SDK.DOMDebuggerModel.EventListener, object: SDK.RemoteObject.RemoteObject,
linkifier: Components.Linkifier.Linkifier, changeCallback: () => void) {
super('', true);
this.eventListenerInternal = eventListener;
this.editable = false;
this.setTitle(object, linkifier);
this.changeCallback = changeCallback;
}
override async onpopulate(): Promise<void> {
const properties = [];
const eventListener = this.eventListenerInternal;
const runtimeModel = eventListener.domDebuggerModel().runtimeModel();
properties.push(runtimeModel.createRemotePropertyFromPrimitiveValue('useCapture', eventListener.useCapture()));
properties.push(runtimeModel.createRemotePropertyFromPrimitiveValue('passive', eventListener.passive()));
properties.push(runtimeModel.createRemotePropertyFromPrimitiveValue('once', eventListener.once()));
if (typeof eventListener.handler() !== 'undefined') {
properties.push(new SDK.RemoteObject.RemoteObjectProperty('handler', eventListener.handler()));
}
ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement.populateWithProperties(
this, properties, [], true, true, null);
}
private setTitle(object: SDK.RemoteObject.RemoteObject, linkifier: Components.Linkifier.Linkifier): void {
const title = this.listItemElement.createChild('span', 'event-listener-details');
const propertyValue = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.createPropertyValue(
object, /* wasThrown */ false, /* showPreview */ false);
this.valueTitle = propertyValue.element;
title.appendChild(this.valueTitle);
if (this.eventListenerInternal.canRemove()) {
const deleteButton = new Buttons.Button.Button();
deleteButton.data = {
variant: Buttons.Button.Variant.ICON,
size: Buttons.Button.Size.MICRO,
iconName: 'bin',
jslogContext: 'delete-event-listener',
};
UI.Tooltip.Tooltip.install(deleteButton, i18nString(UIStrings.deleteEventListener));
deleteButton.addEventListener('click', event => {
this.removeListener();
event.consume();
}, false);
title.appendChild(deleteButton);
}
if (this.eventListenerInternal.isScrollBlockingType() && this.eventListenerInternal.canTogglePassive()) {
const passiveButton = title.createChild('button', 'event-listener-button');
passiveButton.textContent = i18nString(UIStrings.togglePassive);
passiveButton.setAttribute('jslog', `${VisualLogging.action('passive').track({click: true})}`);
UI.Tooltip.Tooltip.install(passiveButton, i18nString(UIStrings.toggleWhetherEventListenerIs));
passiveButton.addEventListener('click', event => {
this.togglePassiveListener();
event.consume();
}, false);
title.appendChild(passiveButton);
}
const subtitle = title.createChild('span', 'event-listener-tree-subtitle');
const linkElement =
linkifier.linkifyRawLocation(this.eventListenerInternal.location(), this.eventListenerInternal.sourceURL());
subtitle.appendChild(linkElement);
this.listItemElement.addEventListener('contextmenu', event => {
const menu = new UI.ContextMenu.ContextMenu(event);
if (event.target !== linkElement) {
menu.appendApplicableItems(linkElement);
}
if (object.subtype === 'node') {
menu.defaultSection().appendItem(
i18nString(UIStrings.openInElementsPanel), () => Common.Revealer.reveal(object),
{jslogContext: 'reveal-in-elements'});
}
menu.defaultSection().appendItem(
i18nString(UIStrings.deleteEventListener), this.removeListener.bind(this),
{disabled: !this.eventListenerInternal.canRemove(), jslogContext: 'delete-event-listener'});
menu.defaultSection().appendCheckboxItem(i18nString(UIStrings.passive), this.togglePassiveListener.bind(this), {
checked: this.eventListenerInternal.passive(),
disabled: !this.eventListenerInternal.canTogglePassive(),
jslogContext: 'passive',
});
void menu.show();
});
}
private removeListener(): void {
this.removeListenerBar();
void this.eventListenerInternal.remove();
}
private togglePassiveListener(): void {
void this.eventListenerInternal.togglePassive().then(() => this.changeCallback());
}
private removeListenerBar(): void {
const parent = this.parent;
if (!parent) {
return;
}
parent.removeChild(this);
if (!parent.childCount()) {
parent.collapse();
}
let allHidden = true;
for (const child of parent.children()) {
if (!child.hidden) {
allHidden = false;
}
}
parent.hidden = allHidden;
}
eventListener(): SDK.DOMDebuggerModel.EventListener {
return this.eventListenerInternal;
}
override onenter(): boolean {
if (this.valueTitle) {
(this.valueTitle as HTMLElement).click();
return true;
}
return false;
}
override ondelete(): boolean {
if (this.eventListenerInternal.canRemove()) {
this.removeListener();
return true;
}
return false;
}
}