Source: app.js

/* global Tab */
'use strict';

/**
 * Start by configure requirejs paths and shim
 */
require.config(
    {
        paths : {
            jquery      : 'ext/jquery-2.1.3.min',
            cookie      : 'ext/jquery.cookie',
            mask        : 'ext/jquery.mask.min',
            sortable    : 'ext/jquery-sortable',
            bootstrap   : 'ext/bootstrap.min',
            colorpicker : 'ext/bootstrap-colorpicker.min',
            datepicker  : 'ext/bootstrap-datepicker.min',
            ko          : 'ext/knockout-3.3.0',
            ckeditor    : 'ext/ckeditor/ckeditor',
            ace         : 'ext/ace/ace',
            less        : 'ext/less'
        },
        shim : {
            jquery : {
                exports : '$'
            },
            ko : {
                exports : 'ko'
            },
            cookie : {
                deps : ['jquery']
            },
            mask : {
                deps : ['jquery']
            },
            sortable : {
                deps : ['jquery']
            },
            bootstrap : {
                deps : ['jquery']
            },
            datepicker : {
                deps : ['bootstrap']
            },
            colorpicker: {
                deps : ['bootstrap']
            },
            'ko-extends' : {
                deps : ['ko']
            },
            ace : {
                exports : 'ace'
            },
            ckeditor : {
                exports : 'CKEDITOR'
            }
        }
    }
);


