Skip to content
Snippets Groups Projects
Alert.js 10.80 KiB
/**
 * @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);