// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. this.EXPORTED_SYMBOLS = ["Finder"]; const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const Services = Cu.import("resource://gre/modules/Services.jsm").Services; function Finder(docShell) { this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind); this._fastFind.init(docShell); this._docShell = docShell; this._listeners = []; this._previousLink = null; this._searchString = null; docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress) .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); } Finder.prototype = { addResultListener: function (aListener) { if (this._listeners.indexOf(aListener) === -1) this._listeners.push(aListener); }, removeResultListener: function (aListener) { this._listeners = this._listeners.filter(l => l != aListener); }, _notify: function (aSearchString, aResult, aFindBackwards, aDrawOutline) { this._searchString = aSearchString; this._outlineLink(aDrawOutline); let foundLink = this._fastFind.foundLink; let linkURL = null; if (foundLink) { let docCharset = null; let ownerDoc = foundLink.ownerDocument; if (ownerDoc) docCharset = ownerDoc.characterSet; if (!this._textToSubURIService) { this._textToSubURIService = Cc["@mozilla.org/intl/texttosuburi;1"] .getService(Ci.nsITextToSubURI); } linkURL = this._textToSubURIService.unEscapeURIForUI(docCharset, foundLink.href); } for (let l of this._listeners) { l.onFindResult(aResult, aFindBackwards, linkURL); } }, get searchString() { return this._searchString; }, set caseSensitive(aSensitive) { this._fastFind.caseSensitive = aSensitive; }, /** * Used for normal search operations, highlights the first match. * * @param aSearchString String to search for. * @param aLinksOnly Only consider nodes that are links for the search. * @param aDrawOutline Puts an outline around matched links. */ fastFind: function (aSearchString, aLinksOnly, aDrawOutline) { let result = this._fastFind.find(aSearchString, aLinksOnly); let searchString = this._fastFind.searchString; this._notify(searchString, result, false, aDrawOutline); }, /** * Repeat the previous search. Should only be called after a previous * call to Finder.fastFind. * * @param aFindBackwards Controls the search direction: * true: before current match, false: after current match. * @param aLinksOnly Only consider nodes that are links for the search. * @param aDrawOutline Puts an outline around matched links. */ findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) { let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly); let searchString = this._fastFind.searchString; this._notify(searchString, result, aFindBackwards, aDrawOutline); }, highlight: function (aHighlight, aWord) { let found = this._highlight(aHighlight, aWord, null); if (aHighlight) { let result = found ? Ci.nsITypeAheadFind.FIND_FOUND : Ci.nsITypeAheadFind.FIND_NOTFOUND; this._notify(aWord, result, false, false); } }, enableSelection: function() { this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON); }, removeSelection: function() { let fastFind = this._fastFind; fastFind.collapseSelection(); this.enableSelection(); this._restoreOriginalOutline(); }, focusContent: function() { // Allow Finder listeners to cancel focusing the content. for (let l of this._listeners) { if (!l.shouldFocusContent()) return; } let fastFind = this._fastFind; const fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); try { // Try to find the best possible match that should receive focus and // block scrolling on focus since find already scrolls. Further // scrolling is due to user action, so don't override this. if (fastFind.foundLink) { fm.setFocus(fastFind.foundLink, fm.FLAG_NOSCROLL); } else if (fastFind.foundEditable) { fm.setFocus(fastFind.foundEditable, fm.FLAG_NOSCROLL); fastFind.collapseSelection(); } else { this._getWindow().focus() } } catch (e) {} }, keyPress: function (aEvent) { let controller = this._getSelectionController(this._getWindow()); switch (aEvent.keyCode) { case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: if (this._fastFind.foundLink) { let view = this._fastFind.foundLink.ownerDocument.defaultView; this._fastFind.foundLink.dispatchEvent(new view.MouseEvent("click", { view: view, cancelable: true, bubbles: true, ctrlKey: aEvent.ctrlKey, altKey: aEvent.altKey, shiftKey: aEvent.shiftKey, metaKey: aEvent.metaKey })); } break; case Ci.nsIDOMKeyEvent.DOM_VK_TAB: let direction = Services.focus.MOVEFOCUS_FORWARD; if (aEvent.shiftKey) { direction = Services.focus.MOVEFOCUS_BACKWARD; } Services.focus.moveFocus(this._getWindow(), null, direction, 0); break; case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: controller.scrollPage(false); break; case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: controller.scrollPage(true); break; case Ci.nsIDOMKeyEvent.DOM_VK_UP: controller.scrollLine(false); break; case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: controller.scrollLine(true); break; } }, _getWindow: function () { return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); }, _outlineLink: function (aDrawOutline) { let foundLink = this._fastFind.foundLink; // Optimization: We are drawing outlines and we matched // the same link before, so don't duplicate work. if (foundLink == this._previousLink && aDrawOutline) return; this._restoreOriginalOutline(); if (foundLink && aDrawOutline) { // Backup original outline this._tmpOutline = foundLink.style.outline; this._tmpOutlineOffset = foundLink.style.outlineOffset; // Draw pseudo focus rect // XXX Should we change the following style for FAYT pseudo focus? // XXX Shouldn't we change default design if outline is visible // already? // Don't set the outline-color, we should always use initial value. foundLink.style.outline = "1px dotted"; foundLink.style.outlineOffset = "0"; this._previousLink = foundLink; } }, _restoreOriginalOutline: function () { // Removes the outline around the last found link. if (this._previousLink) { this._previousLink.style.outline = this._tmpOutline; this._previousLink.style.outlineOffset = this._tmpOutlineOffset; this._previousLink = null; } }, _highlight: function (aHighlight, aWord, aWindow) { let win = aWindow || this._getWindow(); let found = false; for (let i = 0; win.frames && i < win.frames.length; i++) { if (this._highlight(aHighlight, aWord, win.frames[i])) found = true; } let controller = this._getSelectionController(win); let doc = win.document; if (!controller || !doc || !doc.documentElement) { // Without the selection controller, // we are unable to (un)highlight any matches return found; } let body = (doc instanceof Ci.nsIDOMHTMLDocument && doc.body) ? doc.body : doc.documentElement; if (aHighlight) { let searchRange = doc.createRange(); searchRange.selectNodeContents(body); let startPt = searchRange.cloneRange(); startPt.collapse(true); let endPt = searchRange.cloneRange(); endPt.collapse(false); let retRange = null; let finder = Cc["@mozilla.org/embedcomp/rangefind;1"] .createInstance() .QueryInterface(Ci.nsIFind); finder.caseSensitive = this._fastFind.caseSensitive; while ((retRange = finder.Find(aWord, searchRange, startPt, endPt))) { this._highlightRange(retRange, controller); startPt = retRange.cloneRange(); startPt.collapse(false); found = true; } } else { // First, attempt to remove highlighting from main document let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); sel.removeAllRanges(); // Next, check our editor cache, for editors belonging to this // document if (this._editors) { for (let x = this._editors.length - 1; x >= 0; --x) { if (this._editors[x].document == doc) { sel = this._editors[x].selectionController .getSelection(Ci.nsISelectionController.SELECTION_FIND); sel.removeAllRanges(); // We don't need to listen to this editor any more this._unhookListenersAtIndex(x); } } } // Removing the highlighting always succeeds, so return true. found = true; } return found; }, _highlightRange: function(aRange, aController) { let node = aRange.startContainer; let controller = aController; let editableNode = this._getEditableNode(node); if (editableNode) controller = editableNode.editor.selectionController; let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); findSelection.addRange(aRange); if (editableNode) { // Highlighting added, so cache this editor, and hook up listeners // to ensure we deal properly with edits within the highlighting if (!this._editors) { this._editors = []; this._stateListeners = []; } let existingIndex = this._editors.indexOf(editableNode.editor); if (existingIndex == -1) { let x = this._editors.length; this._editors[x] = editableNode.editor; this._stateListeners[x] = this._createStateListener(); this._editors[x].addEditActionListener(this); this._editors[x].addDocumentStateListener(this._stateListeners[x]); } } }, _getSelectionController: function(aWindow) { // display: none iframes don't have a selection controller, see bug 493658 if (!aWindow.innerWidth || !aWindow.innerHeight) return null; // Yuck. See bug 138068. let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell); let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) .QueryInterface(Ci.nsISelectionController); return controller; }, /* * For a given node, walk up it's parent chain, to try and find an * editable node. * * @param aNode the node we want to check * @returns the first node in the parent chain that is editable, * null if there is no such node */ _getEditableNode: function (aNode) { while (aNode) { if (aNode instanceof Ci.nsIDOMNSEditableElement) return aNode.editor ? aNode : null; aNode = aNode.parentNode; } return null; }, /* * Helper method to unhook listeners, remove cached editors * and keep the relevant arrays in sync * * @param aIndex the index into the array of editors/state listeners * we wish to remove */ _unhookListenersAtIndex: function (aIndex) { this._editors[aIndex].removeEditActionListener(this); this._editors[aIndex] .removeDocumentStateListener(this._stateListeners[aIndex]); this._editors.splice(aIndex, 1); this._stateListeners.splice(aIndex, 1); if (!this._editors.length) { delete this._editors; delete this._stateListeners; } }, /* * Remove ourselves as an nsIEditActionListener and * nsIDocumentStateListener from a given cached editor * * @param aEditor the editor we no longer wish to listen to */ _removeEditorListeners: function (aEditor) { // aEditor is an editor that we listen to, so therefore must be // cached. Find the index of this editor let idx = this._editors.indexOf(aEditor); if (idx == -1) return; // Now unhook ourselves, and remove our cached copy this._unhookListenersAtIndex(idx); }, /* * nsIEditActionListener logic follows * * We implement this interface to allow us to catch the case where * the findbar found a match in a HTML or