toolkit/modules/Finder.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/Finder.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,763 @@
     1.4 +// This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +// License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +// file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["Finder"];
     1.9 +
    1.10 +const Ci = Components.interfaces;
    1.11 +const Cc = Components.classes;
    1.12 +const Cu = Components.utils;
    1.13 +
    1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.15 +Cu.import("resource://gre/modules/Geometry.jsm");
    1.16 +Cu.import("resource://gre/modules/Services.jsm");
    1.17 +
    1.18 +XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService",
    1.19 +                                         "@mozilla.org/intl/texttosuburi;1",
    1.20 +                                         "nsITextToSubURI");
    1.21 +XPCOMUtils.defineLazyServiceGetter(this, "Clipboard",
    1.22 +                                         "@mozilla.org/widget/clipboard;1",
    1.23 +                                         "nsIClipboard");
    1.24 +XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
    1.25 +                                         "@mozilla.org/widget/clipboardhelper;1",
    1.26 +                                         "nsIClipboardHelper");
    1.27 +
    1.28 +function Finder(docShell) {
    1.29 +  this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
    1.30 +  this._fastFind.init(docShell);
    1.31 +
    1.32 +  this._docShell = docShell;
    1.33 +  this._listeners = [];
    1.34 +  this._previousLink = null;
    1.35 +  this._searchString = null;
    1.36 +
    1.37 +  docShell.QueryInterface(Ci.nsIInterfaceRequestor)
    1.38 +          .getInterface(Ci.nsIWebProgress)
    1.39 +          .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
    1.40 +}
    1.41 +
    1.42 +Finder.prototype = {
    1.43 +  addResultListener: function (aListener) {
    1.44 +    if (this._listeners.indexOf(aListener) === -1)
    1.45 +      this._listeners.push(aListener);
    1.46 +  },
    1.47 +
    1.48 +  removeResultListener: function (aListener) {
    1.49 +    this._listeners = this._listeners.filter(l => l != aListener);
    1.50 +  },
    1.51 +
    1.52 +  _notify: function (aSearchString, aResult, aFindBackwards, aDrawOutline, aStoreResult = true) {
    1.53 +    if (aStoreResult) {
    1.54 +      this._searchString = aSearchString;
    1.55 +      this.clipboardSearchString = aSearchString
    1.56 +    }
    1.57 +    this._outlineLink(aDrawOutline);
    1.58 +
    1.59 +    let foundLink = this._fastFind.foundLink;
    1.60 +    let linkURL = null;
    1.61 +    if (foundLink) {
    1.62 +      let docCharset = null;
    1.63 +      let ownerDoc = foundLink.ownerDocument;
    1.64 +      if (ownerDoc)
    1.65 +        docCharset = ownerDoc.characterSet;
    1.66 +
    1.67 +      linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
    1.68 +    }
    1.69 +
    1.70 +    let data = {
    1.71 +      result: aResult,
    1.72 +      findBackwards: aFindBackwards,
    1.73 +      linkURL: linkURL,
    1.74 +      rect: this._getResultRect(),
    1.75 +      searchString: this._searchString,
    1.76 +      storeResult: aStoreResult
    1.77 +    };
    1.78 +
    1.79 +    for (let l of this._listeners) {
    1.80 +      l.onFindResult(data);
    1.81 +    }
    1.82 +  },
    1.83 +
    1.84 +  get searchString() {
    1.85 +    if (!this._searchString && this._fastFind.searchString)
    1.86 +      this._searchString = this._fastFind.searchString;
    1.87 +    return this._searchString;
    1.88 +  },
    1.89 +
    1.90 +  get clipboardSearchString() {
    1.91 +    let searchString = "";
    1.92 +    if (!Clipboard.supportsFindClipboard())
    1.93 +      return searchString;
    1.94 +
    1.95 +    try {
    1.96 +      let trans = Cc["@mozilla.org/widget/transferable;1"]
    1.97 +                    .createInstance(Ci.nsITransferable);
    1.98 +      trans.init(this._getWindow()
    1.99 +                     .QueryInterface(Ci.nsIInterfaceRequestor)
   1.100 +                     .getInterface(Ci.nsIWebNavigation)
   1.101 +                     .QueryInterface(Ci.nsILoadContext));
   1.102 +      trans.addDataFlavor("text/unicode");
   1.103 +
   1.104 +      Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
   1.105 +
   1.106 +      let data = {};
   1.107 +      let dataLen = {};
   1.108 +      trans.getTransferData("text/unicode", data, dataLen);
   1.109 +      if (data.value) {
   1.110 +        data = data.value.QueryInterface(Ci.nsISupportsString);
   1.111 +        searchString = data.toString();
   1.112 +      }
   1.113 +    } catch (ex) {}
   1.114 +
   1.115 +    return searchString;
   1.116 +  },
   1.117 +
   1.118 +  set clipboardSearchString(aSearchString) {
   1.119 +    if (!aSearchString || !Clipboard.supportsFindClipboard())
   1.120 +      return;
   1.121 +
   1.122 +    ClipboardHelper.copyStringToClipboard(aSearchString,
   1.123 +                                          Ci.nsIClipboard.kFindClipboard,
   1.124 +                                          this._getWindow().document);
   1.125 +  },
   1.126 +
   1.127 +  set caseSensitive(aSensitive) {
   1.128 +    this._fastFind.caseSensitive = aSensitive;
   1.129 +  },
   1.130 +
   1.131 +  /**
   1.132 +   * Used for normal search operations, highlights the first match.
   1.133 +   *
   1.134 +   * @param aSearchString String to search for.
   1.135 +   * @param aLinksOnly Only consider nodes that are links for the search.
   1.136 +   * @param aDrawOutline Puts an outline around matched links.
   1.137 +   */
   1.138 +  fastFind: function (aSearchString, aLinksOnly, aDrawOutline) {
   1.139 +    let result = this._fastFind.find(aSearchString, aLinksOnly);
   1.140 +    let searchString = this._fastFind.searchString;
   1.141 +    this._notify(searchString, result, false, aDrawOutline);
   1.142 +  },
   1.143 +
   1.144 +  /**
   1.145 +   * Repeat the previous search. Should only be called after a previous
   1.146 +   * call to Finder.fastFind.
   1.147 +   *
   1.148 +   * @param aFindBackwards Controls the search direction:
   1.149 +   *    true: before current match, false: after current match.
   1.150 +   * @param aLinksOnly Only consider nodes that are links for the search.
   1.151 +   * @param aDrawOutline Puts an outline around matched links.
   1.152 +   */
   1.153 +  findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
   1.154 +    let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
   1.155 +    let searchString = this._fastFind.searchString;
   1.156 +    this._notify(searchString, result, aFindBackwards, aDrawOutline);
   1.157 +  },
   1.158 +
   1.159 +  /**
   1.160 +   * Forcibly set the search string of the find clipboard to the currently
   1.161 +   * selected text in the window, on supported platforms (i.e. OSX).
   1.162 +   */
   1.163 +  setSearchStringToSelection: function() {
   1.164 +    // Find the selected text.
   1.165 +    let selection = this._getWindow().getSelection();
   1.166 +    // Don't go for empty selections.
   1.167 +    if (!selection.rangeCount)
   1.168 +      return null;
   1.169 +    let searchString = (selection.toString() || "").trim();
   1.170 +    // Empty strings are rather useless to search for.
   1.171 +    if (!searchString.length)
   1.172 +      return null;
   1.173 +
   1.174 +    this.clipboardSearchString = searchString;
   1.175 +    return searchString;
   1.176 +  },
   1.177 +
   1.178 +  highlight: function (aHighlight, aWord) {
   1.179 +    let found = this._highlight(aHighlight, aWord, null);
   1.180 +    if (aHighlight) {
   1.181 +      let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
   1.182 +                         : Ci.nsITypeAheadFind.FIND_NOTFOUND;
   1.183 +      this._notify(aWord, result, false, false, false);
   1.184 +    }
   1.185 +  },
   1.186 +
   1.187 +  enableSelection: function() {
   1.188 +    this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON);
   1.189 +    this._restoreOriginalOutline();
   1.190 +  },
   1.191 +
   1.192 +  removeSelection: function() {
   1.193 +    this._fastFind.collapseSelection();
   1.194 +    this.enableSelection();
   1.195 +  },
   1.196 +
   1.197 +  focusContent: function() {
   1.198 +    // Allow Finder listeners to cancel focusing the content.
   1.199 +    for (let l of this._listeners) {
   1.200 +      if (!l.shouldFocusContent())
   1.201 +        return;
   1.202 +    }
   1.203 +
   1.204 +    let fastFind = this._fastFind;
   1.205 +    const fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
   1.206 +    try {
   1.207 +      // Try to find the best possible match that should receive focus and
   1.208 +      // block scrolling on focus since find already scrolls. Further
   1.209 +      // scrolling is due to user action, so don't override this.
   1.210 +      if (fastFind.foundLink) {
   1.211 +        fm.setFocus(fastFind.foundLink, fm.FLAG_NOSCROLL);
   1.212 +      } else if (fastFind.foundEditable) {
   1.213 +        fm.setFocus(fastFind.foundEditable, fm.FLAG_NOSCROLL);
   1.214 +        fastFind.collapseSelection();
   1.215 +      } else {
   1.216 +        this._getWindow().focus()
   1.217 +      }
   1.218 +    } catch (e) {}
   1.219 +  },
   1.220 +
   1.221 +  keyPress: function (aEvent) {
   1.222 +    let controller = this._getSelectionController(this._getWindow());
   1.223 +
   1.224 +    switch (aEvent.keyCode) {
   1.225 +      case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
   1.226 +        if (this._fastFind.foundLink) {
   1.227 +          let view = this._fastFind.foundLink.ownerDocument.defaultView;
   1.228 +          this._fastFind.foundLink.dispatchEvent(new view.MouseEvent("click", {
   1.229 +            view: view,
   1.230 +            cancelable: true,
   1.231 +            bubbles: true,
   1.232 +            ctrlKey: aEvent.ctrlKey,
   1.233 +            altKey: aEvent.altKey,
   1.234 +            shiftKey: aEvent.shiftKey,
   1.235 +            metaKey: aEvent.metaKey
   1.236 +          }));
   1.237 +        }
   1.238 +        break;
   1.239 +      case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
   1.240 +        let direction = Services.focus.MOVEFOCUS_FORWARD;
   1.241 +        if (aEvent.shiftKey) {
   1.242 +          direction = Services.focus.MOVEFOCUS_BACKWARD;
   1.243 +        }
   1.244 +        Services.focus.moveFocus(this._getWindow(), null, direction, 0);
   1.245 +        break;
   1.246 +      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP:
   1.247 +        controller.scrollPage(false);
   1.248 +        break;
   1.249 +      case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN:
   1.250 +        controller.scrollPage(true);
   1.251 +        break;
   1.252 +      case Ci.nsIDOMKeyEvent.DOM_VK_UP:
   1.253 +        controller.scrollLine(false);
   1.254 +        break;
   1.255 +      case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
   1.256 +        controller.scrollLine(true);
   1.257 +        break;
   1.258 +    }
   1.259 +  },
   1.260 +
   1.261 +  _getWindow: function () {
   1.262 +    return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
   1.263 +  },
   1.264 +
   1.265 +  /**
   1.266 +   * Get the bounding selection rect in CSS px relative to the origin of the
   1.267 +   * top-level content document.
   1.268 +   */
   1.269 +  _getResultRect: function () {
   1.270 +    let topWin = this._getWindow();
   1.271 +    let win = this._fastFind.currentWindow;
   1.272 +    if (!win)
   1.273 +      return null;
   1.274 +
   1.275 +    let selection = win.getSelection();
   1.276 +    if (!selection.rangeCount || selection.isCollapsed) {
   1.277 +      // The selection can be into an input or a textarea element.
   1.278 +      let nodes = win.document.querySelectorAll("input, textarea");
   1.279 +      for (let node of nodes) {
   1.280 +        if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) {
   1.281 +          let sc = node.editor.selectionController;
   1.282 +          selection = sc.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
   1.283 +          if (selection.rangeCount && !selection.isCollapsed) {
   1.284 +            break;
   1.285 +          }
   1.286 +        }
   1.287 +      }
   1.288 +    }
   1.289 +
   1.290 +    let utils = topWin.QueryInterface(Ci.nsIInterfaceRequestor)
   1.291 +                      .getInterface(Ci.nsIDOMWindowUtils);
   1.292 +
   1.293 +    let scrollX = {}, scrollY = {};
   1.294 +    utils.getScrollXY(false, scrollX, scrollY);
   1.295 +
   1.296 +    for (let frame = win; frame != topWin; frame = frame.parent) {
   1.297 +      let rect = frame.frameElement.getBoundingClientRect();
   1.298 +      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
   1.299 +      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
   1.300 +      scrollX.value += rect.left + parseInt(left, 10);
   1.301 +      scrollY.value += rect.top + parseInt(top, 10);
   1.302 +    }
   1.303 +    let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect());
   1.304 +    return rect.translate(scrollX.value, scrollY.value);
   1.305 +  },
   1.306 +
   1.307 +  _outlineLink: function (aDrawOutline) {
   1.308 +    let foundLink = this._fastFind.foundLink;
   1.309 +
   1.310 +    // Optimization: We are drawing outlines and we matched
   1.311 +    // the same link before, so don't duplicate work.
   1.312 +    if (foundLink == this._previousLink && aDrawOutline)
   1.313 +      return;
   1.314 +
   1.315 +    this._restoreOriginalOutline();
   1.316 +
   1.317 +    if (foundLink && aDrawOutline) {
   1.318 +      // Backup original outline
   1.319 +      this._tmpOutline = foundLink.style.outline;
   1.320 +      this._tmpOutlineOffset = foundLink.style.outlineOffset;
   1.321 +
   1.322 +      // Draw pseudo focus rect
   1.323 +      // XXX Should we change the following style for FAYT pseudo focus?
   1.324 +      // XXX Shouldn't we change default design if outline is visible
   1.325 +      //     already?
   1.326 +      // Don't set the outline-color, we should always use initial value.
   1.327 +      foundLink.style.outline = "1px dotted";
   1.328 +      foundLink.style.outlineOffset = "0";
   1.329 +
   1.330 +      this._previousLink = foundLink;
   1.331 +    }
   1.332 +  },
   1.333 +
   1.334 +  _restoreOriginalOutline: function () {
   1.335 +    // Removes the outline around the last found link.
   1.336 +    if (this._previousLink) {
   1.337 +      this._previousLink.style.outline = this._tmpOutline;
   1.338 +      this._previousLink.style.outlineOffset = this._tmpOutlineOffset;
   1.339 +      this._previousLink = null;
   1.340 +    }
   1.341 +  },
   1.342 +
   1.343 +  _highlight: function (aHighlight, aWord, aWindow) {
   1.344 +    let win = aWindow || this._getWindow();
   1.345 +
   1.346 +    let found = false;
   1.347 +    for (let i = 0; win.frames && i < win.frames.length; i++) {
   1.348 +      if (this._highlight(aHighlight, aWord, win.frames[i]))
   1.349 +        found = true;
   1.350 +    }
   1.351 +
   1.352 +    let controller = this._getSelectionController(win);
   1.353 +    let doc = win.document;
   1.354 +    if (!controller || !doc || !doc.documentElement) {
   1.355 +      // Without the selection controller,
   1.356 +      // we are unable to (un)highlight any matches
   1.357 +      return found;
   1.358 +    }
   1.359 +
   1.360 +    let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ?
   1.361 +               doc.body : doc.documentElement;
   1.362 +
   1.363 +    if (aHighlight) {
   1.364 +      let searchRange = doc.createRange();
   1.365 +      searchRange.selectNodeContents(body);
   1.366 +
   1.367 +      let startPt = searchRange.cloneRange();
   1.368 +      startPt.collapse(true);
   1.369 +
   1.370 +      let endPt = searchRange.cloneRange();
   1.371 +      endPt.collapse(false);
   1.372 +
   1.373 +      let retRange = null;
   1.374 +      let finder = Cc["@mozilla.org/embedcomp/rangefind;1"]
   1.375 +                     .createInstance()
   1.376 +                     .QueryInterface(Ci.nsIFind);
   1.377 +
   1.378 +      finder.caseSensitive = this._fastFind.caseSensitive;
   1.379 +
   1.380 +      while ((retRange = finder.Find(aWord, searchRange,
   1.381 +                                     startPt, endPt))) {
   1.382 +        this._highlightRange(retRange, controller);
   1.383 +        startPt = retRange.cloneRange();
   1.384 +        startPt.collapse(false);
   1.385 +
   1.386 +        found = true;
   1.387 +      }
   1.388 +    } else {
   1.389 +      // First, attempt to remove highlighting from main document
   1.390 +      let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.391 +      sel.removeAllRanges();
   1.392 +
   1.393 +      // Next, check our editor cache, for editors belonging to this
   1.394 +      // document
   1.395 +      if (this._editors) {
   1.396 +        for (let x = this._editors.length - 1; x >= 0; --x) {
   1.397 +          if (this._editors[x].document == doc) {
   1.398 +            sel = this._editors[x].selectionController
   1.399 +                                  .getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.400 +            sel.removeAllRanges();
   1.401 +            // We don't need to listen to this editor any more
   1.402 +            this._unhookListenersAtIndex(x);
   1.403 +          }
   1.404 +        }
   1.405 +      }
   1.406 +
   1.407 +      // Removing the highlighting always succeeds, so return true.
   1.408 +      found = true;
   1.409 +    }
   1.410 +
   1.411 +    return found;
   1.412 +  },
   1.413 +
   1.414 +  _highlightRange: function(aRange, aController) {
   1.415 +    let node = aRange.startContainer;
   1.416 +    let controller = aController;
   1.417 +
   1.418 +    let editableNode = this._getEditableNode(node);
   1.419 +    if (editableNode)
   1.420 +      controller = editableNode.editor.selectionController;
   1.421 +
   1.422 +    let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.423 +    findSelection.addRange(aRange);
   1.424 +
   1.425 +    if (editableNode) {
   1.426 +      // Highlighting added, so cache this editor, and hook up listeners
   1.427 +      // to ensure we deal properly with edits within the highlighting
   1.428 +      if (!this._editors) {
   1.429 +        this._editors = [];
   1.430 +        this._stateListeners = [];
   1.431 +      }
   1.432 +
   1.433 +      let existingIndex = this._editors.indexOf(editableNode.editor);
   1.434 +      if (existingIndex == -1) {
   1.435 +        let x = this._editors.length;
   1.436 +        this._editors[x] = editableNode.editor;
   1.437 +        this._stateListeners[x] = this._createStateListener();
   1.438 +        this._editors[x].addEditActionListener(this);
   1.439 +        this._editors[x].addDocumentStateListener(this._stateListeners[x]);
   1.440 +      }
   1.441 +    }
   1.442 +  },
   1.443 +
   1.444 +  _getSelectionController: function(aWindow) {
   1.445 +    // display: none iframes don't have a selection controller, see bug 493658
   1.446 +    if (!aWindow.innerWidth || !aWindow.innerHeight)
   1.447 +      return null;
   1.448 +
   1.449 +    // Yuck. See bug 138068.
   1.450 +    let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.451 +                          .getInterface(Ci.nsIWebNavigation)
   1.452 +                          .QueryInterface(Ci.nsIDocShell);
   1.453 +
   1.454 +    let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   1.455 +                             .getInterface(Ci.nsISelectionDisplay)
   1.456 +                             .QueryInterface(Ci.nsISelectionController);
   1.457 +    return controller;
   1.458 +  },
   1.459 +
   1.460 +  /*
   1.461 +   * For a given node, walk up it's parent chain, to try and find an
   1.462 +   * editable node.
   1.463 +   *
   1.464 +   * @param aNode the node we want to check
   1.465 +   * @returns the first node in the parent chain that is editable,
   1.466 +   *          null if there is no such node
   1.467 +   */
   1.468 +  _getEditableNode: function (aNode) {
   1.469 +    while (aNode) {
   1.470 +      if (aNode instanceof Ci.nsIDOMNSEditableElement)
   1.471 +        return aNode.editor ? aNode : null;
   1.472 +
   1.473 +      aNode = aNode.parentNode;
   1.474 +    }
   1.475 +    return null;
   1.476 +  },
   1.477 +
   1.478 +  /*
   1.479 +   * Helper method to unhook listeners, remove cached editors
   1.480 +   * and keep the relevant arrays in sync
   1.481 +   *
   1.482 +   * @param aIndex the index into the array of editors/state listeners
   1.483 +   *        we wish to remove
   1.484 +   */
   1.485 +  _unhookListenersAtIndex: function (aIndex) {
   1.486 +    this._editors[aIndex].removeEditActionListener(this);
   1.487 +    this._editors[aIndex]
   1.488 +        .removeDocumentStateListener(this._stateListeners[aIndex]);
   1.489 +    this._editors.splice(aIndex, 1);
   1.490 +    this._stateListeners.splice(aIndex, 1);
   1.491 +    if (!this._editors.length) {
   1.492 +      delete this._editors;
   1.493 +      delete this._stateListeners;
   1.494 +    }
   1.495 +  },
   1.496 +
   1.497 +  /*
   1.498 +   * Remove ourselves as an nsIEditActionListener and
   1.499 +   * nsIDocumentStateListener from a given cached editor
   1.500 +   *
   1.501 +   * @param aEditor the editor we no longer wish to listen to
   1.502 +   */
   1.503 +  _removeEditorListeners: function (aEditor) {
   1.504 +    // aEditor is an editor that we listen to, so therefore must be
   1.505 +    // cached. Find the index of this editor
   1.506 +    let idx = this._editors.indexOf(aEditor);
   1.507 +    if (idx == -1)
   1.508 +      return;
   1.509 +    // Now unhook ourselves, and remove our cached copy
   1.510 +    this._unhookListenersAtIndex(idx);
   1.511 +  },
   1.512 +
   1.513 +  /*
   1.514 +   * nsIEditActionListener logic follows
   1.515 +   *
   1.516 +   * We implement this interface to allow us to catch the case where
   1.517 +   * the findbar found a match in a HTML <input> or <textarea>. If the
   1.518 +   * user adjusts the text in some way, it will no longer match, so we
   1.519 +   * want to remove the highlight, rather than have it expand/contract
   1.520 +   * when letters are added or removed.
   1.521 +   */
   1.522 +
   1.523 +  /*
   1.524 +   * Helper method used to check whether a selection intersects with
   1.525 +   * some highlighting
   1.526 +   *
   1.527 +   * @param aSelectionRange the range from the selection to check
   1.528 +   * @param aFindRange the highlighted range to check against
   1.529 +   * @returns true if they intersect, false otherwise
   1.530 +   */
   1.531 +  _checkOverlap: function (aSelectionRange, aFindRange) {
   1.532 +    // The ranges overlap if one of the following is true:
   1.533 +    // 1) At least one of the endpoints of the deleted selection
   1.534 +    //    is in the find selection
   1.535 +    // 2) At least one of the endpoints of the find selection
   1.536 +    //    is in the deleted selection
   1.537 +    if (aFindRange.isPointInRange(aSelectionRange.startContainer,
   1.538 +                                  aSelectionRange.startOffset))
   1.539 +      return true;
   1.540 +    if (aFindRange.isPointInRange(aSelectionRange.endContainer,
   1.541 +                                  aSelectionRange.endOffset))
   1.542 +      return true;
   1.543 +    if (aSelectionRange.isPointInRange(aFindRange.startContainer,
   1.544 +                                       aFindRange.startOffset))
   1.545 +      return true;
   1.546 +    if (aSelectionRange.isPointInRange(aFindRange.endContainer,
   1.547 +                                       aFindRange.endOffset))
   1.548 +      return true;
   1.549 +
   1.550 +    return false;
   1.551 +  },
   1.552 +
   1.553 +  /*
   1.554 +   * Helper method to determine if an edit occurred within a highlight
   1.555 +   *
   1.556 +   * @param aSelection the selection we wish to check
   1.557 +   * @param aNode the node we want to check is contained in aSelection
   1.558 +   * @param aOffset the offset into aNode that we want to check
   1.559 +   * @returns the range containing (aNode, aOffset) or null if no ranges
   1.560 +   *          in the selection contain it
   1.561 +   */
   1.562 +  _findRange: function (aSelection, aNode, aOffset) {
   1.563 +    let rangeCount = aSelection.rangeCount;
   1.564 +    let rangeidx = 0;
   1.565 +    let foundContainingRange = false;
   1.566 +    let range = null;
   1.567 +
   1.568 +    // Check to see if this node is inside one of the selection's ranges
   1.569 +    while (!foundContainingRange && rangeidx < rangeCount) {
   1.570 +      range = aSelection.getRangeAt(rangeidx);
   1.571 +      if (range.isPointInRange(aNode, aOffset)) {
   1.572 +        foundContainingRange = true;
   1.573 +        break;
   1.574 +      }
   1.575 +      rangeidx++;
   1.576 +    }
   1.577 +
   1.578 +    if (foundContainingRange)
   1.579 +      return range;
   1.580 +
   1.581 +    return null;
   1.582 +  },
   1.583 +
   1.584 +  // Start of nsIWebProgressListener implementation.
   1.585 +
   1.586 +  onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
   1.587 +    if (!aWebProgress.isTopLevel)
   1.588 +      return;
   1.589 +
   1.590 +    // Avoid leaking if we change the page.
   1.591 +    this._previousLink = null;
   1.592 +  },
   1.593 +
   1.594 +  // Start of nsIEditActionListener implementations
   1.595 +
   1.596 +  WillDeleteText: function (aTextNode, aOffset, aLength) {
   1.597 +    let editor = this._getEditableNode(aTextNode).editor;
   1.598 +    let controller = editor.selectionController;
   1.599 +    let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.600 +    let range = this._findRange(fSelection, aTextNode, aOffset);
   1.601 +
   1.602 +    if (range) {
   1.603 +      // Don't remove the highlighting if the deleted text is at the
   1.604 +      // end of the range
   1.605 +      if (aTextNode != range.endContainer ||
   1.606 +          aOffset != range.endOffset) {
   1.607 +        // Text within the highlight is being removed - the text can
   1.608 +        // no longer be a match, so remove the highlighting
   1.609 +        fSelection.removeRange(range);
   1.610 +        if (fSelection.rangeCount == 0)
   1.611 +          this._removeEditorListeners(editor);
   1.612 +      }
   1.613 +    }
   1.614 +  },
   1.615 +
   1.616 +  DidInsertText: function (aTextNode, aOffset, aString) {
   1.617 +    let editor = this._getEditableNode(aTextNode).editor;
   1.618 +    let controller = editor.selectionController;
   1.619 +    let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.620 +    let range = this._findRange(fSelection, aTextNode, aOffset);
   1.621 +
   1.622 +    if (range) {
   1.623 +      // If the text was inserted before the highlight
   1.624 +      // adjust the highlight's bounds accordingly
   1.625 +      if (aTextNode == range.startContainer &&
   1.626 +          aOffset == range.startOffset) {
   1.627 +        range.setStart(range.startContainer,
   1.628 +                       range.startOffset+aString.length);
   1.629 +      } else if (aTextNode != range.endContainer ||
   1.630 +                 aOffset != range.endOffset) {
   1.631 +        // The edit occurred within the highlight - any addition of text
   1.632 +        // will result in the text no longer being a match,
   1.633 +        // so remove the highlighting
   1.634 +        fSelection.removeRange(range);
   1.635 +        if (fSelection.rangeCount == 0)
   1.636 +          this._removeEditorListeners(editor);
   1.637 +      }
   1.638 +    }
   1.639 +  },
   1.640 +
   1.641 +  WillDeleteSelection: function (aSelection) {
   1.642 +    let editor = this._getEditableNode(aSelection.getRangeAt(0)
   1.643 +                                                 .startContainer).editor;
   1.644 +    let controller = editor.selectionController;
   1.645 +    let fSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
   1.646 +
   1.647 +    let selectionIndex = 0;
   1.648 +    let findSelectionIndex = 0;
   1.649 +    let shouldDelete = {};
   1.650 +    let numberOfDeletedSelections = 0;
   1.651 +    let numberOfMatches = fSelection.rangeCount;
   1.652 +
   1.653 +    // We need to test if any ranges in the deleted selection (aSelection)
   1.654 +    // are in any of the ranges of the find selection
   1.655 +    // Usually both selections will only contain one range, however
   1.656 +    // either may contain more than one.
   1.657 +
   1.658 +    for (let fIndex = 0; fIndex < numberOfMatches; fIndex++) {
   1.659 +      shouldDelete[fIndex] = false;
   1.660 +      let fRange = fSelection.getRangeAt(fIndex);
   1.661 +
   1.662 +      for (let index = 0; index < aSelection.rangeCount; index++) {
   1.663 +        if (shouldDelete[fIndex])
   1.664 +          continue;
   1.665 +
   1.666 +        let selRange = aSelection.getRangeAt(index);
   1.667 +        let doesOverlap = this._checkOverlap(selRange, fRange);
   1.668 +        if (doesOverlap) {
   1.669 +          shouldDelete[fIndex] = true;
   1.670 +          numberOfDeletedSelections++;
   1.671 +        }
   1.672 +      }
   1.673 +    }
   1.674 +
   1.675 +    // OK, so now we know what matches (if any) are in the selection
   1.676 +    // that is being deleted. Time to remove them.
   1.677 +    if (numberOfDeletedSelections == 0)
   1.678 +      return;
   1.679 +
   1.680 +    for (let i = numberOfMatches - 1; i >= 0; i--) {
   1.681 +      if (shouldDelete[i])
   1.682 +        fSelection.removeRange(fSelection.getRangeAt(i));
   1.683 +    }
   1.684 +
   1.685 +    // Remove listeners if no more highlights left
   1.686 +    if (fSelection.rangeCount == 0)
   1.687 +      this._removeEditorListeners(editor);
   1.688 +  },
   1.689 +
   1.690 +  /*
   1.691 +   * nsIDocumentStateListener logic follows
   1.692 +   *
   1.693 +   * When attaching nsIEditActionListeners, there are no guarantees
   1.694 +   * as to whether the findbar or the documents in the browser will get
   1.695 +   * destructed first. This leads to the potential to either leak, or to
   1.696 +   * hold on to a reference an editable element's editor for too long,
   1.697 +   * preventing it from being destructed.
   1.698 +   *
   1.699 +   * However, when an editor's owning node is being destroyed, the editor
   1.700 +   * sends out a DocumentWillBeDestroyed notification. We can use this to
   1.701 +   * clean up our references to the object, to allow it to be destroyed in a
   1.702 +   * timely fashion.
   1.703 +   */
   1.704 +
   1.705 +  /*
   1.706 +   * Unhook ourselves when one of our state listeners has been called.
   1.707 +   * This can happen in 4 cases:
   1.708 +   *  1) The document the editor belongs to is navigated away from, and
   1.709 +   *     the document is not being cached
   1.710 +   *
   1.711 +   *  2) The document the editor belongs to is expired from the cache
   1.712 +   *
   1.713 +   *  3) The tab containing the owning document is closed
   1.714 +   *
   1.715 +   *  4) The <input> or <textarea> that owns the editor is explicitly
   1.716 +   *     removed from the DOM
   1.717 +   *
   1.718 +   * @param the listener that was invoked
   1.719 +   */
   1.720 +  _onEditorDestruction: function (aListener) {
   1.721 +    // First find the index of the editor the given listener listens to.
   1.722 +    // The listeners and editors arrays must always be in sync.
   1.723 +    // The listener will be in our array of cached listeners, as this
   1.724 +    // method could not have been called otherwise.
   1.725 +    let idx = 0;
   1.726 +    while (this._stateListeners[idx] != aListener)
   1.727 +      idx++;
   1.728 +
   1.729 +    // Unhook both listeners
   1.730 +    this._unhookListenersAtIndex(idx);
   1.731 +  },
   1.732 +
   1.733 +  /*
   1.734 +   * Creates a unique document state listener for an editor.
   1.735 +   *
   1.736 +   * It is not possible to simply have the findbar implement the
   1.737 +   * listener interface itself, as it wouldn't have sufficient information
   1.738 +   * to work out which editor was being destroyed. Therefore, we create new
   1.739 +   * listeners on the fly, and cache them in sync with the editors they
   1.740 +   * listen to.
   1.741 +   */
   1.742 +  _createStateListener: function () {
   1.743 +    return {
   1.744 +      findbar: this,
   1.745 +
   1.746 +      QueryInterface: function(aIID) {
   1.747 +        if (aIID.equals(Ci.nsIDocumentStateListener) ||
   1.748 +            aIID.equals(Ci.nsISupports))
   1.749 +          return this;
   1.750 +
   1.751 +        throw Components.results.NS_ERROR_NO_INTERFACE;
   1.752 +      },
   1.753 +
   1.754 +      NotifyDocumentWillBeDestroyed: function() {
   1.755 +        this.findbar._onEditorDestruction(this);
   1.756 +      },
   1.757 +
   1.758 +      // Unimplemented
   1.759 +      notifyDocumentCreated: function() {},
   1.760 +      notifyDocumentStateChanged: function(aDirty) {}
   1.761 +    };
   1.762 +  },
   1.763 +
   1.764 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
   1.765 +                                         Ci.nsISupportsWeakReference])
   1.766 +};

mercurial