summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/findbar.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/findbar.js')
-rw-r--r--toolkit/content/widgets/findbar.js1379
1 files changed, 1379 insertions, 0 deletions
diff --git a/toolkit/content/widgets/findbar.js b/toolkit/content/widgets/findbar.js
new file mode 100644
index 0000000000..3cbce11771
--- /dev/null
+++ b/toolkit/content/widgets/findbar.js
@@ -0,0 +1,1379 @@
+/* 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"
+ 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>
+ <checkbox anonid="highlight" class="findbar-highlight tabbable"
+ data-l10n-id="findbar-highlight-all2" oncommand="toggleHighlight(this.checked);"/>
+ <checkbox anonid="find-case-sensitive" class="findbar-case-sensitive tabbable"
+ data-l10n-id="findbar-case-sensitive" oncommand="_setCaseSensitivity(this.checked ? 1 : 0);"/>
+ <checkbox anonid="find-match-diacritics" class="findbar-match-diacritics tabbable"
+ data-l10n-id="findbar-match-diacritics" oncommand="_setDiacriticMatching(this.checked ? 1 : 0);"/>
+ <checkbox anonid="find-entire-word" class="findbar-entire-word tabbable"
+ data-l10n-id="findbar-entire-word" oncommand="toggleEntireWord(this.checked);"/>
+ <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" 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);
+ 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", 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();
+ }
+
+ 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) {
+ 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;
+
+ // 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");
+ 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;
+
+ // 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");
+
+ 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;
+
+ // 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);
+
+ 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;
+ }
+
+ 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 = "";
+ 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) {
+ 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) {
+ 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(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);
+}