/* 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']);