/* 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/. */
"use strict";
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const PREFS_TO_OBSERVE_BOOL =
new Map([
[
"findAsYouType",
"accessibility.typeaheadfind"],
[
"manualFAYT",
"accessibility.typeaheadfind.manual"],
[
"typeAheadLinksOnly",
"accessibility.typeaheadfind.linksonly"],
[
"entireWord",
"findbar.entireword"],
[
"highlightAll",
"findbar.highlightAll"],
[
"useModalHighlight",
"findbar.modalHighlight"],
]);
const PREFS_TO_OBSERVE_INT =
new Map([
[
"typeAheadCaseSensitive",
"accessibility.typeaheadfind.casesensitive"],
[
"matchDiacritics",
"findbar.matchdiacritics"],
]);
const PREFS_TO_OBSERVE_ALL =
new Map([
...PREFS_TO_OBSERVE_BOOL,
...PREFS_TO_OBSERVE_INT,
]);
const TOPIC_MAC_APP_ACTIVATE =
"mac_app_activate";
class MozFindbar
extends MozXULElement {
static get markup() {
return `
<hbox anonid=
"findbar-container" class=
"findbar-container" flex=
"1" align=
"center">
<hbox anonid=
"findbar-textbox-wrapper" align=
"stretch">
<html:input anonid=
"findbar-textbox" class=
"findbar-textbox" />
<toolbarbutton anonid=
"find-previous" class=
"findbar-find-previous tabbable"
data-l10n-attrs=
"tooltiptext" data-l10n-id=
"findbar-previous" disabled=
"true" />
<toolbarbutton anonid=
"find-next" class=
"findbar-find-next tabbable"
data-l10n-id=
"findbar-next" disabled=
"true" />
</hbox>
<checkbox anonid=
"highlight" class=
"findbar-highlight tabbable"
data-l10n-id=
"findbar-highlight-all2"/>
<checkbox anonid=
"find-case-sensitive" class=
"findbar-case-sensitive tabbable"
data-l10n-id=
"findbar-case-sensitive"/>
<checkbox anonid=
"find-match-diacritics" class=
"findbar-match-diacritics tabbable"
data-l10n-id=
"findbar-match-diacritics"/>
<checkbox anonid=
"find-entire-word" class=
"findbar-entire-word tabbable"
data-l10n-id=
"findbar-entire-word"/>
<label anonid=
"match-case-status" class=
"findbar-label"
data-l10n-id=
"findbar-case-sensitive-status" hidden=
"true" />
<label anonid=
"match-diacritics-status" class=
"findbar-label"
data-l10n-id=
"findbar-match-diacritics-status" hidden=
"true" />
<label anonid=
"entire-word-status" class=
"findbar-label"
data-l10n-id=
"findbar-entire-word-status" hidden=
"true" />
<label anonid=
"found-matches" class=
"findbar-label found-matches" hidden=
"true" />
<image anonid=
"find-status-icon" class=
"find-status-icon" />
<description anonid=
"find-status" control=
"findbar-textbox" class=
"findbar-label findbar-find-status" />
</hbox>
<toolbarbutton anonid=
"find-closebutton" class=
"findbar-closebutton tabbable close-icon"
data-l10n-id=
"findbar-find-button-close"/>
`;
}
constructor() {
super();
MozXULElement.insertFTLIfNeeded(
"toolkit/main-window/findbar.ftl");
this.destroy =
this.destroy.bind(
this);
// We have to guard against `this.close` being |null| due to an unknown
// issue, which is tracked in bug 957999.
this.addEventListener(
"keypress",
event => {
if (event.keyCode == event.DOM_VK_ESCAPE) {
if (
this.close) {
this.close();
}
event.preventDefault();
}
},
true
);
}
connectedCallback() {
// Hide the findbar immediately without animation. This prevents a flicker in the case where
// we'll never be shown (i.e. adopting a tab that has a previously-opened-but-now-closed
// findbar into a new window).
this.setAttribute(
"noanim",
"true");
this.hidden =
true;
this.appendChild(
this.constructor.fragment);
if (AppConstants.platform ==
"macosx") {
this.insertBefore(
this.getElement(
"find-closebutton"),
this.getElement(
"findbar-container")
);
}
/**
* Please keep in sync with toolkit/modules/FindBarContent.sys.mjs
*/
this.FIND_NORMAL = 0;
this.FIND_TYPEAHEAD = 1;
this.FIND_LINKS = 2;
this._findMode = 0;
this._flashFindBar = 0;
this._initialFlashFindBarCount = 6;
/**
* For tests that need to know when the find bar is finished
* initializing, we store a promise to notify on.
*/
this._startFindDeferred =
null;
this._browser =
null;
this._destroyed =
false;
this._xulBrowserWindow =
null;
// These elements are accessed frequently and are therefore cached.
this._findField =
this.getElement(
"findbar-textbox");
this._foundMatches =
this.getElement(
"found-matches");
this._findStatusIcon =
this.getElement(
"find-status-icon");
this._findStatusDesc =
this.getElement(
"find-status");
this._foundURL =
null;
let prefsvc = Services.prefs;
this.quickFindTimeoutLength = prefsvc.getIntPref(
"accessibility.typeaheadfind.timeout"
);
this._flashFindBar = prefsvc.getIntPref(
"accessibility.typeaheadfind.flashBar"
);
let observe = (
this._observe =
this.observe.bind(
this));
for (let [propName, prefName] of PREFS_TO_OBSERVE_ALL) {
prefsvc.addObserver(prefName, observe);
let prefGetter = PREFS_TO_OBSERVE_BOOL.has(propName) ?
"Bool" :
"Int";
this[
"_" + propName] = prefsvc[`get${prefGetter}Pref`](prefName);
}
Services.obs.addObserver(observe, TOPIC_MAC_APP_ACTIVATE);
this._findResetTimeout = -1;
// Make sure the FAYT keypress listener is attached by initializing the
// browser property.
if (
this.getAttribute(
"browserid")) {
setTimeout(() => {
// eslint-disable-next-line no-self-assign
this.browser =
this.browser;
}, 0);
}
window.addEventListener(
"unload",
this.destroy);
this._findField.addEventListener(
"input", () => {
// We should do nothing during composition. E.g., composing string
// before converting may matches a forward word of expected word.
// After that, even if user converts the composition string to the
// expected word, it may find second or later searching word in the
// document.
if (
this._isIMEComposing) {
return;
}
const value =
this._findField.value;
if (
this._hadValue && !value) {
this._willfullyDeleted =
true;
this._hadValue =
false;
}
else if (value.trim()) {
this._hadValue =
true;
this._willfullyDeleted =
false;
}
this._find(value);
});
this._findField.addEventListener(
"keypress", event => {
switch (event.keyCode) {
case KeyEvent.DOM_VK_RETURN:
if (
this.findMode ==
this.FIND_NORMAL) {
let findString =
this._findField;
if (!findString.value) {
return;
}
if (event.getModifierState(
"Accel")) {
this.getElement(
"highlight").click();
return;
}
this.onFindAgainCommand(event.shiftKey);
}
else {
this._finishFAYT(event);
}
break;
case KeyEvent.DOM_VK_TAB:
let shouldHandle =
!event.altKey && !event.ctrlKey && !event.metaKey;
if (shouldHandle &&
this.findMode !=
this.FIND_NORMAL) {
this._finishFAYT(event);
}
break;
case KeyEvent.DOM_VK_PAGE_UP:
case KeyEvent.DOM_VK_PAGE_DOWN:
if (
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) {
this.browser.finder.keyPress(event);
event.preventDefault();
}
break;
case KeyEvent.DOM_VK_UP:
case KeyEvent.DOM_VK_DOWN:
this.browser.finder.keyPress(event);
event.preventDefault();
break;
}
});
this._findField.addEventListener(
"blur", () => {
// Note: This code used to remove the selection
// if it matched an editable.
this.browser.finder.enableSelection();
});
this._findField.addEventListener(
"focus", () => {
this._updateBrowserWithState();
});
this._findField.addEventListener(
"compositionstart", () => {
// Don't close the find toolbar while IME is composing.
let findbar =
this;
findbar._isIMEComposing =
true;
if (findbar._quickFindTimeout) {
clearTimeout(findbar._quickFindTimeout);
findbar._quickFindTimeout =
null;
findbar._updateBrowserWithState();
}
});
this._findField.addEventListener(
"compositionend", () => {
this._isIMEComposing =
false;
if (
this.findMode !=
this.FIND_NORMAL) {
this._setFindCloseTimeout();
}
});
this._findField.addEventListener(
"dragover", event => {
if (event.dataTransfer.types.includes(
"text/plain")) {
event.preventDefault();
}
});
this._findField.addEventListener(
"drop", event => {
let value = event.dataTransfer.getData(
"text/plain");
this._findField.value = value;
this._find(value);
event.stopPropagation();
event.preventDefault();
});
this.getElement(
"find-previous").addEventListener(
"command", () =>
this.onFindAgainCommand(
true)
);
this.getElement(
"find-next").addEventListener(
"command", () =>
this.onFindAgainCommand(
false)
);
this.getElement(
"highlight").addEventListener(
"command", event =>
this.toggleHighlight(event.target.checked)
);
this.getElement(
"find-case-sensitive").addEventListener(
"command",
event =>
this._setCaseSensitivity(event.target.checked ? 1 : 0)
);
this.getElement(
"find-match-diacritics").addEventListener(
"command",
event =>
this._setDiacriticMatching(event.target.checked ? 1 : 0)
);
this.getElement(
"find-entire-word").addEventListener(
"command", event =>
this.toggleEntireWord(event.target.checked)
);
this.getElement(
"find-closebutton").addEventListener(
"command", () =>
this.close()
);
}
set findMode(val) {
this._findMode = val;
this._updateBrowserWithState();
}
get findMode() {
return this._findMode;
}
set prefillWithSelection(val) {
this.setAttribute(
"prefillwithselection", val);
}
get prefillWithSelection() {
return this.getAttribute(
"prefillwithselection") !=
"false";
}
get hasTransactions() {
if (
this._findField.value) {
return true;
}
// Watch out for lazy editor init
if (
this._findField.editor) {
return this._findField.editor.canUndo ||
this._findField.editor.canRedo;
}
return false;
}
set browser(val) {
function setFindbarInActor(browser, findbar) {
if (!browser.frameLoader) {
return;
}
let windowGlobal = browser.browsingContext.currentWindowGlobal;
if (windowGlobal) {
let findbarParent = windowGlobal.getActor(
"FindBar");
if (findbarParent) {
findbarParent.setFindbar(browser, findbar);
}
}
}
if (
this._browser) {
setFindbarInActor(
this._browser,
null);
let finder =
this._browser.finder;
if (finder) {
finder.removeResultListener(
this);
}
}
this._browser = val;
if (
this._browser) {
// Need to do this to ensure the correct initial state.
this._updateBrowserWithState();
setFindbarInActor(
this._browser,
this);
this._browser.finder.addResultListener(
this);
}
}
get browser() {
if (!
this._browser) {
const id =
this.getAttribute(
"browserid");
if (id) {
this._browser = document.getElementById(id);
}
}
return this._browser;
}
observe(subject, topic, prefName) {
if (topic == TOPIC_MAC_APP_ACTIVATE) {
this._onAppActivateMac();
return;
}
if (topic !=
"nsPref:changed") {
return;
}
let prefsvc = Services.prefs;
switch (prefName) {
case "accessibility.typeaheadfind":
this._findAsYouType = prefsvc.getBoolPref(prefName);
break;
case "accessibility.typeaheadfind.manual":
this._manualFAYT = prefsvc.getBoolPref(prefName);
break;
case "accessibility.typeaheadfind.timeout":
this.quickFindTimeoutLength = prefsvc.getIntPref(prefName);
break;
case "accessibility.typeaheadfind.linksonly":
this._typeAheadLinksOnly = prefsvc.getBoolPref(prefName);
break;
case "accessibility.typeaheadfind.casesensitive":
this._setCaseSensitivity(prefsvc.getIntPref(prefName));
break;
case "findbar.entireword":
this._entireWord = prefsvc.getBoolPref(prefName);
this.toggleEntireWord(
this._entireWord,
true);
break;
case "findbar.highlightAll":
this.toggleHighlight(prefsvc.getBoolPref(prefName),
true);
break;
case "findbar.matchdiacritics":
this._setDiacriticMatching(prefsvc.getIntPref(prefName));
break;
case "findbar.modalHighlight":
this._useModalHighlight = prefsvc.getBoolPref(prefName);
if (
this.browser.finder) {
this.browser.finder.onModalHighlightChange(
this._useModalHighlight);
}
break;
}
}
getElement(aAnonymousID) {
return this.querySelector(`[anonid=${aAnonymousID}]`);
}
/**
* This is necessary because custom elements don't have a "real" destructor.
* This method is called explicitly from disconnectedCallback, and from
* an unload event handler that we add.
*/
destroy() {
if (
this._destroyed) {
return;
}
window.removeEventListener(
"unload",
this.destroy);
this._destroyed =
true;
this.browser?._finder?.destroy();
// Invoking this setter also removes the message listeners.
this.browser =
null;
let prefsvc = Services.prefs;
let observe =
this._observe;
for (let [, prefName] of PREFS_TO_OBSERVE_ALL) {
prefsvc.removeObserver(prefName, observe);
}
Services.obs.removeObserver(observe, TOPIC_MAC_APP_ACTIVATE);
// Clear all timers that might still be running.
this._cancelTimers();
}
_cancelTimers() {
if (
this._flashFindBarTimeout) {
clearInterval(
this._flashFindBarTimeout);
this._flashFindBarTimeout =
null;
}
if (
this._quickFindTimeout) {
clearTimeout(
this._quickFindTimeout);
this._quickFindTimeout =
null;
}
if (
this._findResetTimeout) {
clearTimeout(
this._findResetTimeout);
this._findResetTimeout =
null;
}
}
_setFindCloseTimeout() {
if (
this._quickFindTimeout) {
clearTimeout(
this._quickFindTimeout);
}
// Don't close the find toolbar while IME is composing OR when the
// findbar is already hidden.
if (
this._isIMEComposing ||
this.hidden) {
this._quickFindTimeout =
null;
this._updateBrowserWithState();
return;
}
if (
this.quickFindTimeoutLength < 1) {
this._quickFindTimeout =
null;
}
else {
this._quickFindTimeout = setTimeout(() => {
if (
this.findMode !=
this.FIND_NORMAL) {
this.close();
}
this._quickFindTimeout =
null;
},
this.quickFindTimeoutLength);
}
this._updateBrowserWithState();
}
/**
* Updates the search match count after each find operation on a new string.
*/
_updateMatchesCount() {
if (!
this._dispatchFindEvent(
"matchescount")) {
return;
}
this.browser.finder.requestMatchesCount(
this._findField.value,
this.findMode ==
this.FIND_LINKS
);
}
/**
* Turns highlighting of all occurrences on or off.
*
* @param {Boolean} highlight Whether to turn the highlight on or off.
* @param {Boolean} fromPrefObserver Whether the callee is the pref
* observer, which means we should not set
* the same pref again.
*/
toggleHighlight(highlight, fromPrefObserver) {
if (highlight ===
this._highlightAll) {
return;
}
this.browser.finder.onHighlightAllChange(highlight);
this._setHighlightAll(highlight, fromPrefObserver);
if (!
this._dispatchFindEvent(
"highlightallchange")) {
return;
}
let word =
this._findField.value;
// Bug 429723. Don't attempt to highlight ""
if (highlight && !word) {
return;
}
this.browser.finder.highlight(
highlight,
word,
this.findMode ==
this.FIND_LINKS
);
// Update the matches count
this._updateMatchesCount(Ci.nsITypeAheadFind.FIND_FOUND);
}
/**
* Updates the highlight-all mode of the findbar and its UI.
*
* @param {Boolean} highlight Whether to turn the highlight on or off.
* @param {Boolean} fromPrefObserver Whether the callee is the pref
* observer, which means we should not set
* the same pref again.
*/
_setHighlightAll(highlight, fromPrefObserver) {
if (
typeof highlight !=
"boolean") {
highlight =
this._highlightAll;
}
if (highlight !==
this._highlightAll) {
this._highlightAll = highlight;
if (!fromPrefObserver) {
Glean.findbar.highlightAll.add(1);
Services.prefs.setBoolPref(
"findbar.highlightAll", highlight);
}
}
let checkbox =
this.getElement(
"highlight");
checkbox.checked =
this._highlightAll;
}
/**
* Updates the case-sensitivity mode of the findbar and its UI.
*
* @param {String} [str] The string for which case sensitivity might be
* turned on. This only used when case-sensitivity is
* in auto mode, see `_shouldBeCaseSensitive`. The
* default value for this parameter is the find-field
* value.
* @see _shouldBeCaseSensitive
*/
_updateCaseSensitivity(str) {
let val = str ||
this._findField.value;
let caseSensitive =
this._shouldBeCaseSensitive(val);
let checkbox =
this.getElement(
"find-case-sensitive");
let statusLabel =
this.getElement(
"match-case-status");
checkbox.checked = caseSensitive;
// Show the checkbox on the full Find bar in non-auto mode.
// Show the label in all other cases.
if (
this.findMode ==
this.FIND_NORMAL &&
(
this._typeAheadCaseSensitive == 0 ||
this._typeAheadCaseSensitive == 1)
) {
checkbox.hidden =
false;
statusLabel.hidden =
true;
}
else {
checkbox.hidden =
true;
statusLabel.hidden = !caseSensitive;
}
this.browser.finder.caseSensitive = caseSensitive;
}
/**
* Sets the findbar case-sensitivity mode.
*
* @param {Number} caseSensitivity 0 - case insensitive,
* 1 - case sensitive,
* 2 - auto = case sensitive if the matching
* string contains upper case letters.
* @see _shouldBeCaseSensitive
*/
_setCaseSensitivity(caseSensitivity) {
this._typeAheadCaseSensitive = caseSensitivity;
this._updateCaseSensitivity();
this._findFailedString =
null;
this._find();
this._dispatchFindEvent(
"casesensitivitychange");
Glean.findbar.matchCase.add(1);
}
/**
* Updates the diacritic-matching mode of the findbar and its UI.
*
* @param {String} [str] The string for which diacritic matching might be
* turned on. This is only used when diacritic
* matching is in auto mode, see
* `_shouldMatchDiacritics`. The default value for
* this parameter is the find-field value.
* @see _shouldMatchDiacritics.
*/
_updateDiacriticMatching(str) {
let val = str ||
this._findField.value;
let matchDiacritics =
this._shouldMatchDiacritics(val);
let checkbox =
this.getElement(
"find-match-diacritics");
let statusLabel =
this.getElement(
"match-diacritics-status");
checkbox.checked = matchDiacritics;
// Show the checkbox on the full Find bar in non-auto mode.
// Show the label in all other cases.
if (
this.findMode ==
this.FIND_NORMAL &&
(
this._matchDiacritics == 0 ||
this._matchDiacritics == 1)
) {
checkbox.hidden =
false;
statusLabel.hidden =
true;
}
else {
checkbox.hidden =
true;
statusLabel.hidden = !matchDiacritics;
}
this.browser.finder.matchDiacritics = matchDiacritics;
}
/**
* Sets the findbar diacritic-matching mode
* @param {Number} diacriticMatching 0 - ignore diacritics,
* 1 - match diacritics,
* 2 - auto = match diacritics if the
* matching string contains
* diacritics.
* @see _shouldMatchDiacritics
*/
_setDiacriticMatching(diacriticMatching) {
this._matchDiacritics = diacriticMatching;
this._updateDiacriticMatching();
this._findFailedString =
null;
this._find();
this._dispatchFindEvent(
"diacriticmatchingchange");
Glean.findbar.matchDiacritics.add(1);
}
/**
* Updates the entire-word mode of the findbar and its UI.
*/
_setEntireWord() {
let entireWord =
this._entireWord;
let checkbox =
this.getElement(
"find-entire-word");
let statusLabel =
this.getElement(
"entire-word-status");
checkbox.checked = entireWord;
// Show the checkbox on the full Find bar.
// Show the label in all other cases.
if (
this.findMode ==
this.FIND_NORMAL) {
checkbox.hidden =
false;
statusLabel.hidden =
true;
}
else {
checkbox.hidden =
true;
statusLabel.hidden = !entireWord;
}
this.browser.finder.entireWord = entireWord;
}
/**
* Sets the findbar entire-word mode.
*
* @param {Boolean} entireWord Whether or not entire-word mode should be
* turned on.
*/
toggleEntireWord(entireWord, fromPrefObserver) {
if (!fromPrefObserver) {
// Just set the pref; our observer will change the find bar behavior.
Services.prefs.setBoolPref(
"findbar.entireword", entireWord);
Glean.findbar.wholeWords.add(1);
return;
}
this._findFailedString =
null;
this._find();
}
/**
* Opens and displays the find bar.
*
* @param {Number} mode The find mode to be used, which is either
* FIND_NORMAL, FIND_TYPEAHEAD or FIND_LINKS. If not
* passed, we revert to the last find mode if any or
* FIND_NORMAL.
* @return {Boolean} `true` if the find bar wasn't previously open, `false`
* otherwise.
*/
open(mode) {
if (mode != undefined) {
this.findMode = mode;
}
this._findFailedString =
null;
this._updateFindUI();
if (
this.hidden) {
Glean.findbar.shown.add(1);
this.removeAttribute(
"noanim");
this.hidden =
false;
this._updateStatusUI(Ci.nsITypeAheadFind.FIND_FOUND);
let event = document.createEvent(
"Events");
event.initEvent(
"findbaropen",
true,
false);
this.dispatchEvent(event);
this.browser.finder.onFindbarOpen();
return true;
}
return false;
}
/**
* Closes the findbar.
*
* @param {Boolean} [noAnim] Whether to disable to closing animation. Used
* to close instantly and synchronously, when
* other operations depend on this state.
*/
close(noAnim) {
if (
this.hidden) {
return;
}
if (noAnim) {
this.setAttribute(
"noanim",
true);
}
this.hidden =
true;
let event = document.createEvent(
"Events");
event.initEvent(
"findbarclose",
true,
false);
this.dispatchEvent(event);
// 'focusContent()' iterates over all listeners in the chrome
// process, so we need to call it from here.
this.browser.finder.focusContent();
this.browser.finder.onFindbarClose();
this._cancelTimers();
this._updateBrowserWithState();
this._findFailedString =
null;
}
clear() {
this.browser.finder.removeSelection();
// Clear value and undo/redo transactions
this._findField.value =
"";
this._findField.editor?.clearUndoRedo();
this.toggleHighlight(
false);
this._updateStatusUI();
this._enableFindButtons(
false);
}
_dispatchKeypressEvent(target, fakeEvent) {
if (!target) {
return;
}
// The event information comes from the child process.
let event =
new target.ownerGlobal.KeyboardEvent(
fakeEvent.type,
fakeEvent
);
target.dispatchEvent(event);
}
_updateStatusUIBar(foundURL) {
if (!
this._xulBrowserWindow) {
try {
this._xulBrowserWindow = window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow).XULBrowserWindow;
}
catch (ex) {}
if (!
this._xulBrowserWindow) {
return false;
}
}
// Call this has the same effect like hovering over link,
// the browser shows the URL as a tooltip.
this._xulBrowserWindow.setOverLink(foundURL ||
"");
return true;
}
_finishFAYT(keypressEvent) {
this.browser.finder.focusContent();
if (keypressEvent) {
keypressEvent.preventDefault();
}
this.browser.finder.keyPress(keypressEvent);
this.close();
return true;
}
_shouldBeCaseSensitive(str) {
if (
this._typeAheadCaseSensitive == 0) {
return false;
}
if (
this._typeAheadCaseSensitive == 1) {
return true;
}
return str != str.toLowerCase();
}
_shouldMatchDiacritics(str) {
if (
this._matchDiacritics == 0) {
return false;
}
if (
this._matchDiacritics == 1) {
return true;
}
return str != str.normalize(
"NFD");
}
onMouseUp() {
if (!
this.hidden &&
this.findMode !=
this.FIND_NORMAL) {
this.close();
}
}
/**
* We get a fake event object through an IPC message when FAYT is being used
* from within the browser. We then stuff that input in the find bar here.
*
* @param {Object} fakeEvent Event object that looks and quacks like a
* native DOM KeyPress event.
*/
_onBrowserKeypress(fakeEvent) {
const FAYT_LINKS_KEY =
"'";
const FAYT_TEXT_KEY =
"/";
if (!
this.hidden &&
this._findField == document.activeElement) {
this._dispatchKeypressEvent(
this._findField, fakeEvent);
return;
}
if (
this.findMode !=
this.FIND_NORMAL &&
this._quickFindTimeout) {
this._findField.select();
this._findField.focus();
this._dispatchKeypressEvent(
this._findField, fakeEvent);
return;
}
let key = fakeEvent.charCode
? String.fromCharCode(fakeEvent.charCode)
:
null;
let manualstartFAYT =
(key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY) &&
this._manualFAYT;
let autostartFAYT =
!manualstartFAYT &&
this._findAsYouType && key && key !=
" ";
if (manualstartFAYT || autostartFAYT) {
let mode =
key == FAYT_LINKS_KEY || (autostartFAYT &&
this._typeAheadLinksOnly)
?
this.FIND_LINKS
:
this.FIND_TYPEAHEAD;
// Clear bar first, so that when openFindBar() calls setCaseSensitivity()
// it doesn't get confused by a lingering value
this._findField.value =
"";
this.open(mode);
this._setFindCloseTimeout();
this._findField.select();
this._findField.focus();
if (autostartFAYT) {
this._dispatchKeypressEvent(
this._findField, fakeEvent);
}
else {
this._updateStatusUI(Ci.nsITypeAheadFind.FIND_FOUND);
}
}
}
_updateBrowserWithState() {
if (
this._browser) {
this._browser.sendMessageToActor(
"Findbar:UpdateState",
{
findMode:
this.findMode,
isOpenAndFocused:
!
this.hidden && document.activeElement ==
this._findField,
hasQuickFindTimeout: !!
this._quickFindTimeout,
},
"FindBar",
"all"
);
}
}
_enableFindButtons(aEnable) {
this.getElement(
"find-next").disabled =
this.getElement(
"find-previous"
).disabled = !aEnable;
}
/**
* Determines whether minimalist or general-purpose search UI is to be
* displayed when the find bar is activated.
*/
_updateFindUI() {
let showMinimalUI =
this.findMode !=
this.FIND_NORMAL;
let nodes =
this.getElement(
"findbar-container").children;
let wrapper =
this.getElement(
"findbar-textbox-wrapper");
let foundMatches =
this._foundMatches;
for (let node of nodes) {
if (node == wrapper || node == foundMatches) {
continue;
}
node.hidden = showMinimalUI;
}
this.getElement(
"find-next").hidden =
this.getElement(
"find-previous"
).hidden = showMinimalUI;
foundMatches.hidden = showMinimalUI || !foundMatches.value;
this._updateCaseSensitivity();
this._updateDiacriticMatching();
this._setEntireWord();
this._setHighlightAll();
if (showMinimalUI) {
this._findField.classList.add(
"minimal");
}
else {
this._findField.classList.remove(
"minimal");
}
let l10nId;
if (
this.findMode ==
this.FIND_TYPEAHEAD) {
l10nId =
"findbar-fast-find";
}
else if (
this.findMode ==
this.FIND_LINKS) {
l10nId =
"findbar-fast-find-links";
}
else {
l10nId =
"findbar-normal-find";
}
document.l10n.setAttributes(
this._findField, l10nId);
}
_find(value) {
if (!
this._dispatchFindEvent(
"")) {
return;
}
let val = value ||
this._findField.value;
// We have to carry around an explicit version of this, because
// finder.searchString doesn't update on failed searches.
this.browser._lastSearchString = val;
// Only search on input if we don't have a last-failed string,
// or if the current search string doesn't start with it.
// In entire-word mode we always attemp a find; since sequential matching
// is not guaranteed, the first character typed may not be a word (no
// match), but the with the second character it may well be a word,
// thus a match.
if (
!
this._findFailedString ||
!val.startsWith(
this._findFailedString) ||
this._entireWord
) {
// Getting here means the user commanded a find op. Make sure any
// initial prefilling is ignored if it hasn't happened yet.
if (
this._startFindDeferred) {
this._startFindDeferred.resolve();
this._startFindDeferred =
null;
}
this._enableFindButtons(val);
this._updateCaseSensitivity(val);
this._updateDiacriticMatching(val);
this._setEntireWord();
this.browser.finder.fastFind(
val,
this.findMode ==
this.FIND_LINKS,
this.findMode !=
this.FIND_NORMAL
);
}
if (
this.findMode !=
this.FIND_NORMAL) {
this._setFindCloseTimeout();
}
if (
this._findResetTimeout != -1) {
clearTimeout(
this._findResetTimeout);
}
// allow a search to happen on input again after a second has expired
// since the previous input, to allow for dynamic content and/ or page
// loading.
this._findResetTimeout = setTimeout(() => {
this._findFailedString =
null;
this._findResetTimeout = -1;
}, 1000);
}
_flash() {
if (
this._flashFindBarCount === undefined) {
this._flashFindBarCount =
this._initialFlashFindBarCount;
}
if (
this._flashFindBarCount-- == 0) {
clearInterval(
this._flashFindBarTimeout);
this._findField.removeAttribute(
"flash");
this._flashFindBarCount = 6;
return;
}
this._findField.setAttribute(
"flash",
this._flashFindBarCount % 2 == 0 ?
"false" :
"true"
);
}
_findAgain(findPrevious) {
this.browser.finder.findAgain(
this._findField.value,
findPrevious,
this.findMode ==
this.FIND_LINKS,
this.findMode !=
this.FIND_NORMAL
);
}
_updateStatusUI(res, findPrevious) {
let statusL10nId;
switch (res) {
case Ci.nsITypeAheadFind.FIND_WRAPPED:
this._findStatusIcon.setAttribute(
"status",
"wrapped");
this._findField.removeAttribute(
"status");
statusL10nId = findPrevious
?
"findbar-wrapped-to-bottom"
:
"findbar-wrapped-to-top";
break;
case Ci.nsITypeAheadFind.FIND_NOTFOUND:
this._findStatusDesc.setAttribute(
"status",
"notfound");
this._findStatusIcon.setAttribute(
"status",
"notfound");
this._findField.setAttribute(
"status",
"notfound");
this._foundMatches.hidden =
true;
statusL10nId =
"findbar-not-found";
break;
case Ci.nsITypeAheadFind.FIND_PENDING:
this._findStatusIcon.setAttribute(
"status",
"pending");
this._findField.removeAttribute(
"status");
this._findStatusDesc.removeAttribute(
"status");
statusL10nId =
"";
break;
case Ci.nsITypeAheadFind.FIND_FOUND:
default:
this._findStatusIcon.removeAttribute(
"status");
this._findField.removeAttribute(
"status");
this._findStatusDesc.removeAttribute(
"status");
statusL10nId =
"";
break;
}
if (statusL10nId) {
document.l10n.setAttributes(
this._findStatusDesc, statusL10nId);
}
else {
delete this._findStatusDesc.dataset.l10nId;
this._findStatusDesc.textContent =
"";
}
}
updateControlState(result, findPrevious) {
this._updateStatusUI(result, findPrevious);
this._enableFindButtons(
result !== Ci.nsITypeAheadFind.FIND_NOTFOUND && !!
this._findField.value
);
}
_dispatchFindEvent(type, findPrevious) {
let event = document.createEvent(
"CustomEvent");
event.initCustomEvent(
"find" + type,
true,
true, {
query:
this._findField.value,
caseSensitive: !!
this._typeAheadCaseSensitive,
matchDiacritics: !!
this._matchDiacritics,
entireWord:
this._entireWord,
highlightAll:
this._highlightAll,
findPrevious,
});
return this.dispatchEvent(event);
}
/**
* Opens the findbar, focuses the findfield and selects its contents.
* Also flashes the findbar the first time it's used.
*
* @param {Number} mode The find mode to be used, which is either
* FIND_NORMAL, FIND_TYPEAHEAD or FIND_LINKS. If not
* passed, we revert to the last find mode if any or
* FIND_NORMAL.
* @return {Promise} A promise that will be resolved when the findbar is
* fully opened.
*/
startFind(mode) {
let prefsvc = Services.prefs;
let userWantsPrefill =
true;
this.open(mode);
if (
this._flashFindBar) {
this._flashFindBarTimeout = setInterval(() =>
this._flash(), 500);
prefsvc.setIntPref(
"accessibility.typeaheadfind.flashBar",
--
this._flashFindBar
);
}
this._startFindDeferred = Promise.withResolvers();
let startFindPromise =
this._startFindDeferred.promise;
if (
this.prefillWithSelection) {
userWantsPrefill = prefsvc.getBoolPref(
"accessibility.typeaheadfind.prefillwithselection"
);
}
if (
this.prefillWithSelection && userWantsPrefill) {
this.browser.finder.getInitialSelection();
// NB: We have to focus this._findField here so tests that send
// key events can open and close the find bar synchronously.
this._findField.focus();
// (e10s) since we focus lets also select it, otherwise that would
// only happen in this.onCurrentSelection and, because it is async,
// there's a chance keypresses could come inbetween, leading to
// jumbled up queries.
this._findField.select();
return startFindPromise;
}
// If userWantsPrefill is false but prefillWithSelection is true,
// then we might need to check the selection clipboard. Call
// onCurrentSelection to do so.
// Note: this.onCurrentSelection clears this._startFindDeferred.
this.onCurrentSelection(
"",
true);
return startFindPromise;
}
/**
* Convenient alias to startFind(gFindBar.FIND_NORMAL);
*
* You should generally map the window's find command to this method.
* e.g. <command name="cmd_find" oncommand="gFindBar.onFindCommand();"/>
*/
onFindCommand() {
return this.startFind(
this.FIND_NORMAL);
}
/**
* Stub for find-next and find-previous commands.
*
* @param {Boolean} findPrevious `true` for find-previous, `false`
* otherwise.
*/
onFindAgainCommand(findPrevious) {
if (findPrevious) {
Glean.findbar.findPrev.add(1);
}
else {
Glean.findbar.findNext.add(1);
}
let findString =
this._browser.finder.searchString ||
this._findField.value;
if (!findString) {
return this.startFind();
}
// We dispatch the findAgain event here instead of in _findAgain since
// if there is a find event handler that prevents the default then
// finder.searchString will never get updated which in turn means
// there would never be findAgain events because of the logic below.
if (!
this._dispatchFindEvent(
"again", findPrevious)) {
return undefined;
}
// user explicitly requested another search, so do it even if we think it'll fail
this._findFailedString =
null;
// Ensure the stored SearchString is in sync with what we want to find
if (
this._findField.value !=
this._browser.finder.searchString) {
this._find(
this._findField.value);
}
else {
this._findAgain(findPrevious);
if (
this._useModalHighlight) {
this.open();
this._findField.focus();
}
}
return undefined;
}
/**
* Fetches the currently selected text and sets that as the text to search
* next. This is a MacOS specific feature.
*/
onFindSelectionCommand() {
this.browser.finder.setSearchStringToSelection().then(searchInfo => {
if (searchInfo.selectedText) {
this._findField.value = searchInfo.selectedText;
}
});
}
_onAppActivateMac() {
const kPref =
"accessibility.typeaheadfind.prefillwithselection";
if (
this.prefillWithSelection && Services.prefs.getBoolPref(kPref)) {
return;
}
let clipboardSearchString =
this._browser.finder.clipboardSearchString;
if (
clipboardSearchString &&
this._findField.value != clipboardSearchString &&
!
this._findField._willfullyDeleted
) {
this._findField.value = clipboardSearchString;
this._findField._hadValue =
true;
// Changing the search string makes the previous status invalid, so
// we better clear it here.
this._updateStatusUI();
}
}
/**
* This handles all the result changes for both type-ahead-find and
* highlighting.
*
* @param {Object} data A dictionary that holds the following properties:
* - {Number} result One of the FIND_* constants
* indicating the result of a search
* operation.
* - {Boolean} findBackwards If the search was done
* from the bottom to the
* top. This is used for
* status messages when
* reaching "the end of the
* page".
* - {String} linkURL When a link matched, then its
* URL. Always null when not in
* FIND_LINKS mode.
*/
onFindResult(data) {
if (data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
// If an explicit Find Again command fails, re-open the toolbar.
if (data.storeResult &&
this.open()) {
this._findField.select();
this._findField.focus();
}
this._findFailedString = data.searchString;
}
else {
this._findFailedString =
null;
}
this._updateStatusUI(data.result, data.findBackwards);
this._updateStatusUIBar(data.linkURL);
if (
this.findMode !=
this.FIND_NORMAL) {
this._setFindCloseTimeout();
}
}
/**
* This handles all the result changes for matches counts.
*
* @param {Object} result Result Object, containing the total amount of
* matches and a vector of the current result.
* - {Number} total Total count number of matches found.
* - {Number} limit Current setting of the number of matches
* to hit to hit the limit.
* - {Number} current Vector of the current result.
*/
onMatchesCountResult(result) {
if (!result.total) {
delete this._foundMatches.dataset.l10nId;
this._foundMatches.hidden =
true;
this._foundMatches.setAttribute(
"value",
"");
}
else {
const l10nId =
result.total === -1
?
"findbar-found-matches-count-limit"
:
"findbar-found-matches";
this._foundMatches.hidden =
false;
document.l10n.setAttributes(
this._foundMatches, l10nId, result);
}
}
onHighlightFinished() {
// Noop.
}
onCurrentSelection(selectionString, isInitialSelection) {
// Ignore the prefill if the user has already typed in the findbar,
// it would have been overwritten anyway. See bug 1198465.
if (isInitialSelection && !
this._startFindDeferred) {
return;
}
if (
AppConstants.platform ==
"macosx" &&
isInitialSelection &&
!selectionString
) {
let clipboardSearchString =
this.browser.finder.clipboardSearchString;
if (clipboardSearchString) {
selectionString = clipboardSearchString;
}
}
if (selectionString) {
this._findField.value = selectionString;
}
if (isInitialSelection) {
this._enableFindButtons(!!
this._findField.value);
this._findField.select();
this._findField.focus();
this._startFindDeferred.resolve();
this._startFindDeferred =
null;
}
}
/**
* This handler may cancel a request to focus content by returning |false|
* explicitly.
*/
shouldFocusContent() {
const fm = Services.focus;
if (fm.focusedWindow != window) {
return false;
}
let focusedElement = fm.focusedElement;
if (!focusedElement) {
return false;
}
let focusedParent = focusedElement.closest(
"findbar");
if (focusedParent !=
this && focusedParent !=
this._findField) {
return false;
}
return true;
}
disconnectedCallback() {
// Empty the DOM. We will rebuild if reconnected.
while (
this.lastChild) {
this.removeChild(
this.lastChild);
}
this.destroy();
}
}
customElements.define(
"findbar", MozFindbar);
}