packages feed

threepenny-gui-0.3.0.0: src/Graphics/UI/driver.js

$.fn.sendvalue = function(trigger){
  $(this).each(function(){
    var self = this;
    var el = $(self);
    el.keydown(function(e){
      if(e.which == 13) {
        trigger.call(self,el.val());
        return false;
      }
      else
        return true;
    });
  });
};

$.fn.livechange = function(ms,trigger){
  $(this).each(function(){
    var self = this;
    var el = $(self);
    var last_val;
    var check = function(){
      var val = el.val();
      if(val != last_val)
        trigger.call(self);
      last_val = val;
    };
    var checker;
    var restart = function(){
      clearTimeout(checker);
      checker = setInterval(check,ms);
    };
    restart();
    el.keypress(restart).change(restart);
  });
};

(function(){

  ////////////////////////////////////////////////////////////////////////////////
  // State
  var sessionToken = null;
  var element_count = 0, el_table = {};
  var tp_enable_log = $.cookie('tp_log') == "true";
  var signal_count = 0;

  document.head = document.head || document.getElementsByTagName('head')[0];

  ////////////////////////////////////////////////////////////////////////////////
  // Logging
  window.do_logging = function(x){
    $.cookie('tp_log',x.toString());
  };

  function console_log(){
    if (tp_enable_log) { window.console.log.apply(window.console,arguments); }
  }

  ////////////////////////////////////////////////////////////////////////////////
  // Main entry point
  $(document).ready(function(){
    // initCommunicationHTTP();
    initCommunicationWebSockets();
  });

  ////////////////////////////////////////////////////////////////////////////////
  // Client-server communication
  // - GET and POST requests
  
  // Initialize communication via HTTP requests.
  function initCommunicationHTTP() {
    setTimeout(function(){
      waitForEvents();
    })
  }
  
  // Poll instruction from the server.
  function waitForEvents(){
    console_log("Polling… (%d signals so far)",signal_count);
    var data = { token: sessionToken };
    var cmd = sessionToken != null? 'poll' : 'init';
    if(cmd == 'init')
      data.info = window.location.href;
    var req = $.ajax({
      dataType: 'json',
      url: cmd,
      data: data,
      success: function(events){
        if(sessionToken == null) {
          sessionToken = req.getResponseHeader('Set-Token').match(/[0-9]+/)*1;
        }
        console_log("Running event" +(events.length>1?'s':'') +"…")
        if(events.length){
          console_log('Event list:');
          runMultipleEvents(events);
        } else {
          runEvent(events, signalEvent, function(response){
            maybeReply(response, waitForEvents);
          });
        }
      },
      error: function(reply){
        console_log("Error, waiting…");
        setTimeout(function(){
          waitForEvents();
        },5000);
      }
    });
  }

  function runMultipleEvents(events){
    if(events.length == 0) {
      return waitForEvents();
    }
    runEvent(events.shift(), signalEvent, function(response){
      maybeReply(response, function(){
        runMultipleEvents(events);
      });
    });
  }
  
  // Send an event to the server.
  function signalEvent(value) {
    signal({ Event: value}, function (){});
  }
  
  // Send a reply to the server if necessary.
  function maybeReply(response, continuation) {
    if (response != undefined) { signal(response, continuation); }
    else { continuation(); }
  }
  
  // Send response back to the server.
  function signal(signal,continuation){
    signal_count++;
    console_log('Signal: %s',JSON.stringify(signal));
    $.ajax({
      dataType: 'json',
      url:'signal',
      data: { token: sessionToken, signal: JSON.stringify(signal) },
      success: function(){
        continuation();
      },
      error: function(reply){
        console_log("Error: %o",reply);
      }
    });
  }
  
  ////////////////////////////////////////////////////////////////////////////////
  // Client-server communication
  // - WebSockets
  
  // Initialize client-server communication via WebSockets.
  function initCommunicationWebSockets() {
    var url = 'ws:' + window.location.href.toString().slice(5) + 'websocket';
    var ws  = new WebSocket(url);
    
    $(window).unload( function () {
      // Make sure that the WebSocket is closed when the browser window is closed.
      ws.close();
    });
    
    var sendEvent = function (e) {
      ws.send(JSON.stringify({ Event : e}));
    }
    var reply     = function (response) {
      if (response != undefined)
        ws.send(JSON.stringify(response));
    }
    // Send ping message in regular intervals.
    // We expect pong messages in return to keep the connection alive.
    function ping(){
      ws.send("ping");
      window.setTimeout(ping,2000);
    }
    
    ws.onopen = function (e){
      ping();
      ws.onmessage = function (msg) {
        // console_log("WebSocket message: %o",msg);
        if (msg.data != "pong")
          { runEvent(JSON.parse(msg.data), sendEvent, reply); }
      }
      ws.onclose = function (e) {
        console_log("WebSocket closed: %o", e);
      }
      ws.onerror = function (e) {
        console_log("WebSocket error: %o", e);
      }
    }
  }


  ////////////////////////////////////////////////////////////////////////////////
  // FFI - Execute and reply to commands from the server
  
  function runEvent(event,sendEvent,reply){
    // reply();      -- Continue without replying to the server.
    // reply(value); -- Send  value  back to the server.
    // sendEvent     -- Function that sends a message { Event : value } to the server.
  
    console_log("Event: %s",JSON.stringify(event));
    for(var key in event){
      switch(key){
                
      case "EmptyEl": {
        var id = event.EmptyEl;
        var el = elidToElement(id);
        // Detach child elements without deleting associated event handlers and data.
        // It is not correct to remove the child elements from the el_table
        // because they may still be present on the server side.
        $(el).contents().detach();
        reply();
        break;
      }
      case "CallDeferredFunction": {
        var call = event.CallDeferredFunction;
        var closure = call[0];
        var theFunction = eval(call[1]);
        var params = call[2];
        theFunction.apply(window, params.concat(function(){
          console_log(this);
          var args = Array.prototype.slice.call(arguments,0);
          sendEvent(closure.concat([args]));
        }));
        reply();
        break;
      }
      case "RunJSFunction": {
        eval(event.RunJSFunction);
        reply();
        break;
      }
      case "CallJSFunction": {
        var result = eval(event.CallJSFunction);
        reply({FunctionResult : result});
        break;
      }
      case "Delete": {
        event_delete(event);
        reply();
        break;
      }
      case "Debug": {
        if(window.console)
          console.log("Server debug: %o",event.Debug);
        reply();
        break;
      }
      case "GetElementsByTagName": {
        var elements = document.getElementsByTagName(event.GetElementsByTagName);
        var els = [];
        var len = elements.length;
        for(var i = 0; i < len; i++) {
          els.push({
            Element: elementToElid(elements[i])
          });
        }
        reply({ Elements: els });
        break;
      }
      case "GetElementsById": {
        // Note that this is the html ID, not the elid that is the key of the el_table.
        var ids = event.GetElementsById;
        var els = [];
        for(var i = 0; i < ids.length; i++) {
            var match = document.getElementById(ids[i]);
            if (match != null) {
                els.push({
                  Element: elementToElid(match)
                });
            }
        }
        reply({ Elements: els });
        break;
      }
      case "GetElementsByClassName": {
        var elements = document.getElementsByClassName(event.GetElementsByClassName);
        var els = [];
        var len = elements.length;
        for(var i = 0; i < len; i++) {
          els.push({
            Element: elementToElid(elements[i])
          });
        }
        reply({ Elements: els });
        break;
      }
      case "SetStyle": {
        var set = event.SetStyle;
        var id = set[0];
        var style = set[1];
        var el = elidToElement(id);
        var len = style.length;
        for(var i = 0; i < len; i++){
          el.style[style[i][0]] = style[i][1];
        }
        reply();
        break;
      }
      case "SetAttr": {
        var set   = event.SetAttr;
        var id    = set[0];
        var key   = set[1];
        var value = set[2];
        var el    = elidToElement(id);
        $(el).attr(key,value);
        reply();
        break;
      }
      case "GetValue": {
        var id = event.GetValue;
        var el = elidToElement(id);
        var value = $(el).val();
        reply({ Value: value });
        break;
      }
      case "GetValues": {
        var ids = event.GetValues;
        var len = ids.length;
        var values = [];
        for(var i = 0; i < len; i++) {
          values.push($(elidToElement(ids[i])).val());
        }
        reply({ Values: values });
        break;
      }
      case "Append": {
        var append = event.Append;
        $(elidToElement(append[0])).append($(elidToElement(append[1])));
        reply();
        break;
      }
      case "SetText": {
        var set = event.SetText;
        $(elidToElement(set[0])).text(set[1]);
        reply();
        break;
      }
      case "SetTitle": {
        document.title = event.SetTitle;
        reply();
        break;
      }
      case "SetHtml": {
        var set = event.SetHtml;
        $(elidToElement(set[0])).html(set[1]);
        reply();
        break;
      }
      case "Bind": {
        var bind        = event.Bind;
        var eventType   = bind[0];
        var handlerGuid = bind[2];
        var el = elidToElement(bind[1]);
        console_log('event type: ' + eventType);
        if(eventType == 'livechange') {
          $(el).livechange(300,function(e){
            sendEvent( handlerGuid.concat([[$(el).val()]]) );
            return true;
          });
        } else if(eventType == 'sendvalue') {
          $(el).sendvalue(function(x){
            sendEvent( handlerGuid.concat([[x]]) );
          });
        } else if(eventType.match('dragstart|dragenter|dragover|dragleave|drag|drop|dragend')) {
          $(el).bind(eventType,function(e){
            sendEvent( handlerGuid.concat([
                e.originalEvent.dataTransfer
                    ?[e.originalEvent.dataTransfer.getData("dragData")]
                    :[]])
              );
            return true;
          });
        } else if(eventType.match('mousemove')) {
          $(el).bind(eventType,function(e){
            sendEvent( handlerGuid.concat([[e.pageX.toString(), e.pageY.toString()]]) );
            return true;
          });
        } else if(eventType.match('keydown|keyup')) {
          $(el).bind(eventType,function(e){
            sendEvent( handlerGuid.concat([[e.keyCode.toString()]]) );
            return true;
          });
        } else {
          $(el).bind(eventType,function(e){
            sendEvent( handlerGuid.concat([e.which?[e.which.toString()]:[]]) );
            return true;
          });
        }
        reply();
        break;
      }
      default: reply();
      }
    }
  }

  ////////////////////////////////////////////////////////////////////////////////
  // FFI - marshaling

  // When the server creates elements, it assigns them a string "elid".  
  // This elidToElement function is used to sync the elids on the server with the 
  // elids on this client code.  Lookups on elids that do not already exist in the client
  // table are created and added automatically.
  function elidToElement(elid){
    if(elid == 'body')
      return document.body;
    else if(elid == 'head')
      return document.head;
    else if(el_table[elid]){
      return el_table[elid];
    } else {
      if(elid[0] == '*'){
        var create = elid.split(':');
        var element = document.createElement(create[1]);
        element.elid   = elid;
        el_table[elid] = element;
        return element;
      } else {
        throw "Unknown element: " + elid;
      }
    }
  }
  function deleteElementTable(elid){
    delete el_table[elid];
  }
  
  // Get/generate a elid for an element.  This function is used for cases in which the
  // element is accessed without knowing an elid from the server, such as when the 
  // element is retrieved by type or html ID attribute.  The element is then added to 
  // elid lookup table using the new elid.
  // Note: The mapping between  elids  and  DOM elements  must be bijective.
  function elementToElid(element){
    if(element.elid) {
        return element.elid;
	}
	else if (element === document.body) {
        return "body";
    }
	else if (element === document.head) {
        return "head";
    }
    else {
        var elid = "!" + element_count.toString();
        element_count++;
        element.elid   = elid;
        el_table[elid] = element;
        return elid;
    }
  }

  ////////////////////////////////////////////////////////////////////////////////
  // FFI - additional primitive functions
  
  function event_delete(event){
    var id = event.Delete;
    var el = elidToElement(id);
    // TODO: Think whether it is correct to remove element ids
    $(el).detach();
    deleteElementTable(id);
  }
  
  window.jquery_animate = function(el_id,props,duration,easing,complete){
    var el = elidToElement(JSON.parse(el_id));
    $(el).animate(JSON.parse(props),
                  duration * 1,
                  easing * 1,
                  complete);
  }

  window.jquery_scrollToBottom = function(el){
    $(el).scrollTop(el.scrollHeight);
  };

  function prim_audio_stop(audio){
    audio.pause();
    audio.currentTime = 0;
  }

  // see http://stackoverflow.com/a/9722502/403805
  CanvasRenderingContext2D.prototype.clear = 
    CanvasRenderingContext2D.prototype.clear || function (preserveTransform) {
      if (preserveTransform) {
        this.save();
        this.setTransform(1, 0, 0, 1, 0, 0);
      }

      this.clearRect(0, 0, this.canvas.width, this.canvas.height);

      if (preserveTransform) {
        this.restore();
      }           
  };

})();