From 3e02d5aff85babc3ffbfcf52313f2108e313aa23 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:46:43 +0200 Subject: Adding upstream version 2.12.1. Signed-off-by: Daniel Baumann --- public/js/icinga/history.js | 338 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 public/js/icinga/history.js (limited to 'public/js/icinga/history.js') diff --git a/public/js/icinga/history.js b/public/js/icinga/history.js new file mode 100644 index 0000000..150be7c --- /dev/null +++ b/public/js/icinga/history.js @@ -0,0 +1,338 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +/** + * Icinga.History + * + * This is where we care about the browser History API + */ +(function (Icinga, $) { + + 'use strict'; + + Icinga.History = function (icinga) { + + /** + * YES, we need Icinga + */ + this.icinga = icinga; + + /** + * Our base url + */ + this.baseUrl = icinga.config.baseUrl; + + /** + * Initial URL at load time + */ + this.initialUrl = location.href; + + /** + * Whether the History API is enabled + */ + this.enabled = false; + }; + + Icinga.History.prototype = { + + /** + * Icinga will call our initialize() function once it's ready + */ + initialize: function () { + + // History API will not be enabled without browser support, no fallback + if ('undefined' !== typeof window.history && + typeof window.history.pushState === 'function' + ) { + this.enabled = true; + this.icinga.logger.debug('History API enabled'); + this.applyLocationBar(true); + $(window).on('popstate', { self: this }, this.onHistoryChange); + } + + }, + + /** + * Get the current state (url and title) as object + * + * @returns {object} + */ + getCurrentState: function () { + if (! this.enabled) { + return null; + } + + var title = null; + var url = null; + + // We only store URLs of containers sitting directly under #main: + $('#main > .container').each(function (idx, container) { + var $container = $(container), + cUrl = $container.data('icingaUrl'), + cTitle = $container.data('icingaTitle'); + + // TODO: I'd prefer to have the rightmost URL first + if ('undefined' !== typeof cUrl) { + // TODO: solve this on server side cUrl = icinga.utils.removeUrlParams(cUrl, blacklist); + if (! url) { + url = cUrl; + } else { + url = url + '#!' + cUrl; + } + } + + if (typeof cTitle !== 'undefined') { + title = cTitle; // Only uses the rightmost title + } + }); + + return { + title: title, + url: url, + }; + }, + + /** + * Detect active URLs and push combined URL to history + * + * TODO: How should we handle POST requests? e.g. search VS login + */ + pushCurrentState: function () { + // No history API, no action + if (! this.enabled) { + return; + } + + var state = this.getCurrentState(); + + // Did we find any URL? Then push it! + if (state.url) { + this.icinga.logger.debug('Pushing current state to history'); + this.push(state.url); + } + if (state.title) { + this.icinga.ui.setTitle(state.title); + } + }, + + /** + * Replace the current history entry with the current state + */ + replaceCurrentState: function () { + if (! this.enabled) { + return; + } + + var state = this.getCurrentState(); + + if (state.url) { + this.icinga.logger.debug('Replacing current history state'); + this.lastPushUrl = state.url; + window.history.replaceState( + this.getBehaviorState(), + null, + state.url + ); + } + }, + + /** + * Push the given url as the new history state, unless the history is disabled + * + * @param {string} url The full url path, including anchor + */ + pushUrl: function (url) { + // No history API, no action + if (!this.enabled) { + return; + } + this.push(url); + }, + + /** + * Execute the history state, preserving the current state of behaviors + * + * Used internally by the history and should not be called externally, instead use {@link pushUrl}. + * + * @param {string} url + */ + push: function (url) { + url = url.replace(/[\?&]?_(render|reload)=[a-z0-9]+/g, ''); + if (this.lastPushUrl === url) { + this.icinga.logger.debug( + 'Ignoring history state push for url ' + url + ' as it\' currently on top of the stack' + ); + return; + } + this.lastPushUrl = url; + window.history.pushState( + this.getBehaviorState(), + null, + url + ); + }, + + /** + * Fetch the current state of all JS behaviors that need history support + * + * @return {Object} A key-value map, mapping behavior names to state + */ + getBehaviorState: function () { + var data = {}; + $.each(this.icinga.behaviors, function (i, behavior) { + if (behavior.onPushState instanceof Function) { + data[i] = behavior.onPushState(); + } + }); + return data; + }, + + /** + * Event handler for pop events + * + * TODO: Fix active selection, multiple cols + */ + onHistoryChange: function (event) { + + var _this = event.data.self, + icinga = _this.icinga; + + icinga.logger.debug('Got a history change'); + + // We might find browsers showing strange behaviour, this log could help + if (event.originalEvent.state === null) { + icinga.logger.debug('No more history steps available'); + } else { + icinga.logger.debug('History state', event.originalEvent.state); + } + + // keep the last pushed url in sync with history changes + _this.lastPushUrl = location.href; + + _this.applyLocationBar(); + + // notify behaviors of the state change + $.each(this.icinga.behaviors, function (i, behavior) { + if (behavior.onPopState instanceof Function && history.state) { + behavior.onPopState(location.href, history.state[i]); + } + }); + }, + + /** + * Update the application containers to match the current url + * + * Read the pane url from the current URL and load the corresponding panes into containers to + * match the current history state. + * + * @param {Boolean} onload Set to true when the main pane should not be updated, defaults to false + */ + applyLocationBar: function (onload = false) { + let col2State = this.getCol2State(); + + if (onload && document.querySelector('#layout > #login')) { + // The user landed on the login + let redirectInput = document.querySelector('#login form input[name=redirect]'); + redirectInput.value = redirectInput.value + col2State; + return; + } + + let col1 = document.getElementById('col1'), + col2 = document.getElementById('col2'), + col1Url = document.location.pathname + document.location.search; + + let col2Url; + if (col2State && col2State.match(/^#!/)) { + col2Url = col2State.split(/#!/)[1]; + } + + // This uses jQuery only because of its internal data attribute cache -.- + let currentCol1Url = $(col1).data('icingaUrl'), + currentCol2Url = $(col2).data('icingaUrl'); + + let loadCol1 = ! onload, + loadCol2 = !! col2Url; + if (currentCol2Url === col1Url) { + // User navigated forward + this.icinga.ui.moveToLeft(); + loadCol1 = false; + } else if (currentCol1Url === col2Url) { + // User navigated back + this.icinga.ui.moveToRight(); + loadCol2 = false; + } + + if (loadCol1 && currentCol1Url !== col1Url) { + let anchor = this.getPaneAnchor(0); + if (anchor) { + col1Url += '#' + anchor; + } + + this.icinga.loader.loadUrl(col1Url, $(col1)).addToHistory = false; + } + + if (loadCol2 && currentCol2Url !== col2Url) { + let col2Req = this.icinga.loader.loadUrl(col2Url, $(col2)); + col2Req.addToHistory = false; + col2Req.scripted = onload; + + this.icinga.ui.layout2col(); + } else if (! loadCol2 && ! col2Url) { + this.icinga.ui.layout1col(); + } + }, + + /** + * Get the state of the selected pane + * + * @param col {int} The column index 0 or 1 + * + * @returns {String} The string representing the state + */ + getPaneAnchor: function (col) { + if (col !== 1 && col !== 0) { + throw 'Trying to get anchor for non-existing column: ' + col; + } + var panes = document.location.toString().split('#!')[col]; + return panes && panes.split('#')[1] || ''; + }, + + /** + * Get the side pane state after (and including) the #! + * + * @returns {string} The pane url + */ + getCol2State: function () { + var hash = document.location.hash; + if (hash) { + if (hash.match(/^#[^!]/)) { + var hashs = hash.split('#'); + hashs.shift(); + hashs.shift(); + hash = '#' + hashs.join('#'); + } + } + return hash || ''; + }, + + /** + * Return the main pane state fragment + * + * @returns {string} The main url including anchors, without #! + */ + getCol1State: function () { + var anchor = this.getPaneAnchor(0); + var hash = window.location.pathname + window.location.search + + (anchor.length ? ('#' + anchor) : ''); + return hash || ''; + }, + + /** + * Cleanup + */ + destroy: function () { + $(window).off('popstate', this.onHistoryChange); + this.icinga = null; + } + }; + +}(Icinga, jQuery)); -- cgit v1.2.3