270 lines
8.8 KiB
JavaScript
270 lines
8.8 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";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ManifestObtainer: "resource://gre/modules/ManifestObtainer.sys.mjs",
|
|
SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs",
|
|
SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.sys.mjs",
|
|
});
|
|
|
|
const MAX_TEXT_LENGTH = 4096;
|
|
|
|
export class ContentDelegateChild extends GeckoViewActorChild {
|
|
notifyParentOfViewportFit() {
|
|
if (this.triggerViewportFitChange) {
|
|
this.contentWindow.cancelIdleCallback(this.triggerViewportFitChange);
|
|
}
|
|
this.triggerViewportFitChange = this.contentWindow.requestIdleCallback(
|
|
() => {
|
|
this.triggerViewportFitChange = null;
|
|
const viewportFit = this.contentWindow.windowUtils.getViewportFitInfo();
|
|
if (this.lastViewportFit === viewportFit) {
|
|
return;
|
|
}
|
|
this.lastViewportFit = viewportFit;
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:DOMMetaViewportFit",
|
|
viewportfit: viewportFit,
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
#getSelection(aElement, aEditFlags) {
|
|
if (aEditFlags & lazy.SpellCheckHelper.TEXTINPUT) {
|
|
return aElement?.editor?.selection;
|
|
}
|
|
|
|
return this.contentWindow.getSelection();
|
|
}
|
|
|
|
#getSelectionBoundingRect(aFocusedElement, aEditFlags) {
|
|
const selection = this.#getSelection(aFocusedElement, aEditFlags);
|
|
if (!selection || selection.isCollapsed || selection.rangeCount != 1) {
|
|
return null;
|
|
}
|
|
const range = selection.getRangeAt(0);
|
|
return range.getBoundingClientRect();
|
|
}
|
|
|
|
/**
|
|
* Show action menu if contextmenu is by mouse and we have selected text
|
|
*/
|
|
#showActionMenu(aEvent) {
|
|
if (aEvent.pointerType !== "mouse") {
|
|
return false;
|
|
}
|
|
|
|
const win = this.contentWindow;
|
|
const selectionInfo = lazy.SelectionUtils.getSelectionDetails(win);
|
|
if (!selectionInfo.text.length) {
|
|
// Don't show action menu by contextmenu event if no selection
|
|
return false;
|
|
}
|
|
|
|
// The selection isn't collapsed and has a text. We try to show action menu
|
|
const focusedElement =
|
|
Services.focus.focusedElement || aEvent.composedTarget;
|
|
|
|
const editFlags = lazy.SpellCheckHelper.isEditable(focusedElement, win);
|
|
const selectionEditable = !!(
|
|
editFlags &
|
|
(lazy.SpellCheckHelper.EDITABLE | lazy.SpellCheckHelper.CONTENTEDITABLE)
|
|
);
|
|
const boundingClientRect = this.#getSelectionBoundingRect(
|
|
focusedElement,
|
|
editFlags
|
|
);
|
|
|
|
const caretEvent = new CaretStateChangedEvent("mozcaretstatechanged", {
|
|
bubbles: true,
|
|
collapsed: selectionInfo.docSelectionIsCollapsed,
|
|
boundingClientRect,
|
|
reason: "taponcaret",
|
|
caretVisible: true,
|
|
selectionVisible: true,
|
|
selectionEditable,
|
|
selectedTextContent: selectionInfo.text,
|
|
});
|
|
|
|
win.dispatchEvent(caretEvent);
|
|
|
|
// If selection is changed, or focus is changed, dismiss action menu
|
|
const eventTarget = (() => {
|
|
if (editFlags & lazy.SpellCheckHelper.TEXTINPUT) {
|
|
return focusedElement;
|
|
}
|
|
return this.contentWindow.document;
|
|
})();
|
|
|
|
function dismissHandler() {
|
|
const dismissEvent = new CaretStateChangedEvent("mozcaretstatechanged", {
|
|
bubbles: true,
|
|
reason: "visibilitychange",
|
|
});
|
|
win.dispatchEvent(dismissEvent);
|
|
|
|
eventTarget.removeEventListener("selectionchange", dismissHandler);
|
|
win.removeEventListener("focusin", dismissHandler);
|
|
win.removeEventListener("focusout", dismissHandler);
|
|
win.removeEventListener("blur", dismissHandler);
|
|
}
|
|
|
|
eventTarget.addEventListener("selectionchange", dismissHandler);
|
|
win.addEventListener("focusin", dismissHandler);
|
|
win.addEventListener("focusout", dismissHandler);
|
|
win.addEventListener("blur", dismissHandler);
|
|
|
|
return true;
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
handleEvent(aEvent) {
|
|
debug`handleEvent: ${aEvent.type}`;
|
|
|
|
switch (aEvent.type) {
|
|
case "contextmenu": {
|
|
if (aEvent.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
if (this.#showActionMenu(aEvent)) {
|
|
aEvent.preventDefault();
|
|
return;
|
|
}
|
|
|
|
function nearestParentAttribute(aNode, aAttribute) {
|
|
while (
|
|
aNode &&
|
|
aNode.hasAttribute &&
|
|
!aNode.hasAttribute(aAttribute)
|
|
) {
|
|
aNode = aNode.parentNode;
|
|
}
|
|
return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute);
|
|
}
|
|
|
|
function createAbsoluteUri(aBaseUri, aUri) {
|
|
if (!aUri || !aBaseUri || !aBaseUri.displaySpec) {
|
|
return null;
|
|
}
|
|
return Services.io.newURI(aUri, null, aBaseUri).displaySpec;
|
|
}
|
|
|
|
const node = aEvent.composedTarget;
|
|
const baseUri = node.ownerDocument.baseURIObject;
|
|
const uri = createAbsoluteUri(
|
|
baseUri,
|
|
nearestParentAttribute(node, "href")
|
|
);
|
|
const title = nearestParentAttribute(node, "title");
|
|
const alt = nearestParentAttribute(node, "alt");
|
|
const elementType = ChromeUtils.getClassName(node);
|
|
const isImage = elementType === "HTMLImageElement";
|
|
const isMedia =
|
|
elementType === "HTMLVideoElement" ||
|
|
elementType === "HTMLAudioElement";
|
|
let elementSrc = (isImage || isMedia) && (node.currentSrc || node.src);
|
|
if (elementSrc) {
|
|
const isBlob = elementSrc.startsWith("blob:");
|
|
if (isBlob && !URL.isBoundToBlob(elementSrc)) {
|
|
elementSrc = null;
|
|
}
|
|
}
|
|
|
|
if (uri || isImage || isMedia) {
|
|
const msg = {
|
|
type: "GeckoView:ContextMenu",
|
|
// We don't have full zoom on Android, so using CSS coordinates
|
|
// here is fine, since the CSS coordinate spaces match between the
|
|
// child and parent processes.
|
|
//
|
|
// TODO(m_kato):
|
|
// title, alt and textContent should consider surrogate pair and grapheme cluster?
|
|
screenX: aEvent.screenX,
|
|
screenY: aEvent.screenY,
|
|
baseUri: (baseUri && baseUri.displaySpec) || null,
|
|
uri,
|
|
title: (title && title.substring(0, MAX_TEXT_LENGTH)) || null,
|
|
alt: (alt && alt.substring(0, MAX_TEXT_LENGTH)) || null,
|
|
elementType,
|
|
elementSrc: elementSrc || null,
|
|
textContent:
|
|
(node.textContent &&
|
|
node.textContent.substring(0, MAX_TEXT_LENGTH)) ||
|
|
null,
|
|
};
|
|
|
|
this.eventDispatcher.sendRequest(msg);
|
|
aEvent.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
case "MozDOMFullscreen:Request": {
|
|
this.sendAsyncMessage("GeckoView:DOMFullscreenRequest", {});
|
|
break;
|
|
}
|
|
case "MozDOMFullscreen:Entered":
|
|
case "MozDOMFullscreen:Exited":
|
|
// Content may change fullscreen state by itself, and we should ensure
|
|
// that the parent always exits fullscreen when content has left
|
|
// full screen mode.
|
|
if (this.contentWindow?.document.fullscreenElement) {
|
|
break;
|
|
}
|
|
// fall-through
|
|
case "MozDOMFullscreen:Exit":
|
|
this.sendAsyncMessage("GeckoView:DOMFullscreenExit", {});
|
|
break;
|
|
case "DOMMetaViewportFitChanged":
|
|
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
|
|
this.notifyParentOfViewportFit();
|
|
}
|
|
break;
|
|
case "DOMContentLoaded": {
|
|
if (aEvent.originalTarget.ownerGlobal == this.contentWindow) {
|
|
// If loaded content doesn't have viewport-fit, parent still
|
|
// uses old value of previous content.
|
|
this.notifyParentOfViewportFit();
|
|
}
|
|
if (this.contentWindow !== this.contentWindow?.top) {
|
|
// Only check WebApp manifest on the top level window.
|
|
return;
|
|
}
|
|
this.contentWindow.requestIdleCallback(async () => {
|
|
const manifest = await lazy.ManifestObtainer.contentObtainManifest(
|
|
this.contentWindow
|
|
);
|
|
if (manifest) {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:WebAppManifest",
|
|
manifest,
|
|
});
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case "MozFirstContentfulPaint": {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:FirstContentfulPaint",
|
|
});
|
|
break;
|
|
}
|
|
case "MozPaintStatusReset": {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:PaintStatusReset",
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const { debug, warn } = ContentDelegateChild.initLogging(
|
|
"ContentDelegateChild"
|
|
);
|