347 lines
11 KiB
JavaScript
347 lines
11 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/. */
|
|
|
|
import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
|
|
|
|
// This needs to match ScreenLength.java
|
|
const SCREEN_LENGTH_TYPE_PIXEL = 0;
|
|
const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH = 1;
|
|
const SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT = 2;
|
|
const SCREEN_LENGTH_DOCUMENT_WIDTH = 3;
|
|
const SCREEN_LENGTH_DOCUMENT_HEIGHT = 4;
|
|
|
|
// This need to match PanZoomController.java
|
|
const SCROLL_BEHAVIOR_SMOOTH = 0;
|
|
const SCROLL_BEHAVIOR_AUTO = 1;
|
|
|
|
const SCREEN_ORIENTATION_PORTRAIT = 0;
|
|
const SCREEN_ORIENTATION_LANDSCAPE = 1;
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs",
|
|
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
|
|
});
|
|
|
|
export class GeckoViewContentChild extends GeckoViewActorChild {
|
|
constructor() {
|
|
super();
|
|
this.lastOrientation = SCREEN_ORIENTATION_PORTRAIT;
|
|
}
|
|
|
|
actorCreated() {
|
|
super.actorCreated();
|
|
|
|
this.pageShow = new Promise(resolve => {
|
|
this.receivedPageShow = resolve;
|
|
});
|
|
}
|
|
|
|
toPixels(aLength, aType) {
|
|
const { contentWindow } = this;
|
|
if (aType === SCREEN_LENGTH_TYPE_PIXEL) {
|
|
return aLength;
|
|
} else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_WIDTH) {
|
|
return aLength * contentWindow.visualViewport.width;
|
|
} else if (aType === SCREEN_LENGTH_TYPE_VISUAL_VIEWPORT_HEIGHT) {
|
|
return aLength * contentWindow.visualViewport.height;
|
|
} else if (aType === SCREEN_LENGTH_DOCUMENT_WIDTH) {
|
|
return aLength * contentWindow.document.body.scrollWidth;
|
|
} else if (aType === SCREEN_LENGTH_DOCUMENT_HEIGHT) {
|
|
return aLength * contentWindow.document.body.scrollHeight;
|
|
}
|
|
|
|
return aLength;
|
|
}
|
|
|
|
toScrollBehavior(aBehavior) {
|
|
const { contentWindow } = this;
|
|
if (!contentWindow) {
|
|
return 0;
|
|
}
|
|
const { windowUtils } = contentWindow;
|
|
if (aBehavior === SCROLL_BEHAVIOR_SMOOTH) {
|
|
return windowUtils.SCROLL_MODE_SMOOTH;
|
|
} else if (aBehavior === SCROLL_BEHAVIOR_AUTO) {
|
|
return windowUtils.SCROLL_MODE_INSTANT;
|
|
}
|
|
return windowUtils.SCROLL_MODE_SMOOTH;
|
|
}
|
|
|
|
collectSessionState() {
|
|
const { docShell, contentWindow } = this;
|
|
const history = lazy.SessionHistory.collect(docShell);
|
|
let formdata = SessionStoreUtils.collectFormData(contentWindow);
|
|
let scrolldata = SessionStoreUtils.collectScrollPosition(contentWindow);
|
|
|
|
// Save the current document resolution.
|
|
let zoom = 1;
|
|
const domWindowUtils = contentWindow.windowUtils;
|
|
zoom = domWindowUtils.getResolution();
|
|
scrolldata = scrolldata || {};
|
|
scrolldata.zoom = {};
|
|
scrolldata.zoom.resolution = zoom;
|
|
|
|
// Save some data that'll help in adjusting the zoom level
|
|
// when restoring in a different screen orientation.
|
|
const displaySize = {};
|
|
const width = {},
|
|
height = {};
|
|
domWindowUtils.getDocumentViewerSize(width, height);
|
|
|
|
displaySize.width = width.value;
|
|
displaySize.height = height.value;
|
|
|
|
scrolldata.zoom.displaySize = displaySize;
|
|
|
|
formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
|
|
|
|
return { history, formdata, scrolldata };
|
|
}
|
|
|
|
orientation() {
|
|
const currentOrientationType = this.contentWindow?.screen.orientation.type;
|
|
if (!currentOrientationType) {
|
|
// Unfortunately, we don't know current screen orientation.
|
|
// Return portrait as default.
|
|
return SCREEN_ORIENTATION_PORTRAIT;
|
|
}
|
|
if (currentOrientationType.startsWith("landscape")) {
|
|
return SCREEN_ORIENTATION_LANDSCAPE;
|
|
}
|
|
return SCREEN_ORIENTATION_PORTRAIT;
|
|
}
|
|
|
|
receiveMessage(message) {
|
|
const { name } = message;
|
|
debug`receiveMessage: ${name}`;
|
|
|
|
switch (name) {
|
|
case "GeckoView:DOMFullscreenEntered": {
|
|
const windowUtils = this.contentWindow?.windowUtils;
|
|
const actor =
|
|
this.contentWindow?.windowGlobalChild?.getActor("ContentDelegate");
|
|
if (!windowUtils) {
|
|
// If we are not able to enter fullscreen, tell the parent to just
|
|
// exit.
|
|
actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
|
|
break;
|
|
}
|
|
this.lastOrientation = this.orientation();
|
|
let remoteFrameBC = message.data.remoteFrameBC;
|
|
if (remoteFrameBC) {
|
|
let remoteFrame = remoteFrameBC.embedderElement;
|
|
if (!remoteFrame) {
|
|
// This could happen when the page navigate away and trigger a
|
|
// process switching during fullscreen transition, tell the parent
|
|
// to just exit.
|
|
actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
|
|
break;
|
|
}
|
|
|
|
windowUtils.remoteFrameFullscreenChanged(remoteFrame);
|
|
break;
|
|
}
|
|
|
|
if (
|
|
!windowUtils.handleFullscreenRequests() &&
|
|
!this.contentWindow?.document.fullscreenElement
|
|
) {
|
|
// If we don't actually have any pending fullscreen request
|
|
// to handle, neither we have been in fullscreen, tell the
|
|
// parent to just exit.
|
|
actor?.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
|
|
}
|
|
break;
|
|
}
|
|
case "GeckoView:DOMFullscreenExited":
|
|
// During fullscreen, window size is changed. So don't restore viewport size.
|
|
const restoreViewSize = this.orientation() == this.lastOrientation;
|
|
this.contentWindow?.windowUtils.exitFullscreen(!restoreViewSize);
|
|
break;
|
|
case "GeckoView:ZoomToInput": {
|
|
const { contentWindow } = this;
|
|
const dwu = contentWindow.windowUtils;
|
|
|
|
const zoomToFocusedInput = function () {
|
|
if (!dwu.flushApzRepaints()) {
|
|
dwu.zoomToFocusedInput();
|
|
return;
|
|
}
|
|
Services.obs.addObserver(function apzFlushDone() {
|
|
Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
|
|
dwu.zoomToFocusedInput();
|
|
}, "apz-repaints-flushed");
|
|
};
|
|
|
|
zoomToFocusedInput();
|
|
break;
|
|
}
|
|
case "RestoreSessionState": {
|
|
this.restoreSessionState(message);
|
|
break;
|
|
}
|
|
case "RestoreHistoryAndNavigate": {
|
|
const { history, switchId } = message.data;
|
|
if (history) {
|
|
lazy.SessionHistory.restore(this.docShell, history);
|
|
const historyIndex = history.requestedIndex - 1;
|
|
const webNavigation = this.docShell.QueryInterface(
|
|
Ci.nsIWebNavigation
|
|
);
|
|
|
|
if (!switchId) {
|
|
// TODO: Bug 1648158 This won't work for Fission or HistoryInParent.
|
|
webNavigation.sessionHistory.legacySHistory.reloadCurrentEntry();
|
|
} else {
|
|
webNavigation.resumeRedirectedLoad(switchId, historyIndex);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "GeckoView:UpdateInitData": {
|
|
// Provide a hook for native code to detect a transfer.
|
|
Services.obs.notifyObservers(
|
|
this.docShell,
|
|
"geckoview-content-global-transferred"
|
|
);
|
|
break;
|
|
}
|
|
case "GeckoView:ScrollBy": {
|
|
const x = {};
|
|
const y = {};
|
|
const { contentWindow } = this;
|
|
const { widthValue, widthType, heightValue, heightType, behavior } =
|
|
message.data;
|
|
contentWindow.windowUtils.getVisualViewportOffset(x, y);
|
|
contentWindow.windowUtils.scrollToVisual(
|
|
x.value + this.toPixels(widthValue, widthType),
|
|
y.value + this.toPixels(heightValue, heightType),
|
|
contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
|
|
this.toScrollBehavior(behavior)
|
|
);
|
|
break;
|
|
}
|
|
case "GeckoView:ScrollTo": {
|
|
const { contentWindow } = this;
|
|
const { widthValue, widthType, heightValue, heightType, behavior } =
|
|
message.data;
|
|
contentWindow.windowUtils.scrollToVisual(
|
|
this.toPixels(widthValue, widthType),
|
|
this.toPixels(heightValue, heightType),
|
|
contentWindow.windowUtils.UPDATE_TYPE_MAIN_THREAD,
|
|
this.toScrollBehavior(behavior)
|
|
);
|
|
break;
|
|
}
|
|
case "CollectSessionState": {
|
|
return this.collectSessionState();
|
|
}
|
|
case "ContainsFormData": {
|
|
return this.containsFormData();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async containsFormData() {
|
|
const { contentWindow } = this;
|
|
let formdata = SessionStoreUtils.collectFormData(contentWindow);
|
|
formdata = lazy.PrivacyFilter.filterFormData(formdata || {});
|
|
if (formdata) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async restoreSessionState(message) {
|
|
// Make sure we showed something before restoring scrolling and form data
|
|
await this.pageShow;
|
|
|
|
const { contentWindow } = this;
|
|
const { formdata, scrolldata } = message.data;
|
|
|
|
/**
|
|
* Restores frame tree |data|, starting at the given root |frame|. As the
|
|
* function recurses into descendant frames it will call cb(frame, data) for
|
|
* each frame it encounters, starting with the given root.
|
|
*/
|
|
function restoreFrameTreeData(frame, data, cb) {
|
|
// Restore data for the root frame.
|
|
// The callback can abort by returning false.
|
|
if (cb(frame, data) === false) {
|
|
return;
|
|
}
|
|
|
|
if (!data.hasOwnProperty("children")) {
|
|
return;
|
|
}
|
|
|
|
// Recurse into child frames.
|
|
SessionStoreUtils.forEachNonDynamicChildFrame(
|
|
frame,
|
|
(subframe, index) => {
|
|
if (data.children[index]) {
|
|
restoreFrameTreeData(subframe, data.children[index], cb);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
if (formdata) {
|
|
restoreFrameTreeData(contentWindow, formdata, (frame, data) => {
|
|
// restore() will return false, and thus abort restoration for the
|
|
// current |frame| and its descendants, if |data.url| is given but
|
|
// doesn't match the loaded document's URL.
|
|
return SessionStoreUtils.restoreFormData(frame.document, data);
|
|
});
|
|
}
|
|
|
|
if (scrolldata) {
|
|
restoreFrameTreeData(contentWindow, scrolldata, (frame, data) => {
|
|
if (data.scroll) {
|
|
SessionStoreUtils.restoreScrollPosition(frame, data);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) {
|
|
const utils = contentWindow.windowUtils;
|
|
// Restore zoom level.
|
|
utils.setRestoreResolution(
|
|
scrolldata.zoom.resolution,
|
|
scrolldata.zoom.displaySize.width,
|
|
scrolldata.zoom.displaySize.height
|
|
);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
handleEvent(aEvent) {
|
|
debug`handleEvent: ${aEvent.type}`;
|
|
|
|
switch (aEvent.type) {
|
|
case "pageshow": {
|
|
this.receivedPageShow();
|
|
break;
|
|
}
|
|
|
|
case "mozcaretstatechanged":
|
|
if (
|
|
aEvent.reason === "presscaret" ||
|
|
aEvent.reason === "releasecaret"
|
|
) {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:PinOnScreen",
|
|
pinned: aEvent.reason === "presscaret",
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const { debug, warn } = GeckoViewContentChild.initLogging("GeckoViewContent");
|