public
Last active

Prevent links in standalone web apps opening Mobile Safari

  • Download Gist
example.html
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!DOCTYPE html>
<head>
<title>Stay Standalone</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<script src="stay_standalone.js" type="text/javascript"></script>
</head>
<body>
<ul>
<li><a href="http://google.com/">Remote Link (Google)</a></li>
<li><a href="javascript:alert('Awesome script is awesome')">JavaScript Link</a></li>
<li><a href="/">Local Link</a></li>
<li><a href="#amp">Local Anchor</a></li>
</ul>
</body>
stay_standalone.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Mobile Safari in standalone mode
if(("standalone" in window.navigator) && window.navigator.standalone){
 
// If you want to prevent remote links in standalone web apps opening Mobile Safari, change 'remotes' to true
var noddy, remotes = false;
document.addEventListener('click', function(event) {
noddy = event.target;
// Bubble up until we hit link or top HTML element. Warning: BODY element is not compulsory so better to stop on HTML
while(noddy.nodeName !== "A" && noddy.nodeName !== "HTML") {
noddy = noddy.parentNode;
}
if('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes))
{
event.preventDefault();
document.location.href = noddy.href;
}
},false);
}

The variable links will be created in the global scope here. How about this?

// Mobile Safari in standalone mode
if (('standalone' in window.navigator) && window.navigator.standalone) {

    window.addEventListener('load', function() {

        var links = document.links,
            link,
            i;

        for (i = 0; i < links.length; i++) {
            // Don't do this for javascript: links
            if (~(link = links[i]).href.toLowerCase().indexOf('javascript')) {
                link.addEventListener('click', function(event) {
                    top.location.href = this.href;
                    event.returnValue = false;
                }, false);
            }
        }

    }, false);

}

Still, for documents with a lot of links in them it would probably be better to use event delegation.

And you should add this CSS property on body to disable the callout shown when you touch and hold a touch target :
```-webkit-touch-callout: none;

And what about document.links?

I've forked the gist and did a complete rewrite with event delegation and a minimal test case html.

Waiting for the loadevent is a bad idea. On 3g networks it could mean your code will not run before the user taps the screen. I've tryied to pull request, but gists can't do that...

https://gist.github.com/1042167

<!DOCTYPE html>
<html>
<head>
    <title>stay standalone</title>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="viewport" content="width=device-width,initial-scale=1.5,user-scalable=no">
    <script type="text/javascript">
        (function(document,navigator,standalone) {
            // prevents links from apps from oppening in mobile safari
            // this javascript must be the first script in your <head>
            if ((standalone in navigator) && navigator[standalone]) {
                var curnode, location=document.location, stop=/^(a|html)$/i;
                document.addEventListener('click', function(e) {
                    curnode=e.target;
                    while (!(stop).test(curnode.nodeName)) {
                        curnode=curnode.parentNode;
                    }
                    // Condidions to do this only on links to your own app
                    // if you want all links, use if('href' in curnode) instead.
                    if('href' in curnode && ( curnode.href.indexOf('http') || ~curnode.href.indexOf(location.host) ) ) {
                        e.preventDefault();
                        location.href = curnode.href;
                    }
                },false);
            }
        })(document,window.navigator,'standalone');
    </script>
</head>
<body>
    <p><a href="http://google.com/">google</a></p>
    <script type="text/javascript" charset="utf-8">
        // NEVER user document.write, unless for test porposes.
        document.write('<p><a href="http://'+document.location.host+'/test/">Same domain</a></p>')
    </script>
    <p><a href="/test/"><span>absolute path</span></a></p>
    <p><a href="test/">relative path</a></p>
    <p><a href="/test?http://othersite.com">http not on beginning</a></p>
    <p><a href="#test">anchor</a></p>
</body>
</html>

Yeah I really dig this event delegation approach instead. Nice work irae

Original script is quick and dirty solution for a site with max of three links and louisremi and irae make good comments regarding performance. Will update with irae's event bubbling, m(_ _)m

Nice catch! It is indeed better to use html instead of body. I am so used to always declaring it that I forgot.
Also, the href check will indeed be faster with indexOf insted of regexp. I changed my version accordingly.

But I'll keep my script inline and my other regexp for the flowing reasons:

  • The <script> is inline on purpose. I feel this is essential functionality and the houndtrip too costly. Have you notice what happens to Facebook's WebApp on bad 3G or EDGE coverage?
  • The stop regexp is compiled once at load and is tested so little. test is the fastest RegExp method by far, since it does not check for backreferences. If you have a very messy DOM structure you would be talking of 10 checks against on precompiled regexp.
  • By not over optimizing and keeping the regexp I gain the little advantage of byte size and case insensitive comparison. (you never know when some other “maybe buggy” OS or browser will support “standalone mode”)

After minification, @irae’s script is only 345 bytes – so I definitely agreed that this script should be inlined unless there already is another script file it can be merged with. Some thoughts on inline vs. external files and the file size threshold: http://mathiasbynens.be/notes/inline-vs-separate-file

Adjusted my script, 306 bytes after minification \o/ https://gist.github.com/1042167

Any thought on how to handle a download when running this script? I have a site that has PDF downloads however when you click to download the iPad does not give you the option to do anything with the PDF, as in, it doesnt give me the pop up for (Open in Good Reader or iBooks) and without the navigation you can't return to the original html page.

P.S. I can't inline code to the download because I actually built my site in joomla.

Thoughts?? Need help on this one.

Adding an indexOf('.pdf') !== -1 test on href value should do the trick.

On 21/08/2011, at 18:42, nyartboy wrote:

Any thought on how to handle a download when running this script? I have a site that has PDF downloads however when you click to download the iPad does not give you the option to do anything with the PDF, as in, it doesnt give me the pop up for (Open in Good Reader or iBooks) and without the navigation you can't return to the original html page.

P.S. I can't inline code to the download because I actually built my site in joomla.

Thoughts?? Need help on this one.

Reply to this email directly or view it on GitHub:
https://gist.github.com/1042026

I'm kind of a newbie at this. Can you show me how and where to put that into the script? Sorry for the high maintenance... still learning.

Replace:

if('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes))

with:

if('href' in noddy && noddy.href.indexOf('http') !== -1 && noddy.href.indexOf('.pdf') == -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes))

Better to use regex but this will work if your PDFs use lowercase .pdf extension.

I got a problem here.

The standalone mode is working perfectly when I click on links to other sides.
But in my case I got some lightbox effects for pictures and youtube videos. So when I click on the pictures it opens with the lightbox effect when I am not in the fullscreen/standalone mode. But as soon as I am in the standalone mode it opens up the picture without the lightbox effect. Is there any way around?

Thanks for your help

Yes, there is a way around that. Your lightbox script must be using the href to grab the picture or video url. In this case, both scripts fire, and since we set document.location here, it prevails.

To fix that, your lightbox script must call event.stopPropagation(); on the click handler. This way the stay_standalone.js script is never called for elements affected by the lightbox.

Thanks irae for the quick response.
I am using fancybox for the lightbox effects. But I am not sure if I understood you right.

I added event.stopPropagation(); into the script of fancybox. The only effect is then that the lightbox effect won't occur at all too. Could you tell me where I have to add this?

@stormiii, since you are not comfortable in finding where to place the stopPropagation(), it's better not to change the original script, this way you won't have problems updating fancybox in the future. There is a better approach in this case: you can bind a new click handler to stop the propagation. You should place it just before the fancybox initialization. For example:

var fancylinks = $("a.grouped_elements");
fancylinks.click(function(event){
    event.stopPropagation();
});
fancylinks.fancybox();

onclick="if(event && event.stopPropagation) event.stopPropagation();"

This one helped me, even I dont feel that comfy with it. But thanks again for your help

All of these methods, including @irae's fork, were failing for me because of some async Facebook plugin code. I ended up with this: https://gist.github.com/3908053

It works just fine for me.

Can anyone help me? I'm using this script for my mobile app. It is meant to be used as a standalone app on iPad, after users save the icon to their home screen. I'm having two issues with links, using this script.

  1. Using Twitter web intents to show mini user profiles, when somebody clicks on a twitter handle link, it goes within the standalone app to the Twitter profile. Mobile Safari doesn't open. But then there's no back button or any way to get back to the main app functionality.

  2. The app uses Facebook authentication through Javascript, which works perfectly on the web. When I click a normal link to an outside site with target="_blank", mobile Safari opens, which I would expect. But when I come back to the standalone app from Safari, I'm no longer signed in. It's as if the browser closed, and I have to click the sign in button again.

Can anybody point me in the right direction?

Thanks!
Justin

Anyone have any ideas on how does a user navigate back after opening a remote link. Seems like the user gets jailed at that point.

Not clear to me what this script does. Is it only for packaged apps using webkit - i.e. a phonegap HTML5 app? Is the idea that all links remain inside the app instead of opening Safari on the iPhone?

All of these scripts are freezing my iphone and I have to hard reboot. Any ideas?

I think it was because I didn't have type="text/javascript" in the script tag. Now it works! Thanks!

Edit... Noep, it still freezes at times. Strange. Maybe it has nothing to to with the script. Dunno.

Thanks man, you saved my day :)

think it would be a good idea to add this event.defaultPrevented !== true on the following line:

if('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes))

So it will end up something like:

if('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes) && event.defaultPrevented !== true)

The reason why I'm proposing it is because if I have attached some other behavior to the same link and I prevent default on it, it won't work, because this tiny script will take over and redirect.

for the revised script, rolandjitsu's proposal is

  if('href' in curnode && ( curnode.href.indexOf('http') || ~curnode.href.indexOf(location.host) ) && e.defaultPrevented !== true) {

I have a link with onclick to confirm the action, with OK and CANCEL option. On any desktop browser (Chrome, Safari...) it works properly, but on iOS WebApp (using that script) the CANCEL button do the same function as OK.

Example Link:

 <a onclick="return confirm('Really want to proceed?')" href="/reservation/cancel/1">Cancel</a>

Any idea to solve it?

Thanks for this gist. I ran into the same problem as lucianocn and I think I have solved it by checking for event.preventDefaulted inside the event handler:

if (noddy.nodeName === 'A' && !event.defaultPrevented) {
    if ('href' in noddy && noddy.href.indexOf('http') !== -1 && (noddy.href.indexOf(document.location.host) !== -1 || remotes)) {
        document.location.href = noddy.href;
        event.preventDefault();
    }
}

I think this might be an important addition to prevent conflicts with existing event handlers. Please consider adding it to the gist.

Edit: just found out that rolandjitsu suggested a similar thing a while back.

Another suggestion. Instead of checking for the host and remotes flag, I have modified the code to simply look at the target attribute like this:

if (noddy.nodeName === 'A' && !event.defaultPrevented) {
    if ('href' in noddy && noddy.target !== '_blank') {
        document.location.href = noddy.href;
        event.preventDefault();
    }
}

This allows the app to control which links exactly should be opened in a browser window.

awesome work dude

how about user login using ajax?. using location.href as a baseURL

This is what i was looking for, but has anyone had any success in iOS 7.1?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.