chrome-devtools-frontend
Version:
Chrome DevTools UI
484 lines (438 loc) • 18.6 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.
import * as Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
export async function frameworkEventListeners(object: SDK.RemoteObject.RemoteObject):
Promise<FrameworkEventListenersObject> {
const domDebuggerModel = object.runtimeModel().target().model(SDK.DOMDebuggerModel.DOMDebuggerModel);
if (!domDebuggerModel) {
return {eventListeners: [], internalHandlers: null} as FrameworkEventListenersObject;
}
const listenersResult = {internalHandlers: null, eventListeners: []} as FrameworkEventListenersObject;
return await object.callFunction(frameworkEventListenersImpl, undefined)
.then(assertCallFunctionResult)
.then(getOwnProperties)
.then(createEventListeners)
.then(returnResult)
.catch(error => {
console.error(error);
return listenersResult;
});
function getOwnProperties(object: SDK.RemoteObject.RemoteObject): Promise<SDK.RemoteObject.GetPropertiesResult> {
return object.getOwnProperties(false /* generatePreview */);
}
async function createEventListeners(result: SDK.RemoteObject.GetPropertiesResult): Promise<void> {
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);
}
}
await Promise.all(promises);
}
function convertToEventListeners(pageEventListenersObject: SDK.RemoteObject.RemoteObject):
Promise<SDK.DOMDebuggerModel.EventListener[]> {
return SDK.RemoteObject.RemoteArray.objectAsArray(pageEventListenersObject)
.map(toEventListener)
.then(filterOutEmptyObjects);
function toEventListener(listenerObject: SDK.RemoteObject.RemoteObject):
Promise<SDK.DOMDebuggerModel.EventListener|null> {
let type: string;
let useCapture: boolean;
let passive: boolean;
let once: boolean;
let handler: SDK.RemoteObject.RemoteObject|null = null;
let originalHandler: SDK.RemoteObject.RemoteObject|null = null;
let location: (SDK.DebuggerModel.Location|null)|null = null;
let removeFunctionObject: SDK.RemoteObject.RemoteObject|null = null;
const promises = [];
promises.push(
listenerObject
.callFunctionJSON(
truncatePageEventListener as (this: Object) => TruncatedEventListenerObjectInInspectedPage, undefined)
.then(storeTruncatedListener));
function truncatePageEventListener(this: EventListenerObjectInInspectedPage):
TruncatedEventListenerObjectInInspectedPage {
return {type: this.type, useCapture: this.useCapture, passive: this.passive, once: this.once};
}
function storeTruncatedListener(truncatedListener: TruncatedEventListenerObjectInInspectedPage|null): void {
if (!truncatedListener) {
return;
}
if (truncatedListener.type !== undefined) {
type = truncatedListener.type;
}
if (truncatedListener.useCapture !== undefined) {
useCapture = truncatedListener.useCapture;
}
if (truncatedListener.passive !== undefined) {
passive = truncatedListener.passive;
}
if (truncatedListener.once !== undefined) {
once = truncatedListener.once;
}
}
promises.push(
listenerObject.callFunction(handlerFunction as (this: Object) => SDK.RemoteObject.RemoteObject | null)
.then(assertCallFunctionResult)
.then(storeOriginalHandler)
.then(toTargetFunction)
.then(storeFunctionWithDetails));
function handlerFunction(this: EventListenerObjectInInspectedPage): SDK.RemoteObject.RemoteObject|null {
return this.handler || null;
}
function storeOriginalHandler(functionObject: SDK.RemoteObject.RemoteObject): SDK.RemoteObject.RemoteObject {
originalHandler = functionObject;
return originalHandler;
}
function storeFunctionWithDetails(functionObject: SDK.RemoteObject.RemoteObject): Promise<void> {
handler = functionObject;
return functionObject.debuggerModel().functionDetailsPromise(functionObject).then(storeFunctionDetails);
}
function storeFunctionDetails(functionDetails: SDK.DebuggerModel.FunctionDetails|null): void {
location = functionDetails ? functionDetails.location : null;
}
promises.push(
listenerObject.callFunction(getRemoveFunction as (this: Object) => SDK.RemoteObject.RemoteObject | null)
.then(assertCallFunctionResult)
.then(storeRemoveFunction));
function getRemoveFunction(this: EventListenerObjectInInspectedPage): SDK.RemoteObject.RemoteObject|null {
return this.remove || null;
}
function storeRemoveFunction(functionObject: SDK.RemoteObject.RemoteObject): void {
if (functionObject.type !== 'function') {
return;
}
removeFunctionObject = functionObject;
}
return Promise.all(promises).then(createEventListener).catch(error => {
console.error(error);
return null;
});
function createEventListener(): SDK.DOMDebuggerModel.EventListener {
if (!location) {
throw new Error('Empty event listener\'s location');
}
return new SDK.DOMDebuggerModel.EventListener(
domDebuggerModel as SDK.DOMDebuggerModel.DOMDebuggerModel, object, type, useCapture, passive, once, handler,
originalHandler, location, removeFunctionObject, SDK.DOMDebuggerModel.EventListener.Origin.FRAMEWORK_USER);
}
}
}
function convertToInternalHandlers(pageInternalHandlersObject: SDK.RemoteObject.RemoteObject):
Promise<SDK.RemoteObject.RemoteArray> {
return SDK.RemoteObject.RemoteArray.objectAsArray(pageInternalHandlersObject)
.map(toTargetFunction)
.then(SDK.RemoteObject.RemoteArray.createFromRemoteObjects.bind(null));
}
function toTargetFunction(functionObject: SDK.RemoteObject.RemoteObject): Promise<SDK.RemoteObject.RemoteObject> {
return SDK.RemoteObject.RemoteFunction.objectAsFunction(functionObject).targetFunction();
}
function storeEventListeners(eventListeners: SDK.DOMDebuggerModel.EventListener[]): void {
listenersResult.eventListeners = eventListeners;
}
function storeInternalHandlers(internalHandlers: SDK.RemoteObject.RemoteArray): void {
listenersResult.internalHandlers = internalHandlers;
}
function printErrorString(errorString: SDK.RemoteObject.RemoteObject): void {
Common.Console.Console.instance().error(String(errorString.value));
}
function returnResult(): FrameworkEventListenersObject {
return listenersResult;
}
function assertCallFunctionResult(result: SDK.RemoteObject.CallFunctionResult): SDK.RemoteObject.RemoteObject {
if (result.wasThrown || !result.object) {
throw new Error('Exception in callFunction or empty result');
}
return result.object;
}
function filterOutEmptyObjects<T>(objects: Array<T|null>): T[] {
return objects.filter(filterOutEmpty) as T[];
function filterOutEmpty(object: T|null): boolean {
return Boolean(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(),
...
]
}
*/
function frameworkEventListenersImpl(this: Object): {eventListeners: EventListenerObjectInInspectedPage[]} {
const errorLines = [];
let eventListeners: EventListenerObjectInInspectedPage[] = [];
let internalHandlers: Array<() => void> = [];
let fetchers = [jQueryFetcher];
try {
// @ts-expect-error Here because of layout tests.
if (self.devtoolsFrameworkEventListeners && isArrayLike(self.devtoolsFrameworkEventListeners)) {
// @ts-expect-error Here because of layout tests.
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)) {
const fetcherResultEventListeners = fetcherResult.eventListeners;
const nonEmptyEventListeners = fetcherResultEventListeners
.map(eventListener => {
return checkEventListener(eventListener);
})
.filter(nonEmptyObject);
eventListeners = eventListeners.concat(nonEmptyEventListeners as EventListenerObjectInInspectedPage[]);
}
if (fetcherResult.internalHandlers && isArrayLike(fetcherResult.internalHandlers)) {
const fetcherResultInternalHandlers = fetcherResult.internalHandlers as Array<() => void>;
const nonEmptyInternalHandlers = fetcherResultInternalHandlers
.map(handler => {
return checkInternalHandler(handler);
})
.filter(nonEmptyObject);
internalHandlers = internalHandlers.concat(nonEmptyInternalHandlers as Array<() => void>);
}
} catch (e) {
errorLines.push('fetcher call produced error: ' + toString(e));
}
}
const result: {
eventListeners: EventListenerObjectInInspectedPage[],
internalHandlers?: Array<() => void>,
errorString?: string,
} = {
eventListeners,
internalHandlers: internalHandlers.length ? internalHandlers : undefined,
errorString: undefined,
};
// The logic further up seems to expect that if the internalHandlers is set,
// that it should have a non-empty Array, but TS / Closure also expect the
// property to exist, so we always add it above, but then remove it again
// here if there's no value set.
if (!result.internalHandlers) {
delete result.internalHandlers;
}
if (errorLines.length) {
let errorString: string = 'Framework Event Listeners API Errors:\n\t' + errorLines.join('\n\t');
errorString = errorString.substr(0, errorString.length - 1);
result.errorString = errorString;
}
// Remove the errorString if it's not set.
if (result.errorString === '' || result.errorString === undefined) {
delete result.errorString;
}
return result;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isArrayLike(obj: any): boolean {
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 {
}
return false;
}
function checkEventListener(eventListener: PossibleEventListenerObjectInInspectedPage|null):
EventListenerObjectInInspectedPage|null {
try {
let errorString = '';
if (!eventListener) {
errorString += 'empty event listener, ';
} else {
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,
useCapture,
passive,
once,
handler,
remove,
} as EventListenerObjectInInspectedPage;
}
}
errorLines.push(errorString.substr(0, errorString.length - 2));
return null;
} catch (error) {
errorLines.push(toString(error));
return null;
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function checkInternalHandler(handler: any): (() => void)|null {
if (handler && (typeof handler === 'function')) {
return handler;
}
errorLines.push('internal handler isn\'t a function or empty');
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function toString(obj: any): string {
try {
return String(obj);
} catch {
return '<error>';
}
}
function nonEmptyObject<T>(obj: T|null): boolean {
return Boolean(obj);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function jQueryFetcher(node: any): {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventListeners: Array<{handler: any, useCapture: boolean, passive: boolean, once: boolean, type: string}>,
internalHandlers?: Array<() => void>,
} {
if (!node || !(node instanceof Node)) {
return {eventListeners: []};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jQuery = (window as any)['jQuery'];
if (!jQuery?.fn) {
return {eventListeners: []};
}
const jQueryFunction = 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,
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};
// 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?.['$handle']) {
internalHandlers.push(entry['$handle']);
}
}
return {eventListeners, internalHandlers};
}
function jQueryRemove(this: Object|null, selector: string, type: string, handler: () => void): void {
if (!this || !(this instanceof Node)) {
return;
}
const node = this;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jQuery = (window as any)['jQuery'];
if (!jQuery?.fn) {
return;
}
const jQueryFunction = jQuery as (arg0: Node) => {
off: (...arg: any[]) => void,
};
jQueryFunction(node).off(type, selector, handler);
}
}
}
export interface FrameworkEventListenersObject {
eventListeners: SDK.DOMDebuggerModel.EventListener[];
internalHandlers: SDK.RemoteObject.RemoteArray|null;
}
export interface PossibleEventListenerObjectInInspectedPage {
type?: string;
useCapture?: boolean;
passive?: boolean;
once?: boolean;
handler?: SDK.RemoteObject.RemoteObject|null;
remove?: SDK.RemoteObject.RemoteObject|null;
}
export interface EventListenerObjectInInspectedPage {
type: string;
useCapture: boolean;
passive: boolean;
once: boolean;
handler: SDK.RemoteObject.RemoteObject|null;
remove: SDK.RemoteObject.RemoteObject|null;
}
export interface TruncatedEventListenerObjectInInspectedPage {
type?: string;
useCapture?: boolean;
passive?: boolean;
once?: boolean;
}