From 95e4ebb76ac7c5953aa966af191a5f3e03d04944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 13 Jun 2013 17:35:31 +0200 Subject: [PATCH 1/3] Add javascript files (all untested) This commit just adds the javascript files from the incubator, even if not fully functional. refs #3753 --- public/js/icinga/icinga.js | 365 ------------------- public/js/icinga/module.js | 100 +++++ public/js/vendor/namespaced_requirejs.min.js | 47 +++ 3 files changed, 147 insertions(+), 365 deletions(-) create mode 100644 public/js/icinga/module.js create mode 100644 public/js/vendor/namespaced_requirejs.min.js diff --git a/public/js/icinga/icinga.js b/public/js/icinga/icinga.js index d26c6f0cd..a9791d9d0 100755 --- a/public/js/icinga/icinga.js +++ b/public/js/icinga/icinga.js @@ -72,368 +72,3 @@ define([ }); - - /** - * - */ - /* prepareContainers: function () - { - $('#icinga-main').attr( - 'icingaurl', - window.location.pathname + window.location.search - ); - return Icinga; - }, - - initializeTimer: function (interval) - { - // This currently messes up the frontend - //Icinga.refresher = setInterval(Icinga.timer, interval); - return Icinga; - }, - - module: function (name) - { - return Icinga.modules[name]; - }, - - timer: function () - { - $('.icinga-container[icingaurl]').each(function (idx, el) - { - el = $(el); - // TODO: Find a better solution for this problem - if (el.find('.icinga-container').length) { - return; - } - Icinga.loadUrl(el.attr('icingaurl'), el, el.data('icingaparams')); - }); - },*/ - - - /** - * Clicks on action table rows shall trigger the first link action found - * in that row - */ - /* prepareActionRow: function (idx, el) - { - var a = $(el).find('a.row-action'), - tr; - if (a.length < 1) { - a = $(el).find('a'); - } - if (a.length < 1) { return; } - tr = a.closest('tr'); - tr.attr('href', a.first().attr('href')); - // $(a).first().replaceWith($(a).html()); - // tr.addClass('hasHref'); - },*/ - - /** - * Apply event handlers within a given parent node - */ - /* applyEventHandlers: function (parent) - { - // Icinga.debug('Applying event handlers'); - $('.dashboard.icinga-container', parent).each(function(idx, el) { - Icinga.loadUrl($(el).attr('icingaurl'), $(el)); - }); - $('div[icingamodule]', parent).each(function (idx, el) { - Icinga.loadModule($(el).attr('icingamodule'), el); - }); - $('table.action tbody tr', parent).each(Icinga.prepareActionRow); -<<<<<<< HEAD:public/js/icinga/icinga.js - $('a[title], span[title], td[title], img[title]', parent).qtip() - .each(Icinga.fixQtippedElement);*/ -/* $('.inlinepie', parent).sparkline('html', { -======= - $('*[title]').tooltip({placement:"bottom",container: "body"}); - $('.inlinepie', parent).sparkline('html', { ->>>>>>> 3845f64dd2380974633fad30c3160944e9bff1d2:public/js/icinga.js - type: 'pie', - sliceColors: ['#0c0', '#f80', '#c00', '#dcd'], - width: '16px', - height: '16px' - });*//* - $('.inlinebullet', parent).sparkline('html', { - type: 'bullet', - targetColor: '#000', - rangeColors: ['#0c0', '#f80', '#c00'], - width: '16px', - height: '16px' - }); - // Icinga.debug('Applying event handlers done'); - return Icinga; - }, -*/ - /** - * Apply some global event handlers - */ - /* applyGlobalEventHandlers: function () - { - // TODO: replace document with deeper parent elements once they are - // defined - $(document).on('click', 'a', Icinga.linkClicked); - $(document).on('click', 'tr[href]', Icinga.linkClicked); - $(document).on('keyup', 'form.auto input', Icinga.formChangeDelayed); - $(document).on('change', 'form.auto input', Icinga.formChanged); - $(document).on('change', 'form.auto select', Icinga.formChanged); - $(document).on('submit', 'form', Icinga.submitForm); - return Icinga; - }, - applyModuleEventHandlers: function (name, el) - { - Icinga.debug('Applying handlers for ' + name); - $.each(Icinga.moduleHandlers[name], function (idx, event) { - if (event[1] === 'each') { - $(event[0], $(el)).each(event[2]); - } else { - $(event[0], $(el)).on(event[1], event[2]); - } - }); - }, - loadModule: function (name, el) - { - if (typeof Icinga.modules[name] !== 'undefined') - { - Icinga.applyModuleEventHandlers(name, $(el)); - return; - } - try { - require([name], function (module) { - Icinga.modules[name] = module; - Icinga.moduleHandlers[name] = []; - if (typeof module.eventHandlers !== 'undefined') { - $.each(module.eventHandlers, function (filter, events) { - $.each(events, function (event, handle) { - var eventHandle = Icinga.modules[name][handle]; - Icinga.moduleHandlers[name].push([ - filter, - event, - eventHandle - ]); - }); - }); - } - Icinga.applyModuleEventHandlers(name, $(el)); - Icinga.debug('Module "' + name + '" has been loaded'); - }); - } catch (e) { - Icinga.debug( - 'Loading module "' + name + '" failed: ' + - e.name + "\n" + e.message - ); - return; - } - }, -*/ - /** - * Load CSS file if not already done so. - * - * This is an unfinished prototype, we have to make sure to not load - * the same file multiple times - * - * TODO: Finish it, remember/discover what has been loaded, to it the - * jQuery way - */ - /* requireCss: function (url) - { - Icinga.debug('Should load CSS file: ' + url); - // var link = document.createElement("link"); - // link.type = "text/css"; - // link.rel = "stylesheet"; - // link.href = url; - // document.getElementsByTagName("head")[0].appendChild(link); - }, -*/ - /** - * Load the given URL to the given target - * - * @param {string} url URL to be loaded - * @param {object} target Target element - */ - /* loadUrl: function (url, target, data) - { - var req = $.ajax({ - type : 'POST', - url : url, - data : data, - headers: { 'X-Icinga-Accept': 'text/html' } - }); - req.done(Icinga.handleResponse); - req.IcingaTarget = target; - req.fail(Icinga.handleFailure); - return req; - }, -*/ - /** - * Create an URL relative to the Icinga base Url, still unused - * - * @param {string} url Relative url - */ - /* url: function (url) - { - return base_url + url; - }, -*/ - /** - * Smoothly render given HTML to given container - */ - /* renderContentToContainer: function (content, container) - { - Icinga.disableQtips(); - Icinga.debug('fire'); - container.html(content); - if (container.attr('id') === 'icinga-detail') { - container.closest('.layout-main-detail').removeClass('collapsed'); - } - Icinga.applyEventHandlers(container); - - }, -*/ - /** - * Handle successful XHR response - */ - /* handleResponse: function (data, textStatus, jqXHR) - { - Icinga.debug('Got response: ' + this.url); - jqXHR.IcingaTarget.attr('icingaurl', this.url); - Icinga.renderContentToContainer(jqXHR.responseText, jqXHR.IcingaTarget); - }, -*/ - /** - * Handle failed XHR response - */ - /* handleFailure: function (jqXHR, textStatus, errorThrown) - { - if (jqXHR.status > 0) { - Icinga.debug(jqXHR.responseText.slice(0, 100)); - Icinga.renderContentToContainer( - '

