fune/toolkit/content/widgets/findbar.js
Emma Malysz 2bdc940a0c Bug 1627688, add telemetry scalars for 'Find Toolbar' buttons. r=mikedeboer
This will track how many times the user opens the find toolbar and clicks
the find previous, find next, highlight all, match case, match diacritics,
and whole words buttons.

Differential Revision: https://phabricator.services.mozilla.com/D78182
2020-06-09 21:33:26 +00:00

1399 lines
45 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/. */
"use strict";
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
let LazyConstants = {};
ChromeUtils.defineModuleGetter(
LazyConstants,
"PluralForm",
"resource://gre/modules/PluralForm.jsm"
);
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 findbar-find-fast" />
<toolbarbutton anonid="find-previous" class="findbar-find-previous tabbable"
data-l10n-attrs="tooltiptext" data-l10n-id="findbar-previous"
oncommand="onFindAgainCommand(true);" disabled="true" />
<toolbarbutton anonid="find-next" class="findbar-find-next tabbable"
data-l10n-id="findbar-next" oncommand="onFindAgainCommand(false);" disabled="true" />
</hbox>
<toolbarbutton anonid="highlight" class="findbar-highlight findbar-button tabbable"
data-l10n-id="findbar-highlight-all2" oncommand="toggleHighlight(this.checked);" type="checkbox" />
<toolbarbutton anonid="find-case-sensitive" class="findbar-case-sensitive findbar-button tabbable"
data-l10n-id="findbar-case-sensitive" oncommand="_setCaseSensitivity(this.checked ? 1 : 0);" type="checkbox" />
<toolbarbutton anonid="find-match-diacritics" class="findbar-match-diacritics findbar-button tabbable"
data-l10n-id="findbar-match-diacritics" oncommand="_setDiacriticMatching(this.checked ? 1 : 0);" type="checkbox" />
<toolbarbutton anonid="find-entire-word" class="findbar-entire-word findbar-button tabbable"
data-l10n-id="findbar-entire-word" oncommand="toggleEntireWord(this.checked);" type="checkbox" />
<label anonid="match-case-status" class="findbar-find-fast" />
<label anonid="match-diacritics-status" class="findbar-find-fast" />
<label anonid="entire-word-status" class="findbar-find-fast" />
<label anonid="found-matches" class="findbar-find-fast found-matches" hidden="true" />
<image anonid="find-status-icon" class="findbar-find-fast find-status-icon" />
<description anonid="find-status" control="findbar-textbox" class="findbar-find-fast findbar-find-status" />
</hbox>
<toolbarbutton anonid="find-closebutton" class="findbar-closebutton close-icon"
data-l10n-id="findbar-find-button-close" oncommand="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);
/**
* Please keep in sync with toolkit/modules/FindBarContent.jsm
*/
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._strBundle = null;
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", event => {
// 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", event => {
// Note: This code used to remove the selection
// if it matched an editable.
this.browser.finder.enableSelection();
});
this._findField.addEventListener("focus", event => {
this._updateBrowserWithState();
});
this._findField.addEventListener("compositionstart", event => {
// 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", event => {
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();
});
}
set findMode(val) {
this._findMode = val;
this._updateBrowserWithState();
return val;
}
get findMode() {
return this._findMode;
}
set prefillWithSelection(val) {
this.setAttribute("prefillwithselection", val);
return 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) {
let tm = this._findField.editor.transactionManager;
return !!(tm.numberOfUndoItems || tm.numberOfRedoItems);
}
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);
}
return val;
}
get browser() {
if (!this._browser) {
this._browser = document.getElementById(this.getAttribute("browserid"));
}
return this._browser;
}
get strBundle() {
if (!this._strBundle) {
this._strBundle = Services.strings.createBundle(
"chrome://global/locale/findbar.properties"
);
}
return this._strBundle;
}
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 the destructor isn't called when we are removed
* from a document that is not destroyed. This needs to be explicitly called
* in this case.
*/
destroy() {
if (this._destroyed) {
return;
}
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
if (this.browser && this.browser.finder) {
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) {
Services.telemetry.scalarAdd("findbar.highlight_all", 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;
statusLabel.value = caseSensitive ? this._caseSensitiveStr : "";
// Show the checkbox on the full Find bar in non-auto mode.
// Show the label in all other cases.
let hideCheckbox =
this.findMode != this.FIND_NORMAL ||
(this._typeAheadCaseSensitive != 0 &&
this._typeAheadCaseSensitive != 1);
checkbox.hidden = hideCheckbox;
statusLabel.hidden = !hideCheckbox;
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");
Services.telemetry.scalarAdd("findbar.match_case", 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;
statusLabel.value = matchDiacritics ? this._matchDiacriticsStr : "";
// Show the checkbox on the full Find bar in non-auto mode.
// Show the label in all other cases.
let hideCheckbox =
this.findMode != this.FIND_NORMAL ||
(this._matchDiacritics != 0 && this._matchDiacritics != 1);
checkbox.hidden = hideCheckbox;
statusLabel.hidden = !hideCheckbox;
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");
Services.telemetry.scalarAdd("findbar.match_diacritics", 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;
statusLabel.value = entireWord ? this._entireWordStr : "";
// Show the checkbox on the full Find bar in non-auto mode.
// Show the label in all other cases.
let hideCheckbox = this.findMode != this.FIND_NORMAL;
checkbox.hidden = hideCheckbox;
statusLabel.hidden = !hideCheckbox;
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);
Services.telemetry.scalarAdd("findbar.whole_words", 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;
}
if (!this._notFoundStr) {
var bundle = this.strBundle;
this._notFoundStr = bundle.GetStringFromName("NotFound");
this._wrappedToTopStr = bundle.GetStringFromName("WrappedToTop");
this._wrappedToBottomStr = bundle.GetStringFromName("WrappedToBottom");
this._normalFindStr = bundle.GetStringFromName("NormalFind");
this._fastFindStr = bundle.GetStringFromName("FastFind");
this._fastFindLinksStr = bundle.GetStringFromName("FastFindLinks");
this._caseSensitiveStr = bundle.GetStringFromName("CaseSensitive");
this._matchDiacriticsStr = bundle.GetStringFromName("MatchDiacritics");
this._entireWordStr = bundle.GetStringFromName("EntireWord");
}
this._findFailedString = null;
this._updateFindUI();
if (this.hidden) {
Services.telemetry.scalarAdd("findbar.shown", 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 = "";
if (this._findField.editor) {
this._findField.editor.transactionManager.clear();
}
this.toggleHighlight(false);
this._updateStatusUI();
this._enableFindButtons(false);
}
_dispatchKeypressEvent(target, fakeEvent) {
if (!target) {
return;
}
// The event information comes from the child process. If we need more
// properties/information here, change the list of sent properties in
// browser-content.js.
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");
}
if (this.findMode == this.FIND_TYPEAHEAD) {
this._findField.placeholder = this._fastFindStr;
} else if (this.findMode == this.FIND_LINKS) {
this._findField.placeholder = this._fastFindLinksStr;
} else {
this._findField.placeholder = this._normalFindStr;
}
}
_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) {
switch (res) {
case Ci.nsITypeAheadFind.FIND_WRAPPED:
this._findStatusIcon.setAttribute("status", "wrapped");
this._findStatusDesc.textContent = findPrevious
? this._wrappedToBottomStr
: this._wrappedToTopStr;
this._findField.removeAttribute("status");
break;
case Ci.nsITypeAheadFind.FIND_NOTFOUND:
this._findStatusIcon.setAttribute("status", "notfound");
this._findStatusDesc.textContent = this._notFoundStr;
this._findField.setAttribute("status", "notfound");
break;
case Ci.nsITypeAheadFind.FIND_PENDING:
this._findStatusIcon.setAttribute("status", "pending");
this._findStatusDesc.textContent = "";
this._findField.removeAttribute("status");
break;
case Ci.nsITypeAheadFind.FIND_FOUND:
default:
this._findStatusIcon.removeAttribute("status");
this._findStatusDesc.textContent = "";
this._findField.removeAttribute("status");
break;
}
}
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
);
}
let { PromiseUtils } = ChromeUtils.import(
"resource://gre/modules/PromiseUtils.jsm"
);
this._startFindDeferred = PromiseUtils.defer();
let startFindPromise = this._startFindDeferred.promise;
if (this.prefillWithSelection) {
userWantsPrefill = prefsvc.getBoolPref(
"accessibility.typeaheadfind.prefillwithselection"
);
}
if (this.prefillWithSelection && userWantsPrefill) {
// 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();
this.browser.finder.getInitialSelection();
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) {
Services.telemetry.scalarAdd("findbar.find_prev", 1);
} else {
Services.telemetry.scalarAdd("findbar.find_next", 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 !== 0) {
if (result.total == -1) {
this._foundMatches.value = LazyConstants.PluralForm.get(
result.limit,
this.strBundle.GetStringFromName("FoundMatchesCountLimit")
).replace("#1", result.limit);
} else {
this._foundMatches.value = LazyConstants.PluralForm.get(
result.total,
this.strBundle.GetStringFromName("FoundMatches")
)
.replace("#1", result.current)
.replace("#2", result.total);
}
this._foundMatches.hidden = false;
} else {
this._foundMatches.hidden = true;
this._foundMatches.value = "";
}
}
onHighlightFinished(result) {
// 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);
}