/** * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch> */ /* global $ */ /* global EventEmitter */ /* @depend QfqEvents.js */ /* @depend AlertManager.js */ /** * Qfq Namespace * * @namespace QfqNS */ var QfqNS = QfqNS || {}; (function (n) { 'use strict'; /** * Display a message. * * A typical call sequence might look like: * * var alert = new QfqNS.Alert({ * message: "Text being displayed", * type: "info" * }); * alert.show(); * * Messages may have different background colors (severity levels), controlled by the `type` property. Possible * values are * * * `"success"` * * `"info"` * * `"warning"` * * `"error"`, `"danger"` * * The values are translated into Bootstrap `alert-*` classes internally. * * If no buttons are configured, a click anywhere on the alert will close it. * * Buttons are configured by passing an array of objects in the `buttons` property. The properties of the object * are as follows * * { * label: <button label>, * focus: true | false, * eventName: <eventname> * } * * You can connect to the button events by using * * var alert = new QfqNS.Alert({ * message: "Text being displayed", * type: "info", * buttons: [ * { label: "OK", eventName: "ok" } * ] * }); * alert.on('alert.ok', function(...) { ... }); * * Events are named according to `alert.<eventname>`. * * If the property `modal` is set to `true`, a kind-of modal alert will be displayed, preventing clicks * anywhere but the alert. * * For compatibility reasons, the old constructor signature is still supported but deprecated * * var alert = new QfqNS.Alert(message, type, buttons) * * @param {object} options option object has following properties * @param {string} options.message message to be displayed * @param {string} [options.type] type of message, can be `"info"`, `"warning"`, or `"error"`. Default is `"info"`. * @param {number} [options.timeout] timeout in milliseconds. If timeout is less than or equal to 0, the alert * won't timeout and stay open until dismissed by the user. Default `n.Alert.constants.NO_TIMEOUT`. * @param {boolean} [options.modal] whether or not alert is modal, i.e. prevent clicks anywhere but the dialog. * Default is `false`. * @param {object[]} options.buttons what buttons to display on alert. If empty array is provided, no buttons are * displayed and a click anywhere in the alert will dismiss it. * @param {string} options.buttons.label label of the button * @param {string} options.buttons.eventName name of the event when button is clicked. * @param {boolean} [options.buttons.focus] whether or not button has focus by default. Default is `false`. * * @constructor */ n.Alert = function (options) { // Emulate old behavior of method signature // function(message, messageType, buttons) if (typeof options === "string") { this.message = arguments[0]; this.messageType = arguments[1] || "info"; this.buttons = arguments[2] || []; this.modal = false; // this.timeout < 1 means forever this.timeout = n.Alert.constants.NO_TIMEOUT; } else { // new style this.message = options.message || "MESSAGE"; this.messageType = options.type || "info"; this.messageTitle = options.title || false; this.errorCode = options.code || false; this.buttons = options.buttons || []; this.modal = options.modal || false; this.timeout = options.timeout || n.Alert.constants.NO_TIMEOUT; } this.$alertDiv = null; this.$modalDiv = null; this.shown = false; this.fadeInDuration = 400; this.fadeOutDuration = 400; this.timerId = null; this.parent = {}; this.identifier = false; this.eventEmitter = new EventEmitter(); }; n.Alert.prototype.on = n.EventEmitter.onMixin; n.Alert.constants = { alertContainerId: "alert-interactive", alertContainerSelector: "#qfqAlertContainer", jQueryAlertRemoveEventName: "qfqalert.remove:", NO_TIMEOUT: 0 }; /** * * @private */ n.Alert.prototype.makeAlertContainerSingleton = function () { if (!n.QfqPage.alertManager) { n.QfqPage.alertManager = new n.AlertManager({}); } return n.QfqPage.alertManager; }; n.Alert.prototype.setIdentifier = function (i) { this.identifier = i; }; /** * * @returns {number|jQuery} * @private */ n.Alert.prototype.countAlertsInAlertContainer = function () { return $(n.Alert.constants.alertContainerSelector + " > div").length; }; /** * @private */ n.Alert.prototype.removeAlertContainer = function () { if (this.modal) { this.shown = false; this.parent.removeModalAlert(); } }; /** * @private */ n.Alert.prototype.getAlertClassBasedOnMessageType = function () { switch (this.messageType) { case "warning": return "border-warning"; case "danger": case "error": return "border-error"; case "info": return "border-info"; case "success": return "border-success"; /* jshint -W086 */ default: n.Log.warning("Message type '" + this.messageType + "' unknown. Use default type."); /* jshint +W086 */ } }; /** * @private */ n.Alert.prototype.getButtons = function () { var $buttons = null; var $container = $("<p>").addClass("buttons"); var numberOfButtons = this.buttons.length; var index; var buttonConfiguration; for (index = 0; index < numberOfButtons; index++) { buttonConfiguration = this.buttons[index]; if (!$buttons) { if (numberOfButtons > 1) { $buttons = $("<div>").addClass("btn-group"); } else { $buttons = $container; } } var focus = buttonConfiguration.focus ? buttonConfiguration.focus : false; $buttons.append($("<button>").append(buttonConfiguration.label) .attr('type', 'button') .addClass("btn btn-default" + (focus ? " wants-focus" : "")) .click(buttonConfiguration, this.buttonHandler.bind(this))); } if (numberOfButtons > 1) { $container.append($buttons); $buttons = $container; } return $buttons; }; /** * @public */ n.Alert.prototype.show = function () { $(".removeMe").remove(); var alertContainer; if (this.shown) { // We only allow showing once return; } this.parent = this.makeAlertContainerSingleton(); this.parent.addAlert(this); if (this.modal) { this.$modalDiv = $("<div>", { class: "removeMe" }); this.parent.createBlockScreen(this); } if (this.messageTitle) { this.$titleWrap = $("<p>") .addClass("title") .append(this.messageTitle); } this.$messageWrap = $("<p>") .addClass("body") .append(this.message); this.$alertDiv = $("<div>") .hide() .addClass(this.getAlertClassBasedOnMessageType()) .attr("role", "alert") .append(this.$messageWrap); if (this.$titleWrap) { this.$alertDiv.prepend(this.$titleWrap); } if (this.modal) { this.$alertDiv.addClass("alert-interactive"); this.$alertDiv.css('z-index', 1000); } else { this.$alertDiv.addClass("alert-side"); } this.$alertDiv.addClass("removeMe"); var buttons = this.getButtons(); if (buttons) { // Buttons will take care of removing the message this.$alertDiv.append(buttons); } else { // Click on the message anywhere will remove the message this.$alertDiv.click(this.removeAlert.bind(this)); // Allows to remove all alerts that do not require user interaction, programmatically. Yes, we could send // the "click" event, but we want to communicate our intention clearly. this.$alertDiv.on(n.Alert.constants.jQueryAlertRemoveEventName, this.removeAlert.bind(this)); } this.parent.$container.append(this.$alertDiv); //this.$alertDiv.slideDown(this.fadeInDuration, this.afterFadeIn.bind(this)); if (!this.modal) { this.$alertDiv.animate({width:'show'}, this.fadeInDuration, this.afterFadeIn.bind(this)); } else { this.$alertDiv.fadeIn(this.fadeInDuration); } this.$alertDiv.find(".wants-focus").focus(); this.shown = true; }; /** * @private */ n.Alert.prototype.afterFadeIn = function () { if (this.timeout > 0) { this.timerId = window.setTimeout(this.removeAlert.bind(this), this.timeout); } }; /** * * * @private */ n.Alert.prototype.removeAlert = function () { // In case we have an armed timer (or expired timer, for that matter), disarm it. if (this.timerId) { window.clearTimeout(this.timerId); this.timerId = null; } var that = this; if (!this.modal) { this.$alertDiv.animate({width:'hide'}, this.fadeOutDuration, function () { that.$alertDiv.remove(); that.$alertDiv = null; that.shown = false; that.parent.removeOutdatedAlerts(); }); } else { this.$alertDiv.fadeOut(this.fadeOutDuration, function(){ that.$alertDiv.remove(); that.$alertDiv = null; that.shown = false; that.removeAlertContainer(); }); } this.parent.removeAlert(this.identifier); }; /** * * @param handler * * @private */ n.Alert.prototype.buttonHandler = function (event) { this.removeAlert(); this.eventEmitter.emitEvent('alert.' + event.data.eventName, n.EventEmitter.makePayload(this, null)); }; n.Alert.prototype.isShown = function () { return this.shown; }; })(QfqNS);