Split service component from widgets and introduce
host->plugin communication

Change-Id: I2377059dfc30c196a5b24d331fe60f0310694ba1
diff --git a/Changes b/Changes
index 628728f..08878a3 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.37 2019-12-09
+0.37 2019-12-11
         - Removed deprecated 'kalamar_test_port' helper.
         - Separated KalamarHelpers and KalamarPages.
         - Renamed 'doc_link_to' to 'embedded_link_to'
@@ -20,6 +20,8 @@
         - Added state object.
         - Added toggle button to buttongroup.
         - Added pipe object to implement KoralPipes.
+        - Separated "service" from "widget" plugin embeddings.
+        - Implemented preliminary host->plugin communication.
 
 0.36 2019-09-19
         - Rename all cookies to be independent
diff --git a/dev/demo/plugin-client.html b/dev/demo/plugin-client.html
index 2dac29b..b0b477d 100644
--- a/dev/demo/plugin-client.html
+++ b/dev/demo/plugin-client.html
@@ -32,7 +32,13 @@
           });
         };
       };
-    </script>
+
+      function pluginit (p) {
+        p.onMessage = function(msg) {
+          console.log("State changed to", msg.key, msg.value);
+        };
+      };
+      </script>
     <ul>
       <li><a onclick="KorAPlugin.log(333, 'Huhu!')">Send log!</a></li>
       <li><a onclick="KorAPlugin.resize()">Resize</a></li>
diff --git a/dev/demo/plugin-serverdemo.js b/dev/demo/plugin-serverdemo.js
index b0b2e2a..eb91f14 100644
--- a/dev/demo/plugin-serverdemo.js
+++ b/dev/demo/plugin-serverdemo.js
@@ -13,6 +13,9 @@
  
     //Load Plugin Server first 
     KorAP.Plugin = pluginClass.create();
+
+    // Add services container to head
+    document.head.appendChild(KorAP.Plugin.element());
     
     //Register result plugin
     KorAP.Plugin.register({
@@ -27,6 +30,14 @@
            'action' : 'addWidget',
            'template' : 'http://localhost:3003/demo/plugin-client.html',
          }
+       },{
+         'panel' : 'result',
+         'title' : 'Glemm',
+         'onClick' : {
+           'action' : 'toggle',
+           'state' : 'glemm',
+           'template' : 'http://localhost:3003/demo/plugin-client.html',
+         }
        }]
      }); 
 
