diff --git a/dom/browser-element/BrowserElementChildPreload.js b/dom/browser-element/BrowserElementChildPreload.js index 1b25df49b126..bec221857c5d 100644 --- a/dom/browser-element/BrowserElementChildPreload.js +++ b/dom/browser-element/BrowserElementChildPreload.js @@ -229,7 +229,10 @@ BrowserElementChild.prototype = { "activate-next-paint-listener": this._activateNextPaintListener.bind(this), "set-input-method-active": this._recvSetInputMethodActive.bind(this), "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this), - "do-command": this._recvDoCommand + "do-command": this._recvDoCommand, + "find-all": this._recvFindAll.bind(this), + "find-next": this._recvFindNext.bind(this), + "clear-match": this._recvClearMatch.bind(this), } addMessageListener("browser-element-api:call", function(aMessage) { @@ -1240,6 +1243,57 @@ BrowserElementChild.prototype = { } }, + _initFinder: function() { + if (!this._finder) { + try { + this._findLimit = Services.prefs.getIntPref("accessibility.typeaheadfind.matchesCountLimit"); + } catch (e) { + // Pref not available, assume 0, no match counting. + this._findLimit = 0; + } + + let {Finder} = Components.utils.import("resource://gre/modules/Finder.jsm", {}); + this._finder = new Finder(docShell); + this._finder.addResultListener({ + onMatchesCountResult: (data) => { + sendAsyncMsg('findchange', { + active: true, + searchString: this._finder.searchString, + searchLimit: this._findLimit, + activeMatchOrdinal: data.current, + numberOfMatches: data.total + }); + } + }); + } + }, + + _recvFindAll: function(data) { + this._initFinder(); + let searchString = data.json.searchString; + this._finder.caseSensitive = data.json.caseSensitive; + this._finder.fastFind(searchString, false, false); + this._finder.requestMatchesCount(searchString, this._findLimit, false); + }, + + _recvFindNext: function(data) { + if (!this._finder) { + debug("findNext() called before findAll()"); + return; + } + this._finder.findAgain(data.json.backward, false, false); + this._finder.requestMatchesCount(this._finder.searchString, this._findLimit, false); + }, + + _recvClearMatch: function(data) { + if (!this._finder) { + debug("clearMach() called before findAll()"); + return; + } + this._finder.removeSelection(); + sendAsyncMsg('findchange', {active: false}); + }, + _recvSetInputMethodActive: function(data) { let msgData = { id: data.json.id }; if (!this._isContentWindowCreated) { diff --git a/dom/browser-element/BrowserElementParent.js b/dom/browser-element/BrowserElementParent.js index 9e80ad887a41..b84d531760b9 100644 --- a/dom/browser-element/BrowserElementParent.js +++ b/dom/browser-element/BrowserElementParent.js @@ -206,6 +206,7 @@ BrowserElementParent.prototype = { "selectionstatechanged": this._handleSelectionStateChanged, "scrollviewchange": this._handleScrollViewChange, "caretstatechanged": this._handleCaretStateChanged, + "findchange": this._handleFindChange }; let mmSecuritySensitiveCalls = { @@ -475,6 +476,12 @@ BrowserElementParent.prototype = { this._frameElement.dispatchEvent(evt); }, + _handleFindChange: function(data) { + let evt = this._createEvent("findchange", data.json, + /* cancelable = */ false); + this._frameElement.dispatchEvent(evt); + }, + _createEvent: function(evtName, detail, cancelable) { // This will have to change if we ever want to send a CustomEvent with null // detail. For now, it's OK. @@ -646,6 +653,23 @@ BrowserElementParent.prototype = { getCanGoForward: defineDOMRequestMethod('get-can-go-forward'), getContentDimensions: defineDOMRequestMethod('get-contentdimensions'), + findAll: defineNoReturnMethod(function(searchString, caseSensitivity) { + return this._sendAsyncMsg('find-all', { + searchString, + caseSensitive: caseSensitivity == Ci.nsIBrowserElementAPI.FIND_CASE_SENSITIVE + }); + }), + + findNext: defineNoReturnMethod(function(direction) { + return this._sendAsyncMsg('find-next', { + backward: direction == Ci.nsIBrowserElementAPI.FIND_BACKWARD + }); + }), + + clearMatch: defineNoReturnMethod(function() { + return this._sendAsyncMsg('clear-match'); + }), + goBack: defineNoReturnMethod(function() { this._sendAsyncMsg('go-back'); }), diff --git a/dom/browser-element/mochitest/browserElement_Find.js b/dom/browser-element/mochitest/browserElement_Find.js new file mode 100644 index 000000000000..2ab378ff1884 --- /dev/null +++ b/dom/browser-element/mochitest/browserElement_Find.js @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the public domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Bug 1163961 - Test search API + +"use strict"; + +SimpleTest.waitForExplicitFinish(); +browserElementTestHelpers.setEnabledPref(true); +browserElementTestHelpers.addPermission(); + +function runTest() { + + let iframe = document.createElement('iframe'); + iframe.setAttribute('mozbrowser', 'true'); + iframe.src = 'data:text/html,foo bar foo XXX Foo BAR foobar foobar'; + + const once = (eventName) => { + return new Promise((resolve) => { + iframe.addEventListener(eventName, function onEvent(...args) { + iframe.removeEventListener(eventName, onEvent); + resolve(...args); + }); + }); + } + + // Test if all key=>value pairs in o1 are present in o2. + const c = (o1, o2) => Object.keys(o1).every(k => o1[k] == o2[k]); + + let testCount = 0; + + once('mozbrowserloadend').then(() => { + iframe.findAll('foo', 'case-insensitive'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'foo', + searchLimit: 100, + activeMatchOrdinal: 1, + numberOfMatches: 5, + }), `test ${testCount++}`); + iframe.findNext('forward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'foo', + searchLimit: 100, + activeMatchOrdinal: 2, + numberOfMatches: 5, + }), `test ${testCount++}`); + iframe.findNext('backward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'foo', + searchLimit: 100, + activeMatchOrdinal: 1, + numberOfMatches: 5, + }), `test ${testCount++}`); + iframe.findAll('xxx', 'case-sensitive'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'xxx', + searchLimit: 100, + activeMatchOrdinal: 0, + numberOfMatches: 0, + }), `test ${testCount++}`); + iframe.findAll('bar', 'case-insensitive'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'bar', + searchLimit: 100, + activeMatchOrdinal: 1, + numberOfMatches: 4, + }), `test ${testCount++}`); + iframe.findNext('forward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'bar', + searchLimit: 100, + activeMatchOrdinal: 2, + numberOfMatches: 4, + }), `test ${testCount++}`); + iframe.findNext('forward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'bar', + searchLimit: 100, + activeMatchOrdinal: 3, + numberOfMatches: 4, + }), `test ${testCount++}`); + iframe.findNext('forward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'bar', + searchLimit: 100, + activeMatchOrdinal: 4, + numberOfMatches: 4, + }), `test ${testCount++}`); + iframe.findNext('forward'); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: true, + searchString: 'bar', + searchLimit: 100, + activeMatchOrdinal: 1, + numberOfMatches: 4, + }), `test ${testCount++}`); + iframe.clearMatch(); + return once('mozbrowserfindchange'); + }).then(({detail}) => { + ok(c(detail, { + msg_name: "findchange", + active: false + }), `test ${testCount++}`); + SimpleTest.finish(); + }); + + document.body.appendChild(iframe); + +} + +addEventListener('testready', runTest); diff --git a/dom/browser-element/mochitest/mochitest-oop.ini b/dom/browser-element/mochitest/mochitest-oop.ini index e0ef5136fc16..74d7cebf0ba8 100644 --- a/dom/browser-element/mochitest/mochitest-oop.ini +++ b/dom/browser-element/mochitest/mochitest-oop.ini @@ -7,6 +7,7 @@ skip-if = os == "android" || (toolkit == "cocoa" && debug) || buildapp == 'mulet support-files = browserElement_OpenMixedProcess.js file_browserElement_OpenMixedProcess.html + browserElement_Find.js browserElement_OpenTab.js [test_browserElement_oop_Viewmode.html] @@ -41,6 +42,7 @@ skip-if = (toolkit == 'gonk' && !debug) disabled = bug 1022281 [test_browserElement_oop_ErrorSecurity.html] skip-if = (toolkit == 'gonk' && !debug) +[test_browserElement_oop_Find.html] [test_browserElement_oop_FirstPaint.html] [test_browserElement_oop_ForwardName.html] [test_browserElement_oop_FrameWrongURI.html] diff --git a/dom/browser-element/mochitest/mochitest.ini b/dom/browser-element/mochitest/mochitest.ini index 157c752eeafd..049e8004e73c 100644 --- a/dom/browser-element/mochitest/mochitest.ini +++ b/dom/browser-element/mochitest/mochitest.ini @@ -29,6 +29,7 @@ support-files = browserElement_Download.js browserElement_ErrorSecurity.js browserElement_ExposableURI.js + browserElement_Find.js browserElement_FirstPaint.js browserElement_ForwardName.js browserElement_FrameWrongURI.js @@ -159,6 +160,7 @@ skip-if = os == "android" || toolkit == 'gonk' # embed-apps doesn't work in the [test_browserElement_inproc_Download.html] disabled = bug 1022281 [test_browserElement_inproc_ExposableURI.html] +[test_browserElement_inproc_Find.html] [test_browserElement_inproc_FirstPaint.html] [test_browserElement_inproc_ForwardName.html] [test_browserElement_inproc_FrameWrongURI.html] diff --git a/dom/browser-element/mochitest/test_browserElement_inproc_Find.html b/dom/browser-element/mochitest/test_browserElement_inproc_Find.html new file mode 100644 index 000000000000..e0333a6bb5db --- /dev/null +++ b/dom/browser-element/mochitest/test_browserElement_inproc_Find.html @@ -0,0 +1,19 @@ + + + + + Test for Bug 1163961 + + + + + +Mozilla Bug 1163961 + + + + + diff --git a/dom/browser-element/mochitest/test_browserElement_oop_Find.html b/dom/browser-element/mochitest/test_browserElement_oop_Find.html new file mode 100644 index 000000000000..e0333a6bb5db --- /dev/null +++ b/dom/browser-element/mochitest/test_browserElement_oop_Find.html @@ -0,0 +1,19 @@ + + + + + Test for Bug 1163961 + + + + + +Mozilla Bug 1163961 + + + + + diff --git a/dom/browser-element/nsIBrowserElementAPI.idl b/dom/browser-element/nsIBrowserElementAPI.idl index 91885e004fcf..95a9ae940ef0 100644 --- a/dom/browser-element/nsIBrowserElementAPI.idl +++ b/dom/browser-element/nsIBrowserElementAPI.idl @@ -26,9 +26,15 @@ interface nsIBrowserElementNextPaintListener : nsISupports * Interface to the BrowserElementParent implementation. All methods * but setFrameLoader throw when the remote process is dead. */ -[scriptable, uuid(3811446f-90bb-42c1-b2b6-aae3603b61e1)] +[scriptable, uuid(8ecb598c-f886-11e4-9915-778f934fbf93)] interface nsIBrowserElementAPI : nsISupports { + const long FIND_CASE_SENSITIVE = 0; + const long FIND_CASE_INSENSITIVE = 1; + + const long FIND_FORWARD = 0; + const long FIND_BACKWARD = 1; + void setFrameLoader(in nsIFrameLoader frameLoader); void setVisible(in boolean visible); @@ -67,6 +73,10 @@ interface nsIBrowserElementAPI : nsISupports nsIDOMDOMRequest getCanGoForward(); nsIDOMDOMRequest getContentDimensions(); + void findAll(in DOMString searchString, in long caseSensitivity); + void findNext(in long direction); + void clearMatch(); + void addNextPaintListener(in nsIBrowserElementNextPaintListener listener); void removeNextPaintListener(in nsIBrowserElementNextPaintListener listener); diff --git a/dom/html/nsBrowserElement.cpp b/dom/html/nsBrowserElement.cpp index 4aab917961ec..5a84096d80fc 100644 --- a/dom/html/nsBrowserElement.cpp +++ b/dom/html/nsBrowserElement.cpp @@ -376,6 +376,62 @@ nsBrowserElement::GetContentDimensions(ErrorResult& aRv) return req.forget().downcast(); } +void +nsBrowserElement::FindAll(const nsAString& aSearchString, + BrowserFindCaseSensitivity aCaseSensitivity, + ErrorResult& aRv) +{ + NS_ENSURE_TRUE_VOID(IsBrowserElementOrThrow(aRv)); + NS_ENSURE_TRUE_VOID(IsNotWidgetOrThrow(aRv)); + + uint32_t caseSensitivity; + if (aCaseSensitivity == BrowserFindCaseSensitivity::Case_insensitive) { + caseSensitivity = nsIBrowserElementAPI::FIND_CASE_INSENSITIVE; + } else { + caseSensitivity = nsIBrowserElementAPI::FIND_CASE_SENSITIVE; + } + + nsresult rv = mBrowserElementAPI->FindAll(aSearchString, caseSensitivity); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +void +nsBrowserElement::FindNext(BrowserFindDirection aDirection, + ErrorResult& aRv) +{ + NS_ENSURE_TRUE_VOID(IsBrowserElementOrThrow(aRv)); + NS_ENSURE_TRUE_VOID(IsNotWidgetOrThrow(aRv)); + + uint32_t direction; + if (aDirection == BrowserFindDirection::Backward) { + direction = nsIBrowserElementAPI::FIND_BACKWARD; + } else { + direction = nsIBrowserElementAPI::FIND_FORWARD; + } + + nsresult rv = mBrowserElementAPI->FindNext(direction); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +void +nsBrowserElement::ClearMatch(ErrorResult& aRv) +{ + NS_ENSURE_TRUE_VOID(IsBrowserElementOrThrow(aRv)); + NS_ENSURE_TRUE_VOID(IsNotWidgetOrThrow(aRv)); + + nsresult rv = mBrowserElementAPI->ClearMatch(); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + void nsBrowserElement::AddNextPaintListener(BrowserElementNextPaintEventCallback& aListener, ErrorResult& aRv) diff --git a/dom/html/nsBrowserElement.h b/dom/html/nsBrowserElement.h index 2270a7fe864d..d30c8b3c2193 100644 --- a/dom/html/nsBrowserElement.h +++ b/dom/html/nsBrowserElement.h @@ -20,6 +20,8 @@ namespace dom { struct BrowserElementDownloadOptions; class BrowserElementNextPaintEventCallback; class DOMRequest; +enum class BrowserFindCaseSensitivity: uint32_t; +enum class BrowserFindDirection: uint32_t; } // namespace dom class ErrorResult; @@ -80,6 +82,11 @@ public: already_AddRefed GetCanGoForward(ErrorResult& aRv); already_AddRefed GetContentDimensions(ErrorResult& aRv); + void FindAll(const nsAString& aSearchString, dom::BrowserFindCaseSensitivity aCaseSensitivity, + ErrorResult& aRv); + void FindNext(dom::BrowserFindDirection aDirection, ErrorResult& aRv); + void ClearMatch(ErrorResult& aRv); + void AddNextPaintListener(dom::BrowserElementNextPaintEventCallback& listener, ErrorResult& aRv); void RemoveNextPaintListener(dom::BrowserElementNextPaintEventCallback& listener, diff --git a/dom/webidl/BrowserElement.webidl b/dom/webidl/BrowserElement.webidl index 256637ed731d..0a569fac23ae 100644 --- a/dom/webidl/BrowserElement.webidl +++ b/dom/webidl/BrowserElement.webidl @@ -6,6 +6,9 @@ callback BrowserElementNextPaintEventCallback = void (); +enum BrowserFindCaseSensitivity { "case-sensitive", "case-insensitive" }; +enum BrowserFindDirection { "forward", "backward" }; + dictionary BrowserElementDownloadOptions { DOMString? filename; DOMString? referrer; @@ -146,4 +149,20 @@ interface BrowserElementPrivileged { Pref="dom.mozBrowserFramesEnabled", CheckPermissions="browser"] void setNFCFocus(boolean isFocus); + + [Throws, + Pref="dom.mozBrowserFramesEnabled", + CheckPermissions="browser"] + void findAll(DOMString searchString, BrowserFindCaseSensitivity caseSensitivity); + + [Throws, + Pref="dom.mozBrowserFramesEnabled", + CheckPermissions="browser"] + void findNext(BrowserFindDirection direction); + + [Throws, + Pref="dom.mozBrowserFramesEnabled", + CheckPermissions="browser"] + void clearMatch(); + }; diff --git a/toolkit/modules/Finder.jsm b/toolkit/modules/Finder.jsm index e1e96b525988..6d080054fd3f 100644 --- a/toolkit/modules/Finder.jsm +++ b/toolkit/modules/Finder.jsm @@ -1048,3 +1048,6 @@ function GetClipboardSearchString(aLoadContext) { return searchString; } + +this.Finder = Finder; +this.GetClipboardSearchString = GetClipboardSearchString;