/* 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 ` `; } 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. */ 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); }