diff --git a/dev/js/spec/pluginSpec.js b/dev/js/spec/pluginSpec.js
index 31ee790..41589ec 100644
--- a/dev/js/spec/pluginSpec.js
+++ b/dev/js/spec/pluginSpec.js
@@ -1,4 +1,4 @@
-define(['plugin/server','plugin/widget','panel', 'panel/query', 'panel/result'], function (pluginServerClass, widgetClass, panelClass, queryPanelClass, resultPanelClass) {
+define(['plugin/server','plugin/widget','panel', 'panel/query', 'panel/result', 'plugin/service'], function (pluginServerClass, widgetClass, panelClass, queryPanelClass, resultPanelClass, serviceClass) {
 
   describe('KorAP.Plugin.Server', function () {
 
@@ -27,6 +27,29 @@
       manager.destroy();
     });
 
+    it('should add a service', function () {
+      var manager = pluginServerClass.create();
+
+      var e = manager.element();
+
+      document.body.appendChild(e);
+
+      expect(document.getElementById("services")).toBeTruthy();
+      
+      expect(e.getAttribute("id")).toBe("services");
+      expect(e.children.length).toBe(0);
+
+      var id = manager.addService('Example 1', 'about:blank');
+      expect(id).toMatch(/^id-/);
+
+      expect(e.children.length).toBe(1);
+
+      manager.destroy();
+
+      expect(document.getElementById("services")).toBeFalsy();
+
+    });
+
     it('should close a widget', function () {
       var manager = pluginServerClass.create();
       var panel = panelClass.create();
@@ -39,7 +62,8 @@
 
       expect(panelE.getElementsByClassName('view').length).toEqual(1);
 
-      var widget = manager.widget(id);
+      var widget = manager.service(id);
+      expect(widget.isWidget).toBeTruthy();
       widget.close();
 
       expect(panelE.getElementsByClassName('view').length).toEqual(0);
@@ -131,7 +155,7 @@
   
   describe('KorAP.Plugin.Widget', function () {
     it('should be initializable', function () {
-      expect(function () { widgetClass.create() }).toThrow(new Error("Widget not well defined"));
+      expect(function () { widgetClass.create() }).toThrow(new Error("Service not well defined"));
 
       widget = widgetClass.create("Test", "https://example", 56);
       expect(widget).toBeTruthy();
@@ -180,6 +204,30 @@
     });
   });
 
+  describe('KorAP.Plugin.Service', function () {
+    it('should be initializable', function () {
+      expect(function () { serviceClass.create() }).toThrow(new Error("Service not well defined"));
+
+      let service = serviceClass.create("Test", "https://example", 56);
+      expect(service).toBeTruthy();
+      expect(service.id).toEqual(56);
+      expect(service.name).toEqual("Test");
+      expect(service.src).toEqual("https://example");
+    });
+
+    it('should be loadable', function () {
+      let service = serviceClass.create("Test", "https://example", 56);
+      expect(service).toBeTruthy();
+
+      let i = service.load();
+      expect(i.tagName).toEqual("IFRAME");
+      expect(i.getAttribute("allowTransparency")).toEqual("true");
+      expect(i.getAttribute("frameborder")).toEqual(''+0);
+      expect(i.getAttribute("name")).toEqual(''+service.id);
+      expect(i.getAttribute("src")).toEqual(service.src);
+    });
+  });
+  
   describe('KorAP.Plugin.QueryPanel', function () {
     it('should establish a query plugin', function () {
       var queryPanel = queryPanelClass.create();
diff --git a/dev/js/src/plugin/client.js b/dev/js/src/plugin/client.js
index 4e6ec3c..8c73625 100644
--- a/dev/js/src/plugin/client.js
+++ b/dev/js/src/plugin/client.js
@@ -44,6 +44,10 @@
     _init : function () {
       this.widgetID = window.name;
       this.server = cs.getAttribute('data-server') || '*';
+
+      // Establish the 'message' hook.
+      this._listener = this._receiveMsg.bind(this);
+      window.addEventListener("message", this._listener);
       return this;
     },
 
@@ -53,6 +57,24 @@
       window.parent.postMessage(data, this.server);
     },
 
+    // Receive a call from the embedded platform.
+    _receiveMsg : function (e) {
+      // Get event data
+      let d = e.data;
+
+      // If no data given - fail
+      // (probably check that it's an assoc array)
+      if (!d)
+        return;
+
+      // TODO:
+      //   check e.origin and d["originID"]!!!
+      //   probably against window.parent!
+
+      if (this.onMessage)
+        this.onMessage(d)
+    },
+    
     /**
      * Send a log message to the embedding KorAP
      */
@@ -86,7 +108,15 @@
   // Create plugin on windows load
   window.onload = function () {
     window.KorAPlugin = window.KorAPlugin || obj.create();
+
+    // TODO:
+    //   Only do this in case of the client being opened
+    //   as a widget!
     window.KorAPlugin.resize();
+
+    if (window.pluginit)
+      window.pluginit(window.KorAPlugin);
   };
+
 })();
 
diff --git a/dev/js/src/plugin/server.js b/dev/js/src/plugin/server.js
index 163a315..1c46dbe 100644
--- a/dev/js/src/plugin/server.js
+++ b/dev/js/src/plugin/server.js
@@ -1,6 +1,6 @@
 /**
  * The plugin system is based
- * on registered widgets (iframes) from
+ * on registered services (iframes) from
  * foreign services.
  * The server component spawns new iframes and
  * listens to them.
@@ -8,15 +8,15 @@
  * @author Nils Diewald
  */
 
-define(["plugin/widget", "util"], function (widgetClass) {
+define(["plugin/widget", 'plugin/service', 'state', "util"], function (widgetClass, serviceClass, stateClass) {
   "use strict";
 
   KorAP.Panel = KorAP.Panel || {};
 
-  // Contains all widgets to address with
+  // Contains all servicess to address with
   // messages to them
-  var widgets = {};
-  var plugins = {};
+  var services = {};
+  var plugins  = {};
 
   // TODO:
   //   These should better be panels and every panel
@@ -33,7 +33,7 @@
   var buttonsSingle = {
     query : [],
     result : []
-  }
+  } 
   
   // This is a counter to limit acceptable incoming messages
   // to a certain amount. For every message, this counter will
@@ -85,6 +85,17 @@
      *         'action' : 'addWidget',
      *         'template' : 'https://localhost:5678/?match={matchid}',
      *       }
+     *     },{
+     *       'title' : 'glemm',
+     *       'panel' : 'query',
+     *       'onClick' : {
+     *         'action' : 'toggle',
+     *         'state' : 'glemm',
+     *         'service' : {
+     *           'id' : 'glemm',
+     *           'template' : 'https://localhost:5678/'
+     *         }
+     *       }
      *     }]
      *   });
      *
@@ -103,7 +114,8 @@
         name : name,
         desc : obj["desc"],
         about : obj["about"],
-        widgets : []
+        widgets : [],
+        services : []
       };
 
       if (typeof obj["embed"] !== 'object')
@@ -111,37 +123,41 @@
  
       // Embed all embeddings of the plugin
       var that = this;
-      for (var i in obj["embed"]) {
-        var embed = obj["embed"][i];
+      for (let i in obj["embed"]) {
+        let embed = obj["embed"][i];
 
         if (typeof embed !== 'object')
           throw new Error("Embedding of plugin is no object");
 
-        var panel = embed["panel"];
-        
-        if (!panel || !(buttons[panel] || buttonsSingle[panel]))
-          throw new Error("Panel for plugin is invalid");
-        var onClick = embed["onClick"];
-
         // Needs to be localized as well
-        var title = embed["title"];
+        let title = embed["title"];        
+        let panel = embed["panel"];
+        let onClick = embed["onClick"];
+
+
+        if (!panel || !(buttons[panel] || buttonsSingle[panel]))
+          throw new Error("Panel for plugin is invalid");        
 
         // The embedding will open a widget
         if (!onClick["action"] || onClick["action"] == "addWidget") {
 
-          var cb = function (e) {
+          
+          let cb = function (e) {
 
             // "this" is bind to the panel
 
             // Get the URL of the widget
-            var url = onClick["template"];
+            let url = onClick["template"];
             // that._interpolateURI(onClick["template"], this.match);
 
             // Add the widget to the panel
-            var id = that.addWidget(this, name, url);
+            let id = that.addWidget(this, name, url);
             plugin["widgets"].push(id);
           };
 
+          // TODO:
+          //   Create button class to be stored and loaded in button groups!
+
           // Add to dynamic button list (e.g. for matches)
           if (buttons[panel]) {
             buttons[panel].push([title, embed["classes"], cb]);
@@ -156,11 +172,53 @@
           else {
             buttonsSingle[panel].push([title, embed["classes"], cb]);
           }
+        }
+
+        else if (onClick["action"] == "toggle") {
+
+          // Todo: Initially false
+          let state = stateClass.create(false);
+
+          // TODO:
+          //   Lazy registration (see above!)
+          KorAP.Panel[panel].actions.addToggle("Title",["title"], state);
+
+          // Get the URL of the service
+
+          // TODO:
+          //   Use the "service" keyword
+          let url = onClick["template"];
+          
+          // Add the service
+          let id = this.addService(name, url);
+
+          // TODO:
+          //   This is a bit stupid to get the service window
+          let iframe = services[id].load();
+          let win = iframe.contentWindow;
+
+          // Create object to communicate the toggle state
+          // once the iframe is loaded.
+          iframe.onload = function () {
+            let sendToggle = {
+              setState : function (val) {
+                win.postMessage({
+                  action: 'state',
+                  key : onClick['state'],
+                  value : val
+                }, '*'); // TODO: Fix origin
+              }
+            };
+
+            // Associate object with the state
+            state.associate(sendToggle);          
+          };
+
+          plugin["services"].push(id);
         };
       };
     },
 
-
     // TODO:
     //   Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
     //   but as simple as possible
@@ -191,17 +249,11 @@
         buttonsSingle[name] = [];
       }
     },
-    
-    /**
-     * Open a new widget view in a certain panel and return
-     * the id.
-     */
-    addWidget : function (panel, name, src) {
 
-      if (!src)
-        return;
+    // Optionally initialize the service mechanism and get an ID
+    _getServiceID : function () {
 
-      // Is it the first widget?
+      // Is it the first service?
       if (!this._listener) {
 
         /*
@@ -210,7 +262,7 @@
         this._listener = this._receiveMsg.bind(this);
         window.addEventListener("message", this._listener);
         
-        // Every second increase the limits of all registered widgets
+        // Every second increase the limits of all registered services
         this._timer = window.setInterval(function () {
           for (var i in limits) {
             if (limits[i]++ >= maxMessages) {
@@ -220,14 +272,51 @@
         }, 1000);
       };
 
-      // Create a unique random ID per widget
-      var id = 'id-' + this._randomID();
+      // Create a unique random ID per service
+      return 'id-' + this._randomID();
+    },
+    
+    /**
+     * Add a service in a certain panel and return the id.
+     */
+    addService : function (name, src) {
+      if (!src)
+        return;
+
+      let id = this._getServiceID();
+
+      // Create a new service
+      let service = serviceClass.create(name, src, id);
+      
+      // TODO!
+      // Store the service based on the identifier
+      services[id] = service;
+      limits[id] = maxMessages;
+
+      // widget._mgr = this;
+
+      // Add service to panel
+      this.element().appendChild(
+        service.load()
+      );
+      
+      return id;
+    },
+
+    
+    /**
+     * Open a new widget view in a certain panel and return
+     * the id.
+     */
+    addWidget : function (panel, name, src) {
+
+      let id = this._getServiceID();
 
       // Create a new widget
       var widget = widgetClass.create(name, src, id);
 
       // Store the widget based on the identifier
-      widgets[id] = widget;
+      services[id] = widget;
       limits[id] = maxMessages;
 
       widget._mgr = this;
@@ -240,14 +329,14 @@
 
 
     /**
-     * Get widget by identifier
+     * Get service by identifier
      */
-    widget : function (id) {
-      return widgets[id];
+    service : function (id) {
+      return services[id];
     },
 
     
-    // Receive a call from an embedded iframe.
+    // Receive a call from an embedded service.
     // The handling needs to be very careful,
     // as this can easily become a security nightmare.
     _receiveMsg : function (e) {
@@ -268,46 +357,60 @@
       if (!id)
         return;
 
-      // Get the widget
-      var widget = widgets[id];
+      // Get the service
+      let service = services[id];
 
-      // If the addressed widget does not exist - fail
-      if (!widget)
+      // If the addressed service does not exist - fail
+      if (!service)
         return;
 
       // Check for message limits
       if (limits[id]-- < 0) {
 
-        // Kill widget
-        KorAP.log(0, 'Suspicious action by widget', widget.src);
+        // Kill service
+        KorAP.log(0, 'Suspicious action by service', service.src);
 
         // TODO:
         //   Potentially kill the whole plugin!
 
-        // This removes all connections before closing the widget 
-        this._closeWidget(widget.id);
-        widget.close();
+        // This removes all connections before closing the service 
+        this._closeService(service.id);
+
+        // if (service.isWidget)
+        service.close();
+     
         return;
       };
 
       // Resize the iframe
-      if (d.action === 'resize') {
-        widget.resize(d);
-      }
+      switch (d.action) {
+      case 'resize':
+        if (service.isWidget)
+          service.resize(d);
+        break;
 
       // Log message from iframe
-      else if (d.action === 'log') {
-        KorAP.log(d.code, d.msg,  widget.src);
+      case 'log':
+        KorAP.log(d.code, d.msg,  service.src);
+        break;
       };
 
       // TODO:
       //   Close
     },
 
-    // Close the widget
-    _closeWidget : function (id) {
+    // Close the service
+    _closeService : function (id) {
       delete limits[id];
-      delete widgets[id];
+
+      // Close the iframe
+      if (services[id] && services[id]._closeIframe) {
+        services[id]._closeIframe();
+
+        // Remove from list
+        delete services[id];
+      };
+
 
       // Remove listeners in case no widget
       // is available any longer
@@ -328,19 +431,39 @@
       this._listener = undefined;
     },
 
+    /**
+     * Return the service element.
+     */
+    element : function () {
+      if (!this._element) {
+        this._element = document.createElement('div');
+        this._element.setAttribute("id", "services");
+      }
+      return this._element;
+    },
+    
     // Destructor, just for testing scenarios
     destroy : function () {
       limits = {};
-      for (let w in widgets) {
-        widgets[w].close();
+      for (let s in services) {
+        services[s].close();
       };
-      widgets = {};
+      services = {};
       for (let b in buttons) {
         buttons[b] = [];
       };
       for (let b in buttonsSingle) {
         buttonsSingle[b] = [];
       };
+
+      if (this._element) {
+        let e = this._element;
+        if (e.parentNode) {
+          e.parentNode.removeChild(e);
+        };
+        this._element = null;
+      };
+      
       this._removeListener();
     }
   };
diff --git a/dev/js/src/plugin/service.js b/dev/js/src/plugin/service.js
new file mode 100644
index 0000000..a02e945
--- /dev/null
+++ b/dev/js/src/plugin/service.js
@@ -0,0 +1,59 @@
+define(function () {
+  "use strict";
+  return {
+    create : function (name, src, id) {
+      return Object.create(this)._init(name, src, id);
+    },
+
+    // Initialize service
+    _init : function (name, src, id) {
+      if (!name || !src || !id)
+        throw Error("Service not well defined");
+      this.name = name;
+      this.src = src;
+      this.id = id;
+
+      // There is no close method defined yet
+      if (!this.close) {
+        this.close = function () {
+          this._closeIframe();
+        }
+      }
+      
+      return this;
+    },
+
+    /**
+     * The element of the service as embedded in the panel
+     */
+    load : function () {
+      if (this._load)
+        return this._load;
+      
+      // Spawn new iframe
+      let e = document.createElement('iframe');
+      e.setAttribute('allowTransparency',"true");
+      e.setAttribute('frameborder', 0);
+      e.setAttribute('sandbox','allow-scripts');
+      e.style.height = '0px';
+      e.setAttribute('name', this.id);
+      e.setAttribute('src', this.src);
+      
+      this._load = e;
+      return e;
+    },
+
+    // onClose : function () {},
+
+    /**
+     * Close the service iframe.
+     */
+    _closeIframe : function () {
+      var e = this._load;
+      if (e && e.parentNode) {
+        e.parentNode.removeChild(e);
+      };
+      this._load = null;
+    }
+  };
+});
diff --git a/dev/js/src/plugin/widget.js b/dev/js/src/plugin/widget.js
index b8aa49e..403954c 100644
--- a/dev/js/src/plugin/widget.js
+++ b/dev/js/src/plugin/widget.js
@@ -8,7 +8,7 @@
  * @author Nils Diewald
  */
 
-define(["view","util"], function (viewClass) {
+define(["view","plugin/service","util"], function (viewClass, serviceClass) {
   "use strict";
 
   return {
@@ -17,16 +17,12 @@
      * Create new widget
      */
     create : function (name, src, id) {
-      return Object.create(viewClass)._init(['widget']).upgradeTo(this)._init(name, src, id);
+      return Object.create(viewClass)._init(['widget']).upgradeTo(serviceClass)._init(name, src, id).upgradeTo(this)._init();
     },
 
     // Initialize widget
-    _init : function (name, src, id) {
-      if (!name || !src || !id)
-        throw Error("Widget not well defined");
-      this.name = name;
-      this.src = src;
-      this.id = id;
+    _init : function () {
+      this.isWidget = true;
       return this;
     },
 
@@ -35,23 +31,16 @@
      */
     show : function () {
 
-      if (this._show)
-        return this._show;
+      if (this._load)
+        return this._load;
 
-      // Spawn new iframe
-      var i = document.createElement('iframe');
-      i.setAttribute('allowTransparency',"true");
-      i.setAttribute('frameborder', 0);
-      i.setAttribute('sandbox','allow-scripts');
-      i.style.height = '0px';
-      i.setAttribute('name', this.id);
-      i.setAttribute('src', this.src);
+      let obj = this.load();
 
       // Per default there should at least be a button
       // for settings, if the plugin requires settings.
       // Otherwise a button indicating this is a plugin
       // is a nice idea as well.
-
+      
       this.actions.add(
         this.name, ['button-icon', 'plugin'], function (e) {
 
@@ -59,8 +48,7 @@
           window.alert("Basic information about this plugin");
       }.bind(this));
       
-      this._show = i;
-      return i;
+      return obj;
     },
 
     // Resize iframe
@@ -72,7 +60,7 @@
     // On closing the widget view
     onClose : function () {
       if (this._mgr) {
-        this._mgr._closeWidget(this._id);
+        this._mgr._closeService(this._id);
         this._mgr = undefined;
       };
     }
diff --git a/dev/js/src/view.js b/dev/js/src/view.js
index 4885afa..fa830d6 100644
--- a/dev/js/src/view.js
+++ b/dev/js/src/view.js
@@ -78,14 +78,17 @@
      * Close the view.
      */
     close : function () {
+
+      // Close embedded things before
+      if (this.onClose)
+        this.onClose();
+
       var e = this.element();
       if (e.parentNode) {
         e.parentNode.removeChild(e);
       };
       this.panel.delView(this);
       this._shown = false;
-      if (this.onClose)
-        this.onClose();
     },
 
     /**
diff --git a/package.json b/package.json
index cfbf19c..2a9da83 100755
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "description": "Mojolicious-based Frontend for KorAP",
   "license": "BSD-2-Clause",
   "version": "0.37.2",
-  "pluginVersion": "0.1",
+  "pluginVersion": "0.2",
   "repository": {
     "type": "git",
     "url": "https://github.com/KorAP/Kalamar.git"