2017-09-15 19:07:41 +03:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2019-01-17 21:18:31 +03:00
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
|
|
);
|
2019-01-10 17:26:02 +03:00
|
|
|
|
2019-01-17 21:18:31 +03:00
|
|
|
const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
|
2019-01-10 17:26:02 +03:00
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
|
|
|
|
|
2017-09-15 19:07:41 +03:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
|
|
"ContentEventObserverService",
|
|
|
|
"WebElementEventTarget",
|
|
|
|
];
|
|
|
|
|
|
|
|
/**
|
2018-02-26 14:21:14 +03:00
|
|
|
* The ``EventTarget`` for web elements can be used to observe DOM
|
2017-09-15 19:07:41 +03:00
|
|
|
* events in the content document.
|
|
|
|
*
|
|
|
|
* A caveat of the current implementation is that it is only possible
|
2018-02-26 14:21:14 +03:00
|
|
|
* to listen for top-level ``window`` global events.
|
2017-09-15 19:07:41 +03:00
|
|
|
*
|
2018-02-26 14:21:14 +03:00
|
|
|
* It needs to be backed by a :js:class:`ContentEventObserverService`
|
|
|
|
* in a content frame script.
|
2017-09-15 19:07:41 +03:00
|
|
|
*
|
2018-02-26 14:21:14 +03:00
|
|
|
* Usage::
|
2017-09-15 19:07:41 +03:00
|
|
|
*
|
|
|
|
* let observer = new WebElementEventTarget(messageManager);
|
|
|
|
* await new Promise(resolve => {
|
|
|
|
* observer.addEventListener("visibilitychange", resolve, {once: true});
|
|
|
|
* chromeWindow.minimize();
|
|
|
|
* });
|
|
|
|
*/
|
|
|
|
class WebElementEventTarget {
|
|
|
|
/**
|
|
|
|
* @param {function(): nsIMessageListenerManager} messageManagerFn
|
|
|
|
* Message manager to the current browser.
|
|
|
|
*/
|
|
|
|
constructor(messageManager) {
|
|
|
|
this.mm = messageManager;
|
|
|
|
this.listeners = {};
|
|
|
|
this.mm.addMessageListener("Marionette:DOM:OnEvent", this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register an event handler of a specific event type from the content
|
|
|
|
* frame.
|
|
|
|
*
|
|
|
|
* @param {string} type
|
|
|
|
* Event type to listen for.
|
|
|
|
* @param {EventListener} listener
|
2018-02-26 14:21:14 +03:00
|
|
|
* Object which receives a notification (a ``BareEvent``)
|
2017-09-15 19:07:41 +03:00
|
|
|
* when an event of the specified type occurs. This must be
|
2018-02-26 14:21:14 +03:00
|
|
|
* an object implementing the ``EventListener`` interface,
|
2017-09-15 19:07:41 +03:00
|
|
|
* or a JavaScript function.
|
|
|
|
* @param {boolean=} once
|
2018-02-26 14:21:14 +03:00
|
|
|
* Indicates that the ``listener`` should be invoked at
|
|
|
|
* most once after being added. If true, the ``listener``
|
2017-09-15 19:07:41 +03:00
|
|
|
* would automatically be removed when invoked.
|
|
|
|
*/
|
|
|
|
addEventListener(type, listener, { once = false } = {}) {
|
|
|
|
if (!(type in this.listeners)) {
|
|
|
|
this.listeners[type] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.listeners[type].includes(listener)) {
|
|
|
|
listener.once = once;
|
|
|
|
this.listeners[type].push(listener);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.mm.sendAsyncMessage("Marionette:DOM:AddEventListener", { type });
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes an event listener.
|
|
|
|
*
|
|
|
|
* @param {string} type
|
|
|
|
* Type of event to cease listening for.
|
|
|
|
* @param {EventListener} listener
|
|
|
|
* Event handler to remove from the event target.
|
|
|
|
*/
|
|
|
|
removeEventListener(type, listener) {
|
|
|
|
if (!(type in this.listeners)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let stack = this.listeners[type];
|
|
|
|
for (let i = stack.length - 1; i >= 0; --i) {
|
|
|
|
if (stack[i] === listener) {
|
|
|
|
stack.splice(i, 1);
|
|
|
|
if (stack.length == 0) {
|
|
|
|
this.mm.sendAsyncMessage("Marionette:DOM:RemoveEventListener", {
|
|
|
|
type,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatchEvent(event) {
|
|
|
|
if (!(event.type in this.listeners)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.target = this;
|
|
|
|
|
|
|
|
let stack = this.listeners[event.type].slice(0);
|
|
|
|
stack.forEach(listener => {
|
2019-01-10 17:26:26 +03:00
|
|
|
if (typeof listener.handleEvent == "function") {
|
|
|
|
listener.handleEvent(event);
|
|
|
|
} else {
|
|
|
|
listener(event);
|
|
|
|
}
|
2017-09-15 19:07:41 +03:00
|
|
|
|
|
|
|
if (listener.once) {
|
|
|
|
this.removeEventListener(event.type, listener);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-03 16:35:47 +03:00
|
|
|
receiveMessage({ name, data, objects }) {
|
2017-09-15 19:07:41 +03:00
|
|
|
if (name != "Marionette:DOM:OnEvent") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let ev = {
|
|
|
|
type: data.type,
|
|
|
|
target: objects.target,
|
|
|
|
};
|
|
|
|
this.dispatchEvent(ev);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.WebElementEventTarget = WebElementEventTarget;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides the frame script backend for the
|
2018-02-26 14:21:14 +03:00
|
|
|
* :js:class:`WebElementEventTarget`.
|
2017-09-15 19:07:41 +03:00
|
|
|
*
|
|
|
|
* This service receives requests for new DOM events to listen for and
|
|
|
|
* to cease listening for, and despatches IPC messages to the browser
|
|
|
|
* when they fire.
|
|
|
|
*/
|
|
|
|
class ContentEventObserverService {
|
|
|
|
/**
|
|
|
|
* @param {WindowProxy} windowGlobal
|
|
|
|
* Window.
|
|
|
|
* @param {nsIMessageSender.sendAsyncMessage} sendAsyncMessage
|
|
|
|
* Function for sending an async message to the parent browser.
|
|
|
|
*/
|
|
|
|
constructor(windowGlobal, sendAsyncMessage) {
|
|
|
|
this.window = windowGlobal;
|
|
|
|
this.sendAsyncMessage = sendAsyncMessage;
|
|
|
|
this.events = new Set();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Observe a new DOM event.
|
|
|
|
*
|
2018-02-26 14:21:14 +03:00
|
|
|
* When the DOM event of ``type`` fires, a message is passed to
|
|
|
|
* the parent browser's event observer.
|
2017-09-15 19:07:41 +03:00
|
|
|
*
|
|
|
|
* If event type is already being observed, only a single message
|
|
|
|
* is sent. E.g. multiple registration for events will only ever emit
|
|
|
|
* a maximum of one message.
|
|
|
|
*
|
|
|
|
* @param {string} type
|
|
|
|
* DOM event to listen for.
|
|
|
|
*/
|
|
|
|
add(type) {
|
|
|
|
if (this.events.has(type)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.window.addEventListener(type, this);
|
|
|
|
this.events.add(type);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ceases observing a DOM event.
|
|
|
|
*
|
|
|
|
* @param {string} type
|
|
|
|
* DOM event to stop listening for.
|
|
|
|
*/
|
|
|
|
remove(type) {
|
|
|
|
if (!this.events.has(type)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.window.removeEventListener(type, this);
|
|
|
|
this.events.delete(type);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Ceases observing all previously registered DOM events. */
|
|
|
|
clear() {
|
|
|
|
for (let ev of this) {
|
|
|
|
this.remove(ev);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*[Symbol.iterator]() {
|
|
|
|
for (let ev of this.events) {
|
|
|
|
yield ev;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleEvent({ type, target }) {
|
2019-01-10 17:26:02 +03:00
|
|
|
logger.trace(`Received DOM event ${type}`);
|
2017-09-15 19:07:41 +03:00
|
|
|
this.sendAsyncMessage("Marionette:DOM:OnEvent", { type }, { target });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.ContentEventObserverService = ContentEventObserverService;
|