From ed2b3308436d0368252f56a521bdc29999985ff1 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 18 Feb 2014 19:00:00 +0000 Subject: [PATCH] A few JS files where missing --- public/js/icinga/events.js | 239 ++++++++++++++++++++++++++ public/js/icinga/loader.js | 338 +++++++++++++++++++++++++++++++++++++ public/js/icinga/module.js | 112 ++++++++++++ public/js/icinga/timer.js | 147 ++++++++++++++++ public/js/icinga/ui.js | 117 +++++++++++++ public/js/icinga/utils.js | 97 +++++++++++ 6 files changed, 1050 insertions(+) create mode 100644 public/js/icinga/events.js create mode 100644 public/js/icinga/loader.js create mode 100644 public/js/icinga/module.js create mode 100644 public/js/icinga/timer.js create mode 100644 public/js/icinga/ui.js create mode 100644 public/js/icinga/utils.js diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js new file mode 100644 index 000000000..d61b786d3 --- /dev/null +++ b/public/js/icinga/events.js @@ -0,0 +1,239 @@ +(function(Icinga) { + + Icinga.Events = function(icinga) { + this.icinga = icinga; + }; + + Icinga.Events.prototype = { + + /** + * Icinga will call our initialize() function once it's ready + */ + initialize: function() + { + this.applyGlobalDefaults(); + this.icinga.ui.prepareContainers(); + }, + + // TODO: What's this? + applyHandlers: function(el) + { + var icinga = this.icinga; + $('.dashboard > div', el).each(function(idx, el) { + var url = $(el).attr('data-icinga-url'); + if (typeof url === 'undefined') return; + icinga.loader.loadUrl(url, $(el)); + }); + // Set first links href in a action table tr as row href: + $('table.action tr', el).each(function(idx, el) { + var $a = $('a[href]', el).first(); + if ($a.length) { + $(el).attr('href', $a.attr('href')); + } + }); + $('.icinga-module', el).each(function(idx, mod) { + $mod = $(mod); + var moduleName = $mod.data('icinga-module'); + if (icinga.hasModule(moduleName)) { + var module = icinga.module(moduleName); + // NOT YET, the applyOnloadDings: module.applyEventHandlers(mod); + } + }); + }, + /** + * Global default event handlers + */ + applyGlobalDefaults: function() + { + // We catch resize events + $(window).on('resize', { self: this }, this.onWindowResize); + + // We catch scroll events in our containers + $('.container').on('scroll', icinga.events.onContainerScroll); + + // We want to catch each link click + $(document).on('click', 'a', { self: this }, this.linkClicked); + + // We treat tr's with a href attribute like links + $(document).on('click', 'tr[href]', { self: this }, this.linkClicked); + + // We catch all form submit events + $(document).on('submit', 'form', { self: this }, this.submitForm); + + // We support an 'autosubmit' class on dropdown form elements + $(document).on('change', 'form select.autosubmit', { self: this }, this.submitForm); + + $(window).on('popstate', { self: this }, this.historyChanged); + + // TBD: a global autocompletion handler + // $(document).on('keyup', 'form.auto input', this.formChangeDelayed); + // $(document).on('change', 'form.auto input', this.formChanged); + // $(document).on('change', 'form.auto select', this.submitForm); + }, + + historyChanged: function(event) + { + var icinga = event.data.self.icinga; + if (event.originalEvent.state === null) { + icinga.logger.debug('No more history steps available'); + } else { + icinga.logger.debug(event.originalEvent.state); + } + icinga.loader.loadUrl( + document.location.pathname + document.location.search, + $('#col1') + ).historyTriggered = true; + }, + + /** + * Our window got resized, let's fix our UI + */ + onWindowResize: function(event) + { + var icinga = event.data.self.icinga; + icinga.ui.fixControls(); + }, + + /** + * A scroll event happened in one of our containers + */ + onContainerScroll: function(event) + { + // Yet ugly. And PLEASE, not so often + icinga.ui.fixControls(); + }, + + /** + * + */ + submitForm: function (event) + { + var icinga = event.data.self.icinga; + event.stopPropagation(); + event.preventDefault(); + + // .closest is not required unless subelements to trigger this + var $form = $(event.currentTarget).closest('form'); + var url = $form.attr('action'); + var method = $form.attr('method'); + + var data = $form.serializeArray(); + // TODO: Check button + data.push({ name: 'btn_submit', value: 'yesss' }); + + icinga.logger.debug('Submitting form: ' + method + ' ' + url); + + + // We should move this to a generic target-finder: + var $target = $form.closest('.container'); + if ($target.length == 0) { + $target = $('#body'); + } + + icinga.loader.loadUrl(url, $target, data, method); + + // TODO: Do we really need to return false with stop/preventDefault? + return false; + }, + + + /** + * Someone clicked a link or tr[href] + */ + linkClicked: function(event) + { + var icinga = event.data.self.icinga; + + var $a = $(this); + var href = $a.attr('href'); + event.stopPropagation(); + event.preventDefault(); + if (href === '#') { + if ($a.closest('#menu')) { + var $li = $a.closest('li'); + $li.siblings('li.active').removeClass('active'); + $li.addClass('active'); + } + return; + } + var $target = $('#col1'); + var $container = $a.closest('.container'); +// If link is hash tag... + if ($a.closest('table').length) { + $target = $('#col2'); + $('#layout').addClass('twocols'); + icinga.ui.fixControls(); + } + if ($a.closest('[data-base-target]').length) { + $target = $('#' + $a.closest('[data-base-target]').data('base-target')); + $('#layout').addClass('twocols'); + icinga.ui.fixControls(); + } + if ($a.closest('.tree').length) { + var $li = $a.closest('li'); + if ($li.find('li').length) { + if ($li.hasClass('collapsed')) { + $li.removeClass('collapsed'); + } else { + $li.addClass('collapsed'); + $li.find('li').addClass('collapsed'); + } + return false; + } else { + $target = $('#col2'); + $('#layout').addClass('twocols'); + icinga.ui.fixControls(); + } + } + icinga.loader.loadUrl(href, $target); + event.stopPropagation(); + event.preventDefault(); + if ($a.closest('#menu').length) { + $('#layout').removeClass('twocols'); + $('#col2').html(''); + icinga.ui.fixControls(); + return false; + } + if ($a.closest('table').length) { + if ($('#layout').hasClass('twocols')) { + if ($target.attr('id') === 'col2') return; + icinga.logger.debug('Switching to single col'); + $('#layout').removeClass('twocols'); + icinga.ui.fixControls(); + } else { + icinga.logger.debug('Switching to double col'); + $('#layout').addClass('twocols'); + icinga.ui.fixControls(); + } + return false; + } + }, + +/* + hrefIsHashtag: function(href) + { + // WARNING: IE gives full URL :( + // Also it doesn't support negativ indexes in substr + return href.substr(href.length - 1, 1) == '#'; + }, +*/ + + unbindGlobalHandlers: function() + { + $(window).off('popstate', this.historyChanged); + $(window).off('resize', this.onWindowResize); + $(document).off('scroll', '.container', this.onContainerScroll); + $(document).off('click', 'a', this.linkClicked); + $(document).off('click', 'tr[href]', this.linkClicked); + $(document).off('submit', 'form', this.submitForm); + $(document).off('change', 'form select.autosubmit', this.submitForm); + }, + + destroy: function() { + // This is gonna be hard, clean up the mess + this.unbindGlobalHandlers(); + this.icinga = null; + } + }; + +}(Icinga)); diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js new file mode 100644 index 000000000..22998c718 --- /dev/null +++ b/public/js/icinga/loader.js @@ -0,0 +1,338 @@ +/** + * Icinga.Loader + * + * This is where we take care of XHR requests, responses and failures. + */ +(function(Icinga) { + + Icinga.Loader = function(icinga) { + + /** + * YES, we need Icinga + */ + this.icinga = icinga; + + /** + * Our base url + */ + this.baseUrl = icinga.config.baseUrl; + + this.failureNotice = null; + + this.exception = null; + + /** + * Pending requests + */ + this.requests = {}; + + this.autorefreshEnabled = true; + }; + + Icinga.Loader.prototype = { + + initialize: function() + { + this.icinga.timer.register(this.autorefresh, this, 10000); + }, + + /** + * Load the given URL to the given target + * + * @param {string} url URL to be loaded + * @param {object} target Target jQuery element + * @param {object} data Optional parameters, usually for POST requests + * @param {string} method HTTP method, default is 'GET' + */ + loadUrl: function (url, $target, data, method) + { + var id = null; + + // Default method is GET + if (typeof method === 'undefined') { + method = 'GET'; + } + + this.icinga.logger.debug('Loading ', url, ' to ', $target); + + // We should do better and ignore requests without target and/or id + if (typeof $target !== 'undefined' && $target.attr('id')) { + id = $target.attr('id'); + } + if (typeof $target !== 'undefined') { + // TODO: We shouldn't use data but keep this information somewhere else. + if ($target.data('icingaUrl') !== url) { + $target.removeAttr('data-icinga-url'); + $target.removeAttr('data-icinga-refresh'); + $target.removeData('icingaUrl'); + $target.removeData('icingaRefresh'); + } + } + + // If we have a pending request for the same target... + if (id in this.requests) { + // ...ignore the new request if it is already pending with the same URL + if (this.requests[id].url === url) { + this.icinga.logger.debug('Request to ', url, ' is already running for ', $target); + return this.requests[id]; + } + // ...or abort the former request otherwise + this.icinga.logger.debug('Aborting pending request loading ', url, ' to ', $target); + this.requests[id].abort(); + } + + // Not sure whether we need this Accept-header + var headers = { 'X-Icinga-Accept': 'text/html' }; + + // Ask for a new window id in case we don't already have one + if (this.icinga.hasWindowId()) { + headers['X-Icinga-WindowId'] = this.icinga.getWindowId(); + } else { + headers['X-Icinga-WindowId'] = 'undefined'; + } + + var self = this; + var req = $.ajax({ + type : method, + url : url, + data : data, + headers: headers, + context: self + }); + + req.$target = $target; + req.url = url; + req.done(this.onResponse); + req.fail(this.onFailure); + req.historyTriggered = false; + req.autorefresh = false; + if (id) { + this.requests[id] = req; + } + return req; + }, + + /** + * Create an URL relative to the Icinga base Url, still unused + * + * @param {string} url Relative url + */ + url: function(url) + { + if (typeof url === 'undefined') { + return this.baseUrl; + } + return this.baseUrl + url; + }, + + autorefresh: function() + { + var self = this; + if (self.autorefreshEnabled !== true) { + return; + } + + $('.container[data-icinga-refresh]').each(function(idx, el) { + var $el = $(el); + self.loadUrl($el.data('icingaUrl'), $el).autorefresh = true; + el = null; + }); + }, + + disableAutorefresh: function() + { + this.autorefreshEnabled = false; + }, + + enableAutorefresh: function() + { + this.autorefreshEnabled = true; + }, + + /** + * Handle successful XHR response + */ + onResponse: function (data, textStatus, req) + { + if (this.failureNotice !== null) { + this.failureNotice.remove(); + this.failureNotice = null; + } + + if (this.exception !== null) { + this.exception.remove(); + this.exception = null; + req.$target.removeClass('impact'); + } + + var url = req.url; + var targetId = req.$target.attr('id'); + this.icinga.logger.debug('Got response for ', req.$target, ', URL was ' + url); + + if (! req.autorefresh) { + // TODO: Hook for response/url? + var $matches = $('[href="' + url + '"]'); + $matches.each(function(idx, el) { + if ($(el).closest('#menu').length) { + $(el).closest('#menu').find('li.active').removeClass('active'); + } else if ($(el).closest('table.action').length) { + $(el).closest('table.action').find('.active').removeClass('active'); + } + }); + + + $matches.each(function(idx, el) { + if ($(el).closest('#menu').length) { + $(el).closest('li').addClass('active'); + $(el).parents('li').addClass('active'); + } else if ($(el).closest('table.action').length) { + $(el).addClass('active'); + } + }); + } + + delete this.requests[targetId]; + req.$target.attr('icingaurl', this.url); + + // + var target = req.getResponseHeader('X-Icinga-Container'); + if (target) { + req.$target = $('body'); + } + + var refresh = req.getResponseHeader('X-Icinga-Refresh'); + if (refresh) { + // Hmmmm... .data() doesn't work here? + req.$target.attr('data-icinga-refresh', refresh); + req.$target.attr('data-icinga-url', req.url); + } + + // Set a window identifier if the server asks us to do so + var windowId = req.getResponseHeader('X-Icinga-WindowId'); + if (windowId) { + this.icinga.setWindowId(windowId); + } + + // Update history when necessary. Don't do so for requests triggered + // by history or autorefresh events + if (! req.historyTriggered && ! req.autorefresh) { + + // We only want to care about top-level containers + if (req.$target.parent().closest('.container').length === 0) { + this.icinga.logger.debug('Pushing ', req.url, ' to history'); + window.history.pushState({icinga: true}, null, req.url); + } + } + $resp = $(req.responseText); + + /* Should we try to fiddle with responses containing full HTML? */ + /* + if ($('body', $resp).length) { + req.responseText = $('script', $('body', $resp).html()).remove(); + } + */ + + this.renderContentToContainer(req.responseText, req.$target); + }, + + /** + * Handle failed XHR response + */ + onFailure: function (req, textStatus, errorThrown) + { + var url = req.url; + delete this.requests[req.$target.attr('id')]; + + if (req.status === 500) { + if (this.exception === null) { + req.$target.addClass('impact'); + + this.exception = this.createNotice( + 'error', + $('h1', $(req.responseText)).first().html() +/* 'The connection to the Icinga web server has been lost at ' + + this.icinga.utils.timeShort() + + '.' +*/ + ); + this.icinga.ui.fixControls(); + } + } else if (req.status > 0) { + this.icinga.logger.debug(req.responseText.slice(0, 100)); + this.renderContentToContainer( + '

' + req.status + ' ' + errorThrown + '

' + + req.responseText, + req.$target + ); + + // Header example: + // Icinga.debug(req.getResponseHeader('X-Icinga-Redirect')); + } else { + if (errorThrown === 'abort') { + this.icinga.logger.info('Request to ', url, ' has been aborted for ', req.$target); + } else { + if (this.failureNotice === null) { + this.failureNotice = this.createNotice( + 'error', + 'The connection to the Icinga web server has been lost at ' + + this.icinga.utils.timeShort() + + '.' + ); + + this.icinga.ui.fixControls(); + } + this.icinga.logger.error( + 'Failed to contact web server loading ', + url, + ' for ', + req.$target + ); + } + } + }, + + createNotice: function(severity, message) { + return $('
  • ' + message + '
  • ').appendTo($('#notifications')); + }, + + /** + * Smoothly render given HTML to given container + */ + renderContentToContainer: function (content, $container) + { + // Disable all click events while rendering + $('*').click(function( event ) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + }); + + // Container update happens here + var scrollPos = $container.scrollTop(); + $container.html(content); + $container.scrollTop(scrollPos); + + // TODO: this.icinga.events.refreshContainer(container); + var icinga = this.icinga; + icinga.events.applyHandlers($container); + icinga.ui.initializeControls($container); + icinga.ui.fixControls(); + + // Re-enable all click events + $('*').off('click'); + }, + + /** + * On shutdown we kill all pending requests + */ + destroy: function() { + $.each(this.requests, function(id, request) { + request.abort(); + }); + this.icinga = null; + this.requests = {}; + } + + }; + +}(Icinga)); diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js new file mode 100644 index 000000000..c2134d96c --- /dev/null +++ b/public/js/icinga/module.js @@ -0,0 +1,112 @@ +/** + * This is how we bootstrap JS code in our modules + */ +(function(Icinga) { + + Icinga.Module = function(icinga, name, prototyp) { + + // The Icinga instance + this.icinga = icinga; + + // Event handlers registered by this module + this.handlers = []; + + this.registeredHandlers = {}; + + // The module name + this.name = name; + + // The JS prototype for this module + this.prototyp = prototyp; + + // Once initialized, this will be an instance of the modules prototype + this.object = {}; + + // Initialize this module + this.initialize(); + }; + + Icinga.Module.prototype = { + + initialize: function() + { + try { + // The constructor of the modules prototype must be prepared to get an + // instance of Icinga.Module + this.object = new this.prototyp(this); + this.applyRegisteredEventHandlers(); + } catch(e) { + this.icinga.logger.error('Failed to load module ', this.name, ': ', e); + return false; + } + + // That's all, the module is ready + this.icinga.logger.debug('Module ' + this.name + ' has been initialized'); + return true; + }, + + /** + * Globally register this modules event handlers + */ + registerEventHandlers: function(handlers) + { + this.registeredHandlers = handlers; + return this; + }, + + applyRegisteredEventHandlers: function() + { + var self = this; + $.each(this.registeredHandlers, function(filter, events) { + $.each(events, function (event, handler) { + // TODO: if (event[1] === 'each') { + // $(event[0], $(el)).each(event[2]); + self.bindEventHandler( + event, + '.module-' + self.name + ' ' + filter, + handler + ); + }); + }); + self = null; + return this; + }, + + /** + * Effectively bind the given event handler + */ + bindEventHandler: function(event, filter, handler) + { + var self = this; + this.icinga.logger.debug('Bound ' + filter + ' .' + event + '()'); + this.handlers.push([event, filter, handler]); + $(document).on(event, filter, handler.bind(self.object)); + }, + + /** + * Unbind all event handlers bound by this module + */ + unbindEventHandlers: function() + { + $.each(this.handlers, function(idx, handler) { + $(document).off(handler[0], handler[1], handler[2]); + }); + }, + + /** + * Allow to destroy and clean up this module + */ + destroy: function() + { + this.unbindEventHandlers(); + if (typeof this.object.destroy === 'function') { + this.object.destroy(); + } + this.object = null; + this.icinga = null; + this.prototyp = null; + } + + }; + +}(Icinga)); diff --git a/public/js/icinga/timer.js b/public/js/icinga/timer.js new file mode 100644 index 000000000..e1a92551f --- /dev/null +++ b/public/js/icinga/timer.js @@ -0,0 +1,147 @@ +/** + * Icinga.Timer + * + * Timer events are triggered once a second. Runs all reegistered callback + * functions and is able to preserve a desired scope. + */ +(function(Icinga) { + + Icinga.Timer = function(icinga) { + + /** + * We keep a reference to the Icinga instance even if we don't need it + */ + this.icinga = icinga; + + /** + * The Interval object + */ + this.ticker = null; + + /** + * Fixed default interval is 250ms + */ + this.interval = 250; + + /** + * Our registerd observers + */ + this.observers = []; + + /** + * Counter + */ + this.stepCounter = 0; + + this.start = (new Date()).getTime(); + + + this.lastRuntime = []; + }; + + Icinga.Timer.prototype = { + + /** + * The initialization function starts our ticker + */ + initialize: function(icinga) + { + var self = this; + this.ticker = setInterval(function() { self.tick(); }, this.interval); + }, + + /** + * We will trigger our tick function once a second. It will call each + * registered observer. + */ + tick: function() + { + var icinga = this.icinga; + $.each(this.observers, function(idx, observer) { + if (observer.isDue()) { + observer.run(); + } else { + // Not due + } + }); + icinga = null; + }, + + /** + * Register a given callback function to be run within an optional scope. + */ + register: function(callback, scope, interval) + { + try { + if (typeof scope === 'undefined') { + this.observers.push(new Icinga.Timer.Interval(callback, interval)); + } else { + this.observers.push( + new Icinga.Timer.Interval( + callback.bind(scope), + interval + ) + ); + } + } catch(err) { + this.icinga.logger.error(err); + } + }, + + /** + * Our destroy function will clean up everything. Unused right now. + */ + destroy: function() + { + if (this.ticker !== null) { + clearInterval(this.ticker); + } + this.icinga = null; + $.each(this.observers, function(idx, observer) { + observer.destroy(); + }); + this.observers = []; + } + }; + + Icinga.Timer.Interval = function(callback, interval) { + + if (typeof interval === 'undefined') { + throw 'Timer interval is required'; + } + + if (interval < 100) { + throw 'Timer interval cannot be less than 100ms, got ' + interval; + } + + this.lastRun = (new Date()).getTime(); + + this.interval = interval; + + this.scheduledNextRun = this.lastRun + interval; + + this.callback = callback; + }; + + Icinga.Timer.Interval.prototype = { + isDue: function() + { + return this.scheduledNextRun < (new Date()).getTime(); + }, + + run: function() + { + this.lastRun = (new Date()).getTime(); + while (this.scheduledNextRun < this.lastRun) { + this.scheduledNextRun += this.interval; + } + this.callback(); + }, + + destroy: function() + { + this.callback = null; + } + }; + +}(Icinga)); diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js new file mode 100644 index 000000000..74a04d0d3 --- /dev/null +++ b/public/js/icinga/ui.js @@ -0,0 +1,117 @@ +(function(Icinga) { + + Icinga.UI = function(icinga) { + this.icinga = icinga; + }; + + Icinga.UI.prototype = { + initialize: function() + { + this.icinga.timer.register(this.refreshDebug, this, 1000); + this.refreshDebug(); + }, + + prepareContainers: function () + { + var icinga = this.icinga; + $('.container').each(function(idx, el) { + icinga.events.applyHandlers($(el)); + icinga.ui.initializeControls($(el)); + }); +/* + $('#icinga-main').attr( + 'icingaurl', + window.location.pathname + window.location.search + ); +*/ + }, + refreshDebug: function() + { + var size = this.icinga.ui.getDefaultFontSize().toString(); + var winWidth = $( window ).width(); + var winHeight = $( window ).height(); + $('#responsive-debug').html( + 'Time: ' + + this.icinga.ui.formatHHiiss(new Date) + + '
     1em: ' + + size + + 'px
     Win: ' + + winWidth + + 'x'+ + winHeight + + 'px
    ' + ).css({display: 'block'}); + }, + formatHHiiss: function(date) + { + var hours = date.getHours(); + var minutes = date.getMinutes(); + var seconds = date.getSeconds(); + if (hours < 10) hours = '0' + hours; + if (minutes < 10) minutes = '0' + minutes; + if (seconds < 10) seconds = '0' + seconds; + return hours + ':' + minutes + ':' + seconds; + }, + createFontSizeCalculator: function() + { + var $el = $('
     
    '); + $('#main').append($el); + return $el; + }, + getDefaultFontSize: function() + { + var $calc = $('#fontsize-calc'); + if (! $calc.length) { + $calc = this.createFontSizeCalculator(); + } + return $calc.width() / 1000; + }, + initializeControls: function(parent) + { + var self = this; + $('.controls', parent).each(function(idx, el) { + var $el = $(el); + if (! $el.next('.fake-controls').length) { + var newdiv = $('
    '); + newdiv.css({ + height: $el.css('height') + }); + $el.after(newdiv); + } + }); + this.fixControls(parent); + }, + fixControls: function($parent) + { + var self = this; + if (typeof $parent === 'undefined') { + $('.container').each(function(idx, container) { + self.fixControls($(container)); + }); + return; + } + self.icinga.logger.debug('Fixing controls for ', $parent); + $('.controls', $parent).each(function(idx, el) { + var $el = $(el); + var $fake = $el.next('.fake-controls'); + var y = $parent.scrollTop(); + $el.css({ + position: 'fixed', + top: $parent.offset().top, + width: $fake.css('width') + }); + $fake.css({ + height: $el.css('height'), + display: 'block' + }); + }); + }, + + destroy: function() { + // This is gonna be hard, clean up the mess + this.icinga = null; + } + + }; + +}(Icinga)); diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js new file mode 100644 index 000000000..c80e24189 --- /dev/null +++ b/public/js/icinga/utils.js @@ -0,0 +1,97 @@ +/** + * Icinga utility functions + */ +(function(Icinga) { + + Icinga.Utils = function(icinga) { + + /** + * Utility functions may need access to their Icinga instance + */ + this.icinga = icinga; + + /** + * We will use this to create an URL helper only once + */ + this.url_helper = null; + }; + + Icinga.Utils.prototype = { + + timeWithMs: function(now) + { + if (typeof now === 'undefined') { + now = new Date(); + } + var ms = now.getMilliseconds() + ''; + while (ms.length < 3) { + ms = '0' + ms; + } + return now.toLocaleTimeString() + '.' + ms; + }, + + timeShort: function(now) + { + if (typeof now === 'undefined') { + now = new Date(); + } + return now.toLocaleTimeString().replace(/:\d{2}$/, ''); + }, + + /** + * Parse a given Url and return an object + */ + parseUrl: function(url) + { + if (this.url_helper === null) { + this.url_helper = document.createElement('a'); + } + var a = this.url_helper; + a.href = url; + + var result = { + source : url, + protocol: a.protocol.replace(':', ''), + host : a.hostname, + port : a.port, + query : a.search, + file : (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1], + hash : a.hash.replace('#',''), + path : a.pathname.replace(/^([^\/])/,'/$1'), + relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1], + segments: a.pathname.replace(/^\//,'').split('/'), + params : this.parseParams(a), + }; + a = null; + + return result; + }, + + /** + * Parse url params + */ + parseParams: function(a) { + var params = {}, + segment = a.search.replace(/^\?/,'').split('&'), + len = segment.length, + i = 0, + s; + for (; i < len; i++) { + if (! segment[i]) { continue; } + s = segment[i].split('='); + params[s[0]] = decodeURIComponent(s[1]); + } + return params; + }, + + /** + * Cleanup + */ + destroy: function() + { + this.url_helper = null; + this.icinga = null; + } + }; + +}(Icinga));