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 +};