796 lines
24 KiB
JavaScript
796 lines
24 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 { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
|
|
ShoppingProduct: "chrome://global/content/shopping/ShoppingProduct.mjs",
|
|
});
|
|
|
|
export class GeckoViewContent extends GeckoViewModule {
|
|
onInit() {
|
|
this.registerListener([
|
|
"GeckoViewContent:ExitFullScreen",
|
|
"GeckoView:ClearMatches",
|
|
"GeckoView:DisplayMatches",
|
|
"GeckoView:FindInPage",
|
|
"GeckoView:HasCookieBannerRuleForBrowsingContextTree",
|
|
"GeckoView:RestoreState",
|
|
"GeckoView:ContainsFormData",
|
|
"GeckoView:RequestCreateAnalysis",
|
|
"GeckoView:RequestAnalysisStatus",
|
|
"GeckoView:RequestAnalysisCreationStatus",
|
|
"GeckoView:PollForAnalysisCompleted",
|
|
"GeckoView:SendClickAttributionEvent",
|
|
"GeckoView:SendImpressionAttributionEvent",
|
|
"GeckoView:SendPlacementAttributionEvent",
|
|
"GeckoView:RequestAnalysis",
|
|
"GeckoView:RequestRecommendations",
|
|
"GeckoView:ReportBackInStock",
|
|
"GeckoView:ScrollBy",
|
|
"GeckoView:ScrollTo",
|
|
"GeckoView:SetActive",
|
|
"GeckoView:SetFocused",
|
|
"GeckoView:SetPriorityHint",
|
|
"GeckoView:UpdateInitData",
|
|
"GeckoView:ZoomToInput",
|
|
"GeckoView:IsPdfJs",
|
|
]);
|
|
}
|
|
|
|
onEnable() {
|
|
this.window.addEventListener(
|
|
"MozDOMFullscreen:Entered",
|
|
this,
|
|
/* capture */ true,
|
|
/* untrusted */ false
|
|
);
|
|
this.window.addEventListener(
|
|
"MozDOMFullscreen:Exited",
|
|
this,
|
|
/* capture */ true,
|
|
/* untrusted */ false
|
|
);
|
|
this.window.addEventListener(
|
|
"framefocusrequested",
|
|
this,
|
|
/* capture */ true,
|
|
/* untrusted */ false
|
|
);
|
|
|
|
this.window.addEventListener("DOMWindowClose", this);
|
|
this.window.addEventListener("pagetitlechanged", this);
|
|
this.window.addEventListener("pageinfo", this);
|
|
|
|
this.window.addEventListener("cookiebannerdetected", this);
|
|
this.window.addEventListener("cookiebannerhandled", this);
|
|
|
|
Services.obs.addObserver(this, "oop-frameloader-crashed");
|
|
Services.obs.addObserver(this, "ipc:content-shutdown");
|
|
}
|
|
|
|
onDisable() {
|
|
this.window.removeEventListener(
|
|
"MozDOMFullscreen:Entered",
|
|
this,
|
|
/* capture */ true
|
|
);
|
|
this.window.removeEventListener(
|
|
"MozDOMFullscreen:Exited",
|
|
this,
|
|
/* capture */ true
|
|
);
|
|
this.window.removeEventListener(
|
|
"framefocusrequested",
|
|
this,
|
|
/* capture */ true
|
|
);
|
|
|
|
this.window.removeEventListener("DOMWindowClose", this);
|
|
this.window.removeEventListener("pagetitlechanged", this);
|
|
this.window.removeEventListener("pageinfo", this);
|
|
|
|
this.window.removeEventListener("cookiebannerdetected", this);
|
|
this.window.removeEventListener("cookiebannerhandled", this);
|
|
|
|
Services.obs.removeObserver(this, "oop-frameloader-crashed");
|
|
Services.obs.removeObserver(this, "ipc:content-shutdown");
|
|
}
|
|
|
|
get actor() {
|
|
return this.getActor("GeckoViewContent");
|
|
}
|
|
|
|
get isPdfJs() {
|
|
return (
|
|
this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html"
|
|
);
|
|
}
|
|
|
|
// Goes up the browsingContext chain and sends the message every time
|
|
// we cross the process boundary so that every process in the chain is
|
|
// notified.
|
|
sendToAllChildren(aEvent, aData) {
|
|
let { browsingContext } = this.actor;
|
|
|
|
while (browsingContext) {
|
|
if (!browsingContext.currentWindowGlobal) {
|
|
break;
|
|
}
|
|
|
|
const currentPid = browsingContext.currentWindowGlobal.osPid;
|
|
const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
|
|
|
|
if (currentPid != parentPid) {
|
|
const actor =
|
|
browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
|
|
actor.sendAsyncMessage(aEvent, aData);
|
|
}
|
|
|
|
browsingContext = browsingContext.parent;
|
|
}
|
|
}
|
|
|
|
#sendDOMFullScreenEventToAllChildren(aEvent) {
|
|
let { browsingContext } = this.actor;
|
|
|
|
while (browsingContext) {
|
|
if (!browsingContext.currentWindowGlobal) {
|
|
break;
|
|
}
|
|
|
|
const currentPid = browsingContext.currentWindowGlobal.osPid;
|
|
const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
|
|
|
|
if (currentPid != parentPid) {
|
|
if (!browsingContext.parent) {
|
|
// Top level browsing context. Use origin actor (Bug 1505916).
|
|
const chromeBC = browsingContext.topChromeWindow?.browsingContext;
|
|
const requestOrigin = chromeBC?.fullscreenRequestOrigin?.get();
|
|
if (requestOrigin) {
|
|
requestOrigin.browsingContext.currentWindowGlobal
|
|
.getActor("GeckoViewContent")
|
|
.sendAsyncMessage(aEvent, {});
|
|
delete chromeBC.fullscreenRequestOrigin;
|
|
return;
|
|
}
|
|
}
|
|
const actor =
|
|
browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
|
|
actor.sendAsyncMessage(aEvent, {});
|
|
}
|
|
|
|
browsingContext = browsingContext.parent;
|
|
}
|
|
}
|
|
|
|
// Bundle event handler.
|
|
onEvent(aEvent, aData, aCallback) {
|
|
debug`onEvent: event=${aEvent}, data=${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoViewContent:ExitFullScreen":
|
|
this.browser.ownerDocument.exitFullscreen();
|
|
break;
|
|
case "GeckoView:ClearMatches": {
|
|
if (!this.isPdfJs) {
|
|
this._clearMatches();
|
|
}
|
|
break;
|
|
}
|
|
case "GeckoView:DisplayMatches": {
|
|
if (!this.isPdfJs) {
|
|
this._displayMatches(aData);
|
|
}
|
|
break;
|
|
}
|
|
case "GeckoView:FindInPage": {
|
|
if (!this.isPdfJs) {
|
|
this._findInPage(aData, aCallback);
|
|
}
|
|
break;
|
|
}
|
|
case "GeckoView:ZoomToInput": {
|
|
const sendZoomToFocusedInputMessage = function () {
|
|
// For ZoomToInput we just need to send the message to the current focused one.
|
|
const actor =
|
|
Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor(
|
|
"GeckoViewContent"
|
|
);
|
|
|
|
actor.sendAsyncMessage(aEvent, aData);
|
|
};
|
|
|
|
const { force } = aData;
|
|
let gotResize = false;
|
|
const onResize = function () {
|
|
gotResize = true;
|
|
if (this.window.windowUtils.isMozAfterPaintPending) {
|
|
this.window.addEventListener(
|
|
"MozAfterPaint",
|
|
() => sendZoomToFocusedInputMessage(),
|
|
{ capture: true, once: true }
|
|
);
|
|
} else {
|
|
sendZoomToFocusedInputMessage();
|
|
}
|
|
};
|
|
|
|
this.window.addEventListener("resize", onResize, { capture: true });
|
|
|
|
// When the keyboard is displayed, we can get one resize event,
|
|
// multiple resize events, or none at all. Try to handle all these
|
|
// cases by allowing resizing within a set interval, and still zoom to
|
|
// input if there is no resize event at the end of the interval.
|
|
this.window.setTimeout(() => {
|
|
this.window.removeEventListener("resize", onResize, {
|
|
capture: true,
|
|
});
|
|
if (!gotResize && force) {
|
|
onResize();
|
|
}
|
|
}, 500);
|
|
break;
|
|
}
|
|
case "GeckoView:ScrollBy":
|
|
// Unclear if that actually works with oop iframes?
|
|
this.sendToAllChildren(aEvent, aData);
|
|
break;
|
|
case "GeckoView:ScrollTo":
|
|
// Unclear if that actually works with oop iframes?
|
|
this.sendToAllChildren(aEvent, aData);
|
|
break;
|
|
case "GeckoView:UpdateInitData":
|
|
this.sendToAllChildren(aEvent, aData);
|
|
break;
|
|
case "GeckoView:SetActive":
|
|
this.browser.docShellIsActive = !!aData.active;
|
|
break;
|
|
case "GeckoView:SetFocused":
|
|
if (aData.focused) {
|
|
this.browser.focus();
|
|
this.browser.setAttribute("primary", "true");
|
|
} else {
|
|
this.browser.removeAttribute("primary");
|
|
this.browser.blur();
|
|
}
|
|
break;
|
|
case "GeckoView:SetPriorityHint":
|
|
if (this.browser.isRemoteBrowser) {
|
|
const remoteTab = this.browser.frameLoader?.remoteTab;
|
|
if (remoteTab) {
|
|
remoteTab.priorityHint = aData.priorityHint;
|
|
}
|
|
}
|
|
break;
|
|
case "GeckoView:RestoreState":
|
|
this.actor.restoreState(aData);
|
|
break;
|
|
case "GeckoView:ContainsFormData":
|
|
this._containsFormData(aCallback);
|
|
break;
|
|
case "GeckoView:RequestAnalysis":
|
|
this._requestAnalysis(aData, aCallback);
|
|
break;
|
|
case "GeckoView:RequestCreateAnalysis":
|
|
this._requestCreateAnalysis(aData, aCallback);
|
|
break;
|
|
case "GeckoView:RequestAnalysisStatus":
|
|
this._requestAnalysisStatus(aData, aCallback);
|
|
break;
|
|
case "GeckoView:RequestAnalysisCreationStatus":
|
|
this._requestAnalysisCreationStatus(aData, aCallback);
|
|
break;
|
|
case "GeckoView:PollForAnalysisCompleted":
|
|
this._pollForAnalysisCompleted(aData, aCallback);
|
|
break;
|
|
case "GeckoView:SendClickAttributionEvent":
|
|
this._sendAttributionEvent("click", aData, aCallback);
|
|
break;
|
|
case "GeckoView:SendImpressionAttributionEvent":
|
|
this._sendAttributionEvent("impression", aData, aCallback);
|
|
break;
|
|
case "GeckoView:SendPlacementAttributionEvent":
|
|
this._sendAttributionEvent("placement", aData, aCallback);
|
|
break;
|
|
case "GeckoView:RequestRecommendations":
|
|
this._requestRecommendations(aData, aCallback);
|
|
break;
|
|
case "GeckoView:ReportBackInStock":
|
|
this._reportBackInStock(aData, aCallback);
|
|
break;
|
|
case "GeckoView:IsPdfJs":
|
|
aCallback.onSuccess(this.isPdfJs);
|
|
break;
|
|
case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
|
|
this._hasCookieBannerRuleForBrowsingContextTree(aCallback);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// DOM event handler
|
|
handleEvent(aEvent) {
|
|
debug`handleEvent: ${aEvent.type}`;
|
|
|
|
switch (aEvent.type) {
|
|
case "framefocusrequested":
|
|
if (this.browser != aEvent.target) {
|
|
return;
|
|
}
|
|
if (this.browser.hasAttribute("primary")) {
|
|
return;
|
|
}
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:FocusRequest",
|
|
});
|
|
aEvent.preventDefault();
|
|
break;
|
|
case "MozDOMFullscreen:Entered":
|
|
if (this.browser == aEvent.target) {
|
|
// Remote browser; dispatch to content process.
|
|
this.#sendDOMFullScreenEventToAllChildren(
|
|
"GeckoView:DOMFullscreenEntered"
|
|
);
|
|
}
|
|
break;
|
|
case "MozDOMFullscreen:Exited":
|
|
this.#sendDOMFullScreenEventToAllChildren(
|
|
"GeckoView:DOMFullscreenExited"
|
|
);
|
|
break;
|
|
case "pagetitlechanged":
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:PageTitleChanged",
|
|
title: this.browser.contentTitle,
|
|
});
|
|
break;
|
|
case "DOMWindowClose":
|
|
// We need this because we want to allow the app
|
|
// to close the window itself. If we don't preventDefault()
|
|
// here Gecko will close it immediately.
|
|
aEvent.preventDefault();
|
|
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:DOMWindowClose",
|
|
});
|
|
break;
|
|
case "pageinfo":
|
|
if (aEvent.detail.previewImageURL) {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:PreviewImage",
|
|
previewImageUrl: aEvent.detail.previewImageURL,
|
|
});
|
|
}
|
|
break;
|
|
case "cookiebannerdetected":
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:CookieBannerEvent:Detected",
|
|
});
|
|
break;
|
|
case "cookiebannerhandled":
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:CookieBannerEvent:Handled",
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// nsIObserver event handler
|
|
observe(aSubject, aTopic) {
|
|
debug`observe: ${aTopic}`;
|
|
this._contentCrashed = false;
|
|
const browser = aSubject.ownerElement;
|
|
|
|
switch (aTopic) {
|
|
case "oop-frameloader-crashed": {
|
|
if (!browser || browser != this.browser) {
|
|
return;
|
|
}
|
|
this.window.setTimeout(() => {
|
|
if (this._contentCrashed) {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:ContentCrash",
|
|
});
|
|
} else {
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:ContentKill",
|
|
});
|
|
}
|
|
}, 250);
|
|
break;
|
|
}
|
|
case "ipc:content-shutdown": {
|
|
aSubject.QueryInterface(Ci.nsIPropertyBag2);
|
|
if (aSubject.get("dumpID")) {
|
|
if (
|
|
browser &&
|
|
aSubject.get("childID") != browser.frameLoader.childID
|
|
) {
|
|
return;
|
|
}
|
|
this._contentCrashed = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async _containsFormData(aCallback) {
|
|
aCallback.onSuccess(await this.actor.containsFormData());
|
|
}
|
|
|
|
async _requestAnalysis(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const analysis = {
|
|
analysis_url: "https://www.example.com/mock_analysis_url",
|
|
product_id: "ABCDEFG123",
|
|
grade: "B",
|
|
adjusted_rating: 4.5,
|
|
needs_analysis: true,
|
|
page_not_supported: true,
|
|
not_enough_reviews: true,
|
|
highlights: null,
|
|
last_analysis_time: 12345,
|
|
deleted_product_reported: true,
|
|
deleted_product: true,
|
|
};
|
|
aCallback.onSuccess({ analysis });
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(`Cannot requestAnalysis on a non-product url.`);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const analysis = await product.requestAnalysis();
|
|
if (!analysis) {
|
|
aCallback.onError(`Product analysis returned null.`);
|
|
return;
|
|
}
|
|
aCallback.onSuccess({ analysis });
|
|
}
|
|
}
|
|
|
|
async _requestCreateAnalysis(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const status = "pending";
|
|
aCallback.onSuccess(status);
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(`Cannot requestCreateAnalysis on a non-product url.`);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const status = await product.requestCreateAnalysis();
|
|
if (!status) {
|
|
aCallback.onError(`Creation of product analysis returned null.`);
|
|
return;
|
|
}
|
|
aCallback.onSuccess(status.status);
|
|
}
|
|
}
|
|
|
|
async _requestAnalysisCreationStatus(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const status = "in_progress";
|
|
aCallback.onSuccess(status);
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(
|
|
`Cannot requestAnalysisCreationStatus on a non-product url.`
|
|
);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const status = await product.requestAnalysisCreationStatus();
|
|
if (!status) {
|
|
aCallback.onError(
|
|
`Status of creation of product analysis returned null.`
|
|
);
|
|
return;
|
|
}
|
|
aCallback.onSuccess(status.status);
|
|
}
|
|
}
|
|
|
|
async _requestAnalysisStatus(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const status = { status: "in_progress", progress: 90.9 };
|
|
aCallback.onSuccess({ status });
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(`Cannot requestAnalysisStatus on a non-product url.`);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const status = await product.requestAnalysisCreationStatus();
|
|
if (!status) {
|
|
aCallback.onError(`Status of product analysis returned null.`);
|
|
return;
|
|
}
|
|
aCallback.onSuccess({ status });
|
|
}
|
|
}
|
|
|
|
async _pollForAnalysisCompleted(aData, aCallback) {
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(
|
|
`Cannot pollForAnalysisCompleted on a non-product url.`
|
|
);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const status = await product.pollForAnalysisCompleted();
|
|
if (!status) {
|
|
aCallback.onError(
|
|
`Polling the status of creation of product analysis returned null.`
|
|
);
|
|
return;
|
|
}
|
|
aCallback.onSuccess(status.status);
|
|
}
|
|
}
|
|
|
|
async _sendAttributionEvent(aEvent, aData, aCallback) {
|
|
let result;
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
result = { TEST_AID: "TEST_AID_RESPONSE" };
|
|
} else {
|
|
result = await lazy.ShoppingProduct.sendAttributionEvent(
|
|
aEvent,
|
|
aData.aid,
|
|
"geckoview_android"
|
|
);
|
|
}
|
|
if (!result || !(aData.aid in result) || !result[aData.aid]) {
|
|
aCallback.onSuccess(false);
|
|
return;
|
|
}
|
|
aCallback.onSuccess(true);
|
|
}
|
|
|
|
async _requestRecommendations(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const recommendations = [
|
|
{
|
|
name: "Mock Product",
|
|
url: "https://example.com/mock_url",
|
|
image_url: "https://example.com/mock_image_url",
|
|
price: "450",
|
|
currency: "USD",
|
|
grade: "C",
|
|
adjusted_rating: 3.5,
|
|
analysis_url: "https://example.com/mock_analysis_url",
|
|
sponsored: true,
|
|
aid: "mock_aid",
|
|
},
|
|
];
|
|
aCallback.onSuccess({ recommendations });
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(`Cannot requestRecommendations on a non-product url.`);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const recommendations = await product.requestRecommendations();
|
|
if (!recommendations) {
|
|
aCallback.onError(`Product recommendations returned null.`);
|
|
return;
|
|
}
|
|
aCallback.onSuccess({ recommendations });
|
|
}
|
|
}
|
|
|
|
async _reportBackInStock(aData, aCallback) {
|
|
if (
|
|
Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
|
|
) {
|
|
const message = "report created";
|
|
aCallback.onSuccess(message);
|
|
return;
|
|
}
|
|
const url = Services.io.newURI(aData.url);
|
|
if (!lazy.isProductURL(url)) {
|
|
aCallback.onError(`Cannot reportBackInStock on a non-product url.`);
|
|
} else {
|
|
const product = new lazy.ShoppingProduct(url);
|
|
const message = await product.sendReport();
|
|
if (!message) {
|
|
aCallback.onError(`Reporting back in stock returned null.`);
|
|
return;
|
|
}
|
|
aCallback.onSuccess(message.message);
|
|
}
|
|
}
|
|
|
|
async _hasCookieBannerRuleForBrowsingContextTree(aCallback) {
|
|
const { browsingContext } = this.actor;
|
|
aCallback.onSuccess(
|
|
Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext)
|
|
);
|
|
}
|
|
|
|
_findInPage(aData, aCallback) {
|
|
debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`;
|
|
|
|
let finder;
|
|
try {
|
|
finder = this.browser.finder;
|
|
} catch (e) {
|
|
if (aCallback) {
|
|
aCallback.onError(`No finder: ${e}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this._finderListener) {
|
|
finder.removeResultListener(this._finderListener);
|
|
}
|
|
|
|
this._finderListener = {
|
|
response: {
|
|
found: false,
|
|
wrapped: false,
|
|
current: 0,
|
|
total: -1,
|
|
searchString: aData.searchString || finder.searchString,
|
|
linkURL: null,
|
|
clientRect: null,
|
|
flags: {
|
|
backwards: !!aData.backwards,
|
|
linksOnly: !!aData.linksOnly,
|
|
matchCase: !!aData.matchCase,
|
|
wholeWord: !!aData.wholeWord,
|
|
},
|
|
},
|
|
|
|
onFindResult(aOptions) {
|
|
if (!aCallback || aOptions.searchString !== aData.searchString) {
|
|
// Result from a previous search.
|
|
return;
|
|
}
|
|
|
|
Object.assign(this.response, {
|
|
found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
|
|
wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
|
|
linkURL: aOptions.linkURL,
|
|
clientRect: aOptions.rect && {
|
|
left: aOptions.rect.left,
|
|
top: aOptions.rect.top,
|
|
right: aOptions.rect.right,
|
|
bottom: aOptions.rect.bottom,
|
|
},
|
|
flags: {
|
|
backwards: aOptions.findBackwards,
|
|
linksOnly: aOptions.linksOnly,
|
|
matchCase: this.response.flags.matchCase,
|
|
wholeWord: this.response.flags.wholeWord,
|
|
},
|
|
});
|
|
|
|
if (!this.response.found) {
|
|
this.response.current = 0;
|
|
this.response.total = 0;
|
|
}
|
|
|
|
// Only send response if we have a count.
|
|
if (!this.response.found || this.response.current !== 0) {
|
|
debug`onFindResult: ${this.response}`;
|
|
aCallback.onSuccess(this.response);
|
|
aCallback = undefined;
|
|
}
|
|
},
|
|
|
|
onMatchesCountResult(aResult) {
|
|
if (!aCallback || finder.searchString !== aData.searchString) {
|
|
// Result from a previous search.
|
|
return;
|
|
}
|
|
|
|
Object.assign(this.response, {
|
|
current: aResult.current,
|
|
total: aResult.total,
|
|
});
|
|
|
|
// Only send response if we have a result. `found` and `wrapped` are
|
|
// both false only when we haven't received a result yet.
|
|
if (this.response.found || this.response.wrapped) {
|
|
debug`onMatchesCountResult: ${this.response}`;
|
|
aCallback.onSuccess(this.response);
|
|
aCallback = undefined;
|
|
}
|
|
},
|
|
|
|
onCurrentSelection() {},
|
|
|
|
onHighlightFinished() {},
|
|
};
|
|
|
|
finder.caseSensitive = !!aData.matchCase;
|
|
finder.entireWord = !!aData.wholeWord;
|
|
finder.matchDiacritics = !!aData.matchDiacritics;
|
|
finder.addResultListener(this._finderListener);
|
|
|
|
const drawOutline =
|
|
this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline;
|
|
|
|
if (!aData.searchString || aData.searchString === finder.searchString) {
|
|
// Search again.
|
|
aData.searchString = finder.searchString;
|
|
finder.findAgain(
|
|
aData.searchString,
|
|
!!aData.backwards,
|
|
!!aData.linksOnly,
|
|
drawOutline
|
|
);
|
|
} else {
|
|
finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline);
|
|
}
|
|
}
|
|
|
|
_clearMatches() {
|
|
debug`clearMatches`;
|
|
|
|
let finder;
|
|
try {
|
|
finder = this.browser.finder;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
finder.removeSelection();
|
|
finder.highlight(false);
|
|
|
|
if (this._finderListener) {
|
|
finder.removeResultListener(this._finderListener);
|
|
this._finderListener = null;
|
|
}
|
|
}
|
|
|
|
_displayMatches(aData) {
|
|
debug`displayMatches: data=${aData}`;
|
|
|
|
let finder;
|
|
try {
|
|
finder = this.browser.finder;
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
this._matchDisplayOptions = aData;
|
|
finder.onModalHighlightChange(!!aData.dimPage);
|
|
finder.onHighlightAllChange(!!aData.highlightAll);
|
|
|
|
if (!aData.highlightAll && !aData.dimPage) {
|
|
finder.highlight(false);
|
|
return;
|
|
}
|
|
|
|
if (!this._finderListener || !finder.searchString) {
|
|
return;
|
|
}
|
|
const linksOnly = this._finderListener.response.linksOnly;
|
|
finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline);
|
|
}
|
|
}
|
|
|
|
const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent");
|