mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			858 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			858 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// vim: set ts=2 sw=2 sts=2 tw=80:
 | 
						|
// This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
// License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
import { Rect } from "resource://gre/modules/Geometry.sys.mjs";
 | 
						|
 | 
						|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  FinderIterator: "resource://gre/modules/FinderIterator.sys.mjs",
 | 
						|
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  lazy,
 | 
						|
  "ClipboardHelper",
 | 
						|
  "@mozilla.org/widget/clipboardhelper;1",
 | 
						|
  "nsIClipboardHelper"
 | 
						|
);
 | 
						|
 | 
						|
const kSelectionMaxLen = 150;
 | 
						|
const kMatchesCountLimitPref = "accessibility.typeaheadfind.matchesCountLimit";
 | 
						|
 | 
						|
const activeFinderRoots = new WeakSet();
 | 
						|
 | 
						|
export function Finder(docShell) {
 | 
						|
  this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(
 | 
						|
    Ci.nsITypeAheadFind
 | 
						|
  );
 | 
						|
  this._fastFind.init(docShell);
 | 
						|
 | 
						|
  this._currentFoundRange = null;
 | 
						|
  this._docShell = docShell;
 | 
						|
  this._listeners = [];
 | 
						|
  this._previousLink = null;
 | 
						|
  this._searchString = null;
 | 
						|
  this._highlighter = null;
 | 
						|
 | 
						|
  docShell
 | 
						|
    .QueryInterface(Ci.nsIInterfaceRequestor)
 | 
						|
    .getInterface(Ci.nsIWebProgress)
 | 
						|
    .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
 | 
						|
  docShell.domWindow.addEventListener(
 | 
						|
    "unload",
 | 
						|
    this.onLocationChange.bind(this, { isTopLevel: true })
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
Finder.isFindbarVisible = function (docShell) {
 | 
						|
  return activeFinderRoots.has(docShell.browsingContext.top);
 | 
						|
};
 | 
						|
 | 
						|
Finder.prototype = {
 | 
						|
  get iterator() {
 | 
						|
    if (!this._iterator) {
 | 
						|
      this._iterator = new lazy.FinderIterator();
 | 
						|
    }
 | 
						|
    return this._iterator;
 | 
						|
  },
 | 
						|
 | 
						|
  destroy() {
 | 
						|
    if (this._iterator) {
 | 
						|
      this._iterator.reset();
 | 
						|
    }
 | 
						|
    let window = this._getWindow();
 | 
						|
    if (this._highlighter && window) {
 | 
						|
      // if we clear all the references before we hide the highlights (in both
 | 
						|
      // highlighting modes), we simply can't use them to find the ranges we
 | 
						|
      // need to clear from the selection.
 | 
						|
      this._highlighter.hide(window);
 | 
						|
      this._highlighter.clear(window);
 | 
						|
      this.highlighter.removeScrollMarks();
 | 
						|
    }
 | 
						|
    this.listeners = [];
 | 
						|
    this._docShell
 | 
						|
      .QueryInterface(Ci.nsIInterfaceRequestor)
 | 
						|
      .getInterface(Ci.nsIWebProgress)
 | 
						|
      .removeProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
 | 
						|
    this._listeners = [];
 | 
						|
    this._currentFoundRange =
 | 
						|
      this._fastFind =
 | 
						|
      this._docShell =
 | 
						|
      this._previousLink =
 | 
						|
      this._highlighter =
 | 
						|
        null;
 | 
						|
  },
 | 
						|
 | 
						|
  addResultListener(aListener) {
 | 
						|
    if (!this._listeners.includes(aListener)) {
 | 
						|
      this._listeners.push(aListener);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  removeResultListener(aListener) {
 | 
						|
    this._listeners = this._listeners.filter(l => l != aListener);
 | 
						|
  },
 | 
						|
 | 
						|
  _setResults(options) {
 | 
						|
    if (typeof options.storeResult != "boolean") {
 | 
						|
      options.storeResult = true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (options.storeResult) {
 | 
						|
      this._searchString = options.searchString;
 | 
						|
      this.clipboardSearchString = options.searchString;
 | 
						|
    }
 | 
						|
 | 
						|
    let foundLink = this._fastFind.foundLink;
 | 
						|
    let linkURL = null;
 | 
						|
    if (foundLink) {
 | 
						|
      linkURL = Services.textToSubURI.unEscapeURIForUI(foundLink.href);
 | 
						|
    }
 | 
						|
 | 
						|
    options.linkURL = linkURL;
 | 
						|
    options.rect = this._getResultRect();
 | 
						|
    options.searchString = this._searchString;
 | 
						|
 | 
						|
    this._outlineLink(options.drawOutline);
 | 
						|
 | 
						|
    for (let l of this._listeners) {
 | 
						|
      try {
 | 
						|
        l.onFindResult(options);
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  get searchString() {
 | 
						|
    if (!this._searchString && this._fastFind.searchString) {
 | 
						|
      this._searchString = this._fastFind.searchString;
 | 
						|
    }
 | 
						|
    return this._searchString;
 | 
						|
  },
 | 
						|
 | 
						|
  get clipboardSearchString() {
 | 
						|
    return GetClipboardSearchString(
 | 
						|
      this._getWindow().docShell.QueryInterface(Ci.nsILoadContext)
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  set clipboardSearchString(aSearchString) {
 | 
						|
    if (!lazy.PrivateBrowsingUtils.isContentWindowPrivate(this._getWindow())) {
 | 
						|
      SetClipboardSearchString(aSearchString);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  set caseSensitive(aSensitive) {
 | 
						|
    if (this._fastFind.caseSensitive === aSensitive) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._fastFind.caseSensitive = aSensitive;
 | 
						|
    this.iterator.reset();
 | 
						|
  },
 | 
						|
 | 
						|
  set matchDiacritics(aMatchDiacritics) {
 | 
						|
    if (this._fastFind.matchDiacritics === aMatchDiacritics) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._fastFind.matchDiacritics = aMatchDiacritics;
 | 
						|
    this.iterator.reset();
 | 
						|
  },
 | 
						|
 | 
						|
  set entireWord(aEntireWord) {
 | 
						|
    if (this._fastFind.entireWord === aEntireWord) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._fastFind.entireWord = aEntireWord;
 | 
						|
    this.iterator.reset();
 | 
						|
  },
 | 
						|
 | 
						|
  get highlighter() {
 | 
						|
    if (this._highlighter) {
 | 
						|
      return this._highlighter;
 | 
						|
    }
 | 
						|
 | 
						|
    const { FinderHighlighter } = ChromeUtils.importESModule(
 | 
						|
      "resource://gre/modules/FinderHighlighter.sys.mjs"
 | 
						|
    );
 | 
						|
    return (this._highlighter = new FinderHighlighter(this));
 | 
						|
  },
 | 
						|
 | 
						|
  get matchesCountLimit() {
 | 
						|
    if (typeof this._matchesCountLimit == "number") {
 | 
						|
      return this._matchesCountLimit;
 | 
						|
    }
 | 
						|
 | 
						|
    this._matchesCountLimit =
 | 
						|
      Services.prefs.getIntPref(kMatchesCountLimitPref) || 0;
 | 
						|
    return this._matchesCountLimit;
 | 
						|
  },
 | 
						|
 | 
						|
  _lastFindResult: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used for normal search operations, highlights the first match.
 | 
						|
   * This method is used only for compatibility with non-remote browsers.
 | 
						|
   *
 | 
						|
   * @param aSearchString String to search for.
 | 
						|
   * @param aLinksOnly Only consider nodes that are links for the search.
 | 
						|
   * @param aDrawOutline Puts an outline around matched links.
 | 
						|
   */
 | 
						|
  fastFind(aSearchString, aLinksOnly, aDrawOutline) {
 | 
						|
    this._lastFindResult = this._fastFind.find(
 | 
						|
      aSearchString,
 | 
						|
      aLinksOnly,
 | 
						|
      Ci.nsITypeAheadFind.FIND_INITIAL,
 | 
						|
      false
 | 
						|
    );
 | 
						|
    let searchString = this._fastFind.searchString;
 | 
						|
 | 
						|
    let results = {
 | 
						|
      searchString,
 | 
						|
      result: this._lastFindResult,
 | 
						|
      findBackwards: false,
 | 
						|
      findAgain: false,
 | 
						|
      drawOutline: aDrawOutline,
 | 
						|
      linksOnly: aLinksOnly,
 | 
						|
      useSubFrames: true,
 | 
						|
    };
 | 
						|
 | 
						|
    this._setResults(results);
 | 
						|
    this.updateHighlightAndMatchCount(results);
 | 
						|
 | 
						|
    return this._lastFindResult;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Repeat the previous search. Should only be called after a previous
 | 
						|
   * call to Finder.fastFind.
 | 
						|
   * This method is used only for compatibility with non-remote browsers.
 | 
						|
   *
 | 
						|
   * @param aSearchString String to search for.
 | 
						|
   * @param aFindBackwards Controls the search direction:
 | 
						|
   *    true: before current match, false: after current match.
 | 
						|
   * @param aLinksOnly Only consider nodes that are links for the search.
 | 
						|
   * @param aDrawOutline Puts an outline around matched links.
 | 
						|
   */
 | 
						|
  findAgain(aSearchString, aFindBackwards, aLinksOnly, aDrawOutline) {
 | 
						|
    let mode = aFindBackwards
 | 
						|
      ? Ci.nsITypeAheadFind.FIND_PREVIOUS
 | 
						|
      : Ci.nsITypeAheadFind.FIND_NEXT;
 | 
						|
    this._lastFindResult = this._fastFind.find(
 | 
						|
      aFindBackwards,
 | 
						|
      aLinksOnly,
 | 
						|
      mode,
 | 
						|
      false
 | 
						|
    );
 | 
						|
    let searchString = this._fastFind.searchString;
 | 
						|
 | 
						|
    let results = {
 | 
						|
      searchString,
 | 
						|
      result: this._lastFindResult,
 | 
						|
      findBackwards: aFindBackwards,
 | 
						|
      findAgain: true,
 | 
						|
      drawOutline: aDrawOutline,
 | 
						|
      linksOnly: aLinksOnly,
 | 
						|
      useSubFrames: true,
 | 
						|
    };
 | 
						|
    this._setResults(results);
 | 
						|
    this.updateHighlightAndMatchCount(results);
 | 
						|
 | 
						|
    return this._lastFindResult;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Used for normal search operations, highlights the first or
 | 
						|
   * subsequent match depending on the mode.
 | 
						|
   *
 | 
						|
   * Options are:
 | 
						|
   *  searchString String to search for.
 | 
						|
   *  findAgain True if this a find again operation.
 | 
						|
   *  mode Search mode from nsITypeAheadFind.
 | 
						|
   *  linksOnly Only consider nodes that are links for the search.
 | 
						|
   *  drawOutline Puts an outline around matched links.
 | 
						|
   *  useSubFrames True to iterate over subframes.
 | 
						|
   *  caseSensitive True for case sensitive searching.
 | 
						|
   *  entireWord True to match entire words.
 | 
						|
   *  matchDiacritics True to match diacritics.
 | 
						|
   */
 | 
						|
  find(options) {
 | 
						|
    this.caseSensitive = options.caseSensitive;
 | 
						|
    this.entireWord = options.entireWord;
 | 
						|
    this.matchDiacritics = options.matchDiacritics;
 | 
						|
 | 
						|
    this._lastFindResult = this._fastFind.find(
 | 
						|
      options.searchString,
 | 
						|
      options.linksOnly,
 | 
						|
      options.mode,
 | 
						|
      !options.useSubFrames
 | 
						|
    );
 | 
						|
    let searchString = this._fastFind.searchString;
 | 
						|
    let results = {
 | 
						|
      searchString,
 | 
						|
      result: this._lastFindResult,
 | 
						|
      findBackwards:
 | 
						|
        options.mode == Ci.nsITypeAheadFind.FIND_PREVIOUS ||
 | 
						|
        options.mode == Ci.nsITypeAheadFind.FIND_LAST,
 | 
						|
      findAgain: options.findAgain,
 | 
						|
      drawOutline: options.drawOutline,
 | 
						|
      linksOnly: options.linksOnly,
 | 
						|
      entireWord: this._fastFind.entireWord,
 | 
						|
      useSubFrames: options.useSubFrames,
 | 
						|
    };
 | 
						|
    this._setResults(results, options.mode);
 | 
						|
    return new Promise(resolve => resolve(results));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Forcibly set the search string of the find clipboard to the currently
 | 
						|
   * selected text in the window, on supported platforms (i.e. OSX).
 | 
						|
   */
 | 
						|
  setSearchStringToSelection() {
 | 
						|
    let searchInfo = this.getActiveSelectionText();
 | 
						|
 | 
						|
    // If an empty string is returned or a subframe is focused, don't
 | 
						|
    // assign the search string.
 | 
						|
    if (searchInfo.selectedText) {
 | 
						|
      this.clipboardSearchString = searchInfo.selectedText;
 | 
						|
    }
 | 
						|
 | 
						|
    return searchInfo;
 | 
						|
  },
 | 
						|
 | 
						|
  async highlight(aHighlight, aWord, aLinksOnly, aUseSubFrames = true) {
 | 
						|
    return this.highlighter.highlight(
 | 
						|
      aHighlight,
 | 
						|
      aWord,
 | 
						|
      aLinksOnly,
 | 
						|
      false,
 | 
						|
      aUseSubFrames
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  async updateHighlightAndMatchCount(aArgs) {
 | 
						|
    this._lastFindResult = aArgs;
 | 
						|
 | 
						|
    if (
 | 
						|
      !this.iterator.continueRunning({
 | 
						|
        caseSensitive: this._fastFind.caseSensitive,
 | 
						|
        entireWord: this._fastFind.entireWord,
 | 
						|
        linksOnly: aArgs.linksOnly,
 | 
						|
        matchDiacritics: this._fastFind.matchDiacritics,
 | 
						|
        word: aArgs.searchString,
 | 
						|
        useSubFrames: aArgs.useSubFrames,
 | 
						|
      })
 | 
						|
    ) {
 | 
						|
      this.iterator.stop();
 | 
						|
    }
 | 
						|
 | 
						|
    let highlightPromise = this.highlighter.update(
 | 
						|
      aArgs,
 | 
						|
      aArgs.useSubFrames ? false : aArgs.foundInThisFrame
 | 
						|
    );
 | 
						|
    let matchCountPromise = this.requestMatchesCount(
 | 
						|
      aArgs.searchString,
 | 
						|
      aArgs.linksOnly,
 | 
						|
      aArgs.useSubFrames
 | 
						|
    );
 | 
						|
 | 
						|
    let results = await Promise.all([highlightPromise, matchCountPromise]);
 | 
						|
 | 
						|
    this.highlighter.updateScrollMarks();
 | 
						|
 | 
						|
    if (results[1]) {
 | 
						|
      return Object.assign(results[1], results[0]);
 | 
						|
    } else if (results[0]) {
 | 
						|
      return results[0];
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  getInitialSelection() {
 | 
						|
    let initialSelection = this.getActiveSelectionText().selectedText;
 | 
						|
    this._getWindow().setTimeout(() => {
 | 
						|
      for (let l of this._listeners) {
 | 
						|
        try {
 | 
						|
          l.onCurrentSelection(initialSelection, true);
 | 
						|
        } catch (ex) {}
 | 
						|
      }
 | 
						|
    }, 0);
 | 
						|
  },
 | 
						|
 | 
						|
  getActiveSelectionText() {
 | 
						|
    let focusedWindow = {};
 | 
						|
    let focusedElement = Services.focus.getFocusedElementForWindow(
 | 
						|
      this._getWindow(),
 | 
						|
      true,
 | 
						|
      focusedWindow
 | 
						|
    );
 | 
						|
    focusedWindow = focusedWindow.value;
 | 
						|
 | 
						|
    let selText;
 | 
						|
 | 
						|
    // If this is a remote subframe, return an empty string but
 | 
						|
    // indiciate which browsing context was focused.
 | 
						|
    if (
 | 
						|
      focusedElement &&
 | 
						|
      "frameLoader" in focusedElement &&
 | 
						|
      BrowsingContext.isInstance(focusedElement.browsingContext)
 | 
						|
    ) {
 | 
						|
      return {
 | 
						|
        focusedChildBrowserContextId: focusedElement.browsingContext.id,
 | 
						|
        selectedText: "",
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    if (focusedElement && focusedElement.editor) {
 | 
						|
      // The user may have a selection in an input or textarea.
 | 
						|
      selText = focusedElement.editor.selectionController
 | 
						|
        .getSelection(Ci.nsISelectionController.SELECTION_NORMAL)
 | 
						|
        .toString();
 | 
						|
    } else {
 | 
						|
      // Look for any selected text on the actual page.
 | 
						|
      selText = focusedWindow.getSelection().toString();
 | 
						|
    }
 | 
						|
 | 
						|
    if (!selText) {
 | 
						|
      return { selectedText: "" };
 | 
						|
    }
 | 
						|
 | 
						|
    // Process our text to get rid of unwanted characters.
 | 
						|
    selText = selText.trim().replace(/\s+/g, " ");
 | 
						|
    let truncLength = kSelectionMaxLen;
 | 
						|
    if (selText.length > truncLength) {
 | 
						|
      let truncChar = selText.charAt(truncLength).charCodeAt(0);
 | 
						|
      if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
 | 
						|
        truncLength++;
 | 
						|
      }
 | 
						|
      selText = selText.substr(0, truncLength);
 | 
						|
    }
 | 
						|
 | 
						|
    return { selectedText: selText };
 | 
						|
  },
 | 
						|
 | 
						|
  enableSelection() {
 | 
						|
    this._fastFind.setSelectionModeAndRepaint(
 | 
						|
      Ci.nsISelectionController.SELECTION_ON
 | 
						|
    );
 | 
						|
    this._restoreOriginalOutline();
 | 
						|
  },
 | 
						|
 | 
						|
  removeSelection(keepHighlight) {
 | 
						|
    this._fastFind.collapseSelection();
 | 
						|
    this.enableSelection();
 | 
						|
    let window = this._getWindow();
 | 
						|
    if (keepHighlight) {
 | 
						|
      this.highlighter.clearCurrentOutline(window);
 | 
						|
    } else {
 | 
						|
      this.highlighter.clear(window);
 | 
						|
      this.highlighter.removeScrollMarks();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  focusContent() {
 | 
						|
    // Allow Finder listeners to cancel focusing the content.
 | 
						|
    for (let l of this._listeners) {
 | 
						|
      try {
 | 
						|
        if ("shouldFocusContent" in l && !l.shouldFocusContent()) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      } catch (ex) {
 | 
						|
        console.error(ex);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let fastFind = this._fastFind;
 | 
						|
    try {
 | 
						|
      // Try to find the best possible match that should receive focus and
 | 
						|
      // block scrolling on focus since find already scrolls. Further
 | 
						|
      // scrolling is due to user action, so don't override this.
 | 
						|
      if (fastFind.foundLink) {
 | 
						|
        Services.focus.setFocus(
 | 
						|
          fastFind.foundLink,
 | 
						|
          Services.focus.FLAG_NOSCROLL
 | 
						|
        );
 | 
						|
      } else if (fastFind.foundEditable) {
 | 
						|
        Services.focus.setFocus(
 | 
						|
          fastFind.foundEditable,
 | 
						|
          Services.focus.FLAG_NOSCROLL
 | 
						|
        );
 | 
						|
        fastFind.collapseSelection();
 | 
						|
      } else {
 | 
						|
        this._getWindow().focus();
 | 
						|
      }
 | 
						|
    } catch (e) {}
 | 
						|
  },
 | 
						|
 | 
						|
  onFindbarClose() {
 | 
						|
    this.enableSelection();
 | 
						|
    this.highlighter.highlight(false);
 | 
						|
    this.highlighter.removeScrollMarks();
 | 
						|
    this.iterator.reset();
 | 
						|
    activeFinderRoots.delete(this._docShell.browsingContext.top);
 | 
						|
  },
 | 
						|
 | 
						|
  onFindbarOpen() {
 | 
						|
    activeFinderRoots.add(this._docShell.browsingContext.top);
 | 
						|
  },
 | 
						|
 | 
						|
  onModalHighlightChange(useModalHighlight) {
 | 
						|
    if (this._highlighter) {
 | 
						|
      this._highlighter.onModalHighlightChange(useModalHighlight);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onHighlightAllChange(highlightAll) {
 | 
						|
    if (this._highlighter) {
 | 
						|
      this._highlighter.onHighlightAllChange(highlightAll);
 | 
						|
    }
 | 
						|
    if (this._iterator) {
 | 
						|
      this._iterator.reset();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  keyPress(aEvent) {
 | 
						|
    let controller = this._getSelectionController(this._getWindow());
 | 
						|
    let accelKeyPressed =
 | 
						|
      AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
 | 
						|
 | 
						|
    switch (aEvent.keyCode) {
 | 
						|
      case aEvent.DOM_VK_RETURN:
 | 
						|
        if (this._fastFind.foundLink) {
 | 
						|
          let view = this._fastFind.foundLink.ownerGlobal;
 | 
						|
          this._fastFind.foundLink.dispatchEvent(
 | 
						|
            new view.MouseEvent("click", {
 | 
						|
              view,
 | 
						|
              cancelable: true,
 | 
						|
              bubbles: true,
 | 
						|
              ctrlKey: aEvent.ctrlKey,
 | 
						|
              altKey: aEvent.altKey,
 | 
						|
              shiftKey: aEvent.shiftKey,
 | 
						|
              metaKey: aEvent.metaKey,
 | 
						|
            })
 | 
						|
          );
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case aEvent.DOM_VK_TAB:
 | 
						|
        let direction = Services.focus.MOVEFOCUS_FORWARD;
 | 
						|
        if (aEvent.shiftKey) {
 | 
						|
          direction = Services.focus.MOVEFOCUS_BACKWARD;
 | 
						|
        }
 | 
						|
        Services.focus.moveFocus(this._getWindow(), null, direction, 0);
 | 
						|
        break;
 | 
						|
      case aEvent.DOM_VK_PAGE_UP:
 | 
						|
        controller.scrollPage(false);
 | 
						|
        break;
 | 
						|
      case aEvent.DOM_VK_PAGE_DOWN:
 | 
						|
        controller.scrollPage(true);
 | 
						|
        break;
 | 
						|
      case aEvent.DOM_VK_UP:
 | 
						|
        if (accelKeyPressed) {
 | 
						|
          controller.completeScroll(false);
 | 
						|
        } else {
 | 
						|
          controller.scrollLine(false);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case aEvent.DOM_VK_DOWN:
 | 
						|
        if (accelKeyPressed) {
 | 
						|
          controller.completeScroll(true);
 | 
						|
        } else {
 | 
						|
          controller.scrollLine(true);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _notifyMatchesCount(aWord, result = this._currentMatchesCountResult) {
 | 
						|
    // The `_currentFound` property is only used for internal bookkeeping.
 | 
						|
    delete result._currentFound;
 | 
						|
    result.searchString = aWord;
 | 
						|
    result.limit = this.matchesCountLimit;
 | 
						|
    if (result.total == result.limit) {
 | 
						|
      result.total = -1;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let l of this._listeners) {
 | 
						|
      try {
 | 
						|
        l.onMatchesCountResult(result);
 | 
						|
      } catch (ex) {}
 | 
						|
    }
 | 
						|
 | 
						|
    this._currentMatchesCountResult = null;
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  async requestMatchesCount(aWord, aLinksOnly, aUseSubFrames = true) {
 | 
						|
    if (
 | 
						|
      this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
 | 
						|
      this.searchString == "" ||
 | 
						|
      !aWord ||
 | 
						|
      !this.matchesCountLimit
 | 
						|
    ) {
 | 
						|
      return this._notifyMatchesCount(aWord, {
 | 
						|
        total: 0,
 | 
						|
        current: 0,
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    this._currentFoundRange = this._fastFind.getFoundRange();
 | 
						|
 | 
						|
    let params = {
 | 
						|
      caseSensitive: this._fastFind.caseSensitive,
 | 
						|
      entireWord: this._fastFind.entireWord,
 | 
						|
      linksOnly: aLinksOnly,
 | 
						|
      matchDiacritics: this._fastFind.matchDiacritics,
 | 
						|
      word: aWord,
 | 
						|
      useSubFrames: aUseSubFrames,
 | 
						|
    };
 | 
						|
    if (!this.iterator.continueRunning(params)) {
 | 
						|
      this.iterator.stop();
 | 
						|
    }
 | 
						|
 | 
						|
    await this.iterator.start(
 | 
						|
      Object.assign(params, {
 | 
						|
        finder: this,
 | 
						|
        limit: this.matchesCountLimit,
 | 
						|
        listener: this,
 | 
						|
        useCache: true,
 | 
						|
        useSubFrames: aUseSubFrames,
 | 
						|
      })
 | 
						|
    );
 | 
						|
 | 
						|
    // Without a valid result, there's nothing to notify about. This happens
 | 
						|
    // when the iterator was started before and won the race.
 | 
						|
    if (!this._currentMatchesCountResult) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    return this._notifyMatchesCount(aWord);
 | 
						|
  },
 | 
						|
 | 
						|
  // FinderIterator listener implementation
 | 
						|
 | 
						|
  onIteratorRangeFound(range) {
 | 
						|
    let result = this._currentMatchesCountResult;
 | 
						|
    if (!result) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    ++result.total;
 | 
						|
    if (!result._currentFound) {
 | 
						|
      ++result.current;
 | 
						|
      result._currentFound =
 | 
						|
        this._currentFoundRange &&
 | 
						|
        range.startContainer == this._currentFoundRange.startContainer &&
 | 
						|
        range.startOffset == this._currentFoundRange.startOffset &&
 | 
						|
        range.endContainer == this._currentFoundRange.endContainer &&
 | 
						|
        range.endOffset == this._currentFoundRange.endOffset;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onIteratorReset() {},
 | 
						|
 | 
						|
  onIteratorRestart({ word, linksOnly, useSubFrames }) {
 | 
						|
    this.requestMatchesCount(word, linksOnly, useSubFrames);
 | 
						|
  },
 | 
						|
 | 
						|
  onIteratorStart() {
 | 
						|
    this._currentMatchesCountResult = {
 | 
						|
      total: 0,
 | 
						|
      current: 0,
 | 
						|
      _currentFound: false,
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  _getWindow() {
 | 
						|
    if (!this._docShell) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    return this._docShell.domWindow;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the bounding selection rect in CSS px relative to the origin of the
 | 
						|
   * top-level content document.
 | 
						|
   */
 | 
						|
  _getResultRect() {
 | 
						|
    let topWin = this._getWindow();
 | 
						|
    let win = this._fastFind.currentWindow;
 | 
						|
    if (!win) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    let selection = win.getSelection();
 | 
						|
    if (!selection.rangeCount || selection.isCollapsed) {
 | 
						|
      // The selection can be into an input or a textarea element.
 | 
						|
      let nodes = win.document.querySelectorAll("input, textarea");
 | 
						|
      for (let node of nodes) {
 | 
						|
        if (node.editor) {
 | 
						|
          try {
 | 
						|
            let sc = node.editor.selectionController;
 | 
						|
            selection = sc.getSelection(
 | 
						|
              Ci.nsISelectionController.SELECTION_NORMAL
 | 
						|
            );
 | 
						|
            if (selection.rangeCount && !selection.isCollapsed) {
 | 
						|
              break;
 | 
						|
            }
 | 
						|
          } catch (e) {
 | 
						|
            // If this textarea is hidden, then its selection controller might
 | 
						|
            // not be intialized. Ignore the failure.
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!selection.rangeCount || selection.isCollapsed) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    let utils = topWin.windowUtils;
 | 
						|
 | 
						|
    let scrollX = {},
 | 
						|
      scrollY = {};
 | 
						|
    utils.getScrollXY(false, scrollX, scrollY);
 | 
						|
 | 
						|
    for (let frame = win; frame != topWin; frame = frame.parent) {
 | 
						|
      let rect = frame.frameElement.getBoundingClientRect();
 | 
						|
      let left = frame.getComputedStyle(frame.frameElement).borderLeftWidth;
 | 
						|
      let top = frame.getComputedStyle(frame.frameElement).borderTopWidth;
 | 
						|
      scrollX.value += rect.left + parseInt(left, 10);
 | 
						|
      scrollY.value += rect.top + parseInt(top, 10);
 | 
						|
    }
 | 
						|
    let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect());
 | 
						|
    return rect.translate(scrollX.value, scrollY.value);
 | 
						|
  },
 | 
						|
 | 
						|
  _outlineLink(aDrawOutline) {
 | 
						|
    let foundLink = this._fastFind.foundLink;
 | 
						|
 | 
						|
    // Optimization: We are drawing outlines and we matched
 | 
						|
    // the same link before, so don't duplicate work.
 | 
						|
    if (foundLink == this._previousLink && aDrawOutline) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._restoreOriginalOutline();
 | 
						|
 | 
						|
    if (foundLink && aDrawOutline) {
 | 
						|
      // Backup original outline
 | 
						|
      this._tmpOutline = foundLink.style.outline;
 | 
						|
      this._tmpOutlineOffset = foundLink.style.outlineOffset;
 | 
						|
 | 
						|
      // Draw pseudo focus rect
 | 
						|
      // XXX Should we change the following style for FAYT pseudo focus?
 | 
						|
      // XXX Shouldn't we change default design if outline is visible
 | 
						|
      //     already?
 | 
						|
      // Don't set the outline-color, we should always use initial value.
 | 
						|
      foundLink.style.outline = "1px dotted";
 | 
						|
      foundLink.style.outlineOffset = "0";
 | 
						|
 | 
						|
      this._previousLink = foundLink;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _restoreOriginalOutline() {
 | 
						|
    // Removes the outline around the last found link.
 | 
						|
    if (this._previousLink) {
 | 
						|
      this._previousLink.style.outline = this._tmpOutline;
 | 
						|
      this._previousLink.style.outlineOffset = this._tmpOutlineOffset;
 | 
						|
      this._previousLink = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _getSelectionController(aWindow) {
 | 
						|
    // display: none iframes don't have a selection controller, see bug 493658
 | 
						|
    try {
 | 
						|
      if (!aWindow.innerWidth || !aWindow.innerHeight) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      // If getting innerWidth or innerHeight throws, we can't get a selection
 | 
						|
      // controller.
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    // Yuck. See bug 138068.
 | 
						|
    let docShell = aWindow.docShell;
 | 
						|
 | 
						|
    let controller = docShell
 | 
						|
      .QueryInterface(Ci.nsIInterfaceRequestor)
 | 
						|
      .getInterface(Ci.nsISelectionDisplay)
 | 
						|
      .QueryInterface(Ci.nsISelectionController);
 | 
						|
    return controller;
 | 
						|
  },
 | 
						|
 | 
						|
  // Start of nsIWebProgressListener implementation.
 | 
						|
 | 
						|
  onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
 | 
						|
    if (!aWebProgress.isTopLevel) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // Ignore events that don't change the document.
 | 
						|
    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Avoid leaking if we change the page.
 | 
						|
    this._lastFindResult = this._previousLink = this._currentFoundRange = null;
 | 
						|
    this.highlighter.onLocationChange();
 | 
						|
    this.iterator.reset();
 | 
						|
  },
 | 
						|
 | 
						|
  QueryInterface: ChromeUtils.generateQI([
 | 
						|
    "nsIWebProgressListener",
 | 
						|
    "nsISupportsWeakReference",
 | 
						|
  ]),
 | 
						|
};
 | 
						|
 | 
						|
export function GetClipboardSearchString(aLoadContext) {
 | 
						|
  let searchString = "";
 | 
						|
  if (
 | 
						|
    !Services.clipboard.isClipboardTypeSupported(
 | 
						|
      Services.clipboard.kFindClipboard
 | 
						|
    )
 | 
						|
  ) {
 | 
						|
    return searchString;
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
 | 
						|
      Ci.nsITransferable
 | 
						|
    );
 | 
						|
    trans.init(aLoadContext);
 | 
						|
    trans.addDataFlavor("text/plain");
 | 
						|
 | 
						|
    Services.clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
 | 
						|
 | 
						|
    let data = {};
 | 
						|
    trans.getTransferData("text/plain", data);
 | 
						|
    if (data.value) {
 | 
						|
      data = data.value.QueryInterface(Ci.nsISupportsString);
 | 
						|
      searchString = data.toString();
 | 
						|
    }
 | 
						|
  } catch (ex) {}
 | 
						|
 | 
						|
  return searchString;
 | 
						|
}
 | 
						|
 | 
						|
export function SetClipboardSearchString(aSearchString) {
 | 
						|
  if (
 | 
						|
    !aSearchString ||
 | 
						|
    !Services.clipboard.isClipboardTypeSupported(
 | 
						|
      Services.clipboard.kFindClipboard
 | 
						|
    )
 | 
						|
  ) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  lazy.ClipboardHelper.copyStringToClipboard(
 | 
						|
    aSearchString,
 | 
						|
    Ci.nsIClipboard.kFindClipboard
 | 
						|
  );
 | 
						|
}
 |