I currently have an iframe within my main page that has a number of checkboxes that need to be actioned prior to leaving the iframe. i.e, if the user commences checking the checkboxes and half way through they then click back on the main page, i.e leaving the iframe, I would like to be able to trap/validate that they have left the iframe and prompt them with a message indicating this, say with a "Note: You will lose all data entered here - Leave: Yes/No?" type message.
Prompting a User to Save When Leaving a Page. This 4guys article sounds like what you need. It talks about the onbeforeunload event. There's some awesome posts here on stackoverflow about onbeforeunload too.
It appears that onbeforeunload indeed does not fire for an iframe. Bugger!
Here's some sample code that should work though. This will only work if you're in the same domain, otherwise same origin policy will prevent the iframe from talking back to the parent.
I also haven't tested these in many browsers so YMMV.
You've got two options here, depending on where you want to put the prompt for changes logic.
Option one involves the iframe telling the parent window when there's changes.
Parent window javascript:
window.onbeforeunload=closeIt;
var changes = false;
function closeIt()
{
if (changes)
{
return "Yo, changes, save 'em?";
}
}
function somethingChanged() {
changes=true;
};
Iframe javascript:
$(function() {
$('input').change(parent.somethingChanged);
});
Option two involves the iframe taking control over the parent window's onbeforeunload
Parent window javascript:
There is none :-)
Iframe javascript:
$(function() {
parent.window.onbeforeunload = myCloseIt;
$('input').change(somethingChanged);
});
var changes = false;
function myCloseIt()
{
if (changes)
{
return "Yo, changes, save 'em?";
}
}
function somethingChanged() {
changes=true;
};
In either option the naive changes variable could be beefed up a bit, probably using techniques from the 4guys article, to see if there's really been any changes.
If they're on different domains, but you're still in charge of "both sides" of the HTML, there's still some options, they're just harder.
xssinterface is a library that uses postMessage and location hashes and secret voodoo black magic to communicate cross site.
Related
I'm adding a chat feature to a couple of our websites. The chat will connect users with people at our help desk to help them use the websites. Our help desk folks want the chat window to appear like a tab on the side of the page and slide out, rather than popping up in a new window. However, I want to allow the user to navigate around the site without losing the chat.
To do this, I've been trying to move the entire page into an iframe once the chat starts (with the chat outside the iframe), so the user can navigate around the site within the iframe without losing the chat.
I used this answer to get started, and that works great visually. However, some of the javascript in the background breaks.
One of the sites is ASP.NET web forms. The other is MVC. I've been working with the web forms one first. Stuff like calling __doPostBack breaks once the page is moved into the iframe since the javascript context is left behind.
Once the user clicks on a link (a real link, not a __doPostBack) and the iframe refreshes, then everything works perfectly.
How I see it, I have a few options:
Copy all javascript variables from window.top into the iframe somehow. Hopefully without having to know all the variable names. I tried this.contentWindow.__doPostBack = window.top.__doPostBack, which works, but other variables are missing so it ultimately fails:
Somehow switch the iframe's context to look at the top window context, if that's even possible? Probably not.
Another thought was to not move the page into an iframe right away, but to wait until the page changes and then load the new page into a new iframe. But I'm not sure how to hook into that event and highjack it.
Something else?
These are sites for use by our employees only, so I only have to support IE11 and Chrome.
Update:
Thanks to LGSon for putting me on the track of using the target attribute (so I can use approach #3). Below is what I ended up doing. When I pop out the chat, I call loadNextPageInIframe(). I'm using jQuery here since we already use it on our site, but everything could be done without. I set the target on all links that don't already have a target pointing to another frame or _blank. I left _parent out, but I don't think we use it anyway.
I have a reference to my chat window div in a global variable called 'chatwindow'.
There still could be some cases where this doesn't work, such as if there is some javascript that sets window.location directly. If we have anything in our sites that does this, I'll have to add a way to handle it.
function loadNextPageInIframe() {
var frame = $("<iframe id=\"mainframe\" name=\"mainframe\" />").css({
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
border: "none",
display: "none"
}).appendTo("body");
$("form, a:not([target]), a[target=''], a[target='_top'], a[target='_self']").attr("target", "mainframe");
var firstload = true;
frame.load(function () {
//Runs every time a new page in the iframe loads
if (firstload) {
$(frame).show();
//Remove all elements from the top window except the iframe and chat window
$("body").children().not(frame).not(window.top.chatwindow).remove();
firstload = false;
}
//Make the browser URL and title reflect the iframe every time it loads a new page
if (this.contentWindow && this.contentWindow.location.href && window.top.location.hostname === this.contentWindow.location.hostname && window.top.location.href !== this.contentWindow.location.href) {
var title = this.contentDocument.title;
document.title = title;
if (window.top.history.replaceState) window.top.history.replaceState(null, title, this.contentWindow.location.href);
}
});
}
May I suggest you do the following
get all links
attach an event click handler to intercept when someone click a link
on click event, check if chat is in progress, and if, feed the iframe with the new link
var links = querySelectorAll("a");
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function(e) {
if (isChatInProgress()) {
e.preventDefault(); //stop the default action
document.getElementById("your_iframe_id").src = e.target.href;
// anything else here, like toggle tabs etc
}
});
}
Update
To handle forms I see 4 ways at the moment
1) Add an onsubmit handler to your forms
function formIsSubmitted(frm) {
if (isChatInProgress()) {
frm.target = "the iframe";
}
return true;
}
<form id="form1" runat="server" onsubmit="return formIsSubmitted(this)">
2) Add a click handler to your buttons
function btnClick(btn) {
if (isChatInProgress()) {
btn.form.target = "the iframe";
}
return true;
}
<asp:Button runat="server" ID="ButtonID" Text="ButtonText"
OnClick="Button_Click" OnClientClick="return btnClick(this);" />
3) When a chat start, you iterate through each form and alter its target attribute
function startChat() {
var forms = querySelectorAll("form");
for (var i = 0; i < forms.length; i++) {
forms[i].target = "the iframe";
});
}
4) Override the original postback event (Src: intercept-postback-event)
// get reference to original postback method before we override it
var __doPostBackOriginal = __doPostBack;
// override
__doPostBack = function (eventTarget, eventArgument) {
if (isChatInProgress()) {
theForm.target = "the iframe";
}
// call original postback
__doPostBackOriginal.call(this, eventTarget, eventArgument);
}
Update 2
The absolute best way to deal with this is of course to use AJAX to load both page and chat content, but that likely means a quite bigger work load if your sites aren't already use it.
If you don't want to do that, you can still use AJAX for chat and if a user were to navigate to a new page, the chat app recreate the ongoing chat window with all its content again.
I suggest instead of loading content to and from iframes - build the chat as an iframe and use a jQuery modal popup on the page for chat.
You can fix the jquery modal to a fixed location and page scrolling is enabled by default. You need to modify css accordingly to make the popup remains on the same location.
If you go down your current path - you will need to worry a lot about how content is moved to the iframe and it might be difficult to re-use the chat on different pages depending on the content. For example, imagine you playing a video on the page and the user clicks chat - if you load the content to the iframe - the user will lose the status on how far he has viewed, etc.
as per my opinion, adding the whole website as an 'I-Frame' is not a good design practice, and not a good solution for the problem. My suggestion would be:
Ensure that the 'Chat' application is loaded in all the pages, across your website
Whenever the 'Chat' is started, either establish the 'web-socket' connection or somehow, maintain the State on the Server
Have the configuration of the 'Chat' as 'Minimized', 'Open' etc and store them in your cookie or session storage
On every page load, call the 'Chat' application too. Read the Chat related configuration from sessionstorage or cookie and maintain it's state as 'Minimized' or 'Open' etc, including the X and Y position, if you want to make it as 'Floated'
Every time, either fetch the entire conversation from the server via Ajax or try to store and fetch from 'Local Storage' and do Ajax only for any Updates from the other party
Use CSS based 'Float' related properties to make it float and sit at some side.
This will ensure that your chat is available for the user and yet he can navigate all through the site.
The Feature I Want:
I want to give a user a link like mysite.com/foo#bar so when they hit this link they land on the page foo and are scrolled half way down the page to the content with the id bar. It should be noted that this link will always be clicked from off site or typed into the address bar manually, the user will not already be on the page with all assets loaded.
Also fyi I am using angular and the page in question each bit of content is in an element directive, and in each directive template there are images
The Issue:
Easy enough right? Well I'm running into some problems, most of the time it works, but maybe 40% of the time it doesn't and the user lands above the content, I believe this is because the browser scrolls to the correct point on the page, but then slightly afterward images are loaded in above it pushing the rest of the page down, leaving the user in a random unintuitive spot. (For some reason the failure rate seems to be worse on iPhones...)
What I've Tried So Far:
In a run function I look for a hash fragment on any route and scroll to it if it exists.
if($location.hash()) {
$anchorScroll();
}
I've tried:
Wrapping it in a timeout
This works sometimes but is obviously not consistent, sure I can set it to 500ms and on a great wifi connection it's fine, but not on a mobile with poor signal
listening for $viewContentLoaded
Too fast, ui-router seems to fire this event way before the page is rendered
Emitting an event in each post-link function for all directives on the page.
link: function ($scope) {
$scope.$emit('loadingFinished');
Then picking that up again in the run function
$scope.$on('loadingFinished', function () {
$timeout(doAnchorScroll, 500);
}
This raised the success rate but still wasn't foolproof. And I witnessed it leaving the user stranded in weird spots mainly on iPhones.
Can anyone suggest a way of detecting a moment where it is safe to scroll, or perhaps some other way of ensuring landing in the correct spot?
Two things - first is a user-scroll should prevent it - you don't want to be scrolling the view if they've already scrolled. Second is listening for the images to finish loading.
Listening for the images is quite easy:
$("img").one("load", $anchorScroll);
Then in $anchorScroll I'd suggest checking if all images have loaded, and returning immediately if not (bonus points if you only check images above the anchor - but only doing a quick reply):
var scrolled = false;
function $anchorScroll() {
var allLoaded = true;
$("img").each(function() {
if (!this.loaded) {
return allLoaded = false;
}
});
if (scrolled || !allLoaded) {
return;
}
...
The scrolling is possibly slightly harder - you can check for scroll events, but they are slightly different between platforms - and might even get fired for the manual scroll - if you find that happens then simply have a global that says "I'm scrolling here" and ignore it, otherwise:
$(document).one("scroll", function() { // Use ".on" if this gets fired for code...
scrolled = true;
});
Note that you probably don't need to care about failed image loads, or ones that have already loaded, since your initial call to $anchorScroll() will catch them.
I have looked around, but I'm not seeing anything that specifically addresses this. My goal is to have a link, which can be clicked to either add content or "undo" the act of adding that content. I am trying to us the following:
function ShowDiv() {
if (null == window.set) {
document.getElementById("box2").innerHTML = "Some Content";
window.set = "set";
} else
location.reload();
}
Link
<div id="box2"></div>
This allows me to click the link to show some content inside some div. And then to click the link again to remove that content.
However, I am wondering if there is a way to achieve this result, that also allows the user to click the browser's back button to return the page to the state it was in before triggering the function (e.g., to reload the page).
You are looking for the HTML5 history API. This allows you to push state onto the history as if the browser loaded a different location without actually sending a request and replacing all content and javascript state. This allows the back and forward buttons to work, as long as your JavaScript code shows the correct content according to the current URL.
Resources:
W3C
MDN
Dive Into HTML5
Clickjacking is when people trick users into clicking a button they're not supposed to, making them perform a malicious action.
I'm working on a product which, as an option for merchants, provides an iFrame component that can be embedded into a website to make a payment. Signed in users will see a button in the iframe that they can click to perform an important action. This action should only be called when the click is genuinely theirs.
If, for example, the iFrame's opacity is set to 0, then it can be positioned such that the button in our iFrame is invisible, but on top of a different visible button. Users can therefore be tricked into clicking it.
I think I have a method for preventing it, but I'm not sure if it's sufficient or not. The following code would go in the iFrame:
<script>
function frameVisible() {
var has_dimension = $(frameElement).is(':visible');
var is_visible = $(frameElement).css('visibility') == 'visible';
var is_opaque = $(frameElement).css('opacity') == '1';
var one_deep = (parent == top);
return has_dimension && is_visible && is_opaque && one_deep;
}
if (!frameVisible()) {
$(document.body).hide()
}
</script>
Basically, if the iframe is obscured in any way, the iframe content will be hidden, preventing any unintended clicks.
I'm just trying to find out if there's a way around the code provided here.
That is not sufficient.
Attackers can position their own elements above the <iframe>, and either leave a small gap for the user to click through, or set pointer-events: none to allow users to click through the cover.
AFAIK, there is no way for you to detect that.
I've got a simple app that parses Tumblr blog templates. It's modeled after their customization screen and contains a header with some configurable options and an iframe. Every time an option changes, the app reloads the iframe, serializing the config form and updating the iframe's src with the serialized data as a query string.
This all works fine, except every time this happens, I am only able to reload the main index page with the changed options. If the user wants to view, say, a single post page by navigating away from the index, the template renders that page, only with the default options.
In other words, the changed options do no persist while the user navigates the blog.
I've been able to have 'persisting changes' with an $('iframe').load() event like so:
$('iframe').load(function(){
updateTheme();
});
The only problem is, as you can tell, this would wait for the iframe to fully render the page using the default options, then re-renders it with the updated options. I mean... it works, but it's not really a great solution.
Does anybody know how I can prevent the iframe from loading, capturing the users desired location, then re-render the frame with the current options as represented in the header?
Any help would be greatly appreciated. Thanks in advance!
Are you hosting both the top-level page and the embedded iframe page? If so, there are some games you can play, but it's not pretty. For example you can rewrite links within the embedded iframe in order to pre-fill the config options, e.g. with something like:
$('iframe').load(function(){
$('a', $('iframe')).each(function() {
var new_url = this.attr("href");
new_url += config_options;
this.attr("href", new_url);
});
});
Here's what I came up with:
var p = top || parent;
(function($){
$('a').click(function(e) {
var prevent = e.isDefaultPrevented(),
is_local = p.isLocal(this.href),
is_hash = $(this).attr('href').match(/^#/);
if(prevent || ! is_local || is_hash) return;
e.prevenDefault();
p.updateTheme(this.href);
return false;
});
})(jQuery);
My worry was that I would be affecting the javascript events attached to <a/> tags by the user, but apparently jQuery will detect default prevented events, even if they weren't prevented with jQuery itself. I tested it like this:
document.getElementById('test-link').onclick = function() {
return false;
}
jQuery detects that the original event has been prevented, so I am just assuming I shouldn't continue.