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