I don't have much experience with external popup software, nor
with popups issues, so I wouldn't be sure enough to provide an
appropriate script (I'll try this WE though), but the approach
you've described, checking the reference then the "closed"
property/load event at regular intervals with specific rules
to make the fallback decision, sounds quite solid to me (and
maybe not as ugly as you say,
Well I have written a version and it seems to work, though it could
probably do with some more testing), but it is ugly (at least I think
so). I have not been feeling very inspired this week so I am sure there
is plenty of room for improvements, but I don't feel like doing any more
with it for at least a day or so. So I am going to post it as it is and
let it be kicked about in public for a bit.
the TimedQue component might give interesting
results with the fallback controller).
I was thinking about a custom setTimeout function but TimedQue does lend
itself to the task in hand very well. I also noticed a slight bug
(commented below) in the earlier versions so using it was useful from
that point of view as well.
In the end, it still will be impossible to tell whether a
window has been closed by a software or the user,
Yes that is a problem, as it proved necessary to keep checking that the
window was still open for a short while after it loaded its page and set
the value in the opener. As it stands, if the user closes the window at
any point prior to the script deciding that the new window is ok and
safe then the fall-back happens, so if the user genuinely changed their
mind about viewing the content then they are going to have to use the
back button to return to the page they started on.
so navigation rules and fallback
will get even more attention.
<snip>
Judging the best values to use for the various repeated tests and timing
intervals is not going to be easy. There are too many unknowns involved
and safe values will make for an unresponsive UI.
My test script:-
<html>
<head>
<title>Pop-up window script with reliable fall-back version 1</title>
<script type="text/javascript">
/* TimedQue takes a function reference as an argument and schedules
the execution of that function at intervals. If the function
returns false when executed it is removed from the queue and not
executed again. If the execution of the function returns true the
function stays in the queue and is executed again following the
interval (until it returns false). (return values will be
type-converted as appropriate.)
*/
var TimedQue = (function(){
var base, timer;
var interval = 40;
var newFncs = null;
function addFnc(next, f){
function t(){
next = next&&next();
if(f()){
return t;
}else{
f = null;
return next;
}
};
t.addItem = function(d){
if(next){
next.addItem(d);
}else{
next = d;
}
return this;
};
t.finalize = function(){
return ((next)&&(next = next.finalize())||(f = null));
};
/* debugging toString function */
//t.toString = function(){return (''+f+
// ((next)?'\n----------------\n'+next:''));};
return t;
};
function tmQue(fc){
if(newFncs){
newFncs = newFncs.addItem(addFnc(null, fc));
}else{
newFncs = addFnc(null, fc);
}
if(!timer){
timer = setTimeout(tmQue.act, interval);
}
};
tmQue.act = function(){
var fn = newFncs, strt = new Date().getTime();
if(fn){
newFncs = null;
if(base){
base.addItem(fn);
}else{
base = fn;
}
}
base = base&&base();
/* I added the following test for newFncs in case one of the
functions executed through this function calls TimedQue and
newFuncs becomes non-null during the - base = base&&base();
- line.
*/
if(base||newFncs){
var t = interval - (new Date().getTime() - strt);
timer = setTimeout(tmQue.act, ((t > 0)?t:1));
}else{
timer = null;
};
};
tmQue.act.toString = function(){
return 'TimedQue.act()';
};
tmQue.finalize = function(){
timer = timer&&clearTimeout(timer);
base = base&&base.finalize();
newFncs = [];
};
return tmQue;
})();
</script>
</head>
<body>
<script type="text/javascript">
/* This function (expression) is executed inline and must be
within the body of the page as it will document.write an
IFRAME element into the page.
*/
(function(){
var global = this;
var readyTest = false, initTestCount = 0;
/* This code is written into the IFRAME to ensure that it
will execute scripts written into it.
*/
var checkAr = [
"<script type=\"text\/javascript\">",
"(function(){",
"if((typeof parent != \'undefined\')&&(parent.winOpen_f)){",
"parent.winOpen_f.ready=true;",
"}",
"})();",
"<\/script>"
];
var ifrmAr = [
"<iframe name=\"",
"",
"\" width=\"0\" height=\"0\" ",
/* Maybe some instructions on IFRAME borders should be
included in the style attribute as Opera 6 displays them.
*/
"style=\"visibility:hidden;\" ",//cannot use display for O7
/* blank.html avoids cross-domain security restrictions on
Opera 7.11 (at least) and is a minimal HTML page:-
<html><head><title></title></head><body></body></html>
*/
"src=\"blank.html\"",
"><\/iframe>"
];
var fName = (ifrmAr[1] = "frame"+getTimeBasedString());
//alert(fName+'\n\n'+ifrmAr.join('\n'))
document.write(ifrmAr.join(''));
/* The following array is the code written into the IFRAME in
order to open a new window (using an unmodified wiondw.open
method).
*/
var outAr = [
"<script type=\"text\/javascript\">",
"(function(){",
/* Check that the window has an open function, if it does
not then the test function will navigate the current
window in 2 seconds as parent.winOpen_f._refSent will
remain false.
*/
"if((this.open)&&(parent.winOpen_f)){",
"parent.winOpen_f._ref[\'",
"", //NAME index 4
"\']=this.open(\'",
"", //URL index 6
"\',\'",
"", //NAME index 8
"\'",
"", //FEATURES ,"..." (including comma and quotes) index 10
");",
"parent.winOpen_f._refSent[\'",
"", //NAME index 13
"\']=true;",
"}",
"})();",
"<\/script>"
];
function getTimeBasedString(){
var rmd, res = '';
var num = (new Date().getTime());
while(num > 0){
rmd = (num % 52)|0;
num = Math.floor(num / 52);
rmd += ((rmd > 25)?71:65);
res = res + String.fromCharCode(rmd);
}
return res;
};
/* This function writes the test script into the IFRAME and sets
readyTest to indicate that it has acted. If it has acted but
the winOpen_f.ready property is not true (within a limited
period) then the IFRAME will not execute scripts written in to
it.
*/
function checkIframe(){
var frm = frames[fName];
if((frm)&&(frm = frm.document)){
frm.open();
frm.write(checkAr.join(''));
frm.close();
readyTest = true;
return false;
}else{
if(initTestCount > 1){
/* If the IFRAME has not been fond by now the chances
are it never will be. So set readyTest to true and let
the rest of the script assume that writing the script
into the IFRAME failed so it can get on with falling
back.
*/
readyTest = true;
}
}
return true;
}
function getTestFunction(name, url){
var startTime = new Date().getTime();
var checkCount = 0;
return (function(){
var interval = new Date().getTime() - startTime;
var winRef;
if(!winOpen_f._refSent[name]){ //window ref not set yet
/*If window.open errors in the IFRAME then
winOpen_f._refSent[name] will never be true.
*/
if(interval > 2000){ //true if tested for more than ...
return setLocation(url);
}
}else{ //win ref is set
if((!(winRef = winOpen_f._ref[name]))||(winRef.closed)){
return setLocation(url);
}else{
if(!winOpen_f._refArived[name]){ //not arrived yet
if(interval > 180000){ //give it 3 minutes to show up
if((winRef)&&(winRef.close))winRef.close();
return setLocation(url);
}
}else{
/* If the window loads quickly it is possible for it
to report its arrival before being closed by the
pop-up blocker so this test is repeated 3 times to
make sure.
Unfortunately, if the user closed the window
within this period the page will be opened in the
current window.
*/
return (++checkCount < 4); //arrived so stop testing?
}
}
}
return true; //keep testing
});
}
function setLocation(url){location = url;return false;};
function cp(url, name, features){ //create popup
if(!name){name = getTimeBasedString();};
if(initTestCount < 6){
TimedQue(function (){ //schedule the window opening function.
var winRef, frm = frames[fName];
if((cp.ready)&&(frm)&&(frm = frm.document)){
/* If a window reference exists under this name then
we may as well re-use it as go to the effort of
re-calling window.open. It is probably also
reasonable to assume that it will not now be
pop-up killed.
*/
if((winRef = cp._ref[name])&&(!winRef.closed)){
winRef.location = url;
/* Mark the page as having arrived (based on the
assumption that it will) so that any unfinished
test functions for the previous page do not
decided to navigate this window.
*/
cp._refArived[name] = true;
if(winRef.focus)winRef.focus();
}else{
cp._refArived[name] = false;
cp._ref[name] = null;
cp._refSent[name] = false;
outAr[6] = url;
outAr[13] = (outAr[4] = (outAr[8] = name));
outAr[10] = (features?(",\'"+features+"\'")
""));
//alert(outAr.join('\n'))
frm.open();
frm.write(outAr.join(''));
frm.close();
TimedQue(getTestFunction(name, url));
}
}else{
/* Either the initial script written into the IFRAME
failed, or checkIframe has not successfully executed
yet.
*/
++initTestCount;
if((!readyTest)||(initTestCount < 6)){
/* If readyTest is false then keep periodically
executing this function to give checkIframe a
chance to execute. Otherwise give it up to 5
more opportunities to set cp.ready to true.
*/
return true; //continue cheking
}else{ //give up and fall back
setLocation(url);
}
}
/* Allow this function to be dropped from
the TimedQue queue by returning false.
*/
return false;
});
}else{
/* Didn't initialise (or IFRAME cannot be located, or it
doesn't contain a document object) so give up.
*/
setLocation(url);
}
return false; //this function always returns false.
}
if((global.location)&&(global.frames)){
if(global.setTimeout){
cp.ready = false;
cp._ref = {};
cp._refSent = {};
cp._refArived = {};
global.winOpen_f = cp;
TimedQue(checkIframe);
}else{
/* Without a working setTimoeout the best that can be done
is to navigate the current window. (Note that setLocation
returns false to cancel onclick navigation). We also need
a frames collection in order to find the IFRAME.
*/
global.winOpen_f = setLocation;
}
}else{
/* If it doesn't look like winOpen_f will be able to handle the
window opening/fall-back actively then set winOpen_f to a
function that will return true so that its return value can
be used to control onclick event handlers to provide HREF
fall-back.
*/
global.winOpen_f = function(){return true;};
}
})();
</script>
<a href="popUp.jsp?delay=3"
onclick="return winOpen_f(this.href, 'cc')">test window open 3</a><br>
<a href="popUp.jsp?delay=4"
onclick="return winOpen_f(this.href, 'cc')">test window open 4</a><br>
<script type="text/javascript">
/* The following line tests the inline creation of an unrequested
pop-up, (with requested pop-ups blocked by the browser it
should navigate the current window, not very useful).
*/
//winOpen_f("popUp.jsp?delay=5", 'cc');
</script>
</body>
</html>
The page that is being loaded in the pop-up (popUp.jsp) is a Java Server
Page. I was using that so I could simulate slow responses from the
server by having the Java thread that would return the page sleep for an
interval before sending anything back. And setting the length of the
delay with a query string on the URLs in the HTML for convenience. The
important part of the page for testing (assuming no Java application
server is available) is the JavaScript that sets the value in the opener
to announce that the page has arrived.
------------------------popUp.jsp-----------------
<%@ page language="java" %><%
String delayString = request.getParameter("delay");
long delayValue;
if(delayString != null){
try{
delayValue = Long.parseLong(delayString);
}catch(Exception e){
delayValue = 1;
}
}else{
delayValue = 1;
}
try{
Thread.sleep(delayValue);
}catch(InterruptedException ignore){
; //just carry on.
}
%><html>
<head>
<title>XXXX XXXX</title>
<script type="text/javascript">
if((this.name)&&(opener)&&(!opener.closed)&&
(opener.parent)&&(opener.parent.MyWinOpen)&&
(opener.parent.MyWinOpen._refArived)){
opener.parent.MyWinOpen._refArived[this.name] = true;
}
</script>
</head>
<body>
delay = <%= delayValue %>
</body>
</html>
I am not convince that this is good enough to solve the pop-up window
reliability problems (at least not yet) and I don't think that may of
the people who think that 5 or 6 lines of cut and paste code is all that
is required to create a new window are going to like the prospect of
replacing that with 200+ lines. There is also still (although there
always was) the problem of having to design the UI for both multi-window
use and one-window use, but at least it is getting closer to just those
two alternatives rather than having a script think it has a new window
when in fact it doesn't.
Richard.