/*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"; var Icinga; /** * Enumeration of possible container types * * @type {{GENERIC: string, MAIN: string, DETAIL: string}} */ var CONTAINER_TYPES = { 'GENERIC' : 'generic', 'MAIN' : 'icingamain', 'DETAIL': 'icingadetail' }; /** * 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) { /** * 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; } this.containerDom.attr('data-icinga-href', this.getContainerHref()); }; /** * Returns the window without the hostname * * @returns {string} path with query, search and hash */ 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(baseUrl) { // main has the url without the icingadetail part var href = URI(getWindowLocationWithoutHost(baseUrl)); 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(baseUrl) { var location = new URI(baseUrl); 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(baseUrl) { baseUrl = baseUrl || getWindowLocationWithoutHost(); switch (this.containerType) { case CONTAINER_TYPES.MAIN: return getMainContainerHrefFromUrl(baseUrl); case CONTAINER_TYPES.DETAIL: return getDetailContainerHrefFromUrl(baseUrl); case CONTAINER_TYPES.GENERIC: if (this.containerDom.attr('data-icinga-href')) { return URI(this.containerDom.attr('data-icinga-href')); } else { return URI(baseUrl).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, baseUrl) { var detail = getDetailContainerHrefFromUrl(baseUrl); 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, baseUrl) { var location = new URI(baseUrl); 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 * * @return {String} url The new Url of the application (main and detail) */ this.updateContainerHref = function(url, baseUrl) { baseUrl = baseUrl || getWindowLocationWithoutHost(); if (typeof url === "string") { url = URI(url); } var containerUrl, windowUrl; switch (this.containerType) { case CONTAINER_TYPES.MAIN: windowUrl = setMainContainerHref(url, baseUrl); containerUrl = windowUrl.clone().removeQuery('detail'); break; case CONTAINER_TYPES.DETAIL: windowUrl = setDetailContainerHref(url, baseUrl); containerUrl = url; break; case CONTAINER_TYPES.GENERIC: containerUrl = url; windowUrl = baseUrl; break; } if (containerUrl) { this.containerDom.attr('data-icinga-href', containerUrl); } else { this.containerDom.removeAttr('data-icinga-href'); } return windowUrl.href(); }; /** * 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) { Icinga.replaceBodyFromUrl(this.updateContainerHref(url)); }; /** * 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) { this.containerDom.empty().append(domNodes); 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, #icingadetail', 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 { // detail links render to main by default; Icinga.replaceBodyFromUrl( mainContainer.updateContainerHref(URI($(targetEl).attr('href')).href()) ); ev.preventDefault(); ev.stopPropagation(); return false; } }); } /** * Injects the icinga object into the Container class * * This can't be done via requirejs as we would end up in circular references * * @param {Icinga} icingaObj The Icinga object to use for reloading */ Container.setIcinga = function(icingaObj) { Icinga = icingaObj; }; return Container; });