blob: 999c7b113a908218d509d13116e85e988eab08be [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.
/** @typedef {{eventListeners:!Array<!SDK.EventListener>, internalHandlers:?SDK.RemoteArray}} */
EventListeners.FrameworkEventListenersObject;
/** @typedef {{type: string, useCapture: boolean, passive: boolean, once: boolean, handler: function()}} */
EventListeners.EventListenerObjectInInspectedPage;
/**
* @param {!SDK.RemoteObject} object
* @return {!Promise<!EventListeners.FrameworkEventListenersObject>}
*/
EventListeners.frameworkEventListeners = function(object) {
const domDebuggerModel = object.runtimeModel().target().model(SDK.DOMDebuggerModel);
if (!domDebuggerModel) {
// TODO(kozyatinskiy): figure out how this should work for |window|.
return Promise.resolve(
/** @type {!EventListeners.FrameworkEventListenersObject} */ ({eventListeners: [], internalHandlers: null}));
}
const listenersResult = /** @type {!EventListeners.FrameworkEventListenersObject} */ ({eventListeners: []});
return object.callFunctionPromise(frameworkEventListeners, undefined)
.then(assertCallFunctionResult)
.then(getOwnProperties)
.then(createEventListeners)
.then(returnResult)
.catchException(listenersResult);
/**
* @param {!SDK.RemoteObject} object
* @return {!Promise<!{properties: ?Array.<!SDK.RemoteObjectProperty>, internalProperties: ?Array.<!SDK.RemoteObjectProperty>}>}
*/
function getOwnProperties(object) {
return object.getOwnPropertiesPromise(false /* generatePreview */);
}
/**
* @param {!{properties: ?Array<!SDK.RemoteObjectProperty>, internalProperties: ?Array<!SDK.RemoteObjectProperty>}} result
* @return {!Promise<undefined>}
*/
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<undefined>} */ (Promise.all(promises));
}
/**
* @param {!SDK.RemoteObject} pageEventListenersObject
* @return {!Promise<!Array<!SDK.EventListener>>}
*/
function convertToEventListeners(pageEventListenersObject) {
return SDK.RemoteArray.objectAsArray(pageEventListenersObject).map(toEventListener).then(filterOutEmptyObjects);
/**
* @param {!SDK.RemoteObject} listenerObject
* @return {!Promise<?SDK.EventListener>}
*/
function toEventListener(listenerObject) {
/** @type {string} */
let type;
/** @type {boolean} */
let useCapture;
/** @type {boolean} */
let passive;
/** @type {boolean} */
let once;
/** @type {?SDK.RemoteObject} */
let handler = null;
/** @type {?SDK.RemoteObject} */
let originalHandler = null;
/** @type {?SDK.DebuggerModel.Location} */
let location = null;
/** @type {?SDK.RemoteObject} */
let removeFunctionObject = null;
const promises = [];
promises.push(
listenerObject.callFunctionJSONPromise(truncatePageEventListener, undefined).then(storeTruncatedListener));
/**
* @suppressReceiverCheck
* @this {EventListeners.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.callFunctionPromise(handlerFunction)
.then(assertCallFunctionResult)
.then(storeOriginalHandler)
.then(toTargetFunction)
.then(storeFunctionWithDetails));
/**
* @suppressReceiverCheck
* @return {function()}
* @this {EventListeners.EventListenerObjectInInspectedPage}
*/
function handlerFunction() {
return this.handler;
}
/**
* @param {!SDK.RemoteObject} functionObject
* @return {!SDK.RemoteObject}
*/
function storeOriginalHandler(functionObject) {
originalHandler = functionObject;
return originalHandler;
}
/**
* @param {!SDK.RemoteObject} functionObject
* @return {!Promise<undefined>}
*/
function storeFunctionWithDetails(functionObject) {
handler = functionObject;
return /** @type {!Promise<undefined>} */ (
functionObject.debuggerModel().functionDetailsPromise(functionObject).then(storeFunctionDetails));
}
/**
* @param {?SDK.DebuggerModel.FunctionDetails} functionDetails
*/
function storeFunctionDetails(functionDetails) {
location = functionDetails ? functionDetails.location : null;
}
promises.push(listenerObject.callFunctionPromise(getRemoveFunction)
.then(assertCallFunctionResult)
.then(storeRemoveFunction));
/**
* @suppressReceiverCheck
* @return {function()}
* @this {EventListeners.EventListenerObjectInInspectedPage}
*/
function getRemoveFunction() {
return this.remove;
}
/**
* @param {!SDK.RemoteObject} functionObject
*/
function storeRemoveFunction(functionObject) {
if (functionObject.type !== 'function')
return;
removeFunctionObject = functionObject;
}
return Promise.all(promises).then(createEventListener).catchException(/** @type {?SDK.EventListener} */ (null));
/**
* @return {!SDK.EventListener}
*/
function createEventListener() {
if (!location)
throw new Error('Empty event listener\'s location');
return new SDK.EventListener(
/** @type {!SDK.DOMDebuggerModel} */ (domDebuggerModel), object, type, useCapture, passive, once, handler,
originalHandler, location, removeFunctionObject, SDK.EventListener.Origin.FrameworkUser);
}
}
}
/**
* @param {!SDK.RemoteObject} pageInternalHandlersObject
* @return {!Promise<!SDK.RemoteArray>}
*/
function convertToInternalHandlers(pageInternalHandlersObject) {
return SDK.RemoteArray.objectAsArray(pageInternalHandlersObject)
.map(toTargetFunction)
.then(SDK.RemoteArray.createFromRemoteObjects);
}
/**
* @param {!SDK.RemoteObject} functionObject
* @return {!Promise<!SDK.RemoteObject>}
*/
function toTargetFunction(functionObject) {
return SDK.RemoteFunction.objectAsFunction(functionObject).targetFunction();
}
/**
* @param {!Array<!SDK.EventListener>} eventListeners
*/
function storeEventListeners(eventListeners) {
listenersResult.eventListeners = eventListeners;
}
/**
* @param {!SDK.RemoteArray} internalHandlers
*/
function storeInternalHandlers(internalHandlers) {
listenersResult.internalHandlers = internalHandlers;
}
/**
* @param {!SDK.RemoteObject} errorString
*/
function printErrorString(errorString) {
Common.console.error(String(errorString.value));
}
/**
* @return {!EventListeners.FrameworkEventListenersObject}
*/
function returnResult() {
return listenersResult;
}
/**
* @param {!SDK.CallFunctionResult} result
* @return {!SDK.RemoteObject}
*/
function assertCallFunctionResult(result) {
if (result.wasThrown || !result.object)
throw new Error('Exception in callFunction or empty result');
return result.object;
}
/**
* @param {!Array<?T>} objects
* @return {!Array<!T>}
* @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<!EventListeners.EventListenerObjectInInspectedPage>, internalHandlers:?Array<function()>}}
* @this {Object}
*/
function frameworkEventListeners() {
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 {?EventListeners.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};
} else {
errorLines.push(errorString.substr(0, errorString.length - 2));
return null;
}
} catch (e) {
errorLines.push(toString(e));
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 '<error>';
}
}
/**
* @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);
}
}
};