define(
    'app',
    [
        'jquery',
        'ko',
        'tabs',
        'form',
        'list',
        'lang',
        'cookie',
        'mask',
        'sortable',
        'bootstrap',
        'colorpicker',
        'datepicker',
        'ko-extends'
    ],
    function($, ko, Tabset, Form, List, Lang) {
        // export libraries to global context
        window.$ = $;
        window.ko = ko;
        window.Tabset = Tabset;
        window.Form = Form;
        window.List = List;
        window.Lang = Lang;

        /**
         * This class describes the behavior of the application
         *
         * @class App
         */
        var App = function() {
            this.conf = window.appConf;

            this.language = '';
            // The application language
            this.rootUrl = '';
            // The application root url
            this.isLogged = false;
            // The user is connected or not ?
            this.routes = [];
            // The application routes
            this.forms = {};
            // The instanciated forms
            this.lists = {};
            // The instanciated lists
            this.isReady = false;
            // The ready state of the application
        };

        /**
         * The URI to return for non existing route
         *
         * @constant
         * @memberOf App
         */
        App.INVALID_URI = window.appConf.basePath + '/INVALID_URI';

        /**
         * Initialize the application
         *
         * @memberOf App
         */
        App.prototype.start = function() {
            // Set the configuration data
            this.setLanguage(this.conf.Lang.language);
            this.setRoutes(this.conf.routes);
            this.setRootUrl(this.conf.rooturl);
            Lang.init(this.conf.Lang.keys);
            this.baseUrl = require.toUrl('');
            this.isLogged = this.conf.user.logged;

            // Manage the notification area
            this.notification = {
                display : ko.observable(false),
                level : ko.observable(),
                message : ko.observable()
            };

            this.tabset = new Tabset();

            /**
             * Call URIs by AJAX on click on links
             */
            var linkSelector = '[href]:not(.real-link):not([href^="#"]):not([href^="javascript:"])';

            $('body').on(
                'click',
                linkSelector,
                function(event) {
                    var node = $(event.currentTarget);
                    var url  = $(node).attr('href');

                    event.preventDefault();
                    var data = {},
                        target = $(node).attr('target');

                    if ((event.which === 2 || !this.tabset.tabs().length) && !target) {
                        target = 'newtab';
                    }

                    switch (target) {
                        case 'newtab' :
                            // Load the page in a new tab of the application
                            data = {newtab : true};
                            this.load(url, data);
                            break;

                        case 'dialog' :
                            this.dialog(url);
                            break;

                        case '_blank' :
                            // Load the whole page in a new browser tab
                            window.open(url);
                            break;

                        case undefined :
                        case '' :
                            // Open the url in the current application tab
                            this.load(url);
                            break;

                        case 'window' :
                            // Open the URL in the current web page
                            location.href = url;
                            break;

                        default :
                            // Open the url in a given DOM node, represented by it CSS selector
                            this.load(url, {selector : $(node).attr('target')});
                            break;
                    }
                }.bind(this)
            )

            // Open a link in a new tab of the application
            .on('mousedown', linkSelector, function(event) {
                if (event.which === 2) {
                    if (!$(this).attr('target')) {
                        event.type = 'click';

                        var clickEvent   = new Event('click', event);

                        clickEvent.which = 2;
                        $(this).get(0).dispatchEvent(clickEvent);
                    }

                    event.preventDefault();
                    event.stopPropagation();
                    event.stopImmediatePropagation();

                    return false;
                }

                return true;
            });

            /**
             * Treat back button
             *
             * @param {Event} event The popstate event
             */
            window.onpopstate = function(event) {
                event.preventDefault();
                if (this.tabset.activeTab()) {
                    var history = this.tabset.activeTab().history;

                    if (event.state) {
                        // call back button
                        if (history.length > 1) {
                            history.pop();
                            this.load(history[history.length - 1]);
                        }
                        else {
                            this.load(this.getUri('new-tab'));
                        }
                    }
                    else if (location.hash.match(/^#\!(\/.*?)$/)) {
                        // Load a new page in the current tab
                        var hash = location.hash.replace(/^#\!(\/.*?)$/, '$1');

                        this.load(hash);
                    }
                    else {
                        // Click on a link with an anchor as href
                        window.history.replaceState({}, '', '#!' + history[history.length - 1]);
                    }
                }
            }.bind(this);

            this.loading = {
                display : ko.observable(false),
                progressing : ko.observable(false),
                purcentage : ko.observable(0),

                /**
                 * Display loading
                 */
                start : function() {
                    this.display(true);
                },

                /**
                 * Show loading progression
                 *
                 * @param {Float} purcentage The advancement purcentage on the progress bar
                 */
                progress : function(purcentage) {
                    this.purcentage(purcentage);
                    this.progressing(Boolean(purcentage));
                },

                /**
                 * Hide loading
                 */
                stop : function() {
                    this.display(false);
                    this.progress(0);
                }
            };

            // trigger the application is ready
            var evt = document.createEvent('Event');

            evt.initEvent('app-ready', true, false);
            dispatchEvent(evt);

            /**
             * Customize app HttpRequestObject
             *
             * @returns {XMLHttpRequest} The xhr object
             */
            this.xhr = function() {
                var xhr = new window.XMLHttpRequest();

                this.computeProgession = function(evt) {
                    if (evt.lengthComputable) {
                        var percentComplete = parseInt(evt.loaded / evt.total * 100);

                        // Do something with upload progress here
                        this.loading.progress(percentComplete);
                    }
                }.bind(this);

                /**
                 * Compute progression on upload AJAX requests
                 */
                xhr.upload.addEventListener('progress', this.computeProgession);

                /**
                 * Compute progression on AJAX requests
                 */
                xhr.addEventListener('progress', this.computeProgession);

                return xhr;
            }.bind(this);

            /**
             * Open the last tabs
             */
            var onload = null,
                hash   = location.hash.replace(/^#\!/, '');

            if (hash) {
                var index = this.conf.tabs.open.indexOf(hash);

                if (index === -1) {
                    if (this.conf.tabs.open.length === 1) {
                        this.conf.tabs.open = [hash];
                    }
                    else {
                        this.conf.tabs.open.push(hash);
                    }
                }

                index = this.conf.tabs.open.indexOf(hash);
                onload = function() {
                    this.tabset.activeTab(this.tabset.tabs()[index]);
                }.bind(this);
            }

            this.openLastTabs(this.conf.tabs.open, onload);
        };

        /**
         * Add a callback when the application is ready to run
         *
         * @param {Function} callback The action to perform when the application is ready to run
         * @memberOf App
         */
        App.prototype.ready = function(callback) {
            if (this.isReady) {
                callback();
            }
            else {
                addEventListener(
                    'app-ready',
                    function() {
                        this.isReady = true;
                        callback();
                    }.bind(this)
                );
            }
        };

        /**
         * Load a page in the current tab, or a new tab, or a given html node
         *
         * @param {string} url The url to load
         * @param {Object} data, the options. This object can hasve the following data :
         *     - newtab (default false) : if set to true, the page will be loaded in a new tab of the application
         *    - onload (default null) : A callback function to execute when the page is loaded
         *    - post (default null) : an object of POST data to send in the URL
         *
         * @memberOf App
         */
        App.prototype.load = function(url, data) {
            /**
             * Default options
             */
            var options = {
                newtab : false,
                onload : null,
                post : null,
                selector : null,
                headers : {}
            };

            for (var i in data) {
                if (data.hasOwnProperty(i)) {
                    options[i] = data[i];
                }
            }

            if (url) {
                /**
                 * We first check that page does not already exist in a tab
                 */
                var route = this.getRouteFromUri(url);

                if (route === 'new-tab') {
                    url = this.conf.tabs.new.url;
                }

                for (var j = 0; j < this.tabset.tabs().length; j++) {
                    var tab = this.tabset.tabs()[j];

                    if (tab.uri() === url || tab.route() === route) {
                        if (tab !== this.tabset.activeTab()) {
                            this.tabset.activeTab(tab);
                            return;
                        }

                        options.newtab = false;
                        break;
                    }
                }

                this.loading.start();

                /**
                 * A new tab has been asked
                 */
                if (options.newtab) {
                    this.tabset.push();
                }

                // Get the element the page will be loaded in
                var element = options.selector ? $(options.selector).get(0) : this.tabset.activeTab();

                // Load the page
                if (element) {
                    $.ajax({
                        xhr : this.xhr,
                        url : url,
                        type : options.post ? 'post' : 'get',
                        data : options.post,
                        dataType : 'text',
                        headers : options.headers
                    })
                    .done(function(response) {
                        this.loading.stop();
                        if (element instanceof Tab) {
                            // The page has been loaded in a whole tab
                            // Register the tab url
                            element.uri(url);
                            element.route(route);

                            element.content(response);

                            // Regiter the tabs in the cookie
                            if (this.isLogged) {
                                this.tabset.registerTabs();
                            }

                            // register the url in the tab history
                            element.history.push(url);

                            history.pushState({}, '', '#!' + url);
                        }
                        else {
                            $(element).html(response);
                        }

                        if (options.onload) {
                            /**
                             * A 'onload' callback has been asked
                             */
                            options.onload();
                        }
                    }.bind(this))

                    .fail(function(xhr) {
                        var code = xhr.status;

                        if (code === 403) {
                            // The page is not accessible for the user
                            var response;

                            try {
                                response = JSON.parse(xhr.responseText);
                            }
                            catch (e) {
                                response = {
                                    message : Lang.get('main.access-forbidden')
                                };
                            }

                            if (response.reason === 'login') {
                                // The user is not connected, display the login form
                                this.dialog(this.getUri('login') + '?redirect=' + url + '&code=' + code);
                            }
                            else {
                                // Other reason, display the message in a notification
                                this.notify('danger', response.message);
                            }
                        }
                        else {
                            this.notify('danger', xhr.responseText);
                        }

                        this.loading.stop();
                    }.bind(this));
                }
                else {
                    /**
                     * The selector to home the loaded url doesn't exist
                     */
                    this.loading.stop();
                    this.notify('danger', Lang.get('main.loading-page-selector-not-exists'));
                }
            }
        };

        /**
         * Open a set of pages
         *
         * @param {Array} uris The uris to open, each one in a tab
         * @param {Function} onload The callback to execute when all the tabs are loaded
         * @memberOf App
         */
        App.prototype.openLastTabs = function(uris, onload) {
            var loaded = ko.observable(0);

            loaded.subscribe(function(value) {
                if (value === uris.length) {
                    this.loading.stop();
                    if (onload) {
                        onload(uris);
                    }
                }
            }.bind(this));

            uris.forEach(function(uri) {
                this.load(uri, {
                    newtab : true,
                    onload : function() {
                        this.loading.start();
                        loaded(loaded() + 1);
                    }.bind(this)
                });
            }.bind(this));
        };

        /**
         * Display a notification on the application or on the user desktop
         *
         * @param {string} level   The notification level (info, success, warning, danger or desktop)
         * @param {string} message The message to display in the notification
         * @param {Object} options The options for desktop notifications
         * @memberOf App
         */
        App.prototype.notify = function(level, message, options) {
            if (level === 'error') {
                level = 'danger';
            }

            if (level === 'desktop') {
                // this is a desktop notification
                if (!('Notification' in window)) {
                    this.notify('success', message);
                }
                else if (Notification.permission === 'granted') {
                    var notif = new Notification(message, options);
                }
                else if (Notification.permission !== 'denied') {
                    // Ask for user permission to display notifications
                    Notification.requestPermission(function(permission) {
                        Notification.permission = permission;
                        this.notify(level, message, options);
                    }.bind(this));
                }
            }
            else {
                // Display an advert message in the application
                this.notification.display(true);
                this.notification.message(message);
                this.notification.level(level);

                if (level !== 'danger') {
                    this.notification.timeout = setTimeout(function() {
                        this.hideNotification();
                    }.bind(this), 5000);
                }
            }
        };

        /**
         * Hide the displayed notification
         *
         * @memberOf App
         */
        App.prototype.hideNotification = function() {
            clearTimeout(this.notification.timeout);
            this.notification.display(false);
        };

        /**
         * Load a URL in a dialog box
         *
         * @param    {string} action The action to perform. If "close", it will wlose the current dialog box,
         *                           else it will load the action in the dialog box and open it
         * @memberOf App
         */
        App.prototype.dialog = function(action) {
            var container = $('#dialogbox');

            container.modal('hide');

            if (action === 'close') {
                return;
            }

            // Load the content from an url
            this.loading.start();
            $.ajax({
                url : action,
                type : 'get',
                data : {
                    _dialog: true
                }
            })

            .done(function(content) {
                // Page successfully loaded
                container.html(content).modal('show');
            })

            .fail(function(xhr) {
                // Page load failed
                var message = xhr.responseText;

                this.notify('danger', message);
            }.bind(this))

            .always(function() {
                this.loading.stop();
            }.bind(this));
        };

        /**
         * Get uri for a given route name or the controller of the route
         *
         * @param {string} method - The route name or the controller method executed by this route
         * @param {Object} args - The route parameters
         * @returns {string} - the computed URI
         * @memberOf App
         */
        App.prototype.getUri = function(method, args) {
            var route = null;

            if (method in this.routes) {
                route = this.routes[method];
            }
            else {
                for (var i in this.routes) {
                    if (this.routes[i].action === method) {
                        route = this.routes[i];
                        break;
                    }
                }
            }

            if (route !== null) {
                var url = route.url;

                if (args) {
                    for (var j in args) {
                        if (args.hasOwnProperty(j)) {
                            url = url.replace('{' + j + '}', args[j]);
                        }
                    }
                }

                return this.conf.basePath + url;
            }

            return App.INVALID_URI;
        };

        /**
         * Get the route name corresponding to an URI
         *
         * @param    {string} uri - The uri to look the corresponding route for
         * @returns   {Object} The found route
         * @memberOf App
         */
        App.prototype.getRouteFromUri = function(uri) {
            var path = uri.replace(/\/?\?.*$/, '');

            for (var i in this.routes) {
                if (this.routes.hasOwnProperty(i)) {
                    var regex = new RegExp('^' + this.routes[i].pattern + '$');

                    if (path.match(regex)) {
                        return i;
                    }
                }
            }

            return null;
        };

        /**
         * Set the existing routes of the application
         *
         * @param {Object} routes - The routes to set
         * @memberOf App
         */
        App.prototype.setRoutes = function(routes) {
            this.routes = routes;
        };

        /**
         * Set the language of the application
         *
         * @param    {string} language - The language tag
         * @memberOf App
         */
        App.prototype.setLanguage = function(language) {
            this.language = language;
        };

        /**
         * Set the root url of the application
         *
         * @param    {string} url - The root url to set
         * @memberOf App
         */
        App.prototype.setRootUrl = function(url) {
            this.rootUrl = url;
        };

        /**
         * Refresh the main menu
         *
         * @memberOf App
         */
        App.prototype.refreshMenu = function() {
            $.get(this.getUri('refresh-menu'), function(response) {
                $('#main-menu').replaceWith(response);

                this.notify('warning', Lang.get('main.main-menu-changed'));
            }.bind(this));
        };

        /**
         * Print a part of the page (or the whole page)
         *
         * @param {NodeElement} element The DOM element to print.
         *                              If not set or null, then this will print the whole page
         * @memberOf App
         */
        App.prototype.print = function(element) {
            if (!element) {
                window.print();
            }
            else {
                // Create a frame to wrap the content to print
                var frame = document.createElement('iframe');

                document.body.appendChild(frame);

                // Add the content to the page
                frame.contentDocument.body.innerHTML = element.outerHTML;

                // Add the css
                var style    = document.createElement('link');

                style.rel = 'stylesheet';
                style.href = document.getElementById('theme-base-stylesheet').href;
                style.type = 'text/css';
                style.media = 'print';
                style.onload = function() {
                    frame.contentWindow.print();
                    frame.contentWindow.close();
                    frame.remove();
                };
                frame.contentDocument.head.appendChild(style);
            }
        };

        // Instanciate the application
        if (!window.app) {
            window.app = new App();
        }

        window.app.ready(
            function() {
                ko.applyBindings(window.app);
            }
        );

        window.app.start();
    }
);


require(['app']);