/* Copyright (c) 2010 by Michael J. Roberts. All Rights Reserved. */
/*
* Utility package for TADS 3 Web UI. This is a javascript file that's
* designed to be included from the main UI page of a Web-UI game. This
* provides a set of useful functions for implementing an IF-style user
* interface in HTML/Javascript.
*/
/* ------------------------------------------------------------------------ */
/*
* Miscellaneous globals
*/
// my page name, as I'm known to the server
var pageName = "";
// the generic noun we use to refer to this program in error messages
// (game, story, program, ...)
var selfNoun = "story";
// default action suggestion
var errorRecoveryAction =
" If the " + selfNoun + " otherwise seems to be running "
+ "properly, you can ignore this error. If the " + selfNoun
+ " isn't responding or is acting strangely, it might help to "
+ "reload this page .";
/* ------------------------------------------------------------------------ */
/*
* Define a property on an object. If the browser supports the
* defineProperty method, we'll use that; otherwise we'll just set the
* property directly.
*
* This differs from direct property setting in that we mark the property
* as non-enumerable. This is particularly useful for our Object.prototype
* extensions when jQuery is also being used. jQuery uses Object property
* enumerations internally and incorrectly assumes that all results are
* browser-defined properties, so our custom extensions can create problems
* if jQuery is also used. Explicitly hiding those from enumerations lets
* us define our own custom Object.prototype extensions without confusing
* jQuery. (The webui library itself doesn't use jQuery, so this isn't an
* issue out of the box, but it is an issue for individual game authors who
* want to use jQuery with the webui code.)
*/
function defineProperty(target, name, method)
{
if (Object.defineProperty && Object.defineProperties)
Object.defineProperty(target, name,
{
'value': method,
'configurable': true,
'enumerable': false,
'writable': true
});
else
target[name] = method;
}
/* ------------------------------------------------------------------------ */
/*
* Initialize. Call this once at page load to set up this package.
*/
function utilInit()
{
// if we have a parent, ask the parent for my name
if (window.parent && window.parent != window)
pageName = window.parent.pathFromWindow(window);
// detect the browser version
BrowserInfo.init();
// copy an object
defineProperty(Object.prototype, "copy", function(deep) {
// create a blank object with the same prototype as me
var newObj = CopyProto.create(this);
// copy each property
for (var prop in this)
{
// include only directly defined properties
if (this.hasOwnProperty(prop))
{
// get this value
var val = this[prop];
// recursively copy objects if this is a "deep" copy
if (deep && typeof(val) == "object")
val = val.copy(true);
// save this value
newObj[prop] = val;
}
}
// return the new object
return newObj;
});
// get the number of enumerable keys
defineProperty(Object.prototype, "propCount", function() {
var cnt = 0;
for (var prop in this)
{
if (this.hasOwnProperty(prop))
++cnt;
}
return cnt;
});
// enumerate keys set in an object, excluding prototype keys
defineProperty(Object.prototype, "forEach", function(func) {
for (var prop in this)
{
// call the callback only for props directly defined in 'this'
if (this.hasOwnProperty(prop))
func(this[prop], prop);
}
});
// find the property for which a callback returns true
defineProperty(Object.prototype, "propWhich", function(func) {
for (var prop in this)
{
if (this.hasOwnProperty(prop) && func(this[prop], prop))
return prop;
}
return null;
});
// find the property value for which a callback returns true
defineProperty(Object.prototype, "valWhich", function(func) {
for (var prop in this)
{
var val;
if (this.hasOwnProperty(prop) && func(val = this[prop], prop))
return val;
}
return null;
});
// htmlify a string - convert markup-significant characters to & entities
String.prototype.htmlify = function() {
return this.replace(/[<>&"']/g, function(m) {
return { '<': "<", '>': ">", '&': "&",
'"': """, '\'': "'" }[m];
});
};
// remove html quoting from a string
String.prototype.unhtmlify = function() {
return this.replace(/&(gt|lt|amp|quot|nbsp|#[0-9]+);/ig,
function(m, i) {
var r = { 'gt': '>', 'lt': '<', 'amp': '&', 'quot': '"',
'nbsp': ' ' }[i.toLowerCase()];
if (r)
return r;
else
return String.fromCharCode(parseInt(i));
});
}
// trim leading and trailing spaces from a string
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, "");
};
// convenience method: find an item in an array
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(val) {
for (var i = 0 ; i < this.length ; ++i)
{
if (val == this[i])
return i;
}
return -1;
};
}
// convenience method: find the index for a value matching a callback
if (!Array.prototype.indexWhich) {
Array.prototype.indexWhich = function(test) {
for (var i = 0 ; i < this.length ; ++i)
{
if (test(this[i], i))
return i;
}
return -1;
};
}
// convenience method: find a value matching a callback
if (!Array.prototype.valWhich) {
Array.prototype.valWhich = function(test) {
for (var i = 0 ; i < this.length ; ++i)
{
var v = this[i];
if (test(v, i))
return v;
}
return -1;
};
}
// convenience method: map items in an array
if (!Array.prototype.map) {
Array.prototype.map = function(mapping) {
var ret = [];
for (var i = 0 ; i < this.length ; ++i)
ret.push(mapping(this[i], i));
return ret;
};
}
// convenience method: filter an array
if (!Array.prototype.filter) {
Array.prototype.filter = function(filterFunc) {
var ret = [];
for (var i = 0 ; i < this.length ; ++i)
{
var ele = this[i];
if (filterFunc(ele, i))
ret.push(ele);
}
return ret;
};
}
// convenience method: get the top element of a stack-like array
Array.prototype.top = function() {
if (this.length)
return this[this.length - 1];
else
return null;
};
// override our copy method for arrays
Array.prototype.copy = function(deep) {
// create the new array
var newArray = [];
// copy each element
for (var i = 0 ; i < this.length ; ++i)
{
// get this value
var val = this[i];
// make a deep copy of object values, if desired
if (deep && typeof(val) == "object")
val = val.copy(true);
// set the new value
newArray[i] = val;
}
// return the new array
return newArray;
};
// override the for-each enumerator for arrays - just step through
// the index values from 0 to length
Array.prototype.forEach = function(func) {
for (var i = 0, len = this.length ; i < len ; ++i)
func(this[i], i);
};
// add a listener for mouse down events on the main document,
// for closing spring-loaded pop-up objects
addEventHandler(document, "mousedown", popupCloser);
// set our custom javascript error handler
window.onerror = function(msg, url, lineNum) {
// generate a stack trace
var s = getStackTrace();
// the top element is the onerror handler we're currently in,
// which isn't part of the error, so clip it out of the list
s.shift();
// create a human-readable trace listing
s = s.map(function(ele) {
return ele.desc.htmlify(); }).join(" ");
// log the error
logError("An error occurred in the " + selfNoun
+ "'s user interface programming. This is probably a "
+ "bug that should be reported to the "
+ selfNoun + "'s author."
+ errorRecoveryAction,
"Javascript error: " + msg.htmlify()
+ " Window: " + pageName
+ " (" + url.htmlify() + ")"
+ " Line: " + lineNum
+ (s ? " Call stack:
"
+ s + "
": ""),
{ failed: true, succeeded: false });
return true;
};
// set up an unload handler
addEventHandler(window, "beforeunload", unloadCleanup);
}
// window unload handler
function unloadCleanup()
{
// cancel outstanding ajax requests
cancelServerRequests();
}
/*
* Create an object with the same prototype as an existing object. To
* invoke, call SameProto.create(obj).
*/
function CopyProto() { }
CopyProto.create = function(obj)
{
/*
* This is a bit tricky, but here's how this works. The ECMA spec says
* that a new object's prototype is set to its constructor's prototype
* *at the moment of creation*. So step 1 is to prepare the
* constructor's prototype so that it matches the source object's; then
* step 2 is to instantiate the object with 'new'.
*/
CopyProto.prototype = obj.constructor.prototype;
return new CopyProto();
}
/*
* Get a stack trace. This returns a list of function call descriptors.
*/
function getStackTrace()
{
// start with an empty list
var s = [];
// walk up the call stack starting with our caller
for (var c = arguments.callee.caller ; c ; c = c.caller)
{
// Make sure we haven't encountered this caller before.
// Javascript uses the function object to represent a call
// level, which makes it impossible to walk a recursive
// stack because each recursive call overwrites the caller
// information in the function object. JS of course has
// the actual caller information internally, but it's
// not exposed through the standard APIs.
for (var i = 0 ; i < s.length ; ++i)
{
if (s[i].fn == c)
{
s.push({ desc: "(recursive)" });
return s;
}
}
// set up the level descriptor
var ele = { fn: c, args: c.arguments };
// get a formatted version of the argument list
var a = [];
if (c.arguments)
{
for (var i = 0 ; i < c.arguments.length ; ++i)
a.push(valToDesc(c.arguments[i]));
}
// build the human-readable function call description
ele.desc = funcToDesc(c) + "(" + a.join(", ") + ")";
// add it to the list
s.push(ele);
}
// return the trace
return s;
}
function valToDesc(v)
{
// convert to string
switch (typeof(v))
{
case "function":
return funcToDesc(v);
break;
case "string":
// shorten it if it's long
if (v.length > 60)
v = v.substr(0, 30) + "..." + v.substr(v.length - 20);
// escape special characters
return "\""
+ v.replace(/["\\\n\r\v\f\t]/g, function(m) {
return { '"': '\\"', '\n': '\\n',
'\r': '\\r', '\\': '\\\\',
'\v': '\\v', '\f': '\\f', '\t': '\\t'
}[m]; })
+ "\"";
case null:
return "null";
case "undefined":
return "undefined";
default:
if (v == null)
{
return "null";
}
else if (v instanceof Array)
{
var a = [], amax = (v.length > 8 ? 5 : 8);
for (var i = 0 ; i < v.length && i < amax ; ++i)
a.push(valToDesc(v[i]));
if (v.length > amax)
a.push("... (" + (v.length - amax) + " more)");
return "[" + a.join(", ") + "]";
}
else
{
try { v = v.toString(); } catch (e) { v = typeof(v); }
if (v.match(/\[object\s+([a-z0-9_$]+)\]/i))
v = "object<" + RegExp.$1 + ">";
return v;
}
return typeof(v);
}
}
// format a function reference to a human-readable form (primarily for
// stack trace generation)
function funcToDesc(f)
{
// get the function in string form, and convert all whitespace
// (including newlines) to ordinary spaces
fn = f.toString().replace(/[ \n\r\v\f\t]+/g, " ");
// For a named function, the usual toString form is like this:
// function name(args) { body }
// Some browsers return this same form for anonymous functions,
// supplying 'anonymous' as the name.
if (fn.match(
/^function\s+([a-z0-9_$]+)\s*\([a-z0-9_$,\s]*\)\s*(\{.*)$/i))
{
// we have the basic syntax - if the name is 'anonymous', show
// using our anonymous function formatter; otherwise just use
// the function name
if (RegExp.$1 == "anonymous")
{
// The browser is using literally "anonymous" as the name.
// Pull out the body to show for context. Use a clipped
// version to keep the size reasonable.
var body = anonFuncToDesc(RegExp.$2);
// add the event handler name, if applicable
return funcToEventDesc(f) + body;
}
else
{
// Named function.
var body = RegExp.$2;
fn = RegExp.$1;
// Some browsers return a synthesized name based on the event
// attribute for an in-line event handler. If we have an
// event name, and the function has the same name, assume
// this is the situation, so include the function body for
// context as though it were an anonymous function.
var ev = funcToEventDesc(f);
if (ev && ev.substr(ev.indexOf(".") + 1) == fn)
return ev + anonFuncToDesc(body);
// it's just a regular named function
return fn;
}
}
else
{
// it's not in the standard format, so it's probably some other
// anonymous function formatting
return funcToEventDesc(f) + anonFuncToDesc(fn);
}
}
// Get the event object for a function call, if applicable
function eventFromFunc(f)
{
// Check for an event object in the function arguments. If we have
// at least one argument, and it's an object, and it has a "type"
// property, and the type property looks like "onxxx", assume it's
// an event object.
if (f.toString().match(/^function\s+[a-z]+\s*\(event\)/)
&& f.arguments
&& f.arguments.length >= 1
&& f.arguments[0] != null
&& typeof(f.arguments[0]) == "object"
&& typeof(f.arguments[0].type) == "string"
&& f.arguments[0].type.match(/^[a-z]+/))
return f.arguments[0];
// if this is IE, use the window.event object
if (window.event)
return window.event;
// no event object
return null;
}
// Check for an event handler. If this is a top-level function,
// and the target of the event has an attribute for the event
// method, and the attribute points to this function, we must be
// the handler for the event. In this case we can supply the
// name of the event and the ID or node type of the target object
// as additional context.
function funcToEventDesc(f)
{
// only top-level functions can be event handlers
if (f.caller != null)
return "";
// we obviously need to have an event for this to be an event handler
var ev = eventFromFunc(f);
if (!ev)
return "";
// figure the event attribute, based on the event type name
var attr = "on" + ev.type;
// The event handler could be on the target, or on a parent object
// by event bubbling. Scan up the parent tree for an object with
// a handler pointing to our function.
for (var targ = getEventTarget(ev) ; targ ; targ = targ.parentNode)
{
// check for a handler in this object pointing to our function
if (targ[attr] == f)
{
// Found it - this must be the handler we're running. Return
// the ID of the element (or just the tag name if there's no ID)
// and the event attribute name as the event description.
return (targ.id ? targ.id : targ.nodeName)
+ "." + attr;
}
}
// didn't find a handler
return "";
}
// Format an anonymous function body to human-readable form. We'll
// show a fragment of the source code body of the function, if available.
function anonFuncToDesc(f)
{
// some browsers give us the format "{ javascript:source }" for
// in-line event handlers (onclick, etc) - remove the "javascript:".
if (f.match(/^\{\s*javascript:\s*(.*)$/i))
f = "{ " + RegExp.$1;
// If the body is short, return it unchanged. If it's long, clip
// out the middle to keep the display reasonably concise - keep
// the head and tail for context, and keep a little more of the
// head than the tail, since that's usually the most easily
// recognizable portion (for the human reader).
if (f.length > 60)
f = f.substr(0, 30) + "..." + f.substr(f.length - 20);
// return what we found
return f;
}
// request our initial state
function getInitState()
{
// send the initial status request for this window
serverRequest("/webui/getState?window=" + pageName,
ServerRequest.Command,
function(req, resp) { window.onGameState(req, resp); });
}
/* ------------------------------------------------------------------------ */
/*
* Generic document-level key handler, with focus setting. This does all
* the work of genericDocKey(), plus we set the default focus control if
* focus isn't already in a key-handling control.
*
* Any document objects that don't have their own special handling for
* keystrokes at the document level can install this default handler at
* load time. This handler looks for a default focus destination, and
* establishes focus there. The default location is normally the main
* command line in the main command transcript window. This handler makes
* the standard terminal-style UI a little smoother by always sending focus
* back to the command line when the user types something with the focus
* set somewhere that doesn't accept input.
*
* Some windows (such as the top-level window) have their own focus
* management rules that differ a little from ours, so they shouldn't use
* this. Instead, they should do their focus management, then call
* genericDocKey(), which does the rest of the generic key handling besides
* the focus setting.
*/
function genericDocKeyFocus(desc)
{
// if it's a text editing key, try setting the default focus
if (isFieldKey(desc))
setDefaultFocus(desc, window);
// handle a generic document key
genericDocKey(desc);
// bubble the event
return true;
}
/*
* Handle a generic document-level keystroke. This should be called from
* each window's document key handler.
*
* First, we check to see if the key is going to an input control that has
* focus. If so, we let the key pass through without further attention, so
* that the control gets the key.
*
* If there's no input control with focus:
*
* - We try sending the key to the main window for server event handling.
* If the server has asked for an input event, we send the key as the
* requested event.
*
* - For certain keys, we suppress the default browser handling. Since our
* UI is primarily oriented around the command line, we want certain keys
* to keep their command-line meanings even between inputs. This is in
* case the the user is typing faster than we can keep up with; we don't
* want the the key to suddenly change meaning when the user thinks it's an
* editing key. In particular, the Backspace and Escape keys are common
* editing keys that would be disruptive to the UI if the browser applied
* their usual accelerator meanings of "go to the previous page" and "stop
* downloading stuff on this page".
*/
function genericDocKey(desc)
{
// if the current focus processes the key, allow the default handling
var ae = document.activeElement;
if (eleKeepsFocus(ae, desc))
return;
// check to see if the server wants keystroke events
$win().keyToServer(desc);
// this key will be processed at the document level, so filter it
switch (desc.keyName)
{
case "U+0008":
case "U+001B":
// block the default handling for these keys
preventDefault(desc.event);
break;
}
}
/*
* Determine if the given keyboard event is a "field" key. Returns true if
* this is a key that's meaningful to an input field: a regular printable
* character, or a cursor movement key (arrow, Home, End, etc).
*/
function isFieldKey(desc)
{
// get the key
var k = desc.keyName;
switch (k)
{
case "Shift":
case "Control":
case "Alt":
case "CapsLock":
case "PageUp":
case "PageDown":
case "NumLock":
case "ScrollLock":
case "Win": // Windows/Start shift keys
case "Pause":
case "F1":
case "F2":
case "F3":
case "F4":
case "F5":
case "F6":
case "F7":
case "F8":
case "F9":
case "F10":
case "F11":
case "F12":
case "U+0009": // tab
// none of these keys directly affect the field, so ignore them
return false;
case "U+0008":
case "U+001B":
// these are explicitly field keys
return true;
default:
// if the Ctrl or Alt keys are down, don't consider this a field
// key, since it's probably an accelerator or shortcut instead
if (desc.ctrlKey || desc.altKey || desc.metaKey)
return false;
// looks like a regular field key
return true;
}
}
/*
* Set the default focus. If the focus isn't currently in a control that
* accepts keyboard input, we'll move the focus to the default command
* input line, if there is one.
*/
function setDefaultFocus(desc, fromWin)
{
// check for retained focus
if (evtKeepsFocus(desc))
return;
// if we have a parent window, ask it to look for a default
var par = window.parent;
if (par && par != window)
{
// there's a parent window - pass the request up to it
par.setDefaultFocus(desc, fromWin);
}
else
{
// there's no parent; try setting the default focus to a child window
setDefaultFocusChild(desc, fromWin);
}
}
/*
* Determine if focus should say put for the current keyboard event.
*/
function evtKeepsFocus(desc)
{
// if there's no keyboard event, focus can move
if (!desc || !desc.event)
return false;
// There's a weird bug in IE where a control has focus for event
// purposes but doesn't actually have keyboard focus. When this
// happens, the actual Windows event goes to the actual focus control,
// which isn't part of the JS event bubble.
var ae = getEventTarget(desc.event);
// if the event is directed to an input control, leave it where it is
for ( ; ae ; ae = ae.parentNode)
{
// if focus is already in an input object of some kind, leave it there
if (eleKeepsFocus(ae, desc))
return true;
}
// didn't find a focus keeper
return false;
}
/*
* Determine if the given element should retain focus for the given
* keyboard event. We'll return true if the element is a focusable input
* control (an INPUT, TEXTAREA, or SELECT), or it's an and the
* event is the Return key (a return key in a hyperlink selects the
* hyperlink).
*/
function eleKeepsFocus(ele, desc)
{
// if there's no element, it doesn't keep focus
if (!ele)
return false;
// Keep focus for all events if this is an input control of some
// kind (INPUT, TEXTAREA, or SELECT).
if (ele.nodeName == "INPUT"
|| ele.nodeName == "TEXTAREA"
|| ele.nodeName == "SELECT")
return true;
// For IE, also treat any element with contentEditable=true as an
// input control.
if (BrowserInfo.ie && ele.isContentEditable)
return true;
// If this is an element, and the event is the Return key,
// keep focus in the hyperlink. Pressing Return while a hyperlink
// has focus is usually equivalent to a click in the hyperlink, so
// this event is meaningful with focus left as it is.
if (ele.nodeName == "A" && ele.href && desc && desc.keyName == "Enter")
return true;
// the event isn't a meaningful input keystroke for this control
return false;
}
/*
* Move focus to a suitable child window. When we reach the top-level
* container window, it will call this to start working back down the
* window tree to find a suitable child to set focus in.
*/
function setDefaultFocusChild(desc, fromWin)
{
// by default, do nothing; other window types can override it (notably
// the layout window)
}
/* ------------------------------------------------------------------------ */
/*
* Default document mouse-click handler: close any spring-loaded popups
* that are currently open.
*/
function popupCloser(ev)
{
// get the event
var ev = getEvent(ev);
// if there's a spring-loaded popup active, and the event isn't within
// the top popup, close the top popup
if (springPopups.length > 0
&& !isEventInObject(ev, springPopups.top().ele)
&& !isEventInObject(ev, springPopups.top().openerEle))
closePopup();
// if we have a parent window, propagate the click
if (window.parent && window.parent != window)
window.parent.popupCloser(ev);
}
/*
* Close the topmost active popup
*/
function closePopup(ele)
{
// if there's nothing to close, don't go on
if (springPopups.length == 0)
return;
// if they want to close a specific popup, and it's not the top
// one, ignore the request - this could be a queued event related
// to the event that already closed the indicated popup
if (ele && springPopups.top().ele != ele)
return;
// remove the top popup from the list
var s = springPopups.pop();
// hide the popup
s.ele.style.display = "none";
// call its 'close' method, if it has one
if (s.close)
s.close();
// if there's a default focus element, restore focus there
if (s.focusEle)
{
var fe = $(s.focusEle);
fe.focus();
if (fe.nodeName == "INPUT" && fe.type == "text")
setSelRangeInEle(fe);
}
}
/*
* Open a popup. 'ele' is the outermost html element of the popup, and
* 'close' is an optional function to call to close the popup. If
* 'closeFocusEle' is specified, it's the element where we'll set focus
* when the popup closes.
*/
function openPopup(ev, ele, close, closeFocusEle, openerEle)
{
// first make sure we don't have another popup to close
popupCloser(ev);
// add it to the popup list
springPopups.push({
ele: ele,
close: close,
focusEle: closeFocusEle,
openerEle: openerEle
});
// move the focus to the popup object
if (ele.focus)
ele.focus();
// if we haven't added our keyboard handlers, do so now
if (!ele.popupKeyHandlers)
{
// add a key handler to catch return and escape while we have focus
var k = function(desc) {
if (desc.keyName == "Enter" || desc.keyName == "U+001B")
{
closePopup(ele);
cancelBubble(desc.event);
return false;
}
return true;
};
// Also set up a focus-loss handler: if focus leaves the popup
// (and its children), and focus isn't going to the opener element,
// close the popup. Not all elements can acquire focus in the
// first place on all browsers, so it's basically a no-op on some
// browsers, but it addresses some edge cases on IE in particular.
var f = function(ev) {
var ae = document.activeElement;
if (!(ae && (isChildElement(ele, ae) || ae == openerEle)))
closePopup(ele);
};
addEventHandler(ele, "keydown", function(ev) { return $kd(ev, k); });
addEventHandler(ele, "keypress", function(ev) { return $kp(ev, k); });
addEventHandler(ele, "blur", f);
ele.popupKeyHandlers = true;
}
// don't propagate this event any further
cancelBubble(ev);
}
// active popup list
var springPopups = [];
/* ------------------------------------------------------------------------ */
/*
* Shorthand to retrieve an object by name. Call like this:
*
*. $(obj, [sub [, sub2 [...]]])
*
* If 'obj' is a string, we treat it as a path of elements separated by
* periods. Each element can be:
*
* - an object ID
*. - a class name, given in square brackets
*. - a node name (tag), given in angle brackets
*
* For example, "mainDiv.[details]" returns the first child with
* class="details" of the element with ID "mainDiv". "form3. "
* returns the first INPUT element of form3.
*
* If 'obj' is any other type, we return it as-is. This allows passing in
* an element reference without first checking its type, since it will
* simply be returned unchanged. This makes it easy to write routines that
* accept path names or references as arguments: if the argument is a path
* name, it will be translated to an element; if it's already an element,
* it will be returned as given.
*
* If any 'sub' arguments are given, they must be strings giving the same
* path notation above. We'll search for each string in turn as a child
* path of the resulting object for the previous argument. This allows
* efficient searching within a part of the DOM tree for which you already
* have the root object.
*/
function $()
{
// scan each argument
for (var obj = document.body, ac = 0 ; ac < arguments.length ; ++ac)
{
// get this argument
var arg = arguments[ac];
// if it's a string, look it up by name
if (typeof(arg) == "string")
{
// break it up into dot-delimited path elements
var path = arg.split(".");
// find each item in the path
for (var i = 0 ; i < path.length ; ++i)
{
// check the syntax for this element
var e = path[i];
if (e.match(/^\[.*\]$/))
{
// search by class name
e = e.substr(1, e.length - 2);
obj = breadthSearch(
obj, function(x) { return x.className == e; });
}
else if (e.match(/^\<.*\>$/))
{
// search by tag name
e = e.substr(1, e.length - 2);
obj = breadthSearch(
obj, function(x) { return x.nodeName == e; });
}
else
{
// search by ID
if (obj == document.body)
obj = document.getElementById(e);
else
obj = breadthSearch(
obj, function(x) { return x.id == e; });
}
// if we didn't find a match at this point, give up
if (!obj)
return null;
}
}
else if (typeof(arg) == "object")
{
// the argument is the pre-resolved object
obj = arg;
}
else
{
// invalid type
return null;
}
}
// return what we ended with
return obj;
}
/*
* Do a breadth-first search for a child matching the given condition
*/
function breadthSearch(obj, cond)
{
// first, search all direct children of 'obj'
var chi;
for (chi = obj.firstChild ; chi ; chi = chi.nextSibling)
{
if (cond(chi))
return chi;
}
// didn't find it among direct children, so search grandchildren
for (chi = obj.firstChild ; chi ; chi = chi.nextSibling)
{
// skip certain types
if (chi.nodeName == "SELECT")
continue;
// search this child's children
var gc = breadthSearch(chi, cond);
if (gc)
return gc;
}
// didn't find it
return null;
}
/* ------------------------------------------------------------------------ */
/*
* Event handler manipulation. These routines let you attach and remove
* event handler functions. An element can have any number of attached
* handlers; these coexist with one another and with any in-line "onxxx"
* handler defined in the element's HTML description.
*/
function addEventHandler(obj, eventName, func)
{
obj = $(obj);
if (obj.addEventListener)
obj.addEventListener(eventName, func, false);
else if (obj.attachEvent)
{
obj.attachEvent("on" + eventName, func);
// save it in our private list as well
if (!obj.$eventList)
obj.$eventList = [];
obj.$eventList.push([eventName, func]);
}
}
function removeEventHandler(obj, eventName, func)
{
obj = $(obj);
if (obj.removeEventListener)
obj.removeEventListener(eventName, func, false);
else if (obj.detachEvent)
{
obj.detachEvent("on" + eventName, func);
// remove it from our private list
var l = obj.$eventList;
if (l)
{
for (var i = 0 ; i < l.length ; ++i)
{
if (l[i][0] == eventName && l[i][1] == func)
{
l.splice(i, 1);
break;
}
}
}
}
}
/* ------------------------------------------------------------------------ */
/*
* Synthetic events. To fire an event, first create the event object with
* createEvent(), then send it with sendEvent().
*/
/*
* Create an event. 'type' is the class of the event object, taken from
* the list below. 'name' is the name of the event, which is the root name
* without the "on" prefix.
*
* 'params' is an object giving extra parameters for the event creation.
* These vary by event class:
*
* - bubble: event bubbles up the hierarchy (default=true)
*. - cancel: event can be canceled (default=true)
*. - view: the window in which the event occurs (default=window)
*. - detail: mouse click count, wheel count, etc (default=1)
*. - screenX: screen x coordinate (default=0)
*. - screenY: screen y coordinate (default=0)
*. - clientX: client x coordinate (default=0)
*. - clientY: client y coordinate (default=0)
*. - ctrlKey: control key setting (default=false)
*. - altKey: alt key setting (default=false)
*. - shiftKey: shift key setting (default=false)
*. - metaKey: meta key setting (default=false)
*. - button: mouse button number (default=1)
*. - relatedTarget: DOM element related to the mouse event (default=null)
*
* The event name can be one of the standard browser events, or can be a
* custom invented name. For reference, we list the most common standard
* names for the supported event classes below.
*
* The event types (classes) that we handle are:
*
* "Event": base event (blur, change, copy, cut, error, focus, input, load,
* paste, reset, resize, scroll, select, submit, onload)
*
* "MouseEvent": mouse event (click, contextmenu, dblclick, drag, drop,
* mousedown, mousemove, mouseout, mouseover, mouseup, mousewheel)
*
* For the above event types, we'll call the class-specific initializer for
* the event, using the parameters specified in 'params'. These event
* types are sufficiently cross-browser that we can support them uniformly.
* Other classes are not universally supported, so we don't provide
* specific handling for them. We'll return the event object that the
* browser gave us, but we won't initialize it.
*/
function createEvent(type, name, params)
{
// get a parameter with a default
var p = function(prop, dflt) {
return params && (prop in params) ? params[prop] : dflt;
};
// create the event
var e;
if (document.createEvent)
{
// firefox, opera, safari... basically everyone but IE
e = document.createEvent(type);
switch (type)
{
case "Event":
e.initEvent(name,
p("bubble", true),
p("cancel", true));
break;
case "MouseEvent":
e.initMouseEvent(name,
p("bubble", true),
p("cancel", true),
p("view", window),
p("detail", 1),
p("screenX", 0),
p("screenY", 0),
p("clientX", 0),
p("clientY", 0),
p("ctrlKey", false),
p("altKey", false),
p("shiftKey", false),
p("metaKey", false),
p("button", 1),
p("relatedTarget", null));
break;
default:
break;
}
}
else if (document.createEventObject)
{
// IE has to do everything its own way, of course...
e = document.createEventObject();
e.type = name;
switch (type)
{
case "MouseEvent":
e.altKey = p("altKey", false);
e.ctrlKey = p("ctrlKey", false);
e.shiftKey = p("shiftKey", false);
e.metaKey = p("metaKey", false);
e.button = p("button", 1);
e.clientX = p("clientX", 0);
e.clientY = p("clientY", 0);
e.fromElement = p("relatedTarget", null);
e.toElement = p("relatedTarget", null);
e.offsetX = 0; // $$$
e.offsetY = 0; // $$$
break;
}
}
else
{
// no event creation function availble
e = null;
}
return e;
}
/*
* Send an event
*/
function sendEvent(ele, ev)
{
if (ele.fireEvent)
{
// the IE way - set the target
ev.srcElement = ele;
// for mouseover and mouseout, set the to/from element properly
if (ev.type == "mouseover")
ev.toElement = ele;
else if (ev.type == "mouseout")
ev.fromElement = ele;
// fire the event
try
{
return ele.fireEvent("on" + ev.type, ev);
}
catch (e)
{
// IE doesn't allow synthesized events; it throws an "invalid
// argument" error if we attempt it. Fall back on manually
// calling the event handlers.
for (ev.cancelBubble = false ; ele && !ev.cancelBubble ;
ele = ele.parentNode)
{
// dispatch to our custom handlers in this element
var l = ele.$eventList;
if (l)
{
l.forEach(function(item) {
if (item[0] == ev.type)
item[1](ev);
});
}
// dispatch to the in-line handler for this element
if (("on" + ev.type) in ele)
ele["on" + ev.type](ev);
}
}
}
else if (ele.dispatchEvent)
{
// firefox, safari, opera... IE-bar
return ele.dispatchEvent(ev);
}
else
{
return false;
}
}
/* ------------------------------------------------------------------------ */
/*
* Add a keyboard event handler to an object. This sets up an event
* handler based on our cross-browser keyboard package, so you only need to
* write a single event handler for the combined keypress and keydown
* events.
*/
function addKeyHandler(obj, func, ctx)
{
// look up the element, if given by name
obj = $(obj);
// set up the keydown and keypress delegation functions
var kd = function(ev) { return $kd(ev, func, ctx); };
var kp = function(ev) { return $kp(ev, func, ctx); };
// set up a record of the function, for later removal if desired
if (!obj.keyHandlers)
obj.keyHandlers = [];
obj.keyHandlers.push({ func: func, kd: kd, kp: kp });
// add the delegation handlers
addEventHandler(obj, "keydown", kd);
addEventHandler(obj, "keypress", kp);
}
/*
* Remove a keyboard event handler
*/
function removeKeyHandler(obj, func, ctx)
{
// look up the element, if given by name
obj = $(obj);
// find the list of handlers we've defined for this element
var kh = obj.keyHandlers;
if (kh)
{
// search for the user function
for (var i = 0 ; i < kh ; ++i)
{
// check for a match to the user function
if (kh[i].func == func && kh[i].ctx == ctx)
{
// found it - remove the delegation handlers
removeEventHandler(obj, "keydown", kh[i].kd);
removeEventHandler(obj, "keypress", kh[i].kp);
// remove this element from the handler list
kh.splice(i, 1);
// no need to keep looking
break;
}
}
}
}
/* ------------------------------------------------------------------------ */
/*
* Abstract event handler object. This simplifies handling for code that's
* conditional on some external event or process completing. Use the
* whenDone() method to invoke a callback if and when the event has
* occurred: if the event has already occurred, we'll invoke the callback
* immediately, otherwise we'll queue it up to be invoked when the event
* occurs.
*
* The 'ele' and 'eventName' arguments are optional. If you provide them,
* we'll automatically link the AbsEvent to the given native browser event
* on the given element. To do this, we'll set up an event listener for
* the given event on the given element that invokes the AbsEvent's fire()
* method when the native event occurs.
*
* If you don't provide 'ele' and 'eventName', you must manually call
* fire() to trigger the AbsEvent when appropriate.
*
* There are two main uses for AbsEvent:
*
* - External events, such as browser events or Flash movie events. For
* this type of use, you can supply the 'ele' and 'eventName' arguments to
* set up a handler automatically, OR you can write your own event handler,
* which calls the AbsEvent object's fire() method when the native event
* occurs. (It might seem redundant to have AbsEvent at all for native
* events. The value of AbsEvent is that it easily lets you attach
* multiple handlers to the event, and you don't have to worry about
* whether or not the event has happened yet when you install a handler: if
* the event has already happened, the handler will be invoked
* immediately.)
*
* - Pseudo-threads. This is a long-running task that you break up into a
* series of steps, and then carry out the steps gradually using timeouts.
* The idea is to allow the UI to remain responsive while the long task is
* being executed. For this, use the startThread() method to initiate the
* task.
*/
function AbsEvent(ele, eventName)
{
// we're not done yet, and we have no callbacks queued yet
this.isDone = false;
this.cb = [];
// set up our event listener, if applicable
if (ele && eventName)
{
var ae = this;
addEventHandler(ele, eventName, function() { ae.fire(); });
}
// return the new object
return this;
}
/*
* Invoke a callback if and when the event has occurred. If the event has
* already occurred, we'll invoke the callback immediately; otherwise we'll
* enqueue it to be invoked when the event fires.
*
* If 'hurry' is true, and this is a pseudo-thread, we'll set the thread's
* step interval to zero so the thread runs to completion as soon as
* possible. This won't affect external events for obvious reasons.
*/
AbsEvent.prototype.whenDone = function(cb, hurry)
{
// check to see if the event has already fired
if (this.isDone)
{
// the event has fired already - invoke the callback immediately
cb();
}
else
{
// the event hasn't fired yet - queue the callback for later
this.cb.push(cb);
// if the caller wants the thread to finish as soon as possible,
// set the step delay interval to zero
if (hurry)
this.interval = 0;
}
};
/*
* Completion event. The caller must invoke this when our external event
* occurs.
*/
AbsEvent.prototype.fire = function()
{
// note that the event has fired
this.isDone = true;
// invoke each queued callback
for (var i = 0 ; i < this.cb.length ; ++i)
this.cb[i]();
// clear the callback list
this.cb = [];
};
/*
* Start a pseudo-thread. This sets up a long-running task to be performed
* in little chunks that run intermittently while the browser UI runs. We
* accomplish this by executing the chunks with timeouts.
*
* thread(step) is the thread function to call. We will invoke this
* periodically on a timer. 'step' starts at 0 and is incremented on each
* call. The function should perform one chunk of work and then return
* true if we should continue calling it, false if the whole task is
* finished.
*
* interval is the optional timer interval in milliseconds. If not
* supplied, we'll use a default of 25 sms.
*/
AbsEvent.prototype.startThread = function(thread, interval)
{
// reset the event
this.isDone = false;
// set the time-slice interval
this.interval = (interval ? interval : 25);
// for statistics, note the starting time
this.startTime = (new Date()).getTime();
// make the first call
this.stepThread(thread, 0);
}
AbsEvent.prototype.stepThread = function(thread, step)
{
// If the interval is zero, run the thread to completion without delay.
// Otherwise do just one step, then schedule the next step after the
// delay interval.
if (this.interval == 0)
{
// no scheduling delay - run the thread to completion
while (thread(step++)) ;
// signal the completion event
this.fire();
}
else if (thread(step++))
{
// the thread wants to continue running - schedule the next step
var e = this;
setTimeout(function() { e.stepThread(thread, step); },
this.interval);
}
else
{
// the thread is done - signal the completion event
this.fire();
}
}
/* ------------------------------------------------------------------------ */
/*
* Utility functions
*/
/*
* Get the current text selection on the page. This returns a string
* containing the text of the highlighted selection range, if any.
*/
function getSelText()
{
// try it the Netscape way
var sel = window.getSelection && window.getSelection();
if (sel)
return sel.toString();
var sel = document.getSelection && document.getSelection();
if (sel)
return sel;
// try it the IE way
sel = document.selection;
sel = sel && sel.createRange();
sel = sel && sel.text;
// return whatever we found
return sel;
}
/*
* Get the current selection range in a given element.
*/
function getSelRangeInEle(ele)
{
// if they gave us the element name, look it up
ele = $(ele);
if (!ele)
return null;
// for IE, use the selection range object for the document
if (document.selection)
{
// IE - use a TextRange object, adjusted to be element-relative
var r, r2, r3;
try
{
if (ele.nodeName == "INPUT" && ele.type.toLowerCase() == "text"
|| ele.nodeName == "TEXTAREA")
{
ele.focus();
r = document.selection.createRange();
r2 = ele.createTextRange();
r3 = ele.createTextRange();
}
else
{
r = document.selection.createRange();
r2 = r.duplicate();
r3 = r.duplicate();
try { r2.moveToElementText(ele); } catch (exc) { }
}
r2.setEndPoint('EndToEnd', r);
try { r3.moveToElementText(ele); } catch (exc) { }
// make sure it actually overlaps 'ele'
if (r.compareEndPoints('StartToEnd', r3) <= 0
&& r.compareEndPoints('EndToStart', r3) >= 0)
{
// figure the relative start and end points
var s = r2.text.length - r.text.length;
var e = s + r.text.length;
// return the range, limiting to the ele's range
return {
start: Math.max(s, 0),
end: Math.min(e, r3.text.length)
};
}
else
return null;
}
catch (exc)
{
}
}
// for other browsers, INPUT and TEXTAREA elements have a special
// way of handling this
if (ele.selectionStart || ele.selectionStart == '0')
return { start: ele.selectionStart, end: ele.selectionEnd };
// for other browsers, try the window selection range
var r = window.getSelection();
if (r)
{
// scan the subranges
for (var i = 0 ; i < r.rangeCount ; ++i)
{
// check this subrange
var sr = r.getRangeAt(i);
if (rangeIntersectsEle(sr, ele))
{
var start, end;
// set up a range encompassing the target element
var sr2 = sr.cloneRange();
sr2.setStart(ele, 0);
sr2.setEndAfter(ele);
// figure the starting point
if (sr.compareBoundaryPoints(Range.START_TO_START, sr2) <= 0)
start = 0;
else
start = sr.startOffset;
// figure the ending point
if (sr.compareBoundaryPoints(Range.END_TO_END, sr2) >= 0)
end = sr2.toString().length;
else
end = sr.endOffset;
// return the result
return { start: start, end: end };
}
}
}
// we didn't find a selection
return null;
}
function rangeIntersectsEle(range, ele)
{
// if they gave us the element name, look it up
ele = $(ele);
var r = ele.ownerDocument.createRange();
try {
r.selectNode(ele);
} catch (e) {
r.selectNodeContents(ele);
}
return range.compareBoundaryPoints(Range.END_TO_START, r) < 0
&& range.compareBoundaryPoints(Range.START_TO_END, r) > 0;
}
// Set the selection range in a given element. If the range is omitted,
// we'll select the whole contents of the element, if we can figure
// the range.
function setSelRangeInEle(ele, range)
{
// if they gave us the element name, look it up
ele = $(ele);
// if the range isn't supplied, synthesize the range to cover
// the whole element
if (!range)
{
// if it's an INPUT or TEXTAREA, use its value
if ((ele.nodeName == "INPUT" && ele.type == "text")
|| ele.nodeName == "TEXTAREA")
{
// it's a text control - use its value string as the range
range = { start: 0, end: ele.value.length };
}
else if (typeof(ele.innerText) == "string")
{
// for anything else, use its inner text length
range = { start: 0, end: ele.innerText.length };
}
else
{
// can't figure a range
range = { start: 0, end: 0 };
}
}
// check for browser variations
if (ele.createTextRange || document.body.createTextRange)
{
// IE - we have to do this indirectly through a TextRange object
var r;
if (ele.createTextRange)
r = ele.createTextRange();
else
{
r = document.body.createTextRange();
r.moveToElementText(ele);
}
r.collapse(true);
r.moveEnd('character', range.end);
r.moveStart('character', range.start);
r.select();
}
else if (ele.setSelectionRange)
{
// non-IE - there's a method that does exactly what we want
ele.setSelectionRange(range.start, range.end);
}
}
// Replace the selection in the given control with the given text
function replaceSelRange(ele, txt, selectNewText)
{
// if they gave us the element name, look it up
ele = $(ele);
// get the current selection range
var r = getSelRangeInEle(ele);
if (r)
{
// make sure we're replacing ONLY the text in this element, by
// explicitly selecting this range (the range within 'ele' might
// only have been a subset of a larger selection)
setSelRangeInEle(ele, r);
// replace the selection range with the new text
if (ele.nodeName == "INPUT" || ele.nodeName == "TEXTAREA")
{
// replace the text in the element value
ele.value = ele.value.substr(0, r.start)
+ txt
+ ele.value.substr(r.end);
}
else
{
// get the selection range object
if (window.getSelection)
{
// non-IE - get the selectionRange for the selection
var rr = window.getSelection();
if (rr)
{
// create the selectionRange
rr = rr.createRange();
// delete its contents
rr.deleteFromDocument();
// make sure that actually happened (Opera is buggy)
if (!window.getSelection().isCollapsed)
document.execCommand("Delete");
// insert the new text
if (rr.rangeCount > 0)
rr.getRangeAt(0).insertNode(
document.createTextNode(txt));
}
}
else if (document.selection)
{
// IE - replace the selection text selection
var rr = document.selection.createRange();
rr.text = txt;
}
else
return;
}
// select the new text if desired, or move the selection to the
// end of the new text if not
var range = {
start: selectNewText ? r.start : r.start + txt.length,
end: r.start + txt.length
};
setSelRangeInEle(ele, range);
}
}
/* ------------------------------------------------------------------------ */
/*
* Add a new option. 'before' is the index of the item to insert
* the new item before; null inserts at the end of the list.
*/
function addSelectOption(sel, label, val, before)
{
// create the option
var opt = new Option(label, val);
// add it at the selected location
if (before != null)
{
// insert before the given index
try
{
// try it the IE way first: use an option reference for 'before'
sel.add(opt, sel.options[before]);
}
catch (e)
{
// that didn't work - try it the standard (non-IE) way, with
// the numeric index
sel.add(opt, before);
}
}
else
{
// insert at the end of the list
try
{
// try it the IE way, with no extra argument
sel.add(opt);
}
catch (e)
{
// that didn't work, so do it the standard way, with a null index
sel.add(opt, null);
}
}
}
/*
* Add a new option, sorting it alphabetically into the existing
* list.
*/
function addSelectOptionSort(sel, label, val)
{
// search the existing list to find the insertion position
for (var i = 0 ; i < sel.length ; ++i)
{
// if the new item sorts before this item, insert here
var cur = sel.options[i].text;
if (label.toLowerCase().localeCompare(cur.toLowerCase()) < 0)
break;
}
// add the new option at the position we figured
addSelectOption(sel, label, val, i < sel.length ? i : null);
}
/*
* Set a 's current selection by value. This looks for an
* child of the with the given value, and sets the 's
* current selection index to that of the first matching . Returns
* the newly selected index if we find a match, null if not.
*/
function setSelectByValue(sel, val)
{
// find the value in the option list
var i = findSelectOptionByValue(sel, val);
// if we found it, select it
if (i != null)
sel.selectedIndex = i;
// return what we found
return i;
}
/*
* Find the select option with the given value
*/
function findSelectOptionByValue(sel, val)
{
// scan all children
for (var i = 0 ; i < sel.length ; ++i)
{
// check this option
if (sel.options[i].value == val)
return i;
}
// didn't find it
return null;
}
/* ------------------------------------------------------------------------ */
/*
* Get the event, given the event parameter - if we actually have an event
* parameter we'll just return it, otherwise we'll use the window event
* object. (This weirdness is necessary because of browser variations.)
*/
function getEvent(e)
{
return e || window.event;
}
function getEventCopy(e)
{
return getEvent(e).copy(false);
}
/*
* cancel the "bubble" for an event
*/
function cancelBubble(ev)
{
ev = getEvent(ev);
if (ev.stopPropagation)
ev.stopPropagation();
else
ev.cancelBubble = true;
}
/*
* cancel the default browser processing for an event
*/
function preventDefault(ev)
{
ev = getEvent(ev);
if (ev.preventDefault)
ev.preventDefault();
else
ev.returnValue = false;
}
/*
* Get the event target: this is the DOM element where the event occurred.
* For example, for a mouse click event, this is the DOM element at the
* mouse position at the time of the click. This routine is needed to
* account for browser variations.
*/
function getEventTarget(e)
{
// get the actual event
e = getEvent(e);
// get the target from the event object - this can be either the 'target'
// or 'srcElement' property, depending on the browser
var t = e.target || e.srcElement;
// this weirdness is a work-around for a bug in some Safari versions
if (t && t.nodeType == 3)
t = t.parentNode;
// got it
return t;
}
/*
* Get the key code from a keyboard event. Note that this returns the key
* code, not the character. The key code is useful for handling special
* keys (cursor arrows, etc). This routine is needed to smooth out browser
* variations.
*
* (Note that this function isn't needed if you're using the $kd/$kp
* functions, which is what we recommend due to their greater portability.
* This function is only useful if you're doing keypress/keydown handling
* directly.)
*/
function getEventKey(e)
{
// get the actual event
e = getEvent(e);
// get the event - this varies by browser, naturally
if (window.event)
return e.keyCode;
else
return e.which;
}
/*
* Get the mouse coordinates of an event, expressed in document-relative
* coordinates. Accounts for browser variations.
*/
function getEventCoords(e)
{
// get the actual event
e = getEvent(e);
// Some browsers use pageX/pageY, while others user clientX/clientY.
var x, y;
if (typeof(e.pageX) == 'number')
{
// We've got pageX and pageY, so this is easy - all browsers that
// provide pageX and pageY use them consistently to mean
// document-relative coordinates, so they tell us exactly what
// we want to know.
x = e.pageX;
y = e.pageY;
}
else
{
// We don't have pageX/pageY, so we'll have to fall back on clientX
// and clientY...
x = e.clientX;
y = e.clientY;
// ...which is trickier than it looks. The problem is that some
// browsers provide document-relative coordinates in these properties,
// while others provide coordinates relative to the client window.
// For the latter, we need to adjust for scrolling to get our
// desired document coordinates.
if (!(BrowserInfo.opera
|| window.ScriptEngine && ScriptEngine().indexOf('InScript') != -1
|| BrowserInfo.konqueror))
{
// Okay, it's a browser that gives us client-window-relative
// coordinates, so we have to adjust for scrolling.
var scrollPos = getScrollPos();
x += scrollPos.x;
y += scrollPos.y;
}
}
// return the x,y coordinates
return { x: x, y: y };
}
/* ------------------------------------------------------------------------ */
/*
* Get the target-relative rectangle of the event. This returns a
* rectangle structure consisting of 'x' and 'y' set to the offset WITHIN
* THE TARGET OBJECT of the mouse position in the event, and 'width' and
* 'height' set to the dimensions of the target object.
*/
function getTargetRelativeRect(e)
{
// get the event coordinates relative to the target object *
return getObjectRelativeRect(e, getEventTarget(e));
}
/*
* Get the object-relative rectangle of an event. This returns a rectangle
* structure consisting of 'x' and 'y' set to the offset within 'obj' of
* the mouse position of the event, and 'width' and 'height' set to the
* dimensions of 'obj'.
*/
function getObjectRelativeRect(e, obj)
{
// get the real event
e = getEvent(e);
// get the event coordinates, in document coordinates
var evtpos = getEventCoords(e);
// get the object rectangle
var objpos = getObjectRect(obj);
// We have the event and target element positions now, both in the
// same coordinate system (document-relative): so calculating the
// event location relative to the target element is just a matter
// of calculating the difference. Return the target-relative x
// position, the target-relative y position, the target width,
// the target height, and the target object.
return { x: evtpos.x - objpos.x, y: evtpos.y - objpos.y,
width: objpos.width, height: objpos.height,
obj: obj };
}
/*
* Is the mouse position of the event within the given object's bounds?
* This determines if the event occurred within the given object, directly
* or indirectly. ("Indirectly" within the object means that the event is
* within the object's bounds, but also within the bounds of a child of the
* object. In this case, the event target will be the innermost child
* containing the event mouse point. It's often useful to know if an event
* occurs within an object or any of its children, which merely looking at
* the target object won't tell you.)
*/
function isEventInObject(e, obj)
{
// get the object-relative event coordinates
var r = getObjectRelativeRect(e, obj);
// if x is between 0 and the width of the object, and y is
// between 0 and the height of the object, we're within the object
return (r.x >= 0
&& r.x <= r.width
&& r.y >= 0
&& r.y <= r.height);
}
/*
* Determine if 'chi' is a child of 'par'
*/
function isChildElement(par, chi)
{
for (var e = chi ; e ; e = e.parentNode)
{
if (e == par)
return true;
}
return false;
}
/* ------------------------------------------------------------------------ */
/*
* Track mouse movement to simulate a button with a custom object. The
* custom object uses styles to display normal, hover, and active button
* states. You can use either text colors/effects (like hyperlinks do) or
* different background images (for graphical buttons).
*
* To use this, set the element's onmousedown and onmouseover events to
* point to this method, passing 'this' for 'ele'.
*
* The element must have a custom class definition for its style, plus two
* extra classes. If the base class is called 'myclass', you should also
* define 'myclassH' for the hovering version, and 'myclassA' for the
* active (clicked) version. We'll automatically add the 'myclassH' or
* 'myclassA' class to the element's class list: so the full class while
* hovering is 'myclass myclassH', and while active 'myclass myclassA'.
*/
var trackButtonDown = null, trackButtonHover = null;
function trackButton(ev, ele)
{
// ignore it if we're tracking another element
if (trackButtonDown && trackButtonDown != ele)
return true;
// get the real event object
ev = getEvent(ev);
// get the object's base class
var baseClass = ele.className.split(" ")[0];
// check the event type
if (ev.type == "mousedown" && trackButtonDown == null)
{
// enter the Active state
ele.className = baseClass + " " + baseClass + "A";
// watch for mouse-up events on the document (we have to track the
// whole document, since we won't get the event in the button itself
// if the mouse is outside the element when the button is released)
trackButtonDown = ele;
addEventHandler(document, "mouseup", trackButtonUp);
// suppress text selection in IE during this tracking
addEventHandler(document, "selectstart", trackButtonSelect);
// don't let this go up to any parent controls
cancelBubble(ev);
preventDefault(ev);
return false;
}
else if (ev.type == "mouseover")
{
// enter the Hovering state, or the Active state if the
// button is down
ele.className = baseClass + " " + baseClass +
(trackButtonDown == ele ? "A" : "H");
// watch for mouse-out, if we're not already
if (!trackButtonHover || !isEventInObject(ev, trackButtonHover))
{
addEventHandler(ele, "mouseout", trackButtonOut);
trackButtonHover = ele;
}
// don't handle it further
return false;
}
// use default handling
return true;
}
function trackButtonUp(ev)
{
ev = getEvent(ev);
var ele = trackButtonDown;
// switch to the base class or Hover class, as appropriate
var baseClass = ele.className.split(" ")[0];
ele.className = baseClass +
(trackButtonHover ? " " + baseClass + "H" : "");
// we're finished with the tracking
removeEventHandler(document, "mouseup", trackButtonUp);
removeEventHandler(document, "selectstart", trackButtonSelect);
trackButtonDown = null;
}
function trackButtonSelect(ev)
{
return false;
}
function trackButtonOut(ev)
{
ev = getEvent(ev);
var ele = trackButtonHover;
// switch to the base class
ele.className = ele.className.split(" ")[0];
// we're finished with hovering for this control
removeEventHandler(ele, "mouseout", trackButtonOut);
trackButtonHover = null;
}
/* ------------------------------------------------------------------------ */
/*
* Get the current scroll position in this document (the way of getting
* this information varies by browser).
*/
function getScrollPos()
{
// try the BODY element first
var base;
if ((base = document.body) != null
&& (base.scrollLeft || base.scrollTop))
return { x: base.scrollLeft, y: base.scrollTop };
// no good; try the document element
if ((base = document.documentElement) != null
&& (base.scrollLeft || base.scrollTop))
return { x: base.scrollLeft, y: base.scrollTop };
// still couldn't find the position - use zeroes
return { x: 0, y: 0 };
}
/*
* Get the window rectangle. This returns a rectangle giving the bounds of
* the window, relative to itself. This means that the 'x' and 'y' offsets
* are always zero; the actual information returned is thus the dimensions
* of the window, as the width and height of the rectangle.
*/
function getWindowRect()
{
var wid = 1000000, ht = 1000000;
// This is one of those things that varies a lot by browser.
// Most browsers support window.innerWidth/Height, but of course
// IE has its own weird way. What's more, IE has two different
// ways, depending on version. And to make matters even worse,
// the innerWidth/Height properties aren't even consistent across
// the browsers that support them - some include the scrollbar
// width in the size, some don't. We don't want the scrollbars
// to count because they're not part of the area we can use
// for drawing. The solution seems to be to try all of the
// different methods, and use the smallest non-zero, non-null,
// non-undefined result. Each browser seems to have at least
// one way getting the area sans scrollers, which is the smallest
// of the various values we'd get.
// try innerHeight on the window (this is the most common approach,
// used by almost everyone except IE, but often includes the area
// covered by scrollbars)
var x = window.innerWidth, y = window.innerHeight;
if (typeof(x) == "number" && x > 0)
{
wid = x;
ht = y;
}
// try clientHeight on the document (this works on most versions of
// IE for Windows, and also gets us the sans-scrollbar sizes on some
// others)
x = document.documentElement.clientWidth;
y = document.documentElement.clientHeight;
if (typeof(x) == "number" && x > 0)
{
// keep the smallest so far
wid = Math.min(wid, x);
ht = Math.min(ht, y);
}
// For the width, also try the clientWidth on the body (this works on
// IE for Mac and some IE for Win, and gives us sans-scrollbar sizes
// for many others). However, DON'T use this for the height unless
// we don't have any height at all so far, since it might only tell
// us the content height, not the window height.
x = document.body.clientWidth;
y = document.body.clientHeight;
if (typeof(x) == "number" && x > 0)
{
wid = Math.min(wid, x);
if (ht == 1000000)
ht = y;
}
// return the result
return { x: 0, y: 0, width: wid, height: ht };
}
/*
* Get the size of the contents of the given window
*/
function getContentRect(win)
{
// we don't know the height or width yet
var ht = 0, wid = 0;
// get the document body element
var doc = win.document;
var body = win.document.body || win.document.documentElement;
if (!body)
return { x: 0, y: 0, width: 0, height: 0 };
// Add a probe division to find the bottom of the existing normal
// flow elements. This is necessary in cases where the document only
// has text elements (without any divisions or other block-level
// elements wrapping them), because text elements don't make position
// information available to javascript. The probe will be inserted
// just below all of the existing normal-flow content, so its top
// position gives us the height of the current normal-flow content.
var probe = doc.createElement("DIV");
body.appendChild(probe);
if (probe.offsetTop > ht)
ht = probe.offsetTop;
// done with the probe for now
body.removeChild(probe);
// Now scan the direct children of the BODY node, and note the maximum
// right and bottom coordinates. This will pick up normal-flow items
// that are actual elements (not #text), as well as floating items and
// position:absolute items that won't figure into our probe positioning.
var chi = body.childNodes;
for (var i = 0 ; i < chi.length ; ++i)
{
// get the child and check the type
var c = chi[i];
if (c.nodeType == 3)
{
// It's a #text node, which means we can't measure its bounding
// box directly. Instead, wrap the same text in a SPAN and
// measure the SPAN's width.
probe = doc.createElement("SPAN");
body.insertBefore(probe, body.firstChild);
probe.innerHTML = c.nodeValue.replace('&', '&')
.replace('<', '>');
// note the width
if (probe.offsetLeft + probe.offsetWidth > wid)
wid = probe.offsetLeft + probe.offsetWidth;
// done with the probe
body.removeChild(probe);
}
else
{
// Normal element - measure its bounding box and check to see
// if it sets a new high-water mark in width or height.
var rc = getObjectRect(chi[i]);
if (rc.x + rc.width > wid)
wid = rc.x + rc.width;
if (rc.y + rc.height > ht)
ht = rc.y + rc.height;
}
}
// return the outer boundary rectangle
return { x: 0, y: 0, width: wid, height: ht };
}
/*
* Get the bounds of an object, in document coordinates.
*/
function getObjectRect(obj)
{
// return nothing if there's no object
obj = $(obj);
if (!obj)
return null;
// IE has a special way of doing this
if (obj.getBoundingClientRect)
{
// we need to do this in a try-catch due to a somewhat mysterious
// problem that occurs with long-running Ajax requests; see
// http://bugdb.tads.org/view.php?id=134
var r = null;
try
{
r = obj.getBoundingClientRect();
}
catch (e)
{
r = { top: 0, left: 0,
right: obj.offsetWidth, bottom: obj.offsetHeight };
}
var de = document.documentElement;
var dx = de.scrollLeft, dy = de.scrollTop;
if (dx == 0 && dy == 0)
{
de = document.body;
dx = de.scrollLeft;
dy = de.scrollTop;
}
return { x: r.left + dx, y: r.top + dy,
width: r.right - r.left, height: r.bottom - r.top };
}
// get the size of the target element (in pixels)
var twid = obj.offsetWidth;
var tht = obj.offsetHeight;
// Calculate the document-relative coordinates of the (upper left corner
// of the) target element. The only information we can get at directly
// is the container-relative position, so start with that...
var tx = obj.offsetLeft;
var ty = obj.offsetTop;
// ...and now work our way up the parent chain, adding in the
// container-relative offset of each container. Keep going until
// we reach the outermost container (the Body element).
for (var par = obj.offsetParent ; par != null && par != document.body ;
par = par.offsetParent)
{
tx += par.offsetLeft;
ty += par.offsetTop;
}
// return an object representing the rectangle
return { x: tx, y: ty, width: twid, height: tht };
}
/*
* move an element to a given document-relative location
*/
function moveObject(obj, x, y)
{
// Find the containing box element (i.e., the parent element with
// position absolute, relative, or fixed. Note that IE, as always,
// has a different way of getting the computed style than the rest
// of the browsers.
var parent;
for (parent = obj.parentNode ; parent != null && parent != document ;
parent = parent.parentNode)
{
var s = parent.currentStyle
|| (document.defaultView
&& document.defaultView.getComputedStyle
&& document.defaultView.getComputedStyle(parent, ""));
if (s)
s = s.position;
if (s == "absolute" || s == "relative" || s == "fixed")
break;
}
if (parent == document)
parent = null;
// get the parent adjustments
var dx = 0, dy = 0;
if (parent)
{
var prc = getObjectRect(parent);
dx = prc.x;
dy = prc.y;
}
// move the object
if (x != null)
obj.style.left = (x - dx) + "px";
if (y != null)
obj.style.top = (y - dy) + "px";
}
/*
* Get the value for a given style of a given element. This returns the
* actual computed style, even if the element doesn't have an explicit
* setting for the style. For example, this can be used to identify the
* current font of a given element.
*
* The property name is given in CSS format: font-name, border-top, etc.
*/
function getStyle(ele, prop)
{
// if the element is given by name, get the object
ele = $(ele);
// if we're asking for 'font' on IE, we need to build the overall
// string manually
if (prop == "font")
{
return getStyle(ele, "font-style")
+ " " + getStyle(ele, "font-variant")
+ " " + getStyle(ele, "font-weight")
+ " " + getStyle(ele, "font-size")
+ "/" + getStyle(ele, "line-height")
+ " " + getStyle(ele, "font-family");
}
// check for the various ways the computed style is represented in
// different browsers
if (ele.currentStyle)
{
// try it with the name as given
var v = ele.currentStyle[prop];
// if that didn't work, we might be on IE, which requires the
// javascript-style font naming convention instead of CSS: so
// font-name becomes fontName, etc
var ieProp = prop.replace(/-([a-z])/g, function(m, lc) {
return lc.toUpperCase(); });
return ele.currentStyle[ieProp];
}
else if (window.getComputedStyle)
return document.defaultView.getComputedStyle(ele, null)
.getPropertyValue(prop);
// no style information is available in this browser, apparently
return null;
}
/* ------------------------------------------------------------------------ */
/*
* Initialize the XmlFrame. This is a hack to work around the (apparently
* well-known) Safari perma-throbber bug. The bug is that Safari animates
* its spinning wheel throbber continuously as long as there's an
* outstanding XMLHttpRequest. This is undesirable for a page that uses a
* publish/subscribe scheme for contra-flow events and requests (from the
* server to the client), because this scheme basically always has an
* outstanding request, which makes Safari throb its throbber all the time.
* At best it's visually distracting; at worst it's confusing, by making it
* look like the browser is stuck loading some last bit of content that
* never arrives.
*
* I've seen a few mentions of this bug on the Web, and some oblique
* references to a known fix, but I haven't been able to track down any
* actual details on what the fix would be. My intuition was that Safari
* probably only spins for requests in the top-level window, and
* empirically this seems to be the case. If the request comes from an
* iframe, Safari doesn't seem to spin its throbber.
*
* But how exactly do you create a request in an iframe? The key seems to
* be the location of the script where the "new XMLHttpRequest()" happens.
* In particular, if a script in an iframe creates the request object, it
* doesn't matter who uses it after that - presumably the browser is
* tagging the object with the frame where it was created, and runs the
* throbber or not accordingly.
*
* So the hack is to create an invisible iframe, and define the script that
* creates our XMLHttpRequests objects in the iframe. The iframe has the
* same domain as the main document, so we can freely call between scripts
* in the main doc and the iframe. The iframe's contents aren't even a
* separate http resource - we define them in-line by writing to the iframe
* document directly from out script here.
*/
function initXmlFrame()
{
// create an invisible iframe
var f = document.createElement("iframe");
f.id = "XmlFrame";
f.style.display = "none";
document.body.appendChild(f);
// get its document object
var d = getIFrameDoc(f);
// give it a script that sets up a window function to create a request
d.open();
d.write("