toolkit/modules/Finder.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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",
    17                                          "nsITextToSubURI");
    18 XPCOMUtils.defineLazyServiceGetter(this, "Clipboard",
    19                                          "@mozilla.org/widget/clipboard;1",
    20                                          "nsIClipboard");
    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;
    30   this._listeners = [];
    31   this._previousLink = null;
    32   this._searchString = null;
    34   docShell.QueryInterface(Ci.nsIInterfaceRequestor)
    35           .getInterface(Ci.nsIWebProgress)
    36           .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
    37 }
    39 Finder.prototype = {
    40   addResultListener: function (aListener) {
    41     if (this._listeners.indexOf(aListener) === -1)
    42       this._listeners.push(aListener);
    43   },
    45   removeResultListener: function (aListener) {
    46     this._listeners = this._listeners.filter(l => l != aListener);
    47   },
    49   _notify: function (aSearchString, aResult, aFindBackwards, aDrawOutline, aStoreResult = true) {
    50     if (aStoreResult) {
    51       this._searchString = aSearchString;
    52       this.clipboardSearchString = aSearchString
    53     }
    54     this._outlineLink(aDrawOutline);
    56     let foundLink = this._fastFind.foundLink;
    57     let linkURL = null;
    58     if (foundLink) {
    59       let docCharset = null;
    60       let ownerDoc = foundLink.ownerDocument;
    61       if (ownerDoc)
    62         docCharset = ownerDoc.characterSet;
    64       linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
    65     }
    67     let data = {
    68       result: aResult,
    69       findBackwards: aFindBackwards,
    70       linkURL: linkURL,
    71       rect: this._getResultRect(),
    72       searchString: this._searchString,
    73       storeResult: aStoreResult
    74     };
    76     for (let l of this._listeners) {
    77       l.onFindResult(data);
    78     }
    79   },
    81   get searchString() {
    82     if (!this._searchString && this._fastFind.searchString)
    83       this._searchString = this._fastFind.searchString;
    84     return this._searchString;
    85   },
    87   get clipboardSearchString() {
    88     let searchString = "";
    89     if (!Clipboard.supportsFindClipboard())
    90       return searchString;
    92     try {
    93       let trans = Cc["@mozilla.org/widget/transferable;1"]
    94                     .createInstance(Ci.nsITransferable);
    95       trans.init(this._getWindow()
    96                      .QueryInterface(Ci.nsIInterfaceRequestor)
    97                      .getInterface(Ci.nsIWebNavigation)
    98                      .QueryInterface(Ci.nsILoadContext));
    99       trans.addDataFlavor("text/unicode");
   101       Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
   103       let data = {};
   104       let dataLen = {};
   105       trans.getTransferData("text/unicode", data, dataLen);
   106       if (data.value) {
   107         data = data.value.QueryInterface(Ci.nsISupportsString);
   108         searchString = data.toString();
   109       }
   110     } catch (ex) {}
   112     return searchString;
   113   },
   115   set clipboardSearchString(aSearchString) {
   116     if (!aSearchString || !Clipboard.supportsFindClipboard())
   117       return;
   119     ClipboardHelper.copyStringToClipboard(aSearchString,
   120                                           Ci.nsIClipboard.kFindClipboard,
   121                                           this._getWindow().document);
   122   },
   124   set caseSensitive(aSensitive) {
   125     this._fastFind.caseSensitive = aSensitive;
   126   },
   128   /**
   129    * Used for normal search operations, highlights the first match.
   130    *
   131    * @param aSearchString String to search for.
   132    * @param aLinksOnly Only consider nodes that are links for the search.
   133    * @param aDrawOutline Puts an outline around matched links.
   134    */
   135   fastFind: function (aSearchString, aLinksOnly, aDrawOutline) {
   136     let result = this._fastFind.find(aSearchString, aLinksOnly);
   137     let searchString = this._fastFind.searchString;
   138     this._notify(searchString, result, false, aDrawOutline);
   139   },
   141   /**
   142    * Repeat the previous search. Should only be called after a previous
   143    * call to Finder.fastFind.
   144    *
   145    * @param aFindBackwards Controls the search direction:
   146    *    true: before current match, false: after current match.
   147    * @param aLinksOnly Only consider nodes that are links for the search.
   148    * @param aDrawOutline Puts an outline around matched links.
   149    */
   150   findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
   151     let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
   152     let searchString = this._fastFind.searchString;
   153     this._notify(searchString, result, aFindBackwards, aDrawOutline);
   154   },
   156   /**
   157    * Forcibly set the search string of the find clipboard to the currently
   158    * selected text in the window, on supported platforms (i.e. OSX).
   159    */
   160   setSearchStringToSelection: function() {
   161     // Find the selected text.
   162     let selection = this._getWindow().getSelection();
   163     // Don't go for empty selections.
   164     if (!selection.rangeCount)
   165       return null;
   166     let searchString = (selection.toString() || "").trim();
   167     // Empty strings are rather useless to search for.
   168     if (!searchString.length)
   169       return null;
   171     this.clipboardSearchString = searchString;
   172     return searchString;
   173   },
   175   highlight: function (aHighlight, aWord) {
   176     let found = this._highlight(aHighlight, aWord, null);
   177     if (aHighlight) {
   178       let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
   179                          : Ci.nsITypeAheadFind.FIND_NOTFOUND;
   180       this._notify(aWord, result, false, false, false);
   181     }
   182   },
   184   enableSelection: function() {
   185     this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON);
   186     this._restoreOriginalOutline();
   187   },
   189   removeSelection: function() {
   190     this._fastFind.collapseSelection();
   191     this.enableSelection();
   192   },
   194   focusContent: function() {
   195     // Allow Finder listeners to cancel focusing the content.
   196     for (let l of this._listeners) {
   197       if (!l.shouldFocusContent())
   198         return;
   199     }
   201     let fastFind = this._fastFind;
   202     const fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
   203     try {
   204       // Try to find the best possible match that should receive focus and
   205       // block scrolling on focus since find already scrolls. Further
   206       // scrolling is due to user action, so don't override this.
   207       if (fastFind.foundLink) {
   208         fm.setFocus(fastFind.foundLink, fm.FLAG_NOSCROLL);
   209       } else if (fastFind.foundEditable) {
   210         fm.setFocus(fastFind.foundEditable, fm.FLAG_NOSCROLL);
   211         fastFind.collapseSelection();
   212       } else {
   213         this._getWindow().focus()
   214       }
   215     } catch (e) {}
   216   },
   218   keyPress: function (aEvent) {
   219     let controller = this._getSelectionController(this._getWindow());
   221     switch (aEvent.keyCode) {
   222       case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
   223         if (this._fastFind.foundLink) {
   224           let view = this._fastFind.foundLink.ownerDocument.defaultView;
   225           this._fastFind.foundLink.dispatchEvent(new view.MouseEvent("click", {
   226             view: view,
   227             cancelable: true,
   228             bubbles: true,
   229             ctrlKey: aEvent.ctrlKey,
   230             altKey: aEvent.altKey,
   231             shiftKey: aEvent.shiftKey,
   232             metaKey: aEvent.metaKey
   233           }));
   234         }
   235         break;
   236       case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
   237         let direction = Services.focus.MOVEFOCUS_FORWARD;
   238         if (aEvent.shiftKey) {
   239           direction = Services.focus.MOVEFOCUS_BACKWARD;
   240         }
   241         Services.focus.moveFocus(this._getWindow(), null, direction, 0);
   242         break;
   243       case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
   244         controller.scrollPage(false);
   245         break;
   246       case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
   247         controller.scrollPage(true);
   248         break;
   249       case Ci.nsIDOMKeyEvent.DOM_VK_UP:
   250         controller.scrollLine(false);
   251         break;
   252       case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
   253         controller.scrollLine(true);
   254         break;
   255     }
   256   },
   258   _getWindow: function () {
   259     return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
   260   },
   262   /**
   263    * Get the bounding selection rect in CSS px relative to the origin of the
   264    * top-level content document.
   265    */
   266   _getResultRect: function () {
   267     let topWin = this._getWindow();
   268     let win = this._fastFind.currentWindow;
   269     if (!win)
   270       return null;
   272     let selection = win.getSelection();
   273     if (!selection.rangeCount || selection.isCollapsed) {
   274       // The selection can be into an input or a textarea element.
   275       let nodes = win.document.querySelectorAll("input, textarea");
   276       for (let node of nodes) {
   277         if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) {
   278           let sc = node.editor.selectionController;
   279           selection = sc.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
   280           if (selection.rangeCount && !selection.isCollapsed) {
   281             break;
   282           }
   283         }
   284       }
   285     }
   287     let utils = topWin.QueryInterface(Ci.nsIInterfaceRequestor)
   288                       .getInterface(Ci.nsIDOMWindowUtils);
   290     let scrollX = {}, scrollY = {};
   291     utils.getScrollXY(false, scrollX, scrollY);
   293     for (let frame = win; frame != topWin; frame = frame.parent) {
   294       let rect = frame.frameElement.getBoundingClientRect();
   295       let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
   296       let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
   297       scrollX.value += rect.left + parseInt(left, 10);
   298       scrollY.value += rect.top + parseInt(top, 10);
   299     }
   300     let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect());
   301     return rect.translate(scrollX.value, scrollY.value);
   302   },
   304   _outlineLink: function (aDrawOutline) {
   305     let foundLink = this._fastFind.foundLink;
   307     // Optimization: We are drawing outlines and we matched
   308     // the same link before, so don't duplicate work.
   309     if (foundLink == this._previousLink && aDrawOutline)
   310       return;
   312     this._restoreOriginalOutline();
   314     if (foundLink && aDrawOutline) {
   315       // Backup original outline
   316       this._tmpOutline = foundLink.style.outline;
   317       this._tmpOutlineOffset = foundLink.style.outlineOffset;
   319       // Draw pseudo focus rect
   320       // XXX Should we change the following style for FAYT pseudo focus?
   321       // XXX Shouldn't we change default design if outline is visible
   322       //     already?
   323       // Don't set the outline-color, we should always use initial value.
   324       foundLink.style.outline = "1px dotted";
   325       foundLink.style.outlineOffset = "0";
   327       this._previousLink = foundLink;
   328     }
   329   },
   331   _restoreOriginalOutline: function () {
   332     // Removes the outline around the last found link.
   333     if (this._previousLink) {
   334       this._previousLink.style.outline = this._tmpOutline;
   335       this._previousLink.style.outlineOffset = this._tmpOutlineOffset;
   336       this._previousLink = null;
   337     }
   338   },
   340   _highlight: function (aHighlight, aWord, aWindow) {
   341     let win = aWindow || this._getWindow();
   343     let found = false;
   344     for (let i = 0; win.frames && i < win.frames.length; i++) {
   345       if (this._highlight(aHighlight, aWord, win.frames[i]))
   346         found = true;
   347     }
   349     let controller = this._getSelectionController(win);
   350     let doc = win.document;
   351     if (!controller || !doc || !doc.documentElement) {
   352       // Without the selection controller,
   353       // we are unable to (un)highlight any matches
   354       return found;
   355     }
   357     let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
   358                doc.body : doc.documentElement;
   360     if (aHighlight) {
   361       let searchRange = doc.createRange();
   362       searchRange.selectNodeContents(body);
   364       let startPt = searchRange.cloneRange();
   365       startPt.collapse(true);
   367       let endPt = searchRange.cloneRange();
   368       endPt.collapse(false);
   370       let retRange = null;
   371       let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
   372                      .createInstance()
   373                      .QueryInterface(Ci.nsIFind);
   375       finder.caseSensitive = this._fastFind.caseSensitive;
   377       while ((retRange = finder.Find(aWord, searchRange,
   378                                      startPt, endPt))) {
   379         this._highlightRange(retRange, controller);
   380         startPt = retRange.cloneRange();
   381         startPt.collapse(false);
   383         found = true;
   384       }
   385     } else {
   386       // First, attempt to remove highlighting from main document
   387       let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   388       sel.removeAllRanges();
   390       // Next, check our editor cache, for editors belonging to this
   391       // document
   392       if (this._editors) {
   393         for (let x = this._editors.length - 1; x >= 0; --x) {
   394           if (this._editors[x].document == doc) {
   395             sel = this._editors[x].selectionController
   396                                   .getSelection(Ci.nsISelectionController.SELECTION_FIND);
   397             sel.removeAllRanges();
   398             // We don't need to listen to this editor any more
   399             this._unhookListenersAtIndex(x);
   400           }
   401         }
   402       }
   404       // Removing the highlighting always succeeds, so return true.
   405       found = true;
   406     }
   408     return found;
   409   },
   411   _highlightRange: function(aRange, aController) {
   412     let node = aRange.startContainer;
   413     let controller = aController;
   415     let editableNode = this._getEditableNode(node);
   416     if (editableNode)
   417       controller = editableNode.editor.selectionController;
   419     let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   420     findSelection.addRange(aRange);
   422     if (editableNode) {
   423       // Highlighting added, so cache this editor, and hook up listeners
   424       // to ensure we deal properly with edits within the highlighting
   425       if (!this._editors) {
   426         this._editors = [];
   427         this._stateListeners = [];
   428       }
   430       let existingIndex = this._editors.indexOf(editableNode.editor);
   431       if (existingIndex == -1) {
   432         let x = this._editors.length;
   433         this._editors[x] = editableNode.editor;
   434         this._stateListeners[x] = this._createStateListener();
   435         this._editors[x].addEditActionListener(this);
   436         this._editors[x].addDocumentStateListener(this._stateListeners[x]);
   437       }
   438     }
   439   },
   441   _getSelectionController: function(aWindow) {
   442     // display: none iframes don't have a selection controller, see bug 493658
   443     if (!aWindow.innerWidth || !aWindow.innerHeight)
   444       return null;
   446     // Yuck. See bug 138068.
   447     let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   448                           .getInterface(Ci.nsIWebNavigation)
   449                           .QueryInterface(Ci.nsIDocShell);
   451     let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   452                              .getInterface(Ci.nsISelectionDisplay)
   453                              .QueryInterface(Ci.nsISelectionController);
   454     return controller;
   455   },
   457   /*
   458    * For a given node, walk up it's parent chain, to try and find an
   459    * editable node.
   460    *
   461    * @param aNode the node we want to check
   462    * @returns the first node in the parent chain that is editable,
   463    *          null if there is no such node
   464    */
   465   _getEditableNode: function (aNode) {
   466     while (aNode) {
   467       if (aNode instanceof Ci.nsIDOMNSEditableElement)
   468         return aNode.editor ? aNode : null;
   470       aNode = aNode.parentNode;
   471     }
   472     return null;
   473   },
   475   /*
   476    * Helper method to unhook listeners, remove cached editors
   477    * and keep the relevant arrays in sync
   478    *
   479    * @param aIndex the index into the array of editors/state listeners
   480    *        we wish to remove
   481    */
   482   _unhookListenersAtIndex: function (aIndex) {
   483     this._editors[aIndex].removeEditActionListener(this);
   484     this._editors[aIndex]
   485         .removeDocumentStateListener(this._stateListeners[aIndex]);
   486     this._editors.splice(aIndex, 1);
   487     this._stateListeners.splice(aIndex, 1);
   488     if (!this._editors.length) {
   489       delete this._editors;
   490       delete this._stateListeners;
   491     }
   492   },
   494   /*
   495    * Remove ourselves as an nsIEditActionListener and
   496    * nsIDocumentStateListener from a given cached editor
   497    *
   498    * @param aEditor the editor we no longer wish to listen to
   499    */
   500   _removeEditorListeners: function (aEditor) {
   501     // aEditor is an editor that we listen to, so therefore must be
   502     // cached. Find the index of this editor
   503     let idx = this._editors.indexOf(aEditor);
   504     if (idx == -1)
   505       return;
   506     // Now unhook ourselves, and remove our cached copy
   507     this._unhookListenersAtIndex(idx);
   508   },
   510   /*
   511    * nsIEditActionListener logic follows
   512    *
   513    * We implement this interface to allow us to catch the case where
   514    * the findbar found a match in a HTML <input> or <textarea>. If the
   515    * user adjusts the text in some way, it will no longer match, so we
   516    * want to remove the highlight, rather than have it expand/contract
   517    * when letters are added or removed.
   518    */
   520   /*
   521    * Helper method used to check whether a selection intersects with
   522    * some highlighting
   523    *
   524    * @param aSelectionRange the range from the selection to check
   525    * @param aFindRange the highlighted range to check against
   526    * @returns true if they intersect, false otherwise
   527    */
   528   _checkOverlap: function (aSelectionRange, aFindRange) {
   529     // The ranges overlap if one of the following is true:
   530     // 1) At least one of the endpoints of the deleted selection
   531     //    is in the find selection
   532     // 2) At least one of the endpoints of the find selection
   533     //    is in the deleted selection
   534     if (aFindRange.isPointInRange(aSelectionRange.startContainer,
   535                                   aSelectionRange.startOffset))
   536       return true;
   537     if (aFindRange.isPointInRange(aSelectionRange.endContainer,
   538                                   aSelectionRange.endOffset))
   539       return true;
   540     if (aSelectionRange.isPointInRange(aFindRange.startContainer,
   541                                        aFindRange.startOffset))
   542       return true;
   543     if (aSelectionRange.isPointInRange(aFindRange.endContainer,
   544                                        aFindRange.endOffset))
   545       return true;
   547     return false;
   548   },
   550   /*
   551    * Helper method to determine if an edit occurred within a highlight
   552    *
   553    * @param aSelection the selection we wish to check
   554    * @param aNode the node we want to check is contained in aSelection
   555    * @param aOffset the offset into aNode that we want to check
   556    * @returns the range containing (aNode, aOffset) or null if no ranges
   557    *          in the selection contain it
   558    */
   559   _findRange: function (aSelection, aNode, aOffset) {
   560     let rangeCount = aSelection.rangeCount;
   561     let rangeidx = 0;
   562     let foundContainingRange = false;
   563     let range = null;
   565     // Check to see if this node is inside one of the selection's ranges
   566     while (!foundContainingRange && rangeidx < rangeCount) {
   567       range = aSelection.getRangeAt(rangeidx);
   568       if (range.isPointInRange(aNode, aOffset)) {
   569         foundContainingRange = true;
   570         break;
   571       }
   572       rangeidx++;
   573     }
   575     if (foundContainingRange)
   576       return range;
   578     return null;
   579   },
   581   // Start of nsIWebProgressListener implementation.
   583   onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
   584     if (!aWebProgress.isTopLevel)
   585       return;
   587     // Avoid leaking if we change the page.
   588     this._previousLink = null;
   589   },
   591   // Start of nsIEditActionListener implementations
   593   WillDeleteText: function (aTextNode, aOffset, aLength) {
   594     let editor = this._getEditableNode(aTextNode).editor;
   595     let controller = editor.selectionController;
   596     let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   597     let range = this._findRange(fSelection, aTextNode, aOffset);
   599     if (range) {
   600       // Don't remove the highlighting if the deleted text is at the
   601       // end of the range
   602       if (aTextNode != range.endContainer ||
   603           aOffset != range.endOffset) {
   604         // Text within the highlight is being removed - the text can
   605         // no longer be a match, so remove the highlighting
   606         fSelection.removeRange(range);
   607         if (fSelection.rangeCount == 0)
   608           this._removeEditorListeners(editor);
   609       }
   610     }
   611   },
   613   DidInsertText: function (aTextNode, aOffset, aString) {
   614     let editor = this._getEditableNode(aTextNode).editor;
   615     let controller = editor.selectionController;
   616     let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   617     let range = this._findRange(fSelection, aTextNode, aOffset);
   619     if (range) {
   620       // If the text was inserted before the highlight
   621       // adjust the highlight's bounds accordingly
   622       if (aTextNode == range.startContainer &&
   623           aOffset == range.startOffset) {
   624         range.setStart(range.startContainer,
   625                        range.startOffset+aString.length);
   626       } else if (aTextNode != range.endContainer ||
   627                  aOffset != range.endOffset) {
   628         // The edit occurred within the highlight - any addition of text
   629         // will result in the text no longer being a match,
   630         // so remove the highlighting
   631         fSelection.removeRange(range);
   632         if (fSelection.rangeCount == 0)
   633           this._removeEditorListeners(editor);
   634       }
   635     }
   636   },
   638   WillDeleteSelection: function (aSelection) {
   639     let editor = this._getEditableNode(aSelection.getRangeAt(0)
   640                                                  .startContainer).editor;
   641     let controller = editor.selectionController;
   642     let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   644     let selectionIndex = 0;
   645     let findSelectionIndex = 0;
   646     let shouldDelete = {};
   647     let numberOfDeletedSelections = 0;
   648     let numberOfMatches = fSelection.rangeCount;
   650     // We need to test if any ranges in the deleted selection (aSelection)
   651     // are in any of the ranges of the find selection
   652     // Usually both selections will only contain one range, however
   653     // either may contain more than one.
   655     for (let fIndex = 0; fIndex < numberOfMatches; fIndex++) {
   656       shouldDelete[fIndex] = false;
   657       let fRange = fSelection.getRangeAt(fIndex);
   659       for (let index = 0; index < aSelection.rangeCount; index++) {
   660         if (shouldDelete[fIndex])
   661           continue;
   663         let selRange = aSelection.getRangeAt(index);
   664         let doesOverlap = this._checkOverlap(selRange, fRange);
   665         if (doesOverlap) {
   666           shouldDelete[fIndex] = true;
   667           numberOfDeletedSelections++;
   668         }
   669       }
   670     }
   672     // OK, so now we know what matches (if any) are in the selection
   673     // that is being deleted. Time to remove them.
   674     if (numberOfDeletedSelections == 0)
   675       return;
   677     for (let i = numberOfMatches - 1; i >= 0; i--) {
   678       if (shouldDelete[i])
   679         fSelection.removeRange(fSelection.getRangeAt(i));
   680     }
   682     // Remove listeners if no more highlights left
   683     if (fSelection.rangeCount == 0)
   684       this._removeEditorListeners(editor);
   685   },
   687   /*
   688    * nsIDocumentStateListener logic follows
   689    *
   690    * When attaching nsIEditActionListeners, there are no guarantees
   691    * as to whether the findbar or the documents in the browser will get
   692    * destructed first. This leads to the potential to either leak, or to
   693    * hold on to a reference an editable element's editor for too long,
   694    * preventing it from being destructed.
   695    *
   696    * However, when an editor's owning node is being destroyed, the editor
   697    * sends out a DocumentWillBeDestroyed notification. We can use this to
   698    * clean up our references to the object, to allow it to be destroyed in a
   699    * timely fashion.
   700    */
   702   /*
   703    * Unhook ourselves when one of our state listeners has been called.
   704    * This can happen in 4 cases:
   705    *  1) The document the editor belongs to is navigated away from, and
   706    *     the document is not being cached
   707    *
   708    *  2) The document the editor belongs to is expired from the cache
   709    *
   710    *  3) The tab containing the owning document is closed
   711    *
   712    *  4) The <input> or <textarea> that owns the editor is explicitly
   713    *     removed from the DOM
   714    *
   715    * @param the listener that was invoked
   716    */
   717   _onEditorDestruction: function (aListener) {
   718     // First find the index of the editor the given listener listens to.
   719     // The listeners and editors arrays must always be in sync.
   720     // The listener will be in our array of cached listeners, as this
   721     // method could not have been called otherwise.
   722     let idx = 0;
   723     while (this._stateListeners[idx] != aListener)
   724       idx++;
   726     // Unhook both listeners
   727     this._unhookListenersAtIndex(idx);
   728   },
   730   /*
   731    * Creates a unique document state listener for an editor.
   732    *
   733    * It is not possible to simply have the findbar implement the
   734    * listener interface itself, as it wouldn't have sufficient information
   735    * to work out which editor was being destroyed. Therefore, we create new
   736    * listeners on the fly, and cache them in sync with the editors they
   737    * listen to.
   738    */
   739   _createStateListener: function () {
   740     return {
   741       findbar: this,
   743       QueryInterface: function(aIID) {
   744         if (aIID.equals(Ci.nsIDocumentStateListener) ||
   745             aIID.equals(Ci.nsISupports))
   746           return this;
   748         throw Components.results.NS_ERROR_NO_INTERFACE;
   749       },
   751       NotifyDocumentWillBeDestroyed: function() {
   752         this.findbar._onEditorDestruction(this);
   753       },
   755       // Unimplemented
   756       notifyDocumentCreated: function() {},
   757       notifyDocumentStateChanged: function(aDirty) {}
   758     };
   759   },
   761   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
   762                                          Ci.nsISupportsWeakReference])
   763 };

mercurial