' + jqXHR.status + ' ' + errorThrown + '

' + - jqXHR.responseText, - jqXHR.IcingaTarget - ); - - // Header example: - // Icinga.debug(jqXHR.getResponseHeader('X-Icinga-Redirect')); - } else { - if (errorThrown === 'abort') { - Icinga.debug('Request to ' + this.url + ' has been aborted'); - } else { - Icinga.debug('Failed to contact web server'); - } - } - }, -*/ - /** - * A link has been clicked. Try to find out it's target and fire the XHR - * request - */ - /* linkClicked: function (event) - { - event.stopPropagation(); - var target = event.currentTarget, - href = $(target).attr('href'), - destination; - - if ($(target).closest('.pagination').length) { - Icinga.debug('Pagination link clicked'); - destination = $(target).closest('.icinga-container'); - } else if ($(target).closest('.nav-tabs').length) { - Icinga.debug('Nav tab link clicked'); - destination = $(target).closest('.icinga-container'); - } else if ($(target).closest('table.action').length || - // TODO: define just one class name instead of this list - $(target).closest('table.pivot').length || - $(target).closest('.bpapp').length || - $(target).closest('.dashboard.icinga-container').length) - { - destination = $('#icinga-detail'); - Icinga.debug('Clicked an action table / pivot / bpapp / dashboard link'); - } else { - Icinga.debug('Something else clicked'); - // target = $(target).closest('.icinga-container'); - destination = $(target).closest('.icinga-container'); - if (!destination.length) { - destination = $('#icinga-main'); - } - } - - Icinga.loadUrl(href, destination); - return false; - }, -*/ - /* BEGIN form handling, still unfinished */ - /* formChanged: function (event) - { - // TODO: event.preventDefault(); - // TODO: event.stopPropatagion(); - if (Icinga.load_form !== false) { - // Already loading. TODO: make it multi-form-aware - // TODO: shorter timeout, but clear not before ajax call finished - return; - } - var target = event.currentTarget, - form = $(target).closest('form'); - Icinga.load_form = form; - Icinga.fireFormLoader(); - }, - - formChangeDelayed: function (event) - { - if (Icinga.load_form !== false) { - // Already loading. TODO: make it multi-form-aware - // TODO: shorter timeout, but clear not before ajax call finished - return; - } - var target = event.currentTarget, - form = $(target).closest('form'); - Icinga.load_form = form; - setTimeout(Icinga.fireFormLoader, 200); - }, - - fireFormLoader: function () - { - if (Icinga.load_form === false) { - return; - } - // Temporarily hardcoded for top search: - // Icinga.loadUrl(Icinga.load_form.attr('action') + '?' + Icinga.load_form.serialize(), $('#icinga-main')); - Icinga.debug(Icinga.load_form); - if ($('#icinga-main').find('.dashboard.icinga-container').length) { - $('#icinga-main .dashboard.icinga-container').each(function (idx, el) { - Icinga.loadSearch($(el)); - }); - } else { - $('#icinga-main').each(function (idx, el) { - Icinga.loadSearch($(el)); - }); - } - Icinga.load_form = false; - - }, - loadSearch: function(target) - { - var url = target.attr('icingaurl'); - var params = url.split('?')[1] || ''; - url = url.split('?')[0]; - var param_list = params.split('&'); - params = {}; - var i, pairs; - for (i = 0; i < param_list.length; i++) { - pairs = param_list[i].split('='); - params[pairs[0]] = pairs[1]; - } - var searchstring = $('input[name=search]', Icinga.load_form).val(); - params['search'] = searchstring || ''; - params = '?' + $.param(params); - Icinga.loadUrl(url + params, target); - }, - submitForm: function (event) - { - event.stopPropagation(); - var form = $(event.currentTarget); - Icinga.loadUrl(form.attr('action'), $('#icinga-main'), form.serializeArray()); - return false; - },*/ - /* END of form handling */ diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js new file mode 100644 index 000000000..6d0ac6080 --- /dev/null +++ b/public/js/icinga/module.js @@ -0,0 +1,100 @@ +/*global Icinga:false, $: false, document: false, define:false requirejs:false base_url:false console:false */ + +/** + This prototype encapsulates the modules registered in the module folder + **/ +(function() { + "use strict"; + + var loaded = {}; + + define(['logging'],function(log) { + + var registerModuleFunctions = function(module) { + var enableFn = module.enable, disableFn = module.disable; + + module.enable = (function(root) { + root = root || $('body'); + for (var jqMatcher in this.eventHandler) { + for (var ev in this.eventHandler[jqMatcher]) { + log.debug("Registered module: ", "'"+ev+"'", jqMatcher); + $(root).on(ev,jqMatcher,this.eventHandler[jqMatcher][ev]); + } + } + if(enableFn) { + enableFn.apply(this,arguments); + } + }).bind(module); + + module.disable = (function(root) { + root = root || $('body'); + for (var jqMatcher in this.eventHandler) { + for (var ev in this.eventHandler[jqMatcher]) { + log.debug("Unregistered module: ", "'"+ev+"'", jqMatcher); + $(root).off(ev,jqMatcher,this.eventHandler[jqMatcher][ev]); + } + } + if (disableFn) { + disableFn.apply(this,arguments); + } + }).bind(module); + + + }; + + var CallInterface = function() { + + /** + * Loads a module and calls successCallback with the module as the parameter on success, otherwise + * the errorCallback with the errorstring as the first parameter + * + * @param name + * @param errorCallback + * @param successCallback + */ + this.enableModule = function(name,errorCallback,successCallback) { + requirejs([name],function(module) { + if (typeof module === "undefined") { + return errorCallback(new Error("Unknown module: "+name)); + } + + if (typeof module.eventHandler === "object") { + registerModuleFunctions(module); + } + if (typeof module.enable === "function") { + module.enable(); + } + loaded[name] = { + module: module, + active: true + }; + if (typeof successCallback === "function") { + successCallback(module); + } + },function(err) { + errorCallback("Could not load module "+name+" "+err,err); + }); + }; + + this.disableModule = function(name) { + if(loaded[name] && loaded[name].active) { + loaded[name].module.disable(); + } + }; + + /** + * This should *ONLY* be called in testcases + **/ + this.resetHard = function() { + if (typeof describe !== "function") { + return; + } + loaded = {}; + }; + }; + + + return new CallInterface(); + }); + +})(); diff --git a/public/js/vendor/namespaced_requirejs.min.js b/public/js/vendor/namespaced_requirejs.min.js new file mode 100644 index 000000000..dcc93072f --- /dev/null +++ b/public/js/vendor/namespaced_requirejs.min.js @@ -0,0 +1,47 @@ +var requirejs = (function () { + //Define a require object here that has any + //default configuration you want for RequireJS. If + //you do not have any config options you want to set, + //just use an simple object literal, {}. You may need + //to at least set baseUrl. + var require = { + baseUrl: '..' + }; + /* + RequireJS 2.1.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + Available via the MIT or new BSD license. + see: http://github.com/jrburke/requirejs for details + */ + var requirejs,require,define; + (function(ba){function J(b){return"[object Function]"===N.call(b)}function K(b){return"[object Array]"===N.call(b)}function z(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(J(n)){if(this.events.error&&this.map.isDefine||h.onError!==ca)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!== + this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(r[c]=e,h.onResourceLoad))h.onResourceLoad(k,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete= + !0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=l(a.prefix);this.depMaps.push(d);u(d,"defined",v(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,C=k.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=l(a.prefix+"!"+d,this.map.parentMap),u(e,"defined",v(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})), + d=m(q,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",v(this,function(a){this.emit("error",a)}));d.enable()}}else n=v(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=v(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];H(q,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),n.fromText=v(this,function(e,c){var d=a.name,g=l(d),i=Q;c&&(e=c);i&&(Q=!1);s(g);t(j.config,b)&&(j.config[d]=j.config[b]);try{h.exec(e)}catch(D){return w(B("fromtexteval", + "fromText eval for "+b+" failed: "+D,D,[b]))}i&&(Q=!0);this.depMaps.push(g);k.completeLoad(d);C([d],n)}),e.load(a.name,C,n,j)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;z(this.depMaps,v(this,function(a,b){var c,e;if("string"===typeof a){a=l(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(P,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;u(a,"defined",v(this,function(a){this.defineDep(b, + a);this.check()}));this.errback&&u(a,"error",v(this,this.errback))}c=a.id;e=q[c];!t(P,c)&&(e&&!e.enabled)&&k.enable(a,this)}));H(this.pluginMaps,v(this,function(a){var b=m(q,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){z(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:j,contextName:b,registry:q,defined:r,urlFetched:V,defQueue:I,Module:$,makeModuleMap:l, + nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.pkgs,c=j.shim,e={paths:!0,config:!0,map:!0};H(a,function(a,b){e[b]?"map"===b?(j.map||(j.map={}),S(j[b],a,!0,!0)):S(j[b],a,!0):j[b]=a});a.shim&&(H(a.shim,function(a,b){K(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),j.shim=c);a.packages&&(z(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name, + location:a.location||a.name,main:(a.main||"main").replace(ka,"").replace(fa,"")}}),j.pkgs=b);H(q,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=l(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,f){function d(e,c,g){var i,j;f.enableBuildCallback&&(c&&J(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(J(c))return w(B("requireargs", + "Invalid require call"),g);if(a&&t(P,e))return P[e](q[a.id]);if(h.get)return h.get(k,e,a,d);i=l(e,a,!1,!0);i=i.id;return!t(r,i)?w(B("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[i]}M();k.nextTick(function(){M();j=s(l(null,a));j.skipMap=f.skipMap;j.init(e,c,g,{enabled:!0});E()});return d}f=f||{};S(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(Q=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,E?y.insertBefore(g,E):y.appendChild(g), + M=null,g;if(ea)try{importScripts(d),b.completeLoad(c)}catch(l){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,l,[c]))}};A&&O(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return s=L,u.baseUrl||(F=s.split("/"),s=F.pop(),ga=F.length?F.join("/")+"/":"./",u.baseUrl=ga),s=s.replace(fa,""),h.jsExtRegExp.test(s)&&(s=L),u.deps=u.deps?u.deps.concat(s):[s],!0});define=function(b,c,d){var h,g;"string"!==typeof b&&(d=c,c=b,b=null); + K(c)||(d=c,c=null);!c&&J(d)&&(c=[],d.length&&(d.toString().replace(ma,"").replace(na,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(Q){if(!(h=M))R&&"interactive"===R.readyState||O(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return R=b}),h=R;h&&(b||(b=h.getAttribute("data-requiremodule")),g=G[h.getAttribute("data-requirecontext")])}(g?g.defQueue:U).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)}; + h(u)}})(this); + return require; +})(); From 63f7b8016e97f0c0f3f0923e9223405dbd9d1f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Thu, 13 Jun 2013 17:37:02 +0200 Subject: [PATCH 2/3] Add tests for javascript module implementation This commit adds tests for the module loader and registry of icinga2-web. It mainly registers event handlers and calls custom enable/disable functions refs #3753 --- test/js/test/icinga/moduleTest.js | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 test/js/test/icinga/moduleTest.js diff --git a/test/js/test/icinga/moduleTest.js b/test/js/test/icinga/moduleTest.js new file mode 100644 index 000000000..da89f4558 --- /dev/null +++ b/test/js/test/icinga/moduleTest.js @@ -0,0 +1,190 @@ +/** +* Test cases for the module loading implementation +* +* +**/ + + +// {{LICENSE_HEADER}} +// {{LICENSE_HEADER}} +var should = require("should"); +var rjsmock = require("requiremock.js"); + +var BASE = "../../../../public/js/"; +require(BASE+"icinga/module.js"); + +var module = rjsmock.getDefine(); + +/** +* Test module that only uses eventhandlers and +* no custom logic +**/ +var eventOnlyModule = function() { + var thiz = this; + this.moduleLinkClick = false; + this.formFocus = false; + + var onModuleLinkClick = function() { + thiz.moduleLinkClick = true; + }; + var onFormFocus = function() { + thiz.formFocus = true; + }; + this.eventHandler = { + '.myModule a' : { + 'click': onModuleLinkClick + }, + '.myModule div.test input' : { + 'focus' : onFormFocus + } + }; +}; +/** +* Module that defines an own enable and disable function +* that is called additionally to the eventhandler setup +* +**/ +var customLogicModule = function() { + var thiz = this; + this.clicked = false; + this.customEnable = false; + this.customDisable = false; + + var onClick = function() { + thiz.clicked = true; + }; + + this.enable = function() { + thiz.customEnable = true; + }; + + this.disable = function() { + thiz.customDisable = true; + }; + + this.eventHandler = { + '.myModule a' : { + 'click' : onClick + } + }; +}; + +var setupTestDOM = function() { + tearDownTestDOM(); + $('body').append($('
') + .append($('funkey')) + .append($('
') + .append(''))); +}; + +var tearDownTestDOM = function() { + $('body').off(); + $('body').empty(); +}; + +describe('Module loader', function() { + var err = null; + var errorCallback = function(error) { + err = error; + }; + + + it('Should call the errorCallback when module isn\'t found', function() { + err = null; + rjsmock.purgeDependencies(); + module.resetHard(); + module.enableModule("testModule", errorCallback); + should.exist(err); + }); + + + it('Should register model event handlers when an \'eventHandler\' attribute exists', function() { + rjsmock.purgeDependencies(); + var testModule = new eventOnlyModule(); + rjsmock.registerDependencies({ + testModule: testModule + }); + err = null; + var toTest = null; + + // Test event handler + setupTestDOM(); + + // Enable the module and asser it is recognized and enabled + module.enableModule("testModule", errorCallback, function(enabled) { + toTest = enabled; + }); + should.not.exist(err, "An error occured during loading: "+err); + should.exist(toTest, "The injected test module didn't work!"); + should.exist(toTest.enable, "Implicit enable method wasn't created"); + $('.myModule a').click(); + should.equal(toTest.moduleLinkClick, true, "Click on link should trigger handler"); + + $('.myModule div.test input').focus(); + should.equal(toTest.formFocus, true, "Form focus should trigger handler"); + + tearDownTestDOM(); + }); + + it('Should be able to deregister events handlers when disable() is called', function() { + rjsmock.purgeDependencies(); + var testModule = new eventOnlyModule(); + rjsmock.registerDependencies({ + testModule: testModule + }); + err = null; + var toTest = null; + + setupTestDOM(); + + module.enableModule("testModule", errorCallback, function(enabled) { + toTest = enabled; + }); + should.not.exist(err, "An error occured during loading: "+err); + should.exist(toTest, "The injected test module didn't work!"); + should.exist(toTest.enable, "Implicit enable method wasn't created"); + + $('.myModule a').click(); + should.equal(toTest.moduleLinkClick, true, "Click on link should trigger handler"); + toTest.moduleLinkClick = false; + + module.disableModule("testModule"); + $('.myModule a').click(); + should.equal(toTest.moduleLinkClick, false, "Click on link shouldn't trigger handler when module is disabled"); + tearDownTestDOM(); + $('body').unbind(); + }); + + it('Should additionally call custom enable and disable functions', function() { + + rjsmock.purgeDependencies(); + var testModule = new customLogicModule(); + rjsmock.registerDependencies({ + testModule: testModule + }); + err = null; + var toTest = null; + + // Test event handler + setupTestDOM(); + + module.enableModule("testModule", errorCallback, function(enabled) { + toTest = enabled; + }); + should.not.exist(err, "An error occured during loading: "+err); + should.exist(toTest, "The injected test module didn't work!"); + should.exist(toTest.enable, "Implicit enable method wasn't created"); + should.equal(toTest.customEnable, true, "Custom enable method wasn't called"); + $('.myModule a').click(); + should.equal(toTest.clicked, true, "Click on link should trigger handler"); + toTest.clicked = false; + + module.disableModule("testModule"); + should.equal(toTest.customDisable, true, "Custom disable method wasn't called"); + $('.myModule a').click(); + should.equal(toTest.clicked, false, "Click on link shouldn't trigger handler when module is disabled"); + tearDownTestDOM(); + $('body').unbind(); + }); + +}); From 0d0db281f726d39db5388dfe3beea8de9738b2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Fri, 14 Jun 2013 14:47:31 +0200 Subject: [PATCH 3/3] Test and icinga.js module setup procedure Behaviours are now modules and icinga.js automatically requests module/list (which should be served by the application and is non static) and retrieves a list of modules to request and enable. refs #3753 --- public/js/icinga/icinga.js | 42 +++++---- public/js/icinga/modules/actionTable.js | 118 ++++++++++++++++++++++++ public/js/icinga/modules/mainDetail.js | 94 +++++++++++++++++++ test/js/test/icinga/moduleTest.js | 40 +++++++- 4 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 public/js/icinga/modules/actionTable.js create mode 100755 public/js/icinga/modules/mainDetail.js diff --git a/public/js/icinga/icinga.js b/public/js/icinga/icinga.js index a9791d9d0..9be6b9013 100755 --- a/public/js/icinga/icinga.js +++ b/public/js/icinga/icinga.js @@ -1,27 +1,25 @@ /*global Icinga:false, document: false, define:false require:false base_url:false console:false */ define([ 'jquery', - 'vendor/jquery.sparkline.min', 'logging', - 'icinga/behaviour', + 'icinga/module', 'icinga/util/async', 'icinga/container', 'modules/list' -], function ($,sparkline,log,behaviour,async,containerMgr, modules) { +], function ($, log, moduleMgr, async, containerMgr, modules) { 'use strict'; /** * Icinga prototype */ var Icinga = function() { - var internalBehaviours = ['icinga/behaviour/actionTable','icinga/behaviour/mainDetail']; + var internalModules = ['icinga/modules/actionTable','icinga/modules/mainDetail']; this.modules = {}; var failedModules = []; var initialize = function () { - require(['modules/list']); - enableDefaultBehaviour(); + enableInternalModules(); containerMgr.registerAsyncMgr(async); containerMgr.initializeContainers(document); @@ -30,9 +28,20 @@ define([ enableModules(); }; - var enableDefaultBehaviour = function() { - $.each(internalBehaviours,function(idx,behaviourImpl) { - behaviour.enableBehaviour(behaviourImpl,log.error); + + + var enableInternalModules = function() { + $.each(internalModules,function(idx,module) { + moduleMgr.enableModule(module, log.error); + }); + }; + + var loadModuleScript = function(name) { + moduleMgr.enableModule("modules/"+name, function(error) { + failedModules.push({ + name: name, + errorMessage: error + }); }); }; @@ -40,17 +49,10 @@ define([ moduleList = moduleList || modules; $.each(modules,function(idx,module) { - if(module.behaviour) { - behaviour.enableBehaviour(module.name+"/"+module.name,function(error) { - failedModules.push({name: module.name,errorMessage: error}); - }); - } + loadModuleScript(module.name); }); }; - var enableCb = function(behaviour) { - behaviour.enable(); - }; $(document).ready(initialize.bind(this)); @@ -60,10 +62,14 @@ define([ */ loadModule: function(blubb,bla) { behaviour.registerBehaviour(blubb,bla); - } , + }, loadIntoContainer: function(ctr) { + }, + + getFailedModules: function() { + return failedModules; } }; diff --git a/public/js/icinga/modules/actionTable.js b/public/js/icinga/modules/actionTable.js new file mode 100644 index 000000000..e8e61952b --- /dev/null +++ b/public/js/icinga/modules/actionTable.js @@ -0,0 +1,118 @@ +/*global Icinga:false, document: false, define:false require:false base_url:false console:false */ + +/** + * ActionTable behaviour as described in + * https://wiki.icinga.org/display/cranberry/Frontend+Components#FrontendComponents-ActionTable + * + * @TODO: Row selection + */ +define(['jquery','logging','icinga/util/async'],function($,log,async) { + "use strict"; + + var ActionTableBehaviour = function() { + var onTableHeaderClick; + + var TABLE_BASE_MATCHER = '.icinga-container table.action'; + var linksInActionTable = TABLE_BASE_MATCHER+" tbody tr > a"; + var actionTableRow = TABLE_BASE_MATCHER+" tbody tr"; + var headerRow = TABLE_BASE_MATCHER+" > th a"; + var searchField = ".icinga-container .actiontable.controls input[type=search]"; + + + onTableHeaderClick = function (ev) { + var target = ev.currentTarget, + href = $(target).attr('href'), + destination; + if ($(target).parents('.layout-main-detail').length) { + if ($(target).parents("#icinga-main").length) { + destination = 'icinga-main'; + } + else { + destination = 'icinga-detail'; + } + + } else { + destination = 'icinga-main'; + } + async.loadToTarget(destination, href); + ev.preventDefault(); + ev.stopImmediatePropagation(); + return false; + }; + + var onLinkTagClick = function(ev) { + + var target = ev.currentTarget, + href = $(target).attr('href'), + destination; + if ($(target).parents('.layout-main-detail').length) { + destination = 'icinga-detail'; + } else { + destination = 'icinga-main'; + } + async.loadToTarget(destination,href); + ev.preventDefault(); + ev.stopImmediatePropagation(); + return false; + + }; + + var onTableRowClick = function(ev) { + ev.stopImmediatePropagation(); + + var target = $(ev.currentTarget), + href = target.attr('href'), + destination; + $('tr.active',target.parent('tbody').first()).removeClass("active"); + target.addClass('active'); + + // When the tr has a href, act like it is a link + if(href) { + ev.currentTarget = target.first(); + return onLinkTagClick(ev); + } + // Try to find a designated row action + var links = $("a.row-action",target); + if(links.length) { + ev.currentTarget = links.first(); + return onLinkTagClick(ev); + } + + // otherwise use the first anchor tag + links = $("a",target); + if(links.length) { + ev.currentTarget = links.first(); + return onLinkTagClick(ev); + } + + log.debug("No target for this table row found"); + return false; + }; + + var onSearchInput = function(ev) { + ev.stopImmediatePropagation(); + var txt = $(this).val(); + }; + + this.eventHandler = {}; + this.eventHandler[linksInActionTable] = { + 'click' : onLinkTagClick + }; + this.eventHandler[actionTableRow] = { + 'click' : onTableRowClick + }; + this.eventHandler[headerRow] = { + 'click' : onTableHeaderClick + }; + this.eventHandler[searchField] = { + 'keyup' : onSearchInput + }; + + this.enable = function() { + + }; + }; + + return new ActionTableBehaviour(); + +}); \ No newline at end of file diff --git a/public/js/icinga/modules/mainDetail.js b/public/js/icinga/modules/mainDetail.js new file mode 100755 index 000000000..9a207c933 --- /dev/null +++ b/public/js/icinga/modules/mainDetail.js @@ -0,0 +1,94 @@ +/*global Icinga:false, document: false, define:false require:false base_url:false console:false */ + +/** + * Main-Detail layout behaviour as described in + * https://wiki.icinga.org/display/cranberry/Frontend+Components#FrontendComponents-Behaviour + * + */ +define(['jquery','logging','icinga/util/async'],function($,log,async) { + "use strict"; + + var MainDetailBehaviour = function() { + + var onOuterLinkClick = function(ev) { + var a = $(ev.currentTarget), + target = a.attr("target"), + href = a.attr("href"); + ev.stopImmediatePropagation(); + collapseDetailView(); + async.loadToTarget("icinga-main",href); + return false; + }; + + var onLinkTagClick = function(ev) { + + var a = $(ev.currentTarget), + target = a.attr("target"), + href = a.attr("href"); + + // check for protocol:// + if(/^[A-Z]{2,10}\:\/\//i.test(href)) { + window.open(href); + ev.stopImmediatePropagation(); + return false; + } + + // check for link in table header + if(a.parents('th').length > 0) { + ev.stopImmediatePropagation(); + return false; + } + + if(typeof target === "undefined") { + if(a.parents("#icinga-detail").length) { + async.loadToTarget("icinga-detail",href); + } else { + async.loadToTarget("icinga-main",href); + } + } else { + switch(target) { + case "main": + async.loadToTarget("icinga-main",href); + collapseDetailView(); + break; + case "detail": + async.loadToTarget("icinga-detail",href); + break; + case "popup": + async.loadToTarget(null,href); + break; + default: + return true; + } + } + ev.stopImmediatePropagation(); + return false; + }; + + var expandDetailView = function() { + $("#icinga-detail").parents(".collapsed").removeClass('collapsed'); + }; + + var collapseDetailView = function(elementInDetailView) { + $("#icinga-detail").parents(".layout-main-detail").addClass('collapsed'); + }; + + this.expandDetailView = expandDetailView; + this.collapseDetailView = collapseDetailView; + + this.eventHandler = { + '.layout-main-detail * a' : { + 'click' : onLinkTagClick + }, + 'a' : { + 'click' : onOuterLinkClick + }, + '.layout-main-detail .icinga-container#icinga-detail' : { + 'focus' : expandDetailView + } + }; + }; + + return new MainDetailBehaviour(); +}); + diff --git a/test/js/test/icinga/moduleTest.js b/test/js/test/icinga/moduleTest.js index da89f4558..6a446b5f4 100644 --- a/test/js/test/icinga/moduleTest.js +++ b/test/js/test/icinga/moduleTest.js @@ -14,7 +14,7 @@ var BASE = "../../../../public/js/"; require(BASE+"icinga/module.js"); var module = rjsmock.getDefine(); - +GLOBAL.document = $('body'); /** * Test module that only uses eventhandlers and * no custom logic @@ -186,5 +186,41 @@ describe('Module loader', function() { tearDownTestDOM(); $('body').unbind(); }); - +}); + + +describe('The icinga module bootstrap', function() { + it("Should automatically load all enabled modules", function() { + rjsmock.purgeDependencies(); + var testClick = false; + rjsmock.registerDependencies({ + "icinga/module": module, + "modules/test" : { + eventHandler: { + "a.test" : { + click : function() { + testClick = true; + } + } + } + }, + "icinga/container" : { + registerAsyncMgr: function() {}, + initializeContainers: function() {} + }, + "modules/list" : [ + { name: 'test' }, + { name: 'test2'} // this one fails + ] + }); + tearDownTestDOM(); + require(BASE+"icinga/icinga.js"); + var icinga = rjsmock.getDefine(); + $('body').append($("")); + $('a.test').click(); + should.equal(testClick, true, "Module wasn't automatically loaded!"); + icinga.getFailedModules().should.have.length(1); + should.equal(icinga.getFailedModules()[0].name, "test2"); + tearDownTestDOM(); + }); });