gecko-dev/dom/browser-element/BrowserElementParent.jsm

278 строки
8.1 KiB
JavaScript

/* 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";
/* BrowserElementParent injects script to listen for certain events in the
* child. We then listen to messages from the child script and take
* appropriate action here in the parent.
*/
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { BrowserElementPromptService } = ChromeUtils.import(
"resource://gre/modules/BrowserElementPromptService.jsm"
);
function debug(msg) {
// dump("BrowserElementParent - " + msg + "\n");
}
function handleWindowEvent(e) {
if (this._browserElementParents) {
let beps = ChromeUtils.nondeterministicGetWeakMapKeys(
this._browserElementParents
);
beps.forEach(bep => bep._handleOwnerEvent(e));
}
}
function BrowserElementParent() {
debug("Creating new BrowserElementParent object");
}
BrowserElementParent.prototype = {
classDescription: "BrowserElementAPI implementation",
classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"),
contractID: "@mozilla.org/dom/browser-element-api;1",
QueryInterface: ChromeUtils.generateQI([
"nsIBrowserElementAPI",
"nsISupportsWeakReference",
]),
setFrameLoader(frameLoader) {
debug("Setting frameLoader");
this._frameLoader = frameLoader;
this._frameElement = frameLoader.ownerElement;
if (!this._frameElement) {
debug("No frame element?");
return;
}
// Listen to visibilitychange on the iframe's owner window, and forward
// changes down to the child. We want to do this while registering as few
// visibilitychange listeners on _window as possible, because such a listener
// may live longer than this BrowserElementParent object.
//
// To accomplish this, we register just one listener on the window, and have
// it reference a WeakMap whose keys are all the BrowserElementParent objects
// on the window. Then when the listener fires, we iterate over the
// WeakMap's keys (which we can do, because we're chrome) to notify the
// BrowserElementParents.
if (!this._window._browserElementParents) {
this._window._browserElementParents = new WeakMap();
let handler = handleWindowEvent.bind(this._window);
let windowEvents = ["visibilitychange"];
for (let event of windowEvents) {
Services.els.addSystemEventListener(
this._window,
event,
handler,
/* useCapture = */ true
);
}
}
this._window._browserElementParents.set(this, null);
// Insert ourself into the prompt service.
BrowserElementPromptService.mapFrameToBrowserElementParent(
this._frameElement,
this
);
this._setupMessageListener();
},
destroyFrameScripts() {
debug("Destroying frame scripts");
this._mm.sendAsyncMessage("browser-element-api:destroy");
},
_setupMessageListener() {
this._mm = this._frameLoader.messageManager;
this._mm.addMessageListener("browser-element-api:call", this);
},
receiveMessage(aMsg) {
if (!this._isAlive()) {
return undefined;
}
// Messages we receive are handed to functions which take a (data) argument,
// where |data| is the message manager's data object.
// We use a single message and dispatch to various function based
// on data.msg_name
let mmCalls = {
hello: this._recvHello,
};
let mmSecuritySensitiveCalls = {
showmodalprompt: this._handleShowModalPrompt,
};
if (aMsg.data.msg_name in mmCalls) {
return mmCalls[aMsg.data.msg_name].apply(this, arguments);
} else if (aMsg.data.msg_name in mmSecuritySensitiveCalls) {
return mmSecuritySensitiveCalls[aMsg.data.msg_name].apply(
this,
arguments
);
}
return undefined;
},
_removeMessageListener() {
this._mm.removeMessageListener("browser-element-api:call", this);
},
/**
* You shouldn't touch this._frameElement or this._window if _isAlive is
* false. (You'll likely get an exception if you do.)
*/
_isAlive() {
return (
!Cu.isDeadWrapper(this._frameElement) &&
!Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
!Cu.isDeadWrapper(this._frameElement.ownerGlobal)
);
},
get _window() {
return this._frameElement.ownerGlobal;
},
_sendAsyncMsg(msg, data) {
try {
if (!data) {
data = {};
}
data.msg_name = msg;
this._mm.sendAsyncMessage("browser-element-api:call", data);
} catch (e) {
return false;
}
return true;
},
_recvHello() {
debug("recvHello");
// Inform our child if our owner element's document is invisible. Note
// that we must do so here, rather than in the BrowserElementParent
// constructor, because the BrowserElementChild may not be initialized when
// we run our constructor.
if (this._window.document.hidden) {
this._ownerVisibilityChange();
}
},
/**
* Fire either a vanilla or a custom event, depending on the contents of
* |data|.
*/
_fireEventFromMsg(data) {
let detail = data.json;
let name = detail.msg_name;
// For events that send a "_payload_" property, we just want to transmit
// this in the event.
if ("_payload_" in detail) {
detail = detail._payload_;
}
debug("fireEventFromMsg: " + name + ", " + JSON.stringify(detail));
let evt = this._createEvent(name, detail, /* cancelable = */ false);
this._frameElement.dispatchEvent(evt);
},
_handleShowModalPrompt(data) {
// Fire a showmodalprmopt event on the iframe. When this method is called,
// the child is spinning in a nested event loop waiting for an
// unblock-modal-prompt message.
//
// If the embedder calls preventDefault() on the showmodalprompt event,
// we'll block the child until event.detail.unblock() is called.
//
// Otherwise, if preventDefault() is not called, we'll send the
// unblock-modal-prompt message to the child as soon as the event is done
// dispatching.
let detail = data.json;
debug("handleShowPrompt " + JSON.stringify(detail));
// Strip off the windowID property from the object we send along in the
// event.
let windowID = detail.windowID;
delete detail.windowID;
debug("Event will have detail: " + JSON.stringify(detail));
let evt = this._createEvent(
"showmodalprompt",
detail,
/* cancelable = */ true
);
let self = this;
let unblockMsgSent = false;
function sendUnblockMsg() {
if (unblockMsgSent) {
return;
}
unblockMsgSent = true;
// We don't need to sanitize evt.detail.returnValue (e.g. converting the
// return value of confirm() to a boolean); Gecko does that for us.
let data = { windowID, returnValue: evt.detail.returnValue };
self._sendAsyncMsg("unblock-modal-prompt", data);
}
Cu.exportFunction(sendUnblockMsg, evt.detail, { defineAs: "unblock" });
this._frameElement.dispatchEvent(evt);
if (!evt.defaultPrevented) {
// Unblock the inner frame immediately. Otherwise we'll unblock upon
// evt.detail.unblock().
sendUnblockMsg();
}
},
_createEvent(evtName, detail, cancelable) {
// This will have to change if we ever want to send a CustomEvent with null
// detail. For now, it's OK.
if (detail !== undefined && detail !== null) {
detail = Cu.cloneInto(detail, this._window);
return new this._window.CustomEvent("mozbrowser" + evtName, {
bubbles: true,
cancelable,
detail,
});
}
return new this._window.Event("mozbrowser" + evtName, {
bubbles: true,
cancelable,
});
},
_handleOwnerEvent(evt) {
switch (evt.type) {
case "visibilitychange":
this._ownerVisibilityChange();
break;
}
},
/**
* Called when the visibility of the window which owns this iframe changes.
*/
_ownerVisibilityChange() {
let bc = this._frameLoader?.browsingContext;
if (bc) {
bc.isActive = !this._window.document.hidden;
}
},
};
var EXPORTED_SYMBOLS = ["BrowserElementParent"];