/* 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: () => this.docShell.doCommand("cmd_moveLeft"),
},
{
id: "org.mozilla.geckoview.COLLAPSE_TO_END",
predicate: e => !e.collapsed && e.selectionEditable,
perform: () => this.docShell.doCommand("cmd_moveRight"),
},
{
id: "org.mozilla.geckoview.UNSELECT",
predicate: e => !e.collapsed && !e.selectionEditable,
perform: () => 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: () => 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