| // 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; |