diff -r 000000000000 -r 6474c204b198 toolkit/modules/Finder.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/modules/Finder.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,763 @@ +// 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"); +Cu.import("resource://gre/modules/Geometry.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService", + "@mozilla.org/intl/texttosuburi;1", + "nsITextToSubURI"); +XPCOMUtils.defineLazyServiceGetter(this, "Clipboard", + "@mozilla.org/widget/clipboard;1", + "nsIClipboard"); +XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); + +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, aStoreResult = true) { + if (aStoreResult) { + this._searchString = aSearchString; + this.clipboardSearchString = 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; + + linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href); + } + + let data = { + result: aResult, + findBackwards: aFindBackwards, + linkURL: linkURL, + rect: this._getResultRect(), + searchString: this._searchString, + storeResult: aStoreResult + }; + + for (let l of this._listeners) { + l.onFindResult(data); + } + }, + + get searchString() { + if (!this._searchString && this._fastFind.searchString) + this._searchString = this._fastFind.searchString; + return this._searchString; + }, + + get clipboardSearchString() { + let searchString = ""; + if (!Clipboard.supportsFindClipboard()) + return searchString; + + try { + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(this._getWindow() + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext)); + trans.addDataFlavor("text/unicode"); + + Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard); + + let data = {}; + let dataLen = {}; + trans.getTransferData("text/unicode", data, dataLen); + if (data.value) { + data = data.value.QueryInterface(Ci.nsISupportsString); + searchString = data.toString(); + } + } catch (ex) {} + + return searchString; + }, + + set clipboardSearchString(aSearchString) { + if (!aSearchString || !Clipboard.supportsFindClipboard()) + return; + + ClipboardHelper.copyStringToClipboard(aSearchString, + Ci.nsIClipboard.kFindClipboard, + this._getWindow().document); + }, + + 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); + }, + + /** + * Forcibly set the search string of the find clipboard to the currently + * selected text in the window, on supported platforms (i.e. OSX). + */ + setSearchStringToSelection: function() { + // Find the selected text. + let selection = this._getWindow().getSelection(); + // Don't go for empty selections. + if (!selection.rangeCount) + return null; + let searchString = (selection.toString() || "").trim(); + // Empty strings are rather useless to search for. + if (!searchString.length) + return null; + + this.clipboardSearchString = searchString; + return searchString; + }, + + 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, false); + } + }, + + enableSelection: function() { + this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON); + this._restoreOriginalOutline(); + }, + + removeSelection: function() { + this._fastFind.collapseSelection(); + this.enableSelection(); + }, + + 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); + }, + + /** + * Get the bounding selection rect in CSS px relative to the origin of the + * top-level content document. + */ + _getResultRect: function () { + let topWin = this._getWindow(); + let win = this._fastFind.currentWindow; + if (!win) + return null; + + let selection = win.getSelection(); + if (!selection.rangeCount || selection.isCollapsed) { + // The selection can be into an input or a textarea element. + let nodes = win.document.querySelectorAll("input, textarea"); + for (let node of nodes) { + if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) { + let sc = node.editor.selectionController; + selection = sc.getSelection(Ci.nsISelectionController.SELECTION_NORMAL); + if (selection.rangeCount && !selection.isCollapsed) { + break; + } + } + } + } + + let utils = topWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let scrollX = {}, scrollY = {}; + utils.getScrollXY(false, scrollX, scrollY); + + for (let frame = win; frame != topWin; frame = frame.parent) { + let rect = frame.frameElement.getBoundingClientRect(); + let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; + let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; + scrollX.value += rect.left + parseInt(left, 10); + scrollY.value += rect.top + parseInt(top, 10); + } + let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect()); + return rect.translate(scrollX.value, scrollY.value); + }, + + _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