commit 6d6b73dcffa2b6b173c520889f7e9bba768e8c19
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Fri Oct 26 13:59:34 2018 +0200

    reworked Loader to SpinLoader ES6 module, spin.js dependency is manager by npm

diff --git a/sources/src/main/webapp/css/main.css b/sources/src/main/webapp/css/main.css
index 7da2c95..9537ee1 100644
--- a/sources/src/main/webapp/css/main.css
+++ b/sources/src/main/webapp/css/main.css
@@ -231,7 +231,7 @@ form input[type="checkbox"] {
  * LOADER
  */
 
-.loader {
+.spinloader {
     position: absolute;
     top: 0;
     bottom: 0;
@@ -240,21 +240,6 @@ form input[type="checkbox"] {
     background: rgba(0,0,0,0.5);
 }
 
-.loader-content {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    width: 200px;
-    height: 200px;
-    margin: auto;
-}
-
-.loader-content p {
-	display: none;
-}
-
 /**
  * GRAPH
  */
diff --git a/sources/src/main/webapp/js/components/spinLoader.js b/sources/src/main/webapp/js/components/spinLoader.js
new file mode 100644
index 0000000..bf8d612
--- /dev/null
+++ b/sources/src/main/webapp/js/components/spinLoader.js
@@ -0,0 +1,62 @@
+import {Spinner} from '../../node_modules/spin.js/spin.js';
+
+/**
+ * Loader animation displayed when running some expensive operation.
+ */
+class SpinLoader {
+	/**
+	 * @constructor
+	 * @param {object} options 
+	 */
+	constructor(options) {
+		let defaultOptions = {
+			lines: 16, // The number of lines to draw
+			length: 21, // The length of each line
+			width: 8, // The line thickness
+			radius: 40, // The radius of the inner circle
+			rotate: 0, // The rotation offset
+			color: '#000', // #rgb or #rrggbb
+			speed: 1.2, // Rounds per second
+			trail: 58, // Afterglow percentage
+			shadow: true, // Whether to render a shadow
+			hwaccel: true, // Whether to use hardware acceleration
+			className: 'loader-spinner', // The CSS class to assign to the spinner
+			zIndex: 2, // The z-index (defaults to 2000000000)
+		};
+
+		this._options = Object.assign(defaultOptions, options);
+	}
+
+	/**
+	 * @returns {HTMLElement} 
+	 */
+	render() {
+		this._rootElement = DOM.h('div', {
+			class: 'spinloader',
+			id: 'loader',
+			hidden: 'hidden',
+		});
+
+		return this._rootElement;
+	}
+
+	/**
+	 * Enables loader.
+	 * @public
+	 */
+	enable() {
+		this._rootElement.removeAttribute('hidden');
+		this._spinner = new Spinner(this._options).spin(this._rootElement);
+	}
+
+	/**
+	 * Disables loader.
+	 * @public
+	 */
+	disable() {
+		this._rootElement.setAttribute('hidden', 'hidden');
+		this._spinner.stop();
+	}
+}
+
+export default SpinLoader;
diff --git a/sources/src/main/webapp/js/libs/spin.js b/sources/src/main/webapp/js/libs/spin.js
deleted file mode 100644
index 5c6b572..0000000
--- a/sources/src/main/webapp/js/libs/spin.js
+++ /dev/null
@@ -1,307 +0,0 @@
-
-//github.com/fgnass/spin.js#v1.2.5
-(function(window, document, undefined) {
-	
-	/**
-	* Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de]
-	* Licensed under the MIT license
-	*/
-	
-	var prefixes = ['webkit', 'Moz', 'ms', 'O']; /* Vendor prefixes */
-	var animations = {}; /* Animation rules keyed by their name */
-	var useCssAnimations;
-	
-	/**
-	* Utility function to create elements. If no tag name is given,
-	* a DIV is created. Optionally properties can be passed.
-	*/
-	function createEl(tag, prop) {
-		var el = document.createElement(tag || 'div');
-		var n;
-		
-		for(n in prop) {
-			el[n] = prop[n];
-		}
-		return el;
-	}
-	
-	/**
-	* Appends children and returns the parent.
-	*/
-	function ins(parent /* child1, child2, ...*/) {
-		for (var i=1, n=arguments.length; i<n; i++) {
-			parent.appendChild(arguments[i]);
-		}
-		return parent;
-	}
-	
-	/**
-	* Insert a new stylesheet to hold the @keyframe or VML rules.
-	*/
-	var sheet = function() {
-		var el = createEl('style');
-		ins(document.getElementsByTagName('head')[0], el);
-		return el.sheet || el.styleSheet;
-	}();
-	
-	/**
-	* Creates an opacity keyframe animation rule and returns its name.
-	* Since most mobile Webkits have timing issues with animation-delay,
-	* we create separate rules for each line/segment.
-	*/
-	function addAnimation(alpha, trail, i, lines) {
-		var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-');
-		var start = 0.01 + i/lines*100;
-		var z = Math.max(1-(1-alpha)/trail*(100-start) , alpha);
-		var prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase();
-		var pre = prefix && '-'+prefix+'-' || '';
-		
-		if (!animations[name]) {
-			sheet.insertRule(
-			'@' + pre + 'keyframes ' + name + '{' +
-				'0%{opacity:'+z+'}' +
-				start + '%{opacity:'+ alpha + '}' +
-				(start+0.01) + '%{opacity:1}' +
-				(start+trail)%100 + '%{opacity:'+ alpha + '}' +
-				'100%{opacity:'+ z + '}' +
-			'}', 0);
-			animations[name] = 1;
-		}
-		return name;
-	}
-	
-	/**
-	* Tries various vendor prefixes and returns the first supported property.
-	**/
-	function vendor(el, prop) {
-		var s = el.style;
-		var pp;
-		var i;
-		
-		if(s[prop] !== undefined) return prop;
-		prop = prop.charAt(0).toUpperCase() + prop.slice(1);
-		for(i=0; i<prefixes.length; i++) {
-			pp = prefixes[i]+prop;
-			if(s[pp] !== undefined) return pp;
-		}
-	}
-	
-	/**
-	* Sets multiple style properties at once.
-	*/
-	function css(el, prop) {
-		for (var n in prop) {
-			el.style[vendor(el, n)||n] = prop[n];
-		}
-		return el;
-	}
-	
-	/**
-	* Fills in default values.
-	*/
-	function merge(obj) {
-		for (var i=1; i < arguments.length; i++) {
-			var def = arguments[i];
-			for (var n in def) {
-				if (obj[n] === undefined) obj[n] = def[n];
-			}
-		}
-		return obj;
-	}
-	
-	/**
-	* Returns the absolute page-offset of the given element.
-	*/
-	function pos(el) {
-		var o = {x:el.offsetLeft, y:el.offsetTop};
-		while((el = el.offsetParent)) {
-			o.x+=el.offsetLeft;
-			o.y+=el.offsetTop;
-		}
-		return o;
-	}
-	
-	var defaults = {
-		lines: 12,            // The number of lines to draw
-		length: 7,            // The length of each line
-		width: 5,             // The line thickness
-		radius: 10,           // The radius of the inner circle
-		rotate: 0,            // rotation offset
-		color: '#000',        // #rgb or #rrggbb
-		speed: 1,             // Rounds per second
-		trail: 100,           // Afterglow percentage
-		opacity: 1/4,         // Opacity of the lines
-		fps: 20,              // Frames per second when using setTimeout()
-		zIndex: 2e9,          // Use a high z-index by default
-		className: 'spinner', // CSS class to assign to the element
-		top: 'auto',          // center vertically
-		left: 'auto'          // center horizontally
-	};
-	
-	/** The constructor */
-	var Spinner = function Spinner(o) {
-		if (!this.spin) return new Spinner(o);
-		this.opts = merge(o || {}, Spinner.defaults, defaults);
-	};
-	
-	Spinner.defaults = {};
-	merge(Spinner.prototype, {
-		spin: function(target) {
-			this.stop();
-			var self = this;
-			var o = self.opts;
-			var el = self.el = css(createEl(0, {className: o.className}), {position: 'relative', zIndex: o.zIndex});
-			var mid = o.radius+o.length+o.width;
-			var ep; // element position
-			var tp; // target position
-			
-			if (target) {
-				target.insertBefore(el, target.firstChild||null);
-				tp = pos(target);
-				ep = pos(el);
-				css(el, {
-					left: (o.left == 'auto' ? tp.x-ep.x + (target.offsetWidth >> 1) : o.left+mid) + 'px',
-					top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : o.top+mid)  + 'px'
-				});
-			}
-			
-			el.setAttribute('aria-role', 'progressbar');
-			self.lines(el, self.opts);
-			
-			if (!useCssAnimations) {
-				// No CSS animation support, use setTimeout() instead
-				var i = 0;
-				var fps = o.fps;
-				var f = fps/o.speed;
-				var ostep = (1-o.opacity)/(f*o.trail / 100);
-				var astep = f/o.lines;
-				
-				!function anim() {
-					i++;
-					for (var s=o.lines; s; s--) {
-						var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity);
-						self.opacity(el, o.lines-s, alpha, o);
-					}
-					self.timeout = self.el && setTimeout(anim, ~~(1000/fps));
-				}();
-			}
-			return self;
-		},
-		stop: function() {
-			var el = this.el;
-			if (el) {
-				clearTimeout(this.timeout);
-				if (el.parentNode) el.parentNode.removeChild(el);
-				this.el = undefined;
-			}
-			return this;
-		},
-		lines: function(el, o) {
-			var i = 0;
-			var seg;
-			
-			function fill(color, shadow) {
-				return css(createEl(), {
-					position: 'absolute',
-					width: (o.length+o.width) + 'px',
-					height: o.width + 'px',
-					background: color,
-					boxShadow: shadow,
-					transformOrigin: 'left',
-					transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)',
-					borderRadius: (o.width>>1) + 'px'
-				});
-			}
-			for (; i < o.lines; i++) {
-				seg = css(createEl(), {
-					position: 'absolute',
-					top: 1+~(o.width/2) + 'px',
-					transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
-					opacity: o.opacity,
-					animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite'
-				});
-				if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}));
-				ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)')));
-			}
-			return el;
-		},
-		opacity: function(el, i, val) {
-			if (i < el.childNodes.length) el.childNodes[i].style.opacity = val;
-		}
-	});
-	
-	/////////////////////////////////////////////////////////////////////////
-	// VML rendering for IE
-	/////////////////////////////////////////////////////////////////////////
-	
-	/**
-	* Check and init VML support
-	*/
-	!function() {
-		
-		function vml(tag, attr) {
-			return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr);
-		}
-		
-		var s = css(createEl('group'), {behavior: 'url(#default#VML)'});
-		
-		if (!vendor(s, 'transform') && s.adj) {
-			
-			// VML support detected. Insert CSS rule ...
-			sheet.addRule('.spin-vml', 'behavior:url(#default#VML)');
-			
-			Spinner.prototype.lines = function(el, o) {
-				var r = o.length+o.width;
-				var s = 2*r;
-				
-				function grp() {
-					return css(vml('group', {coordsize: s +' '+s, coordorigin: -r +' '+-r}), {width: s, height: s});
-				}
-				
-				var margin = -(o.width+o.length)*2+'px';
-				var g = css(grp(), {position: 'absolute', top: margin, left: margin});
-				
-				var i;
-				
-				function seg(i, dx, filter) {
-					ins(g,
-					ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
-					ins(css(vml('roundrect', {arcsize: 1}), {
-					width: r,
-					height: o.width,
-					left: o.radius,
-					top: -o.width>>1,
-					filter: filter
-				}),
-				vml('fill', {color: o.color, opacity: o.opacity}),
-				vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
-				)
-				)
-				);
-			}
-			
-			if (o.shadow) {
-				for (i = 1; i <= o.lines; i++) {
-					seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)');
-				}
-			}
-			for (i = 1; i <= o.lines; i++) seg(i);
-			return ins(el, g);
-		};
-		Spinner.prototype.opacity = function(el, i, val, o) {
-			var c = el.firstChild;
-			o = o.shadow && o.lines || 0;
-			if (c && i+o < c.childNodes.length) {
-				c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild;
-				if (c) c.opacity = val;
-			}
-		};
-	}
-	else {
-		useCssAnimations = vendor(s, 'animation');
-	}
-}();
-
-window.Spinner = Spinner;
-
-})(window, document);
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/services/loader.js b/sources/src/main/webapp/js/services/loader.js
deleted file mode 100644
index 01b9f8d..0000000
--- a/sources/src/main/webapp/js/services/loader.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Loader animation displayed when running some expensive operation.
- * 
- * @constructor
- * @param {object} options 
- */
-function Loader(options) {
-	var defaultOptions = {
-		lines: 16, // The number of lines to draw
-		length: 21, // The length of each line
-		width: 8, // The line thickness
-		radius: 40, // The radius of the inner circle
-		rotate: 0, // The rotation offset
-		color: '#000', // #rgb or #rrggbb
-		speed: 1.2, // Rounds per second
-		trail: 58, // Afterglow percentage
-		shadow: true, // Whether to render a shadow
-		hwaccel: true, // Whether to use hardware acceleration
-		className: 'loader-spinner', // The CSS class to assign to the spinner
-		zIndex: 2, // The z-index (defaults to 2000000000)
-	};
-
-	var opts = $.extend(defaultOptions, options);
-
-	/**
-	 * Spin jQuery plugin.
-	 */
-	$.fn.spin = function(opts) {
-		this.each(function() {
-			var $this = $(this),
-			data = $this.data();
-			
-			if (data.spinner) {
-				data.spinner.stop();
-				delete data.spinner;
-			}
-
-			if (opts !== false) {
-				data.spinner = new Spinner($.extend({
-					color: $this.css('color')
-				}, opts)).spin(this);
-			}
-		});
-		return this;
-	};
-
-	/**
-	* Enables loader.
-	*/
-	this.enable = function() {
-		$('#loader').show();
-		$('#spinLoader').spin(opts);
-	};
-
-	/**
-	* Disables loader.
-	*/
-	this.disable = function() {
-		$('#loader').hide();
-		$('#spinLoader').spin(false);
-	};
-}
diff --git a/sources/src/main/webapp/js/showGraphApp.js b/sources/src/main/webapp/js/showGraphApp.js
index a3ab3b2..7288236 100644
--- a/sources/src/main/webapp/js/showGraphApp.js
+++ b/sources/src/main/webapp/js/showGraphApp.js
@@ -1,4 +1,5 @@
 import App from './app.js';
