diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php index f906dbb05..eacbb8aa5 100644 --- a/library/Icinga/Web/JavaScript.php +++ b/library/Icinga/Web/JavaScript.php @@ -13,6 +13,7 @@ class JavaScript 'js/helpers.js', 'js/icinga.js', 'js/icinga/logger.js', + 'js/icinga/storage.js', 'js/icinga/utils.js', 'js/icinga/ui.js', 'js/icinga/timer.js', diff --git a/public/js/icinga/storage.js b/public/js/icinga/storage.js new file mode 100644 index 000000000..0334234dc --- /dev/null +++ b/public/js/icinga/storage.js @@ -0,0 +1,359 @@ +/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */ + +(function (Icinga, $) { + + 'use strict'; + + /** + * Icinga.Storage + * + * localStorage access + */ + Icinga.Storage = function() { + + /** + * Namespace separator being used + * + * @type {string} + */ + this.keySeparator = '.'; + + /** + * Callbacks for storage events on particular keys + * + * @type {{function}} + */ + this.subscribers = {}; + + this.setup(); + }; + + Icinga.Storage.prototype = { + + /** + * Prefix the given key + * + * Base implementation, noop. + * + * @param {string} key + * @returns {string} + */ + prefixKey: function(key) { + return key; + }, + + /** + * Store the given key-value pair + * + * @param {string} key + * @param {*} value + * + * @returns {void} + */ + set: function(key, value) { + localStorage.setItem(this.prefixKey(key), JSON.stringify(value)); + }, + + /** + * Get value for the given key + * + * @param {string} key + * + * @returns {*} + */ + get: function(key) { + return JSON.parse(localStorage.getItem(this.prefixKey(key))); + }, + + /** + * Remove given key from storage + * + * @param {string} key + * + * @returns {void} + */ + remove: function(key) { + localStorage.removeItem(this.prefixKey(key)); + }, + + /** + * Subscribe with a callback for events on a particular key + * + * @param {string} key + * @param {function} callback + * + * @returns {void} + */ + subscribe: function(key, callback) { + this.subscribers[this.prefixKey(key)] = callback; + }, + + /** + * Pass storage events to subscribers + * + * @param {StorageEvent} event + */ + onStorage: function(event) { + if (typeof this.subscribers[event.key] !== 'undefined') { + this.subscribers[event.key](JSON.parse(event.oldValue), JSON.parse(event.newValue)); + } + }, + + /** + * Add the event listener + * + * @returns {void} + */ + setup: function() { + window.addEventListener('storage', this.onStorage.bind(this)); + }, + + /** + * Remove the event listener + * + * @returns {void} + */ + destroy: function() { + window.removeEventListener('storage', this.onStorage.bind(this)); + } + }; + + /** + * Icinga.BehaviorStorage + * + * @param {string} behaviorName + * @constructor + */ + Icinga.BehaviorStorage = function(behaviorName) { + + /** + * The behavior's name + * + * @type {string} + */ + this.behaviorName = behaviorName; + + Icinga.Storage.call(this); + }; + Icinga.BehaviorStorage.prototype = Object.create(Icinga.Storage.prototype); + + /** + * Prefix the given key with `behavior..` + * + * @param {string} key + * + * @returns {string} + */ + Icinga.BehaviorStorage.prototype.prefixKey = function(key) { + return 'behavior' + this.keySeparator + this.behaviorName + this.keySeparator + key; + }; + + /** + * Icinga.Storage.StorageAwareSet + * + * Emits events `StorageAwareSetDelete` and `StorageAwareSetAdd` in case an update occurs in the storage. + * + * @param {Array} values + * @constructor + */ + Icinga.Storage.StorageAwareSet = function(values) { + + /** + * Storage object + * + * @type {Icinga.Storage} + */ + this.storage = undefined; + + /** + * Storage key + * + * @type {string} + */ + this.key = undefined; + + /** + * The internal (real) set + * + * @type {Set<*>} + */ + this.data = new Set(); + + // items is not passed directly because IE11 doesn't support constructor arguments + if (typeof values !== 'undefined' && !! values && values.length) { + values.forEach(function(value) { + this.data.add(value); + }, this); + } + }; + + /** + * Create a new StorageAwareSet for the given storage and key + * + * @param {Icinga.Storage} storage + * @param {string} key + * + * @returns {Icinga.Storage.StorageAwareSet} + */ + Icinga.Storage.StorageAwareSet.withStorage = function(storage, key) { + return (new Icinga.Storage.StorageAwareSet(storage.get(key)).setStorage(storage, key)); + }; + + Icinga.Storage.StorageAwareSet.prototype = { + + /** + * Bind this set to the given storage and key + * + * @param {Icinga.Storage} storage + * @param {string} key + * + * @returns {this} + */ + setStorage: function(storage, key) { + this.storage = storage; + this.key = key; + + storage.subscribe(key, this.onChange.bind(this)); + return this; + }, + + /** + * Return a boolean indicating this set got a storage + * + * @returns {boolean} + */ + hasStorage: function() { + return typeof this.storage !== 'undefined' && typeof this.key !== 'undefined'; + }, + + /** + * Update the set + * + * @param {Array} oldValue + * @param {Array} newValue + */ + onChange: function(oldValue, newValue) { + // Check for deletions first + this.values().forEach(function (value) { + if (newValue.indexOf(value) < 0) { + this.data.delete(value); + $(window).trigger('StorageAwareSetDelete', value); + } + }, this); + + // Now check for new entries + newValue.forEach(function(value) { + if (! this.data.has(value)) { + this.data.add(value); + $(window).trigger('StorageAwareSetAdd', value); + } + }, this); + }, + + /** + * Return the number of (unique) elements in the set + * + * @returns {number} + */ + get size() { + return this.data.size; + }, + + /** + * Append the given value to the end of the set + * + * @param value + * + * @returns {this} + */ + add: function(value) { + this.data.add(value); + + if (this.hasStorage()) { + this.storage.set(this.key, this.values()); + } + + return this; + }, + + /** + * Remove all elements from the set + * + * @returns {void} + */ + clear: function() { + if (this.hasStorage()) { + this.storage.remove(this.key); + } + + return this.data.clear(); + }, + + /** + * Remove the given value from the set + * + * @param value + * + * @returns {boolean} + */ + delete: function(value) { + var retVal = this.data.delete(value); + + if (this.hasStorage()) { + this.storage.set(this.key, this.values()); + } + + return retVal; + }, + + /** + * Returns an iterable of [v,v] pairs for every value v in the set. + * + * @returns {IterableIterator<[*, *]>} + */ + entries: function() { + return this.data.entries(); + }, + + /** + * Execute a provided function once for each value in the Set object, in insertion order. + * + * @param callback + * + * @returns {void} + */ + forEach: function(callback) { + return this.data.forEach(callback); + }, + + /** + * Return a boolean indicating whether an element with the specified value exists in a Set object or not. + * + * @param value + * + * @returns {boolean} + */ + has: function(value) { + return this.data.has(value); + }, + + /** + * Returns an array of values in the set. + * + * @returns {Array} + */ + values: function() { + var list = []; + + if (this.size > 0) { + // .forEach() is used because IE11 doesn't support .values() + this.forEach(function(value) { + list.push(value); + }); + } + + return list; + } + }; + +}(Icinga, jQuery));