1 // This Source Code Form is subject to the terms of the Mozilla Public
2 // License, v. 2.0. If a copy of the MPL was not distributed with this
3 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 this.EXPORTED_SYMBOLS = ["Finder"];
7 const Ci = Components.interfaces;
8 const Cc = Components.classes;
9 const Cu = Components.utils;
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Geometry.jsm");
13 Cu.import("resource://gre/modules/Services.jsm");
15 XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService",
16 "@mozilla.org/intl/texttosuburi;1",
18 XPCOMUtils.defineLazyServiceGetter(this, "Clipboard",
19 "@mozilla.org/widget/clipboard;1",
21 XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
22 "@mozilla.org/widget/clipboardhelper;1",
23 "nsIClipboardHelper");
25 function Finder(docShell) {
26 this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
27 this._fastFind.init(docShell);
29 this._docShell = docShell;
31 this._previousLink = null;
32 this._searchString = null;
34 docShell.QueryInterface(Ci.nsIInterfaceRequestor)
35 .getInterface(Ci.nsIWebProgress)
36 .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
40 addResultListener: function (aListener) {
41 if (this._listeners.indexOf(aListener) === -1)
42 this._listeners.push(aListener);
45 removeResultListener: function (aListener) {
46 this._listeners = this._listeners.filter(l => l != aListener);
49 _notify: function (aSearchString, aResult, aFindBackwards, aDrawOutline, aStoreResult = true) {
51 this._searchString = aSearchString;
52 this.clipboardSearchString = aSearchString
54 this._outlineLink(aDrawOutline);
56 let foundLink = this._fastFind.foundLink;
59 let docCharset = null;
60 let ownerDoc = foundLink.ownerDocument;
62 docCharset = ownerDoc.characterSet;
64 linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
69 findBackwards: aFindBackwards,
71 rect: this._getResultRect(),
72 searchString: this._searchString,
73 storeResult: aStoreResult
76 for (let l of this._listeners) {
84 if (!this._searchString && this._fastFind.searchString)
85 this._searchString = this._fastFind.searchString;
86 return this._searchString;
89 get clipboardSearchString() {
90 let searchString = "";
91 if (!Clipboard.supportsFindClipboard())
95 let trans = Cc["@mozilla.org/widget/transferable;1"]
96 .createInstance(Ci.nsITransferable);
97 trans.init(this._getWindow()
98 .QueryInterface(Ci.nsIInterfaceRequestor)
99 .getInterface(Ci.nsIWebNavigation)
100 .QueryInterface(Ci.nsILoadContext));
101 trans.addDataFlavor("text/unicode");
103 Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
107 trans.getTransferData("text/unicode", data, dataLen);
109 data = data.value.QueryInterface(Ci.nsISupportsString);
110 searchString = data.toString();
117 set clipboardSearchString(aSearchString) {
118 if (!aSearchString || !Clipboard.supportsFindClipboard())
121 ClipboardHelper.copyStringToClipboard(aSearchString,
122 Ci.nsIClipboard.kFindClipboard,
123 this._getWindow().document);
126 set caseSensitive(aSensitive) {
127 this._fastFind.caseSensitive = aSensitive;
131 * Used for normal search operations, highlights the first match.
133 * @param aSearchString String to search for.
134 * @param aLinksOnly Only consider nodes that are links for the search.
135 * @param aDrawOutline Puts an outline around matched links.
137 fastFind: function (aSearchString, aLinksOnly, aDrawOutline) {
138 let result = this._fastFind.find(aSearchString, aLinksOnly);
139 let searchString = this._fastFind.searchString;
140 this._notify(searchString, result, false, aDrawOutline);
144 * Repeat the previous search. Should only be called after a previous
145 * call to Finder.fastFind.
147 * @param aFindBackwards Controls the search direction:
148 * true: before current match, false: after current match.
149 * @param aLinksOnly Only consider nodes that are links for the search.
150 * @param aDrawOutline Puts an outline around matched links.
152 findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
153 let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
154 let searchString = this._fastFind.searchString;
155 this._notify(searchString, result, aFindBackwards, aDrawOutline);
159 * Forcibly set the search string of the find clipboard to the currently
160 * selected text in the window, on supported platforms (i.e. OSX).
162 setSearchStringToSelection: function() {
163 // Find the selected text.
164 let selection = this._getWindow().getSelection();
165 // Don't go for empty selections.
166 if (!selection.rangeCount)
168 let searchString = (selection.toString() || "").trim();
169 // Empty strings are rather useless to search for.
170 if (!searchString.length)
173 this.clipboardSearchString = searchString;
177 highlight: function (aHighlight, aWord) {
178 let found = this._highlight(aHighlight, aWord, null);
180 let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
181 : Ci.nsITypeAheadFind.FIND_NOTFOUND;
182 this._notify(aWord, result, false, false, false);
186 enableSelection: function() {
187 this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON);
188 this._restoreOriginalOutline();
191 removeSelection: function() {
192 this._fastFind.collapseSelection();
193 this.enableSelection();
196 focusContent: function() {
197 // Allow Finder listeners to cancel focusing the content.
198 for (let l of this._listeners) {
200 if (!l.shouldFocusContent())
205 let fastFind = this._fastFind;
206 const fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
208 // Try to find the best possible match that should receive focus and
209 // block scrolling on focus since find already scrolls. Further
210 // scrolling is due to user action, so don't override this.
211 if (fastFind.foundLink) {
212 fm.setFocus(fastFind.foundLink, fm.FLAG_NOSCROLL);
213 } else if (fastFind.foundEditable) {
214 fm.setFocus(fastFind.foundEditable, fm.FLAG_NOSCROLL);
215 fastFind.collapseSelection();
217 this._getWindow().focus()
222 keyPress: function (aEvent) {
223 let controller = this._getSelectionController(this._getWindow());
225 switch (aEvent.keyCode) {
226 case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
227 if (this._fastFind.foundLink) {
228 let view = this._fastFind.foundLink.ownerDocument.defaultView;
229 this._fastFind.foundLink.dispatchEvent(new view.MouseEvent("click", {
233 ctrlKey: aEvent.ctrlKey,
234 altKey: aEvent.altKey,
235 shiftKey: aEvent.shiftKey,
236 metaKey: aEvent.metaKey
240 case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
241 let direction = Services.focus.MOVEFOCUS_FORWARD;
242 if (aEvent.shiftKey) {
243 direction = Services.focus.MOVEFOCUS_BACKWARD;
245 Services.focus.moveFocus(this._getWindow(), null, direction, 0);
247 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
248 controller.scrollPage(false);
250 case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
251 controller.scrollPage(true);
253 case Ci.nsIDOMKeyEvent.DOM_VK_UP:
254 controller.scrollLine(false);
256 case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
257 controller.scrollLine(true);
262 requestMatchesCount: function(aWord, aMatchLimit, aLinksOnly) {
263 let window = this._getWindow();
264 let result = this._countMatchesInWindow(aWord, aMatchLimit, aLinksOnly, window);
266 // Count matches in (i)frames AFTER searching through the main window.
267 for (let frame of result._framesToCount) {
268 // We've reached our limit; no need to do more work.
269 if (result.total == -1 || result.total == aMatchLimit)
271 this._countMatchesInWindow(aWord, aMatchLimit, aLinksOnly, frame, result);
274 // The `_currentFound` and `_framesToCount` properties are only used for
275 // internal bookkeeping between recursive calls.
276 delete result._currentFound;
277 delete result._framesToCount;
279 for (let l of this._listeners) {
281 l.onMatchesCountResult(result);
287 * Counts the number of matches for the searched word in the passed window's
290 * the word to search for.
292 * the maximum number of matches shown (for speed reasons).
294 * whether we should only search through links.
296 * the window to search in. Passing undefined will search the
297 * current content window. Optional.
299 * the Object that is returned by this function. It may be passed as an
300 * argument here in the case of a recursive call.
301 * @returns an object stating the number of matches and a vector for the current match.
303 _countMatchesInWindow: function(aWord, aMatchLimit, aLinksOnly, aWindow = null, aStats = null) {
304 aWindow = aWindow || this._getWindow();
308 _framesToCount: new Set(),
312 // If we already reached our max, there's no need to do more work!
313 if (aStats.total == -1 || aStats.total == aMatchLimit) {
318 this._collectFrames(aWindow, aStats);
320 let foundRange = this._fastFind.getFoundRange();
322 this._findIterator(aWord, aWindow, aRange => {
323 if (!aLinksOnly || this._rangeStartsInLink(aRange)) {
325 if (!aStats._currentFound) {
327 aStats._currentFound = (foundRange &&
328 aRange.startContainer == foundRange.startContainer &&
329 aRange.startOffset == foundRange.startOffset &&
330 aRange.endContainer == foundRange.endContainer &&
331 aRange.endOffset == foundRange.endOffset);
334 if (aStats.total == aMatchLimit) {
344 * Basic wrapper around nsIFind that provides invoking a callback `aOnFind`
345 * each time an occurence of `aWord` string is found.
348 * the word to search for.
350 * the window to search in.
352 * the Function to invoke when a word is found. if Boolean `false` is
353 * returned, the find operation will be stopped and the Function will
354 * not be invoked again.
356 _findIterator: function(aWord, aWindow, aOnFind) {
357 let doc = aWindow.document;
358 let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
359 doc.body : doc.documentElement;
364 let searchRange = doc.createRange();
365 searchRange.selectNodeContents(body);
367 let startPt = searchRange.cloneRange();
368 startPt.collapse(true);
370 let endPt = searchRange.cloneRange();
371 endPt.collapse(false);
375 let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
377 .QueryInterface(Ci.nsIFind);
378 finder.caseSensitive = this._fastFind.caseSensitive;
380 while ((retRange = finder.Find(aWord, searchRange, startPt, endPt))) {
381 if (aOnFind(retRange) === false)
383 startPt = retRange.cloneRange();
384 startPt.collapse(false);
389 * Helper method for `_countMatchesInWindow` that recursively collects all
390 * visible (i)frames inside a window.
393 * the window to extract the (i)frames from.
395 * Object that contains a Set called '_framesToCount'
397 _collectFrames: function(aWindow, aStats) {
398 if (!aWindow.frames || !aWindow.frames.length)
400 // Casting `aWindow.frames` to an Iterator doesn't work, so we're stuck with
401 // a plain, old for-loop.
402 for (let i = 0, l = aWindow.frames.length; i < l; ++i) {
403 let frame = aWindow.frames[i];
404 // Don't count matches in hidden frames.
405 let frameEl = frame && frame.frameElement;
408 // Construct a range around the frame element to check its visiblity.
409 let range = aWindow.document.createRange();
410 range.setStart(frameEl, 0);
411 range.setEnd(frameEl, 0);
412 if (!this._fastFind.isRangeVisible(range, this._getDocShell(range), true))
414 // All good, so add it to the set to count later.
415 if (!aStats._framesToCount.has(frame))
416 aStats._framesToCount.add(frame);
417 this._collectFrames(frame, aStats);
422 * Helper method to extract the docShell reference from a Window or Range object.
424 * @param aWindowOrRange
425 * Window object to query. May also be a Range, from which the owner
426 * window will be queried.
427 * @returns nsIDocShell
429 _getDocShell: function(aWindowOrRange) {
430 let window = aWindowOrRange;
431 // Ranges may also be passed in, so fetch its window.
432 if (aWindowOrRange instanceof Ci.nsIDOMRange)
433 window = aWindowOrRange.startContainer.ownerDocument.defaultView;
434 return window.QueryInterface(Ci.nsIInterfaceRequestor)
435 .getInterface(Ci.nsIWebNavigation)
436 .QueryInterface(Ci.nsIDocShell);
439 _getWindow: function () {
440 return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
444 * Get the bounding selection rect in CSS px relative to the origin of the
445 * top-level content document.
447 _getResultRect: function () {
448 let topWin = this._getWindow();
449 let win = this._fastFind.currentWindow;
453 let selection = win.getSelection();
454 if (!selection.rangeCount || selection.isCollapsed) {
455 // The selection can be into an input or a textarea element.
456 let nodes = win.document.querySelectorAll("input, textarea");
457 for (let node of nodes) {
458 if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) {
459 let sc = node.editor.selectionController;
460 selection = sc.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
461 if (selection.rangeCount && !selection.isCollapsed) {
468 let utils = topWin.QueryInterface(Ci.nsIInterfaceRequestor)
469 .getInterface(Ci.nsIDOMWindowUtils);
471 let scrollX = {}, scrollY = {};
472 utils.getScrollXY(false, scrollX, scrollY);
474 for (let frame = win; frame != topWin; frame = frame.parent) {
475 let rect = frame.frameElement.getBoundingClientRect();
476 let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
477 let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
478 scrollX.value += rect.left + parseInt(left, 10);
479 scrollY.value += rect.top + parseInt(top, 10);
481 let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect());
482 return rect.translate(scrollX.value, scrollY.value);
485 _outlineLink: function (aDrawOutline) {
486 let foundLink = this._fastFind.foundLink;
488 // Optimization: We are drawing outlines and we matched
489 // the same link before, so don't duplicate work.
490 if (foundLink == this._previousLink && aDrawOutline)
493 this._restoreOriginalOutline();
495 if (foundLink && aDrawOutline) {
496 // Backup original outline
497 this._tmpOutline = foundLink.style.outline;
498 this._tmpOutlineOffset = foundLink.style.outlineOffset;
500 // Draw pseudo focus rect
501 // XXX Should we change the following style for FAYT pseudo focus?
502 // XXX Shouldn't we change default design if outline is visible
504 // Don't set the outline-color, we should always use initial value.
505 foundLink.style.outline = "1px dotted";
506 foundLink.style.outlineOffset = "0";
508 this._previousLink = foundLink;
512 _restoreOriginalOutline: function () {
513 // Removes the outline around the last found link.
514 if (this._previousLink) {
515 this._previousLink.style.outline = this._tmpOutline;
516 this._previousLink.style.outlineOffset = this._tmpOutlineOffset;
517 this._previousLink = null;
521 _highlight: function (aHighlight, aWord, aWindow) {
522 let win = aWindow || this._getWindow();
525 for (let i = 0; win.frames && i < win.frames.length; i++) {
526 if (this._highlight(aHighlight, aWord, win.frames[i]))
530 let controller = this._getSelectionController(win);
531 let doc = win.document;
532 if (!controller || !doc || !doc.documentElement) {
533 // Without the selection controller,
534 // we are unable to (un)highlight any matches
539 this._findIterator(aWord, win, aRange => {
540 this._highlightRange(aRange, controller);
544 // First, attempt to remove highlighting from main document
545 let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
546 sel.removeAllRanges();
548 // Next, check our editor cache, for editors belonging to this
551 for (let x = this._editors.length - 1; x >= 0; --x) {
552 if (this._editors[x].document == doc) {
553 sel = this._editors[x].selectionController
554 .getSelection(Ci.nsISelectionController.SELECTION_FIND);
555 sel.removeAllRanges();
556 // We don't need to listen to this editor any more
557 this._unhookListenersAtIndex(x);
562 // Removing the highlighting always succeeds, so return true.
569 _highlightRange: function(aRange, aController) {
570 let node = aRange.startContainer;
571 let controller = aController;
573 let editableNode = this._getEditableNode(node);
575 controller = editableNode.editor.selectionController;
577 let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
578 findSelection.addRange(aRange);
581 // Highlighting added, so cache this editor, and hook up listeners
582 // to ensure we deal properly with edits within the highlighting
583 if (!this._editors) {
585 this._stateListeners = [];
588 let existingIndex = this._editors.indexOf(editableNode.editor);
589 if (existingIndex == -1) {
590 let x = this._editors.length;
591 this._editors[x] = editableNode.editor;
592 this._stateListeners[x] = this._createStateListener();
593 this._editors[x].addEditActionListener(this);
594 this._editors[x].addDocumentStateListener(this._stateListeners[x]);
599 _getSelectionController: function(aWindow) {
600 // display: none iframes don't have a selection controller, see bug 493658
601 if (!aWindow.innerWidth || !aWindow.innerHeight)
604 // Yuck. See bug 138068.
605 let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
606 .getInterface(Ci.nsIWebNavigation)
607 .QueryInterface(Ci.nsIDocShell);
609 let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
610 .getInterface(Ci.nsISelectionDisplay)
611 .QueryInterface(Ci.nsISelectionController);
616 * For a given node, walk up it's parent chain, to try and find an
619 * @param aNode the node we want to check
620 * @returns the first node in the parent chain that is editable,
621 * null if there is no such node
623 _getEditableNode: function (aNode) {
625 if (aNode instanceof Ci.nsIDOMNSEditableElement)
626 return aNode.editor ? aNode : null;
628 aNode = aNode.parentNode;
634 * Helper method to unhook listeners, remove cached editors
635 * and keep the relevant arrays in sync
637 * @param aIndex the index into the array of editors/state listeners
640 _unhookListenersAtIndex: function (aIndex) {
641 this._editors[aIndex].removeEditActionListener(this);
642 this._editors[aIndex]
643 .removeDocumentStateListener(this._stateListeners[aIndex]);
644 this._editors.splice(aIndex, 1);
645 this._stateListeners.splice(aIndex, 1);
646 if (!this._editors.length) {
647 delete this._editors;
648 delete this._stateListeners;
653 * Remove ourselves as an nsIEditActionListener and
654 * nsIDocumentStateListener from a given cached editor
656 * @param aEditor the editor we no longer wish to listen to
658 _removeEditorListeners: function (aEditor) {
659 // aEditor is an editor that we listen to, so therefore must be
660 // cached. Find the index of this editor
661 let idx = this._editors.indexOf(aEditor);
664 // Now unhook ourselves, and remove our cached copy
665 this._unhookListenersAtIndex(idx);
669 * nsIEditActionListener logic follows
671 * We implement this interface to allow us to catch the case where
672 * the findbar found a match in a HTML <input> or <textarea>. If the
673 * user adjusts the text in some way, it will no longer match, so we
674 * want to remove the highlight, rather than have it expand/contract
675 * when letters are added or removed.
679 * Helper method used to check whether a selection intersects with
682 * @param aSelectionRange the range from the selection to check
683 * @param aFindRange the highlighted range to check against
684 * @returns true if they intersect, false otherwise
686 _checkOverlap: function (aSelectionRange, aFindRange) {
687 // The ranges overlap if one of the following is true:
688 // 1) At least one of the endpoints of the deleted selection
689 // is in the find selection
690 // 2) At least one of the endpoints of the find selection
691 // is in the deleted selection
692 if (aFindRange.isPointInRange(aSelectionRange.startContainer,
693 aSelectionRange.startOffset))
695 if (aFindRange.isPointInRange(aSelectionRange.endContainer,
696 aSelectionRange.endOffset))
698 if (aSelectionRange.isPointInRange(aFindRange.startContainer,
699 aFindRange.startOffset))
701 if (aSelectionRange.isPointInRange(aFindRange.endContainer,
702 aFindRange.endOffset))
709 * Helper method to determine if an edit occurred within a highlight
711 * @param aSelection the selection we wish to check
712 * @param aNode the node we want to check is contained in aSelection
713 * @param aOffset the offset into aNode that we want to check
714 * @returns the range containing (aNode, aOffset) or null if no ranges
715 * in the selection contain it
717 _findRange: function (aSelection, aNode, aOffset) {
718 let rangeCount = aSelection.rangeCount;
720 let foundContainingRange = false;
723 // Check to see if this node is inside one of the selection's ranges
724 while (!foundContainingRange && rangeidx < rangeCount) {
725 range = aSelection.getRangeAt(rangeidx);
726 if (range.isPointInRange(aNode, aOffset)) {
727 foundContainingRange = true;
733 if (foundContainingRange)
740 * Determines whether a range is inside a link.
743 * @returns true if the range starts in a link
745 _rangeStartsInLink: function(aRange) {
746 let isInsideLink = false;
747 let node = aRange.startContainer;
749 if (node.nodeType == node.ELEMENT_NODE) {
750 if (node.hasChildNodes) {
751 let childNode = node.item(aRange.startOffset);
757 const XLink_NS = "http://www.w3.org/1999/xlink";
759 if (node instanceof HTMLAnchorElement) {
760 isInsideLink = node.hasAttribute("href");
762 } else if (typeof node.hasAttributeNS == "function" &&
763 node.hasAttributeNS(XLink_NS, "href")) {
764 isInsideLink = (node.getAttributeNS(XLink_NS, "type") == "simple");
768 node = node.parentNode;
774 // Start of nsIWebProgressListener implementation.
776 onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
777 if (!aWebProgress.isTopLevel)
780 // Avoid leaking if we change the page.
781 this._previousLink = null;
784 // Start of nsIEditActionListener implementations
786 WillDeleteText: function (aTextNode, aOffset, aLength) {
787 let editor = this._getEditableNode(aTextNode).editor;
788 let controller = editor.selectionController;
789 let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
790 let range = this._findRange(fSelection, aTextNode, aOffset);
793 // Don't remove the highlighting if the deleted text is at the
795 if (aTextNode != range.endContainer ||
796 aOffset != range.endOffset) {
797 // Text within the highlight is being removed - the text can
798 // no longer be a match, so remove the highlighting
799 fSelection.removeRange(range);
800 if (fSelection.rangeCount == 0)
801 this._removeEditorListeners(editor);
806 DidInsertText: function (aTextNode, aOffset, aString) {
807 let editor = this._getEditableNode(aTextNode).editor;
808 let controller = editor.selectionController;
809 let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
810 let range = this._findRange(fSelection, aTextNode, aOffset);
813 // If the text was inserted before the highlight
814 // adjust the highlight's bounds accordingly
815 if (aTextNode == range.startContainer &&
816 aOffset == range.startOffset) {
817 range.setStart(range.startContainer,
818 range.startOffset+aString.length);
819 } else if (aTextNode != range.endContainer ||
820 aOffset != range.endOffset) {
821 // The edit occurred within the highlight - any addition of text
822 // will result in the text no longer being a match,
823 // so remove the highlighting
824 fSelection.removeRange(range);
825 if (fSelection.rangeCount == 0)
826 this._removeEditorListeners(editor);
831 WillDeleteSelection: function (aSelection) {
832 let editor = this._getEditableNode(aSelection.getRangeAt(0)
833 .startContainer).editor;
834 let controller = editor.selectionController;
835 let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
837 let selectionIndex = 0;
838 let findSelectionIndex = 0;
839 let shouldDelete = {};
840 let numberOfDeletedSelections = 0;
841 let numberOfMatches = fSelection.rangeCount;
843 // We need to test if any ranges in the deleted selection (aSelection)
844 // are in any of the ranges of the find selection
845 // Usually both selections will only contain one range, however
846 // either may contain more than one.
848 for (let fIndex = 0; fIndex < numberOfMatches; fIndex++) {
849 shouldDelete[fIndex] = false;
850 let fRange = fSelection.getRangeAt(fIndex);
852 for (let index = 0; index < aSelection.rangeCount; index++) {
853 if (shouldDelete[fIndex])
856 let selRange = aSelection.getRangeAt(index);
857 let doesOverlap = this._checkOverlap(selRange, fRange);
859 shouldDelete[fIndex] = true;
860 numberOfDeletedSelections++;
865 // OK, so now we know what matches (if any) are in the selection
866 // that is being deleted. Time to remove them.
867 if (numberOfDeletedSelections == 0)
870 for (let i = numberOfMatches - 1; i >= 0; i--) {
872 fSelection.removeRange(fSelection.getRangeAt(i));
875 // Remove listeners if no more highlights left
876 if (fSelection.rangeCount == 0)
877 this._removeEditorListeners(editor);
881 * nsIDocumentStateListener logic follows
883 * When attaching nsIEditActionListeners, there are no guarantees
884 * as to whether the findbar or the documents in the browser will get
885 * destructed first. This leads to the potential to either leak, or to
886 * hold on to a reference an editable element's editor for too long,
887 * preventing it from being destructed.
889 * However, when an editor's owning node is being destroyed, the editor
890 * sends out a DocumentWillBeDestroyed notification. We can use this to
891 * clean up our references to the object, to allow it to be destroyed in a
896 * Unhook ourselves when one of our state listeners has been called.
897 * This can happen in 4 cases:
898 * 1) The document the editor belongs to is navigated away from, and
899 * the document is not being cached
901 * 2) The document the editor belongs to is expired from the cache
903 * 3) The tab containing the owning document is closed
905 * 4) The <input> or <textarea> that owns the editor is explicitly
906 * removed from the DOM
908 * @param the listener that was invoked
910 _onEditorDestruction: function (aListener) {
911 // First find the index of the editor the given listener listens to.
912 // The listeners and editors arrays must always be in sync.
913 // The listener will be in our array of cached listeners, as this
914 // method could not have been called otherwise.
916 while (this._stateListeners[idx] != aListener)
919 // Unhook both listeners
920 this._unhookListenersAtIndex(idx);
924 * Creates a unique document state listener for an editor.
926 * It is not possible to simply have the findbar implement the
927 * listener interface itself, as it wouldn't have sufficient information
928 * to work out which editor was being destroyed. Therefore, we create new
929 * listeners on the fly, and cache them in sync with the editors they
932 _createStateListener: function () {
936 QueryInterface: function(aIID) {
937 if (aIID.equals(Ci.nsIDocumentStateListener) ||
938 aIID.equals(Ci.nsISupports))
941 throw Components.results.NS_ERROR_NO_INTERFACE;
944 NotifyDocumentWillBeDestroyed: function() {
945 this.findbar._onEditorDestruction(this);
949 notifyDocumentCreated: function() {},
950 notifyDocumentStateChanged: function(aDirty) {}
954 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
955 Ci.nsISupportsWeakReference])