/* 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, { LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", }); const MAGNIFIER_PREF = "layout.accessiblecaret.magnifier.enabled"; const ACCESSIBLECARET_HEIGHT_PREF = "layout.accessiblecaret.height"; const PREFS = [MAGNIFIER_PREF, ACCESSIBLECARET_HEIGHT_PREF]; // Dispatches GeckoView:ShowSelectionAction and GeckoView:HideSelectionAction to // the GeckoSession on accessible caret changes. export class SelectionActionDelegateChild extends GeckoViewActorChild { constructor(aModuleName, aMessageManager) { super(aModuleName, aMessageManager); this._actionCallback = () => {}; this._isActive = false; this._previousMessage = ""; // Bug 1570744 - JSWindowActorChild's cannot be used as nsIObserver's // directly, so we create a new function here instead to act as our // nsIObserver, which forwards the notification to the observe method. this._observerFunction = (subject, topic, data) => { this.observe(subject, topic, data); }; for (const pref of PREFS) { Services.prefs.addObserver(pref, this._observerFunction); } this._magnifierEnabled = Services.prefs.getBoolPref(MAGNIFIER_PREF); this._accessiblecaretHeight = parseFloat( Services.prefs.getCharPref(ACCESSIBLECARET_HEIGHT_PREF, "0") ); } didDestroy() { for (const pref of PREFS) { Services.prefs.removeObserver(pref, this._observerFunction); } } _actions = [ { id: "org.mozilla.geckoview.HIDE", predicate: _ => true, perform: _ => this.handleEvent({ type: "pagehide" }), }, { id: "org.mozilla.geckoview.CUT", predicate: e => !e.collapsed && e.selectionEditable && !this._isPasswordField(e), perform: _ => this.docShell.doCommand("cmd_cut"), }, { id: "org.mozilla.geckoview.COPY", predicate: e => !e.collapsed && !this._isPasswordField(e), perform: _ => this.docShell.doCommand("cmd_copy"), }, { id: "org.mozilla.geckoview.PASTE", predicate: e => (this._isContentHtmlEditable(e) && Services.clipboard.hasDataMatchingFlavors( /* The following image types are considered by editor */ ["image/gif", "image/jpeg", "image/png"], Ci.nsIClipboard.kGlobalClipboard )) || (e.selectionEditable && Services.clipboard.hasDataMatchingFlavors( ["text/plain"], Ci.nsIClipboard.kGlobalClipboard )), perform: _ => this._performPaste(), }, { id: "org.mozilla.geckoview.PASTE_AS_PLAIN_TEXT", predicate: e => this._isContentHtmlEditable(e) && Services.clipboard.hasDataMatchingFlavors( ["text/html"], Ci.nsIClipboard.kGlobalClipboard ), perform: _ => this._performPasteAsPlainText(), }, { id: "org.mozilla.geckoview.DELETE", predicate: e => !e.collapsed && e.selectionEditable, perform: _ => this.docShell.doCommand("cmd_delete"), }, { id: "org.mozilla.geckoview.COLLAPSE_TO_START", predicate: e => !e.collapsed && e.selectionEditable, perform: e => this.docShell.doCommand("cmd_moveLeft"), }, { id: "org.mozilla.geckoview.COLLAPSE_TO_END", predicate: e => !e.collapsed && e.selectionEditable, perform: e => this.docShell.doCommand("cmd_moveRight"), }, { id: "org.mozilla.geckoview.UNSELECT", predicate: e => !e.collapsed && !e.selectionEditable, perform: e => this.docShell.doCommand("cmd_selectNone"), }, { id: "org.mozilla.geckoview.SELECT_ALL", predicate: e => { if (e.reason === "longpressonemptycontent") { return false; } // When on design mode, focusedElement will be null. const element = Services.focus.focusedElement || e.target?.activeElement; if (e.selectionEditable && e.target && element) { let value = ""; if (element.value) { value = element.value; } else if ( element.isContentEditable || e.target.designMode === "on" ) { value = element.innerText; } // Do not show SELECT_ALL if the editable is empty // or all the editable text is already selected. return value !== "" && value !== e.selectedTextContent; } return true; }, perform: e => this.docShell.doCommand("cmd_selectAll"), }, ]; receiveMessage({ name, data }) { debug`receiveMessage ${name}`; switch (name) { case "ExecuteSelectionAction": { this._actionCallback(data); } } } _performPaste() { this.handleEvent({ type: "pagehide" }); this.docShell.doCommand("cmd_paste"); } _performPasteAsPlainText() { this.handleEvent({ type: "pagehide" }); this.docShell.doCommand("cmd_pasteNoFormatting"); } _isPasswordField(aEvent) { if (!aEvent.selectionEditable) { return false; } const win = aEvent.target.defaultView; const focus = aEvent.target.activeElement; return ( win && win.HTMLInputElement && win.HTMLInputElement.isInstance(focus) && !focus.mozIsTextField(/* excludePassword */ true) ); } _isContentHtmlEditable(aEvent) { if (!aEvent.selectionEditable) { return false; } if (aEvent.target.designMode == "on") { return true; } // focused element isn't nor