+import SpinLoader from './components/spinLoader.js';
 
 /**
  * Application running on the ShowGraph page.
@@ -13,7 +14,6 @@ class ShowGraphApp extends App {
 		this.constants = new Constants;
 		this.graphLoader = new GraphLoader;
 		this.graphExporter = new GraphExporter;
-		this.loader = new Loader;
 		this.zoom = new Zoom(0.8);
 		this.markSymbol = new MarkSymbol;
 
@@ -100,6 +100,7 @@ class ShowGraphApp extends App {
 		this.viewportComponent = new Viewport;
 		this.sidebarComponent = new Sidebar;
 		this.modalWindowComponent = new SaveDiagramModalWindow;
+		this.spinLoaderComponent = new SpinLoader;
 
 		const appElement = document.getElementById('app');
 		appElement.appendChild(this.headerComponent.render());
@@ -112,6 +113,7 @@ class ShowGraphApp extends App {
 			this.sidebarComponent.render(),
 		]));
 		appElement.appendChild(this.modalWindowComponent.render());
+		appElement.appendChild(this.spinLoaderComponent.render());
 
 		this.sidebarComponent.minimapComponent.viewportSize = this.viewportComponent.getSize();
 
@@ -144,7 +146,7 @@ class ShowGraphApp extends App {
 	 * @param {string} diagramId Identifier of the diagram to be loaded.
 	 */
 	async _loadGraphData(diagramId) {
-		this.loader.enable();
+		this.spinLoaderComponent.enable();
 
 		let loadGraphDataPromise;
 
@@ -168,7 +170,7 @@ class ShowGraphApp extends App {
 			// construct graph
 			this.graphLoader.run(graphData);
 
-			this.loader.disable();
+			this.spinLoaderComponent.disable();
 
 		} catch (error) {
 			if (error instanceof HttpError) {
diff --git a/sources/src/main/webapp/package-lock.json b/sources/src/main/webapp/package-lock.json
index c8df728..ba947f8 100644
--- a/sources/src/main/webapp/package-lock.json
+++ b/sources/src/main/webapp/package-lock.json
@@ -123,6 +123,11 @@
       "resolved": "https://registry.npmjs.org/save-svg-as-png/-/save-svg-as-png-1.4.6.tgz",
       "integrity": "sha512-CeN0wtmKW02/qfTtXFfiCCniyGExRzPKpp2xOdY8cbLEeIQPWKhSCd1HUl/dGH7WDu2As6Wecs14rlV2Jaf9lg=="
     },
+    "spin.js": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/spin.js/-/spin.js-4.0.0.tgz",
+      "integrity": "sha512-uJN9Q4/I4I7PRZAp1c5reoY89thAfvFB5thbJncAwkPjPhqCfUrCooJc8GMN8q8WKFGIMy+JMf477RW7deCInA=="
+    },
     "strip-json-comments": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
diff --git a/sources/src/main/webapp/package.json b/sources/src/main/webapp/package.json
index b413fcc..caa55d6 100644
--- a/sources/src/main/webapp/package.json
+++ b/sources/src/main/webapp/package.json
@@ -19,6 +19,7 @@
   "dependencies": {
     "html-tags": "^2.0.0",
     "save-svg-as-png": "^1.4.6",
+    "spin.js": "^4.0.0",
     "svg-tags": "^1.0.0"
   }
 }
