From 377386a4b34f016bb39e5fe69633610eda4b3b05 Mon Sep 17 00:00:00 2001 From: surunzi Date: Mon, 4 May 2020 23:16:18 +0800 Subject: [PATCH] fix: event listener view --- front_end/chii_app.js | 2 +- front_end/chii_app.json | 2 +- .../chii_elements/EventListenersWidget.js | 2 +- front_end/chii_elements/module.json | 2 +- .../EventListenersUtils.js | 538 ++++++++++++++++++ .../EventListenersView.js | 431 ++++++++++++++ .../chii_event_listeners-legacy.js | 19 + .../chii_event_listeners.js | 8 + .../eventListenersView.css | 91 +++ front_end/chii_event_listeners/module.json | 11 + target/domains/DOMDebugger.ts | 38 +- target/lib/connector.ts | 3 + 12 files changed, 1131 insertions(+), 16 deletions(-) create mode 100644 front_end/chii_event_listeners/EventListenersUtils.js create mode 100644 front_end/chii_event_listeners/EventListenersView.js create mode 100644 front_end/chii_event_listeners/chii_event_listeners-legacy.js create mode 100644 front_end/chii_event_listeners/chii_event_listeners.js create mode 100644 front_end/chii_event_listeners/eventListenersView.css create mode 100644 front_end/chii_event_listeners/module.json diff --git a/front_end/chii_app.js b/front_end/chii_app.js index 1ae04f2a..bb115d73 100644 --- a/front_end/chii_app.js +++ b/front_end/chii_app.js @@ -870,7 +870,7 @@ Root.allDescriptors.push( 'quick_open', 'inline_editor', 'color_picker', - 'event_listeners', + 'chii_event_listeners', 'object_ui', 'formatter', 'coverage', diff --git a/front_end/chii_app.json b/front_end/chii_app.json index 460d9d28..430e034e 100644 --- a/front_end/chii_app.json +++ b/front_end/chii_app.json @@ -16,7 +16,7 @@ { "name": "coverage" }, { "name": "data_grid" }, { "name": "diff" }, - { "name": "event_listeners" }, + { "name": "chii_event_listeners" }, { "name": "formatter" }, { "name": "heap_snapshot_model" }, { "name": "inline_editor" }, diff --git a/front_end/chii_elements/EventListenersWidget.js b/front_end/chii_elements/EventListenersWidget.js index 8b110155..1682e9d0 100644 --- a/front_end/chii_elements/EventListenersWidget.js +++ b/front_end/chii_elements/EventListenersWidget.js @@ -28,7 +28,7 @@ */ import * as Common from '../common/common.js'; -import * as EventListeners from '../event_listeners/event_listeners.js'; +import * as EventListeners from '../chii_event_listeners/chii_event_listeners.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; diff --git a/front_end/chii_elements/module.json b/front_end/chii_elements/module.json index 6e56d82b..8f29c8b8 100644 --- a/front_end/chii_elements/module.json +++ b/front_end/chii_elements/module.json @@ -254,7 +254,7 @@ "className": "Elements.NodeStackTraceWidget" } ], - "dependencies": ["components", "extensions", "inline_editor", "color_picker", "event_listeners"], + "dependencies": ["components", "extensions", "inline_editor", "color_picker", "chii_event_listeners"], "scripts": [], "modules": [ "chii_elements.js", diff --git a/front_end/chii_event_listeners/EventListenersUtils.js b/front_end/chii_event_listeners/EventListenersUtils.js new file mode 100644 index 00000000..1460ca8e --- /dev/null +++ b/front_end/chii_event_listeners/EventListenersUtils.js @@ -0,0 +1,538 @@ +// 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. + +import * as SDK from '../sdk/sdk.js'; +import * as Common from '../common/common.js'; + +/** + * @param {!SDK.RemoteObject.RemoteObject} object + * @return {!Promise} + */ +export function frameworkEventListeners(object) { + const domDebuggerModel = object.runtimeModel().target().model(SDK.DOMDebuggerModel.DOMDebuggerModel); + if (!domDebuggerModel) { + // TODO(kozyatinskiy): figure out how this should work for |window|. + return Promise.resolve( + /** @type {!FrameworkEventListenersObject} */ ({ eventListeners: [], internalHandlers: null }) + ); + } + + const listenersResult = /** @type {!FrameworkEventListenersObject} */ ({ eventListeners: [] }); + return object + .callFunction(frameworkEventListenersImpl, undefined) + .then(assertCallFunctionResult) + .then(getOwnProperties) + .then(createEventListeners) + .then(returnResult) + .catch(error => { + console.error(error); + return listenersResult; + }); + + /** + * @param {!SDK.RemoteObject.RemoteObject} object + * @return {!Promise} + */ + function getOwnProperties(object) { + return object.getOwnProperties(false /* generatePreview */); + } + + /** + * @param {!SDK.RemoteObject.GetPropertiesResult} result + * @return {!Promise} + */ + function createEventListeners(result) { + if (!result.properties) { + throw new Error('Object properties is empty'); + } + const promises = []; + for (const property of result.properties) { + if (property.name === 'eventListeners' && property.value) { + promises.push(convertToEventListeners(property.value).then(storeEventListeners)); + } + if (property.name === 'internalHandlers' && property.value) { + promises.push(convertToInternalHandlers(property.value).then(storeInternalHandlers)); + } + if (property.name === 'errorString' && property.value) { + printErrorString(property.value); + } + } + return /** @type {!Promise} */ (Promise.all(promises)); + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} pageEventListenersObject + * @return {!Promise>} + */ + function convertToEventListeners(pageEventListenersObject) { + return SDK.RemoteObject.RemoteArray.objectAsArray(pageEventListenersObject) + .map(toEventListener) + .then(filterOutEmptyObjects); + + /** + * @param {!SDK.RemoteObject.RemoteObject} listenerObject + * @return {!Promise} + */ + function toEventListener(listenerObject) { + /** @type {string} */ + let type; + /** @type {boolean} */ + let useCapture; + /** @type {boolean} */ + let passive; + /** @type {boolean} */ + let once; + /** @type {?SDK.RemoteObject.RemoteObject} */ + let handler = null; + /** @type {?SDK.RemoteObject.RemoteObject} */ + let originalHandler = null; + /** @type {?SDK.DebuggerModel.Location} */ + let location = null; + /** @type {?SDK.RemoteObject.RemoteObject} */ + let removeFunctionObject = null; + + const promises = []; + promises.push(listenerObject.callFunctionJSON(truncatePageEventListener, undefined).then(storeTruncatedListener)); + + /** + * @suppressReceiverCheck + * @this {EventListenerObjectInInspectedPage} + * @return {!{type:string, useCapture:boolean, passive:boolean, once:boolean}} + */ + function truncatePageEventListener() { + return { type: this.type, useCapture: this.useCapture, passive: this.passive, once: this.once }; + } + + /** + * @param {!{type:string, useCapture: boolean, passive: boolean, once: boolean}} truncatedListener + */ + function storeTruncatedListener(truncatedListener) { + type = truncatedListener.type; + useCapture = truncatedListener.useCapture; + passive = truncatedListener.passive; + once = truncatedListener.once; + } + + promises.push( + listenerObject + .callFunction(handlerFunction) + .then(assertCallFunctionResult) + .then(storeOriginalHandler) + .then(toTargetFunction) + .then(storeFunctionWithDetails) + ); + + /** + * @suppressReceiverCheck + * @return {function()} + * @this {EventListenerObjectInInspectedPage} + */ + function handlerFunction() { + return this.handler; + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} functionObject + * @return {!SDK.RemoteObject.RemoteObject} + */ + function storeOriginalHandler(functionObject) { + originalHandler = functionObject; + return originalHandler; + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} functionObject + * @return {!Promise} + */ + function storeFunctionWithDetails(functionObject) { + handler = functionObject; + return /** @type {!Promise} */ (functionObject + .debuggerModel() + .functionDetailsPromise(functionObject) + .then(storeFunctionDetails)); + } + + /** + * @param {?SDK.DebuggerModel.FunctionDetails} functionDetails + */ + function storeFunctionDetails(functionDetails) { + location = functionDetails ? functionDetails.location : null; + } + + promises.push( + listenerObject.callFunction(getRemoveFunction).then(assertCallFunctionResult).then(storeRemoveFunction) + ); + + /** + * @suppressReceiverCheck + * @return {function()} + * @this {EventListenerObjectInInspectedPage} + */ + function getRemoveFunction() { + return this.remove; + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} functionObject + */ + function storeRemoveFunction(functionObject) { + if (functionObject.type !== 'function') { + return; + } + removeFunctionObject = functionObject; + } + + return Promise.all(promises) + .then(createEventListener) + .catch(error => { + console.error(error); + return null; + }); + + /** + * @return {!SDK.DOMDebuggerModel.EventListener} + */ + function createEventListener() { + if (!location) { + throw new Error("Empty event listener's location"); + } + return new SDK.DOMDebuggerModel.EventListener( + /** @type {!SDK.DOMDebuggerModel.DOMDebuggerModel} */ (domDebuggerModel), + object, + type, + useCapture, + passive, + once, + handler, + originalHandler, + location, + removeFunctionObject, + SDK.DOMDebuggerModel.EventListener.Origin.FrameworkUser + ); + } + } + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} pageInternalHandlersObject + * @return {!Promise} + */ + function convertToInternalHandlers(pageInternalHandlersObject) { + return SDK.RemoteObject.RemoteArray.objectAsArray(pageInternalHandlersObject) + .map(toTargetFunction) + .then(SDK.RemoteObject.RemoteArray.createFromRemoteObjects.bind(null)); + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} functionObject + * @return {!Promise} + */ + function toTargetFunction(functionObject) { + return SDK.RemoteObject.RemoteFunction.objectAsFunction(functionObject).targetFunction(); + } + + /** + * @param {!Array} eventListeners + */ + function storeEventListeners(eventListeners) { + listenersResult.eventListeners = eventListeners; + } + + /** + * @param {!SDK.RemoteObject.RemoteArray} internalHandlers + */ + function storeInternalHandlers(internalHandlers) { + listenersResult.internalHandlers = internalHandlers; + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} errorString + */ + function printErrorString(errorString) { + Common.Console.Console.instance().error(String(errorString.value)); + } + + /** + * @return {!FrameworkEventListenersObject} + */ + function returnResult() { + return listenersResult; + } + + /** + * @param {!SDK.RemoteObject.CallFunctionResult} result + * @return {!SDK.RemoteObject.RemoteObject} + */ + function assertCallFunctionResult(result) { + if (result.wasThrown || !result.object) { + throw new Error('Exception in callFunction or empty result'); + } + return result.object; + } + + /** + * @param {!Array} objects + * @return {!Array} + * @template T + */ + function filterOutEmptyObjects(objects) { + return objects.filter(filterOutEmpty); + + /** + * @param {?T} object + * @return {boolean} + * @template T + */ + function filterOutEmpty(object) { + return !!object; + } + } + + /* + frameworkEventListeners fetcher functions should produce following output: + { + // framework event listeners + "eventListeners": [ + { + "handler": function(), + "useCapture": true, + "passive": false, + "once": false, + "type": "change", + "remove": function(type, handler, useCapture, passive) + }, + ... + ], + // internal framework event handlers + "internalHandlers": [ + function(), + function(), + ... + ] + } + */ + /** + * @suppressReceiverCheck + * @return {!{eventListeners:!Array, internalHandlers:?Array}} + * @this {Object} + */ + function frameworkEventListenersImpl() { + const errorLines = []; + let eventListeners = []; + let internalHandlers = []; + let fetchers = [jQueryFetcher]; + try { + if (self.devtoolsFrameworkEventListeners && isArrayLike(self.devtoolsFrameworkEventListeners)) { + fetchers = fetchers.concat(self.devtoolsFrameworkEventListeners); + } + } catch (e) { + errorLines.push('devtoolsFrameworkEventListeners call produced error: ' + toString(e)); + } + + for (let i = 0; i < fetchers.length; ++i) { + try { + const fetcherResult = fetchers[i](this); + if (fetcherResult.eventListeners && isArrayLike(fetcherResult.eventListeners)) { + eventListeners = eventListeners.concat( + fetcherResult.eventListeners.map(checkEventListener).filter(nonEmptyObject) + ); + } + if (fetcherResult.internalHandlers && isArrayLike(fetcherResult.internalHandlers)) { + internalHandlers = internalHandlers.concat( + fetcherResult.internalHandlers.map(checkInternalHandler).filter(nonEmptyObject) + ); + } + } catch (e) { + errorLines.push('fetcher call produced error: ' + toString(e)); + } + } + const result = { eventListeners: eventListeners }; + if (internalHandlers.length) { + result.internalHandlers = internalHandlers; + } + if (errorLines.length) { + let errorString = 'Framework Event Listeners API Errors:\n\t' + errorLines.join('\n\t'); + errorString = errorString.substr(0, errorString.length - 1); + result.errorString = errorString; + } + return result; + + /** + * @param {?Object} obj + * @return {boolean} + */ + function isArrayLike(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + try { + if (typeof obj.splice === 'function') { + const len = obj.length; + return typeof len === 'number' && len >>> 0 === len && (len > 0 || 1 / len > 0); + } + } catch (e) {} + return false; + } + + /** + * @param {*} eventListener + * @return {?EventListenerObjectInInspectedPage} + */ + function checkEventListener(eventListener) { + try { + let errorString = ''; + if (!eventListener) { + errorString += 'empty event listener, '; + } + const type = eventListener.type; + if (!type || typeof type !== 'string') { + errorString += "event listener's type isn't string or empty, "; + } + const useCapture = eventListener.useCapture; + if (typeof useCapture !== 'boolean') { + errorString += "event listener's useCapture isn't boolean or undefined, "; + } + const passive = eventListener.passive; + if (typeof passive !== 'boolean') { + errorString += "event listener's passive isn't boolean or undefined, "; + } + const once = eventListener.once; + if (typeof once !== 'boolean') { + errorString += "event listener's once isn't boolean or undefined, "; + } + const handler = eventListener.handler; + if (!handler || typeof handler !== 'function') { + errorString += "event listener's handler isn't a function or empty, "; + } + const remove = eventListener.remove; + if (remove && typeof remove !== 'function') { + errorString += "event listener's remove isn't a function, "; + } + if (!errorString) { + return { type: type, useCapture: useCapture, passive: passive, once: once, handler: handler, remove: remove }; + } + errorLines.push(errorString.substr(0, errorString.length - 2)); + return null; + } catch (error) { + errorLines.push(toString(error)); + return null; + } + } + + /** + * @param {*} handler + * @return {function()|null} + */ + function checkInternalHandler(handler) { + if (handler && typeof handler === 'function') { + return handler; + } + errorLines.push("internal handler isn't a function or empty"); + return null; + } + + /** + * @param {*} obj + * @return {string} + * @suppress {uselessCode} + */ + function toString(obj) { + try { + return '' + obj; + } catch (e) { + return ''; + } + } + + /** + * @param {*} obj + * @return {boolean} + */ + function nonEmptyObject(obj) { + return !!obj; + } + + function jQueryFetcher(node) { + if (!node || !(node instanceof Node)) { + return { eventListeners: [] }; + } + const jQuery = /** @type {?{fn,data,_data}}*/ (window['jQuery']); + if (!jQuery || !jQuery.fn) { + return { eventListeners: [] }; + } + const jQueryFunction = /** @type {function(!Node)} */ (jQuery); + const data = jQuery._data || jQuery.data; + + const eventListeners = []; + const internalHandlers = []; + + if (typeof data === 'function') { + const events = data(node, 'events'); + for (const type in events) { + for (const key in events[type]) { + const frameworkListener = events[type][key]; + if (typeof frameworkListener === 'object' || typeof frameworkListener === 'function') { + const listener = { + handler: frameworkListener.handler || frameworkListener, + useCapture: true, + passive: false, + once: false, + type: type, + }; + listener.remove = jQueryRemove.bind(node, frameworkListener.selector); + eventListeners.push(listener); + } + } + } + const nodeData = data(node); + if (nodeData && typeof nodeData.handle === 'function') { + internalHandlers.push(nodeData.handle); + } + } + const entry = jQueryFunction(node)[0]; + if (entry) { + const entryEvents = entry['$events']; + for (const type in entryEvents) { + const events = entryEvents[type]; + for (const key in events) { + if (typeof events[key] === 'function') { + const listener = { handler: events[key], useCapture: true, passive: false, once: false, type: type }; + // We don't support removing for old version < 1.4 of jQuery because it doesn't provide API for getting "selector". + eventListeners.push(listener); + } + } + } + if (entry && entry['$handle']) { + internalHandlers.push(entry['$handle']); + } + } + return { eventListeners: eventListeners, internalHandlers: internalHandlers }; + } + + /** + * @param {string} selector + * @param {string} type + * @param {function()} handler + * @this {?Object} + */ + function jQueryRemove(selector, type, handler) { + if (!this || !(this instanceof Node)) { + return; + } + const node = /** @type {!Node} */ (this); + const jQuery = /** @type {?{fn,data,_data}}*/ (window['jQuery']); + if (!jQuery || !jQuery.fn) { + return; + } + const jQueryFunction = /** @type {function(!Node)} */ (jQuery); + jQueryFunction(node).off(type, selector, handler); + } + } +} + +/** @typedef {{eventListeners:!Array, internalHandlers:?SDK.RemoteObject.RemoteArray}} */ +export let FrameworkEventListenersObject; + +/** @typedef {{type: string, useCapture: boolean, passive: boolean, once: boolean, handler: function()}} */ +export let EventListenerObjectInInspectedPage; diff --git a/front_end/chii_event_listeners/EventListenersView.js b/front_end/chii_event_listeners/EventListenersView.js new file mode 100644 index 00000000..58e89e05 --- /dev/null +++ b/front_end/chii_event_listeners/EventListenersView.js @@ -0,0 +1,431 @@ +// 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. + +import * as Common from '../common/common.js'; +import * as Components from '../components/components.js'; +import * as ObjectUI from '../object_ui/object_ui.js'; +import * as SDK from '../sdk/sdk.js'; +import * as UI from '../ui/ui.js'; + +import { frameworkEventListeners, FrameworkEventListenersObject } from './EventListenersUtils.js'; // eslint-disable-line no-unused-vars + +/** + * @unrestricted + */ +export class EventListenersView extends UI.Widget.VBox { + /** + * @param {function()} changeCallback + */ + constructor(changeCallback) { + super(); + this._changeCallback = changeCallback; + this._treeOutline = new UI.TreeOutline.TreeOutlineInShadow(); + this._treeOutline.hideOverflow(); + this._treeOutline.registerRequiredCSS('object_ui/objectValue.css'); + this._treeOutline.registerRequiredCSS('chii_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.UIString('No event listeners'); + this._emptyHolder.tabIndex = -1; + this._linkifier = new Components.Linkifier.Linkifier(); + /** @type {!Map} */ + this._treeItemMap = new Map(); + } + + /** + * @override + */ + focus() { + if (!this._emptyHolder.parentNode) { + this._treeOutline.forceSelect(); + } else { + this._emptyHolder.focus(); + } + } + + /** + * @param {!Array} objects + * @return {!Promise} + */ + async addObjects(objects) { + this.reset(); + await Promise.all(objects.map(obj => (obj ? this._addObject(obj) : Promise.resolve()))); + this.addEmptyHolderIfNeeded(); + this._eventListenersArrivedForTest(); + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} object + * @return {!Promise} + */ + _addObject(object) { + /** @type {!Array} */ + let eventListeners; + /** @type {?FrameworkEventListenersObject}*/ + let frameworkEventListenersObject = 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)); + + /** + * @param {!Array} result + */ + function storeEventListeners(result) { + eventListeners = result; + } + + /** + * @param {?FrameworkEventListenersObject} result + */ + function storeFrameworkEventListenersObject(result) { + frameworkEventListenersObject = result; + } + + /** + * @return {!Promise} + */ + function markInternalEventListeners() { + if (!frameworkEventListenersObject.internalHandlers) { + return Promise.resolve(undefined); + } + return frameworkEventListenersObject.internalHandlers + .object() + .callFunctionJSON(isInternalEventListener, eventListeners.map(handlerArgument)) + .then(setIsInternal); + + /** + * @param {!SDK.DOMDebuggerModel.EventListener} listener + * @return {!Protocol.Runtime.CallArgument} + */ + function handlerArgument(listener) { + return SDK.RemoteObject.RemoteObject.toCallArgument(listener.handler()); + } + + /** + * @suppressReceiverCheck + * @return {!Array} + * @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} 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.RemoteObject} object + * @param {?Array} 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.DOMDebuggerModel.EventListener.Origin.FrameworkUser && !showFramework) { + hidden = true; + } + if (listenerOrigin === SDK.DOMDebuggerModel.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.TreeOutline.TreeElement { + /** + * @param {string} type + * @param {!Components.Linkifier.Linkifier} linkifier + * @param {function()} changeCallback + */ + constructor(type, linkifier, changeCallback) { + super(type); + this.toggleOnClick = true; + this._linkifier = linkifier; + this._changeCallback = changeCallback; + UI.ARIAUtils.setAccessibleName(this.listItemElement, `${type}, event listener`); + } + + /** + * @param {!UI.TreeOutline.TreeElement} element1 + * @param {!UI.TreeOutline.TreeElement} element2 + * @return {number} + */ + static comparator(element1, element2) { + if (element1.title === element2.title) { + return 0; + } + return element1.title > element2.title ? 1 : -1; + } + + /** + * @param {!SDK.DOMDebuggerModel.EventListener} eventListener + * @param {!SDK.RemoteObject.RemoteObject} object + */ + addObjectEventListener(eventListener, object) { + const treeElement = new ObjectEventListenerBar(eventListener, object, this._linkifier, this._changeCallback); + this.appendChild(/** @type {!UI.TreeOutline.TreeElement} */ (treeElement)); + } +} + +/** + * @unrestricted + */ +export class ObjectEventListenerBar extends UI.TreeOutline.TreeElement { + /** + * @param {!SDK.DOMDebuggerModel.EventListener} eventListener + * @param {!SDK.RemoteObject.RemoteObject} object + * @param {!Components.Linkifier.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.RemoteObject.RemoteObjectProperty('handler', eventListener.handler())); + } + ObjectUI.ObjectPropertiesSection.ObjectPropertyTreeElement.populateWithProperties(this, properties, [], true, null); + } + + /** + * @param {!SDK.RemoteObject.RemoteObject} object + * @param {!Components.Linkifier.Linkifier} linkifier + */ + _setTitle(object, linkifier) { + const title = this.listItemElement.createChild('span', 'event-listener-details'); + if (!window.ChiiMain) { + const subtitle = this.listItemElement.createChild('span', 'event-listener-tree-subtitle'); + const linkElement = linkifier.linkifyRawLocation(this._eventListener.location(), this._eventListener.sourceURL()); + subtitle.appendChild(linkElement); + } + + const propertyValue = ObjectUI.ObjectPropertiesSection.ObjectPropertiesSection.createPropertyValue( + object, + /* wasThrown */ false, + /* showPreview */ false + ); + this._valueTitle = propertyValue.element; + title.appendChild(this._valueTitle); + + if (this._eventListener.canRemove()) { + const deleteButton = title.createChild('span', 'event-listener-button'); + deleteButton.textContent = Common.UIString.UIString('Remove'); + deleteButton.title = Common.UIString.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.UIString('Toggle Passive'); + passiveButton.title = Common.UIString.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.ContextMenu(event); + if (!window.ChiiMain) { + if (event.target !== linkElement) { + menu.appendApplicableItems(linkElement); + } + } + if (object.subtype === 'node') { + menu.defaultSection().appendItem(ls`Reveal in Elements panel`, () => Common.Revealer.reveal(object)); + } + 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.DOMDebuggerModel.EventListener} + */ + eventListener() { + return this._eventListener; + } + + /** + * @override + */ + onenter() { + if (this._valueTitle) { + this._valueTitle.click(); + return true; + } + + return false; + } +} diff --git a/front_end/chii_event_listeners/chii_event_listeners-legacy.js b/front_end/chii_event_listeners/chii_event_listeners-legacy.js new file mode 100644 index 00000000..78611983 --- /dev/null +++ b/front_end/chii_event_listeners/chii_event_listeners-legacy.js @@ -0,0 +1,19 @@ +// Copyright 2019 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. + +import * as EventListenersModule from './chii_event_listeners.js'; + +self.EventListeners = self.EventListeners || {}; +EventListeners = EventListeners || {}; + +EventListeners.frameworkEventListeners = EventListenersModule.EventListenersUtils.frameworkEventListeners; + +/** @constructor */ +EventListeners.EventListenersView = EventListenersModule.EventListenersView.EventListenersView; + +/** @constructor */ +EventListeners.EventListenersTreeElement = EventListenersModule.EventListenersView.EventListenersTreeElement; + +/** @constructor */ +EventListeners.ObjectEventListenerBar = EventListenersModule.EventListenersView.ObjectEventListenerBar; diff --git a/front_end/chii_event_listeners/chii_event_listeners.js b/front_end/chii_event_listeners/chii_event_listeners.js new file mode 100644 index 00000000..40c78f92 --- /dev/null +++ b/front_end/chii_event_listeners/chii_event_listeners.js @@ -0,0 +1,8 @@ +// Copyright 2019 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. + +import * as EventListenersUtils from './EventListenersUtils.js'; +import * as EventListenersView from './EventListenersView.js'; + +export { EventListenersUtils, EventListenersView }; diff --git a/front_end/chii_event_listeners/eventListenersView.css b/front_end/chii_event_listeners/eventListenersView.css new file mode 100644 index 00000000..4900ade9 --- /dev/null +++ b/front_end/chii_event_listeners/eventListenersView.css @@ -0,0 +1,91 @@ +/* + * 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. + */ + +.tree-outline-disclosure li { + padding: 2px 0 0 5px; + overflow: hidden; + display: flex; + min-height: 17px; + align-items: baseline; +} + +.tree-outline-disclosure > li { + border-top: 1px solid #f0f0f0; +} + +.tree-outline-disclosure > li:first-of-type { + border-top: none; +} + +.tree-outline-disclosure { + padding-left: 0 !important; + padding-right: 3px; +} + +.tree-outline-disclosure li.parent::before { + top: 0 !important; +} + +.tree-outline-disclosure .name { + color: rgb(136, 19, 145); +} + +.tree-outline-disclosure .object-value-node { + overflow: hidden; + text-overflow: ellipsis; +} + +.event-listener-details { + display: flex; +} + +.event-listener-tree-subtitle { + float: right; + margin-left: 5px; + flex-shrink: 0; +} + +.event-listener-button { + padding: 0 3px; + background-color: #f2f2f2; + border-radius: 3px; + border: 1px solid #c3c3c3; + margin-left: 10px; + display: block; + cursor: pointer; + opacity: 0.8; + flex-shrink: 0; +} + +.event-listener-button:hover { + background-color: #e0e0e0; + opacity: 1; +} + +.tree-outline-disclosure li:hover .event-listener-button { + display: inline; +} + +@media (forced-colors: active) { + .event-listener-details .event-listener-button { + forced-color-adjust: none; + opacity: 1; + background: ButtonFace; + color: ButtonText; + border-color: ButtonText; + } + .event-listener-button:hover { + background-color: Highlight !important; + color: HighlightText; + border-color: ButtonText; + } + .tree-outline.hide-selection-when-blurred .selected:focus[data-keyboard-focus="true"] .event-listener-button, + .tree-outline-disclosure li[data-keyboard-focus="true"]:focus .gray-info-message { + background-color: Highlight; + color: HighlightText; + border-color: HighlightText; + } +} diff --git a/front_end/chii_event_listeners/module.json b/front_end/chii_event_listeners/module.json new file mode 100644 index 00000000..61f7875a --- /dev/null +++ b/front_end/chii_event_listeners/module.json @@ -0,0 +1,11 @@ +{ + "dependencies": ["ui", "common", "components", "sdk", "object_ui"], + "scripts": [], + "modules": [ + "chii_event_listeners.js", + "chii_event_listeners-legacy.js", + "EventListenersView.js", + "EventListenersUtils.js" + ], + "resources": ["eventListenersView.css"] +} diff --git a/target/domains/DOMDebugger.ts b/target/domains/DOMDebugger.ts index 5cdb7499..172ac491 100644 --- a/target/domains/DOMDebugger.ts +++ b/target/domains/DOMDebugger.ts @@ -4,6 +4,7 @@ import isFn from 'licia/isFn'; import isBool from 'licia/isBool'; import keys from 'licia/keys'; import each from 'licia/each'; +import defaults from 'licia/defaults'; import * as stringifyObj from '../lib/stringifyObj'; import * as scripts from '../lib/scripts'; @@ -21,8 +22,8 @@ export function getEventListeners(params: any) { type, useCapture: event.useCapture, handler: stringifyObj.wrap(event.listener), - passive: false, - once: false, + passive: event.passive, + once: event.once, scriptId: script.scriptId, columnNumber: 0, lineNumber: 0, @@ -44,30 +45,43 @@ const winEventProto = getWinEventProto(); const origAddEvent = winEventProto.addEventListener; const origRmEvent = winEventProto.removeEventListener; -winEventProto.addEventListener = function (type: string, listener: any, useCapture: boolean) { - addEvent(this, type, listener, useCapture); +winEventProto.addEventListener = function (type: string, listener: any, options: any) { + addEvent(this, type, listener, options); origAddEvent.apply(this, arguments); }; -winEventProto.removeEventListener = function (type: string, listener: any, useCapture: boolean) { - rmEvent(this, type, listener, useCapture); +winEventProto.removeEventListener = function (type: string, listener: any) { + rmEvent(this, type, listener); origRmEvent.apply(this, arguments); }; -function addEvent(el: any, type: string, listener: any, useCapture = false) { - if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return; +function addEvent(el: any, type: string, listener: any, options: any = false) { + if (!isEl(el) || !isFn(listener)) return; + + if (isBool(options)) { + options = { + capture: options, + }; + } + defaults(options, { + capture: false, + passive: false, + once: false, + }); const events = (el.chiiEvents = el.chiiEvents || {}); events[type] = events[type] || []; events[type].push({ - listener: listener, - useCapture: useCapture, + listener, + useCapture: options.capture, + passive: options.passive, + once: options.once, }); } -function rmEvent(el: any, type: string, listener: any, useCapture = false) { - if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return; +function rmEvent(el: any, type: string, listener: any) { + if (!isEl(el) || !isFn(listener)) return; const events = el.chiiEvents; diff --git a/target/lib/connector.ts b/target/lib/connector.ts index 81ecd2a3..892b64a9 100644 --- a/target/lib/connector.ts +++ b/target/lib/connector.ts @@ -10,6 +10,7 @@ const sessionStore = safeStorage('session'); class Connector extends Emitter { private ws: WebSocket; + private isInit: boolean = false; constructor() { super(); @@ -27,12 +28,14 @@ class Connector extends Emitter { })}` ); this.ws.addEventListener('open', () => { + this.isInit = true; this.ws.addEventListener('message', event => { this.emit('message', JSON.parse(event.data)); }); }); } send(message: any) { + if (!this.isInit) return; this.ws.send(JSON.stringify(message)); } trigger(method: string, params: any) {