' + qx.locale.Manager.tr("Delete all") + '
'
+ });
+ qx.dom.Element.insertEnd(elem, body);
+
+ var template = qx.dom.Element.create("script", {
+ id: "MessageTemplate",
+ type: "text/template",
+ html: '
{{#title}}
{{/title}}{{#deletable}}
x
{{/deletable}}
{{&message}}
'
+ });
+ qx.dom.Element.insertEnd(template, body);
+ }
this.__messagesContainer = qx.bom.Selector.query("section.messages", elem)[0];
this.__badge = qx.bom.Selector.query(".badge", elem)[0];
@@ -236,16 +245,12 @@ qx.Class.define("cv.ui.NotificationCenter", {
// add HTML template for messages to header
- var template = qx.dom.Element.create("script", {
- id: "MessageTemplate",
- type: "text/template",
- html: '
{{#title}}
{{/title}}
{{&message}} {{#deletable}}
{{/deletable}}
'
- });
- qx.dom.Element.insertEnd(template, body);
- this.__list = new qx.data.controller.website.List(this.__messages, this.__messagesContainer, "MessageTemplate");
+
+ this._list = new qx.data.controller.website.List(this._messages, this.__messagesContainer, "MessageTemplate");
+ qx.event.Registration.addListener(this.__messagesContainer, "tap", this._onListTap, this);
// connect badge content
- this.__messages.addListener("changeLength", this.__updateBadge, this);
+ this._messages.addListener("changeLength", this.__updateBadge, this);
// update dimensions
new qx.util.DeferredCall(this._onResize, this).schedule();
@@ -253,46 +258,37 @@ qx.Class.define("cv.ui.NotificationCenter", {
__updateBadge: function() {
var currentContent = parseInt(qx.bom.element.Attribute.get(this.__badge, "html"));
- this.setCounter(this.__messages.length);
- if (currentContent < this.__messages.length) {
+ var messages = this.getMessages().getLength();
+ if (this.getMessages().length === 0) {
+ // close center if empty
+ qx.event.Timer.once(function() {
+ // still empty
+ if (messages === 0) {
+ this.hide();
+ }
+ }, this, 1000);
+ }
+ if (currentContent < messages) {
// blink to get the users attention for the new message
qx.bom.element.Animation.animate(this.__badge, cv.ui.NotificationCenter.BLINK);
}
- if (this.__messages.length) {
- qx.bom.element.Attribute.set(this.__badge, "html", ""+this.__messages.length);
+ if (messages) {
+ qx.bom.element.Attribute.set(this.__badge, "html", ""+messages);
} else{
qx.bom.element.Attribute.set(this.__badge, "html", "");
}
- // get the highest severity
- var severityRank = -1;
- this.__messages.forEach(function(message) {
- if (message.severity && this.__severities.indexOf(message.severity) > severityRank) {
- severityRank = this.__severities.indexOf(message.severity);
- }
- }, this);
- qx.bom.element.Class.removeClasses(this.__badge, this.__severities);
- if (severityRank >= 0) {
- this.setGlobalSeverity(this.__severities[severityRank]);
- qx.bom.element.Class.add(this.__badge, this.__severities[severityRank]);
- } else {
- this.resetGlobalSeverity();
- }
- // update favicon badge
- this.__favico.badge(this.__messages.length, {
- bgColor: this.__getSeverityColor(this.__severities[severityRank])
- });
+
},
- __getSeverityColor: function(severity) {
- switch(severity) {
- case "urgent":
- return "#FF0000";
- case "high":
- return "#FF7900";
- default:
- return "#1C391C";
- }
+ _onSeverityChange: function(ev) {
+ qx.bom.element.Class.removeClasses(this.__badge, this._severities);
+ qx.bom.element.Class.add(this.__badge, ev.getData());
+
+ // update favicon badge
+ this.__favico.badge(this.getMessages().getLength(), {
+ bgColor: this.getSeverityColor(ev.getData())
+ });
},
/**
@@ -334,81 +330,6 @@ qx.Class.define("cv.ui.NotificationCenter", {
this.__blocker.unblock();
}, this);
}
- },
-
- _applyMaxEntries: function(value) {
- this.__messages.setMaxEntries(value);
- },
-
- handleMessage: function(message) {
- var found = null;
- if (message.unique) {
- // check if message is already shown
- this.__messages.some(function(msg, index) {
- if (message.topic === msg.topic) {
- // replace message
- found = msg;
- message.id = this.__messages.length;
- if (!message.hasOwnProperty("deletable")) {
- message.deletable = true;
- }
- if (cv.core.notifications.Router.evaluateCondition(message)) {
- this.__messages.setItem(index, message);
- } else{
- this.__messages.removeAt(index);
- }
- // stop search
- return true;
- }
- }, this);
- }
- if (!found) {
- if (cv.core.notifications.Router.evaluateCondition(message)) {
- message.id = this.__messages.length;
- if (!message.hasOwnProperty("deletable")) {
- message.deletable = true;
- }
- this.__messages.push(message);
- }
- } else {
- // refresh list
- this.__list.update();
- }
- },
-
- clear: function() {
- // collect all deletable messages
- var deletable = this.__messages.filter(function(message) {
- return message.deletable === true;
- }, this);
- this.__messages.exclude(deletable);
- },
-
- /**
- * Delete a message by index
- * @param index {Number}
- */
- deleteMessage: function(index) {
- var message = this.__messages.getItem(index);
- if (message.deletable === true) {
- this.__messages.removeAt(index);
- }
- },
-
- performAction: function(messageId) {
- var message = this.__messages.getItem(messageId);
- var action = message.action;
- if (action.needsConfirmation) {
- // TODO: open confirm dialog
- } else {
- this.__performAction(action);
- }
- },
-
- __performAction: function(action) {
- if (action.callback) {
- action.callback.call(action.callback || this, action.params);
- }
}
},
@@ -417,9 +338,10 @@ qx.Class.define("cv.ui.NotificationCenter", {
DESTRUCTOR
*****************************************************************************
*/
- destruct: function () {
+ destruct: /* istanbul ignore next [destructor not called in singleton] */ function () {
qx.event.Registration.removeListener(window, "resize", this._onResize, this);
qx.event.Registration.removeListener(this.__blocker.getBlockerElement(), "tap", this.hide, this);
- this._disposeObjects("__blocker");
+ qx.event.Registration.removeListener(this.__messagesContainer, "tap", this._onListTap, this);
+ this._disposeObjects("__blocker", "__messagesContainer");
}
});
diff --git a/source/class/cv/ui/Popup.js b/source/class/cv/ui/Popup.js
index fe06ea471f6..b4cfd0aafac 100644
--- a/source/class/cv/ui/Popup.js
+++ b/source/class/cv/ui/Popup.js
@@ -32,6 +32,7 @@ qx.Class.define('cv.ui.Popup', {
this.setType(type);
}
this.__deactivateSelectors = ['#top', '#navbarTop', '#centerContainer', '#navbarBottom', '#bottom'];
+ this.__elementMap = {};
},
/*
@@ -65,6 +66,7 @@ qx.Class.define('cv.ui.Popup', {
__counter: 0,
__deactivateSelectors: null,
__domElement: null,
+ __elementMap: null,
getCurrentDomElement: function() {
return this.__domElement;
@@ -80,49 +82,117 @@ qx.Class.define('cv.ui.Popup', {
cv.ui.BodyBlocker.getInstance().block();
var closable = !attributes.hasOwnProperty("closable") || attributes.closable;
var body = qx.bom.Selector.query('body')[0];
- var ret_val = this.__domElement = qx.dom.Element.create("div", {
- id: "popup_"+this.__counter,
- "class": "popup popup_background "+this.getType(),
- style: "display:none",
- html: closable ? '' : ""
- });
- qx.dom.Element.insertEnd(ret_val, body);
+ var ret_val;
+ var classes = ["popup", "popup_background", this.getType()];
+ var isNew = true;
+ var addCloseListeners = false;
+ if (attributes.type) {
+ classes.push(attributes.type);
+ }
+
+ if (!this.__domElement) {
+ ret_val = this.__domElement = qx.dom.Element.create("div", {
+ id: "popup_" + this.__counter,
+ "class": classes.join(" "),
+ style: "display:none",
+ html: closable ? '' : ""
+ });
+ qx.dom.Element.insertEnd(ret_val, body);
+ this.__elementMap.close = qx.bom.Selector.query("div.popup_close", ret_val);
+ addCloseListeners = true;
+ } else {
+ isNew = false;
+ ret_val = this.__domElement;
+ qx.bom.element.Attribute.set(ret_val, "class", classes.join(" "));
+ if (closable && !this.__elementMap.close) {
+ this.__domElement.close = qx.dom.Element.create("div", {"class": "popup_close", "html": "X"});
+ qx.dom.Element.insertBegin(this.__domElement.close, body);
+ addCloseListeners = true;
+ } else if (!closable) {
+ this.destroyElement("close");
+ }
+ }
if (attributes.title) {
- var title = qx.dom.Element.create("div", { "class": "head"});
- qx.dom.Element.insertEnd(title, ret_val);
+ if (!this.__elementMap.title) {
+ this.__elementMap.title = qx.dom.Element.create("div", {"class": "head"});
+ qx.dom.Element.insertEnd(this.__elementMap.title, ret_val);
+ }
if (qx.lang.Type.isString(attributes.title)) {
- qx.bom.element.Attribute.set(title, "html", ""+attributes.title);
+ qx.bom.element.Attribute.set(this.__elementMap.title, "html", "" + attributes.title);
} else {
- qx.dom.Element.insertEnd(attributes.title, title);
+ qx.dom.Element.insertEnd(attributes.title, this.__elementMap.title);
}
}
- if (attributes.content) {
- var content = qx.dom.Element.create("div", { "class": "main"});
- qx.dom.Element.insertEnd(content, ret_val);
- if (qx.lang.Type.isString(attributes.content)) {
- var html = ""+attributes.content;
- if (attributes.icon) {
- var icon = qx.util.ResourceManager.getInstance().toUri("icon/knx-uf-iconset.svg")+"#kuf-"+attributes.icon;
- html = '
'+html;
+ if (attributes.content || attributes.icon || attributes.progress) {
+ if (!this.__elementMap.content) {
+ this.__elementMap.content = qx.dom.Element.create("div", {"class": "main"});
+ qx.dom.Element.insertEnd(this.__elementMap.content, ret_val);
+ }
+
+ if (attributes.icon) {
+ if (!this.__elementMap.icon) {
+ this.__elementMap.icon = qx.dom.Element.create("div", {"html": cv.util.IconTools.svgKUF(attributes.icon)(null, null, "icon " + attributes.iconClasses)});
+ qx.dom.Element.insertBegin(this.__elementMap.icon, this.__elementMap.content);
+ } else {
+ var use = qx.bom.Selector.query("use", this.__elementMap.icon)[0];
+ var currentIconPath = qx.bom.element.Attribute.get(use, "xlink:href");
+ if (!currentIconPath.endsWith("#kuf-"+attributes.icon)) {
+ var parts = currentIconPath.split("#");
+ qx.bom.element.Attribute.set(use, "xlink:href", parts[0]+"#kuf-"+attributes.icon);
+ }
+ }
+ } else {
+ this.destroyElement("icon");
+ }
+ if (attributes.content) {
+ if (!this.__elementMap.messageContent) {
+ this.__elementMap.messageContent = qx.dom.Element.create("div", {"class": "message"});
+ qx.dom.Element.insertBegin(this.__elementMap.messageContent, this.__elementMap.content);
+ }
+ if (qx.lang.Type.isString(attributes.content)) {
+ qx.bom.element.Attribute.set(this.__elementMap.messageContent, "html", attributes.content);
+ } else {
+ qx.dom.Element.replaceChild(attributes.content, this.__elementMap.messageContent);
+ this.__elementMap.messageContent = attributes.content;
+ }
+ } else {
+ this.destroyElement("messageContent");
+ }
+
+ if (attributes.progress) {
+ if (!this.__elementMap.progress) {
+ var bar = new cv.ui.util.ProgressBar();
+ this.__elementMap.progress = bar.getDomElement();
+ qx.dom.Element.insertEnd(this.__elementMap.progress, this.__elementMap.content);
}
- qx.bom.element.Attribute.set(content, "html", html);
+ this.__elementMap.progress.$$widget.setValue(attributes.progress);
} else {
- qx.dom.Element.insertEnd(attributes.content, content);
+ this.destroyElement("progress");
}
}
if (attributes.actions && Object.getOwnPropertyNames(attributes.actions).length > 0) {
- var actions = qx.dom.Element.create("div", {"class": "actions"});
+ if (!this.__elementMap.actions) {
+ this.__elementMap.actions = qx.dom.Element.create("div", {"class": "actions"});
+ qx.dom.Element.insertEnd(this.__elementMap.actions, ret_val);
+ } else {
+ // clear content
+ qx.bom.element.Attribute.set(this.__elementMap.actions, "html", "");
+ }
- Object.getOwnPropertyNames(attributes.actions).forEach(function(type) {
- var actionButton = cv.core.notifications.ActionRegistry.createActionElement(type, attributes.actions[type]);
- qx.dom.Element.insertEnd(actionButton, actions);
- });
- qx.dom.Element.insertEnd(actions, ret_val);
+ Object.getOwnPropertyNames(attributes.actions).forEach(function (type) {
+ var typeActions = qx.lang.Type.isArray(attributes.actions[type]) ? attributes.actions[type] : [attributes.actions[type]];
+ typeActions.forEach(function (action) {
+ var actionButton = cv.core.notifications.ActionRegistry.createActionElement(type, action);
+ qx.dom.Element.insertEnd(actionButton, this.__elementMap.actions);
+ }, this);
+ }, this);
+ } else {
+ this.destroyElement("actions");
}
if (attributes.width) {
@@ -173,7 +243,7 @@ qx.Class.define('cv.ui.Popup', {
qx.bom.element.Style.set(ret_val, 'left', placement.x);
qx.bom.element.Style.set(ret_val, 'top', placement.y);
- if (closable) {
+ if (closable && addCloseListeners) {
this.addListener('close', this.close, this);
qx.event.Registration.addListener(ret_val, 'tap', function () {
// note: this will call two events - one for the popup itself and
@@ -186,12 +256,21 @@ qx.Class.define('cv.ui.Popup', {
}, this);
}
- qx.bom.element.Style.set(ret_val, 'display', 'block');
attributes.id = this.__counter;
- this.__counter++;
+ if (isNew) {
+ qx.bom.element.Style.set(ret_val, 'display', 'block');
+ this.__counter++;
+ }
return ret_val;
},
+ destroyElement: function(name) {
+ if (this.__elementMap[name]) {
+ qx.dom.Element.remove(this.__elementMap[name]);
+ delete this.__elementMap[name];
+ }
+ },
+
/**
* Closes this popup
*/
@@ -200,11 +279,21 @@ qx.Class.define('cv.ui.Popup', {
if (this.__domElement) {
qx.dom.Element.remove(this.__domElement);
this.__domElement = null;
+ this.__elementMap = {};
}
},
isClosed: function(){
return this.__domElement === null;
}
+ },
+
+ /*
+ ******************************************************
+ DESTRUCTOR
+ ******************************************************
+ */
+ destruct: function() {
+ this.close();
}
});
\ No newline at end of file
diff --git a/source/class/cv/ui/PopupHandler.js b/source/class/cv/ui/PopupHandler.js
index 39fd11d0e8c..342c50da6a9 100644
--- a/source/class/cv/ui/PopupHandler.js
+++ b/source/class/cv/ui/PopupHandler.js
@@ -57,8 +57,11 @@ qx.Class.define('cv.ui.PopupHandler', {
title: message.title,
content: message.message,
closable: message.deletable,
- icon: config.icon,
- actions: message.actions
+ icon: message.icon || config.icon,
+ iconClasses: message.iconClasses,
+ actions: message.actions,
+ progress: message.progress,
+ type: "notification"
};
// popups are always unique
if (cv.core.notifications.Router.evaluateCondition(message)) {
@@ -80,9 +83,9 @@ qx.Class.define('cv.ui.PopupHandler', {
*/
showPopup: function (type, attributes) {
var popup = this.getPopup(type);
- if (!popup.isClosed()) {
- popup.close();
- }
+ // if (!popup.isClosed()) {
+ // popup.close();
+ // }
popup.create(attributes);
return popup;
},
diff --git a/source/class/cv/ui/ToastManager.js b/source/class/cv/ui/ToastManager.js
new file mode 100644
index 00000000000..be268929325
--- /dev/null
+++ b/source/class/cv/ui/ToastManager.js
@@ -0,0 +1,123 @@
+/**
+ * Handles toast positioning in the gui.
+ */
+qx.Class.define("cv.ui.ToastManager", {
+ extend: qx.core.Object,
+ implement: cv.core.notifications.IHandler,
+ include: cv.ui.MHandleMessage,
+ type: "singleton",
+
+ /*
+ ******************************************************
+ CONSTRUCTOR
+ ******************************************************
+ */
+ construct: function() {
+ this.base(arguments);
+ this.set({
+ rootElementId: "toast-list",
+ messageElementId: "toast_"
+ });
+
+ this.setDelegate({
+ prepareMessage: function(message) {
+ // all toast messages need a duration
+ if (!message.hasOwnProperty("duration")) {
+ message.duration = this.getMessageDuration();
+ }
+ }.bind(this),
+ postHandleMessage: function(message, config, payload) {
+ if (payload.action === "added" || payload.action === "replaced") {
+ // add removal listener
+ qx.event.Timer.once(function() {
+ this.getMessages().remove(message);
+ }, this, message.duration);
+ }
+ }.bind(this)
+ });
+
+ // as the Mixins constructor has not been called yet, the messages array has not been initialized
+ // so we defer this call here to make sure everything is in place
+ new qx.util.DeferredCall(function() {
+ cv.TemplateEngine.getInstance().executeWhenDomFinished(this._init, this);
+ }, this).schedule();
+ },
+
+ /*
+ ******************************************************
+ PROPERTIES
+ ******************************************************
+ */
+ properties: {
+
+ /**
+ * Default time in MS a toast message is visible
+ */
+ messageDuration: {
+ check: "Number",
+ init: 5000
+ }
+ },
+
+
+ /*
+ ******************************************************
+ MEMBERS
+ ******************************************************
+ */
+ members: {
+ __domElement: null,
+ __timer: null,
+ __opened: false,
+
+ /**
+ * Attach to dom element and style it
+ */
+ _init: function() {
+ if (!this.__domElement) {
+ // check if there is one (might be restored from cache)
+ this.__domElement = qx.bom.Selector.query(this.getRootElementId())[0];
+ if (!this.__domElement) {
+ this.__domElement = qx.dom.Element.create("div", {"id": this.getRootElementId()});
+ }
+ }
+ if (qx.bom.Selector.query(this.getRootElementId()).length === 0) {
+ qx.dom.Element.insertEnd(this.__domElement, document.body);
+ }
+ if (qx.bom.Selector.query("#ToastTemplate").length === 0) {
+ var template = qx.dom.Element.create("script", {
+ id: "ToastTemplate",
+ type: "text/template",
+ html: '
'
+ });
+ qx.dom.Element.insertEnd(template, document.body);
+ }
+ this._list = new qx.data.controller.website.List(this._messages, this.__domElement, "ToastTemplate");
+ qx.event.Registration.addListener(this.__domElement, "tap", this._onListTap, this);
+ },
+
+ _performAction: function(message) {
+ if (message.actions) {
+ return false;
+ }
+ // default is to delete the toast
+ this.deleteMessage(message.id);
+ }
+ },
+
+ /*
+ ******************************************************
+ DESTRUCTOR
+ ******************************************************
+ */
+ destruct: function() {
+ if (this.__timer) {
+ this.__timer.stop();
+ this.__timer = null;
+ }
+ if (this.__domElement) {
+ qx.dom.Element.remove(this.__domElement);
+ this.__domElement = null;
+ }
+ }
+});
\ No newline at end of file
diff --git a/source/class/cv/ui/util/ProgressBar.js b/source/class/cv/ui/util/ProgressBar.js
new file mode 100644
index 00000000000..f6998d316c7
--- /dev/null
+++ b/source/class/cv/ui/util/ProgressBar.js
@@ -0,0 +1,80 @@
+/* NotificationCenter.js
+ *
+ * copyright (c) 2010-2017, Christian Mayer and the CometVisu contributers.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+
+/**
+ * Shows a progressbar to visualize a state that is completed by x %
+ *
+ * @author Tobias Bräutigam
+ * @since 0.11.0
+ */
+qx.Class.define("cv.ui.util.ProgressBar", {
+ extend: qx.core.Object,
+
+ /*
+ ******************************************************
+ CONSTRUCTOR
+ ******************************************************
+ */
+ construct: function() {
+ this.base(arguments);
+ this._createDomElement();
+ },
+
+ /*
+ ******************************************************
+ PROPERTIES
+ ******************************************************
+ */
+ properties: {
+ value: {
+ check: "Integer",
+ init: 0,
+ apply: "_applyValue"
+ }
+ },
+
+ /*
+ ******************************************************
+ MEMBERS
+ ******************************************************
+ */
+ members: {
+ __domElement: null,
+ __progressElement: null,
+
+ _applyValue: function(value) {
+ var totalWidth = qx.bom.element.Dimension.getContentWidth(this.__domElement);
+ var progressWidth = Math.round(totalWidth*value/100)+"px";
+ qx.bom.element.Style.set(this.__progressElement, "width", progressWidth);
+ },
+
+ getDomElement: function() {
+ return this.__domElement;
+ },
+
+ _createDomElement: function() {
+ var container = this.__domElement = qx.dom.Element.create("div", { "class": "progressbar" });
+ this.__domElement.$$widget = this;
+ var progress = this.__progressElement = qx.dom.Element.create("div", { "class": "completed" });
+ qx.dom.Element.insertEnd(progress, container);
+ return container;
+ }
+ }
+});
\ No newline at end of file
diff --git a/source/class/cv/util/IconTools.js b/source/class/cv/util/IconTools.js
index 2dcd88e915a..f4b5349d061 100644
--- a/source/class/cv/util/IconTools.js
+++ b/source/class/cv/util/IconTools.js
@@ -245,9 +245,12 @@ qx.Class.define('cv.util.IconTools', {
}
var iconPath = qx.util.ResourceManager.getInstance().toUri('icon/knx-uf-iconset.svg');
- var style = '';
+ var style = styling;
if (color) {
- style = 'style="color:' + color + '" ';
+ style += 'color:' + color + ';';
+ }
+ if (style) {
+ style = ' style="'+style+'" ';
}
return '
';
};
diff --git a/source/class/cv/util/Location.js b/source/class/cv/util/Location.js
index e29b5072a8d..e204923377e 100644
--- a/source/class/cv/util/Location.js
+++ b/source/class/cv/util/Location.js
@@ -55,6 +55,16 @@ qx.Class.define('cv.util.Location', {
*/
reload: function(value) {
window.location.reload(value);
+ },
+
+ /**
+ * Wrapper for calling
window.open()
+ *
+ * @param url {String} url to open
+ * @param target {String} where to open the window
+ */
+ open: function(url, target) {
+ window.open(url, target);
}
}
diff --git a/source/resource/designs/designglobals.css b/source/resource/designs/designglobals.css
index e5aadac11d4..68ed2b31875 100755
--- a/source/resource/designs/designglobals.css
+++ b/source/resource/designs/designglobals.css
@@ -293,19 +293,19 @@ button.ui-slider-handle {
}
/*severities*/
-#notification-center > .badge.urgent {
+#notification-center > .badge.urgent, #toast-list .toast.urgent {
background-color: rgba(255, 0, 0, 0.9);
}
-#notification-center > .badge.high {
+#notification-center > .badge.high, #toast-list .toast.high {
background-color: rgba(255, 121, 0, 0.9);
}
-#notification-center > .badge.normal {
+#notification-center > .badge.normal, #toast-list .toast.normal {
background-color: rgba(255, 244, 230, 0.9);
}
-#notification-center > .badge.low {
+#notification-center > .badge.low, #toast-list .toast.low {
background-color: rgba(61, 61, 61, 0.9);
}
@@ -349,11 +349,11 @@ button.ui-slider-handle {
font-weight: bold;
}
-#notification-center footer:hover {
+#notification-center footer .clear:hover {
background-color: rgba(0, 0, 0, 0.2);
}
-#notification-center footer::before {
+#notification-center footer .clear::before {
content: "X ";
}
@@ -378,27 +378,28 @@ button.ui-slider-handle {
min-height: 25px;
}
-#notification-center .message:last-child {
- border-bottom: none;
+#notification-center .message.selectable, #notification-center .message .delete {
+ cursor: pointer;
}
-#notification-center .message .content {
- float: left;
- width: 100%;
+#notification-center .message:last-child {
+ border-bottom: none;
}
#notification-center .message .delete {
float: right;
+ font-weight: bold;
+ height: 100%;
+ margin-left: 1em;
+ text-transform: uppercase;
}
#notification-center a {
color: #fff;
}
-.popup.error,
-.popup_background.error,
-.popup.info,
-.popup_background.info {
+.popup.notification,
+.popup_background.notification {
position: absolute;
padding: 30px;
top: 50%;
@@ -442,7 +443,7 @@ button.ui-slider-handle {
border-bottom: 1px solid;
}
-.popup .main .icon {
+.popup .main .icon:not(.spinner) {
float: left;
margin: -1em 1em 0 0;
vertical-align: middle;
@@ -450,15 +451,34 @@ button.ui-slider-handle {
height: 100%;
}
+.popup .main .icon.spinner {
+ vertical-align: middle;
+ align: center;
+ width: 100%;
+ margin-top: 20px;
+ margin-bottom: 10px;
+ animation: spinner 2s linear infinite;
+}
+
+@keyframes spinner {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
+
+.popup.notification .main .message {
+ margin: 1em 0;
+}
+
.popup .main pre {
overflow: auto;
font-size: 0.8em;
}
-.popup .actions { text-align: center; }
+.popup .actions { text-align: center; white-space: nowrap; clear: both; }
.popup .actions button {
padding: 0.3em 1em;
font-size: 0.9em;
+ margin: 0 0.5em;
}
.popup_background.error {
@@ -469,4 +489,46 @@ button.ui-slider-handle {
.popup_background.error a { color: #FFF; }
-pre.inline { display: inline-block; }
\ No newline at end of file
+pre.inline { display: inline-block; }
+
+/* progressbar */
+.popup div.progressbar {
+ height: 10px;
+ background-color: white;
+ padding: 1px;
+ margin: 10px 5px;
+ position: relative;
+}
+
+.popup div.progressbar div.completed {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ background-color: grey;
+ margin: 1px;
+}
+#qxsettings {
+ position: absolute;
+ top: revert;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+#toast-list {
+ position: absolute;
+ right: 100px;
+ bottom: 10px;
+}
+
+#toast-list .toast {
+ background-color: rgba(61, 61, 61, 0.9);
+ padding: 8px 18px;
+ margin: 10px 0;
+ border-radius: 5px;
+ overflow: hidden;
+ width: 200px;
+ font-size: 14px;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/source/resource/visu_config.xsd b/source/resource/visu_config.xsd
index e77c6011f54..0c21bbfbf7d 100644
--- a/source/resource/visu_config.xsd
+++ b/source/resource/visu_config.xsd
@@ -561,7 +561,7 @@
-
+
Template for message title
Vorlage für Nachrichtentitel
@@ -606,6 +606,12 @@
Nachricht im Notification-Center anzeigen
+
+
+ Say message with text-to-speech
+ Nachricht per Sprachausgabe ausgeben
+
+
@@ -628,6 +634,10 @@
+
+ Messages with higher priority are marked with different colors
+ Nachrichten mit höherer Priorität werden farblich gekennzeichnet
+
diff --git a/source/test/karma/core/notifications/ActionRegistry-spec.js b/source/test/karma/core/notifications/ActionRegistry-spec.js
new file mode 100644
index 00000000000..0529d2adbcd
--- /dev/null
+++ b/source/test/karma/core/notifications/ActionRegistry-spec.js
@@ -0,0 +1,17 @@
+
+
+describe('test the ActionRegistry', function () {
+
+ it("should register an action handler", function() {
+ var actionHandler = function() {
+ this.getDomElement = function() {
+ return "test";
+ };
+ };
+
+ cv.core.notifications.ActionRegistry.registerActionHandler("test", actionHandler);
+ expect(cv.core.notifications.ActionRegistry.createActionElement("unknown")).toBeNull();
+ expect(cv.core.notifications.ActionRegistry.createActionElement("test"), {}).toEqual("test");
+ cv.core.notifications.ActionRegistry.unregisterActionHandler("test");
+ });
+});
\ No newline at end of file
diff --git a/source/test/karma/core/notifications/router-spec.js b/source/test/karma/core/notifications/Router-spec.js
similarity index 67%
rename from source/test/karma/core/notifications/router-spec.js
rename to source/test/karma/core/notifications/Router-spec.js
index b026e1d20ba..851429c4cb8 100644
--- a/source/test/karma/core/notifications/router-spec.js
+++ b/source/test/karma/core/notifications/Router-spec.js
@@ -33,17 +33,20 @@ describe('test the notification router', function () {
});
it("should test the routing", function() {
+ var callCounter = 0;
qx.Class.define("cv.test.MessageHandler", {
extend: qx.core.Object,
implement: cv.core.notifications.IHandler,
members: {
- handleMessage: function() {}
+ handleMessage: function() {
+ callCounter++;
+ }
}
});
var handler = new cv.test.MessageHandler();
- var spiedHandleMessage = spyOn(handler, "handleMessage");
+ var spiedHandleMessage = spyOn(handler, "handleMessage").and.callThrough();
router.registerMessageHandler(handler, {
"test.message": {},
@@ -67,6 +70,23 @@ describe('test the notification router', function () {
router.dispatchMessage("test.wildcard.anything.thats.possible", {});
expect(spiedHandleMessage).toHaveBeenCalled();
+
+ // get target from message
+ var spy = spyOn(cv.ui.PopupHandler, "handleMessage");
+ router.dispatchMessage("test.message", {target: "popup"});
+ expect(spy).toHaveBeenCalled();
+
+ spiedHandleMessage.calls.reset();
+ // test unknown topic
+ router.dispatchMessage("unknown.message", {});
+ expect(spiedHandleMessage).not.toHaveBeenCalled();
+
+ // for some reason the spy does not count the number of calls right, so we use our own counter
+ callCounter = 0;
+ // dispatch with wildcard
+ router.dispatchMessage("test.*", {});
+ expect(spy).toHaveBeenCalled();
+ expect(callCounter).toEqual(2);
});
it("should test the state notification handling", function() {
@@ -109,5 +129,22 @@ describe('test the notification router', function () {
popup = qx.bom.Selector.query("#popup_0")[0];
// as the condition isn't met anymore the popup must be gone
expect(popup).toBeUndefined();
+
+ qx.Class.undefine("cv.test.MessageHandler");
+ router.unregisterStateUpdatehandler(["0/0/1"]);
+ });
+
+ it("should test the target mapping", function() {
+ expect(cv.core.notifications.Router.getTarget("popup")).toEqual(cv.ui.PopupHandler);
+ expect(cv.core.notifications.Router.getTarget("notificationCenter")).toEqual(cv.ui.NotificationCenter.getInstance());
+
+ // prevent speech target if no browser support
+ var speechSynthesis = window.speechSynthesis;
+ delete window.speechSynthesis;
+ expect(cv.core.notifications.Router.getTarget("speech")).toBeUndefined();
+ window.speechSynthesis = speechSynthesis;
+ expect(cv.core.notifications.Router.getTarget("speech")).toEqual(cv.core.notifications.SpeechHandler.getInstance());
+
+ expect(cv.core.notifications.Router.getTarget("unknown")).toBeNull();
});
});
\ No newline at end of file
diff --git a/source/test/karma/core/notifications/SpeechHandler-spec.js b/source/test/karma/core/notifications/SpeechHandler-spec.js
new file mode 100644
index 00000000000..58cca6aef53
--- /dev/null
+++ b/source/test/karma/core/notifications/SpeechHandler-spec.js
@@ -0,0 +1,55 @@
+
+
+describe('test the SpeechHandler', function () {
+ var handler = null;
+ var spiedSay = null;
+
+ beforeEach(function() {
+ handler = cv.core.notifications.SpeechHandler.getInstance();
+ spiedSay = spyOn(handler, "say");
+ });
+
+ it("should handle a message", function() {
+ var message = {
+ topic: "cv.test",
+ message: "test message"
+ };
+ var config = {
+ skipInitial: true
+ };
+
+ handler.handleMessage(message, config);
+ // skip initial message
+ expect(spiedSay).not.toHaveBeenCalled();
+
+ // second call should work
+ handler.handleMessage(message, config);
+ expect(spiedSay).toHaveBeenCalled();
+ spiedSay.calls.reset();
+
+ message.message = "";
+ // nothing to say
+ handler.handleMessage(message, config);
+ expect(spiedSay).not.toHaveBeenCalled();
+ message.message = "test message";
+
+ // test condition
+ message.condition = false;
+ handler.handleMessage(message, config);
+ expect(spiedSay).not.toHaveBeenCalled();
+ message.condition = true;
+ handler.handleMessage(message, config);
+ expect(spiedSay).toHaveBeenCalled();
+ spiedSay.calls.reset();
+
+ // test repeat timeout
+ config.repeatTimeout = 0;
+ handler.handleMessage(message, config);
+ expect(spiedSay).not.toHaveBeenCalled();
+ // override by text
+ message.message = "!"+message.message;
+ handler.handleMessage(message, config);
+ expect(spiedSay).toHaveBeenCalled();
+ spiedSay.calls.reset();
+ });
+});
\ No newline at end of file
diff --git a/source/test/karma/core/notifications/actions/Link-spec.js b/source/test/karma/core/notifications/actions/Link-spec.js
new file mode 100644
index 00000000000..48cb7a7e0c6
--- /dev/null
+++ b/source/test/karma/core/notifications/actions/Link-spec.js
@@ -0,0 +1,86 @@
+
+
+describe("testing the Link action", function() {
+
+ it("should create a button DOM element and open an url on click", function() {
+ var action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ url: "http://localhost/test",
+ needsConfirmation: false
+ });
+
+ var actionButton = action.getDomElement();
+ expect(qx.bom.element.Attribute.get(actionButton, "text")).toBe("Title");
+ expect(actionButton).toHaveClass("action");
+
+ var spy = spyOn(cv.util.Location, "open");
+ var event = new qx.event.type.Event();
+ event.init(true, true);
+ event.setType("tap");
+
+ qx.event.Registration.dispatchEvent(actionButton, event);
+ expect(spy).toHaveBeenCalledWith("http://localhost/test", "_blank");
+ });
+
+ it("should transform string values of action property to functions", function() {
+ var action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ action: "reload",
+ needsConfirmation: false
+ });
+ expect(qx.lang.Type.isFunction(action.getAction())).toBeTruthy();
+
+ action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ action: "unknown",
+ needsConfirmation: false
+ });
+ expect(action.getAction()).toBeNull();
+
+ action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ action: function() {},
+ needsConfirmation: false
+ });
+ expect(qx.lang.Type.isFunction(action.getAction())).toBeTruthy();
+ });
+
+ it("should execute the actions", function() {
+ spyOn(cv.util.Location, "reload");
+ var action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ action: "reload",
+ needsConfirmation: false
+ });
+ action.handleAction();
+ expect(cv.util.Location.reload).toHaveBeenCalled();
+
+ spyOn(cv.util.Location, "open");
+ action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ url: "/test",
+ hidden: false,
+ needsConfirmation: false
+ });
+ action.handleAction();
+ expect(cv.util.Location.open).toHaveBeenCalled();
+
+ var Con = qx.io.request.Xhr;
+ var spiedXhr;
+ spyOn(qx.io.request, 'Xhr').and.callFake(function(url) {
+ var obj = new Con(url);
+ spiedXhr = spyOn(obj, "send");
+ return obj;
+ });
+
+ // open url in background
+ action = new cv.core.notifications.actions.Link({
+ title: "Title",
+ url: "/test",
+ hidden: true,
+ needsConfirmation: false
+ });
+ action.handleAction();
+ expect(spiedXhr).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/source/test/karma/core/notifications/actions/link-spec.js b/source/test/karma/core/notifications/actions/link-spec.js
deleted file mode 100644
index 2b022bab78b..00000000000
--- a/source/test/karma/core/notifications/actions/link-spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-describe("testing the Link action", function() {
-
- it("should create a button DOM element and open an url on click", function() {
- var action = new cv.core.notifications.actions.Link({
- title: "Title",
- url: "http://localhost/test",
- needsConfirmation: false
- });
-
- var actionButton = action.getDomElement();
- expect(qx.bom.element.Attribute.get(actionButton, "text")).toBe("Title");
- expect(actionButton).toHaveClass("action");
-
- var spy = spyOn(window, "open");
- var event = new qx.event.type.Event();
- event.init(true, true);
- event.setType("tap");
-
- qx.event.Registration.dispatchEvent(actionButton, event);
- expect(spy).toHaveBeenCalledWith("http://localhost/test", "_blank");
- });
-});
\ No newline at end of file
diff --git a/source/test/karma/ui/NotificationCenter-spec.js b/source/test/karma/ui/NotificationCenter-spec.js
new file mode 100644
index 00000000000..fa0413c41c5
--- /dev/null
+++ b/source/test/karma/ui/NotificationCenter-spec.js
@@ -0,0 +1,241 @@
+
+describe('test the NotificationCenter', function () {
+
+ var center = cv.ui.NotificationCenter.getInstance();
+
+ beforeEach(function() {
+ // set animation time to 0
+ cv.ui.NotificationCenter.SLIDE.duration = 0;
+ center._init();
+ });
+
+ afterEach(function() {
+ cv.ui.NotificationCenter.clear(true);
+ cv.ui.NotificationCenter.SLIDE.duration = 350;
+ });
+
+ it("should test some basics", function () {
+ var severities = center.getSeverities();
+ expect(severities.indexOf("low")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("normal")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("high")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("urgent")).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should toggle the visibility', function(done) {
+ var element = qx.bom.Selector.query("#notification-center")[0];
+
+ expect(element).not.toBeUndefined();
+
+ expect(qx.bom.element.Style.get(element, "transform")).toEqual("none");
+ center.toggleVisibility();
+ setTimeout(function() {
+ expect(qx.bom.element.Style.get(element, "transform")).toEqual("matrix(1, 0, 0, 1, -300, 0)");
+ center.toggleVisibility();
+ setTimeout(function() {
+ expect(qx.bom.element.Style.get(element, "transform")).toEqual("matrix(1, 0, 0, 1, 0, 0)");
+ done();
+ }, 100);
+ }, 100);
+ });
+
+ it('should toggle the badge visibility', function(done) {
+ var element = qx.bom.Selector.query("#notification-center .badge")[0];
+
+ expect(element).not.toBeUndefined();
+
+ expect(qx.bom.element.Class.has(element, "hidden")).toBeFalsy();
+ center.disableBadge(true);
+ setTimeout(function() {
+ expect(qx.bom.element.Class.has(element, "hidden")).toBeTruthy();
+ center.disableBadge(false);
+ setTimeout(function() {
+ expect(qx.bom.element.Class.has(element, "hidden")).toBeFalsy();
+ done();
+ }, 10);
+ }, 10);
+ });
+
+ it('should handle messages', function() {
+
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal"
+ };
+ var badge = qx.bom.Selector.query("#notification-center .badge")[0];
+ expect(badge).not.toBeUndefined();
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ expect(center.getMessages().getLength()).toBe(1);
+
+ expect(qx.bom.element.Attribute.get(badge, "html")).toEqual("1");
+ expect(qx.bom.element.Class.has(badge, "normal")).toBeTruthy();
+
+ // add message with higher severity
+ message.severity = "high";
+ message.unique = true;
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as the message was unique it replaces the old one
+ expect(center.getMessages().getLength()).toBe(1);
+
+ expect(qx.bom.element.Attribute.get(badge, "html")).toEqual("1");
+ expect(qx.bom.element.Class.has(badge, "high")).toBeTruthy();
+
+ // add message with higher severity
+ message.severity = "urgent";
+ message.unique = false;
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as the message was unique it replaces the old one
+ expect(center.getMessages().getLength()).toBe(2);
+
+ expect(qx.bom.element.Attribute.get(badge, "html")).toEqual("2");
+ expect(qx.bom.element.Class.has(badge, "urgent")).toBeTruthy();
+
+ // remove unique messages
+ message.condition = false;
+ message.unique = true;
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as we had 2 messages with same topic both should be gone now
+ expect(center.getMessages().getLength()).toBe(0);
+
+ expect(qx.bom.element.Attribute.get(badge, "html")).toBeNull();
+ expect(qx.bom.element.Class.has(badge, "urgent")).toBeFalsy();
+ });
+
+ it("should test the maxEntries limit", function() {
+ center.setMaxEntries(5);
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal"
+ };
+
+ for(var i=0; i< 10; i++) {
+ var msg = qx.lang.Object.clone(message);
+ msg.title = i;
+ center.handleMessage(msg);
+ }
+
+ expect(center.getMessages().getLength()).toBe(5);
+ expect(center.getMessages().getItem(0).title).toBe(5);
+
+ // delete a message by index
+ cv.ui.NotificationCenter.deleteMessage(0);
+ expect(center.getMessages().getLength()).toBe(4);
+ expect(center.getMessages().getItem(0).title).toBe(6);
+
+ // delete a message by index which is not deletable
+ center.getMessages().getItem(0).deletable = false;
+ cv.ui.NotificationCenter.deleteMessage(0);
+ expect(center.getMessages().getLength()).toBe(4);
+ expect(center.getMessages().getItem(0).title).toBe(6);
+ });
+
+ it("should perform a message action", function() {
+ var spy = jasmine.createSpy();
+
+ qx.Class.define("cv.test.ActionHandler", {
+ extend: cv.core.notifications.actions.AbstractActionHandler,
+ implement: cv.core.notifications.IActionHandler,
+
+ members: {
+ handleAction: function() {
+ spy();
+ },
+ getDomElement: function() {
+ return null;
+ }
+ }
+ });
+ cv.core.notifications.ActionRegistry.registerActionHandler("test", cv.test.ActionHandler);
+
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ actions: {
+ test: [{
+ needsConfirmation: false,
+ deleteMessageAfterExecution: true
+ }]
+ }
+ };
+ center.handleMessage(message);
+ cv.ui.NotificationCenter.performAction(center.getMessages().getLength()-1);
+ expect(spy).toHaveBeenCalled();
+ cv.core.notifications.ActionRegistry.unregisterActionHandler("test");
+
+ // message should have been deleted by action execution
+ expect(center.getMessages().getLength()).toEqual(0);
+
+ qx.Class.undefine("cv.test.ActionHandler");
+ });
+
+ it("should test the interaction handling with list items", function() {
+ if (window.PointerEvent) {
+
+ qx.Class.define("cv.test.ActionHandler", {
+ extend: cv.core.notifications.actions.AbstractActionHandler,
+ implement: cv.core.notifications.IActionHandler,
+
+ members: {
+ handleAction: function () {
+ },
+ getDomElement: function () {
+ return null;
+ }
+ }
+ });
+ cv.core.notifications.ActionRegistry.registerActionHandler("test", cv.test.ActionHandler);
+
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ actions: {
+ test: [{
+ needsConfirmation: false,
+ deleteMessageAfterExecution: true
+ }]
+ }
+ };
+ center.handleMessage(message);
+
+ var messageElement = qx.bom.Selector.query("#notification_0")[0];
+ spyOn(center, "deleteMessage");
+ spyOn(center, "performAction");
+
+ // click on the message content
+ // qx.event.Registration.fireEvent(qx.bom.Selector.query(".content", messageElement)[0], "tap");
+ var down = new PointerEvent("pointerdown", {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+ var up = new PointerEvent("pointerup", {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+ var element = qx.bom.Selector.query(".content", messageElement)[0];
+ element.dispatchEvent(down);
+ element.dispatchEvent(up);
+ expect(center.performAction).toHaveBeenCalledWith(0, jasmine.any(qx.event.type.Event));
+
+ // click on the delete button
+ element = qx.bom.Selector.query(".delete", messageElement)[0];
+ element.dispatchEvent(down);
+ element.dispatchEvent(up);
+ expect(center.deleteMessage).toHaveBeenCalledWith(0, jasmine.any(qx.event.type.Event));
+ }
+ });
+});
\ No newline at end of file
diff --git a/source/test/karma/ui/ToastManager-spec.js b/source/test/karma/ui/ToastManager-spec.js
new file mode 100644
index 00000000000..4d0478d01f6
--- /dev/null
+++ b/source/test/karma/ui/ToastManager-spec.js
@@ -0,0 +1,217 @@
+
+describe('test the NotificationCenter', function () {
+
+ var center = cv.ui.ToastManager.getInstance();
+
+ beforeEach(function() {
+ center._init();
+ });
+
+ afterEach(function() {
+ center.clear(true);
+ });
+
+ it("should test some basics", function () {
+ var severities = center.getSeverities();
+ expect(severities.indexOf("low")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("normal")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("high")).toBeGreaterThanOrEqual(0);
+ expect(severities.indexOf("urgent")).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should handle messages', function() {
+
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ target: "toast"
+ };
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ expect(center.getMessages().getLength()).toBe(1);
+
+ // add message with higher severity
+ message.severity = "high";
+ message.unique = true;
+
+ var messageId = center.__idCounter-1;
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as the message was unique it replaces the old one
+ expect(center.getMessages().getLength()).toBe(1);
+
+ var messageElement = qx.bom.Selector.query("#"+center.getMessageElementId()+messageId)[0];
+ expect(qx.bom.element.Class.has(messageElement, "high")).toBeTruthy();
+
+ // add message with higher severity
+ message.severity = "urgent";
+ message.unique = false;
+
+ messageId = center.__idCounter;
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as the message was unique it replaces the old one
+ expect(center.getMessages().getLength()).toBe(2);
+
+ messageElement = qx.bom.Selector.query("#"+center.getMessageElementId()+messageId)[0];
+ expect(qx.bom.element.Class.has(messageElement, "urgent")).toBeTruthy();
+
+ // remove unique messages
+ message.condition = false;
+ message.unique = true;
+
+ center.handleMessage(qx.lang.Object.clone(message));
+ center.handleMessage(qx.lang.Object.clone(message));
+ // as we had 2 messages with same topic both should be gone now
+ expect(center.getMessages().getLength()).toBe(0);
+
+ });
+
+ it("should test the maxEntries limit", function() {
+ center.setMaxEntries(5);
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ target: "toast"
+ };
+
+ for(var i=0; i< 10; i++) {
+ var msg = qx.lang.Object.clone(message);
+ msg.title = i;
+ center.handleMessage(msg);
+ }
+
+ expect(center.getMessages().getLength()).toBe(5);
+ expect(center.getMessages().getItem(0).title).toBe(5);
+
+ // delete a message by index
+ center.deleteMessage(0);
+ expect(center.getMessages().getLength()).toBe(4);
+ expect(center.getMessages().getItem(0).title).toBe(6);
+
+ // delete a message by index which is not deletable
+ center.getMessages().getItem(0).deletable = false;
+ center.deleteMessage(0);
+ expect(center.getMessages().getLength()).toBe(4);
+ expect(center.getMessages().getItem(0).title).toBe(6);
+ });
+
+ it("should perform a message action", function() {
+ var spy = jasmine.createSpy();
+
+ qx.Class.define("cv.test.ActionHandler", {
+ extend: cv.core.notifications.actions.AbstractActionHandler,
+ implement: cv.core.notifications.IActionHandler,
+
+ members: {
+ handleAction: function() {
+ spy();
+ },
+ getDomElement: function() {
+ return null;
+ }
+ }
+ });
+ cv.core.notifications.ActionRegistry.registerActionHandler("test", cv.test.ActionHandler);
+
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ actions: {
+ test: [{
+ needsConfirmation: false,
+ deleteMessageAfterExecution: true
+ }]
+ },
+ target: "toast"
+ };
+ center.handleMessage(message);
+ center.performAction(center.getMessages().getLength()-1);
+ expect(spy).toHaveBeenCalled();
+ cv.core.notifications.ActionRegistry.unregisterActionHandler("test");
+
+ // message should have been deleted by action execution
+ expect(center.getMessages().getLength()).toEqual(0);
+
+ qx.Class.undefine("cv.test.ActionHandler");
+ });
+
+ it("should test the interaction handling with list items", function() {
+ if (window.PointerEvent) {
+ // click on the message content
+ var down = new PointerEvent("pointerdown", {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+ var up = new PointerEvent("pointerup", {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ });
+
+ qx.Class.define("cv.test.ActionHandler", {
+ extend: cv.core.notifications.actions.AbstractActionHandler,
+ implement: cv.core.notifications.IActionHandler,
+
+ members: {
+ handleAction: function () {
+ },
+ getDomElement: function () {
+ return null;
+ }
+ }
+ });
+ cv.core.notifications.ActionRegistry.registerActionHandler("test", cv.test.ActionHandler);
+
+ spyOn(center, "deleteMessage");
+ // test if message without action gets deleted
+ var message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ target: "toast"
+ };
+ var messageId = center.__idCounter;
+ center.handleMessage(message);
+
+ var element = qx.bom.Selector.query("#"+center.getMessageElementId()+messageId)[0];
+ element.dispatchEvent(down);
+ element.dispatchEvent(up);
+ expect(center.deleteMessage).toHaveBeenCalledWith(messageId);
+
+ message = {
+ topic: "cv.test",
+ title: "Title",
+ message: "Test message",
+ severity: "normal",
+ actions: {
+ test: [{
+ needsConfirmation: false,
+ deleteMessageAfterExecution: true
+ }]
+ },
+ target: "toast"
+ };
+
+ center.handleMessage(message);
+
+ element = qx.bom.Selector.query("#"+center.getMessageElementId()+messageId)[0];
+
+ spyOn(center, "performAction");
+
+ element.dispatchEvent(down);
+ element.dispatchEvent(up);
+ expect(center.performAction).toHaveBeenCalledWith(messageId, jasmine.any(qx.event.type.Event));
+
+ center.deleteMessage(messageId);
+
+
+ }
+ });
+});
\ No newline at end of file
diff --git a/source/test/karma/xml/parser/Meta-spec.js b/source/test/karma/xml/parser/Meta-spec.js
index 3f232ec0e16..c57a187db9f 100644
--- a/source/test/karma/xml/parser/Meta-spec.js
+++ b/source/test/karma/xml/parser/Meta-spec.js
@@ -39,6 +39,11 @@ describe("testing the meta parser", function() {
y = x/1000;
+
+ Flur OG
+ Küche
+ Esszimmer
+
@@ -51,6 +56,18 @@ describe("testing the meta parser", function() {
red
+
+
+ Bewegungsalarm
+ Bewegung erkannt: {{ address }}, {{ time }}
+ ON
+
+ Motion_FF_Dining
+ Motion_FF_Corridor
+ Motion_FF_Kitchen
+
+
+
by CometVisu.org
@@ -77,6 +94,7 @@ describe("testing the meta parser", function() {
expect(cv.Config.hasMapping('Sign')).toBeTruthy();
expect(cv.Config.hasMapping('KonnexHVAC')).toBeTruthy();
expect(cv.Config.hasMapping('One1000th')).toBeTruthy();
+ expect(cv.Config.hasMapping('Motion_name')).toBeTruthy();
// check stylings
expect(cv.Config.hasStyling('Red_Green')).toBeTruthy();
@@ -88,6 +106,21 @@ describe("testing the meta parser", function() {
expect(plugins).toContain("plugin-diagram");
expect(plugins).toContain("plugin-strftime");
+ // test notifications
+ var router = cv.core.notifications.Router.getInstance();
+ var config = router.__stateMessageConfig;
+
+ expect(config.hasOwnProperty("Motion_FF_Dining")).toBeTruthy();
+ expect(config.hasOwnProperty("Motion_FF_Corridor")).toBeTruthy();
+ expect(config.hasOwnProperty("Motion_FF_Kitchen")).toBeTruthy();
+
+ // state listeners must be set
+ var model = cv.data.Model.getInstance();
+ ["Motion_FF_Dining", "Motion_FF_Corridor", "Motion_FF_Kitchen"].forEach(function(address) {
+ expect(model.__stateListeners.hasOwnProperty(address)).toBeTruthy();
+ expect(model.__stateListeners[address].length).toEqual(1);
+ });
+
qx.dom.Element.remove(footer);
});
});
\ No newline at end of file
diff --git a/source/translation/de.po b/source/translation/de.po
index 591746bc208..07dfceae44e 100644
--- a/source/translation/de.po
+++ b/source/translation/de.po
@@ -11,49 +11,64 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: cv/Application.js:172
+#: cv/Application.js:175
msgid "Please describe what you have done until the error occured?"
-msgstr "Bitte beschreiben, was Sie gemacht haben bis der Fehler aufgetreten ist."
+msgstr ""
+"Bitte beschreiben, was Sie gemacht haben bis der Fehler aufgetreten ist."
-#: cv/Application.js:176
+#: cv/Application.js:207
msgid "An error occured"
msgstr "Ein Fehler ist aufgetreten"
-#: cv/Application.js:182
+#: cv/Application.js:214
+msgid "Reload"
+msgstr "Neu laden"
+
+#: cv/Application.js:241
+msgid "Please do not forget to attach the downloaded Logfile to this ticket."
+msgstr ""
+"Bitte vergessen Sie nicht, die heruntergeladenen Log-Datei an dieses Ticket "
+"zu hängen."
+
+#: cv/Application.js:248
+msgid "Enable reporting on reload"
+msgstr "Neu laden mit aktivierter Aufzeichnung"
+
+#: cv/Application.js:253
msgid "Report Bug"
msgstr "Fehler berichten"
-#: cv/Application.js:489
+#: cv/Application.js:561
msgid "Config-File Error!"
msgstr "Fehler in Konfigurations-Datei!"
-#: cv/Application.js:493
+#: cv/Application.js:565
msgid "Invalid config file!"
msgstr "Ungültige Konfigurations-Datei!"
-#: cv/Application.js:493
+#: cv/Application.js:565
msgid "Please check!"
msgstr "Bitte prüfen!"
-#: cv/Application.js:501
+#: cv/Application.js:573
msgid "Config file has wrong library version!"
msgstr "Konfigurations-Datei hat die falsche 'library' Version"
-#: cv/Application.js:502
+#: cv/Application.js:574
msgid "This can cause problems with your configuration"
msgstr "Das kann Probleme in Ihrer Konfiguration verursachen"
-#: cv/Application.js:503
+#: cv/Application.js:575
msgid "You can run the %1Configuration Upgrader%2."
msgstr "Sie können den %1Konfigurations-Upgrader%2 laufen lassen."
-#: cv/Application.js:504
+#: cv/Application.js:576
msgid ""
"Or you can start without upgrading %1with possible configuration problems%2"
msgstr ""
"Oder Sie starten ohne Upgrade %1mit möglichen Konfigurations-Problemen%2"
-#: cv/Application.js:507
+#: cv/Application.js:579
msgid ""
"404: Config file not found. Neither as normal config (%1) nor as demo config"
" (%2)."
@@ -61,27 +76,49 @@ msgstr ""
"404: Konfigurations-Datei nicht gefunden. Weder als normale Konfiguration "
"(%1) noch als Demo-Konfiguration (%2)"
-#: cv/Application.js:510
+#: cv/Application.js:582
msgid "Unhandled error of type \"%1\""
msgstr "Unbekannter Fehler vom Typ \"%1\""
-#: cv/TemplateEngine.js:204
+#: cv/TemplateEngine.js:245
msgid "Connection error"
msgstr "Verbindungsfehler"
-#: cv/TemplateEngine.js:213
+#: cv/TemplateEngine.js:254
msgid "Error requesting %1: %2 - %3."
msgstr "Fehler beim Laden von %1: %2 - %3."
-#: cv/TemplateEngine.js:215
+#: cv/TemplateEngine.js:256
msgid "Connection to backend is lost."
msgstr "Verbindung zum Backend verloren."
-#: cv/ui/NotificationCenter.js:227
+#: cv/plugins/openhab/Settings.js:173
+msgid ""
+"The CometVisu seems to be delivered by a proxied webserver. Changing "
+"configuration values might not have the expected effect. Please proceed only"
+" if you know what you are doing."
+msgstr ""
+"Die CometVisu scheint über einen Proxy ausgeliefert zu werden. Änderungen "
+"an den Einstellungen könnten deshalb nicht den gewünschten Effekt habe."
+"Bitte führen Sie daher nur Änderungen durch, wenn Sie wissen was Sie tun."
+
+#: cv/plugins/openhab/Settings.js:211
+msgid "openHAB backend settings"
+msgstr "openHAB Backend Einstellungen"
+
+#: cv/plugins/openhab/Settings.js:226
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: cv/plugins/openhab/Settings.js:231
+msgid "Save"
+msgstr "Speichern"
+
+#: cv/ui/NotificationCenter.js:257
msgid "Delete all"
msgstr "Alle löschen"
-#: cv/ui/NotificationCenter.js:227
+#: cv/ui/NotificationCenter.js:257
msgid "Message center"
msgstr "Nachrichtenzentrale"
diff --git a/source/translation/en.po b/source/translation/en.po
index fe73f9bcbe3..32ce5f172da 100644
--- a/source/translation/en.po
+++ b/source/translation/en.po
@@ -11,74 +11,105 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: cv/Application.js:172
+#: cv/Application.js:175
msgid "Please describe what you have done until the error occured?"
msgstr ""
-#: cv/Application.js:176
+#: cv/Application.js:207
msgid "An error occured"
msgstr ""
-#: cv/Application.js:182
+#: cv/Application.js:214
+msgid "Reload"
+msgstr ""
+
+#: cv/Application.js:241
+msgid "Please do not forget to attach the downloaded Logfile to this ticket."
+msgstr ""
+
+#: cv/Application.js:248
+msgid "Enable reporting on reload"
+msgstr ""
+
+#: cv/Application.js:253
msgid "Report Bug"
msgstr ""
-#: cv/Application.js:489
+#: cv/Application.js:561
msgid "Config-File Error!"
msgstr ""
-#: cv/Application.js:493
+#: cv/Application.js:565
msgid "Invalid config file!"
msgstr ""
-#: cv/Application.js:493
+#: cv/Application.js:565
msgid "Please check!"
msgstr ""
-#: cv/Application.js:501
+#: cv/Application.js:573
msgid "Config file has wrong library version!"
msgstr ""
-#: cv/Application.js:502
+#: cv/Application.js:574
msgid "This can cause problems with your configuration"
msgstr ""
-#: cv/Application.js:503
+#: cv/Application.js:575
msgid "You can run the %1Configuration Upgrader%2."
msgstr ""
-#: cv/Application.js:504
+#: cv/Application.js:576
msgid ""
"Or you can start without upgrading %1with possible configuration problems%2"
msgstr ""
-#: cv/Application.js:507
+#: cv/Application.js:579
msgid ""
"404: Config file not found. Neither as normal config (%1) nor as demo config"
" (%2)."
msgstr ""
-#: cv/Application.js:510
+#: cv/Application.js:582
msgid "Unhandled error of type \"%1\""
msgstr ""
-#: cv/TemplateEngine.js:204
+#: cv/TemplateEngine.js:245
msgid "Connection error"
msgstr ""
-#: cv/TemplateEngine.js:213
+#: cv/TemplateEngine.js:254
msgid "Error requesting %1: %2 - %3."
msgstr ""
-#: cv/TemplateEngine.js:215
+#: cv/TemplateEngine.js:256
msgid "Connection to backend is lost."
msgstr ""
-#: cv/ui/NotificationCenter.js:227
+#: cv/plugins/openhab/Settings.js:173
+msgid ""
+"The CometVisu seems to be delivered by a proxied webserver. Changing "
+"configuration values might not have the expected effect. Please proceed only"
+" if you know what you are doing."
+msgstr ""
+
+#: cv/plugins/openhab/Settings.js:211
+msgid "openHAB backend settings"
+msgstr ""
+
+#: cv/plugins/openhab/Settings.js:226
+msgid "Cancel"
+msgstr ""
+
+#: cv/plugins/openhab/Settings.js:231
+msgid "Save"
+msgstr ""
+
+#: cv/ui/NotificationCenter.js:257
msgid "Delete all"
msgstr ""
-#: cv/ui/NotificationCenter.js:227
+#: cv/ui/NotificationCenter.js:257
msgid "Message center"
msgstr ""
diff --git a/utils/docutils/directives/common.py b/utils/docutils/directives/common.py
index aa9fb73116e..ad4c9984b7c 100644
--- a/utils/docutils/directives/common.py
+++ b/utils/docutils/directives/common.py
@@ -91,16 +91,18 @@ def generate_table(self, element_name, include_name=False, mandatory=False):
for attr in attributes:
if 'name' in attr.attrib:
name = attr.get('name')
- atype, values = schema.get_attribute_type(attr)
+ atype, values, enums = schema.get_attribute_type(attr)
description = schema.get_node_documentation(attr, self.locale)
if description is not None:
description = re.sub("\n\s+", " ", description.text).strip()
+ elif enums is not None:
+ description = self.get_description(enums)
else:
description = ''
elif 'ref' in attr.attrib:
name = attr.get('ref')
type_def = schema.get_attribute(name)
- atype, values = schema.get_attribute_type(type_def)
+ atype, values, enums = schema.get_attribute_type(type_def)
# check if there is some documentation here
description = schema.get_node_documentation(attr, self.locale)
if description is None:
@@ -108,6 +110,8 @@ def generate_table(self, element_name, include_name=False, mandatory=False):
description = schema.get_node_documentation(type_def, self.locale)
if description is not None:
description = re.sub("\n\s+", " ", description.text).strip()
+ elif enums is not None:
+ description = self.get_description(enums)
else:
description = ''
@@ -150,6 +154,19 @@ def generate_table(self, element_name, include_name=False, mandatory=False):
return table_node
+ def get_description(self, enums):
+ tmp_doc = ""
+ enum_doc_found = False
+ for enum_node in enums:
+ ed = schema.get_node_documentation(enum_node, self.locale)
+ if ed is not None:
+ enum_doc_found = True
+ tmp_doc += "%s* *%s*: %s" % ("\n" if len(tmp_doc) else "", enum_node.get('value'), ed.text)
+ if enum_doc_found:
+ return tmp_doc
+ else:
+ return ""
+
def generate_complex_table(self, element_name, include_name=False, mandatory=False,
table_body=None, sub_run=False, parent=None):
""" needs to be fixed """
@@ -166,19 +183,23 @@ def generate_complex_table(self, element_name, include_name=False, mandatory=Fal
for attr in attributes:
if 'name' in attr.attrib:
name = attr.get('name')
- atype, values = schema.get_attribute_type(attr)
+ atype, values, enums = schema.get_attribute_type(attr)
description = schema.get_node_documentation(attr, self.locale)
if description is not None:
description = description.text
+ elif enums is not None:
+ description = self.get_description(enums)
else:
description = ''
elif 'ref' in attr.attrib:
name = attr.get('ref')
type_def = schema.get_attribute(name)
- atype, values = schema.get_attribute_type(type_def)
+ atype, values, enums = schema.get_attribute_type(type_def)
description = schema.get_node_documentation(type_def, self.locale)
if description is not None:
description = description.text
+ elif enums is not None:
+ description = self.get_description(enums)
else:
description = ''
diff --git a/utils/docutils/directives/helper/schema.py b/utils/docutils/directives/helper/schema.py
index 75a0b73a4b7..641bf3af28b 100644
--- a/utils/docutils/directives/helper/schema.py
+++ b/utils/docutils/directives/helper/schema.py
@@ -84,13 +84,14 @@ def get_node_documentation(self, node, locale):
def get_attribute_type(self, node):
type = None
values = []
+ enums = None
if 'type' in node.attrib:
type = node.get('type')
elif len(node.findall("xs:simpleType/xs:restriction/xs:enumeration".replace("xs:", SCHEMA_SPACE))) > 0:
enums = node.findall("xs:simpleType/xs:restriction/xs:enumeration".replace("xs:", SCHEMA_SPACE))
values = [enum.get('value') for enum in enums]
type = node.find("xs:simpleType/xs:restriction".replace("xs:", SCHEMA_SPACE)).get("base")
- return type, values
+ return type, values, enums
def get_element_attributes(self, name):