ContentDelegateChild.sys.mjs (8909B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 ManifestObtainer: "resource://gre/modules/ManifestObtainer.sys.mjs", 11 SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs", 12 SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.sys.mjs", 13 }); 14 15 const MAX_TEXT_LENGTH = 4096; 16 17 export class ContentDelegateChild extends GeckoViewActorChild { 18 notifyParentOfViewportFit() { 19 if (this.triggerViewportFitChange) { 20 this.contentWindow.cancelIdleCallback(this.triggerViewportFitChange); 21 } 22 this.triggerViewportFitChange = this.contentWindow.requestIdleCallback( 23 () => { 24 this.triggerViewportFitChange = null; 25 const viewportFit = this.contentWindow.windowUtils.getViewportFitInfo(); 26 if (this.lastViewportFit === viewportFit) { 27 return; 28 } 29 this.lastViewportFit = viewportFit; 30 this.sendAsyncMessage("GeckoView:DOMMetaViewportFit", viewportFit); 31 } 32 ); 33 } 34 35 #getSelection(aElement, aEditFlags) { 36 if (aEditFlags & lazy.SpellCheckHelper.TEXTINPUT) { 37 return aElement?.editor?.selection; 38 } 39 40 return this.contentWindow.getSelection(); 41 } 42 43 #getSelectionBoundingRect(aFocusedElement, aEditFlags) { 44 const selection = this.#getSelection(aFocusedElement, aEditFlags); 45 if (!selection || selection.isCollapsed || selection.rangeCount != 1) { 46 return null; 47 } 48 const range = selection.getRangeAt(0); 49 return range.getBoundingClientRect(); 50 } 51 52 /** 53 * Show action menu if contextmenu is by mouse and we have selected text 54 */ 55 #showActionMenu(aEvent) { 56 if (aEvent.pointerType !== "mouse") { 57 return false; 58 } 59 60 const win = this.contentWindow; 61 const selectionInfo = lazy.SelectionUtils.getSelectionDetails(win); 62 if (!selectionInfo.text.length) { 63 // Don't show action menu by contextmenu event if no selection 64 return false; 65 } 66 67 // The selection isn't collapsed and has a text. We try to show action menu 68 const focusedElement = 69 Services.focus.focusedElement || aEvent.composedTarget; 70 71 const editFlags = lazy.SpellCheckHelper.isEditable(focusedElement, win); 72 const selectionEditable = !!( 73 editFlags & 74 (lazy.SpellCheckHelper.EDITABLE | lazy.SpellCheckHelper.CONTENTEDITABLE) 75 ); 76 const boundingClientRect = this.#getSelectionBoundingRect( 77 focusedElement, 78 editFlags 79 ); 80 81 const caretEvent = new CaretStateChangedEvent("mozcaretstatechanged", { 82 bubbles: true, 83 collapsed: selectionInfo.docSelectionIsCollapsed, 84 boundingClientRect, 85 reason: "taponcaret", 86 caretVisible: true, 87 selectionVisible: true, 88 selectionEditable, 89 selectedTextContent: selectionInfo.text, 90 }); 91 92 win.dispatchEvent(caretEvent); 93 94 // If selection is changed, or focus is changed, dismiss action menu 95 const eventTarget = (() => { 96 if (editFlags & lazy.SpellCheckHelper.TEXTINPUT) { 97 return focusedElement; 98 } 99 return this.contentWindow.document; 100 })(); 101 102 function dismissHandler() { 103 const dismissEvent = new CaretStateChangedEvent("mozcaretstatechanged", { 104 bubbles: true, 105 reason: "visibilitychange", 106 }); 107 win.dispatchEvent(dismissEvent); 108 109 eventTarget.removeEventListener("selectionchange", dismissHandler); 110 win.removeEventListener("focusin", dismissHandler); 111 win.removeEventListener("focusout", dismissHandler); 112 win.removeEventListener("blur", dismissHandler); 113 } 114 115 eventTarget.addEventListener("selectionchange", dismissHandler); 116 win.addEventListener("focusin", dismissHandler); 117 win.addEventListener("focusout", dismissHandler); 118 win.addEventListener("blur", dismissHandler); 119 120 return true; 121 } 122 123 // eslint-disable-next-line complexity 124 handleEvent(aEvent) { 125 debug`handleEvent: ${aEvent.type}`; 126 127 switch (aEvent.type) { 128 case "contextmenu": { 129 if (aEvent.defaultPrevented) { 130 return; 131 } 132 133 if (this.#showActionMenu(aEvent)) { 134 aEvent.preventDefault(); 135 return; 136 } 137 138 function nearestParentAttribute(aNode, aAttribute) { 139 while ( 140 aNode && 141 aNode.hasAttribute && 142 !aNode.hasAttribute(aAttribute) 143 ) { 144 aNode = aNode.parentNode; 145 } 146 return aNode && aNode.getAttribute && aNode.getAttribute(aAttribute); 147 } 148 149 function createAbsoluteUri(aBaseUri, aUri) { 150 if (!aUri || !aBaseUri || !aBaseUri.displaySpec) { 151 return null; 152 } 153 return Services.io.newURI(aUri, null, aBaseUri).displaySpec; 154 } 155 156 const node = aEvent.composedTarget; 157 const baseUri = node.ownerDocument.baseURIObject; 158 const uri = createAbsoluteUri( 159 baseUri, 160 nearestParentAttribute(node, "href") 161 ); 162 const title = nearestParentAttribute(node, "title"); 163 const alt = nearestParentAttribute(node, "alt"); 164 const elementType = ChromeUtils.getClassName(node); 165 const isImage = elementType === "HTMLImageElement"; 166 const isMedia = 167 elementType === "HTMLVideoElement" || 168 elementType === "HTMLAudioElement"; 169 let elementSrc = (isImage || isMedia) && (node.currentSrc || node.src); 170 if (elementSrc) { 171 const isBlob = elementSrc.startsWith("blob:"); 172 if (isBlob && !URL.isBoundToBlob(elementSrc)) { 173 elementSrc = null; 174 } 175 } 176 177 if (uri || isImage || isMedia) { 178 const msg = { 179 // We don't have full zoom on Android, so using CSS coordinates 180 // here is fine, since the CSS coordinate spaces match between the 181 // child and parent processes. 182 // 183 // TODO(m_kato): 184 // title, alt and textContent should consider surrogate pair and grapheme cluster? 185 screenX: aEvent.screenX, 186 screenY: aEvent.screenY, 187 baseUri: (baseUri && baseUri.displaySpec) || null, 188 uri, 189 title: (title && title.substring(0, MAX_TEXT_LENGTH)) || null, 190 alt: (alt && alt.substring(0, MAX_TEXT_LENGTH)) || null, 191 elementType, 192 elementSrc: elementSrc || null, 193 textContent: 194 (node.textContent && 195 node.textContent.substring(0, MAX_TEXT_LENGTH)) || 196 null, 197 linkText: 198 (node.innerText && 199 node.innerText.substring(0, MAX_TEXT_LENGTH)) || 200 null, 201 }; 202 203 this.sendAsyncMessage("GeckoView:ContextMenu", msg); 204 aEvent.preventDefault(); 205 } 206 break; 207 } 208 case "MozDOMFullscreen:Request": { 209 this.sendAsyncMessage("GeckoView:DOMFullscreenRequest"); 210 break; 211 } 212 case "MozDOMFullscreen:Entered": 213 case "MozDOMFullscreen:Exited": 214 // Content may change fullscreen state by itself, and we should ensure 215 // that the parent always exits fullscreen when content has left 216 // full screen mode. 217 if (this.contentWindow?.document.fullscreenElement) { 218 break; 219 } 220 // fall-through 221 case "MozDOMFullscreen:Exit": 222 this.sendAsyncMessage("GeckoView:DOMFullscreenExit"); 223 break; 224 case "DOMMetaViewportFitChanged": 225 if (aEvent.originalTarget.ownerGlobal == this.contentWindow) { 226 this.notifyParentOfViewportFit(); 227 } 228 break; 229 case "DOMContentLoaded": { 230 if (aEvent.originalTarget.ownerGlobal == this.contentWindow) { 231 // If loaded content doesn't have viewport-fit, parent still 232 // uses old value of previous content. 233 this.notifyParentOfViewportFit(); 234 } 235 if (this.contentWindow !== this.contentWindow?.top) { 236 // Only check WebApp manifest on the top level window. 237 return; 238 } 239 this.contentWindow.requestIdleCallback(async () => { 240 const manifest = await lazy.ManifestObtainer.contentObtainManifest( 241 this.contentWindow 242 ); 243 if (manifest) { 244 this.sendAsyncMessage("GeckoView:WebAppManifest", manifest); 245 } 246 }); 247 break; 248 } 249 case "MozFirstContentfulPaint": { 250 this.sendAsyncMessage("GeckoView:FirstContentfulPaint"); 251 break; 252 } 253 case "MozPaintStatusReset": { 254 this.sendAsyncMessage("GeckoView:PaintStatusReset"); 255 break; 256 } 257 } 258 } 259 } 260 261 const { debug, warn } = ContentDelegateChild.initLogging( 262 "ContentDelegateChild" 263 );