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"