UNPKG

chrome-devtools-frontend

Version:
418 lines (373 loc) • 16.8 kB
// 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; } }