Attach jQuery event handler to popup window contents - javascript

EDIT - A short/brief description of the question. Is it possible using jQuery and javascript to intercept popup windows (on open) and listen/detect for keyboard input into text boxes from the parent window.
I am working on a site that has recently switched to Single Sign On type authentication. As a result, the user's session is no longer controlled by the application, but rather a central authorization server.
In order to prevent users from being booted without realizing their session is timing out, we created a simple timer. After X minutes of no activity, a warning banner appears with a link to extend the session.
Here is the code snippet we use to listen for activity on a web page
function DetectChanges() {
$('input[type=text], textarea').on('change keyup paste', function () {
console.log('Input detected - Resetting ession timers');
$('input[type=text], textarea').off('change keyup paste');
ResetSessionExpirationInformation();
setTimeout(DetectChanges, msToCheckInput);
});
}
Basically the premise is, every so often, see if the user is hitting the keyboard. If so, extend the session and stop listening for keyboard input for a little bit.
The problem I am running into is that I need to be able to tell if the user is hitting the keyboard in a child window (old schoool popup, not a dialog or modal). Since this site uses tons of inline JS, I don't have an easy way to gain access to these new windows. However, I did find that I can override the window.open function in order to track the windows.
var openedWindows = [];
window._open = window.open; // saving original function
window.open = function (url, name, params) {
var newWin = window._open(url, name, params);
newWin.focus();
newWin.onload = setTimeout(DetectChangesChild(newWin), msToCheckInput);
ResetSessionExpirationInformation();
openedWindows.push(newWin);
I am passing the reference to the window in the above DetectChangesChild method so I have a reference later on the code path.
function DetectChangesChild(childWindow) {
childWindow.alert("Listening Child Window");
$(childWindow).contents('input[type=text], textarea').on('change keyup paste', function () {
alert('Stuff happened');
console.log('Input detected - Resetting session timers');
childWindow.$('input[type=text], textarea').off('change keyup paste');
ResetSessionExpirationInformation();
setTimeout(DetectChangesChild(childWindow), msToCheckInput);
});
}
Does anyone know how to listen on text input elements in a popup for changes so that I can reset the session expiration timers appropriately?
Note - I am aware of the cautionary statements against overriding native functions, but we are in no danger of colliding with another override of this function.

Related

JavaScript: how can I have a constantly running process for the purpose of monitoring?

I'm new to JavaScript and I'm not really sure how this can be achieved. I'm using the Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) to read text from the user's clipboard. I want whatever the user currently has on their clipboard to show up on the page.
I can do a one-time paste just fine, but I'm not sure how to turn this into a constantly running process. I was planning on intermittently checking the user's clipboard to see if the contents have changed, and if they have, then paste that to the page. I want this to happen so long as the HTML page is open. Is this possible?
JavaScript seems to not like while(true) loops as whenever I've tried that the page has just crashed/become unresponsive.
You could listen for the copy event, when that is triggered you know the user has copied something from the page.
For clipboard changes that happen elsewhere, you can listen for the onfocus event on the window and check for clipboard changes then.
A copy in another app/webpage cannot happen without your window losing focus.
If that doesn't work, you can use setInterval(..) instead of an infinite loop.
Here is an example:
let prevClipText;
setInterval(() => {
navigator.clipboard.readText().then(clipText => {
if (clipText !== prevClipText) {
console.log('clipboard content was changed to:', clipText);
prevClipText = clipText;
}
});
}, 500);
You could set up an eventListener to continuously listen for the copy event, I would imagine this would do what you're looking to do. It would make it possible for you to continuously fire an update for the area where you want to display the copied content every time a user copies something. This of course requires a first time read when user enters the page to see if there is anything on the clipboard at that time. Set the event listener immediately after that check.
From MDN:
source.addEventListener('copy', (event) => {
const selection = document.getSelection();
event.clipboardData.setData('text/plain', selection.toString().toUpperCase());
event.preventDefault();
});
Info: https://developer.mozilla.org/en-US/docs/Web/API/Element/copy_event

How can I warn user on back button click?

www.example.com/templates/create-template
I want to warn users if they leave create-template page. I mean whether they go to another page or to templates.
I use this code to warn users on a page reload and route changes should the form be dirty.
function preventPageReload() {
var warningMessage = 'Changes you made may not be saved';
if (ctrl.templateForm.$dirty && !confirm(warningMessage)) {
return false
}
}
$transitions.onStart({}, preventPageReload);
window.onbeforeunload = preventPageReload
It works as expected on a page reload and route changes if it is done by clicking on the menu or if you manually change it. However, when I click the back button, it does not fire the warning. only it does if I click the back button for the second time, reload the page, or change route manually.
I am using ui-router. When you click back button, you go from app.templates.create-template state to app.templates state.
How to warn if they press Back button?
First of all, you are using it wrong:
from https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload:
Note: To combat unwanted pop-ups, some browsers don't display prompts
created in beforeunload event handlers unless the page has been interacted
with; some don't display them at all. For a list of specific browsers, see the
Browser_compatibility section.
and
window.onbeforeunload = funcRef
funcRef is a reference to a function or a function expression.
The function should assign a string value to the returnValue property of the Event object and return the same string.
You cannot open any dialogs in onbeforeunload.
Because you don't need a confirm dialog with onbeforeunload. The browser will do that for you if the function returns a value other than null or undefined when you try to leave the page.
Now, as long as you are on the same page, onbeforeunload will not fire because technically you are still on the same page. In that case, you will need some function that fires before the state change where you can put your confirm dialog.
How you do that depends on the router that you are using. I am using ui-router in my current project and I have that check in the uiCanExit function.
Edit:
You can keep your preventPageReload for state changes in angular. But you need a different function for when the user enters a new address or tries to leave the page via link etc.
Example:
window.onbeforeunload = function(e) {
if (ctrl.templateForm.$dirty) {
// note that most broswer will not display this message, but a builtin one instead
var message = 'You have unsaved changes. Do you really want to leave the site?';
e.returnValue = message;
return message;
}
}
However, you can use this as below:(using $transitions)
$transitions.onBefore({}, function(transition) {
return confirm("Are you sure you want to leave this page?");
});
Use $transitions.onBefore insteadof $transitions.onStart.
Hope this may help you. I haven't tested the solutions. This one also can help you.

AngularJS unbind after onBeforeUnload prompt, $destroy not firing

I have a requirement to show a prompt whenever our reservation page is navigated away from. When navigating within the app, there is no problem. A simple $onLocationChange works fine with a custom prompt. However when navigating to , for example, google or our logout page (which is in a separate angular app) the $onLocationChange does not work. In this case, I used:
window.onbeforeunload = onUnloadAppStart;
function onUnloadAppStart() {
var message = figureOutWhichMessageToUse();
return message;
}
this way I can at least customize the navigate message, even though I cant change the prompt box itself. However this causes a memory leak. So I need to set to null when I leave. Something like this:
$scope.$on("$destroy", function() {
window.onbeforeunload = null;
}
however this(the $destroy event) only seems to fire when I navigate within the app, same as the $onLocationChange. Furthermore I need to clean up binding AFTER the prompt, which is after the onUnloadAppStart function. This is because if I remove the binding and the user says "no, stay on this page", now my "onbeforeunload" is screwed up. Is there a way to fire functions after the onbeforeunload prompt?

Move entire document into iframe

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.

iFrame Validation

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.

Categories

Resources