text: // 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.
  
  /**
   * @param {!SDK.RemoteObject} object
   * @return {!Promise<!EventListeners.FrameworkEventListenersObject>}
   */
  export function frameworkEventListeners(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.callFunction(frameworkEventListenersImpl, undefined)
        .then(assertCallFunctionResult)
        .then(getOwnProperties)
        .then(createEventListeners)
        .then(returnResult)
        .catchException(listenersResult);
  
    /**
     * @param {!SDK.RemoteObject} object
     * @return {!Promise<!SDK.GetPropertiesResult>}
     */
    function getOwnProperties(object) {
      return object.getOwnProperties(false /* generatePreview */);
    }
  
    /**
     * @param {!SDK.GetPropertiesResult} 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.callFunctionJSON(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.callFunction(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.callFunction(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.bind(null));
    }
  
    /**
     * @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 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 {?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);
      }
    }
  }
  
  /* Legacy exported object */
  self.EventListeners = self.EventListeners || {};
  
  /* Legacy exported object */
  EventListeners = EventListeners || {};
  
  EventListeners.frameworkEventListeners = frameworkEventListeners;
  
  /** @typedef {{eventListeners:!Array<!SDK.EventListener>, internalHandlers:?SDK.RemoteArray}} */
  EventListeners.FrameworkEventListenersObject;
  
  /** @typedef {{type: string, useCapture: boolean, passive: boolean, once: boolean, handler: function()}} */
  EventListeners.EventListenerObjectInInspectedPage;
  