diff --git a/sources/src/main/webapp/showGraph.jsp b/sources/src/main/webapp/showGraph.jsp
index b17ed92..b9c8c61 100644
--- a/sources/src/main/webapp/showGraph.jsp
+++ b/sources/src/main/webapp/showGraph.jsp
@@ -12,12 +12,12 @@
 		<meta charset="utf-8">
 
 		<link rel="stylesheet" href="css/main.css">
+		<link rel="stylesheet" href="node_modules/spin.js/spin.css">
 
 		<script id="htmlTags" type="application/json"><%@ include file="node_modules/html-tags/html-tags.json" %></script>
 		<script id="svgTags" type="application/json"><%@ include file="node_modules/svg-tags/lib/svg-tags.json" %></script>
 
 		<script src="js/libs/jquery-3.3.1.min.js"></script>
-		<script src="js/libs/spin.js"></script>
 		<script src="node_modules/save-svg-as-png/lib/saveSvgAsPng.js"></script>
 
 		<script src="js/components/generic/modalWindow.js"></script>
@@ -38,6 +38,7 @@
 		<script src="js/components/sidebar.js"></script>
 		<script src="js/components/sidebarExcludedNodeList.js"></script>
 		<script src="js/components/sidebarUnconnectedNodeList.js"></script>
