Support toggle+widget buttons in plugin-framework
Change-Id: Ibf2b80efc1eeda2f51dedf4a7f3cd5ac67325ef9
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 () {},