1 // vim: set ts=2 sw=2 sts=2 tw=80:
2 // This Source Code Form is subject to the terms of the Mozilla Public
3 // License, v. 2.0. If a copy of the MPL was not distributed with this
4 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 var EXPORTED_SYMBOLS = [
8 "GetClipboardSearchString",
9 "SetClipboardSearchString",
12 const { XPCOMUtils } = ChromeUtils.import(
13 "resource://gre/modules/XPCOMUtils.jsm"
15 const { Rect } = ChromeUtils.import("resource://gre/modules/Geometry.jsm");
16 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
18 ChromeUtils.defineModuleGetter(
21 "resource://gre/modules/BrowserUtils.jsm"
23 ChromeUtils.defineModuleGetter(
26 "resource://gre/modules/FinderIterator.jsm"
29 XPCOMUtils.defineLazyServiceGetter(
32 "@mozilla.org/widget/clipboard;1",
35 XPCOMUtils.defineLazyServiceGetter(
38 "@mozilla.org/widget/clipboardhelper;1",
42 const kSelectionMaxLen = 150;
43 const kMatchesCountLimitPref = "accessibility.typeaheadfind.matchesCountLimit";
45 function Finder(docShell) {
46 this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(
49 this._fastFind.init(docShell);
51 this._currentFoundRange = null;
52 this._docShell = docShell;
54 this._previousLink = null;
55 this._searchString = null;
56 this._highlighter = null;
59 .QueryInterface(Ci.nsIInterfaceRequestor)
60 .getInterface(Ci.nsIWebProgress)
61 .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
62 docShell.domWindow.addEventListener(
64 this.onLocationChange.bind(this, { isTopLevel: true })
70 if (!this._iterator) {
71 this._iterator = new FinderIterator();
73 return this._iterator;
78 this._iterator.reset();
80 let window = this._getWindow();
81 if (this._highlighter && window) {
82 // if we clear all the references before we hide the highlights (in both
83 // highlighting modes), we simply can't use them to find the ranges we
84 // need to clear from the selection.
85 this._highlighter.hide(window);
86 this._highlighter.clear(window);
90 .QueryInterface(Ci.nsIInterfaceRequestor)
91 .getInterface(Ci.nsIWebProgress)
92 .removeProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
94 this._currentFoundRange = this._fastFind = this._docShell = this._previousLink = this._highlighter = null;
97 addResultListener(aListener) {
98 if (!this._listeners.includes(aListener)) {
99 this._listeners.push(aListener);
103 removeResultListener(aListener) {
104 this._listeners = this._listeners.filter(l => l != aListener);
107 _setResults(options, mode) {
108 if (typeof options.storeResult != "boolean") {
109 options.storeResult = true;
112 if (options.storeResult) {
113 this._searchString = options.searchString;
114 this.clipboardSearchString = options.searchString;
117 let foundLink = this._fastFind.foundLink;
120 linkURL = Services.textToSubURI.unEscapeURIForUI(foundLink.href);
123 options.linkURL = linkURL;
124 options.rect = this._getResultRect();
125 options.searchString = this._searchString;
127 this._outlineLink(options.drawOutline);
129 for (let l of this._listeners) {
131 l.onFindResult(options);
137 if (!this._searchString && this._fastFind.searchString) {
138 this._searchString = this._fastFind.searchString;
140 return this._searchString;
143 get clipboardSearchString() {
144 return GetClipboardSearchString(
145 this._getWindow().docShell.QueryInterface(Ci.nsILoadContext)
149 set clipboardSearchString(aSearchString) {
150 SetClipboardSearchString(aSearchString);
153 set caseSensitive(aSensitive) {
154 if (this._fastFind.caseSensitive === aSensitive) {
157 this._fastFind.caseSensitive = aSensitive;
158 this.iterator.reset();
161 set matchDiacritics(aMatchDiacritics) {
162 if (this._fastFind.matchDiacritics === aMatchDiacritics) {
165 this._fastFind.matchDiacritics = aMatchDiacritics;
166 this.iterator.reset();
169 set entireWord(aEntireWord) {
170 if (this._fastFind.entireWord === aEntireWord) {
173 this._fastFind.entireWord = aEntireWord;
174 this.iterator.reset();
178 if (this._highlighter) {
179 return this._highlighter;
182 const { FinderHighlighter } = ChromeUtils.import(
183 "resource://gre/modules/FinderHighlighter.jsm"
185 return (this._highlighter = new FinderHighlighter(this));
188 get matchesCountLimit() {
189 if (typeof this._matchesCountLimit == "number") {
190 return this._matchesCountLimit;
193 this._matchesCountLimit =
194 Services.prefs.getIntPref(kMatchesCountLimitPref) || 0;
195 return this._matchesCountLimit;
198 _lastFindResult: null,
201 * Used for normal search operations, highlights the first match.
202 * This method is used only for compatibility with non-remote browsers.
204 * @param aSearchString String to search for.
205 * @param aLinksOnly Only consider nodes that are links for the search.
206 * @param aDrawOutline Puts an outline around matched links.
208 fastFind(aSearchString, aLinksOnly, aDrawOutline) {
209 this._lastFindResult = this._fastFind.find(
212 Ci.nsITypeAheadFind.FIND_INITIAL,
215 let searchString = this._fastFind.searchString;
219 result: this._lastFindResult,
220 findBackwards: false,
222 drawOutline: aDrawOutline,
223 linksOnly: aLinksOnly,
227 this._setResults(results);
228 this.updateHighlightAndMatchCount(results);
230 return this._lastFindResult;
234 * Repeat the previous search. Should only be called after a previous
235 * call to Finder.fastFind.
236 * This method is used only for compatibility with non-remote browsers.
238 * @param aSearchString String to search for.
239 * @param aFindBackwards Controls the search direction:
240 * true: before current match, false: after current match.
241 * @param aLinksOnly Only consider nodes that are links for the search.
242 * @param aDrawOutline Puts an outline around matched links.
244 findAgain(aSearchString, aFindBackwards, aLinksOnly, aDrawOutline) {
245 let mode = aFindBackwards
246 ? Ci.nsITypeAheadFind.FIND_PREVIOUS
247 : Ci.nsITypeAheadFind.FIND_NEXT;
248 this._lastFindResult = this._fastFind.find(
254 let searchString = this._fastFind.searchString;
258 result: this._lastFindResult,
259 findBackwards: aFindBackwards,
261 drawOutline: aDrawOutline,
262 linksOnly: aLinksOnly,
265 this._setResults(results);
266 this.updateHighlightAndMatchCount(results);
268 return this._lastFindResult;
272 * Used for normal search operations, highlights the first or
273 * subsequent match depending on the mode.
276 * searchString String to search for.
277 * findAgain True if this a find again operation.
278 * mode Search mode from nsITypeAheadFind.
279 * linksOnly Only consider nodes that are links for the search.
280 * drawOutline Puts an outline around matched links.
281 * useSubFrames True to iterate over subframes.
282 * caseSensitive True for case sensitive searching.
283 * entireWord True to match entire words.
284 * matchDiacritics True to match diacritics.
287 this.caseSensitive = options.caseSensitive;
288 this.entireWord = options.entireWord;
289 this.matchDiacritics = options.matchDiacritics;
291 this._lastFindResult = this._fastFind.find(
292 options.searchString,
295 !options.useSubFrames
297 let searchString = this._fastFind.searchString;
300 result: this._lastFindResult,
302 options.mode == Ci.nsITypeAheadFind.FIND_PREVIOUS ||
303 options.mode == Ci.nsITypeAheadFind.FIND_LAST,
304 findAgain: options.findAgain,
305 drawOutline: options.drawOutline,
306 linksOnly: options.linksOnly,
307 entireWord: this._fastFind.entireWord,
308 useSubFrames: options.useSubFrames,
310 this._setResults(results, options.mode);
311 return new Promise(resolve => resolve(results));
315 * Forcibly set the search string of the find clipboard to the currently
316 * selected text in the window, on supported platforms (i.e. OSX).
318 setSearchStringToSelection() {
319 let searchInfo = this.getActiveSelectionText();
321 // If an empty string is returned or a subframe is focused, don't
322 // assign the search string.
323 if (searchInfo.selectedText) {
324 this.clipboardSearchString = searchInfo.selectedText;
330 async highlight(aHighlight, aWord, aLinksOnly, aUseSubFrames = true) {
331 return this.highlighter.highlight(
340 async updateHighlightAndMatchCount(aArgs) {
341 this._lastFindResult = aArgs;
344 !this.iterator.continueRunning({
345 caseSensitive: this._fastFind.caseSensitive,
346 entireWord: this._fastFind.entireWord,
347 linksOnly: aArgs.linksOnly,
348 matchDiacritics: this._fastFind.matchDiacritics,
349 word: aArgs.searchString,
350 useSubFrames: aArgs.useSubFrames,
353 this.iterator.stop();
356 let highlightPromise = this.highlighter.update(
358 aArgs.useSubFrames ? false : aArgs.foundInThisFrame
360 let matchCountPromise = this.requestMatchesCount(
366 let results = await Promise.all([highlightPromise, matchCountPromise]);
368 return Object.assign(results[1], results[0]);
369 } else if (results[0]) {
376 getInitialSelection() {
377 this._getWindow().setTimeout(() => {
378 let initialSelection = this.getActiveSelectionText().selectedText;
379 for (let l of this._listeners) {
381 l.onCurrentSelection(initialSelection, true);
387 getActiveSelectionText() {
388 let focusedWindow = {};
389 let focusedElement = Services.focus.getFocusedElementForWindow(
394 focusedWindow = focusedWindow.value;
398 // If this is a remote subframe, return an empty string but
399 // indiciate which browsing context was focused.
402 "frameLoader" in focusedElement &&
403 focusedElement.browsingContext instanceof BrowsingContext
406 focusedChildBrowserContextId: focusedElement.browsingContext.id,
411 if (focusedElement && focusedElement.editor) {
412 // The user may have a selection in an input or textarea.
413 selText = focusedElement.editor.selectionController
414 .getSelection(Ci.nsISelectionController.SELECTION_NORMAL)
417 // Look for any selected text on the actual page.
418 selText = focusedWindow.getSelection().toString();
422 return { selectedText: "" };
425 // Process our text to get rid of unwanted characters.
426 selText = selText.trim().replace(/\s+/g, " ");
427 let truncLength = kSelectionMaxLen;
428 if (selText.length > truncLength) {
429 let truncChar = selText.charAt(truncLength).charCodeAt(0);
430 if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
433 selText = selText.substr(0, truncLength);
436 return { selectedText: selText };
440 this._fastFind.setSelectionModeAndRepaint(
441 Ci.nsISelectionController.SELECTION_ON
443 this._restoreOriginalOutline();
446 removeSelection(keepHighlight) {
447 this._fastFind.collapseSelection();
448 this.enableSelection();
449 let window = this._getWindow();
451 this.highlighter.clearCurrentOutline(window);
453 this.highlighter.clear(window);
458 // Allow Finder listeners to cancel focusing the content.
459 for (let l of this._listeners) {
461 if ("shouldFocusContent" in l && !l.shouldFocusContent()) {
469 let fastFind = this._fastFind;
471 // Try to find the best possible match that should receive focus and
472 // block scrolling on focus since find already scrolls. Further
473 // scrolling is due to user action, so don't override this.
474 if (fastFind.foundLink) {
475 Services.focus.setFocus(
477 Services.focus.FLAG_NOSCROLL
479 } else if (fastFind.foundEditable) {
480 Services.focus.setFocus(
481 fastFind.foundEditable,
482 Services.focus.FLAG_NOSCROLL
484 fastFind.collapseSelection();
486 this._getWindow().focus();
492 this.enableSelection();
493 this.highlighter.highlight(false);
494 this.iterator.reset();
495 BrowserUtils.trackToolbarVisibility(this._docShell, "findbar", false);
499 BrowserUtils.trackToolbarVisibility(this._docShell, "findbar", true);
502 onModalHighlightChange(useModalHighlight) {
503 if (this._highlighter) {
504 this._highlighter.onModalHighlightChange(useModalHighlight);
508 onHighlightAllChange(highlightAll) {
509 if (this._highlighter) {
510 this._highlighter.onHighlightAllChange(highlightAll);
512 if (this._iterator) {
513 this._iterator.reset();
518 let controller = this._getSelectionController(this._getWindow());
520 switch (aEvent.keyCode) {
521 case aEvent.DOM_VK_RETURN:
522 if (this._fastFind.foundLink) {
523 let view = this._fastFind.foundLink.ownerGlobal;
524 this._fastFind.foundLink.dispatchEvent(
525 new view.MouseEvent("click", {
529 ctrlKey: aEvent.ctrlKey,
530 altKey: aEvent.altKey,
531 shiftKey: aEvent.shiftKey,
532 metaKey: aEvent.metaKey,
537 case aEvent.DOM_VK_TAB:
538 let direction = Services.focus.MOVEFOCUS_FORWARD;
539 if (aEvent.shiftKey) {
540 direction = Services.focus.MOVEFOCUS_BACKWARD;
542 Services.focus.moveFocus(this._getWindow(), null, direction, 0);
544 case aEvent.DOM_VK_PAGE_UP:
545 controller.scrollPage(false);
547 case aEvent.DOM_VK_PAGE_DOWN:
548 controller.scrollPage(true);
550 case aEvent.DOM_VK_UP:
551 controller.scrollLine(false);
553 case aEvent.DOM_VK_DOWN:
554 controller.scrollLine(true);
559 _notifyMatchesCount(aWord, result = this._currentMatchesCountResult) {
560 // The `_currentFound` property is only used for internal bookkeeping.
561 delete result._currentFound;
562 result.searchString = aWord;
563 result.limit = this.matchesCountLimit;
564 if (result.total == result.limit) {
568 for (let l of this._listeners) {
570 l.onMatchesCountResult(result);
574 this._currentMatchesCountResult = null;
578 async requestMatchesCount(aWord, aLinksOnly, aUseSubFrames = true) {
580 this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND ||
581 this.searchString == "" ||
583 !this.matchesCountLimit
585 return this._notifyMatchesCount(aWord, {
591 this._currentFoundRange = this._fastFind.getFoundRange();
594 caseSensitive: this._fastFind.caseSensitive,
595 entireWord: this._fastFind.entireWord,
596 linksOnly: aLinksOnly,
597 matchDiacritics: this._fastFind.matchDiacritics,
599 useSubFrames: aUseSubFrames,
601 if (!this.iterator.continueRunning(params)) {
602 this.iterator.stop();
605 await this.iterator.start(
606 Object.assign(params, {
608 limit: this.matchesCountLimit,
611 useSubFrames: aUseSubFrames,
615 // Without a valid result, there's nothing to notify about. This happens
616 // when the iterator was started before and won the race.
617 if (!this._currentMatchesCountResult) {
621 return this._notifyMatchesCount(aWord);
624 // FinderIterator listener implementation
626 onIteratorRangeFound(range) {
627 let result = this._currentMatchesCountResult;
633 if (!result._currentFound) {
635 result._currentFound =
636 this._currentFoundRange &&
637 range.startContainer == this._currentFoundRange.startContainer &&
638 range.startOffset == this._currentFoundRange.startOffset &&
639 range.endContainer == this._currentFoundRange.endContainer &&
640 range.endOffset == this._currentFoundRange.endOffset;
644 onIteratorReset() {},
646 onIteratorRestart({ word, linksOnly, useSubFrames }) {
647 this.requestMatchesCount(word, linksOnly, useSubFrames);
651 this._currentMatchesCountResult = {
654 _currentFound: false,
659 if (!this._docShell) {
662 return this._docShell.domWindow;
666 * Get the bounding selection rect in CSS px relative to the origin of the
667 * top-level content document.
670 let topWin = this._getWindow();
671 let win = this._fastFind.currentWindow;
676 let selection = win.getSelection();
677 if (!selection.rangeCount || selection.isCollapsed) {
678 // The selection can be into an input or a textarea element.
679 let nodes = win.document.querySelectorAll("input, textarea");
680 for (let node of nodes) {
683 let sc = node.editor.selectionController;
684 selection = sc.getSelection(
685 Ci.nsISelectionController.SELECTION_NORMAL
687 if (selection.rangeCount && !selection.isCollapsed) {
691 // If this textarea is hidden, then its selection controller might
692 // not be intialized. Ignore the failure.
698 if (!selection.rangeCount || selection.isCollapsed) {
702 let utils = topWin.windowUtils;
706 utils.getScrollXY(false, scrollX, scrollY);
708 for (let frame = win; frame != topWin; frame = frame.parent) {
709 let rect = frame.frameElement.getBoundingClientRect();
710 let left = frame.getComputedStyle(frame.frameElement).borderLeftWidth;
711 let top = frame.getComputedStyle(frame.frameElement).borderTopWidth;
712 scrollX.value += rect.left + parseInt(left, 10);
713 scrollY.value += rect.top + parseInt(top, 10);
715 let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect());
716 return rect.translate(scrollX.value, scrollY.value);
719 _outlineLink(aDrawOutline) {
720 let foundLink = this._fastFind.foundLink;
722 // Optimization: We are drawing outlines and we matched
723 // the same link before, so don't duplicate work.
724 if (foundLink == this._previousLink && aDrawOutline) {
728 this._restoreOriginalOutline();
730 if (foundLink && aDrawOutline) {
731 // Backup original outline
732 this._tmpOutline = foundLink.style.outline;
733 this._tmpOutlineOffset = foundLink.style.outlineOffset;
735 // Draw pseudo focus rect
736 // XXX Should we change the following style for FAYT pseudo focus?
737 // XXX Shouldn't we change default design if outline is visible
739 // Don't set the outline-color, we should always use initial value.
740 foundLink.style.outline = "1px dotted";
741 foundLink.style.outlineOffset = "0";
743 this._previousLink = foundLink;
747 _restoreOriginalOutline() {
748 // Removes the outline around the last found link.
749 if (this._previousLink) {
750 this._previousLink.style.outline = this._tmpOutline;
751 this._previousLink.style.outlineOffset = this._tmpOutlineOffset;
752 this._previousLink = null;
756 _getSelectionController(aWindow) {
757 // display: none iframes don't have a selection controller, see bug 493658
759 if (!aWindow.innerWidth || !aWindow.innerHeight) {
763 // If getting innerWidth or innerHeight throws, we can't get a selection
768 // Yuck. See bug 138068.
769 let docShell = aWindow.docShell;
771 let controller = docShell
772 .QueryInterface(Ci.nsIInterfaceRequestor)
773 .getInterface(Ci.nsISelectionDisplay)
774 .QueryInterface(Ci.nsISelectionController);
778 // Start of nsIWebProgressListener implementation.
780 onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
781 if (!aWebProgress.isTopLevel) {
784 // Ignore events that don't change the document.
785 if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
789 // Avoid leaking if we change the page.
790 this._lastFindResult = this._previousLink = this._currentFoundRange = null;
791 this.highlighter.onLocationChange();
792 this.iterator.reset();
795 QueryInterface: ChromeUtils.generateQI([
796 "nsIWebProgressListener",
797 "nsISupportsWeakReference",
801 function GetClipboardSearchString(aLoadContext) {
802 let searchString = "";
803 if (!Clipboard.supportsFindClipboard()) {
808 let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
811 trans.init(aLoadContext);
812 trans.addDataFlavor("text/unicode");
814 Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
817 trans.getTransferData("text/unicode", data);
819 data = data.value.QueryInterface(Ci.nsISupportsString);
820 searchString = data.toString();
827 function SetClipboardSearchString(aSearchString) {
828 if (!aSearchString || !Clipboard.supportsFindClipboard()) {
832 ClipboardHelper.copyStringToClipboard(
834 Ci.nsIClipboard.kFindClipboard