403Webshell
Server IP : 89.248.107.232  /  Your IP : 216.73.217.70
Web Server : Apache
System : Linux host2.kasilh.com 5.14.0-687.36.1.el9_8.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Aug 7 05:40:49 EDT 2026 x86_64
User : seg ( 10005)
PHP Version : 7.4.33
Disable Function : opcache_get_status
MySQL : OFF  |  cURL : ON  |  WGET : OFF  |  Perl : OFF  |  Python : OFF  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /var/www/vhosts/seg-sa.es/grupo-seg.com/wp-content/plugins/mapplic-geo/admin/js/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/vhosts/seg-sa.es/grupo-seg.com/wp-content/plugins/mapplic-geo/admin/js/mgeo-admin.js
(function() {
	"use strict";

	function MapplicGeoAdmin(element) {
		let self = this;
		let obj = null;

		var __ = function(t, n) { return t; }
		if (typeof wp !== 'undefined') __ = wp.i18n.__;
	
		this.init = function() {
			const s = this;

			this.el = document.getElementById('mgeo-admin');
			if (!this.el) return false;

			this.el.classList.add('mgeo-admin');

			element.addEventListener('mapready', function(e) {
				obj = e.detail;

				s.help = new Help().init();

				// bar
				const bar = s.blankSection('mgeo-bar');

				const save = document.createElement('button');
				save.classList.add('mgeo-admin-save', 'mgeo-admin-button', 'mgeo-admin-button-primary');
				save.innerHTML = __('Save Changes', 'mgeo');
				bar.appendChild(save);

				// raw
				const raw = document.createElement('button');
				raw.classList.add('mgeo-admin-button');
				raw.innerHTML = __('Raw', 'mgeo');
				bar.appendChild(raw);
				raw.addEventListener('click', function(e) {
					e.preventDefault();

					const popup = document.createElement('div');
					popup.classList.add('mgeo-raw-popup');

					const h = document.createElement('h1');
					h.innerHTML = __('Raw data', 'mgeo');
					popup.appendChild(h);

					const textarea = document.createElement('textarea');
					textarea.setAttribute('rows', '20');
					textarea.classList.add('mgeo-raw-text');
					textarea.spellcheck = false;
					textarea.value = JSON.stringify(self.fetchData(), null, 2);
					popup.appendChild(textarea);

					const update = document.createElement('button');
					update.classList.add('mgeo-admin-button', 'mgeo-admin-button-primary');
					update.innerHTML = __('Update', 'mgeo');
					popup.appendChild(update);
					update.addEventListener('click', function(e) {
						e.preventDefault();
						let data = false;
						try { data = JSON.parse(textarea.value); }
						catch(e) { alert(__('Invalid JSON!', 'mgeo')); }

						self.save(data);
					});
					
					Fancybox.show([{ src: popup, type: 'html' }], { mainClass: 'mgeo-admin mgeo-admin-popup mgeo-raw-popup' });
				});

				const title = document.createElement('h3');
				title.innerHTML = obj.el.dataset.shortcode || obj.o.data;
				bar.appendChild(title);

				// layouts
				const layouts = document.createElement('div');
				layouts.classList.add('mgeo-admin-layouts');

				const desktop = document.createElement('button');
				desktop.classList.add('mgeo-admin-desktop');
				layouts.appendChild(desktop);

				const tablet = document.createElement('button');
				tablet.classList.add('mgeo-admin-tablet');
				layouts.appendChild(tablet);

				const mobile = document.createElement('button');
				mobile.classList.add('mgeo-admin-mobile');
				layouts.appendChild(mobile);
				
				// panels
				const locations = s.blankSection('mgeo-admin-locations-panel');
				s.locations = new Locations().init(obj.dir.l);
				locations.appendChild(s.locations.el);

				const others = s.blankSection('mgeo-admin-others');
				s.settings = new Settings().init(obj.o);
				s.settings.el.classList.add('two-thirds');
				others.appendChild(s.settings.el);

				const column = document.createElement('div');
				column.classList.add('third', 'last');
				others.appendChild(column);

				s.styles = new Styles().init(obj.styles.s);
				column.appendChild(s.styles.el);
				s.layers = new Layers().init(obj.container.layers);
				column.prepend(s.layers.el);
				s.groups = new Groups().init(obj.dir.g);
				column.appendChild(s.groups.el);

				// colorpicker init
				Coloris({
					el: '.mgeo-colorpicker',
					theme: 'large',
					format: 'mixed'
				});

				save.addEventListener('click', function(e) {
					e.preventDefault();
					self.save();
				});

				const form = document.getElementById('post');
				if (form) document.getElementById('post').addEventListener('submit', function(e) { self.save(); });
			});
		}

		this.save = function(data = null) {
			if (data === null) data = self.fetchData();
			if (!data) return false;

			data = JSON.stringify(data);

			if (obj.wp) {
				document.querySelector('#mgeo-mapdata').value = data;
				document.getElementById('post').submit();
			}
			else {
				fetch('save.php', {
					method: 'POST',
					headers: {'Content-Type': 'application/json'},
					body: data
				}).then(res => {
					window.history.forward(1);
					window.location.reload(true);
				});
			}
		}

		this.blankSection = function(id) {
			const section = document.createElement('section');
			if (id) section.id = id;
			this.el.appendChild(section);

			const inner = document.createElement('div');
			inner.classList.add('mgeo-inner');
			section.appendChild(inner);

			return inner;
		}

		this.fetchLocations = function() {
			let locations = [];

			this.el.querySelectorAll('#mgeo-location-table tbody.mgeo-editable > tr').forEach(function(el, i) {
				const location = {};

				el.querySelectorAll('td.mgeo-cell-field').forEach(function(td, i) {
					if (td.innerHTML) location[td.getAttribute('data-field')] = td.innerHTML;
				});

				locations.push(location);
			});

			return locations;
		}

		this.fetchLayer = function(el) {
			const layer = {};

			layer.id = self.validateId(el.querySelector('.mgeo-layer-id'));
			if (!layer.id) return false;
			
			self.setField(layer, 'title', el.querySelector('.mgeo-layer-title').value);
			self.setField(layer, 'type', el.querySelector('.mgeo-layer-type').value);
			self.setField(layer, 'source', el.querySelector('.mgeo-layer-source')?.value);
			self.setField(layer, 'style', el.querySelector('.mgeo-layer-style').value);
			self.setField(layer, 'extent', el.querySelector('.mgeo-layer-extent').value);
			self.setField(layer, 'minZoom', el.querySelector('.mgeo-layer-minzoom').value);
			self.setField(layer, 'maxZoom', el.querySelector('.mgeo-layer-maxzoom').value);
			self.setField(layer, 'opacity', el.querySelector('.mgeo-layer-opacity').value);
			self.setField(layer, 'selectable', el.querySelector('.mgeo-layer-selectable').checked);
			self.setField(layer, 'isolated', el.querySelector('.mgeo-layer-isolated').checked);
			self.setField(layer, 'hide', el.querySelector('.mgeo-layer-hide').checked);
			self.setField(layer, 'disabled', el.querySelector('.mgeo-layer-disabled').checked);

			return layer;
		}

		this.fetchData = function() {
			const data = {};

			let halt = false;
			this.el.querySelectorAll('.error').forEach(function(el) { el.classList.remove('error'); });

			// settings
			data.settings = {};
			data.settings.data = obj.o.data;
			this.el.querySelectorAll('#mgeo-admin-settings input, #mgeo-admin-settings select, #mgeo-admin-settings textarea').forEach(function(el, i) {
				if (el.value) {
					const setting = el.className.replace('mgeo-setting-', '');
					
					if (el.type == 'checkbox') data.settings[setting] = el.checked;
					else if (el.type == 'number') data.settings[setting] = parseFloat(el.value);
					else data.settings[setting] = el.value;
				}
			});

			// styles
			data.styles = [];
			this.el.querySelectorAll('#mgeo-admin-styles .mgeo-admin-list > .mgeo-admin-list-item').forEach(function(el, i) {
				const style = {
					class: self.validateId(el.querySelector('.mgeo-style-class')),
					base: {}
				};
				if (!style.class) halt = true;

				const base = el.querySelector('.mgeo-style-base').value;
				if (base) style.base = { fill: base };

				const hover = el.querySelector('.mgeo-style-hover').value;
				if (hover) style.hover = { fill: hover };

				const active = el.querySelector('.mgeo-style-active').value;
				if (active) style.active = { fill: active };

				const stroke = el.querySelector('.mgeo-style-stroke').value;
				if (stroke) style.base.stroke = stroke;

				const strokeWidth = el.querySelector('.mgeo-style-stroke-width').value;
				if (strokeWidth) style.base.strokeWidth = strokeWidth;

				const textColor = el.querySelector('.mgeo-style-text-color').value;
				if (textColor) style.base.textColor = textColor;

				data.styles.push(style);
			});

			// groups
			data.groups = [];
			this.el.querySelectorAll('#mgeo-admin-groups .mgeo-admin-list > .mgeo-admin-list-item').forEach(function(el) {
				const group = {};

				group.id = self.validateId(el.querySelector('.mgeo-group-id'));
				if (!group.id) halt = true;

				self.setField(group, 'title', el.querySelector('.mgeo-group-title').value);
				self.setField(group, 'color', el.querySelector('.mgeo-group-color').value);
				self.setField(group, 'thumbnail', el.querySelector('.mgeo-group-thumbnail').value);
				self.setField(group, 'style', el.querySelector('.mgeo-group-style').value);
				self.setField(group, 'hide', el.querySelector('.mgeo-group-hide').checked);

				data.groups.push(group);
			});

			// layers
			data.layers = [];
			this.el.querySelectorAll('#mgeo-admin-layers .mgeo-admin-list > .mgeo-admin-list-item').forEach(function(el) {

				if (el.classList.contains('mgeo-layer-group')) {
					const lg = {};

					lg.id = self.validateId(el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-id'));
					if (!lg.id) halt = true;

					self.setField(lg, 'title', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-title').value);
					self.setField(lg, 'extent', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-extent')?.value);
					self.setField(lg, 'minZoom', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-minzoom').value);
					self.setField(lg, 'maxZoom', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-maxzoom').value);
					self.setField(lg, 'opacity', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-opacity').value);
					self.setField(lg, 'selectable', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-selectable').checked);
					self.setField(lg, 'isolated', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-isolated').checked);
					self.setField(lg, 'hide', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-hide').checked);
					self.setField(lg, 'disabled', el.querySelector(':scope > .mgeo-admin-list-item-body .mgeo-layer-disabled').checked);

					lg.layers = [];

					el.querySelectorAll('.mgeo-gl-list > .mgeo-admin-list-item').forEach(function(sub) {
						let layer = self.fetchLayer(sub);
						if (!layer) halt = true;
						lg.layers.push(layer);
					});

					data.layers.push(lg);
				}
				else {
					let layer = self.fetchLayer(el);
					if (!layer) halt = true;
					data.layers.push(layer);
				}
			});

			// unsaved rows
			this.el.querySelectorAll('#mgeo-location-table tr.mgeo-edit').forEach(function(el) {
				self.locations.saveRow(el);
			});

			// locations
			data.locations = this.fetchLocations();

			if (halt) return false;
			else return data;
		}

		this.setField = function(object, attribute, value) {
			if (value) object[attribute] = value;
		}

		this.newText = function(type, content) {
			const el = document.createElement(type);
			el.innerHTML = content;
			return el;
		}

		this.validateId = function(field) {
			const value = field.value.trim();
			if (/^[A-Za-z]+[\w\-\:\.]*$/.test(value)) return value;
			else {
				field.focus();
				field.closest('.mgeo-admin-list-item')?.classList.add('opened');

				if (field.parentNode.tagName === 'LABEL') field.parentNode.classList.add('error');
				else field.classList.add('error');

				return false;
			}
		}

		// fields
		this.newTextInput = function(name, value, title = false, help = false) {
			const input = document.createElement('input');
			input.setAttribute('type', 'text');
			input.setAttribute('spellcheck', 'false');
			if (name) input.setAttribute('class', name);
			if (value) input.setAttribute('value', value);

			if (title) {
				const label = document.createElement('label');
				label.innerHTML = __(title, 'mgeo');
				if (help) self.help.addHelp(label, help);
				label.appendChild(input);
				return label;
			}
			else return input;
		}

		this.newNumberInput = function(name, value, title = false, help = false) {
			const input = document.createElement('input');
			input.setAttribute('type', 'number');
			if (name) input.setAttribute('class', name);
			if (value) input.setAttribute('value', value);

			if (title) {
				const label = document.createElement('label');
				label.innerHTML = __(title, 'mgeo');
				if (help) self.help.addHelp(label, help);
				label.appendChild(input);
				return label;
			}
			else return input;
		}

		this.newOpacityInput = function(name, value, title = false) {
			const input = document.createElement('input');
			input.setAttribute('type', 'number');
			input.setAttribute('min', '0');
			input.setAttribute('max', '1');
			input.setAttribute('step', '.01');
			if (name) input.setAttribute('class', name);
			if (value) input.setAttribute('value', value);

			if (title) {
				const label = document.createElement('label');
				label.innerHTML = __(title, 'mgeo');
				label.appendChild(input);
				return label;
			}
			else return input;
		}

		this.newActionInput = function(name, type, value, title = false, help = false) {
			const input = document.createElement('input');
			input.setAttribute('type', 'text');
			input.setAttribute('spellcheck', 'false');
			if (name) input.setAttribute('class', name);
			if (value) input.setAttribute('value', value);

			const button = document.createElement('button');
			button.classList.add('mgeo-input-action');

			let el = document.createElement('label');
			if (title) el.innerHTML = __(title, 'mgeo');

			el.appendChild(input);
			el.appendChild(button);

			if (type == 'extent') {
				button.appendChild(document.createElement('i')).classList.add('mgeo-icon-extent');
				button.addEventListener('click', function(e) {
					e.preventDefault();
					obj.container.editExtent(input);
					obj.el.scrollIntoView();
				});
			}
			else if (type == 'file') {
				button.appendChild(document.createElement('i')).classList.add('mgeo-icon-upload');
				button.addEventListener('click', function(e) {
					e.preventDefault();

					const popup = wp.media({
						title: 'Select or Upload File',
						button: { text: 'Select' },
						multiple: false
					});

					popup.open();

					popup.on('select', function() {
						let attachment = popup.state().get('selection').first().toJSON();
						input.value = attachment.url;
					});
				});
			}
			return el;
		}

		this.newCheckbox = function(name, value, title = false, help = false) {
			const input = document.createElement('input');
			input.setAttribute('type', 'checkbox');
			if (name) input.setAttribute('class', name);
			if (value) input.checked = true;

			if (title) {
				const label = document.createElement('label');
				label.classList.add('mgeo-admin-checkbox');
				label.innerHTML = __(title, 'mgeo');
				if (help) self.help.addHelp(label, help);
				label.appendChild(input);
				return label;
			}
			else return input;
		}

		this.newSelect = function(name = false, options, value, title = false, help = false, multi = false, translate = true) {
			const select = document.createElement('select');
			if (name) select.classList.add(name);
			if (multi) select.setAttribute('multiple', '');

			for (const o in options) {
				const option = document.createElement('option');
				option.setAttribute('value', o);
				option.innerHTML = typeof options[o] === 'string' ? options[o] : options[o].title;
				if (translate) option.innerHTML = __(option.innerHTML, 'mgeo');
				if (o == value) option.setAttribute('selected', '');
				select.appendChild(option);
			}

			if (title) {
				const label = document.createElement('label');
				label.innerHTML = __(title, 'mgeo');
				if (help) self.help.addHelp(label, help);
				label.appendChild(select);
				return label;
			}
			else return select;
		}

		this.newTypeInput = function(value = '', title = false) {
			const types = Object.keys(obj.container.markers);
			types.unshift('', 'text', 'circle', 'square', 'thumb', 'dot-l', 'dot-r');

			const container = document.createElement('ul');
			container.classList.add('mgeo-admin-type');

			types.forEach(function(type) {
				const item = document.createElement('li');
				if (type) item.style.backgroundImage = 'url(' + obj.localize.pdir + '/admin/img/markers/' + type + '.png)';
				if (value == type) item.classList.add('mgeo-selected');
				const name = document.createElement('small');
				name.innerHTML = type;
				item.appendChild(name);
				item.addEventListener('click', function() {
					container.querySelector('li.mgeo-selected')?.classList.remove('mgeo-selected');
					this.classList.add('mgeo-selected');
				});

				container.appendChild(item);
			});

			if (title) {
				const label = document.createElement('label');
				label.innerHTML = title;
				label.appendChild(container);
				return label;
			}
			else return container;
		}

		// help
		function Help() {
			const s = this;

			this.text = {
				'id': 'Unique ID (required)<br> - starts with a letter<br> - no space or special characters allowed',
				'class': 'Unique class name (required)<br> - starts with a letter<br> - no space or special characters allowed',
				'title': 'Title of the location displayed in popups and directory.',
				'desc': 'HTML enabled description text displayed in popups.',
				'coord': 'Comma separeted <b>latitude</b> and <b>longitude</b> of location<br> - viewport center is the initial value for new locations<br> - if left blank, position will be estimated',
				'zoom': 'Focus zoom level of location<br> - if left blank, the global <b>Max zoom</b> will be used<br> - if the value is negative, zoom will be disabled',
				'type': 'Type of the marker. Use the first, blank option for no marker.',
				'label': 'Text added to the marker if supported',
				'fill': 'Hex color used for marker and popup styling. It will be overwritten if a <b>style</b> is defined.',
				'style': 'Predefined style used for complex marker and popup styling. Overwrites standard <b>fill</b> attribute.',
				'action': 'Action to perform when a location is focused.',
				'image': 'Image displayed in popups. Always use web optimized images for the best performance.',
				'thumbnail': 'Thumbnail displayed in directory or as marker if "thumb" <b>type</b> is used.',
				'group': 'Group(s) the location belongs to.',
				'link': 'URL used for redirects or popup buttons.',
				'about': 'Short description of the location displayed in popups and directory.',
				'layer': 'ID of the layer the location belongs to. If left blank and there is no matching vector feature available, the global markers layer will be used.',
				'sample': 'ID of the location to use as <b>sample</b>.',
				'hide': 'Hide location from directory only.',
				'disabled': 'Temporarily disable item.',
				'global-sample': 'Every location will inherit attributes from this global sample.',
				'mousewheel': 'Enable zoom with mouse wheel.',
				'mousewheel-shift': 'Require SHIFT key for mouse wheel zooming.',
				'hover-tooltip': 'Enable hover tooltip.',
				'hover-tooltip-follow': 'Hover tooltip will follow the cursor instead of being centered.',
				'deeplinking': 'Deeplinking generates custom URL for each location.',
				'bluronzoomout': 'Unfocus location when zoomed all the way out.',
				'resetzoomonblur': 'Zoom all the way out when a location popup is closed',
				'sidebar': 'Map control containing a location directory with optional search/filters and popup window, displayed on the left side of the map by default.',
				'search': 'Enable sidebar search and filters.',
				'right-sidebar': 'Right aligned sidebar.',
				'sort-by-title': 'Sort sidebar directory by location title.',
				'thumbnails': 'Enable location thumbnails in sidebar directory.',
				'sidebar-above': 'The sidebar appears above the map on portrait view.',
				'csv': 'External CSV file linked to the map. The first line is the header and each additional line defines a location.',
				'import-csv': 'Import all locations from the linked CSV file. Link to external CSV will be removed.',
				'export-csv': 'Export all internal locations in CSV format.',
				'constrain-zoom': 'Constrain zoom levels to whole numbers.',
				'custom-css': 'Customize your map with CSS styles.',
				'map-rotation': 'Enable map rotation and define where the control should appear.',
				'fullscreen': 'Enable fullscreen and define where the control should appear.',
				'zoom-buttons': 'Enable zoom buttons and define where the control should appear.',
				'reset-button': 'Enable reset button and define where the control should appear.',
				'level-selector': 'Enable level selector and define where the control should appear.',
				'attribution': 'Add attribution to the bottom right corner of the map. Note that <b>attributions are required</b> for most data sources!',
				'collapsed-attribution': 'Make the attribution collapsed by default.'
			}

			this.init = function() {
				this.el = document.createElement('small');
				this.el.classList.add('mgeo-help-tooltip');

				document.body.appendChild(this.el);

				return this;
			}

			this.addHelp = function(parent, key) {
				if (!s.text[key] || !parent) return false;

				const el = document.createElement('span');
				el.classList.add('mgeo-help');
				el.innerHTML = '?';
				el.addEventListener('mouseout', function() { s.el.style.visibility = 'hidden'; });
				el.addEventListener('mouseover', function() {
					const bcr = el.getBoundingClientRect();
					s.el.style.top = (bcr.top + window.scrollY) + 'px';
					s.el.style.left = (bcr.left + bcr.width) + 'px';
					s.el.innerHTML = __(s.text[key], 'mgeo');
					s.el.style.visibility = 'visible';
				});

				parent.appendChild(el);
			}
		}

		// settings
		function Settings() {
			const s = this;

			this.zoneOptions = {
				'' : '(Disabled)',
				'top-left': 'Top left',
				'top-right': 'Top right',
				'bottom-left': 'Bottom left',
				'bottom-right': 'Bottom right'
			};

			this.init = function(o) {
				this.el = document.createElement('div');
				this.el.id = 'mgeo-admin-settings';

				const title = document.createElement('h2');
				title.innerHTML = __('Settings', 'mgeo');
				this.el.appendChild(title);

				const icon = document.createElement('i');
				icon.classList.add('mgeo-admin-icon', 'mgeo-icon-settings');
				title.prepend(icon);

				// blocks
				const block1 = document.createElement('div');
				this.el.appendChild(block1);

				const block2 = document.createElement('div');
				this.el.appendChild(block2).classList.add('half');

				const block3 = document.createElement('div');
				this.el.appendChild(block3).classList.add('half', 'last');

				// general
				block1.appendChild(self.newActionInput('mgeo-setting-extent', 'extent', o.extent, 'Extent'));
				block1.appendChild(self.newNumberInput('mgeo-setting-portrait', o.portrait, 'Portrait breakpoint (pixels)')).classList.add('third');
				block1.appendChild(self.newNumberInput('mgeo-setting-minZoom', o.minZoom, 'Min zoom')).classList.add('third');
				block1.appendChild(self.newNumberInput('mgeo-setting-maxZoom', o.maxZoom, 'Max zoom')).classList.add('third', 'last');
				block1.appendChild(self.newTextInput('mgeo-setting-globalSample', o.globalSample, 'Global sample ID', 'global-sample')).classList.add('third');
				block1.appendChild(self.newTextInput('mgeo-setting-moreText', o.moreText, 'Custom "More" text')).classList.add('third');

				block1.appendChild(self.newText('h3', __('Container', 'mgeo')));
				block1.appendChild(self.newNumberInput('mgeo-setting-minHeight', o.minHeight, 'Min height (pixel)')).classList.add('third');
				block1.appendChild(self.newNumberInput('mgeo-setting-maxHeight', o.maxHeight, 'Max height (pixel)')).classList.add('third');

				// features
				block2.appendChild(self.newText('h3', __('Features', 'mgeo')));
				block2.appendChild(self.newCheckbox('mgeo-setting-mouseWheel', o.mouseWheel, 'Mousewheel zoom', 'mousewheel'));
				block2.appendChild(self.newCheckbox('mgeo-setting-mouseWheelShift', o.mouseWheelShift, 'Mousewheel requires shift key', 'mousewheel-shift'));
				block2.appendChild(self.newCheckbox('mgeo-setting-hoverTooltip', o.hoverTooltip, 'Hover tooltip', 'hover-tooltip'));
				block2.appendChild(self.newCheckbox('mgeo-setting-hoverTooltipFollow', o.hoverTooltipFollow, 'Hover tooltip follows cursor', 'hover-tooltip-follow'));
				block2.appendChild(self.newCheckbox('mgeo-setting-deeplinking', o.deeplinking, 'Deeplinking', 'deeplinking'));
				block2.appendChild(self.newCheckbox('mgeo-setting-blurOnZoomOut', o.blurOnZoomOut, 'Blur on zoom out', 'bluronzoomout'));
				block2.appendChild(self.newCheckbox('mgeo-setting-resetZoomOnBlur', o.resetZoomOnBlur, 'Reset zoom on blur', 'resetzoomonblur'));

				// sidebar
				block2.appendChild(self.newText('h3', __('Sidebar', 'mgeo')));
				block2.appendChild(self.newCheckbox('mgeo-setting-sidebar', o.sidebar, 'Sidebar', 'sidebar'));
				block2.appendChild(self.newCheckbox('mgeo-setting-search', o.search, 'Search and filters', 'search'));
				block2.appendChild(self.newCheckbox('mgeo-setting-rightSidebar', o.rightSidebar, 'Right aligned', 'right-sidebar'));
				block2.appendChild(self.newCheckbox('mgeo-setting-sortbytitle', o.sortbytitle, 'Sort by title', 'sort-by-title'));
				block2.appendChild(self.newCheckbox('mgeo-setting-thumbnail', o.thumbnail, 'Thumbnails', 'thumbnails'));
				block2.appendChild(self.newCheckbox('mgeo-setting-portraitSidebarAbove', o.portraitSidebarAbove, 'Sidebar above map on portrait mode', 'sidebar-above'));

				// csv
				block2.appendChild(self.newText('h3', __('CSV data', 'mgeo')));
				block2.appendChild(self.newActionInput('mgeo-setting-csv', 'file', o.csv, 'Link CSV source', 'csv'));

				const importlabel = document.createElement('label');
				importlabel.innerHTML = __('Import from CSV', 'mgeo');
				self.help.addHelp(importlabel, 'import-csv');
				block2.appendChild(importlabel);
				const importcsv = document.createElement('button');
				importcsv.innerHTML = __('Import', 'mgeo');
				if (!o.csv) importcsv.disabled = true;
				else {
					importcsv.addEventListener('click', function(e) {
						e.preventDefault();
						if (!obj.o.csv) alert(__('There is no CSV file linked, nothing to import.', 'mgeo'));
						else {
							const externals = self.locations.tbodyext.querySelectorAll('tr');
							if (confirm('Would you like to import ' + externals.length + ' locations?')) {
								s.el.querySelector('.mgeo-setting-csv').value = '';
								externals.forEach(function(el) { self.locations.tbody.append(el) } );
								self.save();
							}
						}
					});
				}
				importlabel.appendChild(importcsv);

				const exportlabel = document.createElement('label');
				exportlabel.innerHTML = __('Export to CSV file', 'mgeo');
				self.help.addHelp(exportlabel, 'export-csv');
				block2.appendChild(exportlabel);
				const exportcsv = document.createElement('button');
				exportcsv.innerHTML = __('Export', 'mgeo');
				exportcsv.addEventListener('click', function(e) {
					e.preventDefault();
					const l = self.fetchLocations();

					let columns = new Set();
					columns.add('id');
					for (const id in l) columns = new Set([...columns, ...Object.keys(l[id])]);

					let csv = Papa.unparse(Object.values(l), { columns: Array.from(columns) });
					window.open('data:text/csv;charset=utf-8,' + escape(csv));
				});
				exportlabel.appendChild(exportcsv);

				// advanced
				block2.appendChild(self.newText('h3', __('Advanced', 'mgeo')));
				block2.appendChild(self.newCheckbox('mgeo-setting-constrainResolution', o.constrainResolution, 'Constrain zoom resolution', 'constrain-zoom'));
				block2.appendChild(self.newCheckbox('mgeo-setting-improvedPerformance', o.improvedPerformance, 'Improve performance', 'improved-performance'));


				// custom css
				const textarea = document.createElement('textarea');
				textarea.setAttribute('rows', '6');
				textarea.setAttribute('class', 'mgeo-setting-customcss');
				textarea.setAttribute('spellcheck', 'false');
				if (o.customcss) textarea.innerHTML = o.customcss;
				const label = document.createElement('label');
				label.innerHTML = __('Custom CSS', 'mgeo');
				self.help.addHelp(label, 'custom-css');
				label.appendChild(textarea);
				block2.appendChild(label);

				// controls
				block3.appendChild(self.newText('h3', __('Controls', 'mgeo')));
				block3.appendChild(self.newSelect('mgeo-setting-rotation', this.zoneOptions, o.rotation, 'Map rotation', 'map-rotation'));
				block3.appendChild(self.newSelect('mgeo-setting-fullscreen', this.zoneOptions, o.fullscreen, 'Fullscreen', 'fullscreen'));
				block3.appendChild(self.newSelect('mgeo-setting-zoomButtons', this.zoneOptions, o.zoomButtons, 'Zoom buttons', 'zoom-buttons'));
				block3.appendChild(self.newSelect('mgeo-setting-resetButton', this.zoneOptions, o.resetButton, 'Reset button', 'reset-button'));
				block3.appendChild(self.newSelect('mgeo-setting-levelselector', this.zoneOptions, o.levelselector, 'Level selector', 'level-selector'));
				block3.appendChild(self.newCheckbox('mgeo-setting-attribution', o.attribution, 'Show attribution', 'attribution')).classList.add('space');
				block3.appendChild(self.newCheckbox('mgeo-setting-attributionCollapsible', o.attributionCollapsible, 'Collapsed attribution', 'collapsed-attribution'));

				// API keys
				block3.appendChild(self.newText('h3', __('API keys', 'mgeo')));
				block3.appendChild(self.newTextInput('mgeo-setting-maptilerkey', o.maptilerkey, 'MapTiler key'));
				block3.appendChild(self.newTextInput('mgeo-setting-mapboxtoken', o.mapboxtoken, 'Mapbox access token'));
				block3.appendChild(self.newTextInput('mgeo-setting-bingmapskey', o.bingmapskey, 'Bing Maps key'));

				return this;
			}
		}

		// styles
		function Styles() {
			const s = this;

			this.options = {'' : __('(None)', 'mgeo')};

			this.init = function(styles) {
				this.el = document.createElement('div');
				this.el.id = 'mgeo-admin-styles';

				const title = document.createElement('h2');
				title.innerHTML = 'Styles';
				this.el.appendChild(title);

				const icon = document.createElement('i');
				icon.classList.add('mgeo-admin-icon', 'mgeo-icon-styles');
				title.prepend(icon);

				const list = document.createElement('ul');
				list.classList.add('mgeo-admin-list');
				this.el.appendChild(list);

				const add = document.createElement('button'); // add button
				add.classList.add('mgeo-admin-new-style', 'mgeo-admin-button');
				add.innerHTML = 'Style';
				add.insertAdjacentHTML('afterbegin', '<i class="mgeo-icon-plus"></i>');
				add.addEventListener('click', function(e) {
					e.preventDefault();
					const empty = s.createStyleElement(false);
					list.prepend(empty);
					setTimeout(function() { empty.classList.add('opened'); }, 10);
					setTimeout(function() { list.querySelector('input').focus(); }, 200);
				});
				this.el.prepend(add);

				for (const id in styles) {
					this.options[styles[id].class] = styles[id].class;
					list.appendChild(this.createStyleElement(styles[id]));
				}

				// sortable
				Sortable.create(list, {
					animation: 200,
					handle: '#mgeo-admin-styles .mgeo-admin-list-item-handle',
					delay: 50,
					delayOnTouchOnly: true
				});

				return this;
			}

			this.createStyleElement = function(style) {
				const item = document.createElement('li');
				item.classList.add('mgeo-admin-list-item');

				const handle = document.createElement('div');
				handle.classList.add('mgeo-admin-list-item-handle');
				item.appendChild(handle);

				handle.addEventListener('click', function() { item.classList.toggle('opened'); });

				const title = document.createElement('span');
				if (style.class) title.innerHTML = style.class;
				else title.innerHTML = __('[New Style]', 'mgeo');
				handle.appendChild(title);
				handle.appendChild(document.createElement('i')).classList.add('mgeo-icon-arrow-down');

				const body = document.createElement('div');
				body.classList.add('mgeo-admin-list-item-body');
				item.appendChild(body);

				// fields
				body.appendChild(self.newTextInput('mgeo-style-class', style.class, 'Class', 'class')).classList.add('half');
				body.appendChild(self.newTextInput('mgeo-style-text-color mgeo-colorpicker', style?.base?.textColor, 'Text color')).classList.add('half','last');
				body.appendChild(self.newTextInput('mgeo-style-base mgeo-colorpicker', style?.base?.fill, 'Base color')).classList.add('third');
				body.appendChild(self.newTextInput('mgeo-style-hover mgeo-colorpicker', style?.hover?.fill, 'Hover color')).classList.add('third');
				body.appendChild(self.newTextInput('mgeo-style-active mgeo-colorpicker', style?.active?.fill, 'Active color')).classList.add('third', 'last');
				body.appendChild(self.newTextInput('mgeo-style-stroke mgeo-colorpicker', style?.base?.stroke, 'Stroke color')).classList.add('space', 'half');
				body.appendChild(self.newNumberInput('mgeo-style-stroke-width', style?.base?.strokeWidth, 'Stroke width')).classList.add('space', 'half', 'last');

				// remove button
				const remove = document.createElement('a');
				remove.classList.add('mgeo-remove-button', 'space');
				remove.innerHTML = 'Remove';
				body.appendChild(remove);
				remove.addEventListener('click', function() { if (!style.class || confirm('Are you sure you want to remove "' + style.class + '" style?')) item.parentNode.removeChild(item); });

				return item;
			}
		}

		// groups
		function Groups() {
			const s = this;

			this.options = {'' : __('(None)', 'mgeo')};

			this.init = function(groups) {
				this.el = document.createElement('div');
				this.el.id = 'mgeo-admin-groups';

				const title = document.createElement('h2');
				title.innerHTML = __('Groups', 'mgeo');
				this.el.appendChild(title);

				const icon = document.createElement('i');
				icon.classList.add('mgeo-admin-icon', 'mgeo-icon-groups');
				title.prepend(icon);

				const list = document.createElement('ul');
				list.classList.add('mgeo-admin-list');
				this.el.appendChild(list);

				const add = document.createElement('button'); // add button
				add.classList.add('mgeo-admin-new-group', 'mgeo-admin-button');
				add.innerHTML = __('Group', 'mgeo');
				add.insertAdjacentHTML('afterbegin', '<i class="mgeo-icon-plus"></i>');
				add.addEventListener('click', function(e) {
					e.preventDefault();
					const empty = s.createGroupElement(false);
					list.prepend(empty);
					setTimeout(function() { empty.classList.add('opened'); }, 10);
					setTimeout(function() { list.querySelector('input').focus(); }, 200);
				});
				this.el.prepend(add);

				for (const id in groups) {
					this.options[groups[id].id] = groups[id].title;
					list.appendChild(this.createGroupElement(groups[id]));
				}

				// sortable
				Sortable.create(list, {
					animation: 200,
					handle: '#mgeo-admin-groups .mgeo-admin-list-item-handle',
					delay: 50,
					delayOnTouchOnly: true
				});

				return this;
			}

			this.createGroupElement = function(group) {
				const item = document.createElement('li');
				item.classList.add('mgeo-admin-list-item');

				const handle = document.createElement('div');
				handle.classList.add('mgeo-admin-list-item-handle');
				item.appendChild(handle);

				handle.addEventListener('click', function() { item.classList.toggle('opened'); });

				const title = document.createElement('span');
				if (group.title) title.innerHTML = group.title;
				else title.innerHTML = __('[New Group]', 'mgeo');
				handle.appendChild(title);
				handle.appendChild(document.createElement('i')).classList.add('mgeo-icon-arrow-down');

				const body = document.createElement('div');
				body.classList.add('mgeo-admin-list-item-body');
				item.appendChild(body);

				// fields
				const idInput = self.newTextInput('mgeo-group-id', group.id, 'ID', 'id');
				idInput.classList.add('half');
				if (group) idInput.querySelector('.mgeo-group-id').setAttribute('disabled', '');
				body.appendChild(idInput);

				const titleInput = self.newTextInput('mgeo-group-title', group.title, 'Title');
				titleInput.classList.add('half', 'last');
				body.appendChild(titleInput);

				const colorInput = self.newTextInput('mgeo-group-color mgeo-colorpicker', group.color, 'Color');
				colorInput.classList.add('half');
				body.appendChild(colorInput);

				const thumbnailInput = self.newActionInput('mgeo-group-thumbnail', 'file', group.thumbnail, 'Thumbnail');
				thumbnailInput.classList.add('half', 'last');
				body.appendChild(thumbnailInput);

				body.appendChild(self.newSelect('mgeo-group-style', self.styles.options, group.style, 'Style', false, false, false)).classList.add('half');

				const hideCheckbox = self.newCheckbox('mgeo-group-hide', group.hide, 'Hide from directory');
				hideCheckbox.classList.add('single', 'space');
				body.appendChild(hideCheckbox);

				// remove button
				const remove = document.createElement('a');
				remove.classList.add('mgeo-remove-button');
				remove.innerHTML = __('Remove', 'mgeo');
				body.appendChild(remove);
				remove.addEventListener('click', function() { if (!group.id || confirm('Are you sure you want to remove "' + group.title + '" group?')) item.parentNode.removeChild(item); });

				return item;
			}
		}

		// layers
		function Layers() {
			const s = this;
			
			this.options = {'' : __('(Global)', 'mgeo')};

			this.type = {
				'osm': {
					'title': 'OpenStreetMap',
					'source': false
				},
				'markers': {
					'title': 'Markers',
					'source': false
				},
				'builtin': {
					'title': 'Built-in vector',
					'source': {
						'world/world.json': 'World Countries',
						'world/continents.json': 'Continents',
						'world/africa.json': 'Africa',
						'world/asia.json': 'Asia',
						'world/europe.json': 'Europe',
						'world/north-america.json': 'North America',
						'world/south-america.json': 'South America',
						'AL.json': 'Albania',
						'AR.json': 'Argentina',
						'AM.json': 'Armenia',
						'AU.json': 'Australia',
						'AT.json': 'Austria',
						'AZ.json': 'Azerbaijan',
						'BY.json': 'Belarus',
						'BE.json': 'Belgium',
						'BA.json': 'Bosnia and Herzegovina',
						'BR.json': 'Brazil',
						'BG.json': 'Bulgaria',
						'CA.json': 'Canada',
						'CL.json': 'Chile',
						'CN.json': 'China',
						'CO.json': 'Colombia',
						'CR.json': 'Costa Rica',
						'HR.json': 'Croatia',
						'CU.json': 'Cuba',
						'CY.json': 'Cyprus',
						'CZ.json': 'Czech Republic',
						'DK.json': 'Denmark',
						'EG.json': 'Egypt',
						'EE.json': 'Estonia',
						'FI.json': 'Finland',
						'FR.json': 'France',
						'GE.json': 'Georgia',
						'DE.json': 'Germany',
						'GR.json': 'Greece',
						'HU.json': 'Hungary',
						'IS.json': 'Iceland',
						'IN.json': 'India',
						'ID.json': 'Indonesia',
						'IR.json': 'Iran',
						'IQ.json': 'Iraq',
						'IE.json': 'Ireland',
						'IL.json': 'Israel',
						'IT.json': 'Italy',
						'JP.json': 'Japan',
						'KE.json': 'Kenya',
						'KR.json': 'Korea',
						'LV.json': 'Latvia',
						'LT.json': 'Lithuania',
						'LU.json': 'Luxembourg',
						'MK.json': 'Macedonia',
						'MY.json': 'Malaysia',
						'MX.json': 'Mexico',
						'MN.json': 'Mongolia',
						'ME.json': 'Montenegro',
						'MA.json': 'Morocco',
						'NL.json': 'Netherlands',
						'NZ.json': 'New Zealand',
						'NO.json': 'Norway',
						'PK.json': 'Pakistan',
						'PA.json': 'Panama',
						'PY.json': 'Paraguay',
						'PE.json': 'Peru',
						'PH.json': 'Philippines',
						'PL.json': 'Poland',
						'PT.json': 'Portugal',
						'QA.json': 'Qatar',
						'RO.json': 'Romania',
						'RU.json': 'Russian Federation',
						'SA.json': 'Saudi Arabia',
						'RS.json': 'Serbia',
						'SK.json': 'Slovakia',
						'SI.json': 'Slovenia',
						'ZA.json': 'South Africa',
						'ES.json': 'Spain',
						'SE.json': 'Sweden',
						'CH.json': 'Switzerland',
						'TW.json': 'Taiwan',
						'TH.json': 'Thailand',
						'TN.json': 'Tunisia',
						'TR.json': 'Turkey',
						'UA.json': 'Ukraine',
						'AE.json': 'United Arab Emirates',
						'GB.json': 'United Kingdom',
						'US.json': 'United States',
						'UY.json': 'Uruguay',
						'VN.json': 'Vietnam'
					}
				},
				'geojson': {
					'title': 'GeoJSON'
				},
				'topojson': {
					'title': 'TopoJSON'
				},
				'image': {
					'title': 'Image'
				},
				'svg': {
					'title': 'SVG'
				},
				'xyz': {
					'title': 'XYZ'
				},
				'stamen': {
					'title': 'Stamen',
					'source': {
						'watercolor': 'Watercolor',
						'terrain': 'Terrain',
						'terrain-labels': 'Terrain Labels',
						'toner': 'Toner',
						'toner-labels': 'Toner Labels',
						'toner-lite': 'Toner Lite'
					}
				},
				'maptiler': {
					'title': 'MapTiler',
					'source': {
						'basic': 'Basic',
						'bright': 'Bright',
						'hybrid': 'Hybrid',
						'pastel': 'Pastel',
						'satellite-v2': 'Satellite',
						'streets': 'Streets',
						'topo': 'Topo',
						'voyager': 'Voyager'
					}
				},
				'bingmaps': {
					'title': 'Bing Maps',
					'source': {
						'Aerial': 'Aerial',
						'AerialWithLabelsOnDemand': 'Aerial with labels',
						'RoadOnDemand': 'Road',
						'CanvasDark': 'Canvas Dark',
						'CanvasLight': 'Canvas Light',
						'CanvasGray': 'Canvas Gray'
					}
				},
				'mapbox': {
					'title': 'Mapbox Vector',
					'source': {
						'bright': 'Bright',
						'streets': 'Streets',
						'outdoors': 'Outdoors',
						'light': 'Light',
						'dark': 'Dark'
					}
				}	
			}

			this.init = function(layers) {
				this.el = document.createElement('div');
				this.el.id = 'mgeo-admin-layers';

				const title = document.createElement('h2');
				title.innerHTML = __('Layers', 'mgeo');
				this.el.appendChild(title);

				const icon = document.createElement('i');
				icon.classList.add('mgeo-admin-icon', 'mgeo-icon-layers');
				title.prepend(icon);

				const list = document.createElement('ul');
				list.classList.add('mgeo-admin-list');
				this.el.appendChild(list);

				const addlg = document.createElement('button');
				addlg.classList.add('mgeo-admin-new-layer-group', 'mgeo-admin-button');
				addlg.innerHTML = __('Group', 'mgeo');
				addlg.insertAdjacentHTML('afterbegin', '<i class="mgeo-icon-plus"></i>');
				addlg.addEventListener('click', function(e) {
					e.preventDefault();
					const newlg = s.createLayerGroup({ layers: []});
					list.prepend(newlg);
					setTimeout(function() { newlg.classList.add('opened'); }, 10);
					setTimeout(function() { list.querySelector('input').focus(); }, 200);
				});
				this.el.prepend(addlg);

				const add = document.createElement('button'); // add button
				add.classList.add('mgeo-admin-new-layer', 'mgeo-admin-button');
				add.innerHTML = __('Layer', 'mgeo');
				add.insertAdjacentHTML('afterbegin', '<i class="mgeo-icon-plus"></i>');
				add.addEventListener('click', function(e) {
					e.preventDefault();
					const newlayer = s.createLayerElement(false);
					list.prepend(newlayer);
					setTimeout(function() { newlayer.classList.add('opened'); }, 10);
					setTimeout(function() { list.querySelector('input').focus(); }, 200);
				});
				this.el.prepend(add);

				if (layers && layers.length > 0) {
					for (const layer of layers) {
						if (!layer.layers) list.appendChild(this.createLayerElement(layer));
						else {
							const lg = list.appendChild(this.createLayerGroup(layer));
							for (const sub of layer.layers) lg.querySelector(':scope > .mgeo-gl-list').appendChild(this.createLayerElement(sub));
						}
					}
				}

				// sortable
				Sortable.create(list, {
					animation: 200,
					group: 'shared',
					handle: '#mgeo-admin-layers .mgeo-admin-list-item-handle',
					delay: 50,
					delayOnTouchOnly: true
				});

				return this;
			}

			this.createLayerElement = function(layer) {
				if (layer.type == 'markers') this.options[layer.id] = layer.title;

				const item = document.createElement('li');
				item.classList.add('mgeo-admin-list-item');

				const handle = document.createElement('div');
				handle.classList.add('mgeo-admin-list-item-handle');
				item.appendChild(handle);

				handle.addEventListener('click', function() { item.classList.toggle('opened'); });

				const title = document.createElement('span');
				if (layer) title.innerHTML = layer.title;
				else title.innerHTML = __('[New Layer]', 'mgeo');
				handle.appendChild(title);
				handle.appendChild(document.createElement('i')).classList.add('mgeo-icon-arrow-down');

				const type = document.createElement('small');
				if (this.type[layer.type]) type.innerHTML = ' - ' + this.type[layer.type].title;
				handle.appendChild(type);

				const body = document.createElement('div');
				body.classList.add('mgeo-admin-list-item-body');
				item.appendChild(body);

				// fields
				const idInput = self.newTextInput('mgeo-layer-id', layer.id, 'ID', 'id');
				if (layer.id) idInput.querySelector('.mgeo-layer-id').setAttribute('disabled', '');
				idInput.classList.add('half');
				body.appendChild(idInput);

				body.appendChild(self.newTextInput('mgeo-layer-title', layer.title, 'Title')).classList.add('half', 'last');

				const typeSelect = self.newSelect('mgeo-layer-type', this.type, layer.type, 'Type');
				typeSelect.classList.add('half');
				body.appendChild(typeSelect);

				const select = typeSelect.querySelector('select.mgeo-layer-type');
				const sourceContainer = document.createElement('div');
				body.appendChild(sourceContainer).classList.add('half', 'last');

				select.addEventListener('input', function(e) {
					if (sourceContainer.firstChild) sourceContainer.removeChild(sourceContainer.firstChild);
					
					if (s.type[e.target.value].source == false) { /* no field */ }
					else if (s.type[e.target.value].source) sourceContainer.appendChild(self.newSelect('mgeo-layer-source', s.type[e.target.value].source, layer.source, 'Source'));
					else sourceContainer.appendChild(self.newActionInput('mgeo-layer-source', 'file', layer.source, 'Source/URL'));
				});

				select.dispatchEvent(new Event('input', { target: select }));
				
				body.appendChild(self.newSelect('mgeo-layer-style', self.styles.options, layer.style, 'Style', false, false, false)).classList.add('half');
				body.appendChild(self.newActionInput('mgeo-layer-extent', 'extent', layer.extent, 'Extent')).classList.add('space');
				body.appendChild(self.newNumberInput('mgeo-layer-minzoom', layer.minZoom, 'Min zoom')).classList.add('third');
				body.appendChild(self.newNumberInput('mgeo-layer-maxzoom', layer.maxZoom, 'Max zoom')).classList.add('third');
				body.appendChild(self.newOpacityInput('mgeo-layer-opacity', layer.opacity, 'Opacity')).classList.add('third', 'last');
				body.appendChild(self.newCheckbox('mgeo-layer-selectable', layer.selectable, 'Selectable')).classList.add('single', 'space');
				body.appendChild(self.newCheckbox('mgeo-layer-isolated', layer.isolated, 'Isolated')).classList.add('single');
				body.appendChild(self.newCheckbox('mgeo-layer-hide', layer.hide, 'Hide by default')).classList.add('single');
				body.appendChild(self.newCheckbox('mgeo-layer-disabled', layer.disabled, 'Disable')).classList.add('third', 'left');

				// remove button
				const remove = document.createElement('a');
				remove.classList.add('mgeo-remove-button');
				remove.innerHTML = __('Remove', 'mgeo');
				body.appendChild(remove);
				remove.addEventListener('click', function() { if (confirm('Are you sure you want to remove "' + layer.title + '" layer?')) item.parentNode.removeChild(item); });

				return item;
			}

			this.createLayerGroup = function(layer) {
				const item = document.createElement('li');
				item.classList.add('mgeo-admin-list-item', 'mgeo-layer-group');

				const handle = document.createElement('div');
				handle.classList.add('mgeo-admin-list-item-handle');
				item.appendChild(handle);

				handle.addEventListener('click', function() { item.classList.toggle('opened'); });

				const title = document.createElement('span');
				if (layer.title) title.innerHTML = layer.title;
				else title.innerHTML = __('[New Group]', 'mgeo');
				handle.appendChild(title);

				const body = document.createElement('div');
				body.classList.add('mgeo-admin-list-item-body');
				item.appendChild(body);

				const list = document.createElement('ul');
				list.classList.add('mgeo-gl-list');
				item.appendChild(list);

				// sortable
				Sortable.create(list, {
					animation: 200,
					group: {
						name: 'shared',
						put: function(to, from, el) { return !el.classList.contains('mgeo-layer-group'); }
					},
					handle: '#mgeo-admin-layers .mgeo-admin-list-item-handle',
					delay: 50,
					delayOnTouchOnly: true
				});

				// fields
				const idInput = self.newTextInput('mgeo-layer-id', layer.id, 'ID', 'id');
				idInput.classList.add('half');
				body.appendChild(idInput);

				body.appendChild(self.newTextInput('mgeo-layer-title', layer.title, 'Title')).classList.add('half', 'last');
				body.appendChild(self.newNumberInput('mgeo-layer-minzoom', layer.minZoom, 'Min zoom')).classList.add('third');
				body.appendChild(self.newNumberInput('mgeo-layer-maxzoom', layer.maxZoom, 'Max zoom')).classList.add('third');
				body.appendChild(self.newOpacityInput('mgeo-layer-opacity', layer.opacity, 'Opacity')).classList.add('third', 'last');
				body.appendChild(self.newCheckbox('mgeo-layer-selectable', layer.selectable, 'Selectable')).classList.add('single', 'space');
				body.appendChild(self.newCheckbox('mgeo-layer-isolated', layer.isolated, 'Isolated')).classList.add('single');
				body.appendChild(self.newCheckbox('mgeo-layer-hide', layer.hide, 'Hide by default')).classList.add('single');
				body.appendChild(self.newCheckbox('mgeo-layer-disabled', layer.disabled, 'Disable')).classList.add('third');

				// remove button
				const remove = document.createElement('a');
				remove.classList.add('mgeo-remove-button');
				remove.innerHTML = __('Remove', 'mgeo');
				body.appendChild(remove);
				remove.addEventListener('click', function() { if (confirm('Are you sure you want to remove "' + layer.title + '" layer group and all sublayers?')) item.parentNode.removeChild(item); });

				return item;	
			}
		}

		// locations
		function Locations() {
			const s = this;

			this.keys = null;
			this.edit = null;
			this.tbody = null;
			this.tbodyext = null;
			this.tablemode = true;

			let ma = null;
			let maCount = null;

			this.init = function(l) {
				this.el = document.createElement('div');
				this.el.id = 'mgeo-admin-locations';

				const title = document.createElement('h2'); // title
				title.innerHTML = __('Locations', 'mgeo');
				this.el.appendChild(title);

				const toggle = document.createElement('label');
				toggle.classList.add('mgeo-edit-mode');
				toggle.innerHTML = __('Edit table', 'mgeo');
				const editmode = document.createElement('input');
				editmode.setAttribute('type', 'checkbox');
				editmode.checked = s.tablemode;
				editmode.addEventListener('change', function() { s.tablemode = this.checked; });
				toggle.appendChild(editmode);
				title.appendChild(toggle);

				const icon = document.createElement('i');
				icon.classList.add('mgeo-admin-icon', 'mgeo-icon-locations');
				title.prepend(icon);

				// table
				const tc = document.createElement('div');
				tc.id = ('mgeo-location-table')
				this.el.appendChild(tc);

				const table = document.createElement('table');
				tc.appendChild(table);

				// keys
				s.keys = new Set(['id', 'title', 'desc', 'coord', 'zoom', 'type', 'label', 'fill', 'style', 'action', 'image', 'thumbnail', 'group', 'link', 'about', 'layer', 'sample', 'scale', 'hide', 'disabled']);
				for (const id in l) s.keys = new Set([...s.keys, ...Object.keys(l[id])]);
				s.keys.delete('feature');
				s.keys.delete('marker');
				s.keys.delete('res');
				s.keys.delete('external');
				s.keys = [...s.keys];
 
				// header
				const thead = document.createElement('thead');
				const row = document.createElement('tr');

				let el = document.createElement('th');
				let cb = document.createElement('input');
				cb.setAttribute('type', 'checkbox');
				cb.addEventListener('change', function() {
					const state = this.checked;
					const event = new Event('change');
					table.querySelectorAll('tbody tr > td:first-child input[type="checkbox"]').forEach(function(el) {
						el.checked = state;
						el.dispatchEvent(event);
					});
				});
				el.appendChild(cb);
				row.appendChild(el);
				
				for (const key of s.keys) {
					el = document.createElement('th');
					el.innerHTML = key;

					self.help.addHelp(el, key);

					if (key == 'desc') el.classList.add('mgeo-cell-l');
					else if (['id', 'title', 'about', 'type', 'image', 'thumbnail'].includes(key)) el.classList.add('mgeo-cell-m');
					row.appendChild(el);
				}
				thead.appendChild(row);
				table.appendChild(thead);

				// rows
				s.tbody = document.createElement('tbody');
				s.tbody.classList.add('mgeo-editable');
				s.tbodyext = document.createElement('tbody');
				s.tbodyext.classList.add('mgeo-external');

				for (const id in l) {
					if (l[id].external) s.tbodyext.appendChild(this.createRowElement(l[id]));
					else s.tbody.appendChild(this.createRowElement(l[id]));
				}
				table.appendChild(s.tbody);
				table.appendChild(s.tbodyext);

				// multiactions
				ma = document.createElement('div');
				ma.classList.add('mgeo-multiactions');
				this.el.appendChild(ma);

				const maText = document.createElement('span')
				maText.innerHTML = __(' locations selected', 'mgeo');
				ma.append(maText);

				maCount = document.createElement('b');
				maCount.innerHTML = '0';
				maText.prepend(maCount);

				const maDelete = document.createElement('button'); // add button
				maDelete.classList.add('mgeo-admin-button', 'mgeo-admin-button-delete');
				maDelete.innerHTML = __('Delete', 'mgeo');
				maDelete.addEventListener('click', function(e) {
					e.preventDefault();
					s.tbody.querySelectorAll('tr.mgeo-selected').forEach(function(el) {
						s.tbody.removeChild(el);
					});
					cb.checked = false;
					ma.classList.remove('opened');
				});
				ma.append(maDelete);

				// auto populate
				const autopop = document.createElement('button');
				autopop.classList.add('mgeo-admin-autopop', 'mgeo-admin-button');
				autopop.innerHTML = __('Auto-populate', 'mgeo');

				autopop.addEventListener('click', function(e) {
					e.preventDefault();
					for (let id in obj.container.l) {
						let source = obj.container.l[id].object.getSource();
						if (source.getFeatures !== undefined) {
							source.getFeatures().forEach(function(f, i) {
								let id = f.getId();
								if (!l[id]) {
									let data = {id: id, title: f.get('name')};
									s.tbody.appendChild(s.createRowElement(data));
									obj.dir.modifyLocation(null, data);
								}
							});
						}
					}
					autopop.style.display = 'none';
				});
				this.el.prepend(autopop);

				const nrspan = document.createElement('span');
				nrspan.innerHTML = '0';
				autopop.appendChild(nrspan);

				for (let id in obj.container.l) {
					if (obj.container.l[id].object.getSource !== undefined && obj.container.l[id].object?.getSource()) {
						obj.container.l[id].object.getSource().on('featuresloadend', function(e) {
							if (obj.container.emptyfeatures > 0) {
								nrspan.innerHTML = obj.container.emptyfeatures;
								autopop.style.display = 'block';
							}
						});
					}
				}

				// add
				const add = document.createElement('button'); // add button
				add.classList.add('mgeo-admin-new-location', 'mgeo-admin-button');
				add.innerHTML = __('Location', 'mgeo');
				add.insertAdjacentHTML('afterbegin', '<i class="mgeo-icon-plus"></i>');
				this.el.prepend(add);
				add.addEventListener('click', function(e) {
					e.preventDefault();
					s.openPopup();
				});

				// sortable
				Sortable.create(s.tbody, {
					animation: 200,
					handle: '#mgeo-admin-locations tr > td:first-child',
					delay: 50,
					delayOnTouchOnly: true,
					ghostClass: 'mgeo-drag',

					onStart: function() { s.tbody.classList.add('mgeo-dragging'); },
					onEnd: function() { s.tbody.classList.remove('mgeo-dragging'); }
				});

				// feature click
				obj.el.addEventListener('featureclick', function(e) {
					const feature = e.detail.feature,
						location = obj.dir.getLocation(feature.getId());

					if (!location && feature.getId() && confirm('Assign a new location to "' + feature.getId() + '"?')) s.openPopup(false, {id: feature.getId(), title: feature.get('name'), desc: '', coord: obj.container.getCoord(e.detail.coord) });
				});

				// location focus
				obj.el.addEventListener('locationfocus', function(e) {
					const location = e.detail.location;
					if (location?.id) {
						const row = s.getRow(location.id);
						if (row) s.makeRowEditable(row);
					}
				});

				// popup edit button
				obj.el.addEventListener('popuprender', function(e) {
					const id = e.detail.dataset.location;
					if (obj.dir.l[id].res.external) return false;

					const edit = document.createElement('a');
					edit.innerHTML = 'Edit';
					edit.addEventListener('click', function() {
						const row = s.getRow(id);
						if (row) s.openPopup(row);
					});

					e.detail.querySelector('.mgeo-buttons').appendChild(edit);
				});

				// map reset
				obj.el.addEventListener('mapreset', function() {
					s.saveRow(s.edit);
				});

				return this;
			}

			this.getRow = function(id) {
				return s.tbody.querySelector('tr[data-id="' + id + '"]');
			}

			this.getDataFromRow = function(row) {
				if (!row) return null;

				const location = {};
				row.querySelectorAll('.mgeo-cell-field').forEach(function(el) {
					const field = el.getAttribute('data-field');
					if (field) location[field] = el.innerHTML;
				});

				return location;
			}

			this.getDataFromPopup = function(popup) {
				if (!popup) return null;

				const id = self.validateId(popup.querySelector('.mgeo-location-id'));
				if (!id) return false;

				const location = {
					id: id,
					title: popup.querySelector('.mgeo-location-title').value,
					coord: popup.querySelector('.mgeo-location-coord').value,
					zoom: popup.querySelector('.mgeo-location-zoom').value,
					action: popup.querySelector('.mgeo-location-action').value,
					link: popup.querySelector('.mgeo-location-link').value,
					label: popup.querySelector('.mgeo-location-label').value,
					type: popup.querySelector('.mgeo-admin-type > .mgeo-selected > small').innerHTML,
					fill: popup.querySelector('.mgeo-location-fill').value,
					style: popup.querySelector('.mgeo-location-style').value,
					image: popup.querySelector('.mgeo-location-image').value,
					thumbnail: popup.querySelector('.mgeo-location-thumbnail').value,
					group: popup.querySelector('.mgeo-location-group').value,
					about: popup.querySelector('.mgeo-location-about').value,
					layer: popup.querySelector('.mgeo-location-layer').value,
					sample: popup.querySelector('.mgeo-location-sample').value,
					hide: popup.querySelector('.mgeo-location-hide').checked,
					disabled: popup.querySelector('.mgeo-location-disabled').checked
				};

				const textarea = popup.querySelector('.mgeo-location-desc');
				if (textarea) location.desc = tinymce.get(textarea.id).getContent();

				return location;
			}

			this.openPopup = function(row = null, data = {desc: '', coord: obj.container.getCoord()}) {
				s.saveRow(s.edit);

				if (row) data = this.getDataFromRow(row);

				const popup = document.createElement('div');
				this.el.prepend(popup);

				popup.appendChild(self.newTextInput('mgeo-location-id', data.id, 'ID', 'id')).classList.add('half');
				popup.appendChild(self.newTextInput('mgeo-location-title', data.title, 'Title', 'title')).classList.add('half', 'last');

				const textarea = document.createElement('textarea');
				textarea.classList.add('mgeo-location-desc');
				textarea.value = data.desc;
				popup.appendChild(textarea);

				popup.appendChild(self.newTextInput('mgeo-location-about', data.about, 'About', 'about'));

				popup.appendChild(self.newTextInput('mgeo-location-coord', data.coord, 'Coordinate', 'coord')).classList.add('three-fourths');
				popup.appendChild(self.newNumberInput('mgeo-location-zoom', data.zoom, 'Zoom', 'zoom')).classList.add('fourth', 'last');
				popup.appendChild(self.newSelect('mgeo-location-action', obj.actions.a, data.action, 'Action', 'action')).classList.add('half');
				popup.appendChild(self.newTextInput('mgeo-location-link', data.link, 'Link', 'link')).classList.add('half', 'last');

				popup.appendChild(self.newTypeInput(data.type, 'Type', 'type')).classList.add('half');
				const col = document.createElement('div');
				col.classList.add('half', 'last')
				col.appendChild(self.newTextInput('mgeo-location-label', data.label, 'Label', 'label'));
				col.appendChild(self.newTextInput('mgeo-location-fill mgeo-colorpicker', data.fill, 'Fill', 'fill'));
				col.appendChild(self.newSelect('mgeo-location-style', self.styles.options, data.style, 'Style', 'style', false, false));
				popup.appendChild(col);

				popup.appendChild(self.newActionInput('mgeo-location-image', 'file', data.image, 'Image', 'image')).classList.add('half');
				popup.appendChild(self.newActionInput('mgeo-location-thumbnail', 'file', data.thumbnail, 'Thumbnail', 'thumbnail')).classList.add('half', 'last');
				popup.appendChild(self.newTextInput('mgeo-location-sample', data.sample, 'Sample', 'sample')).classList.add('half');
				popup.appendChild(self.newSelect('mgeo-location-layer', self.layers.options, data.layer, 'Layer', 'layer', false, false)).classList.add('half', 'last');
				popup.appendChild(self.newSelect('mgeo-location-group', self.groups.options, data.group, 'Group', 'group', true, false)).classList.add('half', 'last');
				popup.appendChild(self.newCheckbox('mgeo-location-hide', data.hide, 'Hide from directory', 'hide'));
				popup.appendChild(self.newCheckbox('mgeo-location-disabled', data.disabled, 'Disable', 'disabled'));

				if (row) {
					const del = document.createElement('button');
					del.classList.add('mgeo-admin-button');
					del.innerHTML = __('Delete', 'mgeo');
					del.addEventListener('click', function(e) {
						e.preventDefault();
						if (confirm(__('Are you sure you want to delete the location?', 'mgeo'))) {
							row.parentNode.removeChild(row);
							Fancybox.close();

							obj.actions.blur();
							obj.dir.modifyLocation(data.id, null);
						}
					});
					popup.appendChild(del);

					const duplicate = document.createElement('button');
					duplicate.classList.add('mgeo-admin-button');
					duplicate.innerHTML = __('Duplicate', 'mgeo');
					duplicate.addEventListener('click', function(e) {
						e.preventDefault();
						/* duplicate location */
					});
					popup.appendChild(duplicate);

					const save = document.createElement('button');
					save.classList.add('mgeo-admin-button', 'mgeo-admin-button-primary');
					save.innerHTML = __('Save location', 'mgeo');
					save.addEventListener('click', function(e) {
						e.preventDefault();
						const updated = s.getDataFromPopup(popup);
						if (updated) {
							s.updateRow(row, updated);
							Fancybox.close();

							obj.actions.blur();
							obj.dir.modifyLocation(data.id, s.getDataFromRow(row));
						}
					});
					popup.appendChild(save);
				}
				else {
					const add = document.createElement('button');
					add.classList.add('mgeo-admin-button', 'mgeo-admin-button-primary');
					add.innerHTML = __('Add location', 'mgeo');
					add.addEventListener('click', function(e) {
						e.preventDefault();
						const data = s.getDataFromPopup(popup);
						if (data) {
							s.tbody.prepend(s.createRowElement(data));
							Fancybox.close();

							obj.dir.modifyLocation(null, data);
						}
					});
					popup.appendChild(add);
				}

				// popup
				Fancybox.show([{ src: popup, type: 'html' }], {
					mainClass: 'mgeo-admin mgeo-admin-popup',
					on: {
						reveal: function() {
							tinymce.init({
								target: textarea,
								plugins: 'link image',
								toolbar: 'undo redo | bold italic | alignleft aligncenter alignright alignjustify | outdent indent | styleselect',
								menubar: false
							});

							Coloris({
								el: '.mgeo-admin-popup .mgeo-colorpicker',
								theme: 'large',
								format: 'mixed'
							});
						}
					}
				});
			}

			this.createRowElement = function(location) {
				const row = document.createElement('tr');
				if (location?.id) row.setAttribute('data-id', location.id);

				// first col
				let el = document.createElement('td');
				if (!location || !location.external) {
					const cb = document.createElement('input');
					cb.setAttribute('type', 'checkbox');
					cb.addEventListener('change', function() {
						if (this.checked) row.classList.add('mgeo-selected');
						else row.classList.remove('mgeo-selected');
						const count = s.tbody.querySelectorAll('tr > td:first-child input[type="checkbox"]:checked').length;
						maCount.innerHTML = count;
						ma.classList.toggle('opened', count > 0);
					});
					el.appendChild(cb);

					// save
					const save = document.createElement('button');
					save.classList.add('mgeo-location-save');
					save.setAttribute('title', 'Save location');
					save.appendChild(document.createElement('i')).classList.add('mgeo-icon-save');
					save.addEventListener('click', function(e) {
						e.preventDefault();
						s.saveRow(row);
					});
					el.appendChild(save);

					// duplicate
					const duplicate = document.createElement('button');
					duplicate.classList.add('mgeo-location-duplicate');
					duplicate.setAttribute('title', 'Duplicate location');
					duplicate.appendChild(document.createElement('i')).classList.add('mgeo-icon-duplicate');
					duplicate.addEventListener('click', function(e) {
						e.preventDefault();
						s.saveRow(row);
						const data = s.getDataFromRow(row);
						data.id = data.id + '-d';
						const clone = s.createRowElement(data);
						s.tbody.prepend(clone);
						s.makeRowEditable(clone);
					});
					el.appendChild(duplicate);

					// delete
					const del = document.createElement('button');
					del.classList.add('mgeo-location-delete');
					del.setAttribute('title', 'Delete location');
					del.appendChild(document.createElement('i')).classList.add('mgeo-icon-delete');
					del.addEventListener('click', function(e) {
						e.preventDefault();
						if (confirm(__('Are you sure you want to delete the location?', 'mgeo'))) {
							row.parentNode.removeChild(row);
							obj.dir.modifyLocation(location.id, null);
						}
					});
					el.appendChild(del);

					// edit
					row.addEventListener('click', function(e) {
						if (!row.classList.contains('mgeo-edit') && (e.target.classList.contains('mgeo-cell-field') || e.target.closest('.mgeo-cell-field'))) {
							if (s.tablemode) s.makeRowEditable(row);
							else s.openPopup(row);

							if (location) obj.container.zoomTo(location);
						}
					});
				}
				row.appendChild(el);

				for (const key of s.keys) {
					el = document.createElement('td');
					el.classList.add('mgeo-cell-field');
					el.setAttribute('data-field', key);
					if (location && location[key]) el.innerHTML = location[key];
					if (key == 'id') el.classList.add('mgeo-cell-nobreak');
					if (key == 'fill') el.classList.add('mgeo-cell-color');
					row.appendChild(el);
				}

				return row;
			}

			this.makeRowEditable = function(row) {
				if (s.edit) s.saveRow(s.edit);

				let coord = null;

				row.querySelectorAll('td.mgeo-cell-field').forEach(function(el, i) {
					el.setAttribute('data-value', el.innerHTML);
					el.innerHTML = '';

					const value = el.getAttribute('data-value');

					switch (el.getAttribute('data-field')) {
						case 'desc':
							const textarea = document.createElement('textarea');
							textarea.value = value;
							el.appendChild(textarea);
							tinymce.init({
								target: textarea,
								plugins: 'link image code',
								toolbar: 'undo redo | code | bold italic | alignleft aligncenter alignright alignjustify | outdent indent | styleselect',
								menubar: false
							});
							break;
						case 'coord':
							coord = el.appendChild(self.newTextInput(false, value));
							break;
						case 'action':
							el.appendChild(self.newSelect(false, obj.actions.a, value));
							break;
						case 'zoom':
							el.appendChild(self.newNumberInput(false, value));
							break;
						case 'type':
							el.appendChild(self.newTypeInput(value));
							break;
						case 'image':
						case 'thumbnail':
							el.appendChild(self.newActionInput(false, 'file', value));
							break;
						case 'group':
							el.appendChild(self.newSelect(false, self.groups.options, value, false, true, false, false));
							break;
						case 'layer':
							el.appendChild(self.newSelect(false, self.layers.options, value, false, false, false, false));
							break;
						case 'style':
							el.appendChild(self.newSelect(false, self.styles.options, value, false, false, false, false));
							break;
						case 'scale':
							el.appendChild(self.newNumberInput(false, value));
							break;
						case 'hide':
						case 'disabled':
							el.appendChild(self.newCheckbox(false, value === 'true'));
							break;
						default:
							const input = document.createElement('input');
							input.setAttribute('type', 'text');
							input.setAttribute('size', 12);
							input.value = value;

							if (el.classList.contains('mgeo-cell-color')) input.classList.add('mgeo-colorpicker');
							el.appendChild(input);
					}
				});

				row.classList.add('mgeo-edit');
				s.edit = row;
				obj.container.startMarkerDrag(row.getAttribute('data-id'), coord);
			}

			this.updateRow = function(row, location) {
				row.setAttribute('data-id', location.id);
				row.querySelectorAll('td.mgeo-cell-field').forEach(function(el) {
					const field = el.getAttribute('data-field');
					if (field) {
						if (field === 'hide' || field == 'disabled') {
							if (location[field]) el.innerHTML = 'true';
							else el.innerHTML = '';
						}
						else if (location[field] != undefined) {
							el.innerHTML = location[field];
							el.dataset.value = location[field];
						}
					}
				});
			}

			this.saveRow = function(row) {
				if (!row) return false;

				const id = self.validateId(row.querySelector('.mgeo-cell-field[data-field="id"] > input'));
				if (!id) return false;

				row.querySelectorAll('td.mgeo-cell-field').forEach(function(el) {
					const field = el.querySelector(':scope > input, :scope > select, :scope > label > input');
					if (field) {
						if (field.getAttribute('type') === 'checkbox') {
							if (field.checked) el.innerHTML = 'true';
							else el.innerHTML = '';
						}
						else el.innerHTML = field.value;
						return;
					}

					// desc
					const textarea = el.querySelector(':scope > textarea');
					if (textarea) {
						el.innerHTML = tinymce.get(textarea.id).getContent();
						tinymce.get(textarea.id).remove();
						return;
					}

					// type
					const type = el.querySelector(':scope > .mgeo-admin-type > .mgeo-selected > small');
					if (type) el.innerHTML = type.innerHTML;
				});

				row.setAttribute('data-id', row.querySelector('.mgeo-cell-field[data-field="id"]').innerHTML);
				row.classList.remove('mgeo-edit');
				obj.container.stopMarkerDrag();
				s.edit = null;
			}
		}
	}

	let admin = new MapplicGeoAdmin(document.querySelector('.mgeo-map'));
	admin.init();

})();

Youez - 2016 - github.com/yon3zu
LinuXploit