Skip to content
Snippets Groups Projects
Commit ba174c72d750 authored by Jean-Francois Pieronne's avatar Jean-Francois Pieronne
Browse files

runtime/dclinabox/acme.js initial version

parent cbe7999d7eb5
No related branches found
No related tags found
No related merge requests found
///////////////////////////////////////////////////////////////////////////////
/*
acme.js
Some minimal shared infrastructure used by aLaMode, DCLinabox (as a child only)
and MonDeSi, and allowing each other's monitoring iframes to be embedded in the
other. This JavaScript is loaded into the top-level, parent page. An uncommon
name was chosen for the module.
COPYRIGHT
---------
Copyright (C) 2014,2015 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt
VERSION
-------
11-JAN-2015 MGD v1.0.0 release
02-FEB-2014 MGD initial development
*/
///////////////////////////////////////////////////////////////////////////////
var $AcmeVersion = '1.0.0';
// some invoking executable defined values
if (typeof $ExeVersion == 'undefined') throw '$ExeVersion?';
if (typeof $ScriptName == 'undefined') throw '$ScriptName?';
if (typeof $WebSocket == 'undefined') throw '$WebSocket?';
if (!window.WebSocket) $WebSocket = false;
if (!window.XMLHttpRequest) throw 'XMLHttpRequest?';
var acme_AppName;
var JSON_SENTINAL = '"""';
// shortcuts
var $byId = document.getElementById.bind(document);
var $byName = document.getElementsByName.bind(document);
var $byTag = document.getElementsByTagName.bind(document);
acmeBaseHref();
// a child always has a frame number delivered to it in the query string
var acme_IsChild = (location.search.substr(0,7) == '?frame=');
function acmeIsChild () { return acme_IsChild; }
function acmeIsParent () { return !acme_IsChild; }
// frames (children) are numbered 1..n with zero indicating the parent
var acme_FrameNumber = parseInt(location.search.substr('?frame='.length));
function acmeFrameNumber () { return acme_FrameNumber; }
// standlone window (cf. embedded)
var acme_StandAlone = (window.location.search.search('salone=1') > 0) ||
(window.location.search.search('monitor=2') > 0);
function acmeStandAlone () { return acme_StandAlone; }
///////////////////////////////////////////////////////////////////////////////
/*
Derive an application identifying name from the script name.
*/
function acmeAppName ()
{
if (acme_AppName) return acme_AppName;
var lio = $ScriptName.lastIndexOf('/');
if (lio == -1)
var name = $ScriptName.toLowerCase();
else
var name = $ScriptName.substr(lio+1).toLowerCase();
lio = name.lastIndexOf('.exe');
if (lio == -1) lio = name.lastIndexOf('.com');
if (lio != -1) name = name.substring(0,lio);
// support script name obfuscation by eliminating all but alphanumerics
name = name.replace(/[^a-zA-Z0-9-]/g,'');
return (acme_AppName = name);
}
///////////////////////////////////////////////////////////////////////////////
/*
Set the default base href.
*/
function acmeBaseHref ()
{
var base = document.createElement('base');
base.href = '/' + acmeAppName() + '/-/';
$byTag('head')[0].appendChild(base);
}
///////////////////////////////////////////////////////////////////////////////
/*
Dynamically load a file. Multiple files are loaded serially IN-ORDER. Can be
JavaScript (.js), style sheet (.css) or other (e.g. .html). Callback is
optional. Non-JavaScript, non-style-sheet files must have an associated
callback via which the content is delivered.
*/
if (typeof acme_LoadFileName == 'undefined')
{
// do not reinitialise if this script is called multiple times
var acme_LoadFileName = [];
var acme_LoadFileCall = [];
var acme_LoadFileIndex = 0;
}
function acmeLoadFile(filename,callback)
{
if (arguments.length)
{
acme_LoadFileName.push(filename);
if (typeof callback == 'undefined') callback = null;
acme_LoadFileCall.push(callback);
if (acme_LoadFileIndex < acme_LoadFileName.length-1) return;
}
var fname = acme_LoadFileName[acme_LoadFileIndex];
var ext = fname.match(/\.[0-9a-z]+$/i);
if (ext == '.js' || ext == '.css')
{
if (ext == '.js')
{
hndl = document.createElement('script');
hndl.setAttribute('type','text/javascript');
hndl.setAttribute('language','JavaScript');
hndl.setAttribute('src',fname);
}
else
{
hndl = document.createElement('link');
hndl.setAttribute("rel", "stylesheet");
hndl.setAttribute('type','text/css');
hndl.setAttribute('href',fname);
}
hndl.onerror = function()
{
alert('Failed to load: ' + fname);
throw new Error('Failed to load: ' + fname);
};
hndl.onload = function()
{
var callb = acme_LoadFileCall[acme_LoadFileIndex];
if (callb)
{
if (typeof callb == 'string')
eval(callb);
else
callb(hndl.responseText);
}
if (++acme_LoadFileIndex < acme_LoadFileName.length)
setTimeout(acmeLoadFile,1);
};
document.getElementsByTagName('head')[0].appendChild(hndl);
}
else
{
var hndl = new XMLHttpRequest();
hndl.open("GET",fname);
hndl.onreadystatechange = function()
{
if (hndl.readyState == 4)
{
if (hndl.status == 200)
{
var callb = acme_LoadFileCall[acme_LoadFileIndex];
if (callb)
{
if (typeof callb == 'string')
{
// string must be in the form: <function>(responseText);
var responseText = hndl.responseText;
eval(callb);
}
else
callb(hndl.responseText);
}
if (++acme_LoadFileIndex < acme_LoadFileName.length)
setTimeout(acmeLoadFile,1);
}
else
{
alert('Failed to load: ' + fname);
throw new Error('Failed to load: ' + fname);
}
}
}
hndl.send();
}
}
///////////////////////////////////////////////////////////////////////////////
/*
Set a child iframe width and height.
*/
var acme_ThisFrame = null;
if (acme_IsChild)
{
// periodically check that the iframe size has been adjusted
setInterval (acmeAdjustSize, 4900);
}
else
if (acme_StandAlone)
{
// in the parent ensure a standalone page size is periodically adjusted
acme_FitWindowTimer = setInterval (acmeFitWindow, 5000);
window.addEventListener ('resize', acmeFitWindow);
}
function acmeAdjustSize ()
{
if (arguments.length == 0)
{
// in child iframe
if (!acme_ThisFrame)
{
var number = parseInt(location.search.substr('?frame='.length));
acme_ThisFrame = 'FRAME' + number;
}
setTimeout('acmeAdjustSize(true)',100);
}
else
if (arguments.length == 1)
{
// in child iframe
var div;
if (!(div = $byId('acmeAppDiv')))
if (!(div = $byName('acmeAppDiv')[0]))
div = document.body.firstChild;
var displayWidth = Math.max(div.scrollWidth,
div.offsetWidth+20,
div.clientWidth+20);
var displayHeight = Math.max(div.scrollHeight,
div.offsetHeight+20,
div.clientHeight+20);
var json = '{"$SetIframeSize":true';
json += ',"frame":"' + acme_ThisFrame + '"';
json += ',"width":' + displayWidth;
json += ',"height":' + displayHeight;
json += '}';
window.parent.postMessage(json,'*');
}
else
if (arguments.length > 1)
{
// in parent window
var frame = arguments[0];
var width = arguments[1];
var height = arguments[2];
var obj = $byId(frame);
obj.width = obj.style.width = width + "px";
obj.height = obj.style.height = height + "px";
if (acme_StandAlone) acmeFitWindow();
}
}
///////////////////////////////////////////////////////////////////////////////
/*
Fit the open()ed window to the sections of the page (generally iframes).
The window size changes with elements' being made hidden and visible.
*/
var acme_FitHeight = 0,
acme_FitLast = 0,
acme_FitTimer = null,
acme_FitWidth = 0;
function acmeFitWindow (forceFit)
{
var cnt, obj,
height = 0,
width = 0;
if (acme_IsChild)
{
// in child iframe, pass the message up to the parent
window.parent.postMessage('{"$FitWindow":true}','*');
return;
}
// no more than every 200mS
var last = ((new Date()).getTime() / 200).toFixed(0);
if (last == acme_FitLast) return;
acme_FitLast = last;
// provide a single trailing timer event that forces a fit
if (acme_FitTimer)
{
clearInterval(acme_FitTimer);
acme_FitTimer = null;
}
if (!forceFit) acme_FitTimer = setTimeout('acmeFitWindow(true)',250);
for (cnt = 1; true; cnt++)
{
if (!(obj = $byId('SECTION'+cnt))) break;
width = Math.max(width,obj.scrollWidth,obj.offsetWidth,obj.clientWidth);
height += Math.max(obj.scrollHeight,obj.offsetHeight,obj.clientHeight);
}
if (height) height += 15;
for (cnt = 1; true; cnt++)
{
if (!(obj = $byId('FRAME'+cnt))) break;
width = Math.max(width,obj.scrollWidth,obj.offsetWidth,obj.clientWidth);
height += Math.max(obj.scrollHeight,obj.offsetHeight,obj.clientHeight);
height += 5;
}
// plus window accoutrements
width += (window.outerWidth - window.innerWidth);
height += (window.outerHeight - window.innerHeight);
var ht = $byTag('html')[0];
ht.style.overflowX = 'hidden';
// if the fit is not being forced and it's the same size as last time
if (!forceFit && acme_FitWidth == width && acme_FitHeight == height) return;
acme_FitWidth = width;
acme_FitHeight = height;
if (acme_FitHeight >= screen.availHeight)
{
ht.style.overflowY = 'scroll';
acme_FitWidth += 15;
}
else
ht.style.overflowY = 'hidden';
window.resizeTo(acme_FitWidth,acme_FitHeight);
}
///////////////////////////////////////////////////////////////////////////////
/*
Change the window title.
Remotely called via acmePostedMessage() via postMessage().
*/
var acme_TitleArray = new Array();
var acme_TitleTitle = null;
function acmeAddToTitle (string)
{
// only add the once
if (acme_TitleArray.indexOf(string) != -1) return;
if (!acme_TitleTitle) acme_TitleTitle = document.title;
acme_TitleArray.push(string);
acme_TitleArray.sort(function(s1,s2){return s1.localeCompare(s2)});
document.title = '';
for (var idx = 0; idx < acme_TitleArray.length; idx++)
document.title += acme_TitleArray[idx] + '\u00a0~\u00a0';
document.title += acme_TitleTitle;
}
///////////////////////////////////////////////////////////////////////////////
/*
Manipulate an array containing a boolean representing the success or not of the
frame load. When the frame(s) are created by the iframe builder the array is
set to false. When acmeCheckIframe() is called by the frame onload=.. it uses
this function (incestuously called via a postMessage() RPC) to check for a
successful load and provide an alert in the parent window if not. Must be
capable of working cross-domain.
*/
// only need this in the iframe(s) parent
var acme_IframeCheckParam = [];
var acme_IframeCheckTimer = [];
function acmeIframeCheck ()
{
var fnumber = arguments[0];
var param = arguments[1];
if (acme_IsChild)
{
// child (inside iframe)
if (typeof arguments[0] != 'number')
{
param = arguments[0];
fnumber = acme_FrameNumber;
}
if (typeof param == 'undefined') param = null;
// provide the parent with the parameter
var json = '{"$IframeCheck":true,"frame":' + fnumber;
if (typeof param == 'string')
json += ',"param":"' + param + '"}';
else
json += ',"param":' + param + '}';
window.parent.postMessage(json,'*');
}
else
{
// parent (of iframe(s))
if (typeof param == 'undefined')
{
// iframe onload=".." irrespective of success or fail
if (!acme_IframeCheckTimer[fnumber])
{
// give a loaded resource's JavaScript time to set the boolean
acme_IframeCheckTimer[fnumber] =
setTimeout('acmeIframeCheck('+fnumber+')',5000);
return;
}
}
else
if (typeof param == 'string')
{
// initial call to indicate we're begining to load this frame
acme_IframeCheckParam[fnumber] = param;
return;
}
if (param == true) acme_IframeCheckParam[fnumber] = param;
if (acme_IframeCheckParam[fnumber] == true) return;
var URI = acme_IframeCheckParam[fnumber];
var msg = '"' + URI + '" failed to load. Retry?';
if (confirm(msg))
{
// retry the iframe load
var ifr = $byId('FRAME'+fnumber);
ifr.setAttribute('src','javascript:');
ifr.setAttribute('src',URI);
ifr.setAttribute('onload','acmeIframeCheck('+fnumber+')');
}
}
}
///////////////////////////////////////////////////////////////////////////////
/*
Double-duty; receive message from a (potentially cross-domain) child via
postMessage(), and receives message from (potentially cross-domain) parent
postMessage().
*/
function acmePostedMessage(event)
{
if (typeof event.data != 'string') throw 'non-string postMessage()';
var data = JSON.parse(event.data);
if (data.$SetIframeSize)
acmeAdjustSize(data.frame, data.width, data.height);
else
if (data.$FitWindow)
acmeFitWindow();
else
if (data.$IframeCheck)
acmeIframeCheck(data.frame,data.param);
else
if (data.$AddToTitle)
acmeAddToTitle(data.node);
else
if (data.$ConfigCollect)
$CollectClick(data.checked);
else
if (console.log)
console.log(JSON.stringify(data));
else
alert(JSON.stringify(data));
}
///////////////////////////////////////////////////////////////////////////////
/*
Called from acmePostedMessage() from the overall configuration panel to
open/close the frame's WebSocket connection.
*/
function acmeConfigCollect (checked)
{
if (checked)
acmeIpcOpen();
else
acmeIpcClose();
}
///////////////////////////////////////////////////////////////////////////////
/*
WASD IPC WebSocket/XHR management.
Transparently handles WebSocket-enabled environments (combination of WASD and
browser) and the fallback to a slightly clunkier XMLHttpResquest()
implementation. TRhis IPC is on;y designed to support the single streaming
JSON data stream associated with these WASD application.
*/
var acme_IpcCount = 0,
acme_IpcConn = null,
acme_IpcOnClose = null,
acme_IpcOnMessage = null,
acme_IpcOnOpen = null,
acme_IpcReconnect = null,
acme_IpcXrhParseStart = 0,
acme_IpcXhrParseTimer = null;
// register a function for when the WebSocket closes
function acmeIpcOnClose (callback)
{
if (callback != null && typeof callback != 'function')
throw 'not a function';
acme_IpcOnClose = callback;
}
// register a function to receive data
function acmeIpcOnMessage (callback)
{
if (callback != null && typeof callback != 'function')
throw 'not a function';
acme_IpcOnMessage = callback;
}
// register a function to execute when opened
function acmeIpcOnOpen (callback)
{
if (callback != null && typeof callback != 'function')
throw 'not a function';
acme_IpcOnOpen = callback;
}
// return the WebSocket/XHR summary
function acmeIpcSummary ()
{
var txt;
if ($WebSocket)
txt = 'WebSocket()\n(re)connections: ' + acme_IpcCount + '\n';
else
txt = 'XMLHttpRequest()\n(re)connections: ' + acme_IpcCount + '\n' +
'responseText.length: ' + acme_IpcConn.responseText.length + '\n';
return txt;
}
/*
With an XMLHttpRequest() the response content stream is returned in periodic
bursts of data. Parse these progressive bursts using the sentinal string
(three consecutive quote marks should never occur in well-formed JSON).
*/
function acmeIpcXhrParseData ()
{
var data, end, len, start, txt;
start = acme_IpcXrhParseStart;
// accomodate MSIE (again)
if (typeof acme_IpcConn.responseText == 'unknown') return;
txt = acme_IpcConn.responseText;
len = txt.length;
if (len <= start+1) return;
for (;;)
{
end = txt.substr(start).indexOf(JSON_SENTINAL);
if (end == -1) break;
end += start;
data = txt.substring(start,end);
start = end + 3;
if (acme_IpcOnMessage) acme_IpcOnMessage(data);
}
acme_IpcXrhParseStart = start;
}
function acmeIpcOpen (params)
{
if (acme_IpcConn) return true;
if (typeof params == 'undefined') params = '';
// reopen after a short random period of time
var tout = 1500 + Math.floor((Math.random()*1000));
acme_IpcReconnect = 'setTimeout("acmeIpcOpen(\'' +
params + '\')",' + tout + ')';
if ($WebSocket)
{
if (window.location.protocol == 'http:')
var URL = 'ws://';
else
var URL = 'wss://';
URL += window.location.host + $ScriptName + '?data=1';
acme_IpcConn = new WebSocket(URL);
acme_IpcConn.onopen = function(evt)
{
acme_IpcCount++;
if (acme_IpcOnOpen) acme_IpcOnOpen();
};
acme_IpcConn.onclose = function (evt) { acmeIpcClosure(evt) };
acme_IpcConn.onmessage = function (evt)
{
if (acme_IpcOnMessage)
{
// unpack multiple JSON data from a single message
var data = evt.data.split(JSON_SENTINAL);
for (var idx = 0; idx < data.length; idx++)
if (data[idx].length) acme_IpcOnMessage(data[idx]);
}
};
}
else
{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function ()
{
if (xhr.readyState == 2)
{
acme_IpcCount++;
if (acme_IpcOnOpen) acme_IpcOnOpen();
}
else
if (xhr.readyState == 4)
{
// request complete (may be OK may be disconnected */
acmeIpcClosure ();
}
}
acme_IpcConn = xhr;
xhr.onerror = function()
{
acmeIpcClosure ();
}
xhr.onabort = function()
{
acmeIpcClosure ();
}
acme_IpcXrhParseStart = 0;
if (typeof xhr.onprogress == 'undefined')
acme_IpcXhrParseTimer = setInterval('acmeIpcXhrParseData()',1000);
else
xhr.onprogress = acmeIpcXhrParseData;
if (params.length)
params = '?data=1;' + params;
else
params = '?data=1';
xhr.open('GET',$ScriptName+params,true);
xhr.send();
}
return true;
}
// handle the closure
function acmeIpcClosure ()
{
// can be called multiple times (ie.e onabort, onerror, readyState == 4)
if (!acme_IpcConn) return;
if ($WebSocket) acme_IpcConn.close();
acme_IpcConn = null;
if (acme_IpcXhrParseTimer)
{
clearInterval(acme_IpcXhrParseTimer);
acme_IpcXhrParseTimer = null;
}
if (acme_IpcOnClose) acme_IpcOnClose();
// if intended to reconnect (either re-establish or new with params)
if (acme_IpcReconnect) setTimeout(acme_IpcReconnect,10);
}
// explicitly close the WebSocket/XHR
function acmeIpcClose (reconnect)
{
if (acme_IpcConn)
{
if (typeof reconnect == 'undefined')
acme_IpcReconnect = null;
else
acme_IpcReconnect = reconnect;
if ($WebSocket)
acme_IpcConn.close();
else
acme_IpcConn.abort();
}
else
if (reconnect)
setTimeout(reconnect,10);
}
// send a WebSocket/XHR message
function acmeIpcSend (msg)
{
if (acme_IpcConn)
{
if ($WebSocket)
acme_IpcConn.send(msg);
else
{
// much heavier-handed with XHR
acmeIpcClose('acmeIpcOpen("'+msg+'")');
}
}
}
function acmeIpcConnected ()
{
if (acme_IpcConn)
return true;
else
return false;
}
function acmeIpcCount ()
{
return acme_IpcCount;
}
///////////////////////////////////////////////////////////////////////////////
/*
In-line code to load the corresponding JavaScript code and when loaded to
execute a callback of the same name prefixed by a dollar.
*/
acmeLoadFile (acmeAppName()+'.js','$'+acmeAppName()+'()');
///////////////////////////////////////////////////////////////////////////////
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment