UNPKG

chrome-devtools-frontend

Version:
484 lines (438 loc) • 18.6 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. 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; }