+		<script src="js/components/spinLoader.js" type="module"></script>
 		<script src="js/components/statusBar.js"></script>
 		<script src="js/components/vertex.js"></script>
 		<script src="js/components/vertexContextMenuList.js"></script>
@@ -56,7 +57,6 @@
 		<script src="js/services/forceDirected.js"></script>
 		<script src="js/services/graphLoader.js"></script>
 		<script src="js/services/graphExporter.js"></script>
-		<script src="js/services/loader.js"></script>
 		<script src="js/services/markSymbol.js"></script>
 		<script src="js/services/zoom.js"></script>
 
@@ -77,11 +77,6 @@
 	<body class="${isLoggedIn ? 'loggedIn' : 'loggedOut'}">
 		<div id="app"></div>
 
-		<div class="loader" id="loader">
-			<div class="loader-content" id="spinLoader">
-				<p>Loading graph...</p>
-			</div>
-		</div>
 		<script type="module">
 			import ShowGraphApp from './js/showGraphApp.js';
 
@@ -91,7 +86,7 @@
 				app.run('${param.diagramId}');
 
 				// user is logged in
-				if ('${user}' !== '') {
+				if ('${isLoggedIn}' === 'true') {
 					document.dispatchEvent(new LoggedInEvent({
 						id: '${user.id}',
 						username: '${user.username}',
