I'm building a little browser app using android webview and I've been using window.getSelection() in javascript to get the nature of any text selected by the user and show a custom context menu based on the type of the selection i.e. whether it's a range, a carat, whether it's in a contenteditable etc.
This works fine unless the selection is in an iframe, then the browser security measures kick in and prevent me sniffing what has been selected using window.getSelection(). How can I workaround this?
Ideally I need a way to get better information about what was selected from the webview or if that's not possible I need a way to sniff whether the selection occurred in an iframe so I can disable my custom context menu logic and fallback to the default android context menu.
UPDATE/FURTHER CLARIFICATION 07/05/2019:
Seems I wasn't clear enough in my initial description...
My goal is to have a visually and functionally custom menu when selecting content in the webview that can cut/copy/paste as the standard context menu does in any part of the page/iframes etc. e.g.
I realised my original approach using javascript to detect the type of selection and to perform the cut/copy/paste was wrong because it will be blocked by cross origin security in iframes.
What I need is a native android/webview based approach. I've discovered that I can sniff the type of selection in the webview by looking at the items in mode.getMenu() on onActionModeStarted. This will allow me to show the correct buttons in my custom menu UI but I have been unable to manually trigger the same logic that gets called when cut/copy/paste is clicked. I thought I found the solution with webView.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CUT, null); but this doesn't work for some reason so I guess my question really is how can I manually trigger cut/copy/paste on the selected text from webview without using javascript? or any other approach that will allow me to have a custom selection menu with lots of options based on what was selected without hitting the browser security limitations?
Okay I figured out how roughly how to do this.
Step 1) In your activity, override onActionModeStarted and check the menu items available in the default context menu. This gives you a clue as to what the type of selection is and which buttons you will need to show in your custom menu. Also it gives you a reference to the item ID which you can use to later to trigger the action e.g.
systemSelectionMenu = mode.getMenu(); // keep a reference to the menu
MenuItem copyItem = systemSelectionMenu.getItem(0); // fetch any menu items you want
copyActionId = copyItem.getItemId(); // store reference to each item you want to manually trigger
Step 2) Instead of clearing the menu, use setVisible() to hide each menu item you want a custom button for e.g.
copyItem.setVisible(false);
Step 3) In your custom button onclick event you can trigger the copy action using:
myActivity.systemSelectionMenu.performIdentifierAction(myActivity.copyActionId, 0)
You can retrieve iframe's selection only if it has the same origin. Otherwise, you have no chances to track any iframe's events(clicks, touches, key presses, etc.).
const getSelectedText = (win, doc) => {
const isWindowSelectionAvailable = win && typeof win.getSelection != "undefined";
if (isWindowSelectionAvailable) {
return win.getSelection().toString();
}
const hasDocumentSelection = doc && typeof doc.selection != "undefined" && doc.selection.type == "Text";
if (hasDocumentSelection) {
return doc.selection.createRange().text;
}
return '';
}
const doIfTextSelected = (win, doc, cb) => () => {
const selectedText = getSelectedText(win, doc);
if (selectedText) {
cb(selectedText);
}
}
const setupSelectionListener = (win, doc, cb) => {
doc.onmouseup = doIfTextSelected(win, doc, cb);
doc.onkeyup = doIfTextSelected(win, doc, cb);
}
const getIframeWinAndDoc = (iframe) => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
const win = iframe.contentWindow || iframe.contentDocument.defaultView;
return { win, doc };
} catch (e) {
console.error(`${e}`);
return {};
}
}
const callback = console.log;
setupSelectionListener(window, document, callback);
document.querySelectorAll('iframe').forEach(iframe => {
const { win, doc } = getIframeWinAndDoc(iframe, console.log);
// Only for same origin iframes due to https://en.wikipedia.org/wiki/Same-origin_policy
if (win && doc) {
setupSelectionListener(win, doc, callback);
}
})
<h3>Select me</h3>
<div class="container">
<iframe src="https://teimurjan.github.io"></iframe>
</div>
This issue varying from browser to other if it works with internet explorer so it may fall with chrome
Try this
App.util.getSelectedText = function(frameId) {
var frame = Ext.getDom(frameId);
var frameWindow = frame.contentWindow;
var frameDocument = frameWindow.document;
if (frameDocument.getSelection) {
return frameDocument.getSelection();
}
else if (frameDocument.selection) {
return frameDocument.selection.createRange().text;
}
};
Hope it runs fine
Main problem is the window.getSelection() will return selection only for the main context/window. As iframe is the other window and other context, you should call getSelection() from iframe which is "current".
Related
I am using appium and webdriverIO to automate android native app. I need to perform scroll down and i have used
ele.scrollIntoView(true) //this returns not yet implemented error
is there any other way to scroll down?
I don't used java script to scroll down. but I have already given a detail answer with different approach (by some text, element and screen size). Please have a look on it.
How to reach the end of a scroll bar in appium?
Hope it will help.
I have find a way to perform swipe down by calling JsonWireProtocol api directly from my code.
https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
here is my code
module.exports.scrollAndroid = function (ele) {
console.log('driver.sessionID = ', driver.sessionId);
while (!$(ele).isDisplayed()) { . //$(ele)=$(//xpath or any other attribute)
this.scrollAPICall();
driver.pause(3000);
}
};
module.exports.scrollAPICall = function () {
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
console.log("result status >> ", this.status);
console.log("result text >> ", this.responseText);
};
url1 = `http://127.0.0.1:4723/wd/hub/session/${driver.sessionId}/touch/flick`;
xhttp.open('POST', url1);
console.log("URL >> ", url1)
xhttp.setRequestHeader("Content-Type", "application/json");
xhttp.setRequestHeader("charset", "UTF-8");
xhttp.send(JSON.stringify({
xspeed: 10,
yspeed: -100,
}));
if you want to scroll up then give yspeed as + value. ex:100
You can achieve a scroll down by performing a Screen Swipe Up action. Implementation of swipe class can be found from Boilerplate project. The Gestures.js class has all required functions. Here is the link to class
Please keep in mind that the swipe is performed based on the percentage. Once you implement this Gestures class you can then use it like below:
while(!element.isDisplayed()) {
Gestures.swipeUp(0.5)
}
In my flutter app I have html inside a webview and some javascript with jquery attached. I want to trigger different actions depending on if a list item receives 'tap' or 'taphold'. I'm using jquery-mobile 1.5.0-rc1.
My javascript is as follows:
$('div.board').on('taphold tap', 'div.thread, div.thread-special', function(event) {
console.log('event');
var $item = $(this); // The item that was clicked or held
var scope = $item.data('itemscope'); // No numbered IDs! You can find everything about the item
// etc.
var type = event.type;
if (type === 'tap') {
console.log('tap');
} else if (type === 'taphold') {
console.log('taphold');
}
});
When I tap, I can see a 'tap' in the console. When I taphold, nothing at all happens or appears on the console. Any idea why?
My question is exactly the same as this one but for Android and not iOS.
Get URL from remote URL in webview and open it in safari
Anyone have an idea. I am creating a cross-platform app and I have used the Clayton's answer to get it to work for iOS with some tweaks to open with a controller. But when trying different methods on Android and it is not working. This is as close as I have gotten (which is what Aaron provided on that same page) and it is not quite right as it opens the remote web page in a new browser window as well in the apps webview:
$.floorView.addEventListener('load', function(e) {
if (e.url.indexOf("http") !== -1) {
// stop the event
e.bubble = false;
// stop the url from loading
$.floorView.stopLoading();
// open
Ti.Platform.openURL(e.url);
}
});
Thanks!
I'd listen to the beforeload event, although I'm not 100% sure if you can actually prevent the Webview from still continuing the load as well.
Another way would be to intercept these links via JS you load or inject (evalJS()) in the webpage. Then fire a Ti.App event and respond to it in Titanium.
The Titanium.UI.Webview has a specific property for intercepting links: onlink.
This is not implemented as an event because it is a callback and needs to return a boolean to tell the Webview whether or not to load the URL of the link.
Oddly, setting the onlink callback right away makes the URL immediately load in Safari, so I did it this way:
$.webview.addEventListener('load', function(e) {
$.webview.onlink = function(e) {
Ti.Platform.openURL(e.url);
return false;
};
});
You can of course check the e.url string and decide whether to open it internally or externally.
I think I may have figured it out. Thanks to those whose ideas and suggestions lead to this code.
It appears to be working as I want on iOS and Android. Any suggestions or issues that you guys have I would appreciate the feedback. Thanks!
if ("iOS") {
$.webView.addEventListener('beforeload', function(e) {
if (e.navigationType == Titanium.UI.iOS.WEBVIEW_NAVIGATIONTYPE_LINK_CLICKED) {
// stop the event
e.bubble = false;
// stop the url from loading
$.webView.stopLoading();
//opens up the clicked URL for bill in new webView
var link = e.url;
var args = {url: link,};
// open link in my default webView for iOS
var newWebView=Alloy.createController('defaultWebView', args).getView();
newWebView.open();
}
});
}
else if ("Android") {
$.webView.addEventListener('beforeload', function(e) {
if (e.url.indexOf("http") !== -1) {
function Parser(text) {
var html = text;
var urlRegex = /((http|https):\/\/(\w+:{0,1}\w*#)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%#!\-\/]))?)/gi;
this.getHTML = function() {
return html;
};
} // end Parser
var parser = new Parser(e.url);
html = parser.getHTML();
if (html != "url of $.webView") {
// stop it from loding in current webView
$.webView.stopLoading();
// open link in browser
Ti.Platform.openURL(html);
}
}
});
}
else {
.....................
}
I have Android WebView which displays some links as: Link1TextLink2Text Now I would like to retrieve Link1Text and Link2Text when I long press these links. I have contextMenu implemented in the code and I could successfully get the link urls (http://link1.html, http://link2.html) using HitTestResult getExtra() method but how ccan I get those link texts ?FYI, I require those link texts for implementing "Copy link text" option in the contextMenu.
To get the text of an achor link:
I. Hook a touchstart listener to every web pages in the onPageFinished() callback of WebViewClient via evaluateJavascript. like:
//Javascripts to evaluate in onPageFinished
const w=window;
w.addEventListener('touchstart',wrappedOnDownFunc);
function wrappedOnDownFunc(e){
if(e.touches.length==1){
w._touchtarget = e.touches[0].target;
}
console.log('hey touched something ' +w._touchtarget);
}
note we've saved the touch target.
II. Then implement OnLongClicklisenter for webview. use evaluateJavascript again when you long pressed on a link object:
#Override
public boolean onLongClick(View v) {
WebView.HitTestResult result = ((WebView)v).getHitTestResult();
if (null == result) return false;
int type = result.getType();
switch (type) {
case WebView.HitTestResult.SRC_ANCHOR_TYPE:
if(result.getExtra()!=null){
((WebView)v).evaluateJavascript("window._touchtarget?window._touchtarget.innerText:''", new ValueCallback<String>() {
#Override
public void onReceiveValue(String value) {
System.out.println("hey received link text : "+value);
}
});
}
return true;
}
return false;
}
What's more, we can even choose to select the text of the anchor element! Actually this is one of the options that samsung browser offers when you long-pressed an tag .
To achieve this, we still need that recorded touch target. Besides we need 2 new javascript methods:
function selectTouchtarget(){
var tt = w._touchtarget;
if(tt){
w._touchtarget_href = tt.getAttribute("href");
tt.removeAttribute("href");
var sel = w.getSelection();
var range = document.createRange();
range.selectNodeContents(tt);
sel.removeAllRanges();
sel.addRange(range);
}
}
function restoreTouchtarget(){
var tt = w._touchtarget;
if(tt){
tt.setAttribute("href", w._touchtarget_href);
}
}
Finnaly in the onLongClick listener, instead of just fetch the innerText, we programmatically set the selection, trigger the action menu bar, and restore the removed href attribute of our touch target.
case WebViewmy.HitTestResult.SRC_ANCHOR_TYPE:
if(result.getExtra()!=null){
WebViewmy mWebView = ((WebViewmy)v);
mWebView.evaluateJavascript("selectTouchtarget()", new ValueCallback<String>() {
#Override
public void onReceiveValue(String value) {
/* bring in action mode by a fake click on the programmatically selected text. */
MotionEvent te = MotionEvent.obtain(0,0,KeyEvent.ACTION_DOWN,mWebView.lastX,mWebView.lastY,0);
mWebView.dispatchTouchEvent(te);
te.setAction(KeyEvent.ACTION_UP);
mWebView.dispatchTouchEvent(te);
te.recycle();
//if it's not delayed for a while or the href attribute is not removed, then the above code would click into
// the anchor element instead of select it's text.
/* restore href attribute */
mWebView.postDelayed(() -> mWebView.evaluateJavascript("restoreTouchtarget()", null), 100);
}
});
}
return true;
In my case, I've extended the WebView as WebViewmy to record last touched positions, lastX and lastY, in the onTouchEvent method.
Unfortunately, a clear, official way to do this is not available. Although, there are two APIs (selectText and copySelection) which are pending API council approval, that may help to do this, but they are not available at the moment.
I'm working on making a browser as a hybrid app using worklight framework for Android. I implemented my address bar as an input element which received the user input and pass the arguments to the webview to load the page.
However, I cannot figure out how to do the reverse: whenever the user click on a link in webview, I want the address bar to change to the new location.
Are you implementing a native page that is opened? If so, take a look at ChildBrowser, that basically does the same thing. It has a TextView being used as an address bar. You may decide to use it, or get the bits and pieces you want out of it. Regardless, I would image what you want to do something like this. By overriding the onLoadResource in the WebViewClient, you should be able to grab the url and change your TextBox.
In response to the comment below: inside your environment's main js file in the wlEnvInit() function:
function wlEnvInit(){
wlCommonInit();
// Environment initialization code goes here
document.onclick=manageLinks;
}
Then in this function get the url and set the text of your input element:
function manageLinks(event) {
var link = event.target;
//go up the family tree until we find the A tag
while (link && link.tagName != 'A') {
link = link.parentNode;
}
if (link) {
var url = link.href;
console.log("url = " + url);
//You can decide if you want to separate external or
//internal links, depending on your application
var linkIsExternal = ((url.indexOf('http://') == 0) || (url.indexOf('https://') == 0));
if (linkIsExternal) {
myInput.setText(url);
return false;
}
}
return true;
}
Inside of your WebView, inside the plugin, intercept the URL like this:
webview.setWebViewClient(new WebViewClient() {
#Override
public void onPageFinished(WebView view, String url) {
//use this area to set your input. Depending on how you
//implemented your plugin, you may need to return this value
//back to your main activity
Toast.makeText(cordova.getActivity(), "Loading: " + url, Toast.LENGTH_LONG).show();
}
});
Have you try to get the url from the href of and assign to the input variable and do the get/post? I know that it is possible in SDK i figure it dont will be harder in a framework. You can store the hiperlinks in a array with a parser or something similar.
example pseudocode:
When_hiperlink_clicked: //could be like a listener (search about it)
url = hiperlink.getURL("myHiperlink");
myinput.setText(url);
execute_input_bar_action();
Is difficult to figure out without code or something more, sorry.