Support toggle+widget buttons in plugin-framework
Change-Id: Ibf2b80efc1eeda2f51dedf4a7f3cd5ac67325ef9
diff --git a/.gitignore b/.gitignore
index dfb29e9..69a3b4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,4 +49,6 @@
/npm-debug.log
/package-lock.json
/kalamar.code-workspace
-slim.report.json
\ No newline at end of file
+slim.report.json
+/dev/js/src-lib/
+/local/
\ No newline at end of file
diff --git a/Changes b/Changes
index d5deedd..ccdc780 100644
--- a/Changes
+++ b/Changes
@@ -14,6 +14,7 @@
- Restrict allow-same-origin to plugins that actually ARE AND request it (Requires the export plugin >= 0.4.1)(kupietz, tests hebasta)
- Add data-testid to glimpse (diewald)
- Update dependencies (diewald)
+ - Support combined toggle+widget button for plugins (diewald)
0.64 2026-02-14
- Improve 'Plugins' mounting (diewald)
diff --git a/dev/demo/plugin-client.html b/dev/demo/plugin-client.html
index 530682c..45b9852 100644
--- a/dev/demo/plugin-client.html
+++ b/dev/demo/plugin-client.html
@@ -22,7 +22,7 @@
</style>
</head>
<body class="result-view">
- <h2>Example Widget!</h2>
+ <h1>Example Widget!</h1>
<script>
function flood () {
var i = 0;
@@ -78,7 +78,8 @@
});
}
- </script>
+ </script>
+ <section>
<ul>
<li><a onclick="KorAPlugin.log(333, 'Huhu!')">Send log!</a></li>
<li><a onclick="KorAPlugin.resize()">Resize</a></li>
@@ -89,7 +90,9 @@
<li><a onclick="KorAPlugin.requestMsg({'action':'get', 'key':'KQ'}, function (d) { document.write(JSON.stringify(d.value))})">Get KQ</a></li>
<li><a onclick="KorAPlugin.requestMsg({'action':'get', 'key':'textSigle', 'value':'textSigle'}, function (d) { document.write(JSON.stringify(d.value))})">Get textSigle</a></li>
<li><a onclick="KorAPlugin.sendMsg({'action':'set', 'key':'QueryForm', 'value':{'q':'[This][is][a][query]'}})">Set query</a></li>
+ <li><a onclick="KorAPlugin.sendMsg({'action':'set', 'key':'Title', 'value':'Huhuhu'})">Set title</a></li>
</ul>
<p style="width: 2000px">------------------------------------------------------------------------------------------------------------------------</p>
+ </section>
</body>
</html>
diff --git a/dev/demo/plugin-server.html b/dev/demo/plugin-server.html
index 6a1e12f..2a2d047 100644
--- a/dev/demo/plugin-server.html
+++ b/dev/demo/plugin-server.html
@@ -12,52 +12,96 @@
window.KorAP = window.KorAP || {};
//]]></script>
+
<script data-main="plugin-serverdemo.js" src="../js/lib/require.js" async="async"></script>
</head>
- <body>
+ <body class="no-js"
+ data-korap-url="/"
+ data-hint-foundries="base,corenlp,dereko,malt,marmot,opennlp,spacy,tt"
+ itemscope
+ itemtype="http://schema.org/WebApplication">
<p>Start the demo server with <code>morbo -l 'http://*:3003' t/server/plugin.pl</code> and open <a href="http://localhost:3003/demo/plugin-server.html"><code>this website</code></a>.</p>
+<div id="kalamar-bg"></div>
+
+
+
+
+
+
+
+<aside tabindex="0" class=" invisible off">
+ <div>
+
+ </div>
+</aside>
+
+
+
+
+<nav class="navbar">
+ <a class="logo" data-testid="home" href="/instance/test"><h1><span>KorAP - Korpusanalyseplattform der nächsten Generation</span></h1></a>
+
+ <div class="navbar-group">
+ <h3 class="nav-link"><a class="link-guided-tour" data-testid="tour-start">Tour</a></h3><h3 class="nav-link"><a href="/instance/test/doc" class="tutorial" id="view-tutorial">Hilfe</a></h3>
+
+
+
+
+ <nav class="dropdown">
+ <div class="dropdown-btn login"></div>
+ <div class="dropdown-content dropdown-content--right">
+ <form accept-charset="utf-8" action="/instance/test/user/login" class="login" method="POST">
+ <legend><span>Anmelden</span></legend>
+ <input name="csrf_token" type="hidden" value="0a3feea1e8b1400421d18783b4aba1950a388215">
+ <input data-testid="handle-or-email" name="handle_or_email" placeholder="Benutzername oder Email" type="text">
+ <input name="fwd" type="hidden" value="/instance/test">
+ <input data-testid="pwd" name="pwd" placeholder="Passwort" type="password">
+ <button type="submit" class="btn-login" data-testid="login-submit"><span>Los!</span></button>
+</form> <p>Anmeldung mit einem registrierten <a href="https://perso.ids-mannheim.de/registration/" style="white-space: nowrap">Cosmas-II</a>-Konto</p>
+ </div>
+ </nav>
+
+ </div>
+
+ <div class="burger-icon"></div>
+</nav>
+
+
<header>
<form autocomplete="off" action="/kalamar" id="searchform">
<div id="searchbar">
- <input type="search"
- placeholder="Find ..."
- name="q"
- id="q-field"
- value="abcdefghijklmnopqrstuvwxyz"
- autofocus="autofocus" />
- <button id="qsubmit" type="submit"><span>Go</span></button>
+ <input autocapitalize="off"
+ autocomplete="off"
+ autocorrect="off"
+ autofocus="autofocus"
+ id="q-field"
+ name="q"
+ placeholder="Finde ..."
+ spellcheck="false"
+ value="abcdefghijklmnopqrstuvwxyz"
+ type="search">
+ <button type="submit" id="qsubmit" title="Los!"><span>Los!</span></button>
<!-- <i class="fa fa-arrow-circle-down show-hint" onclick="hint.popUp()"></i> -->
</div>
- <!-- Search in the following virtual corpus -->
- <div id="vc-view"></div>
- in
- <input id="show" name="show" type="hidden" />
- <!--<input id="collection-name" name="collection-name" type="hidden"> -->
- <input id="cq" name="cq" type="text" value="">
- with <span class="menu select">
- <select name="ql" id="ql-field">
- <option value="poliqarp">Poliqarp</option>
- <option value="cosmas2">Cosmas II</option>
- <option value="annis">Annis</option>
- <option value="cql">CQL v1.2</option>
- </select>
- </span>
- <div class="button right">
- <input type="checkbox"
- value="1"
- name="cutoff"
- class="checkbox"
- id="q-cutoff-field" />
- <label for="q-cutoff-field"><span id="glimpse"></span>Glimpse</label>
- <!-- Todo: open tutorial - like openTutorial() -->
- <a href="doc/faq" tabindex="-1" class="question"><span>Question</span></a>
- <a href="tutorial.html" title="Tutorial" class="tutorial" id="view-tutorial"><span>Tutorial</span></a>
- </div>
- </form>
- </header>
+ <!-- Search in the following virtual corpus -->
+ <div id="vc-view"></div>
+ in
+ <input id="cq" name="cq" type="text">
+
+ mit
+ <span class="select">
+ <select id="ql-field" name="ql"><option value="poliqarp">Poliqarp</option><option value="cosmas2">Cosmas II</option><option value="annis">Annis QL</option><option value="cqp">CQP (neu)</option><option value="cql">CQL v1.2</option><option value="fcsql">FCSQL</option></select>
+ </span>
+ <div class="button right">
+ <input checked class="checkbox" id="q-cutoff-field" name="cutoff" type="checkbox" value="1">
+ <label for="q-cutoff-field" title="Zeige nur die ersten Treffer in beliebiger Reihenfolge"><span id="glimpse"></span>Glimpse</label>
+ </div>
+ <div class="clear"></div>
+ </form>
+</header>
<main>
<script>
@@ -155,5 +199,16 @@
</ol>
</div>
</main>
+
+<footer>
+ <nav>
+ <div><a href="http://www.ids-mannheim.de/kl/projekte/korap/">Über KorAP</a></div><div><a href="/instance/test/doc//faq#howToCite">Zitierhilfe</a></div><div><a href="http://www.ids-mannheim.de/allgemein/impressum.html">Impressum</a></div><div><a href="http://www.ids-mannheim.de/allgemein/datenschutz/">Datenschutz</a></div><div><a class="embedded-link" href="/instance/test/doc/korap/kalamar">V 0.64.0</a></div>
+ </nav>
+ <div id="logos">
+ <a href="http://www.ids-mannheim.de/" class="logo"><p id="ids-logo"><span>Entwickelt am Leibniz-Institut für Deutsche Sprache (IDS)</span></p></a>
+ </div>
+</footer>
+
+
</body>
</html>
diff --git a/dev/demo/plugin-serverdemo.js b/dev/demo/plugin-serverdemo.js
index 6540591..406ea2b 100644
--- a/dev/demo/plugin-serverdemo.js
+++ b/dev/demo/plugin-serverdemo.js
@@ -17,6 +17,7 @@
'classes' : [ 'button-icon', 'plugin', 'export' ],
'onClick' : {
'action' : 'addWidget',
+ 'active' : true,
'template' : 'http://localhost:3003/demo/plugin-client.html',
"permissions": [
"forms",
@@ -51,10 +52,28 @@
]
},
}]
+},{
+ "name": "Koral-Mapper",
+ "desc": "Mapping Service",
+ "embed": [
+ {
+ "classes": [
+ "termmapper"
+ ],
+ "onClick" : {
+ "action" : "setWidget",
+ "active" : false,
+ "template" : "http://localhost:5725",
+ "permissions": ["forms", "scripts", "downloads"]
+ },
+ "panel": "query",
+ "title": "Map"
+ }
+ ]
}];
-define(['plugin/server', 'lib/domReady', 'app/en', 'init'], function(pluginClass, domReady) {
+define(['plugin/server', 'pipe', 'lib/domReady', 'app/en', 'init'], function(pluginClass, pipeClass, domReady) {
domReady(function (event) {
if (KorAP.Plugin === undefined) {
// Load Plugin Server first
@@ -62,6 +81,16 @@
// Add services container to head
document.head.appendChild(KorAP.Plugin.element());
};
+
+ if (KorAP.Pipe === undefined) {
+ KorAP.Pipe = pipeClass.create("pipe");
+ let searchF = document.getElementById("searchform");
+ searchF.appendChild(KorAP.Pipe.element());
+
+ KorAP.ResponsePipe = pipeClass.create("response-pipe");
+ searchF.appendChild(KorAP.ResponsePipe.element());
+ };
+
KorAP.Plugins.forEach(i => KorAP.Plugin.register(i));
});
});
diff --git a/dev/js/spec/buttongroupSpec.js b/dev/js/spec/buttongroupSpec.js
index 189fb24..1812607 100644
--- a/dev/js/spec/buttongroupSpec.js
+++ b/dev/js/spec/buttongroupSpec.js
@@ -275,15 +275,15 @@
expect(check.classList.contains("checked")).toBeFalsy();
expect(count).toEqual(0);
- // Clicking the check toggles active state, but does not trigger callback.
+ // Clicking the check toggles active state and triggers the callback.
check.click();
expect(active.get()).toBeTruthy();
expect(check.classList.contains("checked")).toBeTruthy();
- expect(count).toEqual(0);
-
- // Clicking the button still triggers the callback.
- button.click();
expect(count).toEqual(1);
+
+ // Clicking the button also triggers the callback.
+ button.click();
+ expect(count).toEqual(2);
});
it('should allow adoption', function () {
diff --git a/dev/js/spec/pipeSpec.js b/dev/js/spec/pipeSpec.js
index e1f47a0..e47a9a0 100644
--- a/dev/js/spec/pipeSpec.js
+++ b/dev/js/spec/pipeSpec.js
@@ -55,6 +55,22 @@
expect(p.toString()).toEqual('service2,service1');
});
+ it('should not add duplicates', function () {
+ let p = pipeClass.create();
+ p.append('service1');
+ p.append('service2');
+ expect(p.size()).toEqual(2);
+ expect(p.toString()).toEqual('service1,service2');
+
+ p.append('service1');
+ expect(p.size()).toEqual(2);
+ expect(p.toString()).toEqual('service1,service2');
+
+ p.prepend('service2');
+ expect(p.size()).toEqual(2);
+ expect(p.toString()).toEqual('service1,service2');
+ });
+
it('should be deletable', function () {
let p = pipeClass.create();
p.append('service1');
diff --git a/dev/js/spec/pluginSpec.js b/dev/js/spec/pluginSpec.js
index 32a6e5e..107f44d 100644
--- a/dev/js/spec/pluginSpec.js
+++ b/dev/js/spec/pluginSpec.js
@@ -244,6 +244,457 @@
KorAP.Panel["result"] = undefined;
});
+ it('should accept valid registrations for setWidget with active', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+
+ let manager = pluginServerClass.create();
+
+ // Attach services container so addService works
+ document.body.appendChild(manager.element());
+
+ manager.register({
+ name : 'Check',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+ expect(b.getAttribute("title")).toEqual("Map");
+
+ // Button should have a check element (marker-box)
+ expect(b.firstChild.classList.contains('check')).toBeTruthy();
+ expect(b.firstChild.classList.contains('button-icon')).toBeTruthy();
+
+ // active state should be set to false initially
+ expect(b.active).toBeDefined();
+ expect(b.active.get()).toBe(false);
+
+ // Check is not checked initially
+ expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+ expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+ // Click on check (marker-box) - loads background service,
+ // no visible widget in the panel
+ b.firstChild.click();
+
+ // No iframe in the panel - service is in the services container
+ expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+ expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+ // No widget view element should exist in the panel
+ expect(p.element().querySelectorAll("div.view").length).toEqual(0);
+
+ // active state should have toggled to true
+ expect(b.active.get()).toBe(true);
+ expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+ // Click on check again - should not add another service, just toggle active
+ b.firstChild.click();
+ expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+ expect(b.active.get()).toBe(false);
+ expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+ // Click on title - should open the widget visibly
+ b.click();
+ expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+ // Click title again - should toggle visibility (hide via state)
+ b.click();
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(0);
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should not show any view element when checkbox is clicked', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+ let manager = pluginServerClass.create();
+
+ document.body.appendChild(manager.element());
+
+ manager.register({
+ name : 'NoBar',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+
+ // Click checkbox
+ b.firstChild.click();
+
+ // No view element must exist in the panel at all —
+ // not visible, not hidden, not minimized. The checkbox
+ // must only create a background service, never a widget.
+ expect(p.element().querySelectorAll("div.view").length).toEqual(0);
+ expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+ // The iframe must be in the services container instead
+ expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+ // Clicking the title afterwards must properly open the widget
+ b.click();
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+ expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should toggle active via checkbox when widget is open', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+ let manager = pluginServerClass.create();
+
+ document.body.appendChild(manager.element());
+
+ manager.register({
+ name : 'Check',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+
+ // Open the widget via title-click
+ b.click();
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+ expect(b.active.get()).toBe(false);
+
+ let id = b['widgetID'];
+ expect(id).toBeDefined();
+
+ // Active association should exist on the widget
+ expect(b.active.associates()).toBeGreaterThan(0);
+
+ // Click checkbox to activate - should toggle active state
+ b.firstChild.click();
+ expect(b.active.get()).toBe(true);
+ expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+ // Widget should still be visible
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+ // widgetID should remain the same (no new service created)
+ expect(b['widgetID']).toEqual(id);
+
+ // Click checkbox again to deactivate
+ b.firstChild.click();
+ expect(b.active.get()).toBe(false);
+ expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+ // Widget should still be visible
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should not duplicate pipes when opening widget after checkbox', function () {
+ var tempPipe = KorAP.Pipe;
+ KorAP.Pipe = pipeClass.create();
+
+ let p = KorAP.Panel["result"] = panelClass.create();
+ let manager = pluginServerClass.create();
+
+ document.body.appendChild(manager.element());
+
+ manager.register({
+ name : 'PipeCheck',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+
+ // Click checkbox - background service created, active becomes true
+ b.firstChild.click();
+ expect(b.active.get()).toBe(true);
+ expect(manager.element().querySelectorAll("iframe").length).toEqual(1);
+
+ // Simulate the plugin adding a pipe via the background service
+ let bgId = b['widgetID'];
+ manager._receiveMsg({
+ "data" : {
+ "originID" : bgId,
+ "action" : "pipe",
+ "job" : "add",
+ "service" : "https://mapper.example"
+ }
+ });
+ expect(KorAP.Pipe.toString()).toEqual("https://mapper.example");
+
+ // Click title - opens widget, closes background service
+ b.click();
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+ // The new widget's plugin re-initializes and tries to add the
+ // same pipe again — the pipe system must deduplicate.
+ let wId = b['widgetID'];
+ manager._receiveMsg({
+ "data" : {
+ "originID" : wId,
+ "action" : "pipe",
+ "job" : "add",
+ "service" : "https://mapper.example"
+ }
+ });
+
+ // Pipe must still contain only one entry
+ expect(KorAP.Pipe.toString()).toEqual("https://mapper.example");
+ expect(KorAP.Pipe.size()).toEqual(1);
+
+ manager.destroy();
+ KorAP.Pipe = tempPipe;
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should toggle checkbox visual when widget is open', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+ let manager = pluginServerClass.create();
+
+ document.body.appendChild(manager.element());
+
+ manager.register({
+ name : 'VisualCheck',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+ let check = b.firstChild;
+
+ // Open widget via title
+ b.click();
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+ expect(check.classList.contains('checked')).toBeFalsy();
+
+ // Click checkbox - should visually check
+ check.click();
+ expect(b.active.get()).toBe(true);
+ expect(check.classList.contains('checked')).toBeTruthy();
+
+ // Click checkbox again - should visually uncheck
+ check.click();
+ expect(b.active.get()).toBe(false);
+ expect(check.classList.contains('checked')).toBeFalsy();
+
+ // Widget should still be visible throughout
+ expect(p.element().querySelectorAll("div.view.show.widget").length).toEqual(1);
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should accept valid registrations for addWidget with active', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+
+ let manager = pluginServerClass.create();
+
+ manager.register({
+ name : 'Check',
+ embed : [{
+ panel : 'result',
+ title : 'Export',
+ onClick : {
+ template : 'about:blank',
+ action : 'addWidget',
+ active : true
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+
+ // active state should be set to true initially
+ expect(b.active).toBeDefined();
+ expect(b.active.get()).toBe(true);
+ expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+ expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+
+ // Click on check - only toggles active, no widget created
+ b.firstChild.click();
+ expect(p.element().querySelectorAll("iframe").length).toEqual(0);
+ expect(b.active.get()).toBe(false);
+ expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+ // Click on title - should load a widget visibly
+ b.click();
+ expect(p.element().querySelectorAll("iframe").length).toEqual(1);
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should support changeTitle on buttons', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+
+ let manager = pluginServerClass.create();
+
+ manager.register({
+ name : 'Check',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget'
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+ expect(b.lastChild.textContent).toEqual("Map");
+
+ b.changeTitle("New Title");
+ expect(b.lastChild.textContent).toEqual("New Title");
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should handle Title set message', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+
+ let manager = pluginServerClass.create();
+
+ manager.register({
+ name : 'TitlePlugin',
+ embed : [{
+ panel : 'result',
+ title : 'Original',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget'
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+ expect(b.lastChild.textContent).toEqual("Original");
+
+ // Click to open widget
+ b.click();
+
+ let id = b['widgetID'];
+ expect(id).toBeDefined();
+
+ // Send Title set message
+ manager._receiveMsg({
+ "data" : {
+ "originID" : id,
+ "action" : "set",
+ "key" : "Title",
+ "value" : "Changed"
+ }
+ });
+
+ expect(b.lastChild.textContent).toEqual("Changed");
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
+ it('should handle Active get/set messages', function () {
+ let p = KorAP.Panel["result"] = panelClass.create();
+
+ let manager = pluginServerClass.create();
+
+ manager.register({
+ name : 'ActivePlugin',
+ embed : [{
+ panel : 'result',
+ title : 'Map',
+ onClick : {
+ template : 'about:blank',
+ action : 'setWidget',
+ active : false
+ }
+ }]
+ });
+
+ let b = p.actions().element().firstChild;
+ expect(b.active.get()).toBe(false);
+
+ // Click title to open widget
+ b.click();
+
+ let id = b['widgetID'];
+ expect(id).toBeDefined();
+
+ // Get active state
+ let data = {
+ "originID" : id,
+ "action" : "get",
+ "key" : "Active"
+ };
+ manager._receiveMsg({ "data" : data });
+ expect(data.value).toBe(false);
+
+ // Set active state via message
+ manager._receiveMsg({
+ "data" : {
+ "originID" : id,
+ "action" : "set",
+ "key" : "Active",
+ "value" : true
+ }
+ });
+ expect(b.active.get()).toBe(true);
+ expect(b.firstChild.classList.contains('checked')).toBeTruthy();
+
+ // Roll active state via message (no value)
+ manager._receiveMsg({
+ "data" : {
+ "originID" : id,
+ "action" : "set",
+ "key" : "Active"
+ }
+ });
+ expect(b.active.get()).toBe(false);
+ expect(b.firstChild.classList.contains('checked')).toBeFalsy();
+
+ manager.destroy();
+ KorAP.Panel["result"] = undefined;
+ });
+
it('should accept valid registrations for toggle', function () {
let p = KorAP.Panel["result"] = panelClass.create();
diff --git a/dev/js/src/buttongroup.js b/dev/js/src/buttongroup.js
index 40eb61f..922ca1c 100644
--- a/dev/js/src/buttongroup.js
+++ b/dev/js/src/buttongroup.js
@@ -83,6 +83,7 @@
const b = this._insert('span');
let desc = title;
+ let that = this;
if (data !== undefined) {
if (data['cls'] !== undefined) {
b.classList.add.apply(b.classList, data['cls']);
@@ -98,14 +99,21 @@
if (data['active'] !== undefined) {
let active = data['active'];
+ b['active'] = active;
- let check = _addCheck(b,active);
- check.addEventListener('click', function (e) {
- // Do not bubble
- e.halt();
+ let check = _addCheck(b,active);
+ check.addEventListener('click', function (e) {
+ // Do not bubble
+ e.halt();
// Toggle state
- active.roll();
- });
+ active.roll();
+
+ let obj = that._bind || this;
+ obj.button = b;
+ b._activeClick = true;
+ cb.apply(obj, e);
+ b._activeClick = false;
+ });
};
@@ -119,10 +127,9 @@
innerSpan.addT(title);
b["changeTitle"] = function (title) {
- innerSpan.textContent = title;
+ innerSpan.textContent = title;
};
- let that = this;
b.addEventListener('click', function (e) {
// Do not bubble
diff --git a/dev/js/src/pipe.js b/dev/js/src/pipe.js
index f10853e..241b126 100644
--- a/dev/js/src/pipe.js
+++ b/dev/js/src/pipe.js
@@ -37,7 +37,7 @@
*/
append : function (service) {
service = _notNull(service);
- if (service) {
+ if (service && this._pipe.indexOf(service) === -1) {
this._pipe.push(service);
this._update();
};
@@ -49,7 +49,7 @@
*/
prepend : function (service) {
service = _notNull(service);
- if (service) {
+ if (service && this._pipe.indexOf(service) === -1) {
this._pipe.unshift(service);
this._update();
};
diff --git a/dev/js/src/plugin/server.js b/dev/js/src/plugin/server.js
index f8c9855..ab84ed3 100644
--- a/dev/js/src/plugin/server.js
+++ b/dev/js/src/plugin/server.js
@@ -161,6 +161,55 @@
// "this".button is the button
// "that" is the server object
+ // Active-check click: load iframe as a background service,
+ // not as a visible widget.
+ if (this.button._activeClick && 'active' in this.button) {
+
+ // Service already running - just return
+ // (active.roll() was already called in buttongroup)
+ if (this.button['widgetID'] &&
+ services[this.button['widgetID']]) {
+ return;
+ };
+
+ // For addWidget mode (no state), just toggle - no service
+ if (!('state' in this.button)) {
+ return;
+ };
+
+ // Create a background service (iframe in services container)
+ let id = that.addService({
+ "name": name,
+ "src": onClick["template"],
+ "permissions": onClick["permissions"]
+ });
+ plugin["widgets"].push(id);
+
+ this.button['widgetID'] = id;
+
+ // Store panel reference so 'get Active' works
+ services[id].panel = this;
+
+ let activeState = this.button.active;
+ let iframe = services[id].load();
+
+ iframe.onload = function () {
+ activeState.associate({
+ setState : function (value) {
+ if (services[id]) {
+ services[id].sendMsg({
+ action: 'state',
+ key : 'active',
+ value : value
+ });
+ };
+ }
+ });
+ };
+
+ return;
+ };
+
// 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) {
@@ -182,15 +231,26 @@
}
};
+ // If a background service exists (from active-check),
+ // close it before creating the widget
+ if ('active' in this.button &&
+ this.button['widgetID'] &&
+ services[this.button['widgetID']] &&
+ !services[this.button['widgetID']].isWidget) {
+ that._closeService(this.button['widgetID']);
+ this.button['widgetID'] = undefined;
+ };
+
// 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
+ "desc":desc,
+ "panel":panel
});
plugin["widgets"].push(id);
-
+
// If a state exists, associate with a mediator object
if ('state' in this.button) {
this.button['widgetID'] = id;
@@ -205,7 +265,29 @@
};
}
});
- }
+ };
+
+ // If an active state exists, associate it with the widget
+ // so active-state changes can be reflected in the iframe.
+ if ('active' in this.button) {
+ this.button['widgetID'] = id;
+ let first = true;
+ this.button.active.associate({
+ setState : function (value) {
+ if (first) {
+ first = false;
+ return;
+ };
+ if (services[id]) {
+ services[id].sendMsg({
+ action: 'state',
+ key : 'active',
+ value : value
+ });
+ };
+ }
+ });
+ };
};
@@ -215,6 +297,11 @@
if (embed['desc'] != undefined)
obj['desc'] = embed['desc'];
+ if (onClick['active'] !== undefined) {
+ obj['active'] = stateClass.create([true, false]);
+ obj['active'].setIfNotYet(onClick['active'] ? true : false);
+ };
+
if (onClick["action"] && onClick["action"] == "setWidget") {
// Create a boolean state value,
@@ -559,6 +646,14 @@
v["page"] = pi.page();
v["total"] = pi.total();
v["count"] = pi.count();
+ }
+
+ // Get active toggle state of the widget button
+ else if (d.key == 'Active') {
+ let button = services[d["originID"]].panel.button;
+ if (button && button.active) {
+ d["value"] = button.active.get();
+ };
};
// data needs to be mirrored
@@ -596,6 +691,24 @@
// if (v["cq"] != undefined) {};
}
+ else if (d.key == "Title") {
+ // TODO: Only support "Add title"!
+ let v = d["value"];
+ services[d["originID"]].panel.button.changeTitle(v);
+ }
+
+ else if (d.key == "Active") {
+ let v = d["value"];
+ let button = services[d["originID"]].panel.button;
+ if (button && button.active) {
+ if (v !== undefined) {
+ button.active.set(v);
+ } else {
+ button.active.roll();
+ };
+ };
+ };
+
break;
// Redirect to a different page relative to the current
diff --git a/dev/js/src/plugin/service.js b/dev/js/src/plugin/service.js
index ef6836c..7b4e998 100644
--- a/dev/js/src/plugin/service.js
+++ b/dev/js/src/plugin/service.js
@@ -107,10 +107,12 @@
*/
sendMsg : function (d) {
let iframe = this.load();
- iframe.contentWindow.postMessage(
- d,
- '*'
- ); // TODO: Fix origin
+ if (iframe && iframe.contentWindow) {
+ iframe.contentWindow.postMessage(
+ d,
+ '*'
+ ); // TODO: Fix origin
+ };
},
// onClose : function () {},