whalesong/world/raw-jsworld.js
2011-07-21 15:20:59 -04:00

1401 lines
32 KiB
JavaScript

var rawJsworld = {};
// Stuff here is copy-and-pasted from Chris King's JSWorld.
//
// dyoo: as I remember, most of this code had been revised from
// Chris's original code by Ethan Cechetti, who rewrote it to
// continuation passing style during summer 2010.
(function() {
/* Type signature notation
* CPS(a b ... -> c) is used to denote
* a b ... (c -> void) -> void
*/
var Jsworld = rawJsworld;
var currentFocusedNode = false;
var doNothing = function() {};
// forEachK: CPS( array CPS(array -> void) (error -> void) -> void )
// Iterates through an array and applies f to each element using CPS
// If an error is thrown, it catches the error and calls f_error on it
var forEachK = function(a, f, f_error, k) {
var forEachHelp = function(i) {
if( i >= a.length ) {
if (k) {
return k();
} else {
return;
}
}
try {
return f(a[i], function() { return forEachHelp(i+1); });
} catch (e) {
f_error(e);
}
};
return forEachHelp(0);
};
//
// WORLD STUFFS
//
function InitialWorld() {}
var world = new InitialWorld();
var worldListeners = [];
var eventDetachers = [];
var runningBigBangs = [];
var changingWorld = false;
// Close all world computations.
Jsworld.shutdown = function() {
while(runningBigBangs.length > 0) {
var currentRecord = runningBigBangs.pop();
if (currentRecord) { currentRecord.pause(); }
}
clear_running_state();
}
function add_world_listener(listener) {
worldListeners.push(listener);
}
function remove_world_listener(listener) {
var index = worldListeners.indexOf(listener);
if (index != -1) {
worldListeners.splice(index, 1);
}
}
function clear_running_state() {
world = new InitialWorld();
worldListeners = [];
for (var i = 0; i < eventDetachers.length; i++) {
eventDetachers[i]();
}
eventDetachers = [];
changingWorld = false;
}
// If we're in the middle of a change_world, delay.
var DELAY_BEFORE_RETRY = 10;
// change_world: CPS( CPS(world -> world) -> void )
// Adjust the world, and notify all listeners.
var change_world = function(updater, k) {
// Check to see if we're in the middle of changing
// the world already. If so, put on the queue
// and exit quickly.
if (changingWorld) {
setTimeout(
function() {
change_world(updater, k)},
DELAY_BEFORE_RETRY);
return;
}
changingWorld = true;
var originalWorld = world;
var changeWorldHelp = function() {
if (world instanceof WrappedWorldWithEffects) {
var effects = world.getEffects();
forEachK(effects,
function(anEffect, k2) {
anEffect.invokeEffect(change_world, k2);
},
function (e) {
changingWorld = false;
throw e;
},
function() {
world = world.getWorld();
changeWorldHelp2();
});
} else {
changeWorldHelp2();
}
};
var changeWorldHelp2 = function() {
forEachK(worldListeners,
function(listener, k2) {
listener(world, originalWorld, k2);
},
function(e) {
changingWorld = false;
world = originalWorld;
throw e; },
function() {
changingWorld = false;
k();
});
};
try {
updater(world, function(newWorld) {
world = newWorld;
changeWorldHelp();
});
} catch(e) {
changingWorld = false;
world = originalWorld;
if (typeof(console) !== 'undefined' && console.log && e.stack) {
console.log(e.stack);
}
throw e;
}
}
Jsworld.change_world = change_world;
//
// STUFF THAT SHOULD REALLY BE IN ECMASCRIPT
//
Number.prototype.NaN0=function(){return isNaN(this)?0:this;}
function getPosition(e){
var left = 0;
var top = 0;
while (e.offsetParent){
left += e.offsetLeft + (e.currentStyle?(parseInt(e.currentStyle.borderLeftWidth)).NaN0():0);
top += e.offsetTop + (e.currentStyle?(parseInt(e.currentStyle.borderTopWidth)).NaN0():0);
e = e.offsetParent;
}
left += e.offsetLeft + (e.currentStyle?(parseInt(e.currentStyle.borderLeftWidth)).NaN0():0);
top += e.offsetTop + (e.currentStyle?(parseInt(e.currentStyle.borderTopWidth)).NaN0():0);
return {x:left, y:top};
}
Jsworld.getPosition = getPosition;
var gensym_counter = 0;
function gensym(){ return gensym_counter++;}
Jsworld.gensym = gensym;
var map = function(a1, f) {
var b = new Array(a1.length);
for (var i = 0; i < a1.length; i++) {
b[i] = f(a1[i]);
}
return b;
}
Jsworld.map = map;
var concat_map = function(a, f) {
var b = [];
for (var i = 0; i < a.length; i++) {
b = b.concat(f(a[i]));
}
return b;
}
var mapi = function(a, f) {
var b = new Array(a.length);
for (var i = 0; i < a.length; i++) {
b[i] = f(a[i], i);
}
return b;
}
Jsworld.mapi = mapi;
var fold = function(a, x, f) {
for (var i = 0; i < a.length; i++) {
x = f(a[i], x);
}
return x;
}
Jsworld.fold = fold;
function augment(o, a) {
var oo = {};
for (var e in o)
oo[e] = o[e];
for (var e in a)
oo[e] = a[e];
return oo;
}
Jsworld.augment = augment;
function assoc_cons(o, k, v) {
var oo = {};
for (var e in o)
oo[e] = o[e];
oo[k] = v;
return oo;
}
Jsworld.assoc_cons = assoc_cons;
function cons(value, array) {
return [value].concat(array);
}
Jsworld.cons = cons;
function append(array1, array2){
return array1.concat(array2);
}
Jsworld.append = append;
function array_join(array1, array2){
var joined = [];
for (var i = 0; i < array1.length; i++)
joined.push([array1[i], array2[i]]);
return joined;
}
Jsworld.array_join = array_join;
function removeq(a, value) {
for (var i = 0; i < a.length; i++)
if (a[i] === value){
return a.slice(0, i).concat(a.slice(i+1));
}
return a;
}
Jsworld.removeq = removeq;
function removef(a, value) {
for (var i = 0; i < a.length; i++)
if ( f(a[i]) ){
return a.slice(0, i).concat(a.slice(i+1));
}
return a;
}
Jsworld.removef = removef;
function filter(a, f) {
var b = [];
for (var i = 0; i < a.length; i++) {
if ( f(a[i]) ) {
b.push(a[i]);
}
}
return b;
}
Jsworld.filter = filter;
function without(obj, attrib) {
var o = {};
for (var a in obj)
if (a != attrib)
o[a] = obj[a];
return o;
}
Jsworld.without = without;
function memberq(a, x) {
for (var i = 0; i < a.length; i++)
if (a[i] === x) return true;
return false;
}
Jsworld.memberq = memberq;
function member(a, x) {
for (var i = 0; i < a.length; i++)
if (a[i] == x) return true;
return false;
}
Jsworld.member = member;
function head(a){
return a[0];
}
Jsworld.head = head;
function tail(a){
return a.slice(1, a.length);
}
Jsworld.tail = tail;
//
// DOM UPDATING STUFFS
//
// tree(N): { node: N, children: [tree(N)] }
// relation(N): { relation: 'parent', parent: N, child: N } | { relation: 'neighbor', left: N, right: N }
// relations(N): [relation(N)]
// nodes(N): [N]
// css(N): [css_node(N)]
// css_node(N): { node: N, attribs: attribs } | { className: string, attribs: attribs }
// attrib: { attrib: string, values: [string] }
// attribs: [attrib]
// treeable(nodes(N), relations(N)) = bool
/*function treeable(nodes, relations) {
// for all neighbor relations between x and y
for (var i = 0; i < relations.length; i++)
if (relations[i].relation == 'neighbor') {
var x = relations[i].left, y = relations[i].right;
// there does not exist a neighbor relation between x and z!=y or z!=x and y
for (var j = 0; j < relations.length; j++)
if (relations[j].relation === 'neighbor')
if (relations[j].left === x && relations[j].right !== y ||
relations[j].left !== x && relations[j].right === y)
return false;
}
// for all parent relations between x and y
for (var i = 0; i < relations.length; i++)
if (relations[i].relation == 'parent') {
var x = relations[i].parent, y = relations[i].child;
// there does not exist a parent relation between z!=x and y
for (var j = 0; j < relations.length; j++)
if (relations[j].relation == 'parent')
if (relations[j].parent !== x && relations[j].child === y)
return false;
}
// for all neighbor relations between x and y
for (var i = 0; i < relations.length; i++)
if (relations[i].relation == 'neighbor') {
var x = relations[i].left, y = relations[i].right;
// all parent relations between z and x or y share the same z
for (var j = 0; j < relations.length; j++)
if (relations[j].relation == 'parent')
for (var k = 0; k < relations.length; k++)
if (relations[k].relation == 'parent')
if (relations[j].child === x && relations[k].child === y &&
relations[j].parent !== relations[k].parent)
return false;
}
return true;
}*/
// node_to_tree: dom -> dom-tree
// Given a native dom node, produces the appropriate tree.
function node_to_tree(domNode) {
var result = [domNode];
for (var c = domNode.firstChild; c != null; c = c.nextSibling) {
result.push(node_to_tree(c));
}
return result;
}
Jsworld.node_to_tree = node_to_tree;
// nodes(tree(N)) = nodes(N)
function nodes(tree) {
var ret;
if (tree.node.jsworldOpaque == true) {
return [tree.node];
}
ret = [tree.node];
for (var i = 0; i < tree.children.length; i++)
ret = ret.concat(nodes(tree.children[i]));
return ret;
}
// relations(tree(N)) = relations(N)
function relations(tree) {
var ret = [];
for (var i = 0; i < tree.children.length; i++)
ret.push({ relation: 'parent',
parent: tree.node,
child: tree.children[i].node });
for (var i = 0; i < tree.children.length - 1; i++)
ret.push({ relation: 'neighbor',
left: tree.children[i].node,
right: tree.children[i + 1].node });
if (! tree.node.jsworldOpaque) {
for (var i = 0; i < tree.children.length; i++) {
ret = ret.concat(relations(tree.children[i]));
}
}
return ret;
}
var removeAllChildren = function(n) {
while (n.firstChild) {
n.removeChild(n.firstChild);
}
}
// Preorder traversal.
var preorder = function(node, f) {
f(node, function() {
var child = node.firstChild;
var nextSibling;
while (child) {
var nextSibling = child.nextSibling;
preorder(child, f);
child = nextSibling;
}
});
};
// update_dom(nodes(Node), relations(Node)) = void
function update_dom(toplevelNode, nodes, relations) {
// TODO: rewrite this to move stuff all in one go... possible? necessary?
// move all children to their proper parents
for (var i = 0; i < relations.length; i++) {
if (relations[i].relation == 'parent') {
var parent = relations[i].parent, child = relations[i].child;
if (child.parentNode !== parent) {
parent.appendChild(child);
}
}
}
// arrange siblings in proper order
// truly terrible... BUBBLE SORT
var unsorted = true;
while (unsorted) {
unsorted = false;
for (var i = 0; i < relations.length; i++) {
if (relations[i].relation == 'neighbor') {
var left = relations[i].left, right = relations[i].right;
if (! nodeEq(left.nextSibling, right)) {
left.parentNode.insertBefore(left, right)
unsorted = true;
}
}
}
}
// Finally, remove nodes that shouldn't be attached anymore.
var nodesPlus = nodes.concat([toplevelNode]);
preorder(toplevelNode, function(aNode, continueTraversalDown) {
if (aNode.jsworldOpaque) {
if (! isMemq(aNode, nodesPlus)) {
aNode.parentNode.removeChild(aNode);
}
} else {
if (! isMemq(aNode, nodesPlus)) {
aNode.parentNode.removeChild(aNode);
} else {
continueTraversalDown();
}
}
});
refresh_node_values(nodes);
}
// isMemq: X (arrayof X) -> boolean
// Produces true if any of the elements of L are nodeEq to x.
var isMemq = function(x, L) {
var i;
for (i = 0 ; i < L.length; i++) {
if (nodeEq(x, L[i])) {
return true;
}
}
return false;
};
// nodeEq: node node -> boolean
// Returns true if the two nodes should be the same.
var nodeEq = function(node1, node2) {
return (node1 && node2 && node1 === node2);
}
// camelCase: string -> string
function camelCase(name) {
return name.replace(/\-(.)/g, function(m, l){return l.toUpperCase()});
}
function set_css_attribs(node, attribs) {
for (var j = 0; j < attribs.length; j++){
node.style[camelCase(attribs[j].attrib)] = attribs[j].values.join(" ");
}
}
// isMatchingCssSelector: node css -> boolean
// Returns true if the CSS selector matches.
function isMatchingCssSelector(node, css) {
if (css.id.match(/^\./)) {
// Check to see if we match the class
return ('className' in node && member(node['className'].split(/\s+/),
css.id.substring(1)));
} else {
return ('id' in node && node.id == css.id);
}
}
function update_css(nodes, css) {
// clear CSS
for (var i = 0; i < nodes.length; i++) {
if ( !nodes[i].jsworldOpaque ) {
clearCss(nodes[i]);
}
}
// set CSS
for (var i = 0; i < css.length; i++)
if ('id' in css[i]) {
for (var j = 0; j < nodes.length; j++)
if (isMatchingCssSelector(nodes[j], css[i])) {
set_css_attribs(nodes[j], css[i].attribs);
}
}
else set_css_attribs(css[i].node, css[i].attribs);
}
var clearCss = function(node) {
// FIXME: we should not be clearing the css
// if ('style' in node)
// node.style.cssText = "";
}
// If any node cares about the world, send it in.
function refresh_node_values(nodes) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].onWorldChange) {
nodes[i].onWorldChange(world);
}
}
}
function do_redraw(world, oldWorld, toplevelNode, redraw_func, redraw_css_func, k) {
if (oldWorld instanceof InitialWorld) {
// Simple path
redraw_func(world,
function(drawn) {
var t = sexp2tree(drawn);
var ns = nodes(t);
// HACK: css before dom, due to excanvas hack.
redraw_css_func(world,
function(css) {
update_css(ns, sexp2css(css));
update_dom(toplevelNode, ns, relations(t));
k();
});
});
} else {
maintainingSelection(
function(k2) {
// For legibility, here is the non-CPS version of the same function:
/*
var oldRedraw = redraw_func(oldWorld);
var newRedraw = redraw_func(world);
var oldRedrawCss = redraw_css_func(oldWorld);
var newRedrawCss = redraw_css_func(world);
var t = sexp2tree(newRedraw);
var ns = nodes(t);
// Try to save the current selection and preserve it across
// dom updates.
if(oldRedraw !== newRedraw) {
// Kludge: update the CSS styles first.
// This is a workaround an issue with excanvas: any style change
// clears the content of the canvas, so we do this first before
// attaching the dom element.
update_css(ns, sexp2css(newRedrawCss));
update_dom(toplevelNode, ns, relations(t));
} else {
if(oldRedrawCss !== newRedrawCss) {
update_css(ns, sexp2css(newRedrawCss));
}
}
*/
redraw_func(
world,
function(newRedraw) {
redraw_css_func(
world,
function(newRedrawCss) {
var t = sexp2tree(newRedraw);
var ns = nodes(t);
// Try to save the current selection and preserve it across
// dom updates.
// Kludge: update the CSS styles first.
// This is a workaround an issue with excanvas: any style change
// clears the content of the canvas, so we do this first before
// attaching the dom element.
update_css(ns, sexp2css(newRedrawCss));
update_dom(toplevelNode, ns, relations(t));
k2();
})
})
}, k);
}
}
// maintainingSelection: (-> void) -> void
// Calls the thunk f while trying to maintain the current focused selection.
function maintainingSelection(f, k) {
var currentFocusedSelection;
if (hasCurrentFocusedSelection()) {
currentFocusedSelection = getCurrentFocusedSelection();
f(function() {
currentFocusedSelection.restore();
k();
});
} else {
f(function() { k(); });
}
}
function FocusedSelection() {
this.focused = currentFocusedNode;
this.selectionStart = currentFocusedNode.selectionStart;
this.selectionEnd = currentFocusedNode.selectionEnd;
}
// Try to restore the focus.
FocusedSelection.prototype.restore = function() {
// FIXME: if we're scrolling through, what's visible
// isn't restored yet.
if (this.focused.parentNode) {
this.focused.selectionStart = this.selectionStart;
this.focused.selectionEnd = this.selectionEnd;
this.focused.focus();
} else if (this.focused.id) {
var matching = document.getElementById(this.focused.id);
if (matching) {
matching.selectionStart = this.selectionStart;
matching.selectionEnd = this.selectionEnd;
matching.focus();
}
}
};
function hasCurrentFocusedSelection() {
return currentFocusedNode != undefined;
}
function getCurrentFocusedSelection() {
return new FocusedSelection();
}
//////////////////////////////////////////////////////////////////////
function BigBangRecord(top, world, handlerCreators, handlers, attribs) {
this.top = top;
this.world = world;
this.handlers = handlers;
this.handlerCreators = handlerCreators;
this.attribs = attribs;
}
BigBangRecord.prototype.restart = function() {
bigBang(this.top, this.world, this.handlerCreators, this.attribs);
}
BigBangRecord.prototype.pause = function() {
for(var i = 0 ; i < this.handlers.length; i++) {
if (this.handlers[i] instanceof StopWhenHandler) {
// Do nothing for now.
} else {
this.handlers[i].onUnregister(top);
}
}
};
//////////////////////////////////////////////////////////////////////
// Notes: bigBang maintains a stack of activation records; it should be possible
// to call bigBang re-entrantly.
// top: dom
// init_world: any
// handlerCreators: (Arrayof (-> handler))
// k: any -> void
function bigBang(top, init_world, handlerCreators, attribs, succ) {
// clear_running_state();
// Construct a fresh set of the handlers.
var handlers = map(handlerCreators, function(x) { return x();} );
if (runningBigBangs.length > 0) {
runningBigBangs[runningBigBangs.length - 1].pause();
}
// Create an activation record for this big-bang.
var activationRecord =
new BigBangRecord(top, init_world, handlerCreators, handlers, attribs);
runningBigBangs.push(activationRecord);
function keepRecordUpToDate(w, oldW, k2) {
activationRecord.world = w;
k2();
}
add_world_listener(keepRecordUpToDate);
// Monitor for termination and register the other handlers.
var stopWhen = new StopWhenHandler(function(w, k2) { k2(false); },
function(w, k2) { k2(w); });
for(var i = 0 ; i < handlers.length; i++) {
if (handlers[i] instanceof StopWhenHandler) {
stopWhen = handlers[i];
} else {
handlers[i].onRegister(top);
}
}
var watchForTermination = function(w, oldW, k2) {
stopWhen.test(w,
function(stop) {
if (stop) {
Jsworld.shutdown();
succ(w);
/*
stopWhen.receiver(world,
function() {
var currentRecord = runningBigBangs.pop();
if (currentRecord) { currentRecord.pause(); }
if (runningBigBangs.length > 0) {
var restartingBigBang = runningBigBangs.pop();
restartingBigBang.restart();
}
k();
});
*/
}
else { k2(); }
});
};
add_world_listener(watchForTermination);
// Finally, begin the big-bang.
copy_attribs(top, attribs);
change_world(function(w, k2) { k2(init_world); }, doNothing);
}
Jsworld.bigBang = bigBang;
// on_tick: number CPS(world -> world) -> handler
var on_tick = function(delay, tick) {
return function() {
var scheduleTick, ticker;
(new Date()).valueOf()
scheduleTick = function(t) {
ticker.watchId = setTimeout(
function() {
ticker.watchId = undefined;
var startTime = (new Date()).valueOf();
change_world(tick,
function() {
var endTime = (new Date()).valueOf();
scheduleTick(Math.max(delay - (endTime - startTime),
0));
});
},
t);
};
ticker = {
watchId: -1,
onRegister: function (top) {
scheduleTick(delay);
},
onUnregister: function (top) {
if (ticker.watchId)
clearTimeout(ticker.watchId);
}
};
return ticker;
};
}
Jsworld.on_tick = on_tick;
function on_key(press) {
return function() {
var wrappedPress = function(e) {
preventDefault(e);
stopPropagation(e);
change_world(function(w, k) { press(w, e, k); }, doNothing);
};
return {
onRegister: function(top) {
//http://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribue
$(top).attr('tabindex', 1);
$(top).focus();
attachEvent(top, 'keydown', wrappedPress);
},
onUnregister: function(top) {
detachEvent(top, 'keydown', wrappedPress);
}
};
}
}
Jsworld.on_key = on_key;
// on_draw: CPS(world -> (sexpof node)) CPS(world -> (sexpof css-style)) -> handler
function on_draw(redraw, redraw_css) {
var wrappedRedraw = function(w, k) {
redraw(w, function(newDomTree) {
checkDomSexp(newDomTree, newDomTree);
k(newDomTree);
});
}
return function() {
var drawer = {
_top: null,
_listener: function(w, oldW, k2) {
do_redraw(w, oldW, drawer._top, wrappedRedraw, redraw_css, k2);
},
onRegister: function (top) {
drawer._top = top;
add_world_listener(drawer._listener);
},
onUnregister: function (top) {
remove_world_listener(drawer._listener);
}
};
return drawer;
};
}
Jsworld.on_draw = on_draw;
function StopWhenHandler(test, receiver) {
this.test = test;
this.receiver = receiver;
}
// stop_when: CPS(world -> boolean) CPS(world -> boolean) -> handler
function stop_when(test, receiver) {
return function() {
if (receiver == undefined) {
receiver = function(w, k) { k(w); };
}
return new StopWhenHandler(test, receiver);
};
}
Jsworld.stop_when = stop_when;
function on_world_change(f) {
var listener = function(world, oldW, k) { f(world, k); };
return function() {
return {
onRegister: function (top) {
add_world_listener(listener); },
onUnregister: function (top) {
remove_world_listener(listener)}
};
};
}
Jsworld.on_world_change = on_world_change;
// Compatibility for attaching events to nodes.
function attachEvent(node, eventName, fn) {
if (node.addEventListener) {
// Mozilla
node.addEventListener(eventName, fn, false);
} else {
// IE
node.attachEvent('on' + eventName, fn, false);
}
}
var detachEvent = function(node, eventName, fn) {
if (node.addEventListener) {
// Mozilla
node.removeEventListener(eventName, fn, false);
} else {
// IE
node.detachEvent('on' + eventName, fn, false);
}
}
//
// DOM CREATION STUFFS
//
// add_ev: node string CPS(world event -> world) -> void
// Attaches a world-updating handler when the world is changed.
function add_ev(node, event, f) {
var eventHandler = function(e) { change_world(function(w, k) { f(w, e, k); },
doNothing); };
attachEvent(node, event, eventHandler);
eventDetachers.push(function() { detachEvent(node, event, eventHandler); });
}
// add_ev_after: node string CPS(world event -> world) -> void
// Attaches a world-updating handler when the world is changed, but only
// after the fired event has finished.
function add_ev_after(node, event, f) {
var eventHandler = function(e) {
setTimeout(function() { change_world(function(w, k) { f(w, e, k); },
doNothing); },
0);
};
attachEvent(node, event, eventHandler);
eventDetachers.push(function() { detachEvent(node, event, eventHandler); });
}
function addFocusTracking(node) {
attachEvent(node, "focus", function(e) {
currentFocusedNode = node; });
attachEvent(node, "blur", function(e) {
currentFocusedNode = undefined;
});
return node;
}
//
// WORLD STUFFS
//
function sexp2tree(sexp) {
if(sexp.length == undefined) return { node: sexp, children: [] };
else return { node: sexp[0], children: map(sexp.slice(1), sexp2tree) };
}
function sexp2attrib(sexp) {
return { attrib: sexp[0], values: sexp.slice(1) };
}
function sexp2css_node(sexp) {
var attribs = map(sexp.slice(1), sexp2attrib);
if (typeof sexp[0] == 'string'){
return [{ id: sexp[0], attribs: attribs }];
} else if ('length' in sexp[0]){
return map(sexp[0], function (id) { return { id: id, attribs: attribs } });
} else {
return [{ node: sexp[0], attribs: attribs }];
}
}
function sexp2css(sexp) {
return concat_map(sexp, sexp2css_node);
}
function isTextNode(n) {
return (n.nodeType == Node.TEXT_NODE);
};
function isElementNode(n) {
return (n.nodeType == Node.ELEMENT_NODE);
};
var throwDomError = function(thing, topThing) {
throw new JsworldDomError(
plt.baselib.format.format(
"Expected a non-empty array, received ~s within ~s",
[thing, topThing]),
thing);
};
// checkDomSexp: X X -> boolean
// Checks to see if thing is a DOM-sexp. If not,
// throws an object that explains why not.
function checkDomSexp(thing, topThing) {
if (! thing instanceof Array) {
throwDomError(thing, topThing);
}
if (thing.length == 0) {
throwDomError(thing, topThing);
}
// Check that the first element is a Text or an element.
if (isTextNode(thing[0])) {
if (thing.length > 1) {
throw new JsworldDomError(plt.baselib.format.format("Text node ~s can not have children",
[thing]),
thing);
}
} else if (isElementNode(thing[0])) {
for (var i = 1; i < thing.length; i++) {
checkDomSexp(thing[i], thing);
}
} else {
console.log(thing[0]);
throw new JsworldDomError(
plt.baselib.format.format(
"expected a Text or an Element, received ~s within ~s",
[thing, topThing]),
thing[0]);
}
}
function JsworldDomError(msg, elt) {
this.msg = msg;
this.elt = elt;
}
JsworldDomError.prototype.toString = function() {
return "JsworldDomError: " + this.msg;
}
//
// DOM CREATION STUFFS
//
function copy_attribs(node, attribs) {
if (attribs)
for (a in attribs) {
if (attribs.hasOwnProperty(a)) {
if (typeof attribs[a] == 'function')
add_ev(node, a, attribs[a]);
else{
node[a] = attribs[a];//eval("node."+a+"='"+attribs[a]+"'");
}
}
}
return node;
}
//
// NODE TYPES
//
function p(attribs) {
return addFocusTracking(copy_attribs(document.createElement('p'), attribs));
}
Jsworld.p = p;
function div(attribs) {
return addFocusTracking(copy_attribs(document.createElement('div'), attribs));
}
Jsworld.div = div;
// Used To Be: (world event -> world) (hashof X Y) -> domElement
// Now: CPS(world event -> world) (hashof X Y) -> domElement
function button(f, attribs) {
var n = document.createElement('button');
n.onclick = function(e) {return false;};
add_ev(n, 'click', f);
return addFocusTracking(copy_attribs(n, attribs));
}
Jsworld.button = button;
var preventDefault = function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
var stopPropagation = function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
var stopClickPropagation = function(node) {
attachEvent(node, "click",
function(e) {
stopPropagation(e);
});
return node;
}
// input: string CPS(world -> world)
function input(aType, updateF, attribs) {
aType = aType.toLowerCase();
var dispatchTable = { text : text_input,
password: text_input,
checkbox: checkbox_input
//button: button_input,
//radio: radio_input
};
if (dispatchTable[aType]) {
return (dispatchTable[aType])(aType, updateF, attribs);
}
else {
throw new Error("js-input: does not currently support type " + aType);
}
}
Jsworld.input = input;
var text_input = function(type, updateF, attribs) {
var n = document.createElement('input');
n.type = type;
var lastVal = n.value;
var onEvent = function() {
if (! n.parentNode) { return; }
setTimeout(
function() {
if (lastVal != n.value) {
lastVal = n.value;
change_world(function (w, k) {
updateF(w, n.value, k);
}, doNothing);
}
},
0);
}
attachEvent(n, "keydown", onEvent);
eventDetachers.push(function() {
detachEvent(n, "keydown", onEvent); });
attachEvent(n, "change", onEvent);
eventDetachers.push(function() {
detachEvent(n, "change", onEvent); });
return stopClickPropagation(
addFocusTracking(copy_attribs(n, attribs)));
};
var checkbox_input = function(type, updateF, attribs) {
var n = document.createElement('input');
n.type = type;
var onCheck = function(w, e, k) {
updateF(w, n.checked, k);
};
// This established the widget->world direction
add_ev_after(n, 'change', onCheck);
attachEvent(n, 'click', function(e) {
stopPropagation(e);
});
return copy_attribs(n, attribs);
};
var button_input = function(type, updateF, attribs) {
var n = document.createElement('button');
add_ev(n, 'click', function(w, e, k) { updateF(w, n.value, k); });
return addFocusTracking(copy_attribs(n, attribs));
};
function text(s, attribs) {
var result = document.createElement("div");
result.appendChild(document.createTextNode(String(s)));
result.jsworldOpaque = true;
return result;
}
Jsworld.text = text;
function select(attribs, opts, f){
var n = document.createElement('select');
for(var i = 0; i < opts.length; i++) {
n.add(option({value: opts[i]}), null);
}
n.jsworldOpaque = true;
add_ev(n, 'change', f);
var result = addFocusTracking(copy_attribs(n, attribs));
return result;
}
Jsworld.select = select;
function option(attribs){
var node = document.createElement("option");
node.text = attribs.value;
node.value = attribs.value;
return node;
}
function textarea(attribs){
return addFocusTracking(copy_attribs(document.createElement('textarea'), attribs));
}
Jsworld.textarea = textarea;
function h1(attribs){
return addFocusTracking(copy_attribs(document.createElement('h1'), attribs));
}
Jsworld.h1 = h1;
function canvas(attribs){
return addFocusTracking(copy_attribs(document.createElement('canvas'), attribs));
}
Jsworld.canvas = canvas;
function img(src, attribs) {
var n = document.createElement('img');
n.src = src;
return addFocusTracking(copy_attribs(n, attribs));
}
Jsworld.img = img;
function raw_node(node, attribs) {
return addFocusTracking(copy_attribs(node, attribs));
}
Jsworld.raw_node = raw_node;
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// Effects
// An effect is an object with an invokeEffect() method.
var WrappedWorldWithEffects = function(w, effects) {
if (w instanceof WrappedWorldWithEffects) {
this.w = w.w;
this.e = w.e.concat(effects);
} else {
this.w = w;
this.e = effects;
}
};
WrappedWorldWithEffects.prototype.getWorld = function() {
return this.w;
};
WrappedWorldWithEffects.prototype.getEffects = function() {
return this.e;
};
//////////////////////////////////////////////////////////////////////
Jsworld.with_effect = function(w, e) {
return new WrappedWorldWithEffects(w, [e]);
};
Jsworld.with_multiple_effects = function(w, effects) {
return new WrappedWorldWithEffects(w, effects);
};
Jsworld.has_effects = function(w) {
return w instanceof WrappedWorldWithEffects;
};
})();