| /** |
| * The plugin system is based |
| * on registered services (iframes) from |
| * foreign services. |
| * The server component spawns new iframes and |
| * listens to them. |
| * |
| * @author Nils Diewald |
| */ |
| "use strict"; |
| |
| define(['plugin/widget', 'plugin/service', 'state', 'state/manager', 'pageInfo', 'util'], function (widgetClass, serviceClass, stateClass, stateManagerClass, pageInfoClass) { |
| |
| KorAP.Panel = KorAP.Panel || {}; |
| |
| const d = document; |
| |
| // State manager undefined |
| const states = KorAP.States ? KorAP.States : |
| |
| // Not serialized state manager |
| stateManagerClass.create(document.createElement('input')); |
| |
| // Contains all servicess to address with |
| // messages to them |
| var services = {}; |
| var plugins = {}; |
| |
| // TODO: |
| // These should better be panels and every panel |
| // has a buttonGroup |
| |
| // List of panels with dynamic buttons, i.e. |
| // panels that may occur multiple times. |
| var buttons = { |
| match : [] |
| }; |
| |
| // List of panels with static buttons, i.e. |
| // panels that occur only once. |
| var buttonsSingle = { |
| query : [], |
| result : [] |
| } |
| |
| // This is a counter to limit acceptable incoming messages |
| // to a certain amount. For every message, this counter will |
| // be decreased (down to 0), for every second this will be |
| // increased (up to 100). |
| // Once a widget surpasses the limit, it will be killed |
| // and called suspicious. |
| var maxMessages = 100; |
| var limits = {}; |
| |
| // TODO: |
| // It may be useful to establish a watcher that pings |
| // all widgets every second to see if it is still alive, |
| // otherwise kill |
| |
| // Load Plugin server |
| return { |
| |
| /** |
| * Create new plugin management system |
| */ |
| create : function () { |
| return Object.create(this)._init(); |
| }, |
| |
| /* |
| * Initialize the plugin manager |
| */ |
| _init : function () { |
| this._ql = d.getElementById("ql-field"); |
| this._q = d.getElementById("q-field") |
| this._cutoff = d.getElementById("q-cutoff-field"); |
| |
| return this; |
| }, |
| |
| /** |
| * Register a plugin described as a JSON object. |
| * |
| * This is work in progress. |
| * |
| * Example: |
| * |
| * KorAP.Plugin.register({ |
| * 'name' : 'CatContent', |
| * 'desc' : 'Some content about cats', |
| * 'about' : 'https://localhost:5678/', |
| * 'embed' : [{ |
| * 'panel' : 'match', |
| * 'title' : loc.TRANSLATE, |
| * 'classes' : ['translate'] |
| * 'onClick' : { |
| * 'action' : 'addWidget', |
| * 'template' : 'https://localhost:5678/?match={matchid}', |
| * } |
| * },{ |
| * 'title' : 'glemm', |
| * 'panel' : 'query', |
| * 'onClick' : { |
| * 'action' : 'toggle', |
| * 'state' : 'glemm', |
| * 'service' : { |
| * 'id' : 'glemm', |
| * 'template' : 'https://localhost:5678/' |
| * } |
| * } |
| * }] |
| * }); |
| * |
| */ |
| register : function (obj) { |
| // TODO: |
| // These fields need to be localized for display by a structure like |
| // { de : { name : '..' }, en : { .. } } |
| const name = obj["name"]; |
| |
| if (!name) |
| throw new Error("Missing name of plugin"); |
| |
| var desc = obj["desc"]; |
| |
| // Register plugin by name |
| var plugin = plugins[name] = { |
| name : name, |
| desc : desc, |
| about : obj["about"], |
| widgets : [], |
| services : [] |
| }; |
| |
| if (typeof obj["embed"] !== 'object') |
| throw new Error("Embedding of plugin is no list"); |
| |
| // Embed all embeddings of the plugin |
| var that = this; |
| obj["embed"].forEach(function(embed) { |
| |
| if (typeof embed !== 'object') |
| throw new Error("Embedding of plugin is no object"); |
| |
| // Needs to be localized as well |
| let title = embed["title"]; |
| let panel = embed["panel"]; |
| let onClick = embed["onClick"]; |
| let icon = embed["icon"]; |
| |
| 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" || |
| onClick["action"] == "setWidget") { |
| |
| let cb = function (e) { |
| |
| // "this" is bind to the panel |
| // "this".button is the button |
| // "that" is the server object |
| |
| // The button has a state and the state is associated to the |
| // a intermediate object to toggle the view |
| if ('state' in this.button && this.button.state.associates() > 0) { |
| |
| const s = this.button.state; |
| |
| // The associated service is existent |
| if (services[this.button['widgetID']]) { |
| s.roll(); |
| return; |
| } |
| |
| // The service is not existent |
| else { |
| |
| // Remove broken state associations |
| s.clear(); |
| s.set(true); |
| } |
| }; |
| |
| // Add the widget to the panel |
| let id = that.addWidget(this, { |
| "name": name, |
| "src": onClick["template"], // that._interpolateURI(onClick["template"], this.match); |
| "permissions": onClick["permissions"], |
| "desc":desc |
| }); |
| plugin["widgets"].push(id); |
| |
| // If a state exists, associate with a mediator object |
| if ('state' in this.button) { |
| this.button['widgetID'] = id; |
| this.button.state.associate({ |
| setState : function (value) { |
| // Minimize the widget |
| if (value == false) { |
| services[id].minimize(); |
| } |
| else { |
| services[id].show(); |
| }; |
| } |
| }); |
| } |
| }; |
| |
| |
| // Button object |
| let obj = {'cls':embed["classes"], 'icon': icon }; |
| |
| if (embed['desc'] != undefined) |
| obj['desc'] = embed['desc']; |
| |
| if (onClick["action"] && onClick["action"] == "setWidget") { |
| |
| // Create a boolean state value, |
| // that initializes to true == opened |
| obj['state'] = stateClass.create([true, false]); |
| obj['state'].setIfNotYet(true); |
| }; |
| |
| // Add to dynamic button list (e.g. for matches) |
| if (buttons[panel]) { |
| buttons[panel].push([title, obj, cb]); |
| } |
| |
| // Add to static button list (e.g. for query) already loaded |
| else if (KorAP.Panel[panel]) { |
| KorAP.Panel[panel].actions().add(title, obj, cb); |
| } |
| |
| // Add to static button list (e.g. for query) not yet loaded |
| else { |
| buttonsSingle[panel].push([title, obj, cb]); |
| } |
| } |
| |
| else if (onClick["action"] == "toggle") { |
| |
| // TODO: |
| // Accept a "value" list here for toggling, which should |
| // also allow for "rolling" through states via CSS classes |
| // as 'toggle-true', 'toggle-false' etc. |
| let state = states.newState( |
| (onClick["state"] ? onClick["state"] : name), |
| [true, false], |
| onClick["default"] |
| ); |
| |
| let obj = {'cls':["title"], 'icon': icon}; |
| |
| if (embed['desc'] != undefined) |
| obj['desc'] = embed['desc']; |
| |
| // TODO: |
| // Lazy registration (see above!) |
| KorAP.Panel[panel].actions().addToggle(title, obj, state); |
| |
| // Add the service |
| let id = this.addService({ |
| "name" : name, |
| // TODO: |
| // Use the "service" keyword |
| "src" : onClick["template"], |
| "permissions" : onClick["permissions"] |
| }); |
| |
| // TODO: |
| // This is a bit stupid to get the service window |
| let service = services[id]; |
| let iframe = service.load(); |
| |
| // Create object to communicate the toggle state |
| // once the iframe is loaded. |
| iframe.onload = function () { |
| let sendToggle = { |
| setState : function (val) { |
| service.sendMsg({ |
| action: 'state', |
| key : onClick['state'], |
| value : val |
| }); |
| } |
| }; |
| |
| // Associate object with the state |
| state.associate(sendToggle); |
| }; |
| |
| plugin["services"].push(id); |
| }; |
| }, this); |
| }, |
| |
| // TODO: |
| // Interpolate URIs similar to https://tools.ietf.org/html/rfc6570 |
| // but as simple as possible |
| _interpolateURI : function (uri, obj) { |
| // ... |
| }, |
| |
| |
| /** |
| * Get named button group - better rename to "action" |
| */ |
| buttonGroup : function (name) { |
| if (buttons[name] != undefined) { |
| return buttons[name]; |
| } else if (buttonsSingle[name] != undefined) { |
| return buttonsSingle[name]; |
| }; |
| return []; |
| }, |
| |
| /** |
| * Clear named button group - better rename to "action" |
| */ |
| clearButtonGroup : function (name) { |
| if (buttons[name] != undefined) { |
| buttons[name] = []; |
| } else if (buttonsSingle[name] != undefined) { |
| buttonsSingle[name] = []; |
| } |
| }, |
| |
| // Optionally initialize the service mechanism and get an ID |
| _getServiceID : function () { |
| |
| // Is it the first service? |
| if (!this._listener) { |
| |
| /* |
| * Establish the global 'message' hook. |
| */ |
| this._listener = this._receiveMsg.bind(this); |
| window.addEventListener("message", this._listener); |
| |
| // Every second increase the limits of all registered services |
| this._timer = window.setInterval(function () { |
| for (let i = 0; i < limits.length; i++) { |
| if (limits[i]++ >= maxMessages) { |
| limits[i] = maxMessages; |
| } |
| } |
| }, 1000); |
| }; |
| |
| // Create a unique random ID per service |
| return 'id-' + this._randomID(); |
| }, |
| |
| /** |
| * Add a service in a certain panel and return the id. |
| */ |
| addService : function (data) { |
| if (!data["src"]) |
| return; |
| |
| let id = this._getServiceID(); |
| |
| data["id"] = id; |
| |
| // Create a new service |
| let service = serviceClass.create(data); |
| |
| services[id] = service; |
| limits[id] = maxMessages; |
| |
| // 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, data) { |
| // panel, name, src, permissions |
| |
| let id = this._getServiceID(); |
| |
| data["id"] = id; |
| |
| // Create a new widget |
| var widget = widgetClass.create(data); |
| |
| // Store the widget based on the identifier |
| services[id] = widget; |
| limits[id] = maxMessages; |
| |
| widget._mgr = this; |
| |
| // Add widget to panel |
| panel.add(widget); |
| |
| return id; |
| }, |
| |
| |
| /** |
| * Get service by identifier |
| */ |
| service : function (id) { |
| return services[id]; |
| }, |
| |
| |
| // 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) { |
| |
| // Get event data |
| var d = e.data; |
| |
| // If no data given - fail |
| // (probably check that it's an assoc array) |
| if (!d) |
| return; |
| |
| // e.origin is probably set and okay - CHECK! |
| // TODO: Check e.origin is in the list of registered participants |
| // if (e.origin !== "http://example.com:8080") |
| // return; |
| |
| |
| // Get origin ID |
| var id = d["originID"]; |
| |
| // If no origin ID given - fail |
| if (!id) |
| return; |
| |
| // Get the service |
| let service = services[id]; |
| |
| // If the addressed service does not exist - fail |
| if (!service) |
| return; |
| |
| // Check for message limits |
| if (limits[id]-- < 0) { |
| |
| // Kill service |
| KorAP.log(0, 'Suspicious action by service', service.src); |
| |
| // TODO: |
| // Potentially kill the whole plugin! |
| |
| // This removes all connections before closing the service |
| this._closeService(service.id); |
| |
| // if (service.isWidget) |
| service.close(); |
| |
| return; |
| }; |
| |
| // Resize the iframe |
| switch (d.action) { |
| case 'resize': |
| if (service.isWidget) |
| service.resize(d); |
| break; |
| |
| // Log message from iframe |
| case 'log': |
| KorAP.log(d.code, d.msg, service.src); |
| break; |
| |
| // Modify pipes |
| case 'pipe': |
| if (KorAP.Pipe != undefined) { |
| if (d.job == 'del') { |
| KorAP.Pipe.remove(d.service); |
| } else { |
| KorAP.Pipe.append(d.service); |
| }; |
| }; |
| break; |
| |
| // Get information from the embedding platform |
| case 'get': |
| |
| // Get KoralQuery |
| if (d.key == 'KQ') { |
| if (KorAP.koralQuery !== undefined) { |
| d["value"] = KorAP.koralQuery; |
| }; |
| } |
| |
| // Get Query information from form |
| else if (d.key == 'QueryForm') { |
| let doc = document; |
| let v = d["value"] = {}; |
| |
| var el; |
| if (el = doc.getElementById('q-field')) { |
| v["q"] = el.value; |
| }; |
| if (el = doc.getElementById('ql-field')) { |
| v["ql"] = el.value; |
| }; |
| if (el = KorAP.vc) { |
| v["cq"] = el.toQuery(); |
| }; |
| if (el = KorAP.Hint) { |
| v["selection"] = KorAP.Hint.selectionRange(); |
| }; |
| } |
| |
| // Get text sigle from match |
| else if (d.key == 'textSigle') { |
| if (service.panel.type != "match") { |
| KorAP.log(0, "Service can only be called on matches", service.src); |
| return; |
| }; |
| let v = d["value"] = {}; |
| v["value"] = service.panel._match.textSigle; |
| } |
| |
| // Get Query information from parameters |
| else if (d.key == 'QueryParam') { |
| |
| // Supported in all modern browsers |
| var p = new URLSearchParams(window.location.search); |
| let v = d["value"] = {}; |
| v["search"] = window.location.search; // readonly |
| v["q"] = p.get('q'); |
| v["ql"] = p.get('ql'); |
| v["cq"] = p.get('cq'); |
| } |
| |
| // Get pagination information |
| else if (d.key == 'Pagination') { |
| const pi = pageInfoClass.create(); |
| let v = d["value"] = {}; |
| v["page"] = pi.page(); |
| v["total"] = pi.total(); |
| v["count"] = pi.count(); |
| }; |
| |
| // data needs to be mirrored |
| if (d._id) { |
| service.sendMsg(d); |
| }; |
| |
| break; |
| |
| case 'set': |
| |
| // Get Query information from data |
| if (d.key == 'QueryForm') { |
| let v = d["value"]; |
| |
| if (v["q"] != undefined && this._q) { |
| this._q.value = v["q"]; |
| }; |
| |
| // Set query language field |
| // Identical to tutorial.js |
| if (v["ql"] != undefined) { |
| if (KorAP.QLmenu) { |
| KorAP.QLmenu.selectValue(v["ql"]); |
| } |
| |
| else if (this._ql) { |
| let found = Array.from(this._ql.options).find(o => o.value === v["ql"]); |
| if (found) |
| found.selected = true; |
| }; |
| }; |
| |
| window.scrollTo(0, 0); |
| // if (v["cq"] != undefined) {}; |
| } |
| |
| break; |
| |
| // Redirect to a different page relative to the current |
| case 'redirect': |
| const url = new URL(window.location); |
| |
| // Currently this only accepts search parameters |
| if (d["queryParam"]) { |
| url.search = new URLSearchParams(d["queryParam"]); |
| }; |
| |
| window.location = url.toString(); |
| break; |
| }; |
| |
| // TODO: |
| // Close |
| }, |
| |
| // Close the service |
| _closeService : function (id) { |
| delete limits[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 |
| if (Object.keys(limits).length == 0) |
| this._removeListener(); |
| }, |
| |
| // Get a random identifier |
| _randomID : function () { |
| return randomID(20); |
| }, |
| |
| // Remove the listener |
| _removeListener : function () { |
| window.clearInterval(this._timer); |
| this._timer = undefined; |
| window.removeEventListener("message", this._listener); |
| this._listener = undefined; |
| }, |
| |
| /** |
| * Return the service element. |
| */ |
| element : function () { |
| if (!this._el) { |
| this._el = document.createElement('div'); |
| this._el.setAttribute("id", "services"); |
| } |
| return this._el; |
| }, |
| |
| |
| // Return states object |
| states : function () { |
| return states; |
| }, |
| |
| // Destructor, just for testing scenarios |
| destroy : function () { |
| limits = {}; |
| Object.keys(services).forEach( |
| s => services[s].close() |
| ); |
| services = {}; |
| Object.keys(buttons).forEach( |
| b => buttons[b] = [] |
| ); |
| Object.keys(buttonsSingle).forEach( |
| b => buttonsSingle[b] = [] |
| ); |
| |
| if (this._el) { |
| let e = this._el; |
| if (e.parentNode) { |
| e.parentNode.removeChild(e); |
| }; |
| this._el = null; |
| }; |
| this._removeListener(); |
| } |
| }; |
| }); |