blob: 3a2355d3f05a044f7c184b2ad9786f30fd91f16d [file] [log] [blame]
// 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.
/**
* @unrestricted
*/
export class EventListenersView extends UI.VBox {
/**
* @param {function()} changeCallback
*/
constructor(changeCallback) {
super();
this._changeCallback = changeCallback;
this._treeOutline = new UI.TreeOutlineInShadow();
this._treeOutline.hideOverflow();
this._treeOutline.registerRequiredCSS('object_ui/objectValue.css');
this._treeOutline.registerRequiredCSS('event_listeners/eventListenersView.css');
this._treeOutline.setComparator(EventListenersTreeElement.comparator);
this._treeOutline.element.classList.add('monospace');
this._treeOutline.setShowSelectionOnKeyboardFocus(true);
this._treeOutline.setFocusable(true);
this.element.appendChild(this._treeOutline.element);
this._emptyHolder = createElementWithClass('div', 'gray-info-message');
this._emptyHolder.textContent = Common.UIString('No event listeners');
this._emptyHolder.tabIndex = -1;
this._linkifier = new Components.Linkifier();
/** @type {!Map<string, !EventListenersTreeElement>} */
this._treeItemMap = new Map();
}
/**
* @override
*/
focus() {
if (!this._emptyHolder.parentNode) {
this._treeOutline.forceSelect();
} else {
this._emptyHolder.focus();
}
}
/**
* @param {!Array<?SDK.RemoteObject>} objects
* @return {!Promise<undefined>}
*/
async addObjects(objects) {
this.reset();
await Promise.all(objects.map(obj => obj ? this._addObject(obj) : Promise.resolve()));
this.addEmptyHolderIfNeeded();
this._eventListenersArrivedForTest();
}
/**
* @param {!SDK.RemoteObject} object
* @return {!Promise<undefined>}
*/
_addObject(object) {
/** @type {!Array<!SDK.EventListener>} */
let eventListeners;
/** @type {?EventListeners.FrameworkEventListenersObject}*/
let frameworkEventListenersObject = null;
const promises = [];
const domDebuggerModel = object.runtimeModel().target().model(SDK.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(EventListeners.frameworkEventListeners(object).then(storeFrameworkEventListenersObject));
return Promise.all(promises).then(markInternalEventListeners).then(addEventListeners.bind(this));
/**
* @param {!Array<!SDK.EventListener>} result
*/
function storeEventListeners(result) {
eventListeners = result;
}
/**
* @param {?EventListeners.FrameworkEventListenersObject} result
*/
function storeFrameworkEventListenersObject(result) {
frameworkEventListenersObject = result;
}
/**
* @return {!Promise<undefined>}
*/
function markInternalEventListeners() {
if (!frameworkEventListenersObject.internalHandlers) {
return Promise.resolve(undefined);
}
return frameworkEventListenersObject.internalHandlers.object()
.callFunctionJSON(isInternalEventListener, eventListeners.map(handlerArgument))
.then(setIsInternal);
/**
* @param {!SDK.EventListener} listener
* @return {!Protocol.Runtime.CallArgument}
*/
function handlerArgument(listener) {
return SDK.RemoteObject.toCallArgument(listener.handler());
}
/**
* @suppressReceiverCheck
* @return {!Array<boolean>}
* @this {Array<*>}
*/
function isInternalEventListener() {
const isInternal = [];
const internalHandlersSet = new Set(this);
for (const handler of arguments) {
isInternal.push(internalHandlersSet.has(handler));
}
return isInternal;
}
/**
* @param {!Array<boolean>} isInternal
*/
function setIsInternal(isInternal) {
for (let i = 0; i < eventListeners.length; ++i) {
if (isInternal[i]) {
eventListeners[i].markAsFramework();
}
}
}
}
/**
* @this {EventListenersView}
*/
function addEventListeners() {
this._addObjectEventListeners(object, eventListeners);
this._addObjectEventListeners(object, frameworkEventListenersObject.eventListeners);
}
}
/**
* @param {!SDK.RemoteObject} object
* @param {?Array<!SDK.EventListener>} eventListeners
*/
_addObjectEventListeners(object, eventListeners) {
if (!eventListeners) {
return;
}
for (const eventListener of eventListeners) {
const treeItem = this._getOrCreateTreeElementForType(eventListener.type());
treeItem.addObjectEventListener(eventListener, object);
}
}
/**
* @param {boolean} showFramework
* @param {boolean} showPassive
* @param {boolean} showBlocking
*/
showFrameworkListeners(showFramework, showPassive, showBlocking) {
const eventTypes = this._treeOutline.rootElement().children();
for (const eventType of eventTypes) {
let hiddenEventType = true;
for (const listenerElement of eventType.children()) {
const listenerOrigin = listenerElement.eventListener().origin();
let hidden = false;
if (listenerOrigin === SDK.EventListener.Origin.FrameworkUser && !showFramework) {
hidden = true;
}
if (listenerOrigin === SDK.EventListener.Origin.Framework && showFramework) {
hidden = true;
}
if (!showPassive && listenerElement.eventListener().passive()) {
hidden = true;
}
if (!showBlocking && !listenerElement.eventListener().passive()) {
hidden = true;
}
listenerElement.hidden = hidden;
hiddenEventType = hiddenEventType && hidden;
}
eventType.hidden = hiddenEventType;
}
}
/**
* @param {string} type
* @return {!EventListenersTreeElement}
*/
_getOrCreateTreeElementForType(type) {
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.remove();
return treeItem;
}
addEmptyHolderIfNeeded() {
let allHidden = true;
let firstVisibleChild = 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.parentNode) {
this.element.appendChild(this._emptyHolder);
}
if (firstVisibleChild) {
firstVisibleChild.select(true /* omitFocus */);
}
}
reset() {
const eventTypes = this._treeOutline.rootElement().children();
for (const eventType of eventTypes) {
eventType.removeChildren();
}
this._linkifier.reset();
}
_eventListenersArrivedForTest() {
}
}
/**
* @unrestricted
*/
export class EventListenersTreeElement extends UI.TreeElement {
/**
* @param {string} type
* @param {!Components.Linkifier} linkifier
* @param {function()} changeCallback
*/
constructor(type, linkifier, changeCallback) {
super(type);
this.toggleOnClick = true;
this._linkifier = linkifier;
this._changeCallback = changeCallback;
}
/**
* @param {!UI.TreeElement} element1
* @param {!UI.TreeElement} element2
* @return {number}
*/
static comparator(element1, element2) {
if (element1.title === element2.title) {
return 0;
}
return element1.title > element2.title ? 1 : -1;
}
/**
* @param {!SDK.EventListener} eventListener
* @param {!SDK.RemoteObject} object
*/
addObjectEventListener(eventListener, object) {
const treeElement = new ObjectEventListenerBar(eventListener, object, this._linkifier, this._changeCallback);
this.appendChild(/** @type {!UI.TreeElement} */ (treeElement));
}
}
/**
* @unrestricted
*/
export class ObjectEventListenerBar extends UI.TreeElement {
/**
* @param {!SDK.EventListener} eventListener
* @param {!SDK.RemoteObject} object
* @param {!Components.Linkifier} linkifier
* @param {function()} changeCallback
*/
constructor(eventListener, object, linkifier, changeCallback) {
super('', true);
this._eventListener = eventListener;
this.editable = false;
this._setTitle(object, linkifier);
this._changeCallback = changeCallback;
}
/**
* @override
* @returns {!Promise}
*/
async onpopulate() {
const properties = [];
const eventListener = this._eventListener;
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.RemoteObjectProperty('handler', eventListener.handler()));
}
ObjectUI.ObjectPropertyTreeElement.populateWithProperties(this, properties, [], true, null);
}
/**
* @param {!SDK.RemoteObject} object
* @param {!Components.Linkifier} linkifier
*/
_setTitle(object, linkifier) {
const title = this.listItemElement.createChild('span', 'event-listener-details');
const subtitle = this.listItemElement.createChild('span', 'event-listener-tree-subtitle');
const linkElement = linkifier.linkifyRawLocation(this._eventListener.location(), this._eventListener.sourceURL());
subtitle.appendChild(linkElement);
this._valueTitle =
ObjectUI.ObjectPropertiesSection.createValueElement(object, false /* wasThrown */, false /* showPreview */);
title.appendChild(this._valueTitle);
if (this._eventListener.canRemove()) {
const deleteButton = title.createChild('span', 'event-listener-button');
deleteButton.textContent = Common.UIString('Remove');
deleteButton.title = Common.UIString('Delete event listener');
deleteButton.addEventListener('click', event => {
this._removeListener();
event.consume();
}, false);
title.appendChild(deleteButton);
}
if (this._eventListener.isScrollBlockingType() && this._eventListener.canTogglePassive()) {
const passiveButton = title.createChild('span', 'event-listener-button');
passiveButton.textContent = Common.UIString('Toggle Passive');
passiveButton.title = Common.UIString('Toggle whether event listener is passive or blocking');
passiveButton.addEventListener('click', event => {
this._togglePassiveListener();
event.consume();
}, false);
title.appendChild(passiveButton);
}
this.listItemElement.addEventListener('contextmenu', event => {
const menu = new UI.ContextMenu(event);
if (event.target !== linkElement) {
menu.appendApplicableItems(linkElement);
}
menu.defaultSection().appendItem(
ls`Delete event listener`, this._removeListener.bind(this), !this._eventListener.canRemove());
menu.defaultSection().appendCheckboxItem(
ls`Passive`, this._togglePassiveListener.bind(this), this._eventListener.passive(),
!this._eventListener.canTogglePassive());
menu.show();
});
}
_removeListener() {
this._removeListenerBar();
this._eventListener.remove();
}
_togglePassiveListener() {
this._eventListener.togglePassive().then(this._changeCallback());
}
_removeListenerBar() {
const parent = this.parent;
parent.removeChild(this);
if (!parent.childCount()) {
parent.collapse();
}
let allHidden = true;
for (let i = 0; i < parent.childCount(); ++i) {
if (!parent.childAt(i).hidden) {
allHidden = false;
}
}
parent.hidden = allHidden;
}
/**
* @return {!SDK.EventListener}
*/
eventListener() {
return this._eventListener;
}
/**
* @override
*/
onenter() {
if (this._valueTitle) {
this._valueTitle.click();
return true;
}
return false;
}
}
/* Legacy exported object */
self.EventListeners = self.EventListeners || {};
/* Legacy exported object */
EventListeners = EventListeners || {};
/** @constructor */
EventListeners.EventListenersView = EventListenersView;
/** @constructor */
EventListeners.EventListenersTreeElement = EventListenersTreeElement;
/** @constructor */
EventListeners.ObjectEventListenerBar = ObjectEventListenerBar;
/**
* @typedef {Array<{object: !SDK.RemoteObject, eventListeners: ?Array<!SDK.EventListener>, frameworkEventListeners: ?{eventListeners: ?Array<!SDK.EventListener>, internalHandlers: ?SDK.RemoteArray}, isInternal: ?Array<boolean>}>}
*/
EventListeners.EventListenersResult;