/*jslint browser: true, unparam: true, vars: true, white: true, plusplus: true, maxerr: 50, indent: 4, forin: true */ /*global plt,MACHINE,$,EXPORTS,TreeCursor*/ (function() { "use strict"; var makePrimitiveProcedure = plt.baselib.functions.makePrimitiveProcedure; var makeClosure = plt.baselib.functions.makeClosure; var finalizeClosureCall = plt.baselib.functions.finalizeClosureCall; var PAUSE = plt.runtime.PAUSE; var isString = plt.baselib.strings.isString; var isSymbol = plt.baselib.symbols.isSymbol; var isList = plt.baselib.lists.isList; var isEmpty = plt.baselib.lists.isEmpty; var listLength = plt.baselib.lists.length; var makeList = plt.baselib.lists.makeList; var makePair = plt.baselib.lists.makePair; var makeSymbol = plt.baselib.symbols.makeSymbol; // EventHandler and the other classes here will be defined below. // We're just trying to keep jslint happy. var EventHandler, DomEventSource; // FIXME: as soon as we get real parameters, use parameters // instead. Global: defines the currently running big bang. // Parameterized around the call to bigBang. var currentBigBangRecord; var resourceStructType = MACHINE.modules['whalesong/resource/structs.rkt'].namespace['struct:resource']; var eventStructType = MACHINE.modules['whalesong/web-world/event.rkt'].namespace['struct:event']; var shallowCloneNode = function(node) { var result = node.cloneNode(false); $(result).data($(node).data()); return result; }; ////////////////////////////////////////////////////////////////////// // domNodeToArrayTree: dom -> dom-tree // Given a native dom node, produces the appropriate array tree representation var domNodeToArrayTree = function(domNode) { var result = [domNode]; var c; for (c = domNode.firstChild; c !== null; c = c.nextSibling) { result.push(domNodeToArrayTree(c)); } return result; }; var arrayTreeToDomNode = function(tree) { var result = shallowCloneNode(tree[0]); var i; for (i = 1; i < tree.length; i++) { result.appendChild(arrayTreeToDomNode(tree[i])); } return result; }; var domToArrayTreeCursor = function(dom) { var domOpenF = // To go down, just take the children. function(tree) { return tree.slice(1); }; var domCloseF = // To go back up, take the tree and reconstruct it. function(tree, children) { return [tree[0]].concat(children); }; var domAtomicF = function(tree) { return tree[0].nodeType !== 1; }; return TreeCursor.adaptTreeCursor(domNodeToArrayTree($(dom).clone(true).get(0)), domOpenF, domCloseF, domAtomicF); }; var treeText = function(tree) { var text = []; var visit = function(tree) { var i; if (tree[0].nodeType === 3) { text.push(tree[0].nodeValue); } for (i = 1; i < tree.length; i++) { visit(tree[i]); } }; visit(tree); return text.join(''); }; ////////////////////////////////////////////////////////////////////// // For the moment, we only support selection by id. var selectorMatches = function(selector, tree) { if (tree[0].nodeType === 1) { return tree[0].getAttribute('id') === selector; } else { return false; } }; var EMPTY_PENDING_ACTIONS = plt.baselib.lists.EMPTY; ////////////////////////////////////////////////////////////////////// // A MockView provides a functional interface to the DOM. It // includes a cursor to the currently focused dom, the pending // actions to perform on the actual view, and a nonce to detect // freshness of the MockView. var MockView = function(cursor, pendingActions, eventHandlers, nonce) { this.cursor = cursor; // (listof (view -> void)) this.pendingActions = pendingActions; this.eventHandlers = eventHandlers; this.nonce = nonce; }; var isMockView = plt.baselib.makeClassPredicate(MockView); MockView.prototype.toString = function() { return "<#view>"; }; MockView.prototype.getPendingActions = function() { return plt.baselib.lists.listToArray(this.pendingActions).reverse(); }; MockView.prototype.act = function(actionForCursor, actionForEventHandlers, actionForReal) { if (arguments.length !== 3) { throw new Error("act: insufficient arguments"); } return new MockView(actionForCursor(this.cursor), plt.baselib.lists.makePair(actionForReal, this.pendingActions), actionForEventHandlers(this.eventHandlers), this.nonce); }; MockView.prototype.updateFocus = function(selector) { selector = selector.toString(); return this.act( function(cursor) { var c = cursor.top(); while (true) { if (selectorMatches(selector, c.node)) { return c; } if (c.canSucc()) { c = c.succ(); } else { throw new Error("unable to find " + selector); } } }, function(eventHandlers) { return eventHandlers; }, function(view) { view.focus = document.getElementById(selector); } ); }; MockView.prototype.getText = function() { var tree = this.cursor.node; return treeText(tree); }; MockView.prototype.updateText = function(text) { return this.act( function(cursor) { return cursor.replaceNode([cursor.node[0]] .concat([[document.createTextNode(text)]])); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).text(text); } ); }; MockView.prototype.getAttr = function(name) { return this.cursor.node[0].getAttribute(name); }; MockView.prototype.updateAttr = function(name, value) { return this.act( function(cursor) { return cursor.replaceNode([$(shallowCloneNode(cursor.node[0])) .attr(name, value).get(0)] .concat(cursor.node.slice(1))); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).attr(name, value); }); }; MockView.prototype.getCss = function(name) { return $(this.cursor.node[0]).css(name); }; MockView.prototype.updateCss = function(name, value) { return this.act( function(cursor) { return cursor.replaceNode([$(shallowCloneNode(cursor.node[0])) .css(name, value).get(0)] .concat(cursor.node.slice(1))); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).css(name, value); }); }; MockView.prototype.getFormValue = function() { return $(this.cursor.node[0]).val(); }; MockView.prototype.updateFormValue = function(value) { return this.act( function(cursor) { return cursor.replaceNode([$(shallowCloneNode(cursor.node[0])) .val(value).get(0)] .concat(cursor.node.slice(1))); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).val(value); }); }; MockView.prototype.left = function() { return this.act( function(cursor) { return cursor.left(); }, function(eventHandlers) { return eventHandlers; }, function(view) { view.focus = view.focus.previousSibling; }); }; MockView.prototype.right = function() { return this.act( function(cursor) { return cursor.right(); }, function(eventHandlers) { return eventHandlers; }, function(view) { view.focus = view.focus.nextSibling; }); }; MockView.prototype.up = function() { return this.act( function(cursor) { return cursor.up(); }, function(eventHandlers) { return eventHandlers; }, function(view) { view.focus = view.focus.parentNode; }); }; MockView.prototype.down = function() { return this.act( function(cursor) { return cursor.down(); }, function(eventHandlers) { return eventHandlers; }, function(view) { view.focus = view.focus.firstChild; }); }; MockView.prototype.forward = function() { return this.act( function(cursor) { return cursor.succ(); }, function(eventHandlers) { return eventHandlers; }, function(view) { if (view.focus.firstChild) { view.focus = view.focus.firstChild; } else if (view.focus.nextSibling) { view.focus = view.focus.nextSibling; } else { while (view.focus !== view.top) { view.focus = view.focus.parentNode; if (view.focus.nextSibling) { view.focus = view.focus.nextSibling; return; } } } }); }; MockView.prototype.backward = function() { return this.act( function(cursor) { return cursor.pred(); }, function(eventHandlers) { return eventHandlers; }, function(view) { if (view.focus.previousSibling) { view.focus = view.focus.previousSibling; while (view.focus.children().length > 0) { view.focus = view.focus.firstChild; while(view.focus.nextSibling) { view.focus = view.focus.nextSibling; } } } else { view.focus = view.focus.parentNode; } }); }; var mockViewIdGensym = 0; MockView.prototype.bind = function(name, worldF) { var that = this; // HACK: every node that is bound needs to have an id. We // enforce this by mutating the node. if (! this.cursor.node[0].id) { this.cursor.node[0].id = ("__webWorldId_" + mockViewIdGensym++); } return this.act( function(cursor) { return cursor; }, function(eventHandlers) { var handler = new EventHandler(name, new DomEventSource( name, that.cursor.node[0].id), worldF); var newHandlers = eventHandlers.concat([handler]); return newHandlers; }, function(view) { // HACK: every node that is bound needs to have an id. We // enforce this by mutating the node. if (! view.focus.id) { view.focus.id = ("__webWorldId_" + mockViewIdGensym++); } var handler = new EventHandler(name, new DomEventSource( name, view.focus.id), worldF); view.addEventHandler(handler); currentBigBangRecord.startEventHandler(handler); }); }; MockView.prototype.show = function() { return this.act( function(cursor) { return cursor.replaceNode([$(shallowCloneNode(cursor.node[0])) .show().get(0)] .concat(cursor.node.slice(1))); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).show(); } ); }; MockView.prototype.hide = function() { return this.act( function(cursor) { return cursor.replaceNode([$(shallowCloneNode(cursor.node[0])) .hide().get(0)] .concat(cursor.node.slice(1))); }, function(eventHandlers) { return eventHandlers; }, function(view) { $(view.focus).hide(); } ); }; MockView.prototype.remove = function() { return this.act( function(cursor) { return cursor.deleteNode(); }, function(eventHandlers) { return eventHandlers; }, function(view) { var elt = view.focus; if (view.focus.nextSibling) { view.focus = view.focus.nextSibling; } else if (view.focus.previousSibling) { view.focus = view.focus.previousSibling; } else { view.focus = view.focus.parentNode; } $(elt).remove(); }); }; MockView.prototype.appendChild = function(domNode) { return this.act( function(cursor) { if (cursor.canDown()) { cursor = cursor.down(); while (cursor.canRight()) { cursor = cursor.right(); } return cursor.insertRight(domNodeToArrayTree(domNode)); } else { return cursor.insertDown(domNodeToArrayTree(domNode)); } }, function(eventHandlers) { return eventHandlers; }, function(view) { var clone = $(domNode).clone(true); clone.appendTo($(view.focus)); view.focus = clone.get(0); } ); }; MockView.prototype.insertRight = function(domNode) { return this.act( function(cursor) { return cursor.insertRight(domNodeToArrayTree(domNode)); }, function(eventHandlers) { return eventHandlers; }, function(view) { var clone = $(domNode).clone(true); clone.insertAfter($(view.focus)); view.focus = clone.get(0); } ); }; MockView.prototype.insertLeft = function(domNode) { return this.act( function(cursor) { return cursor.insertLeft(domNodeToArrayTree(domNode)); }, function(eventHandlers) { return eventHandlers; }, function(view) { var clone = $(domNode).clone(true); clone.insertBefore($(view.focus)); view.focus = clone.get(0); } ); }; MockView.prototype.id = function() { return this.cursor.node[0].id; }; MockView.prototype.isUpMovementOk = function() { return this.cursor.canUp(); }; MockView.prototype.isDownMovementOk = function() { return this.cursor.canDown(); }; MockView.prototype.isLeftMovementOk = function() { return this.cursor.canLeft(); }; MockView.prototype.isRightMovementOk = function() { return this.cursor.canRight(); }; MockView.prototype.isForwardMovementOk = function() { return this.cursor.canSucc(); }; MockView.prototype.isBackwardMovementOk = function() { return this.cursor.canPred(); }; ////////////////////////////////////////////////////////////////////// // A View represents a representation of the DOM tree. var View = function(top, eventHandlers) { // top: dom node this.top = top; // focus: dom node this.focus = top; this.eventHandlers = eventHandlers; }; View.prototype.toString = function() { return "#"; }; var defaultToRender = function(){}; View.prototype.initialRender = function(top) { $(top).empty(); // Special case: if this.top is an html, we merge into the // existing page. if ($(this.top).children("title").length !== 0) { $(document.head).find('title').remove(); } $(document.head).append($(this.top).children("title")); $(document.head).append($(this.top).children("link")); $(top).append(this.top); // The snip here is meant to accomodate weirdness with canvas dom // elements. cloning a canvas doesn't preserve how it draws. // However, we attach a toRender using jQuery's data(), which does // do the preservation we need. On an initial render, we walk // through all the elements and toRender them. // It may be that this will deprecate the afterAttach stuff // that I'm using earlier. ($(this.top).data('toRender') || defaultToRender)(); $('*', this.top).each( function(index, elt) { ($(elt).data('toRender') || defaultToRender).call(elt); }); }; View.prototype.addEventHandler = function(handler) { this.eventHandlers.push(handler); }; // Return a list of the event sources from the view. // fixme: may need to apply the pending actions to get the real set. View.prototype.getEventHandlers = function() { return this.eventHandlers; }; View.prototype.getMockAndResetFocus = function(nonce) { this.focus = this.top; return new MockView(domToArrayTreeCursor($(this.top).get(0)), EMPTY_PENDING_ACTIONS, this.eventHandlers.slice(0), nonce); }; var isView = plt.baselib.makeClassPredicate(View); var isResource = resourceStructType.predicate; // var resourcePath = function(r) { return resourceStructType.accessor(r, 0); }; // var resourceKey = function(r) { return resourceStructType.accessor(r, 1); }; var resourceContent = function(r) { return resourceStructType.accessor(r, 2); }; var parseStringAsHtml = function(str) { var dom = $('
').append($(str)); return dom; }; // coerseToView: (U resource View) -> View // Coerse a value into a view. var coerseToView = function(x, onSuccess, onFail) { var dom; if (isView(x)) { return onSuccess(x); } else if (isResource(x)) { try { dom = parseStringAsHtml(resourceContent(x).toString()) .css("margin", "0px") .css("padding", "0px") .css("border", "0px"); dom.children("body").css("margin", "0px"); } catch (exn1) { return onFail(exn1); } return onSuccess(new View(dom.get(0), [])); } else if (isMockView(x)) { return onSuccess(new View(arrayTreeToDomNode(x.cursor.top().node), x.eventHandlers.slice(0))); } else { try { dom = plt.baselib.format.toDomNode(x); } catch (exn2) { return onFail(exn2); } return onSuccess(new View(dom, [])); } }; var coerseToMockView = function(x, onSuccess, onFail) { var dom; if (isMockView(x)) { return onSuccess(x); } else if (isResource(x)) { try { dom = parseStringAsHtml(resourceContent(x).toString()) .css("margin", "0px") .css("padding", "0px") .css("border", "0px"); dom.children("body").css("margin", "0px"); } catch (exn1) { return onFail(exn1); } return onSuccess(new MockView(domToArrayTreeCursor(dom.get(0)), EMPTY_PENDING_ACTIONS, [], undefined)); } else { try { dom = plt.baselib.format.toDomNode(x); } catch (exn2) { return onFail(exn2); } return onSuccess(new MockView(domToArrayTreeCursor(dom), EMPTY_PENDING_ACTIONS, [], undefined)); } }; var isDomNode = function(x) { return (x.nodeType === 1); }; var coerseToDomNode = function(x, onSuccess, onFail) { var dom; if (isDomNode(x)) { return onSuccess(x); } else if (isResource(x)) { try { dom = parseStringAsHtml(resourceContent(x).toString()) .css("margin", "0px") .css("padding", "0px") .css("border", "0px"); } catch (exn1) { return onFail(exn1); } return onSuccess(dom.get(0)); } else if (isMockView(x)) { return onSuccess(arrayTreeToDomNode(x.cursor.top().node)); } else { try { dom = plt.baselib.format.toDomNode(x); } catch (exn2) { return onFail(exn2); } return onSuccess(dom); } }; ////////////////////////////////////////////////////////////////////// // // The inputs into a big bang are WorldHandlers, which configure the big // bang in terms of the initial view, the inputs, event sources, etc. // var WorldHandler = function() {}; var isWorldHandler = plt.baselib.makeClassPredicate(WorldHandler); var InitialViewHandler = function(view) { WorldHandler.call(this); // view: View this.view = view; }; InitialViewHandler.prototype = plt.baselib.heir(WorldHandler.prototype); InitialViewHandler.prototype.toString = function() { return "#"; }; var isInitialViewHandler = plt.baselib.makeClassPredicate(InitialViewHandler); var StopWhenHandler = function(stopWhen) { WorldHandler.call(this); // stopWhen: Racket procedure (World -> boolean) this.stopWhen = stopWhen; }; StopWhenHandler.prototype = plt.baselib.heir(WorldHandler.prototype); StopWhenHandler.prototype.toString = function() { return "#"; }; var isStopWhenHandler = plt.baselib.makeClassPredicate(StopWhenHandler); var ToDrawHandler = function(toDraw) { WorldHandler.call(this); // toDraw: Racket procedure (World View -> View) this.toDraw = toDraw; }; ToDrawHandler.prototype = plt.baselib.heir(WorldHandler.prototype); ToDrawHandler.prototype.toString = function() { return "#"; }; var isToDrawHandler = plt.baselib.makeClassPredicate(ToDrawHandler); // An EventHandler combines a EventSource with a racketWorldCallback. EventHandler = function(name, eventSource, racketWorldCallback) { WorldHandler.call(this); this.name = name; this.eventSource = eventSource; this.racketWorldCallback = racketWorldCallback; }; EventHandler.prototype = plt.baselib.heir(WorldHandler.prototype); EventHandler.prototype.toString = function() { return "#<" + this.name + ">"; }; var isEventHandler = plt.baselib.makeClassPredicate(EventHandler); var WithOutputToHandler = function(outputPort) { this.outputPort = outputPort; }; WithOutputToHandler.prototype = plt.baselib.heir(WorldHandler.prototype); var isWithOutputToHandler = plt.baselib.makeClassPredicate(WithOutputToHandler); ////////////////////////////////////////////////////////////////////// var find = function(handlers, pred) { var i; for (i = 0; i < handlers.length; i++) { if (pred(handlers[i])) { return handlers[i]; } } return undefined; }; var filter = function(handlers, pred) { var i, lst = []; for (i = 0; i < handlers.length; i++) { if (pred(handlers[i])) { lst.push(handlers[i]); } } return lst; }; // convert an object to an event. // At the moment, we only copy over those values which are numbers or strings. var objectToEvent = function(obj) { var key, val; var result = makeList(); // Note: for some reason, jslint is not satisfied that I check // that the object has a hasOwnProperty before I use it. I've intentionally // turned off jslint's forin check because it's breaking here: for (key in obj) { if (obj.hasOwnProperty && obj.hasOwnProperty(key)) { val = obj[key]; if (typeof(val) === 'number') { result = makePair(makeList(makeSymbol(key), plt.baselib.numbers.makeFloat(val)), result); } else if (typeof(val) === 'string') { result = makePair(makeList(makeSymbol(key), val), result); } } } return eventStructType.constructor(result); }; /* Event sources. An event source is a way to send input to a web-world program. An event source may be started or stopped. Pause and Unpause are semantically meant to be cheaper than start, stop, so that's why they're a part of this API. */ var EventSource = function() {}; EventSource.prototype.onStart = function(fireEvent) { }; EventSource.prototype.onStop = function() { }; // TickEventSource sends tick events. var TickEventSource = function(delay) { this.delay = delay; // delay in milliseconds. this.id = undefined; // either undefined, or an integer representing the // id to cancel a timeout. }; TickEventSource.prototype = plt.baselib.heir(EventSource.prototype); TickEventSource.prototype.onStart = function(fireEvent) { if (this.id === undefined) { this.id = setInterval( function(evt) { fireEvent(undefined, objectToEvent(evt)); }, this.delay); } }; TickEventSource.prototype.onStop = function() { if (this.id !== undefined) { clearInterval(this.id); this.id = undefined; } }; var MockLocationEventSource = function() { this.elt = undefined; }; MockLocationEventSource.prototype = plt.baselib.heir(EventSource.prototype); MockLocationEventSource.prototype.onStart = function(fireEvent) { if (this.elt === undefined) { var mockLocationSetter = document.createElement("div"); var latInput = document.createElement("input"); latInput.type = "text"; var latOutput = document.createElement("input"); latOutput.type = "text"; var submitButton = document.createElement("input"); submitButton.type = "button"; submitButton.value = "send lat/lng"; submitButton.onclick = function() { fireEvent(undefined, objectToEvent({ latitude: Number(latInput.value), longitude: Number(latOutput.value)})); return false; }; mockLocationSetter.style.border = "1pt solid black"; mockLocationSetter.appendChild( document.createTextNode("mock location setter")); mockLocationSetter.appendChild(latInput); mockLocationSetter.appendChild(latOutput); mockLocationSetter.appendChild(submitButton); document.body.appendChild(mockLocationSetter); this.elt = mockLocationSetter; } }; MockLocationEventSource.prototype.onStop = function() { if (this.elt !== undefined) { document.body.removeChild(this.elt); this.elt = undefined; } }; // This version really does use the geolocation object. var LocationEventSource = function() { this.id = undefined; }; LocationEventSource.prototype = plt.baselib.heir(EventSource.prototype); LocationEventSource.prototype.onStart = function(fireEvent) { var that = this; if (this.id === undefined) { var success = function(position) { if (position.hasOwnProperty && position.hasOwnProperty('coords') && position.coords.hasOwnProperty && position.coords.hasOwnProperty('latitude') && position.coords.hasOwnProperty('longitude')) { fireEvent(undefined, objectToEvent({ 'latitude' : Number(position.coords.latitude), 'longitude' : Number(position.coords.longitude) })); } }; // If we fail while trying to watch the position // using high accuracy, switch over to the coarse one. var onFailSwitchoverToCoerse = function() { navigator.geolocation.clearWatch(that.id); that.id = navigator.geolocation.watchPosition( success, fail); }; var fail = function(err) { // Quiet failure }; if (!!(navigator.geolocation)) { navigator.geolocation.getCurrentPosition(success, fail); this.id = navigator.geolocation.watchPosition( success, onFailSwitchoverToCoerse, { enableHighAccuracy : true, // Try every ten seconds maximumAge : 10000}); } } }; LocationEventSource.prototype.onStop = function() { if (this.id !== undefined) { navigator.geolocation.clearWatch(this.id); this.id = undefined; } }; // DomElementSource: string (U DOM string) -> EventSource // A DomEventSource allows DOM elements to send events over to // web-world. DomEventSource = function(type, elementOrId) { this.type = type; this.elementOrId = elementOrId; this.handler = undefined; }; DomEventSource.prototype = plt.baselib.heir(EventSource.prototype); DomEventSource.prototype.onStart = function(fireEvent) { var element = this.elementOrId; if (typeof(this.elementOrId) === 'string') { element = document.getElementById(this.elementOrId); } if (! element) { return; } if (this.handler !== undefined) { $(element).unbind(this.type, this.handler); this.handler = undefined; } this.handler = function(evt) { if (element !== undefined) { fireEvent(element, objectToEvent(evt)); } }; $(element).bind(this.type, this.handler); }; DomEventSource.prototype.onStop = function() { var element = this.elementOrId; if (typeof(this.elementOrId) === 'string') { element = document.getElementById(this.elementOrId); } if (this.handler !== undefined) { if (element !== undefined) { $(element).unbind(this.type, this.handler); } this.handler = undefined; } }; var EventQueue = function() { this.elts = []; }; EventQueue.prototype.queue = function(elt) { this.elts.push(elt); }; EventQueue.prototype.dequeue = function() { return this.elts.shift(); }; EventQueue.prototype.isEmpty = function() { return this.elts.length === 0; }; var EventQueueElement = function(who, handler, data) { this.who = who; this.handler = handler; this.data = data; }; var defaultToDraw = function(MACHINE, world, view, success, fail) { coerseToMockView(world, success, fail); }; var defaultStopWhen = function(MACHINE, world, view, success, fail) { return success(false); }; // bigBang. var bigBang = function(MACHINE, world, handlers) { var oldCurrentBigBangRecord = currentBigBangRecord; var running = true; var dispatchingEvents = false; var top = $("
").get(0); var view = (find(handlers, isInitialViewHandler) || { view : new View($('
').get(0), []) }).view; var stopWhen = (find(handlers, isStopWhenHandler) || { stopWhen: defaultStopWhen }).stopWhen; var toDraw = (find(handlers, isToDrawHandler) || {toDraw : defaultToDraw} ).toDraw; var oldOutputPort = MACHINE.params.currentOutputPort; var eventQueue = new EventQueue(); var eventHandlers = filter(handlers, isEventHandler).concat(view.getEventHandlers()); view.eventHandlers = eventHandlers; MACHINE.params.currentDisplayer(MACHINE, top); // From this point forward, redirect standard output if requested. if (find(handlers, isWithOutputToHandler)) { MACHINE.params.currentOutputPort = find(handlers, isWithOutputToHandler).outputPort; } PAUSE(function(restart) { var onCleanRestart, onMessyRestart, startEventHandlers, stopEventHandlers, startEventHandler, stopEventHandler, dispatchEventsInQueue, refreshView; onCleanRestart = function() { running = false; stopEventHandlers(); restart(function(MACHINE) { MACHINE.params.currentOutputPort = oldOutputPort; currentBigBangRecord = oldCurrentBigBangRecord; finalizeClosureCall(MACHINE, world); }); }; onMessyRestart = function(exn) { running = false; stopEventHandlers(); restart(function(MACHINE) { currentBigBangRecord = oldCurrentBigBangRecord; MACHINE.params.currentOutputPort = oldOutputPort; plt.baselib.exceptions.raise(MACHINE, exn); }); }; startEventHandlers = function() { var i; for (i = 0; i < eventHandlers.length; i++) { startEventHandler(eventHandlers[i]); } }; stopEventHandlers = function() { var i; for (i = 0; i < eventHandlers.length; i++) { stopEventHandler(eventHandlers[i]); } }; startEventHandler = function(handler) { var fireEvent = function(who) { if (! running) { return; } var args = [].slice.call(arguments, 1); eventQueue.queue(new EventQueueElement(who, handler, args)); if (! dispatchingEvents) { dispatchingEvents = true; setTimeout( function() { dispatchEventsInQueue( function() { refreshView(function() {}, onMessyRestart); }, onMessyRestart); }, 0); } }; handler.eventSource.onStart(fireEvent); }; stopEventHandler = function(handler) { handler.eventSource.onStop(); }; dispatchEventsInQueue = function(success, fail) { // Apply all the events on the queue, call toDraw, and then stop. // If the world ever satisfies stopWhen, stop immediately and quit. var nextEvent; var data; var racketWorldCallback; var mockView; dispatchingEvents = true; if(! eventQueue.isEmpty() ) { // Set up the proxy object so we can do what appear to be functional // queries. mockView = view.getMockAndResetFocus(); nextEvent = eventQueue.dequeue(); if (nextEvent.who !== undefined) { mockView = mockView.updateFocus(nextEvent.who.id); } // FIXME: deal with event data here racketWorldCallback = nextEvent.handler.racketWorldCallback; data = nextEvent.data[0]; var onGoodWorldUpdate = function(newWorld) { world = newWorld; stopWhen(MACHINE, world, mockView, function(shouldStop) { if (shouldStop) { refreshView(onCleanRestart, fail); } else { dispatchEventsInQueue(success, fail); } }, fail); }; if (plt.baselib.arity.isArityMatching(racketWorldCallback.racketArity, 3)) { racketWorldCallback(MACHINE, world, mockView, data, onGoodWorldUpdate, fail); } else { racketWorldCallback(MACHINE, world, mockView, onGoodWorldUpdate, fail); } } else { dispatchingEvents = false; success(); } }; refreshView = function(success, failure) { // Note: we create a random nonce, and watch to see if the MockView we get back // from the user came from here. If not, we have no hope to do a nice, efficient // update, and have to do it from scratch. var nonce = Math.random(); var originalMockView = view.getMockAndResetFocus(nonce); toDraw(MACHINE, world, originalMockView, function(newMockView) { if (newMockView.nonce === nonce) { var i; var actions = newMockView.getPendingActions(); for (i = 0; i < actions.length; i++) { actions[i](view); } } else { view.top = arrayTreeToDomNode(newMockView.cursor.top().node); view.initialRender(top); eventHandlers = newMockView.eventHandlers.slice(0); view.eventHandlers = eventHandlers; startEventHandlers(); } success(); }, function(err) { failure(err); }); }; currentBigBangRecord = { stop : onCleanRestart, stopWithExn : onMessyRestart, startEventHandler : startEventHandler, stopEventHandler : stopEventHandler }; view.initialRender(top); startEventHandlers(); refreshView(function() {}, onMessyRestart); }); }; var wrapFunction = function(proc) { var f = function(MACHINE) { var success = arguments[arguments.length - 2]; var fail = arguments[arguments.length - 1]; var args = [].slice.call(arguments, 1, arguments.length - 2); return plt.baselib.functions.internalCallDuringPause.apply(null, [MACHINE, proc, success, fail].concat(args)); }; f.racketArity = proc.racketArity; return f; }; var DomElementOutputPort = function(id) { this.id = id; }; DomElementOutputPort.prototype = plt.baselib.heir(plt.baselib.ports.OutputPort.prototype); DomElementOutputPort.prototype.writeDomNode = function (MACHINE, v) { $(document.getElementById(this.id)).append(v); $(v).trigger({type : 'afterAttach'}); $('*', v).trigger({type : 'afterAttach'}); }; var isAttributeList = function(x) { var children; if (isList(x) && (! isEmpty(x))){ if (isSymbol(x.first) && x.first.val === '@') { children = x.rest; while(! isEmpty(children)) { if (isList(children.first) && listLength(children.first) === 2 && isSymbol(children.first.first) && isString(children.first.rest.first)) { children = children.rest; } else { return false; } } return true; } else { return false; } } else { return false; } }; // An xexp is one of the following: // xexp :== (name (@ (key value) ...) xexp ...) // :== (name xexp ...) // :== string var isXexp = function(x) { var children; if (isString(x)) { return true; } if (isSymbol(x)) { return true; } if (isList(x) && !(isEmpty(x))) { if (isSymbol(x.first)) { children = x.rest; // Check the rest of the children. The first is special. if (isEmpty(children)) { return true; } if (isAttributeList(children.first)) { children = children.rest; } while (! (isEmpty(children))) { if (! isXexp(children.first)) { return false; } children = children.rest; } return true; } else { return false; } } return false; }; var assignAttributes = function(node, x) { var children, key, value; if (isList(x) && (! isEmpty(x))){ if (isSymbol(x.first) && x.first.val === '@') { children = x.rest; while(! isEmpty(children)) { if (isList(children.first) && listLength(children.first) === 2 && isSymbol(children.first.first) && isString(children.first.rest.first)) { key = children.first.first; value = children.first.rest.first; $(node).attr(key.val, value.toString()); children = children.rest; } else { return; } } return; } else { return; } } else { return; } }; var xexpToDom = function(x) { var children; var name; var node; if (isString(x)) { return document.createTextNode(x); } if (isSymbol(x)) { return $("
&" + x.val + ";
").get(0).firstChild; } if (isList(x) && !(isEmpty(x))) { if (isSymbol(x.first)) { name = x.first.val; node = document.createElement(name); children = x.rest; // Check the rest of the children. The first is special. if (isEmpty(children)) { return node; } if (isAttributeList(children.first)) { assignAttributes(node, children.first); children = children.rest; } while (! (isEmpty(children))) { node.appendChild(xexpToDom(children.first)); children = children.rest; } return node; } else { return false; } } return false; }; var firstLessThan = function(x, y) { return x[0] < y[0]; }; var domToXexp = function(dom) { var child, attrs, name, convertedChildren, i, attributes; if (dom.nodeType === 1) { attributes = []; attrs = plt.baselib.lists.EMPTY; name = plt.baselib.symbols.makeSymbol(dom.nodeName.toLowerCase()); child = dom.firstChild; convertedChildren = plt.baselib.lists.EMPTY; for (i = 0; i < dom.attributes.length; i++) { attributes.push([dom.attributes[i].nodeName, dom.attributes[i].nodeValue]); } attributes.sort(firstLessThan); for (i = 0; i < attributes.length; i++) { attrs = plt.baselib.lists.makePair( plt.baselib.lists.makeList(plt.baselib.symbols.makeSymbol(attributes[i][0]), attributes[i][1]), attrs); } while(child !== null) { if (child.nodeType === 1) { convertedChildren = plt.baselib.lists.makePair( domToXexp(child), convertedChildren); } else if (child.nodeType === 3) { convertedChildren = plt.baselib.lists.makePair( domToXexp(child), convertedChildren); } // Ignore other types. child = child.nextSibling; } if (attrs === plt.baselib.lists.EMPTY) { return plt.baselib.lists.makePair( name, plt.baselib.lists.reverse(convertedChildren)); } else { return plt.baselib.lists.makePair( name, plt.baselib.lists.makePair( plt.baselib.lists.makePair(plt.baselib.symbols.makeSymbol("@"), attrs), plt.baselib.lists.reverse(convertedChildren))); } } else if (dom.nodeType === 3) { return dom.nodeValue; } else { // If we can't convert it, return false. return false; } }; ////////////////////////////////////////////////////////////////////// var checkReal = plt.baselib.check.checkReal; var checkString = plt.baselib.check.checkString; var checkSymbolOrString = plt.baselib.check.checkSymbolOrString; var checkProcedure = plt.baselib.check.checkProcedure; var checkWorldHandler = plt.baselib.check.makeCheckArgumentType( isWorldHandler, 'world handler'); var checkMockView = plt.baselib.check.makeCheckArgumentType( isMockView, 'view'); var checkSelector = plt.baselib.check.makeCheckArgumentType( isString, 'selector'); var checkXexp = plt.baselib.check.makeCheckArgumentType( isXexp, 'xexp'); EXPORTS['big-bang'] = makeClosure( 'big-bang', plt.baselib.arity.makeArityAtLeast(1), function(MACHINE) { var world = MACHINE.e[MACHINE.e.length - 1]; var handlers = []; var i; for (i = 1; i < MACHINE.a; i++) { handlers.push(checkWorldHandler(MACHINE, 'big-bang', i)); } return bigBang(MACHINE, world, handlers); }); EXPORTS['initial-view'] = makeClosure( 'initial-view', 1, function(MACHINE) { var viewable = MACHINE.e[MACHINE.e.length - 1]; PAUSE(function(restart) { coerseToView(viewable, function(v) { restart(function(MACHINE) { finalizeClosureCall(MACHINE, new InitialViewHandler(v)); }); }, function(exn) { restart(function(MACHINE) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to translate ~s to view: ~a", [viewable, exn.message]))); }); }); }); }); EXPORTS['view?'] = makePrimitiveProcedure( 'view?', 1, function(M) { return isMockView(M.e[M.e.length - 1]); }); EXPORTS['->view'] = makeClosure( '->view', 1, function(MACHINE) { var viewable = MACHINE.e[MACHINE.e.length - 1]; PAUSE(function(restart) { coerseToMockView(viewable, function(v) { restart(function(MACHINE) { finalizeClosureCall(MACHINE, v); }); }, function(exn) { restart(function(MACHINE) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to translate ~s to view: ~a", [viewable, exn.message]))); }); }); }); }); EXPORTS['stop-when'] = makePrimitiveProcedure( 'stop-when', 1, function(MACHINE) { var stopWhen = wrapFunction(checkProcedure(MACHINE, 'stop-when', 0)); return new StopWhenHandler(stopWhen); }); EXPORTS['to-draw'] = makePrimitiveProcedure( 'to-draw', 1, function(MACHINE) { var toDraw = wrapFunction(checkProcedure(MACHINE, 'to-draw', 0)); var coersingToMockView = function(MACHINE, world, view, success, fail) { return toDraw(MACHINE, world, view, function(v) { coerseToMockView(v, success, fail); }, fail); }; return new ToDrawHandler(coersingToMockView); }); EXPORTS['on-tick'] = makePrimitiveProcedure( 'on-tick', plt.baselib.lists.makeList(1, 2), function(MACHINE) { var onTick = wrapFunction(checkProcedure(MACHINE, 'on-tick', 0)); var delay = Math.floor(1000/28); if (MACHINE.a === 2) { delay = Math.floor(plt.baselib.numbers.toFixnum(checkReal(MACHINE, 'on-tick', 1)) * 1000); } return new EventHandler('on-tick', new TickEventSource(delay), onTick); }); EXPORTS['view-focus?'] = makePrimitiveProcedure( 'view-focus?', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-focus', 0); var selector = checkSelector(MACHINE, 'view-focus', 1); try { view.updateFocus(selector); return true; } catch (e) { return false; } }); EXPORTS['view-focus'] = makePrimitiveProcedure( 'view-focus', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-focus', 0); var selector = checkSelector(MACHINE, 'view-focus', 1); try { return view.updateFocus(selector); } catch (e) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to focus to ~s: ~s", [selector, e.message]))); } }); EXPORTS['view-left'] = makePrimitiveProcedure( 'view-left', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-left', 0); return view.left(); }); EXPORTS['view-right'] = makePrimitiveProcedure( 'view-right', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-right', 0); return view.right(); }); EXPORTS['view-up'] = makePrimitiveProcedure( 'view-up', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-up', 0); return view.up(); }); EXPORTS['view-down'] = makePrimitiveProcedure( 'view-down', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-down', 0); return view.down(); }); EXPORTS['view-forward'] = makePrimitiveProcedure( 'view-forward', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-forward', 0); return view.forward(); }); EXPORTS['view-backward'] = makePrimitiveProcedure( 'view-backward', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-backward', 0); return view.backward(); }); EXPORTS['view-left?'] = makePrimitiveProcedure( 'view-left?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-left?', 0); return view.isLeftMovementOk(); }); EXPORTS['view-right?'] = makePrimitiveProcedure( 'view-right?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-right?', 0); return view.isRightMovementOk(); }); EXPORTS['view-up?'] = makePrimitiveProcedure( 'view-up?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-up?', 0); return view.isUpMovementOk(); }); EXPORTS['view-down?'] = makePrimitiveProcedure( 'view-down?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-down?', 0); return view.isDownMovementOk(); }); EXPORTS['view-forward?'] = makePrimitiveProcedure( 'view-down?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-forward?', 0); return view.isForwardMovementOk(); }); EXPORTS['view-backward?'] = makePrimitiveProcedure( 'view-backward?', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-backward?', 0); return view.isBackwardMovementOk(); }); EXPORTS['view-text'] = makePrimitiveProcedure( 'view-text', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-text', 0); return view.getText(); }); EXPORTS['update-view-text'] = makePrimitiveProcedure( 'update-view-text', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'update-view-text', 0); var text = plt.baselib.format.toDisplayedString(MACHINE.e[MACHINE.e.length - 2]); return view.updateText(text); }); EXPORTS['view-attr'] = makePrimitiveProcedure( 'view-attr', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-attr', 0); var name = checkSymbolOrString(MACHINE, 'view-attr', 1).toString(); return view.getAttr(name); }); EXPORTS['update-view-attr'] = makePrimitiveProcedure( 'update-view-attr', 3, function(MACHINE) { var view = checkMockView(MACHINE, 'update-view-attr', 0); var name = checkSymbolOrString(MACHINE, 'update-view-attr', 1).toString(); var value = checkSymbolOrString(MACHINE, 'update-view-attr', 2).toString(); return view.updateAttr(name, value); }); EXPORTS['view-css'] = makePrimitiveProcedure( 'view-css', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-css', 0); var name = checkSymbolOrString(MACHINE, 'view-css', 1).toString(); return view.getCss(name); }); EXPORTS['update-view-css'] = makePrimitiveProcedure( 'update-view-css', 3, function(MACHINE) { var view = checkMockView(MACHINE, 'update-view-css', 0); var name = checkSymbolOrString(MACHINE, 'update-view-css', 1).toString(); var value = checkSymbolOrString(MACHINE, 'update-view-css', 2).toString(); return view.updateCss(name, value); }); EXPORTS['view-bind'] = makePrimitiveProcedure( 'view-bind', 3, function(MACHINE) { var view = checkMockView(MACHINE, 'view-bind', 0); var name = checkSymbolOrString(MACHINE, 'view-bind', 1); var worldF = wrapFunction(checkProcedure(MACHINE, 'view-bind', 2)); return view.bind(name, worldF); }); EXPORTS['view-form-value'] = makePrimitiveProcedure( 'view-form-value', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-form-value', 0); return view.getFormValue(); }); EXPORTS['update-view-form-value'] = makePrimitiveProcedure( 'update-view-form-value', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'update-view-form-value', 0); var value = checkSymbolOrString(MACHINE, 'update-view-form-value', 1).toString(); return view.updateFormValue(value); }); EXPORTS['view-show'] = makePrimitiveProcedure( 'view-show', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-show', 0); return view.show(); }); EXPORTS['view-hide'] = makePrimitiveProcedure( 'view-hide', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-hide', 0); return view.hide(); }); EXPORTS['view-remove'] = makePrimitiveProcedure( 'view-remove', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-remove', 0); return view.remove(); }); EXPORTS['view-append-child'] = makeClosure( 'view-append-child', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-append-child', 0); var x = MACHINE.e[MACHINE.e.length - 2]; PAUSE(function(restart) { coerseToDomNode(x, function(dom) { restart(function(MACHINE) { var updatedView = view.appendChild(dom); finalizeClosureCall(MACHINE, updatedView); }); }, function(err) { restart(function(MACHINE) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to translate ~s to dom node: ~a", [x, err.message]))); }); }); }); }); EXPORTS['view-insert-right'] = makeClosure( 'view-insert-right', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-insert-right', 0); var x = MACHINE.e[MACHINE.e.length - 2]; PAUSE(function(restart) { coerseToDomNode(x, function(dom) { restart(function(MACHINE) { var updatedView = view.insertRight(dom); finalizeClosureCall(MACHINE, updatedView); }); }, function(err) { restart(function(MACHINE) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to translate ~s to dom node: ~a", [x, err.message]))); }); }); }); }); EXPORTS['view-insert-left'] = makeClosure( 'view-insert-left', 2, function(MACHINE) { var view = checkMockView(MACHINE, 'view-insert-left', 0); var x = MACHINE.e[MACHINE.e.length - 2]; PAUSE(function(restart) { coerseToDomNode(x, function(dom) { restart(function(MACHINE) { var updatedView = view.insertLeft(dom); finalizeClosureCall(MACHINE, updatedView); }); }, function(err) { restart(function(MACHINE) { plt.baselib.exceptions.raise( MACHINE, new Error(plt.baselib.format.format( "unable to translate ~s to dom node: ~a", [x, err.message]))); }); }); }); }); EXPORTS['view-id'] = makePrimitiveProcedure( 'view-id', 1, function(MACHINE) { var view = checkMockView(MACHINE, 'view-hide', 0); return view.id(); }); EXPORTS['on-location-change'] = makePrimitiveProcedure( 'on-location-change', 1, function(MACHINE) { var onChange = wrapFunction(checkProcedure(MACHINE, 'on-location-change', 0)); return new EventHandler('on-location-change', new LocationEventSource(), onChange); }); EXPORTS['on-mock-location-change'] = makePrimitiveProcedure( 'on-mock-location-change', 1, function(MACHINE) { var onChange = wrapFunction(checkProcedure(MACHINE, 'on-mock-location-change', 0)); return new EventHandler('on-mock-location-change', new MockLocationEventSource(), onChange); }); EXPORTS['open-output-element'] = makePrimitiveProcedure( 'open-output-element', 1, function(MACHINE) { var id = checkString(MACHINE, 'open-output-element', 0); return new DomElementOutputPort(id.toString()); }); EXPORTS['xexp?'] = makePrimitiveProcedure( 'xexp?', 1, function(MACHINE) { return isXexp(MACHINE.e[MACHINE.e.length - 1]); }); EXPORTS['xexp->dom'] = makePrimitiveProcedure( 'xexp->dom', 1, function(MACHINE) { var xexp = checkXexp(MACHINE, 'xexp->dom', 0); return xexpToDom(xexp); }); EXPORTS['view->xexp'] = makePrimitiveProcedure( 'view->xexp', 1, function(MACHINE) { var mockView = checkMockView(MACHINE, 'view-hide', 0); var domNode = arrayTreeToDomNode(mockView.cursor.top().node); return domToXexp(domNode); }); ////////////////////////////////////////////////////////////////////// }());