`response.result` could be undefined if `currentBC.currentWindowGlobal` is null, so that sendQueryToContext fails to send the message to the global. This becomes possible with bug 1882670 as we start to initialize the frame loader for iframes earlier. The windowGlobal will be attached once the iframes are loaded, this patch fixes the case when they haven't loaded. Differential Revision: https://phabricator.services.mozilla.com/D222701
615 lines
17 KiB
JavaScript
615 lines
17 KiB
JavaScript
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
|
// vim: set ts=2 sw=2 sts=2 et tw=80: */
|
|
// 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/.
|
|
|
|
const kModalHighlightPref = "findbar.modalHighlight";
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import {
|
|
initNotFoundSound,
|
|
playNotFoundSound,
|
|
} from "resource://gre/modules/FinderSound.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
GetClipboardSearchString: "resource://gre/modules/Finder.sys.mjs",
|
|
RFPHelper: "resource://gre/modules/RFPHelper.sys.mjs",
|
|
Rect: "resource://gre/modules/Geometry.sys.mjs",
|
|
});
|
|
|
|
const kPrefLetterboxing = "privacy.resistFingerprinting.letterboxing";
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"isLetterboxingEnabled",
|
|
kPrefLetterboxing,
|
|
false
|
|
);
|
|
|
|
export function FinderParent(browser) {
|
|
this._listeners = new Set();
|
|
this._searchString = "";
|
|
this._foundSearchString = null;
|
|
this._lastFoundBrowsingContext = null;
|
|
|
|
// The correct states of these will be updated when the findbar is opened.
|
|
this._caseSensitive = false;
|
|
this._entireWord = false;
|
|
this._matchDiacritics = false;
|
|
|
|
this.swapBrowser(browser);
|
|
}
|
|
|
|
FinderParent.prototype = {
|
|
get browsingContext() {
|
|
return this._browser.browsingContext;
|
|
},
|
|
|
|
get useRemoteSubframes() {
|
|
return this._browser.ownerGlobal.docShell.nsILoadContext.useRemoteSubframes;
|
|
},
|
|
|
|
swapBrowser(aBrowser) {
|
|
this._browser = aBrowser;
|
|
// Ideally listeners would have removed themselves but that doesn't happen
|
|
// right now
|
|
this._listeners.clear();
|
|
},
|
|
|
|
addResultListener(aListener) {
|
|
this._listeners.add(aListener);
|
|
},
|
|
|
|
removeResultListener(aListener) {
|
|
this._listeners.delete(aListener);
|
|
},
|
|
|
|
callListeners(aCallback, aArgs) {
|
|
for (let l of this._listeners) {
|
|
// Don't let one callback throwing stop us calling the rest
|
|
try {
|
|
l[aCallback].apply(l, aArgs);
|
|
} catch (e) {
|
|
if (!l[aCallback]) {
|
|
console.error(
|
|
`Missing ${aCallback} callback on RemoteFinderListener`
|
|
);
|
|
} else {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
getLastFoundBrowsingContext(aList) {
|
|
// If a search was already performed, returned the last
|
|
// browsing context where the result was found. However,
|
|
// ensure that this browsing context is still valid, and
|
|
// if not, return null.
|
|
if (
|
|
aList.includes(this._lastFoundBrowsingContext) &&
|
|
!this._lastFoundBrowsingContext.isUnderHiddenEmbedderElement
|
|
) {
|
|
return this._lastFoundBrowsingContext;
|
|
}
|
|
|
|
this._lastFoundBrowsingContext = null;
|
|
return null;
|
|
},
|
|
|
|
sendMessageToContext(aMessageName, aArgs = {}) {
|
|
// If there is a last found browsing context, use that. Otherwise,
|
|
// use the top-level browsing context.
|
|
let browsingContext = null;
|
|
if (this._lastFoundBrowsingContext) {
|
|
let list = this.gatherBrowsingContexts(this.browsingContext);
|
|
let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
|
|
if (lastBrowsingContext) {
|
|
browsingContext = lastBrowsingContext;
|
|
}
|
|
}
|
|
|
|
if (!browsingContext) {
|
|
browsingContext = this.browsingContext;
|
|
}
|
|
|
|
let windowGlobal = browsingContext.currentWindowGlobal;
|
|
if (windowGlobal) {
|
|
let actor = windowGlobal.getActor("Finder");
|
|
actor.sendAsyncMessage(aMessageName, aArgs);
|
|
}
|
|
},
|
|
|
|
sendQueryToContext(aMessageName, aArgs, aBrowsingContext) {
|
|
let windowGlobal = aBrowsingContext.currentWindowGlobal;
|
|
if (windowGlobal) {
|
|
let actor = windowGlobal.getActor("Finder");
|
|
return actor.sendQuery(aMessageName, aArgs).then(
|
|
result => result,
|
|
() => {}
|
|
);
|
|
}
|
|
|
|
return Promise.resolve({});
|
|
},
|
|
|
|
sendMessageToAllContexts(aMessageName, aArgs = {}) {
|
|
let list = this.gatherBrowsingContexts(this.browsingContext);
|
|
for (let browsingContext of list) {
|
|
let windowGlobal = browsingContext.currentWindowGlobal;
|
|
if (windowGlobal) {
|
|
let actor = windowGlobal.getActor("Finder");
|
|
actor.sendAsyncMessage(aMessageName, aArgs);
|
|
}
|
|
}
|
|
},
|
|
|
|
gatherBrowsingContexts(aBrowsingContext) {
|
|
if (aBrowsingContext.isUnderHiddenEmbedderElement) {
|
|
return [];
|
|
}
|
|
|
|
let list = [aBrowsingContext];
|
|
|
|
for (let child of aBrowsingContext.children) {
|
|
list.push(...this.gatherBrowsingContexts(child));
|
|
}
|
|
|
|
return list;
|
|
},
|
|
|
|
// If the modal highlighter is on, and there are no out-of-process child
|
|
// frames, send a message only to the top-level frame and set the useSubFrames
|
|
// flag, so that the finder iterator iterates over subframes. If there is
|
|
// an out-of-process subframe, modal highlighting is disabled.
|
|
needSubFrameSearch(aList) {
|
|
let useSubFrames = false;
|
|
|
|
let useModalHighlighter = Services.prefs.getBoolPref(kModalHighlightPref);
|
|
let hasOutOfProcessChild = false;
|
|
if (useModalHighlighter) {
|
|
if (this.useRemoteSubframes) {
|
|
return false;
|
|
}
|
|
|
|
for (let browsingContext of aList) {
|
|
if (
|
|
browsingContext != this.browsingContext &&
|
|
browsingContext.currentWindowGlobal.isProcessRoot
|
|
) {
|
|
hasOutOfProcessChild = true;
|
|
}
|
|
}
|
|
|
|
if (!hasOutOfProcessChild) {
|
|
aList.splice(0);
|
|
aList.push(this.browsingContext);
|
|
useSubFrames = true;
|
|
}
|
|
}
|
|
|
|
return useSubFrames;
|
|
},
|
|
|
|
onResultFound(aResponse) {
|
|
this._foundSearchString = aResponse.searchString;
|
|
// The rect stops being a Geometry.sys.mjs:Rect over IPC.
|
|
if (aResponse.rect) {
|
|
aResponse.rect = lazy.Rect.fromRect(aResponse.rect);
|
|
}
|
|
|
|
this.callListeners("onFindResult", [aResponse]);
|
|
},
|
|
|
|
get searchString() {
|
|
return this._foundSearchString;
|
|
},
|
|
|
|
get clipboardSearchString() {
|
|
return lazy.GetClipboardSearchString(this._browser.loadContext);
|
|
},
|
|
|
|
set caseSensitive(aSensitive) {
|
|
this._caseSensitive = aSensitive;
|
|
this.sendMessageToAllContexts("Finder:CaseSensitive", {
|
|
caseSensitive: aSensitive,
|
|
});
|
|
},
|
|
|
|
set entireWord(aEntireWord) {
|
|
this._entireWord = aEntireWord;
|
|
this.sendMessageToAllContexts("Finder:EntireWord", {
|
|
entireWord: aEntireWord,
|
|
});
|
|
},
|
|
|
|
set matchDiacritics(aMatchDiacritics) {
|
|
this._matchDiacritics = aMatchDiacritics;
|
|
this.sendMessageToAllContexts("Finder:MatchDiacritics", {
|
|
matchDiacritics: aMatchDiacritics,
|
|
});
|
|
},
|
|
|
|
async setSearchStringToSelection() {
|
|
return this.setToSelection("Finder:SetSearchStringToSelection", false);
|
|
},
|
|
|
|
async getInitialSelection() {
|
|
return this.setToSelection("Finder:GetInitialSelection", true);
|
|
},
|
|
|
|
async setToSelection(aMessage, aInitial) {
|
|
let browsingContext = this.browsingContext;
|
|
|
|
// Iterate over focused subframe descendants until one is found
|
|
// that has the selection.
|
|
let result;
|
|
do {
|
|
result = await this.sendQueryToContext(aMessage, {}, browsingContext);
|
|
if (!result || !result.focusedChildBrowserContextId) {
|
|
break;
|
|
}
|
|
|
|
browsingContext = BrowsingContext.get(
|
|
result.focusedChildBrowserContextId
|
|
);
|
|
} while (browsingContext);
|
|
|
|
if (result) {
|
|
this.callListeners("onCurrentSelection", [result.selectedText, aInitial]);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
async doFind(aFindNext, aArgs) {
|
|
let rootBC = this.browsingContext;
|
|
let highlightList = this.gatherBrowsingContexts(rootBC);
|
|
|
|
let searchLengthened =
|
|
aArgs.searchString.length > this._searchString.length;
|
|
|
|
this._searchString = aArgs.searchString;
|
|
|
|
let initialBC = this.getLastFoundBrowsingContext(highlightList);
|
|
if (!initialBC) {
|
|
initialBC = rootBC;
|
|
aFindNext = false;
|
|
}
|
|
|
|
// Make a copy of the list starting from the
|
|
// browsing context that was last searched from. The original
|
|
// list will be used for the highlighter where the search
|
|
// order doesn't matter.
|
|
let searchList = [];
|
|
for (let c = 0; c < highlightList.length; c++) {
|
|
if (highlightList[c] == initialBC) {
|
|
searchList = highlightList.slice(c);
|
|
searchList.push(...highlightList.slice(0, c));
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mode = Ci.nsITypeAheadFind.FIND_INITIAL;
|
|
if (aFindNext) {
|
|
mode = aArgs.findBackwards
|
|
? Ci.nsITypeAheadFind.FIND_PREVIOUS
|
|
: Ci.nsITypeAheadFind.FIND_NEXT;
|
|
}
|
|
aArgs.findAgain = aFindNext;
|
|
|
|
aArgs.caseSensitive = this._caseSensitive;
|
|
aArgs.matchDiacritics = this._matchDiacritics;
|
|
aArgs.entireWord = this._entireWord;
|
|
|
|
aArgs.useSubFrames = this.needSubFrameSearch(searchList);
|
|
if (aArgs.useSubFrames) {
|
|
// Use the single frame for the highlight list as well.
|
|
highlightList = searchList;
|
|
}
|
|
|
|
if (searchLengthened) {
|
|
initNotFoundSound();
|
|
}
|
|
|
|
// Add the initial browsing context twice to allow looping around.
|
|
searchList = [...searchList, initialBC];
|
|
|
|
if (aArgs.findBackwards) {
|
|
searchList.reverse();
|
|
}
|
|
|
|
let response = null;
|
|
let wrapped = false;
|
|
let foundBC = null;
|
|
|
|
for (let c = 0; c < searchList.length; c++) {
|
|
let currentBC = searchList[c];
|
|
aArgs.mode = mode;
|
|
|
|
// A search has started for a different string, so
|
|
// ignore further searches of the old string.
|
|
if (this._searchString != aArgs.searchString) {
|
|
return;
|
|
}
|
|
|
|
response = await this.sendQueryToContext("Finder:Find", aArgs, currentBC);
|
|
|
|
// This can happen if the tab is closed while the find is in progress.
|
|
if (!response) {
|
|
break;
|
|
}
|
|
|
|
// If the search term was found, stop iterating.
|
|
//
|
|
// If the BC doesn't have a window global we can return an empty object. We should treat that
|
|
// as "not found" and continue looking in the rest of the list of BCs.
|
|
if (
|
|
Object.hasOwn(response, "result") &&
|
|
response.result != Ci.nsITypeAheadFind.FIND_NOTFOUND
|
|
) {
|
|
if (
|
|
this._lastFoundBrowsingContext &&
|
|
this._lastFoundBrowsingContext != currentBC
|
|
) {
|
|
// If the new result is in a different frame than the previous result,
|
|
// clear the result from the old frame. If it is the same frame, the
|
|
// previous result will be cleared by the find component.
|
|
this.removeSelection(true);
|
|
}
|
|
this._lastFoundBrowsingContext = currentBC;
|
|
|
|
// Set the wrapped result flag if needed.
|
|
if (wrapped) {
|
|
response.result = Ci.nsITypeAheadFind.FIND_WRAPPED;
|
|
}
|
|
|
|
foundBC = currentBC;
|
|
break;
|
|
}
|
|
|
|
if (aArgs.findBackwards && currentBC == rootBC) {
|
|
wrapped = true;
|
|
} else if (
|
|
!aArgs.findBackwards &&
|
|
c + 1 < searchList.length &&
|
|
searchList[c + 1] == rootBC
|
|
) {
|
|
wrapped = true;
|
|
}
|
|
|
|
mode = aArgs.findBackwards
|
|
? Ci.nsITypeAheadFind.FIND_LAST
|
|
: Ci.nsITypeAheadFind.FIND_FIRST;
|
|
}
|
|
|
|
if (response) {
|
|
response.useSubFrames = aArgs.useSubFrames;
|
|
// Update the highlight in all browsing contexts. This needs to happen separately
|
|
// once it is clear whether a match was found or not.
|
|
this.updateHighlightAndMatchCount({
|
|
list: highlightList,
|
|
message: "Finder:UpdateHighlightAndMatchCount",
|
|
args: response,
|
|
foundBrowsingContextId: foundBC ? foundBC.id : -1,
|
|
doHighlight: true,
|
|
doMatchCount: true,
|
|
});
|
|
|
|
// Use the last result found.
|
|
this.onResultFound(response);
|
|
|
|
if (
|
|
searchLengthened &&
|
|
response.result == Ci.nsITypeAheadFind.FIND_NOTFOUND &&
|
|
!aFindNext &&
|
|
!response.entireWord
|
|
) {
|
|
playNotFoundSound();
|
|
}
|
|
}
|
|
},
|
|
|
|
fastFind(aSearchString, aLinksOnly, aDrawOutline) {
|
|
this.doFind(false, {
|
|
searchString: aSearchString,
|
|
findBackwards: false,
|
|
linksOnly: aLinksOnly,
|
|
drawOutline: aDrawOutline,
|
|
});
|
|
},
|
|
|
|
findAgain(aSearchString, aFindBackwards, aLinksOnly, aDrawOutline) {
|
|
this.doFind(true, {
|
|
searchString: aSearchString,
|
|
findBackwards: aFindBackwards,
|
|
linksOnly: aLinksOnly,
|
|
drawOutline: aDrawOutline,
|
|
});
|
|
},
|
|
|
|
highlight(aHighlight, aWord, aLinksOnly) {
|
|
let list = this.gatherBrowsingContexts(this.browsingContext);
|
|
let args = {
|
|
highlight: aHighlight,
|
|
linksOnly: aLinksOnly,
|
|
searchString: aWord,
|
|
};
|
|
|
|
args.useSubFrames = this.needSubFrameSearch(list);
|
|
|
|
let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
|
|
this.updateHighlightAndMatchCount({
|
|
list,
|
|
message: "Finder:Highlight",
|
|
args,
|
|
foundBrowsingContextId: lastBrowsingContext ? lastBrowsingContext.id : -1,
|
|
doHighlight: true,
|
|
doMatchCount: false,
|
|
});
|
|
},
|
|
|
|
requestMatchesCount(aSearchString, aLinksOnly) {
|
|
let list = this.gatherBrowsingContexts(this.browsingContext);
|
|
let args = { searchString: aSearchString, linksOnly: aLinksOnly };
|
|
|
|
args.useSubFrames = this.needSubFrameSearch(list);
|
|
|
|
let lastBrowsingContext = this.getLastFoundBrowsingContext(list);
|
|
this.updateHighlightAndMatchCount({
|
|
list,
|
|
message: "Finder:MatchesCount",
|
|
args,
|
|
foundBrowsingContextId: lastBrowsingContext ? lastBrowsingContext.id : -1,
|
|
doHighlight: false,
|
|
doMatchCount: true,
|
|
});
|
|
},
|
|
|
|
updateHighlightAndMatchCount(options) {
|
|
let promises = [];
|
|
let found = options.args.result != Ci.nsITypeAheadFind.FIND_NOTFOUND;
|
|
for (let browsingContext of options.list) {
|
|
options.args.foundInThisFrame =
|
|
options.foundBrowsingContextId != -1 &&
|
|
found &&
|
|
browsingContext.id == options.foundBrowsingContextId;
|
|
|
|
// Don't wait for the result
|
|
let promise = this.sendQueryToContext(
|
|
options.message,
|
|
options.args,
|
|
browsingContext
|
|
);
|
|
promises.push(promise);
|
|
}
|
|
|
|
Promise.all(promises).then(responses => {
|
|
if (options.doHighlight) {
|
|
let sendNotification = false;
|
|
let highlight = false;
|
|
let found = false;
|
|
for (let response of responses) {
|
|
if (!response) {
|
|
break;
|
|
}
|
|
|
|
sendNotification = true;
|
|
if (response.found) {
|
|
found = true;
|
|
}
|
|
highlight = response.highlight;
|
|
}
|
|
|
|
if (sendNotification) {
|
|
this.callListeners("onHighlightFinished", [
|
|
{ searchString: options.args.searchString, highlight, found },
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (options.doMatchCount) {
|
|
let sendNotification = false;
|
|
let current = 0;
|
|
let total = 0;
|
|
let limit = 0;
|
|
for (let response of responses) {
|
|
// A null response can happen if another search was started
|
|
// and this one became invalid.
|
|
if (!response || !("total" in response)) {
|
|
break;
|
|
}
|
|
|
|
sendNotification = true;
|
|
|
|
if (
|
|
options.args.useSubFrames ||
|
|
(options.foundBrowsingContextId >= 0 &&
|
|
response.browsingContextId == options.foundBrowsingContextId)
|
|
) {
|
|
current = total + response.current;
|
|
}
|
|
total += response.total;
|
|
limit = response.limit;
|
|
}
|
|
|
|
if (sendNotification) {
|
|
this.callListeners("onMatchesCountResult", [
|
|
{ searchString: options.args.searchString, current, total, limit },
|
|
]);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
enableSelection() {
|
|
this.sendMessageToContext("Finder:EnableSelection");
|
|
},
|
|
|
|
removeSelection(aKeepHighlight) {
|
|
this.sendMessageToContext("Finder:RemoveSelection", {
|
|
keepHighlight: aKeepHighlight,
|
|
});
|
|
},
|
|
|
|
focusContent() {
|
|
// Allow Finder listeners to cancel focusing the content.
|
|
for (let l of this._listeners) {
|
|
try {
|
|
if ("shouldFocusContent" in l && !l.shouldFocusContent()) {
|
|
return;
|
|
}
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
}
|
|
|
|
this._browser.focus();
|
|
this.sendMessageToContext("Finder:FocusContent");
|
|
},
|
|
|
|
onFindbarClose() {
|
|
this._lastFoundBrowsingContext = null;
|
|
this.sendMessageToAllContexts("Finder:FindbarClose");
|
|
|
|
if (lazy.isLetterboxingEnabled) {
|
|
let window = this._browser.ownerGlobal;
|
|
lazy.RFPHelper.contentSizeUpdated(window);
|
|
}
|
|
},
|
|
|
|
onFindbarOpen() {
|
|
this.sendMessageToAllContexts("Finder:FindbarOpen");
|
|
|
|
if (lazy.isLetterboxingEnabled) {
|
|
let window = this._browser.ownerGlobal;
|
|
lazy.RFPHelper.contentSizeUpdated(window);
|
|
}
|
|
},
|
|
|
|
onModalHighlightChange(aUseModalHighlight) {
|
|
this.sendMessageToAllContexts("Finder:ModalHighlightChange", {
|
|
useModalHighlight: aUseModalHighlight,
|
|
});
|
|
},
|
|
|
|
onHighlightAllChange(aHighlightAll) {
|
|
this.sendMessageToAllContexts("Finder:HighlightAllChange", {
|
|
highlightAll: aHighlightAll,
|
|
});
|
|
},
|
|
|
|
keyPress(aEvent) {
|
|
this.sendMessageToContext("Finder:KeyPress", {
|
|
keyCode: aEvent.keyCode,
|
|
ctrlKey: aEvent.ctrlKey,
|
|
metaKey: aEvent.metaKey,
|
|
altKey: aEvent.altKey,
|
|
shiftKey: aEvent.shiftKey,
|
|
});
|
|
},
|
|
};
|