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