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.

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

mercurial