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