From e6b3a54e90b69632d385fe25cdf1a6a1584dbb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannis=20Mo=C3=9Fhammer?= Date: Wed, 11 Sep 2013 17:11:13 +0200 Subject: [PATCH] Add/modify JavaScript components for main/detail and their tests This commit introduces the following changes (although they are not implemented in the templates, this follows): - links in the top and navigation bar are loaded in the background - generic main/detail container component handling click/load delegation - mainDetailGrid implementation handling selection and render targets - Generic history implementation handling URI change detection and reloading refs #4611 --- public/js/icinga/components/container.js | 481 ++++++++++++++++++ public/js/icinga/components/form.js | 20 +- public/js/icinga/components/mainDetailGrid.js | 199 ++++++++ public/js/icinga/icinga.js | 74 ++- public/js/icinga/util/async.js | 199 -------- public/js/main.js | 7 +- public/js/vendor/history.js | 2 +- test/js/test/icinga/asyncTest.js | 34 -- test/js/testlib/requiremock.js | 3 +- 9 files changed, 764 insertions(+), 255 deletions(-) create mode 100644 public/js/icinga/components/container.js create mode 100644 public/js/icinga/components/mainDetailGrid.js delete mode 100644 public/js/icinga/util/async.js delete mode 100644 test/js/test/icinga/asyncTest.js diff --git a/public/js/icinga/components/container.js b/public/js/icinga/components/container.js new file mode 100644 index 000000000..5d1cc1774 --- /dev/null +++ b/public/js/icinga/components/container.js @@ -0,0 +1,481 @@ +/*global Icinga:false, Modernizr: false, document: false, History: false, define:false require:false base_url:false console:false */ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITemplate'], function($, logger, componentLoader, URI) { + "use strict"; + + /** + * Static reference to the main container, populated on the first 'getMainContainer' call + * + * @type {Container} + */ + var mainContainer = null; + + /** + * Static reference to the detail container, populated on the first getDetailContainer call + * + * @type {Container} + */ + var detailContainer = null; + + /** + * A handler for accessing icinga containers, i.e. the #icingamain, #icingadetail containers and specific 'app/container' + * components. + * + * This component can be constructed with every object as the parameter and will provide access to the nearest + * container (which could be the applied object itself, if it is a container) wrapping this object. + * + * The windows url should always be modified with this implementation, so an objects context should point to a + * new URL, call new Container('#myObject').updateContainerHref('/my/url') + * + * This requirejs module also registers a global handler catching all links of the main container and rendering + * their content to the main container, in case you don't want to extend the container with additional handlers. + * + * @param {HTMLElement, jQuery, String} target A jQuery resultset, dom element or matcher string + */ + var Container = function(target) { + + /** + * Enumeration of possible container types + * + * @type {{GENERIC: string, MAIN: string, DETAIL: string}} + */ + var CONTAINER_TYPES = { + 'GENERIC' : 'generic', + 'MAIN' : 'icingamain', + 'DETAIL': 'icingadetail' + }; + + /** + * Set to true when no history changes should be made + * + * @type {boolean} true to disable History.js calls, false to reenable them + */ + this.freezeHistory = false; + + /** + * Return the container that is at the nearest location to this element, or the element itself if it is a container + * + * Containers are either the icingamain and icingadetail ids or components tagged as app/container + * + * @param {String, jQuery, HTMLElement} target The node to use as the starting point + * + * @returns {HTMLElement|null} The nearest container found or null if target is no container + * and no container is above target + */ + var findNearestContainer = function(target) { + target = $(target); + if (target.attr('data-icinga-component') === 'app/container' || + target.attr('id') === 'icingamain' || target.attr('id') === 'icingadetail') { + return target; + } + return target.parents('[data-icinga-component="app/container"], #icingamain, #icingadetail')[0]; + }; + + /** + * Find the container responsible for target and determine it's type + * + * @param {HTMLElement, jQuery, String} target A jQuery resultset, dom element or matcher string + */ + this.construct = function(target) { + this.containerDom = $(findNearestContainer(target)); + this.containerType = CONTAINER_TYPES.GENERIC; + + if (this.containerDom.attr('id') === CONTAINER_TYPES.MAIN) { + this.containerType = CONTAINER_TYPES.MAIN; + } else if (this.containerDom.attr('id') === CONTAINER_TYPES.DETAIL) { + this.containerType = CONTAINER_TYPES.DETAIL; + } else { + this.containerType = CONTAINER_TYPES.GENERIC; + } + }; + + var getWindowLocationWithoutHost = function() { + return window.location.pathname + window.location.search + window.location.hash; + }; + + /** + * Extract and return the main container's location from the current Url + * + * This takes the window's Url and removes the detail part + * + * @returns {string} The Url of the main container + */ + var getMainContainerHrefFromUrl = function() { + // main has the url without the icingadetail part + var href = URI(getWindowLocationWithoutHost()); + href.removeQuery('detail'); + return href.href(); + }; + + /** + * Return the detail container's location from the current Url + * + * This takes the detail parameter of the url and returns it or + * undefined if no location is given + * + * @returns {string|undefined} The Url of the detail container or undefined if no detail container is active + */ + var getDetailContainerHrefFromUrl = function() { + var location = new URI(getWindowLocationWithoutHost()); + var href = URI.parseQuery(location.query()).detail; + if (!href) { + return; + } + // detail is a query param, so it is possible that (due to a bug or whatever) multiple + // detail fields are declared and returned as arrays + if (typeof href !== 'string') { + href = href[0]; + } + // transform the detail parmameter to an Url + return URI(href).href(); + }; + + /** + * Return the Url of this container + * + * This is mostly determined by the Url of the window, but for generic containers we have to rely on the + * "data-icinga-href" attribute of the container (which is also available for main and detail, but less + * reliable during history changes) + * + * @returns {String|undefined} The Url of the container or undefined if the container has no Url set + */ + this.getContainerHref = function() { + switch (this.containerType) { + case CONTAINER_TYPES.MAIN: + return getMainContainerHrefFromUrl(); + case CONTAINER_TYPES.DETAIL: + return getDetailContainerHrefFromUrl(); + case CONTAINER_TYPES.GENERIC: + if (this.containerDom.attr('data-icinga-href')) { + return URI(this.containerDom.attr('data-icinga-href')); + } else { + return URI(getWindowLocationWithoutHost()).href(); + } + } + }; + + /** + * Return a href with representing the current view, but url as the main container + * + * @param {URI} url The main Url to use as an URI.js object + * + * @returns {URI} The modified URI.js containing the new main and the current detail link + */ + var setMainContainerHref = function(url) { + var detail = getDetailContainerHrefFromUrl(); + if (detail) { + url.addQuery('detail', detail); + } + return url; + }; + + /** + * Return a complete Href string representing the current detail href and the provided main Url + * + * @param {URI} url The detail Url to use as an URI.js object + * + * @returns {URI} The modified URI.js containing the new detail and the current main link + */ + var setDetailContainerHref = function(url) { + var location = new URI(window.location.href); + location.removeQuery('detail'); + if (typeof url !== 'undefined') { // no detail Url given + location.addQuery('detail', url); + } + return location; + }; + + /** + * Update the Url of this container and let the Url reflect the new changes, if required + * + * This updates the window Url and the data-icinga-href attribute of the container. The latter one is required + * to see which url is the last one the container displayed (e.g. after History changes, the url has changed + * but the containers data-icinga-href still points to the containers element). + * + * @param {String|URI} url An Url string or a URI.js object representing the new Url for this container + */ + this.updateContainerHref = function(url) { + if (typeof url === "string") { + url = URI(url); + } + var containerUrl, windowUrl; + switch (this.containerType) { + case CONTAINER_TYPES.MAIN: + windowUrl = setMainContainerHref(url); + containerUrl = windowUrl.clone().removeQuery('detail'); + break; + case CONTAINER_TYPES.DETAIL: + windowUrl = setDetailContainerHref(url); + containerUrl = url; + break; + case CONTAINER_TYPES.GENERIC: + containerUrl = url; + break; + } + + if (containerUrl) { + this.containerDom.attr('data-icinga-href', containerUrl); + } else { + this.containerDom.removeAttr('data-icinga-href'); + } + if (!this.freezeHistory) { + History.pushState({container: this.containerDom.attr('id')}, document.title, windowUrl.href()); + } + return windowUrl; + }; + + /** + * Synchronize the container with the currently active window Url + * + * This is called mostly after changes in the history and makes sure the container contains the same content + * as the Url refers to. If the Url is the same as the Url in the container (the one in the data-icinga-href + * attribute), the container will be untouched, otherwise it's content and data-icinga-href attribute will be + * updated with the Url from the window. + */ + this.syncWithCurrentUrl = function() { + if (this.containerType === CONTAINER_TYPES.GENERIC) { + return; // generic containers would require this method to be specialised + } + // Catch initial page loading: Here no data-icinga-href is set and no url is given, so we're safe to ignore this + if (typeof this.containerDom.attr('data-icinga-href') === 'undefined' && + typeof this.getContainerHref() === 'undefined') { + return; + } + // This is the case when an detail is removed on history back + if (typeof this.getContainerHref() === 'undefined' && typeof this.containerDom.attr('data-icinga-href') !== 'undefined') { + this.containerDom.removeAttr('data-icinga-href'); + this.containerDom.empty(); + this.hideDetail(); + logger.debug("Hiding detail panel on Url change"); + return; + } + + if (!URI(this.getContainerHref()).equals(this.containerDom.attr('data-icinga-href'))) { + logger.debug( + "Applying URL change for ", this.containerType, + "from", this.getContainerHref(), + "to", this.containerDom.attr('data-icinga-href') + ); + + if (typeof this.containerDom.attr('data-icinga-href') === 'undefined') { // container is empty now + //this.replaceDom(''); + } else { + this.freezeHistory = true; + this.replaceDomFromUrl(this.getContainerHref()); + this.freezeHistory = false; + } + } else { + logger.debug("No action performed on Url change, same Url for ", this.containerType); + } + }; + + /** + * Load the provided url, stop all pending requests for this container and call replaceDom for the returned html + * + * This method relaods the page if a 401 (Authorization required) header is encountered + * + * @param {String, URI} url The Url to load or and URI.js object encapsulating it + */ + this.replaceDomFromUrl = function(url) { + if (!Modernizr.history) { + window.location.href = this.updateContainerHref(url); + } + + this.updateContainerHref(url); + var scope = this; + if (this.containerDom.pending) { + this.containerDom.pending.abort(); + } + this.containerDom.pending = $.ajax({ + url: url, + success: function(domNodes) { + scope.replaceDom(domNodes); + }, + error: function(response) { + if (response.status === 401) { + window.location.reload(); + } + } + }); + }; + + /** + * Remove all dom nodes from this container and replace them with the ones from domNodes + * + * Triggers the custom "updated" event and causes a rescan for components on the DOM nodes + * + * If keepLayout is given, the detail panel won't be expanded if this is an update for the detail panel, + * otherwise it will be automatically shown. + * + * @param {String, jQuery, HTMLElement, Array} domNodes Any valid representation of the Dom nodes to insert + * @param {boolean} keepLayout Whether to keep the layout untouched, even if detail + * is updated end collapsed + * + * @see registerOnUpdate + */ + this.replaceDom = function(domNodes, keepLayout) { + var newDom = $('#icingamain', domNodes); + this.containerDom.empty().append(newDom.children()); + this.containerDom.trigger('updated', [domNodes]); + componentLoader.load(); + if (!keepLayout) { + if (this.containerType === CONTAINER_TYPES.DETAIL) { + this.showDetail(); + } + } + }; + + /** + * Register a method to be called when this container is updated + * + * @param {function} fn The function to call when the container is updated + */ + this.registerOnUpdate = function(fn) { + this.containerDom.on('updated', fn); + }; + + this.construct(target); + }; + + /** + * Static method for detecting whether the given link is external or only browserside (hash links) + * + * @param {String} link The link to test for being site-related + * + * @returns {boolean} True when the link should be executed with the browsers normal behaviour, false + * when the link should be catched and processed internally + */ + Container.isExternalLink = function(link) { + if (link[0] === '#') { + return true; + } + return (/^\/\//).test(URI(link).relativeTo(window.location.href).href()); + }; + + /** + * Return the page's detail container (which is always there) + * + * @returns {Container} The detail container of the page + */ + Container.getDetailContainer = function() { + detailContainer = detailContainer || new Container('#icingadetail'); + if(!jQuery.contains(document.body, detailContainer)) { + detailContainer = new Container('#icingadetail'); + } + return detailContainer; + }; + + /** + * Return the page's main container (which is always there) + * + * @returns {Container} The main container of the page + */ + Container.getMainContainer = function() { + mainContainer = mainContainer || new Container('#icingamain'); + if(!jQuery.contains(document.body, mainContainer)) { + mainContainer = new Container('#icingamain'); + } + return mainContainer; + }; + + /** + * Expand the detail container and shrinken the main container + * + * Available as a static method on the Container object or as an instance method + */ + Container.prototype.showDetail = Container.showDetail = function() { + var mainDom = Container.getMainContainer().containerDom, + detailDom = Container.getDetailContainer().containerDom; + + mainDom.removeClass(); + detailDom.removeClass(); + mainDom.addClass('hidden-md'); + detailDom.addClass('col-md-12'); + mainDom.addClass('col-lg-7'); + detailDom.addClass('col-lg-5'); + mainDom.addClass('hidden-xs'); + detailDom.addClass('col-xs-12'); + mainDom.addClass('hidden-sm'); + detailDom.addClass('col-sm-12'); + }; + + /** + * Hide the detail container and expand the main container + * + * Also updates the Url by removing the detail part + * + * Available as a static method on the Container object or as an instance method + */ + Container.prototype.hideDetail = Container.hideDetail = function() { + var mainDom = Container.getMainContainer().containerDom, + detailDom = Container.getDetailContainer().containerDom; + + mainDom.removeClass(); + detailDom.removeClass(); + mainDom.addClass('col-md-12'); + detailDom.addClass('hidden-md'); + mainDom.addClass('col-lg-12'); + detailDom.addClass('hidden-lg'); + mainDom.addClass('col-xs-12'); + detailDom.addClass('hidden-xs'); + mainDom.addClass('col-sm-12'); + detailDom.addClass('hidden-sm'); + detailDom.removeAttr('data-icinga-href'); + if (typeof this.freezeHistory === 'undefined' || !this.freezeHistory) { + History.replaceState( + {}, + document.title, + URI(window.location.href).removeQuery('detail').href() + ); + } + }; + if (Modernizr.history) { + /** + * Register the click behaviour of the main container, which means that every link, if not catched in a + * more specific handler, causes an update of the main container if it's not external or a browser behaviour link + * (those starting with '#'). + */ + $('body').on('click', '#icingamain', function(ev) { + var targetEl = ev.target || ev.toElement || ev.relatedTarget; + if (targetEl.tagName.toLowerCase() !== 'a') { + return true; + } + + if (Container.isExternalLink($(targetEl).attr('href'))) { + return true; + } else { + Container.getMainContainer().replaceDomFromUrl(URI($(targetEl).attr('href')).href()); + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); + } + return Container; +}); \ No newline at end of file diff --git a/public/js/icinga/components/form.js b/public/js/icinga/components/form.js index 9b57715fe..8a94f3e1b 100644 --- a/public/js/icinga/components/form.js +++ b/public/js/icinga/components/form.js @@ -43,17 +43,7 @@ define(['jquery'], function($) { */ var ATTR_MODIFIED = 'data-icinga-form-modified'; - /** - * Return true when the input element is a autosubmit field - * - * @param {string|DOMElement|jQuery} el The element to test for autosubmission - * - * @returns {boolean} True when the element should be automatically submitted - */ - var isAutoSubmitInput = function(el) { - return $(el).attr('data-icinga-form-autosubmit') === 'true' || - $(el).attr('data-icinga-form-autosubmit') === '1'; - }; + /** * Takes a form and returns an overloaded jQuery object @@ -105,16 +95,16 @@ define(['jquery'], function($) { */ var registerFormEventHandler = function(form) { form.change(function(changed) { - if (isAutoSubmitInput(changed.target)) { + if ($(changed.target).attr('data-icinga-form-autosubmit')) { form.clearModificationFlag(); - form.submit(); } else { form.setModificationFlag(); } - }); // submissions should clear the modification flag - form.submit(form.clearModificationFlag); + form.submit(function() { + form.clearModificationFlag() + }); }; /** diff --git a/public/js/icinga/components/mainDetailGrid.js b/public/js/icinga/components/mainDetailGrid.js new file mode 100644 index 000000000..1a0d00e39 --- /dev/null +++ b/public/js/icinga/components/mainDetailGrid.js @@ -0,0 +1,199 @@ +/*global Icinga:false, document: false, define:false require:false base_url:false console:false */ +// {{{ICINGA_LICENSE_HEADER}}} +/** + * This file is part of Icinga 2 Web. + * + * Icinga 2 Web - Head for multiple monitoring backends. + * Copyright (C) 2013 Icinga Development Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @copyright 2013 Icinga Development Team + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} +define(['components/app/container', 'jquery', 'logging', 'icinga/util/async', 'URIjs/URI', 'URIjs/URITemplate'], +function(Container, $, logger, async, URI) { + "use strict"; + + /** + * Master/Detail grid component handling history, link behaviour, selection (@TODO 3788) and updates of + * grids + * + * @param {HTMLElement} The outer element to apply the behaviour on + */ + return function(gridDomNode) { + + /** + * Reference to the outer container of this component + * + * @type {*|HTMLElement} + */ + gridDomNode = $(gridDomNode); + + /** + * A container component to use for updating URLs and content + * + * @type {Container} + */ + this.container = null; + + /** + * The node wrapping the table and pagination + * + * @type {jQuery} + */ + var contentNode; + + /** + * jQuery matcher result of the form components wrapping the controls + * + * @type {jQuery} + */ + var controlForms; + + /** + * Detect and select control forms for this table and return them + * + * Form controls are either all forms underneath the of the component, but not underneath the table + * or in a dom node explicitly tagged with the 'data-icinga-actiongrid-controls' attribute + * + * @param {jQuery|null} domContext The context to use as the root node for matching, if null + * the component node given in the constructor is used + * + * @returns {jQuery} A selector result with all forms modifying this grid + */ + var determineControlForms = function(domContext) { + domContext = domContext || gridDomNode; + var controls = $('[data-icinga-grid-controls]', domContext); + if (controls.length > 0) { + return $('form', controls); + } else { + return $('form', domContext).filter(function () { + return $(this).parentsUntil(domContext, 'table').length === 0; + }); + } + }; + + /** + * Detect and select the dom of all tables displaying content for this mainDetailGrid component + * + * The table can either explicitly tagged with the 'data-icinga-grid-content' attribute, if not every table + * underneath the components root dom will be used + * + * @param {jQuery|null} domContext The context to use as the root node for matching, if null + * the component node given in the constructor is used + * + * @returns {jQuery} A selector result with all tables displaying information in the + * grid + */ + var determineContentTable = function(domContext) { + domContext = domContext || gridDomNode; + var maindetail = $('[data-icinga-grid-content]', domContext); + if (maindetail.length > 0) { + return maindetail; + } else { + return $('table', domContext); + } + }; + + /** + * Register the row links of tables using the first link found in the table (no matter if visible or not) + * + * Row level links can only be realized via JavaScript, so every row should provide additional links for + * Users that don't have javascript enabled + * + * @param {jQuery|null} domContext The rootnode to use for selecting rows or null to use contentNode + */ + this.registerTableLinks = function(domContext) { + domContext = domContext || contentNode; + this.container.disableClickHandler(); + + $('tbody tr', domContext).on('click', function(ev) { + var targetEl = ev.target || ev.toElement || ev.relatedTarget; + + if (targetEl.nodeName.toLowerCase() === "a") { + // test if the URL is on the current server, if not open it directly + if(Container.isExternalLink($(targetEl).attr('href'))) { + return true; + } + } + + Container.getDetailContainer().replaceDomFromUrl($('a', this).attr('href')); + if (!ev.ctrlKey && !ev.metaKey) { + $('tr', $(this).parent()).removeClass('active'); + } + + $(this).addClass('active'); + return false; + }); + }; + + + /** + * Register submit handler for the form controls (sorting, filtering, etc). Reloading happens in the + * current container + */ + this.registerControls = function() { + controlForms.on('submit', (function(evt) { + // append the form's parameters to the current container href + var form = $(evt.currentTarget); + var url = URI(this.container.getContainerHref()); + url.search(URI.parseQuery(form.serialize())); + // reload this container + this.container.replaceDomFromUrl(url); + + evt.preventDefault(); + evt.stopPropagation(); + return false; + }).bind(this)); + }; + + /** + * Synchronize the current selection with the url displayed in the detail box + */ + this.syncSelectionWithDetail = function() { + $('tr', contentNode).removeClass('active'); + var selection = $('a[href="' + Container.getDetailContainer().getContainerHref() + '"]', contentNode). + parentsUntil('table', 'tr'); + selection.addClass('active'); + }; + + /** + * Register listener for history changes in the detail box + */ + this.registerHistoryChanges = function() { + Container.getDetailContainer().registerOnUpdate(this.syncSelectionWithDetail.bind(this)); + }; + + /** + * Create this component, setup listeners and behaviour + */ + this.construct = function(target) { + this.container = new Container(target); + logger.debug("Registering table events for ", this.container.containerType); + controlForms = determineControlForms(); + contentNode = determineContentTable(); + + this.registerControls(); + this.registerTableLinks(); + this.registerHistoryChanges(); + }; + + this.construct(gridDomNode); + }; + +}); diff --git a/public/js/icinga/icinga.js b/public/js/icinga/icinga.js index e538a7e93..4f460af15 100755 --- a/public/js/icinga/icinga.js +++ b/public/js/icinga/icinga.js @@ -30,8 +30,10 @@ define([ 'jquery', 'logging', 'icinga/util/async', - 'icinga/componentLoader' -], function ($, log, async,components) { + 'icinga/componentLoader', + 'components/app/container', + 'URIjs/URI' +], function ($, log, async, components, Container, URI) { 'use strict'; /** @@ -39,15 +41,79 @@ define([ */ var Icinga = function() { + var ignoreHistoryChanges = false; + var initialize = function () { components.load(); + registerGenericHistoryHandler(); log.debug("Initialization finished"); + }; - $(document).ready(initialize.bind(this)); + /** + * Register handler for handling the history state generically + * + */ + var registerGenericHistoryHandler = function() { + var lastUrl = URI(window.location.href); + History.Adapter.bind(window, 'popstate', function() { + if (ignoreHistoryChanges) { + return; + } + log.debug(URI(History.getState().url).relativeTo(lastUrl).href()); + var relativeURLPart = URI(History.getState().url).relativeTo(lastUrl).href(); + if (relativeURLPart !== "" && relativeURLPart[0] === '?' ) { + // same controller, different parameters, so only update the container + Container.getMainContainer().syncWithCurrentUrl(); + Container.getDetailContainer().syncWithCurrentUrl(); + } else { + gotoUrl(History.getState().url); + } + lastUrl = URI(window.location.href); + }); + }; + + var gotoUrl = function(href) { + if (typeof document.body.pending !== 'undefined') { + document.body.pending.abort(); + } + $.ajax({ + success: function(domNodes) { + ignoreHistoryChanges = true; + History.pushState({}, document.title, href); + $('body').empty().append($(domNodes)); + ignoreHistoryChanges = false; + components.load(); + }, + url: href + }); + + return false; + }; + + if (Modernizr.history) { + $(document.body).on('click', '#icinganavigation, #icingatopbar', function(ev) { + var targetEl = ev.target || ev.toElement || ev.relatedTarget; + if (targetEl.tagName.toLowerCase() !== 'a') { + return true; + } + + var href = $(targetEl).attr('href'); + if (Container.isExternalLink(href)) { + return true; + } + ev.preventDefault(); + ev.stopPropagation(); + gotoUrl(href); + return false; + }); + } + $(document).ready(initialize.bind(this)); return { - components: components + + components: components, + gotoUrl: gotoUrl }; }; return new Icinga(); diff --git a/public/js/icinga/util/async.js b/public/js/icinga/util/async.js deleted file mode 100644 index de2a46ef5..000000000 --- a/public/js/icinga/util/async.js +++ /dev/null @@ -1,199 +0,0 @@ -// {{{ICINGA_LICENSE_HEADER}}} -/** - * This file is part of Icinga 2 Web. - * - * Icinga 2 Web - Head for multiple monitoring backends. - * Copyright (C) 2013 Icinga Development Team - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * @copyright 2013 Icinga Development Team - * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 - * @author Icinga Development Team - */ -// {{{ICINGA_LICENSE_HEADER}}} -/*global Icinga:false define:false require:false base_url:false console:false */ - -(function() { - "use strict"; - var asyncMgrInstance = null; - - define(['logging','jquery'],function(log,$,containerMgr) { - - var headerListeners = {}; - - var pending = { - - }; - - var encodeForURL = function(param) { - return encodeURIComponent(param); - }; - - var getCurrentGETParameters = function() { - var currentGET = window.location.search.substring(1).split("&"); - var params = {}; - if(currentGET.length > 0) { - $.each(currentGET, function(idx, elem) { - var keyVal = elem.split("="); - params[keyVal[0]] = encodeForURL(keyVal[1]); - }); - } - return params; - } -; - var pushGet = function(param, value, url) { - url = url || (window.location.origin+window.location.pathname); - var params = getCurrentGETParameters(); - params[param] = encodeForURL(value); - var search = "?"; - for (var name in params) { - if (name === "" || typeof params[name] == "undefined") { - continue; - } - if (search != "?") - search += "&"; - search += name+"="+params[name]; - } - - return url+search+"#"+window.location.hash; - }; - - var getDOMForDestination = function(destination) { - var target = destination; - if (typeof destination === "string") { - target = containerMgr.getContainer(destination)[0]; - } else if(typeof destination.context !== "undefined") { - target = destination[0]; - } - return target; - }; - - var applyHeaderListeners = function(headers) { - for (var header in headerListeners) { - if (headers.getResponseHeader(header) === null) { - // see if the browser/server converts headers to lowercase - if (headers.getResponseHeader(header.toLowerCase()) === null) { - continue; - } - header = header.toLowerCase(); - } - var value = headers.getResponseHeader(header); - var listeners = headerListeners[header]; - for (var i=0;i")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) \ No newline at end of file +(function(e,t){"use strict";var n=e.History=e.History||{};if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) diff --git a/test/js/test/icinga/asyncTest.js b/test/js/test/icinga/asyncTest.js deleted file mode 100644 index 2f4bb5378..000000000 --- a/test/js/test/icinga/asyncTest.js +++ /dev/null @@ -1,34 +0,0 @@ - -// {{LICENSE_HEADER}} -// {{LICENSE_HEADER}} -var should = require("should"); -var rjsmock = require("requiremock.js"); -var asyncMock = require("asyncmock.js"); - -GLOBAL.document = $('body'); - - -describe('The async module', function() { - it("Allows to react on specific headers", function(done) { - rjsmock.purgeDependencies(); - rjsmock.registerDependencies({ - 'icinga/container' : { - updateContainer : function() {}, - createPopupContainer: function() {} - } - }); - - requireNew("icinga/util/async.js"); - var async = rjsmock.getDefine(); - var headerValue = null; - asyncMock.setNextAsyncResult(async, "result", false, { - 'X-Dont-Care' : 'Ignore-me', - 'X-Test-Header' : 'Testme123' - }); - async.registerHeaderListener("X-Test-Header", function(value, header) { - should.equal("Testme123", value); - done(); - },this); - var test = async.createRequest(); - }); -}); diff --git a/test/js/testlib/requiremock.js b/test/js/testlib/requiremock.js index ccebda91f..86221e13a 100644 --- a/test/js/testlib/requiremock.js +++ b/test/js/testlib/requiremock.js @@ -47,7 +47,7 @@ var logger = { /** * Mock for the 'define' function of requireJS, behaves exactly the same -* except that it looks up the dependencies in the list provided by registerDepencies() +* except that it looks up the dependencies in the list provided by registerDependencies() * A module that hasn't been defined with a name can be fetched with getDefined() (without parameter) * **/ @@ -88,6 +88,7 @@ var defineMock = function() { **/ function initRequireMethods() { GLOBAL.$ = require('jquery'); + GLOBAL.jQuery = GLOBAL.$; GLOBAL.requirejs = requireJsMock; GLOBAL.define = defineMock; registeredDependencies = {