Browse Source

Fix merge conflicts with default config

root 4 years ago
parent
commit
468b88e903
51 changed files with 4782 additions and 866 deletions
  1. 170 37
      api/classes/organizr.class.php
  2. 45 21
      api/config/default.php
  3. 1 1
      api/functions/auth-functions.php
  4. 13 2
      api/functions/homepage-functions.php
  5. 2 2
      api/functions/option-functions.php
  6. 52 3
      api/functions/organizr-functions.php
  7. 1 1
      api/functions/token-functions.php
  8. 45 0
      api/functions/upgrade-functions.php
  9. 1 7
      api/homepage/calendar.php
  10. 1 7
      api/homepage/couchpotato.php
  11. 5 11
      api/homepage/deluge.php
  12. 14 13
      api/homepage/emby.php
  13. 1 7
      api/homepage/healthchecks.php
  14. 26 76
      api/homepage/html.php
  15. 1 7
      api/homepage/jackett.php
  16. 1 7
      api/homepage/jdownloader.php
  17. 1 7
      api/homepage/jellyfin.php
  18. 1 7
      api/homepage/lidarr.php
  19. 1 7
      api/homepage/monitorr.php
  20. 1 7
      api/homepage/netdata.php
  21. 1 7
      api/homepage/nzbget.php
  22. 1 7
      api/homepage/octoprint.php
  23. 1 7
      api/homepage/ombi.php
  24. 1 7
      api/homepage/pihole.php
  25. 119 100
      api/homepage/plex.php
  26. 1 7
      api/homepage/qbittorrent.php
  27. 1 7
      api/homepage/radarr.php
  28. 2 8
      api/homepage/rtorrent.php
  29. 1 7
      api/homepage/sabnzbd.php
  30. 1 7
      api/homepage/sickrage.php
  31. 1 7
      api/homepage/sonarr.php
  32. 1 7
      api/homepage/speedtest.php
  33. 1 7
      api/homepage/tautulli.php
  34. 2 8
      api/homepage/trakt.php
  35. 1 7
      api/homepage/transmission.php
  36. 1 7
      api/homepage/unifi.php
  37. 1 7
      api/homepage/weather.php
  38. 6 6
      api/pages/settings-tab-editor-categories.php
  39. 18 4
      api/pages/settings-tab-editor-tabs.php
  40. 311 345
      api/pages/settings.php
  41. 28 7
      api/plugins/bookmark/settings.js
  42. 21 12
      api/v2/routes/token.php
  43. 22 0
      css/organizr.css
  44. 0 0
      css/organizr.min.css
  45. 3783 0
      js/Sortable.js
  46. 1 0
      js/Sortable.min.js
  47. 12 7
      js/custom.js
  48. 0 0
      js/custom.min.js
  49. 53 41
      js/functions.js
  50. 7 0
      js/version.json
  51. BIN
      plugins/images/tabs/HTML5.png

+ 170 - 37
api/classes/organizr.class.php

@@ -61,7 +61,7 @@ class Organizr
 	
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.496';
+	public $version = '2.1.525';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.3';
@@ -190,7 +190,7 @@ class Organizr
 			if ($this->config['blacklisted'] !== '') {
 				if (in_array($currentIP, $this->arrayIP($this->config['blacklisted']))) {
 					$this->debug('User was sent to blackhole - Blacklisted IPs: ' . $this->config['blacklisted']);
-					die($this->config['blacklistedMessage']);
+					die($this->showHTML('Blacklisted', $this->config['blacklistedMessage']));
 				}
 			}
 		}
@@ -571,7 +571,7 @@ class Organizr
 	private function checkPHP()
 	{
 		if (!(version_compare(PHP_VERSION, $this->minimumPHP) >= 0)) {
-			die('Organizr needs PHP Version: ' . $this->minimumPHP . '<br/> You have PHP Version: ' . PHP_VERSION);
+			die($this->showHTML('PHP Version', 'Organizr needs PHP Version: ' . $this->minimumPHP . '<br/> You have PHP Version: ' . PHP_VERSION));
 		}
 	}
 	
@@ -580,7 +580,7 @@ class Organizr
 		if ($this->hasDB()) {
 			$db = is_writable($this->config['dbLocation'] . $this->config['dbName']);
 			if (!$db) {
-				die('Organizr DB is not writable!!!  Please fix...');
+				die($this->showHTML('Organizr DB is not writable!', 'Please check permissions and/or disk space'));
 			}
 		}
 	}
@@ -596,7 +596,7 @@ class Organizr
 				@$this->rrmdir($cleanup);
 			}
 			if (file_exists($tempLock)) {
-				die('upgrading');
+				die($this->showHTML('Upgrading', 'Please wait...'));
 			}
 			$updateDB = false;
 			$updateSuccess = true;
@@ -639,6 +639,14 @@ class Organizr
 				$this->upgradeToVersion($versionCheck);
 			}
 			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.525';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
 			if ($updateDB == true) {
 				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
 				// Upgrade database to latest version
@@ -650,7 +658,7 @@ class Organizr
 				$this->debug('Updated config version to ' . $this->version);
 			}
 			if ($updateSuccess == false) {
-				die('Database update failed - Please manually check logs and fix - Then reload this page');
+				die($this->showHTML('Database update failed', 'Please manually check logs and fix - Then reload this page'));
 			}
 			return true;
 		}
@@ -781,7 +789,7 @@ class Organizr
 			}
 		}
 		// Build output
-		$output = (!$nest ? "<?php\nreturn " : '') . "array(\n" . implode(",\n", $output) . "\n" . str_repeat("\t", $nest) . ')' . (!$nest ? ';' : '');
+		$output = (!$nest ? "<?php\nreturn " : '') . "[\n" . implode(",\n", $output) . "\n" . str_repeat("\t", $nest) . ']' . (!$nest ? ';' : '');
 		if (!$nest && $path) {
 			$pathDigest = pathinfo($path);
 			@mkdir($pathDigest['dirname'], 0770, true);
@@ -818,6 +826,25 @@ class Organizr
 		return $this->createConfig($current);
 	}
 	
+	public function removeConfigItem($new, $current = false)
+	{
+		// Get config if not supplied
+		if ($current === false) {
+			$current = $this->config;
+		} elseif (is_string($current) && is_file($current)) {
+			$current = $this->loadConfig($current);
+		}
+		// Inject Parts
+		foreach ($new as $k) {
+			if (isset($current[$k])) {
+				$current['deletedConfigItems'][$k] = $current[$k];
+			}
+			unset($current[$k]);
+		}
+		// Return Create
+		return $this->createConfig($current);
+	}
+	
 	public function loadConfig($path = null)
 	{
 		$path = ($path) ? $path : $this->userConfigPath;
@@ -2569,22 +2596,10 @@ class Organizr
 					'value' => $this->config['ssoPlex']
 				)
 			),
-			'Tautulli' => array(
-				array(
-					'type' => 'input',
-					'name' => 'tautulliURL',
-					'label' => 'Tautulli URL',
-					'value' => $this->config['tautulliURL'],
-					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-					'placeholder' => 'http(s)://hostname:port'
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'ssoTautulli',
-					'label' => 'Enable',
-					'value' => $this->config['ssoTautulli']
-				)
-			),
+			'Tautulli' => [
+				$this->settingsOption('multiple-url', 'tautulliURL'),
+				$this->settingsOption('enable', 'ssoTautulli'),
+			],
 			'Overseerr' => array(
 				array(
 					'type' => 'input',
@@ -2719,6 +2734,120 @@ class Organizr
 		);
 	}
 	
+	public function systemMenuLists()
+	{
+		$userManagementMenu = [
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_user_manage_users',
+				'anchor' => 'settings-user-manage-users-anchor',
+				'name' => 'Manage Users'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_user_manage_groups',
+				'anchor' => 'settings-user-manage-groups-anchor',
+				'name' => 'Manage Groups'
+			],
+			[
+				'active' => false,
+				'api' => false,
+				'anchor' => 'settings-user-import-users-anchor',
+				'name' => 'Import Users'
+			],
+		];
+		$customizeMenu = [
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_customize_appearance',
+				'anchor' => 'settings-customize-appearance-anchor',
+				'name' => 'Appearance',
+			],
+			[
+				'active' => false,
+				'api' => false,
+				'anchor' => 'settings-customize-marketplace-anchor',
+				'name' => 'Marketplace',
+				'onclick' => 'loadMarketplace(\'themes\');'
+			],
+		];
+		$tabEditorMenu = [
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_tab_editor_tabs',
+				'anchor' => 'settings-tab-editor-tabs-anchor',
+				'name' => 'Tabs'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_tab_editor_categories',
+				'anchor' => 'settings-tab-editor-categories-anchor',
+				'name' => 'Categories'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_tab_editor_homepage',
+				'anchor' => 'settings-tab-editor-homepage-anchor',
+				'name' => 'Homepage Items'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_tab_editor_homepage_order',
+				'anchor' => 'settings-tab-editor-homepage-order-anchor',
+				'name' => 'Homepage Order'
+			],
+		];
+		$systemSettingsMenu = [
+			[
+				'active' => true,
+				'api' => false,
+				'anchor' => 'settings-settings-about-anchor',
+				'name' => 'About'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_settings_main',
+				'anchor' => 'settings-settings-main-anchor',
+				'name' => 'Main'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_settings_sso',
+				'anchor' => 'settings-settings-sso-anchor',
+				'name' => 'SSO'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_settings_logs',
+				'anchor' => 'settings-settings-logs-anchor',
+				'name' => 'Logs'
+			],
+			[
+				'active' => false,
+				'api' => false,
+				'anchor' => 'settings-settings-updates-anchor',
+				'name' => 'Updates'
+			],
+			[
+				'active' => false,
+				'api' => 'api/v2/page/settings_settings_backup',
+				'anchor' => 'settings-settings-backup-anchor',
+				'name' => 'Backup'
+			],
+			[
+				'active' => false,
+				'api' => false,
+				'anchor' => 'settings-settings-donate-anchor',
+				'name' => 'Donate'
+			],
+		];
+		$systemMenus['system_settings'] = $this->buildSettingsMenus($systemSettingsMenu, 'System Settings');
+		$systemMenus['tab_editor'] = $this->buildSettingsMenus($tabEditorMenu, 'Tab Editor');
+		$systemMenus['customize'] = $this->buildSettingsMenus($customizeMenu, 'Customize');
+		$systemMenus['user_management'] = $this->buildSettingsMenus($userManagementMenu, 'User Management');
+		return $systemMenus;
+	}
+	
 	public function updateConfigMultiple($array)
 	{
 		return ($this->updateConfig($array)) ? true : false;
@@ -3205,7 +3334,7 @@ class Organizr
 		->identifiedBy('4f1g23a12aa', true)// Configures the id (jti claim), replicating as a header item
 		->issuedAt(time())// Configures the time that the token was issue (iat claim)
 		->expiresAt(time() + (86400 * $days))// Configures the expiration time of the token (exp claim)
-		//->withClaim('username', $result['username'])// Configures a new claim, called "username"
+		->withClaim('username', $result['username'])// Configures a new claim, called "username"
 		->withClaim('group', $result['group'])// Configures a new claim, called "group"
 		//->withClaim('groupID', $result['group_id'])// Configures a new claim, called "groupID"
 		//->withClaim('email', $result['email'])// Configures a new claim, called "email"
@@ -4029,6 +4158,9 @@ class Organizr
 			//new way
 			if (method_exists($this, $key)) {
 				$homepageBuilt .= $this->$key();
+			} elseif (strpos($key, 'homepageOrdercustomhtml') !== false) {
+				$iteration = substr($key, -2);
+				$homepageBuilt .= $this->homepageOrdercustomhtml($iteration);
 			} else {
 				$homepageBuilt .= '<div id="' . $key . '"></div>';
 			}
@@ -4045,17 +4177,18 @@ class Organizr
 		$inputList = '<form id="homepage-values" class="row">';
 		foreach ($homepageOrder as $key => $val) {
 			switch ($key) {
-				case 'homepageOrdercustomhtml':
-					$class = 'bg-info';
-					$image = 'plugins/images/tabs/custom1.png';
-					if (!$this->config['homepageCustomHTMLoneEnabled']) {
-						$class .= ' faded';
-					}
-					break;
-				case 'homepageOrdercustomhtmlTwo':
+				case 'homepageOrdercustomhtml01':
+				case 'homepageOrdercustomhtml02':
+				case 'homepageOrdercustomhtml03':
+				case 'homepageOrdercustomhtml04':
+				case 'homepageOrdercustomhtml05':
+				case 'homepageOrdercustomhtml06':
+				case 'homepageOrdercustomhtml07':
+				case 'homepageOrdercustomhtml08':
+					$iteration = substr($key, -2);
 					$class = 'bg-info';
-					$image = 'plugins/images/tabs/custom2.png';
-					if (!$this->config['homepageCustomHTMLtwoEnabled']) {
+					$image = 'plugins/images/tabs/HTML5.png';
+					if (!$this->config['homepageCustomHTML' . $iteration . 'Enabled']) {
 						$class .= ' faded';
 					}
 					break;
@@ -6463,11 +6596,11 @@ class Organizr
 		return '
 		<h3 lang="en">' . ucwords($app) . ' SOCKS API Connection</h3>
 		<p>Using this feature allows you to access the API without having to reverse proxy it.  Just access it from: </p>
-		<code>' . $this->getServerPath() . 'api/v2/socks/' . $app . '/</code>
+		<code class="elip hidden-xs">' . $this->getServerPath() . 'api/v2/socks/' . $app . '/</code>
 		<p>If you are using multiple URL\'s (using the csv method) you will have to use the url like these: </p>
-		<code>' . $this->getServerPath() . 'api/v2/multiple/socks/' . $app . '/1</code>
+		<code class="elip hidden-xs">' . $this->getServerPath() . 'api/v2/multiple/socks/' . $app . '/1</code>
 		<br/>
-		<code>' . $this->getServerPath() . 'api/v2/multiple/socks/' . $app . '/2</code>
+		<code class="elip hidden-xs">' . $this->getServerPath() . 'api/v2/multiple/socks/' . $app . '/2</code>
 		';
 	}
 	

+ 45 - 21
api/config/default.php

@@ -158,6 +158,17 @@ return array(
 	'qBittorrentSocksEnabled' => false,
 	'qBittorrentSocksAuth' => '999',
 	'rTorrentURL' => '',
+        'uTorrentCombine' => false,
+        'uTorrentCookie' => '',
+        'uTorrentDisableCertCheck' => false,
+        'uTorrentHideCompleted' => false,
+        'uTorrentHideSeeding' => false,
+        'uTorrentPassword' => '',
+        'uTorrentRefresh' => '60000',
+        'uTorrentToken' => '',
+        'uTorrentURL' => '',
+        'uTorrentUseCustomCertificate' => false,
+        'uTorrentUsername' => '',
 	'rTorrentURLOverride' => '',
 	'rTorrentUsername' => '',
 	'rTorrentPassword' => '',
@@ -170,17 +181,6 @@ return array(
 	'rTorrentUseCustomCertificate' => false,
 	'rTorrentLimit' => '200',
 	'rTorrentRefresh' => '60000',
-        'uTorrentURL' => '',
-        'uTorrentUsername' => '',
-        'uTorrentPassword' => '',
-        'uTorrentHideSeeding' => false,
-        'uTorrentHideCompleted' => false,
-        'uTorrentCombine' => false,
-        'uTorrentDisableCertCheck' => false,
-        'uTorrentUseCustomCertificate' => false,
-        'uTorrentRefresh' => '60000',
-        'uTorrentToken' => '',
-        'uTorrentCookie' => '',
 	'homepageJackettEnabled' => false,
 	'homepageJackettAuth' => '1',
 	'jackettURL' => '',
@@ -191,10 +191,30 @@ return array(
 	'homepageCalendarEnabled' => false,
 	'homepageCalendarAuth' => '4',
 	'calendariCal' => '',
-	'homepageCustomHTMLoneEnabled' => false,
-	'homepageCustomHTMLoneAuth' => '1',
-	'homepageCustomHTMLtwoEnabled' => false,
-	'homepageCustomHTMLtwoAuth' => '1',
+	'homepageCustomHTML01Enabled' => false,
+	'homepageCustomHTML01Auth' => '1',
+	'customHTML01' => '',
+	'homepageCustomHTML02Enabled' => false,
+	'homepageCustomHTML02Auth' => '1',
+	'customHTML02' => '',
+	'homepageCustomHTML03Enabled' => false,
+	'homepageCustomHTML03Auth' => '1',
+	'customHTML03' => '',
+	'homepageCustomHTML04Enabled' => false,
+	'homepageCustomHTML04Auth' => '1',
+	'customHTML04' => '',
+	'homepageCustomHTML05Enabled' => false,
+	'homepageCustomHTML05Auth' => '1',
+	'customHTML05' => '',
+	'homepageCustomHTML06Enabled' => false,
+	'homepageCustomHTML06Auth' => '1',
+	'customHTML06' => '',
+	'homepageCustomHTML07Enabled' => false,
+	'homepageCustomHTML07Auth' => '1',
+	'customHTML07' => '',
+	'homepageCustomHTML08Enabled' => false,
+	'homepageCustomHTML08Auth' => '1',
+	'customHTML08' => '',
 	'homepageDelugeEnabled' => false,
 	'homepageDelugeAuth' => '1',
 	'homepageJdownloaderEnabled' => false,
@@ -225,8 +245,8 @@ return array(
 	'homepageqBittorrentAuth' => '1',
 	'homepagerTorrentEnabled' => false,
 	'homepagerTorrentAuth' => '1',
-        'homepageuTorrentEnabled' => false,
         'homepageuTorrentAuth' => '1',
+        'homepageuTorrentEnabled' => true,
 	'homepageNzbgetEnabled' => false,
 	'homepageNzbgetAuth' => '1',
 	'homepagePlexEnabled' => false,
@@ -261,8 +281,8 @@ return array(
 	'homepageHealthChecksShowTags' => false,
 	'healthChecksDisableCertCheck' => false,
 	'healthChecksUseCustomCertificate' => false,
-	'homepageOrdercustomhtml' => '1',
-	'homepageOrdercustomhtmlTwo' => '2',
+	'homepageOrdercustomhtml01' => '1',
+	'homepageOrdercustomhtml02' => '2',
 	'homepageOrdertransmission' => '3',
 	'homepageOrderqBittorrent' => '4',
 	'homepageOrderdeluge' => '5',
@@ -292,7 +312,13 @@ return array(
 	'homepageOrderjellyfinnowplaying' => '29',
 	'homepageOrderjellyfinrecent' => '30',
 	'homepageOrderJackett' => '31',
-	'homepageOrderuTorrent' => '32',
+	'homepageOrdercustomhtml03' => '32',
+	'homepageOrdercustomhtml04' => '33',
+	'homepageOrdercustomhtml05' => '34',
+	'homepageOrdercustomhtml06' => '35',
+	'homepageOrdercustomhtml07' => '36',
+	'homepageOrdercustomhtml08' => '37',
+        'homepageOrderuTorrent' => '38',
 	'homepageShowStreamNames' => false,
 	'homepageShowStreamNamesAuth' => '1',
 	'homepageUseCustomStreamNames' => false,
@@ -331,8 +357,6 @@ return array(
 	'calendarLocale' => 'en',
 	'customCss' => '',
 	'customThemeCss' => '',
-	'customHTMLone' => '',
-	'customHTMLtwo' => '',
 	'mediaSearch' => false,
 	'mediaSearchType' => '',
 	'mediaSearchAuth' => '1',

+ 1 - 1
api/functions/auth-functions.php

@@ -482,7 +482,7 @@ trait AuthFunctions
 				if (is_array($json)) {
 					foreach ($json as $key => $value) { // Scan for this user
 						if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
-							if (strtolower($value['ConnectUserName']) == $username || strtolower($value['Name']) == $username) {
+							if (strtolower($value['ConnectUserName']) == strtolower($username) || strtolower($value['Name']) == strtolower($username)) {
 								$connectUserName = $value['ConnectUserName'];
 								$this->writeLog('success', 'Emby Connect Auth Function - Found User', $username);
 								break;

+ 13 - 2
api/functions/homepage-functions.php

@@ -2,6 +2,17 @@
 
 trait HomepageFunctions
 {
+	public function homepageCheckKeyPermissions($key, $permissions)
+	{
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
 	public function getHomepageSettingsList()
 	{
 		$methods = get_class_methods($this);
@@ -74,7 +85,7 @@ trait HomepageFunctions
 					if ($settingsType == 'string') {
 						if (empty($this->config[$setting])) {
 							if ($api) {
-								$this->setAPIResponse('error', $setting . 'was not supplied', 422);
+								$this->setAPIResponse('error', $setting . ' was not supplied', 422);
 							}
 							return false;
 						}
@@ -82,7 +93,7 @@ trait HomepageFunctions
 						foreach ($setting as $item) {
 							if (empty($this->config[$item])) {
 								if ($api) {
-									$this->setAPIResponse('error', $item . 'was not supplied', 422);
+									$this->setAPIResponse('error', $item . ' was not supplied', 422);
 								}
 								return false;
 							}

+ 2 - 2
api/functions/option-functions.php

@@ -20,7 +20,7 @@ trait OptionsFunction
 		$type = strtolower(str_replace('-', '', $type));
 		$setting = [
 			'name' => $name,
-			'value' => $this->config[$name]
+			'value' => $this->config[$name] ?? ''
 		];
 		switch ($type) {
 			case 'enable':
@@ -257,7 +257,7 @@ trait OptionsFunction
 					'type' => 'html',
 					'override' => 12,
 					'label' => 'Custom Code',
-					'html' => '<button type="button" class="hidden save' . $name . 'Textarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="' . $name . 'Editor" style="height:300px">' . htmlentities($this->config[$name]) . '</div>'
+					'html' => '<div id="' . $name . 'Editor" style="height:300px">' . htmlentities($this->config[$name]) . '</div>'
 				];
 				break;
 			// CALENDAR ITEMS

+ 52 - 3
api/functions/organizr-functions.php

@@ -20,14 +20,14 @@ trait OrganizrFunctions
 		];
 		$scripts = '';
 		foreach ($javaFiles as $file) {
-			$scripts .= '<script src="' . $file . '?v=' . $this->fileHash . '"></script>' . "\n";
+			$scripts .= '<script src="' . $file . '?v=' . trim($this->fileHash) . '"></script>' . "\n";
 		}
 		return $scripts;
 	}
 	
 	public function loadJavascriptFile($file)
 	{
-		return '<script>loadJavascript("' . $file . '?v=' . $this->fileHash . '");' . "</script>\n";
+		return '<script>loadJavascript("' . $file . '?v=' . trim($this->fileHash) . '");' . "</script>\n";
 	}
 	
 	public function embyJoinAPI($array)
@@ -489,7 +489,7 @@ trait OrganizrFunctions
 			ob_end_flush(); // Send the output to the browser
 			die();
 		} else {
-			die("Invalid Request");
+			die($this->showHTML('Invalid Request', 'No image returned'));
 		}
 	}
 	
@@ -734,4 +734,53 @@ trait OrganizrFunctions
 		}
 		return $options;
 	}
+	
+	public function showHTML(string $title = 'Organizr Alert', string $notice = '')
+	{
+		return
+			'<!DOCTYPE html>
+			<html lang="en">
+			<head>
+				<link rel="stylesheet" href="' . $this->getServerPath() . '/css/mvp.css">
+				<meta charset="utf-8">
+				<meta name="description" content="Trakt OAuth">
+				<meta name="viewport" content="width=device-width, initial-scale=1.0">
+				<title>' . $title . '</title>
+			</head>
+
+			<body>
+				<main>
+					<section>
+						<aside>
+							<h3>' . $title . '</h3>
+							<p>' . $notice . '</p>
+						</aside>
+					</section>
+				</main>
+			</body>
+			</html>';
+	}
+	
+	public function buildSettingsMenus($menuItems, $menuName)
+	{
+		$selectMenuItems = '';
+		$unorderedListMenuItems = '';
+		$menuNameLower = strtolower(str_replace(' ', '-', $menuName));
+		foreach ($menuItems as $menuItem) {
+			$anchorShort = str_replace('-anchor', '', $menuItem['anchor']);
+			$active = ($menuItem['active']) ? 'active' : '';
+			$apiPage = ($menuItem['api']) ? 'loadSettingsPage2(\'' . $menuItem['api'] . '\',\'#' . $anchorShort . '\',\'' . $menuItem['name'] . '\');' : '';
+			$onClick = (isset($menuItem['onclick'])) ? $menuItem['onclick'] : '';
+			$selectMenuItems .= '<option value="#' . $menuItem['anchor'] . '" lang="en">' . $menuItem['name'] . '</option>';
+			$unorderedListMenuItems .= '
+				<li onclick="changeSettingsMenu(\'Settings::' . $menuName . '::' . $menuItem['name'] . '\'); ' . $apiPage . $onClick . '" role="presentation" class="' . $active . '">
+					<a id="' . $menuItem['anchor'] . '" href="#' . $anchorShort . '" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true">
+						<span lang="en">' . $menuItem['name'] . '</span>
+					</a>
+			</li>';
+		}
+		$selectMenu = '<select class="form-control settings-dropdown-box ' . $menuNameLower . '-menu w-100 visible-xs">' . $selectMenuItems . '</select>';
+		$unorderedListMenu = '<ul class="nav customtab2 nav-tabs nav-non-mobile hidden-xs" data-dropdown="' . $menuNameLower . '-menu" role="tablist">' . $unorderedListMenuItems . '</ul>';
+		return $selectMenu . $unorderedListMenu;
+	}
 }

+ 1 - 1
api/functions/token-functions.php

@@ -25,7 +25,7 @@ trait TokenFunctions
 				$data->setAudience('Organizr');
 				if ($jwttoken->validate($data)) {
 					$result['valid'] = true;
-					//$result['username'] = $jwttoken->getClaim('username');
+					$result['username'] = ($jwttoken->hasClaim('username')) ? $jwttoken->getClaim('username') : 'N/A';
 					$result['group'] = ($jwttoken->hasClaim('group')) ? $jwttoken->getClaim('group') : 'N/A';
 					//$result['groupID'] = $jwttoken->getClaim('groupID');
 					$result['userID'] = $jwttoken->getClaim('userID');

+ 45 - 0
api/functions/upgrade-functions.php

@@ -10,6 +10,8 @@ trait UpgradeFunctions
 				$this->upgradeHomepageTabURL();
 			case '2.1.400':
 				$this->removeOldPluginDirectoriesAndFiles();
+			case '2.1.525':
+				$this->removeOldCustomHTML();
 			default:
 				$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
 				return true;
@@ -77,4 +79,47 @@ trait UpgradeFunctions
 		}
 		return true;
 	}
+	
+	public function checkForConfigKeyAddToArray($keys)
+	{
+		$updateItems = [];
+		foreach ($keys as $new => $old) {
+			if ($this->config[$old]) {
+				if ($this->config[$old] !== '') {
+					$updateItemsNew = [$new => $this->config[$old]];
+					$updateItems = array_merge($updateItems, $updateItemsNew);
+				}
+			}
+		}
+		return $updateItems;
+	}
+	
+	public function removeOldCustomHTML()
+	{
+		$backup = $this->backupOrganizr();
+		if ($backup) {
+			$keys = [
+				'homepageCustomHTML01Enabled' => 'homepageCustomHTMLoneEnabled',
+				'homepageCustomHTML01Auth' => 'homepageCustomHTMLoneAuth',
+				'customHTML01' => 'customHTMLone',
+				'homepageCustomHTML02Enabled' => 'homepageCustomHTMLtwoEnabled',
+				'homepageCustomHTML02Auth' => 'homepageCustomHTMLtwoAuth',
+				'customHTML02' => 'customHTMLtwo',
+			];
+			$updateItems = $this->checkForConfigKeyAddToArray($keys);
+			$updateComplete = false;
+			if (!empty($updateItems)) {
+				$updateComplete = $this->updateConfig($updateItems);
+			}
+			if ($updateComplete) {
+				$this->config = $this->config();
+				$removeConfigItems = $this->removeConfigItem(['homepageOrdercustomhtml', 'homepageOrdercustomhtmlTwo', 'homepageCustomHTMLoneEnabled', 'homepageCustomHTMLoneAuth', 'customHTMLone', 'homepageCustomHTMLtwoEnabled', 'homepageCustomHTMLtwoAuth', 'customHTMLtwo']);
+				if ($removeConfigItems) {
+					$this->config = $this->config();
+				}
+				return true;
+			}
+		}
+		return false;
+	}
 }

+ 1 - 7
api/homepage/calendar.php

@@ -52,13 +52,7 @@ trait CalendarHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdercalendar()

+ 1 - 7
api/homepage/couchpotato.php

@@ -59,13 +59,7 @@ trait CouchPotatoHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getCouchPotatoCalendar()

+ 5 - 11
api/homepage/deluge.php

@@ -66,8 +66,7 @@ trait DelugeHomepageItem
 	
 	public function testConnectionDeluge()
 	{
-		if (empty($this->config['delugeURL'])) {
-			$this->setAPIResponse('error', 'Deluge URL is not defined', 422);
+		if (!$this->homepageItemPermissions($this->delugeHomepagePermissions('main'), true)) {
 			return false;
 		}
 		try {
@@ -99,13 +98,7 @@ trait DelugeHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderdeluge()
@@ -133,7 +126,8 @@ trait DelugeHomepageItem
 			return false;
 		}
 		try {
-			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$options = $this->requestOptions($this->config['delugeURL'], $this->config['delugeRefresh'], $this->config['delugeDisableCertCheck'], $this->config['delugeUseCustomCertificate'], ['organizr_cert' => $this->getCert(), 'custom_cert' => $this->getCustomCert()]);
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']),$options);
 			$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
 			foreach ($torrents as $key => $value) {
 				$tempStatus = $this->delugeStatus($value->queue, $value->state, $value->progress);
@@ -168,4 +162,4 @@ trait DelugeHomepageItem
 		}
 		return ($state) ? $state : $status;
 	}
-}
+}

+ 14 - 13
api/homepage/emby.php

@@ -57,12 +57,7 @@ trait EmbyHomepageItem
 	
 	public function testConnectionEmby()
 	{
-		if (empty($this->config['embyURL'])) {
-			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
-			return false;
-		}
-		if (empty($this->config['embyToken'])) {
-			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+		if (!$this->homepageItemPermissions($this->embyHomepagePermissions('test'), true)) {
 			return false;
 		}
 		$url = $this->qualifyURL($this->config['embyURL']);
@@ -86,6 +81,18 @@ trait EmbyHomepageItem
 	public function embyHomepagePermissions($key = null)
 	{
 		$permissions = [
+			'test' => [
+				'enabled' => [
+					'homepageEmbyEnabled',
+				],
+				'auth' => [
+					'homepageEmbyAuth',
+				],
+				'not_empty' => [
+					'embyURL',
+					'embyToken'
+				]
+			],
 			'streams' => [
 				'enabled' => [
 					'homepageEmbyEnabled',
@@ -127,13 +134,7 @@ trait EmbyHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderembynowplaying()

+ 1 - 7
api/homepage/healthchecks.php

@@ -54,13 +54,7 @@ trait HealthChecksHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderhealthchecks()

+ 26 - 76
api/homepage/html.php

@@ -2,40 +2,17 @@
 
 trait HTMLHomepageItem
 {
-	public function htmlOneSettingsArray($infoOnly = false)
+	public function customHtmlNumber()
 	{
-		$homepageInformation = [
-			'name' => 'CustomHTML-1',
-			'enabled' => strpos('personal,business', $this->config['license']) !== false,
-			'image' => 'plugins/images/tabs/custom1.png',
-			'category' => 'Custom',
-			'settingsArray' => __FUNCTION__
-		];
-		if ($infoOnly) {
-			return $homepageInformation;
-		}
-		$homepageSettings = [
-			'debug' => true,
-			'settings' => [
-				'Enable' => [
-					$this->settingsOption('enable', 'homepageCustomHTMLoneEnabled'),
-					$this->settingsOption('auth', 'homepageCustomHTMLoneAuth'),
-				],
-				'Code' => [
-					$this->settingsOption('pre-code-editor', 'customHTMLone'),
-					$this->settingsOption('code-editor', 'customHTMLone'),
-				]
-			]
-		];
-		return array_merge($homepageInformation, $homepageSettings);
+		return 8;
 	}
 	
-	public function htmlTwoSettingsArray($infoOnly = false)
+	public function customHtmlSettingsArray($infoOnly = false)
 	{
 		$homepageInformation = [
-			'name' => 'CustomHTML-2',
+			'name' => 'CustomHTML',
 			'enabled' => strpos('personal,business', $this->config['license']) !== false,
-			'image' => 'plugins/images/tabs/custom2.png',
+			'image' => 'plugins/images/tabs/HTML5.png',
 			'category' => 'Custom',
 			'settingsArray' => __FUNCTION__
 		];
@@ -44,72 +21,45 @@ trait HTMLHomepageItem
 		}
 		$homepageSettings = [
 			'debug' => true,
-			'settings' => [
-				'Enable' => [
-					$this->settingsOption('enable', 'homepageCustomHTMLtwoEnabled'),
-					$this->settingsOption('auth', 'homepageCustomHTMLtwoAuth'),
-				],
-				'Code' => [
-					$this->settingsOption('pre-code-editor', 'customHTMLtwo'),
-					$this->settingsOption('code-editor', 'customHTMLtwo'),
-				]
-			]
+			'settings' => []
 		];
+		for ($i = 1; $i <= $this->customHtmlNumber(); $i++) {
+			$i = sprintf('%02d', $i);
+			$homepageSettings['settings']['Custom HTML ' . $i] = array(
+				$this->settingsOption('enable', 'homepageCustomHTML' . $i . 'Enabled'),
+				$this->settingsOption('auth', 'homepageCustomHTML' . $i . 'Auth'),
+				$this->settingsOption('pre-code-editor', 'customHTML' . $i),
+				$this->settingsOption('code-editor', 'customHTML' . $i, ['label' => 'Custom HTML Code']),
+			);
+		}
 		return array_merge($homepageInformation, $homepageSettings);
 	}
 	
 	public function htmlHomepagePermissions($key = null)
 	{
-		$permissions = [
-			'one' => [
-				'enabled' => [
-					'homepageCustomHTMLoneEnabled'
-				],
-				'auth' => [
-					'homepageCustomHTMLoneAuth'
-				],
-				'not_empty' => [
-					'customHTMLone'
-				]
-			],
-			'two' => [
+		for ($i = 1; $i <= $this->customHtmlNumber(); $i++) {
+			$i = sprintf('%02d', $i);
+			$permissions[$i] = [
 				'enabled' => [
-					'homepageCustomHTMLtwoEnabled'
+					'homepageCustomHTML' . $i . 'Enabled'
 				],
 				'auth' => [
-					'homepageCustomHTMLtwoAuth'
+					'homepageCustomHTML' . $i . 'Auth'
 				],
 				'not_empty' => [
-					'customHTMLtwo'
+					'customHTML' . $i
 				]
-			]
-		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
-	}
-	
-	public function homepageOrdercustomhtml()
-	{
-		if ($this->homepageItemPermissions($this->htmlHomepagePermissions('one'))) {
-			return '
-				<div id="' . __FUNCTION__ . '">
-					' . $this->config['customHTMLone'] . '
-				</div>
-				';
+			];
 		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
-	public function homepageOrdercustomhtmlTwo()
+	public function homepageOrdercustomhtml($key = '01')
 	{
-		if ($this->homepageItemPermissions($this->htmlHomepagePermissions('two'))) {
+		if ($this->homepageItemPermissions($this->htmlHomepagePermissions($key))) {
 			return '
 				<div id="' . __FUNCTION__ . '">
-					' . $this->config['customHTMLtwo'] . '
+					' . $this->config['customHTML' . $key] . '
 				</div>
 				';
 		}

+ 1 - 7
api/homepage/jackett.php

@@ -51,13 +51,7 @@ trait JackettHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderJackett()

+ 1 - 7
api/homepage/jdownloader.php

@@ -100,13 +100,7 @@ trait JDownloaderHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderjdownloader()

+ 1 - 7
api/homepage/jellyfin.php

@@ -133,13 +133,7 @@ trait JellyfinHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderjellyfinnowplaying()

+ 1 - 7
api/homepage/lidarr.php

@@ -130,13 +130,7 @@ trait LidarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getLidarrQueue()

+ 1 - 7
api/homepage/monitorr.php

@@ -53,13 +53,7 @@ trait MonitorrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderMonitorr()

+ 1 - 7
api/homepage/netdata.php

@@ -209,13 +209,7 @@ trait NetDataHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderNetdata()

+ 1 - 7
api/homepage/nzbget.php

@@ -92,13 +92,7 @@ trait NZBGetHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdernzbget()

+ 1 - 7
api/homepage/octoprint.php

@@ -52,13 +52,7 @@ trait OctoPrintHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderOctoprint()

+ 1 - 7
api/homepage/ombi.php

@@ -100,13 +100,7 @@ trait OmbiHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderombi()

+ 1 - 7
api/homepage/pihole.php

@@ -95,13 +95,7 @@ trait PiHoleHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderPihole()

+ 119 - 100
api/homepage/plex.php

@@ -183,13 +183,7 @@ trait PlexHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderplexnowplaying()
@@ -252,22 +246,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/status/sessions?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, $this->config['homepageStreamRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -283,35 +282,40 @@ trait PlexHomepageItem
 		$urls['movie'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=1";
 		$urls['tv'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=2";
 		$urls['music'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=8";
-		foreach ($urls as $k => $v) {
-			$options = $this->requestOptions($url, $this->config['homepageRecentRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-			$response = Requests::get($v, [], $options);
-			libxml_use_internal_errors(true);
-			if ($response->success) {
-				$items = array();
-				$plex = simplexml_load_string($response->body);
-				foreach ($plex as $child) {
-					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-						$items[] = $this->resolvePlexItem($child);
+		try {
+			foreach ($urls as $k => $v) {
+				$options = $this->requestOptions($url, $this->config['homepageRecentRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
+				$response = Requests::get($v, [], $options);
+				libxml_use_internal_errors(true);
+				if ($response->success) {
+					$items = array();
+					$plex = simplexml_load_string($response->body);
+					foreach ($plex as $child) {
+						if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+							$items[] = $this->resolvePlexItem($child);
+						}
+					}
+					if (isset($api)) {
+						$api['content'] = array_merge($api['content'], ($resolve) ? $items : $plex);
+					} else {
+						$api['content'] = ($resolve) ? $items : $plex;
 					}
-				}
-				if (isset($api)) {
-					$api['content'] = array_merge($api['content'], ($resolve) ? $items : $plex);
-				} else {
-					$api['content'] = ($resolve) ? $items : $plex;
 				}
 			}
+			if (isset($api['content'])) {
+				usort($api['content'], function ($a, $b) {
+					return $b['addedAt'] <=> $a['addedAt'];
+				});
+			}
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
-		if (isset($api['content'])) {
-			usort($api['content'], function ($a, $b) {
-				return $b['addedAt'] <=> $a['addedAt'];
-			});
-		}
-		$api['plexID'] = $this->config['plexID'];
-		$api['showNames'] = true;
-		$api['group'] = '1';
-		$this->setAPIResponse('success', null, 200, $api);
-		return $api;
 	}
 	
 	public function getPlexHomepagePlaylists()
@@ -322,35 +326,40 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/playlists?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
-					$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
-					$playlistURL = $this->qualifyURL($this->config['plexURL']);
-					$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $this->config['plexToken'];
-					$options = ($this->localURL($url)) ? array('verify' => false) : array();
-					$playlistResponse = Requests::get($playlistURL, array(), $options);
-					if ($playlistResponse->success) {
-						$playlistResponse = simplexml_load_string($playlistResponse->body);
-						$items[$playlistTitleClean]['title'] = (string)$child['title'];
-						foreach ($playlistResponse->Video as $playlistItem) {
-							$items[$playlistTitleClean][] = $this->resolvePlexItem($playlistItem);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
+						$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
+						$playlistURL = $this->qualifyURL($this->config['plexURL']);
+						$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $this->config['plexToken'];
+						$options = ($this->localURL($url)) ? array('verify' => false) : array();
+						$playlistResponse = Requests::get($playlistURL, array(), $options);
+						if ($playlistResponse->success) {
+							$playlistResponse = simplexml_load_string($playlistResponse->body);
+							$items[$playlistTitleClean]['title'] = (string)$child['title'];
+							foreach ($playlistResponse->Video as $playlistItem) {
+								$items[$playlistTitleClean][] = $this->resolvePlexItem($playlistItem);
+							}
 						}
 					}
 				}
+				$api['content'] = $items;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Plex API error', 500);
+				return false;
 			}
-			$api['content'] = $items;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
-		} else {
-			$this->setAPIResponse('error', 'Plex API error', 500);
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
 			return false;
 		}
 	}
@@ -370,22 +379,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -405,22 +419,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/search?query=" . rawurlencode($query) . "&X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -627,8 +646,8 @@ trait PlexHomepageItem
 		$url .= '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		$url .= '&cmd=get_users';
 		$options = $this->requestOptions($url, null, $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
 		try {
+			$response = Requests::get($url, [], $options);
 			$response = json_decode($response->body, true);
 			foreach ($response['response']['data'] as $user) {
 				if ($user['user_id'] != 0) {

+ 1 - 7
api/homepage/qbittorrent.php

@@ -117,13 +117,7 @@ trait QBitTorrentHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderqBittorrent()

+ 1 - 7
api/homepage/radarr.php

@@ -140,13 +140,7 @@ trait RadarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderRadarrQueue()

+ 2 - 8
api/homepage/rtorrent.php

@@ -126,13 +126,7 @@ trait RTorrentHomepageItem
 				'not_empty' => []
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderrTorrent()
@@ -195,7 +189,7 @@ trait RTorrentHomepageItem
 			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
 			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
 			$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
-			$options = $this->requestOptions($url, $this->config['rTorrentRefresh'], $this->config['rTorrentDisableCertCheck'], $this->config['rtorrentUseCustomCertificate']);
+			$options = $this->requestOptions($url, $this->config['rTorrentRefresh'], $this->config['rTorrentDisableCertCheck'], $this->config['rTorrentUseCustomCertificate']);
 			if ($this->config['rTorrentUsername'] !== '' && $this->decrypt($this->config['rTorrentPassword']) !== '') {
 				$credentials = array('auth' => new Requests_Auth_Digest(array($this->config['rTorrentUsername'], $this->decrypt($this->config['rTorrentPassword']))));
 				$options = array_merge($options, $credentials);

+ 1 - 7
api/homepage/sabnzbd.php

@@ -97,13 +97,7 @@ trait SabNZBdHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdersabnzbd()

+ 1 - 7
api/homepage/sickrage.php

@@ -108,13 +108,7 @@ trait SickRageHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getSickRageCalendar($startDate = null, $endDate = null)

+ 1 - 7
api/homepage/sonarr.php

@@ -141,13 +141,7 @@ trait SonarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderSonarrQueue()

+ 1 - 7
api/homepage/speedtest.php

@@ -51,13 +51,7 @@ trait SpeedTestHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderSpeedtest()

+ 1 - 7
api/homepage/tautulli.php

@@ -110,13 +110,7 @@ trait TautulliHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdertautulli()

+ 2 - 8
api/homepage/trakt.php

@@ -30,7 +30,7 @@ trait TraktHomepageItem
 										<p lang="en">In order for this item to be setup, you need to goto the following URL to create a new API app.</p>
 										<p><a href="https://trakt.tv/oauth/applications/new" target="_blank">New API App</a></p>
 										<p lang="en">Enter anything for Name and Description.  You can leave Javascript and Permissions blank.  The only info you have to enter is for Redirect URI.  Enter the following URL:</p>
-										<code>' . $this->getServerPath() . 'api/v2/oauth/trakt</code>
+										<code class="elip hidden-xs">' . $this->getServerPath() . 'api/v2/oauth/trakt</code>
 									</div>
 								</div>
 							</div>'
@@ -77,13 +77,7 @@ trait TraktHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getTraktCalendar($startDate = null)

+ 1 - 7
api/homepage/transmission.php

@@ -105,13 +105,7 @@ trait TransmissionHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdertransmission()

+ 1 - 7
api/homepage/unifi.php

@@ -60,13 +60,7 @@ trait UnifiHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderunifi()

+ 1 - 7
api/homepage/weather.php

@@ -57,13 +57,7 @@ trait WeatherHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderWeatherAndAir()

+ 6 - 6
api/pages/settings-tab-editor-categories.php

@@ -67,15 +67,15 @@ function get_page_settings_tab_editor_categories($Organizr)
 	return '
 <script>
 buildCategoryEditor();
-$( \'#categoryEditorTable\' ).sortable({
-	stop: function () {
-		var inputs = $(\'input.order\');
-		var nbElems = inputs.length;
-		inputs.each(function(idx) {
+
+let el = document.getElementById(\'categoryEditorTable\');
+let sortable = new Sortable(el, {
+	onUpdate: function (evt) {
+		$(\'input.order\').each(function(idx) {
 			$(this).val(idx + 1);
 		});
 		submitCategoryOrder();
-	}
+	},
 });
 ' . $iconSelectors . '
 </script>

+ 18 - 4
api/pages/settings-tab-editor-tabs.php

@@ -67,8 +67,9 @@ function get_page_settings_tab_editor_tabs($Organizr)
 	return '
 	<script>
 	buildTabEditor();
-	!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
-	$( \'#tabEditorTable\' ).sortable({
+	//!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
+	/*$( \'#tabEditorTable\' ).sortable({
+		handle: "td:first",
 		stop: function () {
 			$(\'input.order\').each(function(idx) {
 				$(this).val(idx + 1);
@@ -78,8 +79,20 @@ function get_page_settings_tab_editor_tabs($Organizr)
 			$(\'.saveTabOrderButton\').removeClass(\'hidden\');
 			//submitTabOrder(newTabs);
 		}
+	});*/
+	let el = document.getElementById(\'tabEditorTable\');
+	let sortable = new Sortable(el, {
+		handle: ".sort-tabs-handle",
+		onUpdate: function (evt) {
+			$(\'input.order\').each(function(idx) {
+				$(this).val(idx + 1);
+			});
+			var newTabs = $( "#submit-tabs-form" ).serializeToJSON();
+			newTabsGlobal = newTabs;
+			$(\'.saveTabOrderButton\').removeClass(\'hidden\');
+		},
 	});
-	$( \'#tabEditorTable\' ).disableSelection();
+	//$( \'#tabEditorTable\' ).disableSelection();
 	' . $iconSelectors . '
 	</script>
 	<div class="panel bg-org panel-info">
@@ -94,7 +107,8 @@ function get_page_settings_tab_editor_tabs($Organizr)
 				<table class="table table-hover manage-u-table">
 					<thead>
 						<tr>
-							<th width="70" class="text-center">#</th>
+							<th width="20" class="text-center"></th>
+							<th width="70" class="text-center"></th>
 							<th lang="en">NAME</th>
 							<th lang="en">CATEGORY</th>
 							<th lang="en">GROUP</th>

+ 311 - 345
api/pages/settings.php

@@ -12,198 +12,164 @@ function get_page_settings($Organizr)
 		return false;
 	}
 	$Organizr->writeLog('success', 'Admin Function -  Accessed Settings Page', $Organizr->user['username']);
-	return $Organizr->pluginFiles('js', true) . '
+	$systemMenus = $Organizr->systemMenuLists();
+	return $Organizr->pluginFiles('js', true) . $Organizr->loadJavascriptFile('js/Sortable.min.js') . '
 <script>
-    (function() {
-        updateCheck();
-        authDebugCheck();
-        sponsorLoad();
-        newsLoad();
-        checkCommitLoad();
-        backersLoad();
-        [].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
-            new CBPFWTabs(el);
-        });
-    })();
+	(function() {
+		updateCheck();
+		authDebugCheck();
+		sponsorLoad();
+		newsLoad();
+		checkCommitLoad();
+		backersLoad();
+		[].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
+			new CBPFWTabs(el);
+		});
+	})();
 </script>
 <div class="container-fluid">
-    <div class="row bg-title">
-        <div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
-            <h4 class="page-title" lang="en">Organizr Settings</h4>
-        </div>
-        <div class="col-lg-9 col-sm-8 col-md-8 col-xs-12">
-            <ol id="settingsBreadcrumb" class="breadcrumb">
-                <li lang="en">Settings</li>
-                <li lang="en">Tab Editor</li>
-            </ol>
-        </div>
-        <!-- /.col-lg-12 -->
-    </div>
-    <!--.row-->
-    <div class="row">
-        <!-- Tab style start -->
-        <section class="">
-            <div class="sttabs sttabs-main-settings-div tabs-style-flip">
-                <nav>
-                    <ul>
-                        <li onclick="changeSettingsMenu(\'Settings::Tab Editor\')" id="settings-main-tab-editor-anchor"><a href="#settings-main-tab-editor" class="sticon ti-layout-tab-v"><span lang="en">Tab Editor</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::Customize\')" id="settings-main-customize-anchor"><a href="#settings-main-customize" class="sticon ti-paint-bucket"><span lang="en">Customize</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::User Management\')" id="settings-main-user-management-anchor"><a href="#settings-main-user-management" class="sticon ti-user"><span lang="en">User Management</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::Image Manager\');loadSettingsPage2(\'api/v2/page/settings_image_manager\',\'#settings-image-manager-view\',\'Image Viewer\');" id="settings-main-image-manager-anchor"><a href="#settings-main-image-manager" class="sticon ti-image"><span lang="en">Image Manager</span></a></li>
-    					<li onclick="changeSettingsMenu(\'Settings::Plugins\');loadSettingsPage2(\'api/v2/page/settings_plugins\',\'#settings-main-plugins\',\'Plugins\');" id="settings-main-plugins-anchor"><a href="#settings-main-plugins" class="sticon ti-plug"><span lang="en">Plugins</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::System Settings\');authDebugCheck();" id="settings-main-system-settings-anchor"><a href="#settings-main-system-settings" class="sticon ti-settings"><span lang="en">System Settings</span></a></li>
-                    </ul>
-                </nav>
-                <div class="content-wrap">
-                    <! -- TAB EDITOR -->
-                    <section id="settings-main-tab-editor">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Tabs\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_tabs\',\'#settings-tab-editor-tabs\',\'Tab Editor\');" role="presentation" class=""><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Tabs</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Categories\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_categories\',\'#settings-tab-editor-categories\',\'Category Editor\');" role="presentation" class=""><a id="settings-tab-editor-categories-anchor" href="#settings-tab-editor-categories" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-layout-list-thumb"></i></span><span class="hidden-xs" lang="en">Categories</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Homepage Items\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_homepage\',\'#settings-tab-editor-homepage\',\'Homepage Items\');" role="presentation" class=""><a id="settings-tab-editor-homepage-anchor" href="#settings-tab-editor-homepage" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-home"></i></span><span class="hidden-xs" lang="en">Homepage Items</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Homepage Order\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_homepage_order\',\'#settings-tab-editor-homepage-order\',\'Homepage Order\');" role="presentation" class=""><a id="settings-tab-editor-homepage-order-anchor" href="#settings-tab-editor-homepage-order" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-exchange-vertical"></i></span><span class="hidden-xs" lang="en">Homepage Order</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-tabs">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-categories">
-                                <h2 lang="en">Loading...</h2>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage">
-                                <h2 lang="en">Loading...</h2>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage-order">
-                                <h2 lang="en">Coming Soon...</h2>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- Customize -->
-                    <section id="settings-main-customize">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Customize::Appearance\');loadSettingsPage2(\'api/v2/page/settings_customize_appearance\',\'#settings-customize-appearance\',\'Customize Appearance\');" role="presentation" class=""><a id="settings-customize-appearance-anchor" href="#settings-customize-appearance" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-eye"></i></span><span class="hidden-xs" lang="en">Appearance</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Customize::Marketplace\');loadMarketplace(\'themes\');" role="presentation" class=""><a id="settings-customize-marketplace-anchor" href="#settings-customize-marketplace" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-shopping-cart-full"></i></span><span class="hidden-xs" lang="en">Marketplace</span></a></li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-customize-appearance">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-customize-marketplace">
+	<div class="row bg-title">
+		<div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
+			<h4 class="page-title" lang="en">Organizr Settings</h4>
+		</div>
+		<div class="col-lg-9 col-sm-8 col-md-8 col-xs-12">
+			<ol id="settingsBreadcrumb" class="breadcrumb">
+				<li lang="en">Settings</li>
+				<li lang="en">Tab Editor</li>
+			</ol>
+		</div>
+		<!-- /.col-lg-12 -->
+	</div>
+	<!--.row-->
+	<div class="row">
+		<!-- Tab style start -->
+		<section class="">
+			<div class="sttabs sttabs-main-settings-div tabs-style-flip">
+				<nav>
+					<ul>
+						<li onclick="changeSettingsMenu(\'Settings::Tab Editor\')" id="settings-main-tab-editor-anchor"><a href="#settings-main-tab-editor" class="sticon ti-layout-tab-v"><span lang="en">Tab Editor</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Customize\')" id="settings-main-customize-anchor"><a href="#settings-main-customize" class="sticon ti-paint-bucket"><span lang="en">Customize</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::User Management\')" id="settings-main-user-management-anchor"><a href="#settings-main-user-management" class="sticon ti-user"><span lang="en">User Management</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Image Manager\');loadSettingsPage2(\'api/v2/page/settings_image_manager\',\'#settings-image-manager-view\',\'Image Viewer\');" id="settings-main-image-manager-anchor"><a href="#settings-main-image-manager" class="sticon ti-image"><span lang="en">Image Manager</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Plugins\');loadSettingsPage2(\'api/v2/page/settings_plugins\',\'#settings-main-plugins\',\'Plugins\');" id="settings-main-plugins-anchor"><a href="#settings-main-plugins" class="sticon ti-plug"><span lang="en">Plugins</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::System Settings\');authDebugCheck();" id="settings-main-system-settings-anchor"><a href="#settings-main-system-settings" class="sticon ti-settings"><span lang="en">System Settings</span></a></li>
+					</ul>
+				</nav>
+				<div class="content-wrap">
+					<! -- TAB EDITOR -->
+					<section id="settings-main-tab-editor">
+						' . $systemMenus['tab_editor'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-tabs">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-categories">
+								<h2 lang="en">Loading...</h2>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage">
+								<h2 lang="en">Loading...</h2>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage-order">
+								<h2 lang="en">Loading...</h2>
+							</div>
+						</div>
+					</section>
+					<! -- Customize -->
+					<section id="settings-main-customize">
+						' . $systemMenus['customize'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-customize-appearance">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-customize-marketplace">
 								<div class="panel bg-org panel-info">
 									<div class="panel-heading">
 										<span lang="en">Theme Marketplace</span>
 									</div>
 									<div class="panel-wrapper collapse in" aria-expanded="true">
 										<div class="table-responsive">
-					                        <table class="table table-hover manage-u-table">
-					                            <thead>
-					                                <tr>
-					                                    <th width="70" class="text-center" lang="en">THEME</th>
-					                                    <th></th>
-					                                    <th lang="en">CATEGORY</th>
-					                                    <th lang="en">STATUS</th>
-					                                    <th lang="en" style="text-align:center">INFO</th>
-					                                    <th lang="en" style="text-align:center">INSTALL</th>
-					                                    <th lang="en" style="text-align:center">DELETE</th>
-					                                </tr>
-					                            </thead>
-					                            <tbody id="manageThemeTable"></tbody>
-					                        </table>
-					                    </div>
+											<table class="table table-hover manage-u-table">
+												<thead>
+													<tr>
+														<th width="70" class="text-center" lang="en">THEME</th>
+														<th></th>
+														<th lang="en">CATEGORY</th>
+														<th lang="en">STATUS</th>
+														<th lang="en" style="text-align:center">INFO</th>
+														<th lang="en" style="text-align:center">INSTALL</th>
+														<th lang="en" style="text-align:center">DELETE</th>
+													</tr>
+												</thead>
+												<tbody id="manageThemeTable"></tbody>
+											</table>
+										</div>
 									</div>
 								</div>
 							</div>
-                        </div>
-                    </section>
-                    <! -- USER MANAGEMENT -->
-                    <section id="settings-main-user-management">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Manage Users\');loadSettingsPage2(\'api/v2/page/settings_user_manage_users\',\'#settings-user-manage-users\',\'User Management\');" role="presentation" class=""><a id="settings-user-manage-users-anchor" href="#settings-user-manage-users" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-id-badge"></i></span><span class="hidden-xs" lang="en">Users</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Manage Groups\');loadSettingsPage2(\'api/v2/page/settings_user_manage_groups\',\'#settings-user-manage-groups\',\'Group Management\');" role="presentation" class=""><a id="settings-user-manage-groups-anchor" href="#settings-user-manage-groups" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-briefcase"></i></span><span class="hidden-xs" lang="en">Groups</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Import Users\');" role="presentation" class=""><a id="settings-user-import-users-anchor" href="#settings-user-import-users" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-import"></i></span><span class="hidden-xs" lang="en">Import</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-manage-users">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-manage-groups">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-import-users">
-                                ' . $Organizr->importUserButtons() . '
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- IMAGE MANAGER -->
-                    <section id="settings-main-image-manager">
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade active in" id="settings-image-manager-view">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- PLUGINS -->
-                    <section id="settings-main-plugins">
-                        <h2 lang="en">Plugins</h2>
-                    </section>
-                    <! -- SYSTEM SETTINGS -->
-                    <section id="settings-main-system-settings">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::About\')" role="presentation" class="active"><a id="settings-settings-about-anchor" href="#settings-settings-about" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-info-alt"></i></span><span class="hidden-xs" lang="en">About</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Main\');loadSettingsPage2(\'api/v2/page/settings_settings_main\',\'#settings-settings-main\',\'Main Settings\');" role="presentation" class=""><a id="settings-settings-main-anchor" href="#settings-settings-main" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-settings"></i></span><span class="hidden-xs" lang="en">Main</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::SSO\');loadSettingsPage2(\'api/v2/page/settings_settings_sso\',\'#settings-settings-sso\',\'SSO\');" role="presentation" class=""><a id="settings-settings-sso-anchor" href="#settings-settings-sso" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-key"></i></span><span class="hidden-xs" lang="en">Single Sign-On</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Logs\');loadSettingsPage2(\'api/v2/page/settings_settings_logs\',\'#settings-settings-logs\',\'Log Viewer\');" role="presentation" class=""><a id="settings-settings-logs-anchor" href="#settings-settings-logs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-receipt"></i></span><span class="hidden-xs" lang="en">Logs</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Updates\')" role="presentation" class=""><a id="update-button" href="#settings-settings-updates" aria-controls="profile" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-package"></i></span> <span class="hidden-xs" lang="en">Updates</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Backup\');loadSettingsPage2(\'api/v2/page/settings_settings_backup\',\'#settings-settings-backup\',\'Backup\');" role="presentation" class=""><a id="settings-settings-backup-anchor" href="#settings-settings-backup" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-export"></i></span><span class="hidden-xs" lang="en">Backup</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Donate\')" role="presentation" class=""><a id="settings-settings-donate-anchor" href="#settings-settings-donate" aria-controls="profile" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-money"></i></span> <span class="hidden-xs" lang="en">Donate</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-main">
-                                <h2 lang="en">Main Settings</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-sso">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-logs">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-backup">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade active in" id="settings-settings-about">
-                            	<div class="row">
-	                                <div class="col-lg-12">
-							            <div class="panel panel-default">
+						</div>
+					</section>
+					<! -- USER MANAGEMENT -->
+					<section id="settings-main-user-management">
+						' . $systemMenus['user_management'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-manage-users">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-manage-groups">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-import-users">
+								' . $Organizr->importUserButtons() . '
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+					<! -- IMAGE MANAGER -->
+					<section id="settings-main-image-manager">
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade active in" id="settings-image-manager-view">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+					<! -- PLUGINS -->
+					<section id="settings-main-plugins">
+						<h2 lang="en">Plugins</h2>
+					</section>
+					<! -- SYSTEM SETTINGS -->
+					<section id="settings-main-system-settings">
+					' . $systemMenus['system_settings'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-main">
+								<h2 lang="en">Main Settings</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-sso">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-logs">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-backup">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade active in" id="settings-settings-about">
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5">
 													<img class="lazyload loginTitle" data-src="plugins/images/organizr/logo-no-border.png"> &nbsp;
@@ -211,182 +177,182 @@ function get_page_settings($Organizr)
 												</span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							        			<div id="organizrNewsPanel"></div>
-							                </div>
-							            </div>
-							        </div>
-    							</div>
-    							<div class="row">
-    								<div class="col-lg-6 col-sm-12 col-md-6">
-    									<div class="panel bg-org">
-    										<div class="p-30">
-    											<div class="row">
-    												<div class="col-xs-12"><img src="plugins/images/organizr/logo-wide.png" alt="organizr" class="img-responsive"></div>
-    											</div>
-    										</div>
-    										<hr class="m-t-10">
-    										<div class="p-20 text-center">
-    											<p lang="en">Below you will find all the links for everything that has to do with Organizr</p>
-    										</div>
-    										<hr>
-    										<ul class="dp-table profile-social-icons">
-    											<li><a href="https://organizr.app" target="_blank"><i class="mdi mdi-web mdi-24px"></i></a></li>
-    											<li><a href="https://reddit.com/r/organizr" target="_blank"><i class="mdi mdi-reddit mdi-24px"></i></a></li>
-    											<li><a href="https://organizr.app/discord" target="_blank"><i class="mdi mdi-discord mdi-24px"></i></a></li>
-    											<li><a href="https://github.com/causefx/organizr" target="_blank"><i class="mdi mdi-github-box mdi-24px"></i></a></li>
-    										</ul>
-    										<hr>
-    										<a href="https://poeditor.com/join/project/T6l68hksTE" target="_blank">
-		                                        <div class="white-box bg-org">
-		                                            <h4 lang="en">Want to help translate?</h4>
-		                                            <p lang="en">Head on over to POEditor and help us translate Organizr into your language</p>
-		                                            <p lang="en">I will try and import new strings every Friday</p>
-		                                        </div>
-		                                    </a>
-    										
-    									</div>
-    								</div>
-                                    <div class="col-lg-6 col-sm-12 col-md-6">
-                                        <div class="white-box bg-org">
-                                            <h3 class="box-title" lang="en">Information</h3>
-                                            <ul class="feeds">
-                                                <li><div class="bg-info"><i class="mdi mdi-webpack mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Version</span> ' . $Organizr->version . '</li>
-                                                <li><div class="bg-info"><i class="mdi mdi-github-box mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Branch</span><a href="https://github.com/causefx/Organizr/commits/' . $Organizr->config['branch'] . '" target="_blank"> ' . $Organizr->config['branch'] . '</a></li>
-                                                <li><div class="bg-info"><i class="mdi mdi-database mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Database Location</span> ' . $Organizr->config['dbLocation'] . $Organizr->config['dbName'] . '</li>
-                                                ' . $Organizr->settingsDocker() . $Organizr->settingsPathChecks() . '
-                                                <hr class="m-t-10">
-                                                <li><div class="bg-info"><i class="mdi mdi-language-php mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">PHP Version</span> ' . phpversion() . '</li>
-                                                <li><div class="bg-info"><i class="mdi mdi-package-variant-closed mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Webserver Version</span> ' . $_SERVER['SERVER_SOFTWARE'] . '</li>
-                                                <hr class="m-t-10">
-                                                <li><div class="bg-info"><i class="mdi mdi-account-card-details mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">License</span> ' . ucwords($Organizr->config['license']) . '</li>
-                                            </ul>
-                                        </div>
-                                    </div>
-    							</div>
-    							<div class="row">
-	                                <div class="col-lg-12">
-							            <div class="panel panel-default">
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="organizrNewsPanel"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-6 col-sm-12 col-md-6">
+										<div class="panel bg-org">
+											<div class="p-30">
+												<div class="row">
+													<div class="col-xs-12"><img src="plugins/images/organizr/logo-wide.png" alt="organizr" class="img-responsive"></div>
+												</div>
+											</div>
+											<hr class="m-t-10">
+											<div class="p-20 text-center">
+												<p lang="en">Below you will find all the links for everything that has to do with Organizr</p>
+											</div>
+											<hr>
+											<ul class="dp-table profile-social-icons">
+												<li><a href="https://organizr.app" target="_blank"><i class="mdi mdi-web mdi-24px"></i></a></li>
+												<li><a href="https://reddit.com/r/organizr" target="_blank"><i class="mdi mdi-reddit mdi-24px"></i></a></li>
+												<li><a href="https://organizr.app/discord" target="_blank"><i class="mdi mdi-discord mdi-24px"></i></a></li>
+												<li><a href="https://github.com/causefx/organizr" target="_blank"><i class="mdi mdi-github-box mdi-24px"></i></a></li>
+											</ul>
+											<hr>
+											<a href="https://poeditor.com/join/project/T6l68hksTE" target="_blank">
+												<div class="white-box bg-org">
+													<h4 lang="en">Want to help translate?</h4>
+													<p lang="en">Head on over to POEditor and help us translate Organizr into your language</p>
+													<p lang="en">I will try and import new strings every Friday</p>
+												</div>
+											</a>
+											
+										</div>
+									</div>
+									<div class="col-lg-6 col-sm-12 col-md-6">
+										<div class="white-box bg-org">
+											<h3 class="box-title" lang="en">Information</h3>
+											<ul class="feeds">
+												<li><div class="bg-info"><i class="mdi mdi-webpack mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Version</span> ' . $Organizr->version . '</li>
+												<li><div class="bg-info"><i class="mdi mdi-github-box mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Branch</span><a href="https://github.com/causefx/Organizr/commits/' . $Organizr->config['branch'] . '" target="_blank"> ' . $Organizr->config['branch'] . '</a></li>
+												<li><div class="bg-info"><i class="mdi mdi-database mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Database Location</span> ' . $Organizr->config['dbLocation'] . $Organizr->config['dbName'] . '</li>
+												' . $Organizr->settingsDocker() . $Organizr->settingsPathChecks() . '
+												<hr class="m-t-10">
+												<li><div class="bg-info"><i class="mdi mdi-language-php mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">PHP Version</span> ' . phpversion() . '</li>
+												<li><div class="bg-info"><i class="mdi mdi-package-variant-closed mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Webserver Version</span> ' . $_SERVER['SERVER_SOFTWARE'] . '</li>
+												<hr class="m-t-10">
+												<li><div class="bg-info"><i class="mdi mdi-account-card-details mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">License</span> ' . ucwords($Organizr->config['license']) . '</li>
+											</ul>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5"><span lang="en">Sponsors</span></span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							                	<div id="sponsorList" class="owl-carousel owl-theme sponsor-items"></div>
-							        			<div id="sponsorListModals"></div>
-							                </div>
-							            </div>
-							        </div>
-    							</div>
-    							<div class="row">
-    							    <div class="col-lg-12">
-    							        <div class="panel panel-default">
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="sponsorList" class="owl-carousel owl-theme sponsor-items"></div>
+												<div id="sponsorListModals"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5"><span lang="en">Backers</span></span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							                	<div id="backersList" class="owl-carousel owl-theme backers-items"></div>
-							                </div>
-							            </div>
-                                    </div>
-    							</div>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-donate">
-                                <div class="col-lg-12">
-                                    <div class="white-box bg-org">
-                                        <ul class="nav nav-tabs tabs customtab">
-                                            <li class="tab active">
-                                                <a href="#donate-github" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-github text-warning"></i></span> <span class="hidden-xs" lang="en">Github Sponsor</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-paypal" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-paypal text-info"></i></span> <span class="hidden-xs" lang="en">PayPal</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-square" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-square-inc-cash mdi-18px text-success"></i></span> <span class="hidden-xs" lang="en">Square Cash</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-crypto" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-coin mdi-18px text-info"></i></span> <span class="hidden-xs" lang="en">Cryptos</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-patreon" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-account-multiple mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Patreon</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-open-collective" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa fa-circle-o-notch text-primary"></i></span> <span class="hidden-xs" lang="en">Open Collective</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-ads" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-google mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Google Ads</span> </a>
-                                            </li>
-                                        </ul>
-                                        <div class="tab-content">
-                                        	<div class="tab-pane active" id="donate-github">
-                                                <blockquote>Want to show support on Github?  Sponsor me :)<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://github.com/sponsors/causefx\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-paypal">
-                                                <blockquote>I have chosen to go with PayPal Pools so everyone can see how much people have donated.<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://paypal.me/pools/c/83JNaMBESR\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-square">
-                                                <blockquote>If you use the Square Cash App, you can donate with that if you like.<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://cash.me/$CauseFX\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-crypto">
-                                                <blockquote>Want to donate a small amount of Crypto?.<br/>Please use the QR Code or Wallet ID.</blockquote>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Bitcoin_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>18dNtPKgor6pV5DJhYNqFxLJJ2BKugo4K9</code>
-                                                </div>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Litecoin_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>LejRxt8huhFGpVrp7TM43VSstrzKGxf8Cj</code>
-                                                </div>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Ethereum_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>0x605b678761af62C02Fe0fA86A99053D666dF5d6f</code>
-                                                </div>
-                                                <div class="clearfix"></div>
-                                            </div>
-                                            <div class="tab-pane" id="donate-patreon">
-                                                <blockquote>Need specialized support or just want to support Organizr?  If so head to Patreon...<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://www.patreon.com/join/organizr?\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-open-collective">
-                                                <blockquote>Need specialized support or just want to support Organizr?  If so head to Open Collective...<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://opencollective.com/organizr\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-ads">
-                                                <blockquote>Money not an option?  No problem.  Show some love to this Google Ad below:</blockquote>
-                                                 <button onclick="window.open(\'https://organizr.app/ads/google.html\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-updates">
-                                <div id="githubVersions"></div>
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                </div>
-                <!-- /content -->
-            </div>
-            <!-- /tabs -->
-        </section>
-    </div>
-    <!--./row-->
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="backersList" class="owl-carousel owl-theme backers-items"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-donate">
+								<div class="col-lg-12">
+									<div class="white-box bg-org">
+										<ul class="nav nav-tabs tabs customtab">
+											<li class="tab active">
+												<a href="#donate-github" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-github text-warning"></i></span> <span class="hidden-xs" lang="en">Github Sponsor</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-paypal" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-paypal text-info"></i></span> <span class="hidden-xs" lang="en">PayPal</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-square" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-square-inc-cash mdi-18px text-success"></i></span> <span class="hidden-xs" lang="en">Square Cash</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-crypto" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-coin mdi-18px text-info"></i></span> <span class="hidden-xs" lang="en">Cryptos</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-patreon" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-account-multiple mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Patreon</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-open-collective" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa fa-circle-o-notch text-primary"></i></span> <span class="hidden-xs" lang="en">Open Collective</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-ads" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-google mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Google Ads</span> </a>
+											</li>
+										</ul>
+										<div class="tab-content">
+											<div class="tab-pane active" id="donate-github">
+												<blockquote>Want to show support on Github?  Sponsor me :)<br/>Please click the button to continue.</blockquote>
+												<button onclick="window.open(\'https://github.com/sponsors/causefx\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-paypal">
+												<blockquote>I have chosen to go with PayPal Pools so everyone can see how much people have donated.<br/>Please click the button to continue.</blockquote>
+												<button onclick="window.open(\'https://paypal.me/pools/c/83JNaMBESR\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-square">
+												<blockquote>If you use the Square Cash App, you can donate with that if you like.<br/>Please click the button to continue.</blockquote>
+												<button onclick="window.open(\'https://cash.me/$CauseFX\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-crypto">
+												<blockquote>Want to donate a small amount of Crypto?.<br/>Please use the QR Code or Wallet ID.</blockquote>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Bitcoin_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>18dNtPKgor6pV5DJhYNqFxLJJ2BKugo4K9</code>
+												</div>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Litecoin_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>LejRxt8huhFGpVrp7TM43VSstrzKGxf8Cj</code>
+												</div>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Ethereum_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>0x605b678761af62C02Fe0fA86A99053D666dF5d6f</code>
+												</div>
+												<div class="clearfix"></div>
+											</div>
+											<div class="tab-pane" id="donate-patreon">
+												<blockquote>Need specialized support or just want to support Organizr?  If so head to Patreon...<br/>Please click the button to continue.</blockquote>
+												<button onclick="window.open(\'https://www.patreon.com/join/organizr?\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-open-collective">
+												<blockquote>Need specialized support or just want to support Organizr?  If so head to Open Collective...<br/>Please click the button to continue.</blockquote>
+												<button onclick="window.open(\'https://opencollective.com/organizr\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-ads">
+												<blockquote>Money not an option?  No problem.  Show some love to this Google Ad below:</blockquote>
+												 <button onclick="window.open(\'https://organizr.app/ads/google.html\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-updates">
+								<div id="githubVersions"></div>
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+				</div>
+				<!-- /content -->
+			</div>
+			<!-- /tabs -->
+		</section>
+	</div>
+	<!--./row-->
 </div>
 <!-- /.container-fluid -->
 <form id="about-theme-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h2 id="about-theme-title">Loading...</h2>
-    <div class="clearfix"></div>
-    <div id="about-theme-body" class=""></div>
+	<h2 id="about-theme-title">Loading...</h2>
+	<div class="clearfix"></div>
+	<div id="about-theme-body" class=""></div>
 </form>
 <div id="editHomepageItemDiv"><div id="editHomepageItem" class=""></div></div>
 ';

+ 28 - 7
api/plugins/bookmark/settings.js

@@ -51,8 +51,18 @@ function bookmarkLaunch(){
 
 // TAB MANAGEMENT
 function bookmarkTabsLaunch(){
-	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Tabs');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_tabs','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Tabs</span></a></li>`;
-	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+	var menuList = `<li class="bookmarkTabsMenu-added" onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Tabs');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_tabs','#settings-tab-editor-bookmark-tabs','Bookmark Tab Editor');" role="presentation"><a id="settings-tab-editor-bookmark-tabs-anchor" href="#settings-tab-editor-bookmark-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Tabs</span></a></li>`;
+	let menuListAlt = `<option value="#settings-tab-editor-bookmark-tabs-anchor" lang="en">Bookmark Tabs</option>`;
+	let div = `
+	<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-bookmark-tabs">
+		<h2 lang="en">Loading...</h2>
+	</div>
+	`;
+	if($('.bookmarkCategoryMenu-added').length === 0 ){
+		$('#settings-main-tab-editor .nav-tabs').append(menuList);
+		$('.settings-dropdown-box.tab-editor-menu').append(menuListAlt);
+		$('#settings-main-tab-editor .tab-content').append(div);
+	}
 }
 
 function getColorPickerOptionsWithCallback(func){
@@ -68,7 +78,7 @@ function buildBookmarkTabEditor(){
 			organizrCatchError(e,data);
 		}
 		$('#bookmarkTabEditorTable').html(buildBookmarkTabEditorItem(response.data));
-		
+
 		// initialize color pickers only first time
 		if(!colorPickerInitialized){
 			$("input.bookmark-pick-a-color").ColorPickerSliders({
@@ -77,7 +87,7 @@ function buildBookmarkTabEditor(){
 				hsvpanel: true,
 				previewformat: 'hex',
 				flat: true,
-				onchange: function(container, color){ 
+				onchange: function(container, color){
 					generatePreviewBookmarkNewTab();
 					generatePreviewBookmarkEditTab();
 				}
@@ -473,8 +483,19 @@ $(document).on('input', "#edit-bookmark-tab-form-inputTextColor", generatePrevie
 
 // CATEGORY MANAGEMENT
 function bookmarkCategoriesLaunch(){
-	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Categories');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_categories','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Categories</span></a></li>`;
-	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+	var menuList = `<li class="bookmarkCategoryMenu-added" onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Categories');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_categories','#settings-tab-editor-bookmark-categories','Bookmark Category Editor');" role="presentation"><a id="settings-tab-editor-bookmark-categories-anchor" href="#settings-tab-editor-bookmark-categories" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Categories</span></a></li>`;
+	let menuListAlt = `<option value="#settings-tab-editor-bookmark-categories-anchor" lang="en">Bookmark Categories</option>`;
+	let div = `
+	<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-bookmark-categories">
+		<h2 lang="en">Loading...</h2>
+	</div>
+	`;
+	if($('.bookmarkCategoryMenu-added').length === 0 ){
+		$('#settings-main-tab-editor .nav-tabs').append(menuList);
+		$('.settings-dropdown-box.tab-editor-menu').append(menuListAlt);
+		$('#settings-main-tab-editor .tab-content').append(div);
+	}
+
 }
 
 function buildBookmarkCategoryEditor(){
@@ -653,4 +674,4 @@ function submitBookmarkCategoryOrder(){
 	}).fail(function(xhr) {
 		OrganizrApiError(xhr, 'Update Error');
 	});
-}
+}

+ 21 - 12
api/v2/routes/token.php

@@ -1,22 +1,31 @@
 <?php
-$app->delete('/token/{id}', function ($request, $response, $args) {
+$app->get('/token/me', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	if ($Organizr->qualifyRequest(998, true)) {
-		$Organizr->revokeTokenByIdCurrentUser($args['id']);
+	if ($Organizr->checkRoute($request)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->user;
 	}
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
-
 $app->post('/token/validate', function ($request, $response, $args) {
-        $Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-        if ($Organizr->qualifyRequest(999, true)) {
-                $GLOBALS['api']['response']['data'] = $Organizr->validateToken($_REQUEST["Token"]);
-        }
-        $response->getBody()->write(jsonE($GLOBALS['api']));
-        return $response
-                ->withHeader('Content-Type', 'application/json;charset=UTF-8')
-                ->withStatus($GLOBALS['responseCode']);
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(999, true)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->validateToken($_REQUEST["Token"]);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
+$app->delete('/token/{id}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(998, true)) {
+		$Organizr->revokeTokenByIdCurrentUser($args['id']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 22 - 0
css/organizr.css

@@ -4567,4 +4567,26 @@ html {
 }
 span.select2-selection__choice__remove {
     -webkit-text-security: initial;
+}
+.dropside .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu {
+    margin-bottom: 2px;
+    margin-left: 60px;
+    top: 0;
+}
+.dropside.open>.dropdown-menu {
+     display: inline-flex;
+}
+.dropside .dropdown-menu>li>a {
+      display: inline;
+      padding: 3px 20px;
+      clear: both;
+      font-weight: 400;
+      line-height: 1.42857143;
+      white-space: nowrap;
+}
+.sortable-ghost {
+     background: #151515;
+}
+.sortable-chosen {
+     background: #2f2f2f;
 }

File diff suppressed because it is too large
+ 0 - 0
css/organizr.min.css


+ 3783 - 0
js/Sortable.js

@@ -0,0 +1,3783 @@
+/**!
+ * Sortable 1.14.0
+ * @author	RubaXa   <trash@rubaxa.org>
+ * @author	owenm    <owen23355@gmail.com>
+ * @license MIT
+ */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global = global || self, global.Sortable = factory());
+}(this, (function () { 'use strict';
+
+  function ownKeys(object, enumerableOnly) {
+    var keys = Object.keys(object);
+
+    if (Object.getOwnPropertySymbols) {
+      var symbols = Object.getOwnPropertySymbols(object);
+
+      if (enumerableOnly) {
+        symbols = symbols.filter(function (sym) {
+          return Object.getOwnPropertyDescriptor(object, sym).enumerable;
+        });
+      }
+
+      keys.push.apply(keys, symbols);
+    }
+
+    return keys;
+  }
+
+  function _objectSpread2(target) {
+    for (var i = 1; i < arguments.length; i++) {
+      var source = arguments[i] != null ? arguments[i] : {};
+
+      if (i % 2) {
+        ownKeys(Object(source), true).forEach(function (key) {
+          _defineProperty(target, key, source[key]);
+        });
+      } else if (Object.getOwnPropertyDescriptors) {
+        Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
+      } else {
+        ownKeys(Object(source)).forEach(function (key) {
+          Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
+        });
+      }
+    }
+
+    return target;
+  }
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
+      _typeof = function (obj) {
+        return typeof obj;
+      };
+    } else {
+      _typeof = function (obj) {
+        return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+      };
+    }
+
+    return _typeof(obj);
+  }
+
+  function _defineProperty(obj, key, value) {
+    if (key in obj) {
+      Object.defineProperty(obj, key, {
+        value: value,
+        enumerable: true,
+        configurable: true,
+        writable: true
+      });
+    } else {
+      obj[key] = value;
+    }
+
+    return obj;
+  }
+
+  function _extends() {
+    _extends = Object.assign || function (target) {
+      for (var i = 1; i < arguments.length; i++) {
+        var source = arguments[i];
+
+        for (var key in source) {
+          if (Object.prototype.hasOwnProperty.call(source, key)) {
+            target[key] = source[key];
+          }
+        }
+      }
+
+      return target;
+    };
+
+    return _extends.apply(this, arguments);
+  }
+
+  function _objectWithoutPropertiesLoose(source, excluded) {
+    if (source == null) return {};
+    var target = {};
+    var sourceKeys = Object.keys(source);
+    var key, i;
+
+    for (i = 0; i < sourceKeys.length; i++) {
+      key = sourceKeys[i];
+      if (excluded.indexOf(key) >= 0) continue;
+      target[key] = source[key];
+    }
+
+    return target;
+  }
+
+  function _objectWithoutProperties(source, excluded) {
+    if (source == null) return {};
+
+    var target = _objectWithoutPropertiesLoose(source, excluded);
+
+    var key, i;
+
+    if (Object.getOwnPropertySymbols) {
+      var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
+
+      for (i = 0; i < sourceSymbolKeys.length; i++) {
+        key = sourceSymbolKeys[i];
+        if (excluded.indexOf(key) >= 0) continue;
+        if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
+        target[key] = source[key];
+      }
+    }
+
+    return target;
+  }
+
+  function _toConsumableArray(arr) {
+    return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
+  }
+
+  function _arrayWithoutHoles(arr) {
+    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
+  }
+
+  function _iterableToArray(iter) {
+    if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
+  }
+
+  function _unsupportedIterableToArray(o, minLen) {
+    if (!o) return;
+    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
+    var n = Object.prototype.toString.call(o).slice(8, -1);
+    if (n === "Object" && o.constructor) n = o.constructor.name;
+    if (n === "Map" || n === "Set") return Array.from(o);
+    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
+  }
+
+  function _arrayLikeToArray(arr, len) {
+    if (len == null || len > arr.length) len = arr.length;
+
+    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
+
+    return arr2;
+  }
+
+  function _nonIterableSpread() {
+    throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
+  }
+
+  var version = "1.14.0";
+
+  function userAgent(pattern) {
+    if (typeof window !== 'undefined' && window.navigator) {
+      return !! /*@__PURE__*/navigator.userAgent.match(pattern);
+    }
+  }
+
+  var IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i);
+  var Edge = userAgent(/Edge/i);
+  var FireFox = userAgent(/firefox/i);
+  var Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i);
+  var IOS = userAgent(/iP(ad|od|hone)/i);
+  var ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i);
+
+  var captureMode = {
+    capture: false,
+    passive: false
+  };
+
+  function on(el, event, fn) {
+    el.addEventListener(event, fn, !IE11OrLess && captureMode);
+  }
+
+  function off(el, event, fn) {
+    el.removeEventListener(event, fn, !IE11OrLess && captureMode);
+  }
+
+  function matches(
+  /**HTMLElement*/
+  el,
+  /**String*/
+  selector) {
+    if (!selector) return;
+    selector[0] === '>' && (selector = selector.substring(1));
+
+    if (el) {
+      try {
+        if (el.matches) {
+          return el.matches(selector);
+        } else if (el.msMatchesSelector) {
+          return el.msMatchesSelector(selector);
+        } else if (el.webkitMatchesSelector) {
+          return el.webkitMatchesSelector(selector);
+        }
+      } catch (_) {
+        return false;
+      }
+    }
+
+    return false;
+  }
+
+  function getParentOrHost(el) {
+    return el.host && el !== document && el.host.nodeType ? el.host : el.parentNode;
+  }
+
+  function closest(
+  /**HTMLElement*/
+  el,
+  /**String*/
+  selector,
+  /**HTMLElement*/
+  ctx, includeCTX) {
+    if (el) {
+      ctx = ctx || document;
+
+      do {
+        if (selector != null && (selector[0] === '>' ? el.parentNode === ctx && matches(el, selector) : matches(el, selector)) || includeCTX && el === ctx) {
+          return el;
+        }
+
+        if (el === ctx) break;
+        /* jshint boss:true */
+      } while (el = getParentOrHost(el));
+    }
+
+    return null;
+  }
+
+  var R_SPACE = /\s+/g;
+
+  function toggleClass(el, name, state) {
+    if (el && name) {
+      if (el.classList) {
+        el.classList[state ? 'add' : 'remove'](name);
+      } else {
+        var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' ');
+        el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' ');
+      }
+    }
+  }
+
+  function css(el, prop, val) {
+    var style = el && el.style;
+
+    if (style) {
+      if (val === void 0) {
+        if (document.defaultView && document.defaultView.getComputedStyle) {
+          val = document.defaultView.getComputedStyle(el, '');
+        } else if (el.currentStyle) {
+          val = el.currentStyle;
+        }
+
+        return prop === void 0 ? val : val[prop];
+      } else {
+        if (!(prop in style) && prop.indexOf('webkit') === -1) {
+          prop = '-webkit-' + prop;
+        }
+
+        style[prop] = val + (typeof val === 'string' ? '' : 'px');
+      }
+    }
+  }
+
+  function matrix(el, selfOnly) {
+    var appliedTransforms = '';
+
+    if (typeof el === 'string') {
+      appliedTransforms = el;
+    } else {
+      do {
+        var transform = css(el, 'transform');
+
+        if (transform && transform !== 'none') {
+          appliedTransforms = transform + ' ' + appliedTransforms;
+        }
+        /* jshint boss:true */
+
+      } while (!selfOnly && (el = el.parentNode));
+    }
+
+    var matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix;
+    /*jshint -W056 */
+
+    return matrixFn && new matrixFn(appliedTransforms);
+  }
+
+  function find(ctx, tagName, iterator) {
+    if (ctx) {
+      var list = ctx.getElementsByTagName(tagName),
+          i = 0,
+          n = list.length;
+
+      if (iterator) {
+        for (; i < n; i++) {
+          iterator(list[i], i);
+        }
+      }
+
+      return list;
+    }
+
+    return [];
+  }
+
+  function getWindowScrollingElement() {
+    var scrollingElement = document.scrollingElement;
+
+    if (scrollingElement) {
+      return scrollingElement;
+    } else {
+      return document.documentElement;
+    }
+  }
+  /**
+   * Returns the "bounding client rect" of given element
+   * @param  {HTMLElement} el                       The element whose boundingClientRect is wanted
+   * @param  {[Boolean]} relativeToContainingBlock  Whether the rect should be relative to the containing block of (including) the container
+   * @param  {[Boolean]} relativeToNonStaticParent  Whether the rect should be relative to the relative parent of (including) the contaienr
+   * @param  {[Boolean]} undoScale                  Whether the container's scale() should be undone
+   * @param  {[HTMLElement]} container              The parent the element will be placed in
+   * @return {Object}                               The boundingClientRect of el, with specified adjustments
+   */
+
+
+  function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) {
+    if (!el.getBoundingClientRect && el !== window) return;
+    var elRect, top, left, bottom, right, height, width;
+
+    if (el !== window && el.parentNode && el !== getWindowScrollingElement()) {
+      elRect = el.getBoundingClientRect();
+      top = elRect.top;
+      left = elRect.left;
+      bottom = elRect.bottom;
+      right = elRect.right;
+      height = elRect.height;
+      width = elRect.width;
+    } else {
+      top = 0;
+      left = 0;
+      bottom = window.innerHeight;
+      right = window.innerWidth;
+      height = window.innerHeight;
+      width = window.innerWidth;
+    }
+
+    if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) {
+      // Adjust for translate()
+      container = container || el.parentNode; // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312)
+      // Not needed on <= IE11
+
+      if (!IE11OrLess) {
+        do {
+          if (container && container.getBoundingClientRect && (css(container, 'transform') !== 'none' || relativeToNonStaticParent && css(container, 'position') !== 'static')) {
+            var containerRect = container.getBoundingClientRect(); // Set relative to edges of padding box of container
+
+            top -= containerRect.top + parseInt(css(container, 'border-top-width'));
+            left -= containerRect.left + parseInt(css(container, 'border-left-width'));
+            bottom = top + elRect.height;
+            right = left + elRect.width;
+            break;
+          }
+          /* jshint boss:true */
+
+        } while (container = container.parentNode);
+      }
+    }
+
+    if (undoScale && el !== window) {
+      // Adjust for scale()
+      var elMatrix = matrix(container || el),
+          scaleX = elMatrix && elMatrix.a,
+          scaleY = elMatrix && elMatrix.d;
+
+      if (elMatrix) {
+        top /= scaleY;
+        left /= scaleX;
+        width /= scaleX;
+        height /= scaleY;
+        bottom = top + height;
+        right = left + width;
+      }
+    }
+
+    return {
+      top: top,
+      left: left,
+      bottom: bottom,
+      right: right,
+      width: width,
+      height: height
+    };
+  }
+  /**
+   * Checks if a side of an element is scrolled past a side of its parents
+   * @param  {HTMLElement}  el           The element who's side being scrolled out of view is in question
+   * @param  {String}       elSide       Side of the element in question ('top', 'left', 'right', 'bottom')
+   * @param  {String}       parentSide   Side of the parent in question ('top', 'left', 'right', 'bottom')
+   * @return {HTMLElement}               The parent scroll element that the el's side is scrolled past, or null if there is no such element
+   */
+
+
+  function isScrolledPast(el, elSide, parentSide) {
+    var parent = getParentAutoScrollElement(el, true),
+        elSideVal = getRect(el)[elSide];
+    /* jshint boss:true */
+
+    while (parent) {
+      var parentSideVal = getRect(parent)[parentSide],
+          visible = void 0;
+
+      if (parentSide === 'top' || parentSide === 'left') {
+        visible = elSideVal >= parentSideVal;
+      } else {
+        visible = elSideVal <= parentSideVal;
+      }
+
+      if (!visible) return parent;
+      if (parent === getWindowScrollingElement()) break;
+      parent = getParentAutoScrollElement(parent, false);
+    }
+
+    return false;
+  }
+  /**
+   * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible)
+   * and non-draggable elements
+   * @param  {HTMLElement} el       The parent element
+   * @param  {Number} childNum      The index of the child
+   * @param  {Object} options       Parent Sortable's options
+   * @return {HTMLElement}          The child at index childNum, or null if not found
+   */
+
+
+  function getChild(el, childNum, options, includeDragEl) {
+    var currentChild = 0,
+        i = 0,
+        children = el.children;
+
+    while (i < children.length) {
+      if (children[i].style.display !== 'none' && children[i] !== Sortable.ghost && (includeDragEl || children[i] !== Sortable.dragged) && closest(children[i], options.draggable, el, false)) {
+        if (currentChild === childNum) {
+          return children[i];
+        }
+
+        currentChild++;
+      }
+
+      i++;
+    }
+
+    return null;
+  }
+  /**
+   * Gets the last child in the el, ignoring ghostEl or invisible elements (clones)
+   * @param  {HTMLElement} el       Parent element
+   * @param  {selector} selector    Any other elements that should be ignored
+   * @return {HTMLElement}          The last child, ignoring ghostEl
+   */
+
+
+  function lastChild(el, selector) {
+    var last = el.lastElementChild;
+
+    while (last && (last === Sortable.ghost || css(last, 'display') === 'none' || selector && !matches(last, selector))) {
+      last = last.previousElementSibling;
+    }
+
+    return last || null;
+  }
+  /**
+   * Returns the index of an element within its parent for a selected set of
+   * elements
+   * @param  {HTMLElement} el
+   * @param  {selector} selector
+   * @return {number}
+   */
+
+
+  function index(el, selector) {
+    var index = 0;
+
+    if (!el || !el.parentNode) {
+      return -1;
+    }
+    /* jshint boss:true */
+
+
+    while (el = el.previousElementSibling) {
+      if (el.nodeName.toUpperCase() !== 'TEMPLATE' && el !== Sortable.clone && (!selector || matches(el, selector))) {
+        index++;
+      }
+    }
+
+    return index;
+  }
+  /**
+   * Returns the scroll offset of the given element, added with all the scroll offsets of parent elements.
+   * The value is returned in real pixels.
+   * @param  {HTMLElement} el
+   * @return {Array}             Offsets in the format of [left, top]
+   */
+
+
+  function getRelativeScrollOffset(el) {
+    var offsetLeft = 0,
+        offsetTop = 0,
+        winScroller = getWindowScrollingElement();
+
+    if (el) {
+      do {
+        var elMatrix = matrix(el),
+            scaleX = elMatrix.a,
+            scaleY = elMatrix.d;
+        offsetLeft += el.scrollLeft * scaleX;
+        offsetTop += el.scrollTop * scaleY;
+      } while (el !== winScroller && (el = el.parentNode));
+    }
+
+    return [offsetLeft, offsetTop];
+  }
+  /**
+   * Returns the index of the object within the given array
+   * @param  {Array} arr   Array that may or may not hold the object
+   * @param  {Object} obj  An object that has a key-value pair unique to and identical to a key-value pair in the object you want to find
+   * @return {Number}      The index of the object in the array, or -1
+   */
+
+
+  function indexOfObject(arr, obj) {
+    for (var i in arr) {
+      if (!arr.hasOwnProperty(i)) continue;
+
+      for (var key in obj) {
+        if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i);
+      }
+    }
+
+    return -1;
+  }
+
+  function getParentAutoScrollElement(el, includeSelf) {
+    // skip to window
+    if (!el || !el.getBoundingClientRect) return getWindowScrollingElement();
+    var elem = el;
+    var gotSelf = false;
+
+    do {
+      // we don't need to get elem css if it isn't even overflowing in the first place (performance)
+      if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
+        var elemCSS = css(elem);
+
+        if (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) {
+          if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement();
+          if (gotSelf || includeSelf) return elem;
+          gotSelf = true;
+        }
+      }
+      /* jshint boss:true */
+
+    } while (elem = elem.parentNode);
+
+    return getWindowScrollingElement();
+  }
+
+  function extend(dst, src) {
+    if (dst && src) {
+      for (var key in src) {
+        if (src.hasOwnProperty(key)) {
+          dst[key] = src[key];
+        }
+      }
+    }
+
+    return dst;
+  }
+
+  function isRectEqual(rect1, rect2) {
+    return Math.round(rect1.top) === Math.round(rect2.top) && Math.round(rect1.left) === Math.round(rect2.left) && Math.round(rect1.height) === Math.round(rect2.height) && Math.round(rect1.width) === Math.round(rect2.width);
+  }
+
+  var _throttleTimeout;
+
+  function throttle(callback, ms) {
+    return function () {
+      if (!_throttleTimeout) {
+        var args = arguments,
+            _this = this;
+
+        if (args.length === 1) {
+          callback.call(_this, args[0]);
+        } else {
+          callback.apply(_this, args);
+        }
+
+        _throttleTimeout = setTimeout(function () {
+          _throttleTimeout = void 0;
+        }, ms);
+      }
+    };
+  }
+
+  function cancelThrottle() {
+    clearTimeout(_throttleTimeout);
+    _throttleTimeout = void 0;
+  }
+
+  function scrollBy(el, x, y) {
+    el.scrollLeft += x;
+    el.scrollTop += y;
+  }
+
+  function clone(el) {
+    var Polymer = window.Polymer;
+    var $ = window.jQuery || window.Zepto;
+
+    if (Polymer && Polymer.dom) {
+      return Polymer.dom(el).cloneNode(true);
+    } else if ($) {
+      return $(el).clone(true)[0];
+    } else {
+      return el.cloneNode(true);
+    }
+  }
+
+  function setRect(el, rect) {
+    css(el, 'position', 'absolute');
+    css(el, 'top', rect.top);
+    css(el, 'left', rect.left);
+    css(el, 'width', rect.width);
+    css(el, 'height', rect.height);
+  }
+
+  function unsetRect(el) {
+    css(el, 'position', '');
+    css(el, 'top', '');
+    css(el, 'left', '');
+    css(el, 'width', '');
+    css(el, 'height', '');
+  }
+
+  var expando = 'Sortable' + new Date().getTime();
+
+  function AnimationStateManager() {
+    var animationStates = [],
+        animationCallbackId;
+    return {
+      captureAnimationState: function captureAnimationState() {
+        animationStates = [];
+        if (!this.options.animation) return;
+        var children = [].slice.call(this.el.children);
+        children.forEach(function (child) {
+          if (css(child, 'display') === 'none' || child === Sortable.ghost) return;
+          animationStates.push({
+            target: child,
+            rect: getRect(child)
+          });
+
+          var fromRect = _objectSpread2({}, animationStates[animationStates.length - 1].rect); // If animating: compensate for current animation
+
+
+          if (child.thisAnimationDuration) {
+            var childMatrix = matrix(child, true);
+
+            if (childMatrix) {
+              fromRect.top -= childMatrix.f;
+              fromRect.left -= childMatrix.e;
+            }
+          }
+
+          child.fromRect = fromRect;
+        });
+      },
+      addAnimationState: function addAnimationState(state) {
+        animationStates.push(state);
+      },
+      removeAnimationState: function removeAnimationState(target) {
+        animationStates.splice(indexOfObject(animationStates, {
+          target: target
+        }), 1);
+      },
+      animateAll: function animateAll(callback) {
+        var _this = this;
+
+        if (!this.options.animation) {
+          clearTimeout(animationCallbackId);
+          if (typeof callback === 'function') callback();
+          return;
+        }
+
+        var animating = false,
+            animationTime = 0;
+        animationStates.forEach(function (state) {
+          var time = 0,
+              target = state.target,
+              fromRect = target.fromRect,
+              toRect = getRect(target),
+              prevFromRect = target.prevFromRect,
+              prevToRect = target.prevToRect,
+              animatingRect = state.rect,
+              targetMatrix = matrix(target, true);
+
+          if (targetMatrix) {
+            // Compensate for current animation
+            toRect.top -= targetMatrix.f;
+            toRect.left -= targetMatrix.e;
+          }
+
+          target.toRect = toRect;
+
+          if (target.thisAnimationDuration) {
+            // Could also check if animatingRect is between fromRect and toRect
+            if (isRectEqual(prevFromRect, toRect) && !isRectEqual(fromRect, toRect) && // Make sure animatingRect is on line between toRect & fromRect
+            (animatingRect.top - toRect.top) / (animatingRect.left - toRect.left) === (fromRect.top - toRect.top) / (fromRect.left - toRect.left)) {
+              // If returning to same place as started from animation and on same axis
+              time = calculateRealTime(animatingRect, prevFromRect, prevToRect, _this.options);
+            }
+          } // if fromRect != toRect: animate
+
+
+          if (!isRectEqual(toRect, fromRect)) {
+            target.prevFromRect = fromRect;
+            target.prevToRect = toRect;
+
+            if (!time) {
+              time = _this.options.animation;
+            }
+
+            _this.animate(target, animatingRect, toRect, time);
+          }
+
+          if (time) {
+            animating = true;
+            animationTime = Math.max(animationTime, time);
+            clearTimeout(target.animationResetTimer);
+            target.animationResetTimer = setTimeout(function () {
+              target.animationTime = 0;
+              target.prevFromRect = null;
+              target.fromRect = null;
+              target.prevToRect = null;
+              target.thisAnimationDuration = null;
+            }, time);
+            target.thisAnimationDuration = time;
+          }
+        });
+        clearTimeout(animationCallbackId);
+
+        if (!animating) {
+          if (typeof callback === 'function') callback();
+        } else {
+          animationCallbackId = setTimeout(function () {
+            if (typeof callback === 'function') callback();
+          }, animationTime);
+        }
+
+        animationStates = [];
+      },
+      animate: function animate(target, currentRect, toRect, duration) {
+        if (duration) {
+          css(target, 'transition', '');
+          css(target, 'transform', '');
+          var elMatrix = matrix(this.el),
+              scaleX = elMatrix && elMatrix.a,
+              scaleY = elMatrix && elMatrix.d,
+              translateX = (currentRect.left - toRect.left) / (scaleX || 1),
+              translateY = (currentRect.top - toRect.top) / (scaleY || 1);
+          target.animatingX = !!translateX;
+          target.animatingY = !!translateY;
+          css(target, 'transform', 'translate3d(' + translateX + 'px,' + translateY + 'px,0)');
+          this.forRepaintDummy = repaint(target); // repaint
+
+          css(target, 'transition', 'transform ' + duration + 'ms' + (this.options.easing ? ' ' + this.options.easing : ''));
+          css(target, 'transform', 'translate3d(0,0,0)');
+          typeof target.animated === 'number' && clearTimeout(target.animated);
+          target.animated = setTimeout(function () {
+            css(target, 'transition', '');
+            css(target, 'transform', '');
+            target.animated = false;
+            target.animatingX = false;
+            target.animatingY = false;
+          }, duration);
+        }
+      }
+    };
+  }
+
+  function repaint(target) {
+    return target.offsetWidth;
+  }
+
+  function calculateRealTime(animatingRect, fromRect, toRect, options) {
+    return Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) * options.animation;
+  }
+
+  var plugins = [];
+  var defaults = {
+    initializeByDefault: true
+  };
+  var PluginManager = {
+    mount: function mount(plugin) {
+      // Set default static properties
+      for (var option in defaults) {
+        if (defaults.hasOwnProperty(option) && !(option in plugin)) {
+          plugin[option] = defaults[option];
+        }
+      }
+
+      plugins.forEach(function (p) {
+        if (p.pluginName === plugin.pluginName) {
+          throw "Sortable: Cannot mount plugin ".concat(plugin.pluginName, " more than once");
+        }
+      });
+      plugins.push(plugin);
+    },
+    pluginEvent: function pluginEvent(eventName, sortable, evt) {
+      var _this = this;
+
+      this.eventCanceled = false;
+
+      evt.cancel = function () {
+        _this.eventCanceled = true;
+      };
+
+      var eventNameGlobal = eventName + 'Global';
+      plugins.forEach(function (plugin) {
+        if (!sortable[plugin.pluginName]) return; // Fire global events if it exists in this sortable
+
+        if (sortable[plugin.pluginName][eventNameGlobal]) {
+          sortable[plugin.pluginName][eventNameGlobal](_objectSpread2({
+            sortable: sortable
+          }, evt));
+        } // Only fire plugin event if plugin is enabled in this sortable,
+        // and plugin has event defined
+
+
+        if (sortable.options[plugin.pluginName] && sortable[plugin.pluginName][eventName]) {
+          sortable[plugin.pluginName][eventName](_objectSpread2({
+            sortable: sortable
+          }, evt));
+        }
+      });
+    },
+    initializePlugins: function initializePlugins(sortable, el, defaults, options) {
+      plugins.forEach(function (plugin) {
+        var pluginName = plugin.pluginName;
+        if (!sortable.options[pluginName] && !plugin.initializeByDefault) return;
+        var initialized = new plugin(sortable, el, sortable.options);
+        initialized.sortable = sortable;
+        initialized.options = sortable.options;
+        sortable[pluginName] = initialized; // Add default options from plugin
+
+        _extends(defaults, initialized.defaults);
+      });
+
+      for (var option in sortable.options) {
+        if (!sortable.options.hasOwnProperty(option)) continue;
+        var modified = this.modifyOption(sortable, option, sortable.options[option]);
+
+        if (typeof modified !== 'undefined') {
+          sortable.options[option] = modified;
+        }
+      }
+    },
+    getEventProperties: function getEventProperties(name, sortable) {
+      var eventProperties = {};
+      plugins.forEach(function (plugin) {
+        if (typeof plugin.eventProperties !== 'function') return;
+
+        _extends(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name));
+      });
+      return eventProperties;
+    },
+    modifyOption: function modifyOption(sortable, name, value) {
+      var modifiedValue;
+      plugins.forEach(function (plugin) {
+        // Plugin must exist on the Sortable
+        if (!sortable[plugin.pluginName]) return; // If static option listener exists for this option, call in the context of the Sortable's instance of this plugin
+
+        if (plugin.optionListeners && typeof plugin.optionListeners[name] === 'function') {
+          modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value);
+        }
+      });
+      return modifiedValue;
+    }
+  };
+
+  function dispatchEvent(_ref) {
+    var sortable = _ref.sortable,
+        rootEl = _ref.rootEl,
+        name = _ref.name,
+        targetEl = _ref.targetEl,
+        cloneEl = _ref.cloneEl,
+        toEl = _ref.toEl,
+        fromEl = _ref.fromEl,
+        oldIndex = _ref.oldIndex,
+        newIndex = _ref.newIndex,
+        oldDraggableIndex = _ref.oldDraggableIndex,
+        newDraggableIndex = _ref.newDraggableIndex,
+        originalEvent = _ref.originalEvent,
+        putSortable = _ref.putSortable,
+        extraEventProperties = _ref.extraEventProperties;
+    sortable = sortable || rootEl && rootEl[expando];
+    if (!sortable) return;
+    var evt,
+        options = sortable.options,
+        onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); // Support for new CustomEvent feature
+
+    if (window.CustomEvent && !IE11OrLess && !Edge) {
+      evt = new CustomEvent(name, {
+        bubbles: true,
+        cancelable: true
+      });
+    } else {
+      evt = document.createEvent('Event');
+      evt.initEvent(name, true, true);
+    }
+
+    evt.to = toEl || rootEl;
+    evt.from = fromEl || rootEl;
+    evt.item = targetEl || rootEl;
+    evt.clone = cloneEl;
+    evt.oldIndex = oldIndex;
+    evt.newIndex = newIndex;
+    evt.oldDraggableIndex = oldDraggableIndex;
+    evt.newDraggableIndex = newDraggableIndex;
+    evt.originalEvent = originalEvent;
+    evt.pullMode = putSortable ? putSortable.lastPutMode : undefined;
+
+    var allEventProperties = _objectSpread2(_objectSpread2({}, extraEventProperties), PluginManager.getEventProperties(name, sortable));
+
+    for (var option in allEventProperties) {
+      evt[option] = allEventProperties[option];
+    }
+
+    if (rootEl) {
+      rootEl.dispatchEvent(evt);
+    }
+
+    if (options[onName]) {
+      options[onName].call(sortable, evt);
+    }
+  }
+
+  var _excluded = ["evt"];
+
+  var pluginEvent = function pluginEvent(eventName, sortable) {
+    var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
+        originalEvent = _ref.evt,
+        data = _objectWithoutProperties(_ref, _excluded);
+
+    PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, _objectSpread2({
+      dragEl: dragEl,
+      parentEl: parentEl,
+      ghostEl: ghostEl,
+      rootEl: rootEl,
+      nextEl: nextEl,
+      lastDownEl: lastDownEl,
+      cloneEl: cloneEl,
+      cloneHidden: cloneHidden,
+      dragStarted: moved,
+      putSortable: putSortable,
+      activeSortable: Sortable.active,
+      originalEvent: originalEvent,
+      oldIndex: oldIndex,
+      oldDraggableIndex: oldDraggableIndex,
+      newIndex: newIndex,
+      newDraggableIndex: newDraggableIndex,
+      hideGhostForTarget: _hideGhostForTarget,
+      unhideGhostForTarget: _unhideGhostForTarget,
+      cloneNowHidden: function cloneNowHidden() {
+        cloneHidden = true;
+      },
+      cloneNowShown: function cloneNowShown() {
+        cloneHidden = false;
+      },
+      dispatchSortableEvent: function dispatchSortableEvent(name) {
+        _dispatchEvent({
+          sortable: sortable,
+          name: name,
+          originalEvent: originalEvent
+        });
+      }
+    }, data));
+  };
+
+  function _dispatchEvent(info) {
+    dispatchEvent(_objectSpread2({
+      putSortable: putSortable,
+      cloneEl: cloneEl,
+      targetEl: dragEl,
+      rootEl: rootEl,
+      oldIndex: oldIndex,
+      oldDraggableIndex: oldDraggableIndex,
+      newIndex: newIndex,
+      newDraggableIndex: newDraggableIndex
+    }, info));
+  }
+
+  var dragEl,
+      parentEl,
+      ghostEl,
+      rootEl,
+      nextEl,
+      lastDownEl,
+      cloneEl,
+      cloneHidden,
+      oldIndex,
+      newIndex,
+      oldDraggableIndex,
+      newDraggableIndex,
+      activeGroup,
+      putSortable,
+      awaitingDragStarted = false,
+      ignoreNextClick = false,
+      sortables = [],
+      tapEvt,
+      touchEvt,
+      lastDx,
+      lastDy,
+      tapDistanceLeft,
+      tapDistanceTop,
+      moved,
+      lastTarget,
+      lastDirection,
+      pastFirstInvertThresh = false,
+      isCircumstantialInvert = false,
+      targetMoveDistance,
+      // For positioning ghost absolutely
+  ghostRelativeParent,
+      ghostRelativeParentInitialScroll = [],
+      // (left, top)
+  _silent = false,
+      savedInputChecked = [];
+  /** @const */
+
+  var documentExists = typeof document !== 'undefined',
+      PositionGhostAbsolutely = IOS,
+      CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float',
+      // This will not pass for IE9, because IE9 DnD only works on anchors
+  supportDraggable = documentExists && !ChromeForAndroid && !IOS && 'draggable' in document.createElement('div'),
+      supportCssPointerEvents = function () {
+    if (!documentExists) return; // false when <= IE11
+
+    if (IE11OrLess) {
+      return false;
+    }
+
+    var el = document.createElement('x');
+    el.style.cssText = 'pointer-events:auto';
+    return el.style.pointerEvents === 'auto';
+  }(),
+      _detectDirection = function _detectDirection(el, options) {
+    var elCSS = css(el),
+        elWidth = parseInt(elCSS.width) - parseInt(elCSS.paddingLeft) - parseInt(elCSS.paddingRight) - parseInt(elCSS.borderLeftWidth) - parseInt(elCSS.borderRightWidth),
+        child1 = getChild(el, 0, options),
+        child2 = getChild(el, 1, options),
+        firstChildCSS = child1 && css(child1),
+        secondChildCSS = child2 && css(child2),
+        firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + getRect(child1).width,
+        secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + getRect(child2).width;
+
+    if (elCSS.display === 'flex') {
+      return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' ? 'vertical' : 'horizontal';
+    }
+
+    if (elCSS.display === 'grid') {
+      return elCSS.gridTemplateColumns.split(' ').length <= 1 ? 'vertical' : 'horizontal';
+    }
+
+    if (child1 && firstChildCSS["float"] && firstChildCSS["float"] !== 'none') {
+      var touchingSideChild2 = firstChildCSS["float"] === 'left' ? 'left' : 'right';
+      return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ? 'vertical' : 'horizontal';
+    }
+
+    return child1 && (firstChildCSS.display === 'block' || firstChildCSS.display === 'flex' || firstChildCSS.display === 'table' || firstChildCSS.display === 'grid' || firstChildWidth >= elWidth && elCSS[CSSFloatProperty] === 'none' || child2 && elCSS[CSSFloatProperty] === 'none' && firstChildWidth + secondChildWidth > elWidth) ? 'vertical' : 'horizontal';
+  },
+      _dragElInRowColumn = function _dragElInRowColumn(dragRect, targetRect, vertical) {
+    var dragElS1Opp = vertical ? dragRect.left : dragRect.top,
+        dragElS2Opp = vertical ? dragRect.right : dragRect.bottom,
+        dragElOppLength = vertical ? dragRect.width : dragRect.height,
+        targetS1Opp = vertical ? targetRect.left : targetRect.top,
+        targetS2Opp = vertical ? targetRect.right : targetRect.bottom,
+        targetOppLength = vertical ? targetRect.width : targetRect.height;
+    return dragElS1Opp === targetS1Opp || dragElS2Opp === targetS2Opp || dragElS1Opp + dragElOppLength / 2 === targetS1Opp + targetOppLength / 2;
+  },
+
+  /**
+   * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold.
+   * @param  {Number} x      X position
+   * @param  {Number} y      Y position
+   * @return {HTMLElement}   Element of the first found nearest Sortable
+   */
+  _detectNearestEmptySortable = function _detectNearestEmptySortable(x, y) {
+    var ret;
+    sortables.some(function (sortable) {
+      var threshold = sortable[expando].options.emptyInsertThreshold;
+      if (!threshold || lastChild(sortable)) return;
+      var rect = getRect(sortable),
+          insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold,
+          insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold;
+
+      if (insideHorizontally && insideVertically) {
+        return ret = sortable;
+      }
+    });
+    return ret;
+  },
+      _prepareGroup = function _prepareGroup(options) {
+    function toFn(value, pull) {
+      return function (to, from, dragEl, evt) {
+        var sameGroup = to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name;
+
+        if (value == null && (pull || sameGroup)) {
+          // Default pull value
+          // Default pull and put value if same group
+          return true;
+        } else if (value == null || value === false) {
+          return false;
+        } else if (pull && value === 'clone') {
+          return value;
+        } else if (typeof value === 'function') {
+          return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt);
+        } else {
+          var otherGroup = (pull ? to : from).options.group.name;
+          return value === true || typeof value === 'string' && value === otherGroup || value.join && value.indexOf(otherGroup) > -1;
+        }
+      };
+    }
+
+    var group = {};
+    var originalGroup = options.group;
+
+    if (!originalGroup || _typeof(originalGroup) != 'object') {
+      originalGroup = {
+        name: originalGroup
+      };
+    }
+
+    group.name = originalGroup.name;
+    group.checkPull = toFn(originalGroup.pull, true);
+    group.checkPut = toFn(originalGroup.put);
+    group.revertClone = originalGroup.revertClone;
+    options.group = group;
+  },
+      _hideGhostForTarget = function _hideGhostForTarget() {
+    if (!supportCssPointerEvents && ghostEl) {
+      css(ghostEl, 'display', 'none');
+    }
+  },
+      _unhideGhostForTarget = function _unhideGhostForTarget() {
+    if (!supportCssPointerEvents && ghostEl) {
+      css(ghostEl, 'display', '');
+    }
+  }; // #1184 fix - Prevent click event on fallback if dragged but item not changed position
+
+
+  if (documentExists) {
+    document.addEventListener('click', function (evt) {
+      if (ignoreNextClick) {
+        evt.preventDefault();
+        evt.stopPropagation && evt.stopPropagation();
+        evt.stopImmediatePropagation && evt.stopImmediatePropagation();
+        ignoreNextClick = false;
+        return false;
+      }
+    }, true);
+  }
+
+  var nearestEmptyInsertDetectEvent = function nearestEmptyInsertDetectEvent(evt) {
+    if (dragEl) {
+      evt = evt.touches ? evt.touches[0] : evt;
+
+      var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY);
+
+      if (nearest) {
+        // Create imitation event
+        var event = {};
+
+        for (var i in evt) {
+          if (evt.hasOwnProperty(i)) {
+            event[i] = evt[i];
+          }
+        }
+
+        event.target = event.rootEl = nearest;
+        event.preventDefault = void 0;
+        event.stopPropagation = void 0;
+
+        nearest[expando]._onDragOver(event);
+      }
+    }
+  };
+
+  var _checkOutsideTargetEl = function _checkOutsideTargetEl(evt) {
+    if (dragEl) {
+      dragEl.parentNode[expando]._isOutsideThisEl(evt.target);
+    }
+  };
+  /**
+   * @class  Sortable
+   * @param  {HTMLElement}  el
+   * @param  {Object}       [options]
+   */
+
+
+  function Sortable(el, options) {
+    if (!(el && el.nodeType && el.nodeType === 1)) {
+      throw "Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(el));
+    }
+
+    this.el = el; // root element
+
+    this.options = options = _extends({}, options); // Export instance
+
+    el[expando] = this;
+    var defaults = {
+      group: null,
+      sort: true,
+      disabled: false,
+      store: null,
+      handle: null,
+      draggable: /^[uo]l$/i.test(el.nodeName) ? '>li' : '>*',
+      swapThreshold: 1,
+      // percentage; 0 <= x <= 1
+      invertSwap: false,
+      // invert always
+      invertedSwapThreshold: null,
+      // will be set to same as swapThreshold if default
+      removeCloneOnHide: true,
+      direction: function direction() {
+        return _detectDirection(el, this.options);
+      },
+      ghostClass: 'sortable-ghost',
+      chosenClass: 'sortable-chosen',
+      dragClass: 'sortable-drag',
+      ignore: 'a, img',
+      filter: null,
+      preventOnFilter: true,
+      animation: 0,
+      easing: null,
+      setData: function setData(dataTransfer, dragEl) {
+        dataTransfer.setData('Text', dragEl.textContent);
+      },
+      dropBubble: false,
+      dragoverBubble: false,
+      dataIdAttr: 'data-id',
+      delay: 0,
+      delayOnTouchOnly: false,
+      touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1,
+      forceFallback: false,
+      fallbackClass: 'sortable-fallback',
+      fallbackOnBody: false,
+      fallbackTolerance: 0,
+      fallbackOffset: {
+        x: 0,
+        y: 0
+      },
+      supportPointer: Sortable.supportPointer !== false && 'PointerEvent' in window && !Safari,
+      emptyInsertThreshold: 5
+    };
+    PluginManager.initializePlugins(this, el, defaults); // Set default options
+
+    for (var name in defaults) {
+      !(name in options) && (options[name] = defaults[name]);
+    }
+
+    _prepareGroup(options); // Bind all private methods
+
+
+    for (var fn in this) {
+      if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+        this[fn] = this[fn].bind(this);
+      }
+    } // Setup drag mode
+
+
+    this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+    if (this.nativeDraggable) {
+      // Touch start threshold cannot be greater than the native dragstart threshold
+      this.options.touchStartThreshold = 1;
+    } // Bind events
+
+
+    if (options.supportPointer) {
+      on(el, 'pointerdown', this._onTapStart);
+    } else {
+      on(el, 'mousedown', this._onTapStart);
+      on(el, 'touchstart', this._onTapStart);
+    }
+
+    if (this.nativeDraggable) {
+      on(el, 'dragover', this);
+      on(el, 'dragenter', this);
+    }
+
+    sortables.push(this.el); // Restore sorting
+
+    options.store && options.store.get && this.sort(options.store.get(this) || []); // Add animation state manager
+
+    _extends(this, AnimationStateManager());
+  }
+
+  Sortable.prototype =
+  /** @lends Sortable.prototype */
+  {
+    constructor: Sortable,
+    _isOutsideThisEl: function _isOutsideThisEl(target) {
+      if (!this.el.contains(target) && target !== this.el) {
+        lastTarget = null;
+      }
+    },
+    _getDirection: function _getDirection(evt, target) {
+      return typeof this.options.direction === 'function' ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction;
+    },
+    _onTapStart: function _onTapStart(
+    /** Event|TouchEvent */
+    evt) {
+      if (!evt.cancelable) return;
+
+      var _this = this,
+          el = this.el,
+          options = this.options,
+          preventOnFilter = options.preventOnFilter,
+          type = evt.type,
+          touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === 'touch' && evt,
+          target = (touch || evt).target,
+          originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target,
+          filter = options.filter;
+
+      _saveInputCheckedState(el); // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+
+
+      if (dragEl) {
+        return;
+      }
+
+      if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) {
+        return; // only left button and enabled
+      } // cancel dnd if original target is content editable
+
+
+      if (originalTarget.isContentEditable) {
+        return;
+      } // Safari ignores further event handling after mousedown
+
+
+      if (!this.nativeDraggable && Safari && target && target.tagName.toUpperCase() === 'SELECT') {
+        return;
+      }
+
+      target = closest(target, options.draggable, el, false);
+
+      if (target && target.animated) {
+        return;
+      }
+
+      if (lastDownEl === target) {
+        // Ignoring duplicate `down`
+        return;
+      } // Get the index of the dragged element within its parent
+
+
+      oldIndex = index(target);
+      oldDraggableIndex = index(target, options.draggable); // Check filter
+
+      if (typeof filter === 'function') {
+        if (filter.call(this, evt, target, this)) {
+          _dispatchEvent({
+            sortable: _this,
+            rootEl: originalTarget,
+            name: 'filter',
+            targetEl: target,
+            toEl: el,
+            fromEl: el
+          });
+
+          pluginEvent('filter', _this, {
+            evt: evt
+          });
+          preventOnFilter && evt.cancelable && evt.preventDefault();
+          return; // cancel dnd
+        }
+      } else if (filter) {
+        filter = filter.split(',').some(function (criteria) {
+          criteria = closest(originalTarget, criteria.trim(), el, false);
+
+          if (criteria) {
+            _dispatchEvent({
+              sortable: _this,
+              rootEl: criteria,
+              name: 'filter',
+              targetEl: target,
+              fromEl: el,
+              toEl: el
+            });
+
+            pluginEvent('filter', _this, {
+              evt: evt
+            });
+            return true;
+          }
+        });
+
+        if (filter) {
+          preventOnFilter && evt.cancelable && evt.preventDefault();
+          return; // cancel dnd
+        }
+      }
+
+      if (options.handle && !closest(originalTarget, options.handle, el, false)) {
+        return;
+      } // Prepare `dragstart`
+
+
+      this._prepareDragStart(evt, touch, target);
+    },
+    _prepareDragStart: function _prepareDragStart(
+    /** Event */
+    evt,
+    /** Touch */
+    touch,
+    /** HTMLElement */
+    target) {
+      var _this = this,
+          el = _this.el,
+          options = _this.options,
+          ownerDocument = el.ownerDocument,
+          dragStartFn;
+
+      if (target && !dragEl && target.parentNode === el) {
+        var dragRect = getRect(target);
+        rootEl = el;
+        dragEl = target;
+        parentEl = dragEl.parentNode;
+        nextEl = dragEl.nextSibling;
+        lastDownEl = target;
+        activeGroup = options.group;
+        Sortable.dragged = dragEl;
+        tapEvt = {
+          target: dragEl,
+          clientX: (touch || evt).clientX,
+          clientY: (touch || evt).clientY
+        };
+        tapDistanceLeft = tapEvt.clientX - dragRect.left;
+        tapDistanceTop = tapEvt.clientY - dragRect.top;
+        this._lastX = (touch || evt).clientX;
+        this._lastY = (touch || evt).clientY;
+        dragEl.style['will-change'] = 'all';
+
+        dragStartFn = function dragStartFn() {
+          pluginEvent('delayEnded', _this, {
+            evt: evt
+          });
+
+          if (Sortable.eventCanceled) {
+            _this._onDrop();
+
+            return;
+          } // Delayed drag has been triggered
+          // we can re-enable the events: touchmove/mousemove
+
+
+          _this._disableDelayedDragEvents();
+
+          if (!FireFox && _this.nativeDraggable) {
+            dragEl.draggable = true;
+          } // Bind the events: dragstart/dragend
+
+
+          _this._triggerDragStart(evt, touch); // Drag start event
+
+
+          _dispatchEvent({
+            sortable: _this,
+            name: 'choose',
+            originalEvent: evt
+          }); // Chosen item
+
+
+          toggleClass(dragEl, options.chosenClass, true);
+        }; // Disable "draggable"
+
+
+        options.ignore.split(',').forEach(function (criteria) {
+          find(dragEl, criteria.trim(), _disableDraggable);
+        });
+        on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent);
+        on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent);
+        on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent);
+        on(ownerDocument, 'mouseup', _this._onDrop);
+        on(ownerDocument, 'touchend', _this._onDrop);
+        on(ownerDocument, 'touchcancel', _this._onDrop); // Make dragEl draggable (must be before delay for FireFox)
+
+        if (FireFox && this.nativeDraggable) {
+          this.options.touchStartThreshold = 4;
+          dragEl.draggable = true;
+        }
+
+        pluginEvent('delayStart', this, {
+          evt: evt
+        }); // Delay is impossible for native DnD in Edge or IE
+
+        if (options.delay && (!options.delayOnTouchOnly || touch) && (!this.nativeDraggable || !(Edge || IE11OrLess))) {
+          if (Sortable.eventCanceled) {
+            this._onDrop();
+
+            return;
+          } // If the user moves the pointer or let go the click or touch
+          // before the delay has been reached:
+          // disable the delayed drag
+
+
+          on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+          on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+          on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+          on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler);
+          on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler);
+          options.supportPointer && on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler);
+          _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+        } else {
+          dragStartFn();
+        }
+      }
+    },
+    _delayedDragTouchMoveHandler: function _delayedDragTouchMoveHandler(
+    /** TouchEvent|PointerEvent **/
+    e) {
+      var touch = e.touches ? e.touches[0] : e;
+
+      if (Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) >= Math.floor(this.options.touchStartThreshold / (this.nativeDraggable && window.devicePixelRatio || 1))) {
+        this._disableDelayedDrag();
+      }
+    },
+    _disableDelayedDrag: function _disableDelayedDrag() {
+      dragEl && _disableDraggable(dragEl);
+      clearTimeout(this._dragStartTimer);
+
+      this._disableDelayedDragEvents();
+    },
+    _disableDelayedDragEvents: function _disableDelayedDragEvents() {
+      var ownerDocument = this.el.ownerDocument;
+      off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+      off(ownerDocument, 'touchend', this._disableDelayedDrag);
+      off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+      off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler);
+      off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler);
+      off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler);
+    },
+    _triggerDragStart: function _triggerDragStart(
+    /** Event */
+    evt,
+    /** Touch */
+    touch) {
+      touch = touch || evt.pointerType == 'touch' && evt;
+
+      if (!this.nativeDraggable || touch) {
+        if (this.options.supportPointer) {
+          on(document, 'pointermove', this._onTouchMove);
+        } else if (touch) {
+          on(document, 'touchmove', this._onTouchMove);
+        } else {
+          on(document, 'mousemove', this._onTouchMove);
+        }
+      } else {
+        on(dragEl, 'dragend', this);
+        on(rootEl, 'dragstart', this._onDragStart);
+      }
+
+      try {
+        if (document.selection) {
+          // Timeout neccessary for IE9
+          _nextTick(function () {
+            document.selection.empty();
+          });
+        } else {
+          window.getSelection().removeAllRanges();
+        }
+      } catch (err) {}
+    },
+    _dragStarted: function _dragStarted(fallback, evt) {
+
+      awaitingDragStarted = false;
+
+      if (rootEl && dragEl) {
+        pluginEvent('dragStarted', this, {
+          evt: evt
+        });
+
+        if (this.nativeDraggable) {
+          on(document, 'dragover', _checkOutsideTargetEl);
+        }
+
+        var options = this.options; // Apply effect
+
+        !fallback && toggleClass(dragEl, options.dragClass, false);
+        toggleClass(dragEl, options.ghostClass, true);
+        Sortable.active = this;
+        fallback && this._appendGhost(); // Drag start event
+
+        _dispatchEvent({
+          sortable: this,
+          name: 'start',
+          originalEvent: evt
+        });
+      } else {
+        this._nulling();
+      }
+    },
+    _emulateDragOver: function _emulateDragOver() {
+      if (touchEvt) {
+        this._lastX = touchEvt.clientX;
+        this._lastY = touchEvt.clientY;
+
+        _hideGhostForTarget();
+
+        var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+        var parent = target;
+
+        while (target && target.shadowRoot) {
+          target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+          if (target === parent) break;
+          parent = target;
+        }
+
+        dragEl.parentNode[expando]._isOutsideThisEl(target);
+
+        if (parent) {
+          do {
+            if (parent[expando]) {
+              var inserted = void 0;
+              inserted = parent[expando]._onDragOver({
+                clientX: touchEvt.clientX,
+                clientY: touchEvt.clientY,
+                target: target,
+                rootEl: parent
+              });
+
+              if (inserted && !this.options.dragoverBubble) {
+                break;
+              }
+            }
+
+            target = parent; // store last element
+          }
+          /* jshint boss:true */
+          while (parent = parent.parentNode);
+        }
+
+        _unhideGhostForTarget();
+      }
+    },
+    _onTouchMove: function _onTouchMove(
+    /**TouchEvent*/
+    evt) {
+      if (tapEvt) {
+        var options = this.options,
+            fallbackTolerance = options.fallbackTolerance,
+            fallbackOffset = options.fallbackOffset,
+            touch = evt.touches ? evt.touches[0] : evt,
+            ghostMatrix = ghostEl && matrix(ghostEl, true),
+            scaleX = ghostEl && ghostMatrix && ghostMatrix.a,
+            scaleY = ghostEl && ghostMatrix && ghostMatrix.d,
+            relativeScrollOffset = PositionGhostAbsolutely && ghostRelativeParent && getRelativeScrollOffset(ghostRelativeParent),
+            dx = (touch.clientX - tapEvt.clientX + fallbackOffset.x) / (scaleX || 1) + (relativeScrollOffset ? relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0] : 0) / (scaleX || 1),
+            dy = (touch.clientY - tapEvt.clientY + fallbackOffset.y) / (scaleY || 1) + (relativeScrollOffset ? relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1] : 0) / (scaleY || 1); // only set the status to dragging, when we are actually dragging
+
+        if (!Sortable.active && !awaitingDragStarted) {
+          if (fallbackTolerance && Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) < fallbackTolerance) {
+            return;
+          }
+
+          this._onDragStart(evt, true);
+        }
+
+        if (ghostEl) {
+          if (ghostMatrix) {
+            ghostMatrix.e += dx - (lastDx || 0);
+            ghostMatrix.f += dy - (lastDy || 0);
+          } else {
+            ghostMatrix = {
+              a: 1,
+              b: 0,
+              c: 0,
+              d: 1,
+              e: dx,
+              f: dy
+            };
+          }
+
+          var cssMatrix = "matrix(".concat(ghostMatrix.a, ",").concat(ghostMatrix.b, ",").concat(ghostMatrix.c, ",").concat(ghostMatrix.d, ",").concat(ghostMatrix.e, ",").concat(ghostMatrix.f, ")");
+          css(ghostEl, 'webkitTransform', cssMatrix);
+          css(ghostEl, 'mozTransform', cssMatrix);
+          css(ghostEl, 'msTransform', cssMatrix);
+          css(ghostEl, 'transform', cssMatrix);
+          lastDx = dx;
+          lastDy = dy;
+          touchEvt = touch;
+        }
+
+        evt.cancelable && evt.preventDefault();
+      }
+    },
+    _appendGhost: function _appendGhost() {
+      // Bug if using scale(): https://stackoverflow.com/questions/2637058
+      // Not being adjusted for
+      if (!ghostEl) {
+        var container = this.options.fallbackOnBody ? document.body : rootEl,
+            rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container),
+            options = this.options; // Position absolutely
+
+        if (PositionGhostAbsolutely) {
+          // Get relatively positioned parent
+          ghostRelativeParent = container;
+
+          while (css(ghostRelativeParent, 'position') === 'static' && css(ghostRelativeParent, 'transform') === 'none' && ghostRelativeParent !== document) {
+            ghostRelativeParent = ghostRelativeParent.parentNode;
+          }
+
+          if (ghostRelativeParent !== document.body && ghostRelativeParent !== document.documentElement) {
+            if (ghostRelativeParent === document) ghostRelativeParent = getWindowScrollingElement();
+            rect.top += ghostRelativeParent.scrollTop;
+            rect.left += ghostRelativeParent.scrollLeft;
+          } else {
+            ghostRelativeParent = getWindowScrollingElement();
+          }
+
+          ghostRelativeParentInitialScroll = getRelativeScrollOffset(ghostRelativeParent);
+        }
+
+        ghostEl = dragEl.cloneNode(true);
+        toggleClass(ghostEl, options.ghostClass, false);
+        toggleClass(ghostEl, options.fallbackClass, true);
+        toggleClass(ghostEl, options.dragClass, true);
+        css(ghostEl, 'transition', '');
+        css(ghostEl, 'transform', '');
+        css(ghostEl, 'box-sizing', 'border-box');
+        css(ghostEl, 'margin', 0);
+        css(ghostEl, 'top', rect.top);
+        css(ghostEl, 'left', rect.left);
+        css(ghostEl, 'width', rect.width);
+        css(ghostEl, 'height', rect.height);
+        css(ghostEl, 'opacity', '0.8');
+        css(ghostEl, 'position', PositionGhostAbsolutely ? 'absolute' : 'fixed');
+        css(ghostEl, 'zIndex', '100000');
+        css(ghostEl, 'pointerEvents', 'none');
+        Sortable.ghost = ghostEl;
+        container.appendChild(ghostEl); // Set transform-origin
+
+        css(ghostEl, 'transform-origin', tapDistanceLeft / parseInt(ghostEl.style.width) * 100 + '% ' + tapDistanceTop / parseInt(ghostEl.style.height) * 100 + '%');
+      }
+    },
+    _onDragStart: function _onDragStart(
+    /**Event*/
+    evt,
+    /**boolean*/
+    fallback) {
+      var _this = this;
+
+      var dataTransfer = evt.dataTransfer;
+      var options = _this.options;
+      pluginEvent('dragStart', this, {
+        evt: evt
+      });
+
+      if (Sortable.eventCanceled) {
+        this._onDrop();
+
+        return;
+      }
+
+      pluginEvent('setupClone', this);
+
+      if (!Sortable.eventCanceled) {
+        cloneEl = clone(dragEl);
+        cloneEl.draggable = false;
+        cloneEl.style['will-change'] = '';
+
+        this._hideClone();
+
+        toggleClass(cloneEl, this.options.chosenClass, false);
+        Sortable.clone = cloneEl;
+      } // #1143: IFrame support workaround
+
+
+      _this.cloneId = _nextTick(function () {
+        pluginEvent('clone', _this);
+        if (Sortable.eventCanceled) return;
+
+        if (!_this.options.removeCloneOnHide) {
+          rootEl.insertBefore(cloneEl, dragEl);
+        }
+
+        _this._hideClone();
+
+        _dispatchEvent({
+          sortable: _this,
+          name: 'clone'
+        });
+      });
+      !fallback && toggleClass(dragEl, options.dragClass, true); // Set proper drop events
+
+      if (fallback) {
+        ignoreNextClick = true;
+        _this._loopId = setInterval(_this._emulateDragOver, 50);
+      } else {
+        // Undo what was set in _prepareDragStart before drag started
+        off(document, 'mouseup', _this._onDrop);
+        off(document, 'touchend', _this._onDrop);
+        off(document, 'touchcancel', _this._onDrop);
+
+        if (dataTransfer) {
+          dataTransfer.effectAllowed = 'move';
+          options.setData && options.setData.call(_this, dataTransfer, dragEl);
+        }
+
+        on(document, 'drop', _this); // #1276 fix:
+
+        css(dragEl, 'transform', 'translateZ(0)');
+      }
+
+      awaitingDragStarted = true;
+      _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback, evt));
+      on(document, 'selectstart', _this);
+      moved = true;
+
+      if (Safari) {
+        css(document.body, 'user-select', 'none');
+      }
+    },
+    // Returns true - if no further action is needed (either inserted or another condition)
+    _onDragOver: function _onDragOver(
+    /**Event*/
+    evt) {
+      var el = this.el,
+          target = evt.target,
+          dragRect,
+          targetRect,
+          revert,
+          options = this.options,
+          group = options.group,
+          activeSortable = Sortable.active,
+          isOwner = activeGroup === group,
+          canSort = options.sort,
+          fromSortable = putSortable || activeSortable,
+          vertical,
+          _this = this,
+          completedFired = false;
+
+      if (_silent) return;
+
+      function dragOverEvent(name, extra) {
+        pluginEvent(name, _this, _objectSpread2({
+          evt: evt,
+          isOwner: isOwner,
+          axis: vertical ? 'vertical' : 'horizontal',
+          revert: revert,
+          dragRect: dragRect,
+          targetRect: targetRect,
+          canSort: canSort,
+          fromSortable: fromSortable,
+          target: target,
+          completed: completed,
+          onMove: function onMove(target, after) {
+            return _onMove(rootEl, el, dragEl, dragRect, target, getRect(target), evt, after);
+          },
+          changed: changed
+        }, extra));
+      } // Capture animation state
+
+
+      function capture() {
+        dragOverEvent('dragOverAnimationCapture');
+
+        _this.captureAnimationState();
+
+        if (_this !== fromSortable) {
+          fromSortable.captureAnimationState();
+        }
+      } // Return invocation when dragEl is inserted (or completed)
+
+
+      function completed(insertion) {
+        dragOverEvent('dragOverCompleted', {
+          insertion: insertion
+        });
+
+        if (insertion) {
+          // Clones must be hidden before folding animation to capture dragRectAbsolute properly
+          if (isOwner) {
+            activeSortable._hideClone();
+          } else {
+            activeSortable._showClone(_this);
+          }
+
+          if (_this !== fromSortable) {
+            // Set ghost class to new sortable's ghost class
+            toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false);
+            toggleClass(dragEl, options.ghostClass, true);
+          }
+
+          if (putSortable !== _this && _this !== Sortable.active) {
+            putSortable = _this;
+          } else if (_this === Sortable.active && putSortable) {
+            putSortable = null;
+          } // Animation
+
+
+          if (fromSortable === _this) {
+            _this._ignoreWhileAnimating = target;
+          }
+
+          _this.animateAll(function () {
+            dragOverEvent('dragOverAnimationComplete');
+            _this._ignoreWhileAnimating = null;
+          });
+
+          if (_this !== fromSortable) {
+            fromSortable.animateAll();
+            fromSortable._ignoreWhileAnimating = null;
+          }
+        } // Null lastTarget if it is not inside a previously swapped element
+
+
+        if (target === dragEl && !dragEl.animated || target === el && !target.animated) {
+          lastTarget = null;
+        } // no bubbling and not fallback
+
+
+        if (!options.dragoverBubble && !evt.rootEl && target !== document) {
+          dragEl.parentNode[expando]._isOutsideThisEl(evt.target); // Do not detect for empty insert if already inserted
+
+
+          !insertion && nearestEmptyInsertDetectEvent(evt);
+        }
+
+        !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation();
+        return completedFired = true;
+      } // Call when dragEl has been inserted
+
+
+      function changed() {
+        newIndex = index(dragEl);
+        newDraggableIndex = index(dragEl, options.draggable);
+
+        _dispatchEvent({
+          sortable: _this,
+          name: 'change',
+          toEl: el,
+          newIndex: newIndex,
+          newDraggableIndex: newDraggableIndex,
+          originalEvent: evt
+        });
+      }
+
+      if (evt.preventDefault !== void 0) {
+        evt.cancelable && evt.preventDefault();
+      }
+
+      target = closest(target, options.draggable, el, true);
+      dragOverEvent('dragOver');
+      if (Sortable.eventCanceled) return completedFired;
+
+      if (dragEl.contains(evt.target) || target.animated && target.animatingX && target.animatingY || _this._ignoreWhileAnimating === target) {
+        return completed(false);
+      }
+
+      ignoreNextClick = false;
+
+      if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = parentEl !== rootEl) // Reverting item into the original list
+      : putSortable === this || (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && group.checkPut(this, activeSortable, dragEl, evt))) {
+        vertical = this._getDirection(evt, target) === 'vertical';
+        dragRect = getRect(dragEl);
+        dragOverEvent('dragOverValid');
+        if (Sortable.eventCanceled) return completedFired;
+
+        if (revert) {
+          parentEl = rootEl; // actualization
+
+          capture();
+
+          this._hideClone();
+
+          dragOverEvent('revert');
+
+          if (!Sortable.eventCanceled) {
+            if (nextEl) {
+              rootEl.insertBefore(dragEl, nextEl);
+            } else {
+              rootEl.appendChild(dragEl);
+            }
+          }
+
+          return completed(true);
+        }
+
+        var elLastChild = lastChild(el, options.draggable);
+
+        if (!elLastChild || _ghostIsLast(evt, vertical, this) && !elLastChild.animated) {
+          // Insert to end of list
+          // If already at end of list: Do not insert
+          if (elLastChild === dragEl) {
+            return completed(false);
+          } // if there is a last element, it is the target
+
+
+          if (elLastChild && el === evt.target) {
+            target = elLastChild;
+          }
+
+          if (target) {
+            targetRect = getRect(target);
+          }
+
+          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
+            capture();
+            el.appendChild(dragEl);
+            parentEl = el; // actualization
+
+            changed();
+            return completed(true);
+          }
+        } else if (elLastChild && _ghostIsFirst(evt, vertical, this)) {
+          // Insert to start of list
+          var firstChild = getChild(el, 0, options, true);
+
+          if (firstChild === dragEl) {
+            return completed(false);
+          }
+
+          target = firstChild;
+          targetRect = getRect(target);
+
+          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
+            capture();
+            el.insertBefore(dragEl, firstChild);
+            parentEl = el; // actualization
+
+            changed();
+            return completed(true);
+          }
+        } else if (target.parentNode === el) {
+          targetRect = getRect(target);
+          var direction = 0,
+              targetBeforeFirstSwap,
+              differentLevel = dragEl.parentNode !== el,
+              differentRowCol = !_dragElInRowColumn(dragEl.animated && dragEl.toRect || dragRect, target.animated && target.toRect || targetRect, vertical),
+              side1 = vertical ? 'top' : 'left',
+              scrolledPastTop = isScrolledPast(target, 'top', 'top') || isScrolledPast(dragEl, 'top', 'top'),
+              scrollBefore = scrolledPastTop ? scrolledPastTop.scrollTop : void 0;
+
+          if (lastTarget !== target) {
+            targetBeforeFirstSwap = targetRect[side1];
+            pastFirstInvertThresh = false;
+            isCircumstantialInvert = !differentRowCol && options.invertSwap || differentLevel;
+          }
+
+          direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target);
+          var sibling;
+
+          if (direction !== 0) {
+            // Check if target is beside dragEl in respective direction (ignoring hidden elements)
+            var dragIndex = index(dragEl);
+
+            do {
+              dragIndex -= direction;
+              sibling = parentEl.children[dragIndex];
+            } while (sibling && (css(sibling, 'display') === 'none' || sibling === ghostEl));
+          } // If dragEl is already beside target: Do not insert
+
+
+          if (direction === 0 || sibling === target) {
+            return completed(false);
+          }
+
+          lastTarget = target;
+          lastDirection = direction;
+          var nextSibling = target.nextElementSibling,
+              after = false;
+          after = direction === 1;
+
+          var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after);
+
+          if (moveVector !== false) {
+            if (moveVector === 1 || moveVector === -1) {
+              after = moveVector === 1;
+            }
+
+            _silent = true;
+            setTimeout(_unsilent, 30);
+            capture();
+
+            if (after && !nextSibling) {
+              el.appendChild(dragEl);
+            } else {
+              target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+            } // Undo chrome's scroll adjustment (has no effect on other browsers)
+
+
+            if (scrolledPastTop) {
+              scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);
+            }
+
+            parentEl = dragEl.parentNode; // actualization
+            // must be done before animation
+
+            if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) {
+              targetMoveDistance = Math.abs(targetBeforeFirstSwap - getRect(target)[side1]);
+            }
+
+            changed();
+            return completed(true);
+          }
+        }
+
+        if (el.contains(dragEl)) {
+          return completed(false);
+        }
+      }
+
+      return false;
+    },
+    _ignoreWhileAnimating: null,
+    _offMoveEvents: function _offMoveEvents() {
+      off(document, 'mousemove', this._onTouchMove);
+      off(document, 'touchmove', this._onTouchMove);
+      off(document, 'pointermove', this._onTouchMove);
+      off(document, 'dragover', nearestEmptyInsertDetectEvent);
+      off(document, 'mousemove', nearestEmptyInsertDetectEvent);
+      off(document, 'touchmove', nearestEmptyInsertDetectEvent);
+    },
+    _offUpEvents: function _offUpEvents() {
+      var ownerDocument = this.el.ownerDocument;
+      off(ownerDocument, 'mouseup', this._onDrop);
+      off(ownerDocument, 'touchend', this._onDrop);
+      off(ownerDocument, 'pointerup', this._onDrop);
+      off(ownerDocument, 'touchcancel', this._onDrop);
+      off(document, 'selectstart', this);
+    },
+    _onDrop: function _onDrop(
+    /**Event*/
+    evt) {
+      var el = this.el,
+          options = this.options; // Get the index of the dragged element within its parent
+
+      newIndex = index(dragEl);
+      newDraggableIndex = index(dragEl, options.draggable);
+      pluginEvent('drop', this, {
+        evt: evt
+      });
+      parentEl = dragEl && dragEl.parentNode; // Get again after plugin event
+
+      newIndex = index(dragEl);
+      newDraggableIndex = index(dragEl, options.draggable);
+
+      if (Sortable.eventCanceled) {
+        this._nulling();
+
+        return;
+      }
+
+      awaitingDragStarted = false;
+      isCircumstantialInvert = false;
+      pastFirstInvertThresh = false;
+      clearInterval(this._loopId);
+      clearTimeout(this._dragStartTimer);
+
+      _cancelNextTick(this.cloneId);
+
+      _cancelNextTick(this._dragStartId); // Unbind events
+
+
+      if (this.nativeDraggable) {
+        off(document, 'drop', this);
+        off(el, 'dragstart', this._onDragStart);
+      }
+
+      this._offMoveEvents();
+
+      this._offUpEvents();
+
+      if (Safari) {
+        css(document.body, 'user-select', '');
+      }
+
+      css(dragEl, 'transform', '');
+
+      if (evt) {
+        if (moved) {
+          evt.cancelable && evt.preventDefault();
+          !options.dropBubble && evt.stopPropagation();
+        }
+
+        ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl);
+
+        if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') {
+          // Remove clone(s)
+          cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl);
+        }
+
+        if (dragEl) {
+          if (this.nativeDraggable) {
+            off(dragEl, 'dragend', this);
+          }
+
+          _disableDraggable(dragEl);
+
+          dragEl.style['will-change'] = ''; // Remove classes
+          // ghostClass is added in dragStarted
+
+          if (moved && !awaitingDragStarted) {
+            toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false);
+          }
+
+          toggleClass(dragEl, this.options.chosenClass, false); // Drag stop event
+
+          _dispatchEvent({
+            sortable: this,
+            name: 'unchoose',
+            toEl: parentEl,
+            newIndex: null,
+            newDraggableIndex: null,
+            originalEvent: evt
+          });
+
+          if (rootEl !== parentEl) {
+            if (newIndex >= 0) {
+              // Add event
+              _dispatchEvent({
+                rootEl: parentEl,
+                name: 'add',
+                toEl: parentEl,
+                fromEl: rootEl,
+                originalEvent: evt
+              }); // Remove event
+
+
+              _dispatchEvent({
+                sortable: this,
+                name: 'remove',
+                toEl: parentEl,
+                originalEvent: evt
+              }); // drag from one list and drop into another
+
+
+              _dispatchEvent({
+                rootEl: parentEl,
+                name: 'sort',
+                toEl: parentEl,
+                fromEl: rootEl,
+                originalEvent: evt
+              });
+
+              _dispatchEvent({
+                sortable: this,
+                name: 'sort',
+                toEl: parentEl,
+                originalEvent: evt
+              });
+            }
+
+            putSortable && putSortable.save();
+          } else {
+            if (newIndex !== oldIndex) {
+              if (newIndex >= 0) {
+                // drag & drop within the same list
+                _dispatchEvent({
+                  sortable: this,
+                  name: 'update',
+                  toEl: parentEl,
+                  originalEvent: evt
+                });
+
+                _dispatchEvent({
+                  sortable: this,
+                  name: 'sort',
+                  toEl: parentEl,
+                  originalEvent: evt
+                });
+              }
+            }
+          }
+
+          if (Sortable.active) {
+            /* jshint eqnull:true */
+            if (newIndex == null || newIndex === -1) {
+              newIndex = oldIndex;
+              newDraggableIndex = oldDraggableIndex;
+            }
+
+            _dispatchEvent({
+              sortable: this,
+              name: 'end',
+              toEl: parentEl,
+              originalEvent: evt
+            }); // Save sorting
+
+
+            this.save();
+          }
+        }
+      }
+
+      this._nulling();
+    },
+    _nulling: function _nulling() {
+      pluginEvent('nulling', this);
+      rootEl = dragEl = parentEl = ghostEl = nextEl = cloneEl = lastDownEl = cloneHidden = tapEvt = touchEvt = moved = newIndex = newDraggableIndex = oldIndex = oldDraggableIndex = lastTarget = lastDirection = putSortable = activeGroup = Sortable.dragged = Sortable.ghost = Sortable.clone = Sortable.active = null;
+      savedInputChecked.forEach(function (el) {
+        el.checked = true;
+      });
+      savedInputChecked.length = lastDx = lastDy = 0;
+    },
+    handleEvent: function handleEvent(
+    /**Event*/
+    evt) {
+      switch (evt.type) {
+        case 'drop':
+        case 'dragend':
+          this._onDrop(evt);
+
+          break;
+
+        case 'dragenter':
+        case 'dragover':
+          if (dragEl) {
+            this._onDragOver(evt);
+
+            _globalDragOver(evt);
+          }
+
+          break;
+
+        case 'selectstart':
+          evt.preventDefault();
+          break;
+      }
+    },
+
+    /**
+     * Serializes the item into an array of string.
+     * @returns {String[]}
+     */
+    toArray: function toArray() {
+      var order = [],
+          el,
+          children = this.el.children,
+          i = 0,
+          n = children.length,
+          options = this.options;
+
+      for (; i < n; i++) {
+        el = children[i];
+
+        if (closest(el, options.draggable, this.el, false)) {
+          order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+        }
+      }
+
+      return order;
+    },
+
+    /**
+     * Sorts the elements according to the array.
+     * @param  {String[]}  order  order of the items
+     */
+    sort: function sort(order, useAnimation) {
+      var items = {},
+          rootEl = this.el;
+      this.toArray().forEach(function (id, i) {
+        var el = rootEl.children[i];
+
+        if (closest(el, this.options.draggable, rootEl, false)) {
+          items[id] = el;
+        }
+      }, this);
+      useAnimation && this.captureAnimationState();
+      order.forEach(function (id) {
+        if (items[id]) {
+          rootEl.removeChild(items[id]);
+          rootEl.appendChild(items[id]);
+        }
+      });
+      useAnimation && this.animateAll();
+    },
+
+    /**
+     * Save the current sorting
+     */
+    save: function save() {
+      var store = this.options.store;
+      store && store.set && store.set(this);
+    },
+
+    /**
+     * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+     * @param   {HTMLElement}  el
+     * @param   {String}       [selector]  default: `options.draggable`
+     * @returns {HTMLElement|null}
+     */
+    closest: function closest$1(el, selector) {
+      return closest(el, selector || this.options.draggable, this.el, false);
+    },
+
+    /**
+     * Set/get option
+     * @param   {string} name
+     * @param   {*}      [value]
+     * @returns {*}
+     */
+    option: function option(name, value) {
+      var options = this.options;
+
+      if (value === void 0) {
+        return options[name];
+      } else {
+        var modifiedValue = PluginManager.modifyOption(this, name, value);
+
+        if (typeof modifiedValue !== 'undefined') {
+          options[name] = modifiedValue;
+        } else {
+          options[name] = value;
+        }
+
+        if (name === 'group') {
+          _prepareGroup(options);
+        }
+      }
+    },
+
+    /**
+     * Destroy
+     */
+    destroy: function destroy() {
+      pluginEvent('destroy', this);
+      var el = this.el;
+      el[expando] = null;
+      off(el, 'mousedown', this._onTapStart);
+      off(el, 'touchstart', this._onTapStart);
+      off(el, 'pointerdown', this._onTapStart);
+
+      if (this.nativeDraggable) {
+        off(el, 'dragover', this);
+        off(el, 'dragenter', this);
+      } // Remove draggable attributes
+
+
+      Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+        el.removeAttribute('draggable');
+      });
+
+      this._onDrop();
+
+      this._disableDelayedDragEvents();
+
+      sortables.splice(sortables.indexOf(this.el), 1);
+      this.el = el = null;
+    },
+    _hideClone: function _hideClone() {
+      if (!cloneHidden) {
+        pluginEvent('hideClone', this);
+        if (Sortable.eventCanceled) return;
+        css(cloneEl, 'display', 'none');
+
+        if (this.options.removeCloneOnHide && cloneEl.parentNode) {
+          cloneEl.parentNode.removeChild(cloneEl);
+        }
+
+        cloneHidden = true;
+      }
+    },
+    _showClone: function _showClone(putSortable) {
+      if (putSortable.lastPutMode !== 'clone') {
+        this._hideClone();
+
+        return;
+      }
+
+      if (cloneHidden) {
+        pluginEvent('showClone', this);
+        if (Sortable.eventCanceled) return; // show clone at dragEl or original position
+
+        if (dragEl.parentNode == rootEl && !this.options.group.revertClone) {
+          rootEl.insertBefore(cloneEl, dragEl);
+        } else if (nextEl) {
+          rootEl.insertBefore(cloneEl, nextEl);
+        } else {
+          rootEl.appendChild(cloneEl);
+        }
+
+        if (this.options.group.revertClone) {
+          this.animate(dragEl, cloneEl);
+        }
+
+        css(cloneEl, 'display', '');
+        cloneHidden = false;
+      }
+    }
+  };
+
+  function _globalDragOver(
+  /**Event*/
+  evt) {
+    if (evt.dataTransfer) {
+      evt.dataTransfer.dropEffect = 'move';
+    }
+
+    evt.cancelable && evt.preventDefault();
+  }
+
+  function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvent, willInsertAfter) {
+    var evt,
+        sortable = fromEl[expando],
+        onMoveFn = sortable.options.onMove,
+        retVal; // Support for new CustomEvent feature
+
+    if (window.CustomEvent && !IE11OrLess && !Edge) {
+      evt = new CustomEvent('move', {
+        bubbles: true,
+        cancelable: true
+      });
+    } else {
+      evt = document.createEvent('Event');
+      evt.initEvent('move', true, true);
+    }
+
+    evt.to = toEl;
+    evt.from = fromEl;
+    evt.dragged = dragEl;
+    evt.draggedRect = dragRect;
+    evt.related = targetEl || toEl;
+    evt.relatedRect = targetRect || getRect(toEl);
+    evt.willInsertAfter = willInsertAfter;
+    evt.originalEvent = originalEvent;
+    fromEl.dispatchEvent(evt);
+
+    if (onMoveFn) {
+      retVal = onMoveFn.call(sortable, evt, originalEvent);
+    }
+
+    return retVal;
+  }
+
+  function _disableDraggable(el) {
+    el.draggable = false;
+  }
+
+  function _unsilent() {
+    _silent = false;
+  }
+
+  function _ghostIsFirst(evt, vertical, sortable) {
+    var rect = getRect(getChild(sortable.el, 0, sortable.options, true));
+    var spacer = 10;
+    return vertical ? evt.clientX < rect.left - spacer || evt.clientY < rect.top && evt.clientX < rect.right : evt.clientY < rect.top - spacer || evt.clientY < rect.bottom && evt.clientX < rect.left;
+  }
+
+  function _ghostIsLast(evt, vertical, sortable) {
+    var rect = getRect(lastChild(sortable.el, sortable.options.draggable));
+    var spacer = 10;
+    return vertical ? evt.clientX > rect.right + spacer || evt.clientX <= rect.right && evt.clientY > rect.bottom && evt.clientX >= rect.left : evt.clientX > rect.right && evt.clientY > rect.top || evt.clientX <= rect.right && evt.clientY > rect.bottom + spacer;
+  }
+
+  function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) {
+    var mouseOnAxis = vertical ? evt.clientY : evt.clientX,
+        targetLength = vertical ? targetRect.height : targetRect.width,
+        targetS1 = vertical ? targetRect.top : targetRect.left,
+        targetS2 = vertical ? targetRect.bottom : targetRect.right,
+        invert = false;
+
+    if (!invertSwap) {
+      // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold
+      if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) {
+        // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2
+        // check if past first invert threshold on side opposite of lastDirection
+        if (!pastFirstInvertThresh && (lastDirection === 1 ? mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 : mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2)) {
+          // past first invert threshold, do not restrict inverted threshold to dragEl shadow
+          pastFirstInvertThresh = true;
+        }
+
+        if (!pastFirstInvertThresh) {
+          // dragEl shadow (target move distance shadow)
+          if (lastDirection === 1 ? mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow
+          : mouseOnAxis > targetS2 - targetMoveDistance) {
+            return -lastDirection;
+          }
+        } else {
+          invert = true;
+        }
+      } else {
+        // Regular
+        if (mouseOnAxis > targetS1 + targetLength * (1 - swapThreshold) / 2 && mouseOnAxis < targetS2 - targetLength * (1 - swapThreshold) / 2) {
+          return _getInsertDirection(target);
+        }
+      }
+    }
+
+    invert = invert || invertSwap;
+
+    if (invert) {
+      // Invert of regular
+      if (mouseOnAxis < targetS1 + targetLength * invertedSwapThreshold / 2 || mouseOnAxis > targetS2 - targetLength * invertedSwapThreshold / 2) {
+        return mouseOnAxis > targetS1 + targetLength / 2 ? 1 : -1;
+      }
+    }
+
+    return 0;
+  }
+  /**
+   * Gets the direction dragEl must be swapped relative to target in order to make it
+   * seem that dragEl has been "inserted" into that element's position
+   * @param  {HTMLElement} target       The target whose position dragEl is being inserted at
+   * @return {Number}                   Direction dragEl must be swapped
+   */
+
+
+  function _getInsertDirection(target) {
+    if (index(dragEl) < index(target)) {
+      return 1;
+    } else {
+      return -1;
+    }
+  }
+  /**
+   * Generate id
+   * @param   {HTMLElement} el
+   * @returns {String}
+   * @private
+   */
+
+
+  function _generateId(el) {
+    var str = el.tagName + el.className + el.src + el.href + el.textContent,
+        i = str.length,
+        sum = 0;
+
+    while (i--) {
+      sum += str.charCodeAt(i);
+    }
+
+    return sum.toString(36);
+  }
+
+  function _saveInputCheckedState(root) {
+    savedInputChecked.length = 0;
+    var inputs = root.getElementsByTagName('input');
+    var idx = inputs.length;
+
+    while (idx--) {
+      var el = inputs[idx];
+      el.checked && savedInputChecked.push(el);
+    }
+  }
+
+  function _nextTick(fn) {
+    return setTimeout(fn, 0);
+  }
+
+  function _cancelNextTick(id) {
+    return clearTimeout(id);
+  } // Fixed #973:
+
+
+  if (documentExists) {
+    on(document, 'touchmove', function (evt) {
+      if ((Sortable.active || awaitingDragStarted) && evt.cancelable) {
+        evt.preventDefault();
+      }
+    });
+  } // Export utils
+
+
+  Sortable.utils = {
+    on: on,
+    off: off,
+    css: css,
+    find: find,
+    is: function is(el, selector) {
+      return !!closest(el, selector, el, false);
+    },
+    extend: extend,
+    throttle: throttle,
+    closest: closest,
+    toggleClass: toggleClass,
+    clone: clone,
+    index: index,
+    nextTick: _nextTick,
+    cancelNextTick: _cancelNextTick,
+    detectDirection: _detectDirection,
+    getChild: getChild
+  };
+  /**
+   * Get the Sortable instance of an element
+   * @param  {HTMLElement} element The element
+   * @return {Sortable|undefined}         The instance of Sortable
+   */
+
+  Sortable.get = function (element) {
+    return element[expando];
+  };
+  /**
+   * Mount a plugin to Sortable
+   * @param  {...SortablePlugin|SortablePlugin[]} plugins       Plugins being mounted
+   */
+
+
+  Sortable.mount = function () {
+    for (var _len = arguments.length, plugins = new Array(_len), _key = 0; _key < _len; _key++) {
+      plugins[_key] = arguments[_key];
+    }
+
+    if (plugins[0].constructor === Array) plugins = plugins[0];
+    plugins.forEach(function (plugin) {
+      if (!plugin.prototype || !plugin.prototype.constructor) {
+        throw "Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(plugin));
+      }
+
+      if (plugin.utils) Sortable.utils = _objectSpread2(_objectSpread2({}, Sortable.utils), plugin.utils);
+      PluginManager.mount(plugin);
+    });
+  };
+  /**
+   * Create sortable instance
+   * @param {HTMLElement}  el
+   * @param {Object}      [options]
+   */
+
+
+  Sortable.create = function (el, options) {
+    return new Sortable(el, options);
+  }; // Export
+
+
+  Sortable.version = version;
+
+  var autoScrolls = [],
+      scrollEl,
+      scrollRootEl,
+      scrolling = false,
+      lastAutoScrollX,
+      lastAutoScrollY,
+      touchEvt$1,
+      pointerElemChangedInterval;
+
+  function AutoScrollPlugin() {
+    function AutoScroll() {
+      this.defaults = {
+        scroll: true,
+        forceAutoScrollFallback: false,
+        scrollSensitivity: 30,
+        scrollSpeed: 10,
+        bubbleScroll: true
+      }; // Bind all private methods
+
+      for (var fn in this) {
+        if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+          this[fn] = this[fn].bind(this);
+        }
+      }
+    }
+
+    AutoScroll.prototype = {
+      dragStarted: function dragStarted(_ref) {
+        var originalEvent = _ref.originalEvent;
+
+        if (this.sortable.nativeDraggable) {
+          on(document, 'dragover', this._handleAutoScroll);
+        } else {
+          if (this.options.supportPointer) {
+            on(document, 'pointermove', this._handleFallbackAutoScroll);
+          } else if (originalEvent.touches) {
+            on(document, 'touchmove', this._handleFallbackAutoScroll);
+          } else {
+            on(document, 'mousemove', this._handleFallbackAutoScroll);
+          }
+        }
+      },
+      dragOverCompleted: function dragOverCompleted(_ref2) {
+        var originalEvent = _ref2.originalEvent;
+
+        // For when bubbling is canceled and using fallback (fallback 'touchmove' always reached)
+        if (!this.options.dragOverBubble && !originalEvent.rootEl) {
+          this._handleAutoScroll(originalEvent);
+        }
+      },
+      drop: function drop() {
+        if (this.sortable.nativeDraggable) {
+          off(document, 'dragover', this._handleAutoScroll);
+        } else {
+          off(document, 'pointermove', this._handleFallbackAutoScroll);
+          off(document, 'touchmove', this._handleFallbackAutoScroll);
+          off(document, 'mousemove', this._handleFallbackAutoScroll);
+        }
+
+        clearPointerElemChangedInterval();
+        clearAutoScrolls();
+        cancelThrottle();
+      },
+      nulling: function nulling() {
+        touchEvt$1 = scrollRootEl = scrollEl = scrolling = pointerElemChangedInterval = lastAutoScrollX = lastAutoScrollY = null;
+        autoScrolls.length = 0;
+      },
+      _handleFallbackAutoScroll: function _handleFallbackAutoScroll(evt) {
+        this._handleAutoScroll(evt, true);
+      },
+      _handleAutoScroll: function _handleAutoScroll(evt, fallback) {
+        var _this = this;
+
+        var x = (evt.touches ? evt.touches[0] : evt).clientX,
+            y = (evt.touches ? evt.touches[0] : evt).clientY,
+            elem = document.elementFromPoint(x, y);
+        touchEvt$1 = evt; // IE does not seem to have native autoscroll,
+        // Edge's autoscroll seems too conditional,
+        // MACOS Safari does not have autoscroll,
+        // Firefox and Chrome are good
+
+        if (fallback || this.options.forceAutoScrollFallback || Edge || IE11OrLess || Safari) {
+          autoScroll(evt, this.options, elem, fallback); // Listener for pointer element change
+
+          var ogElemScroller = getParentAutoScrollElement(elem, true);
+
+          if (scrolling && (!pointerElemChangedInterval || x !== lastAutoScrollX || y !== lastAutoScrollY)) {
+            pointerElemChangedInterval && clearPointerElemChangedInterval(); // Detect for pointer elem change, emulating native DnD behaviour
+
+            pointerElemChangedInterval = setInterval(function () {
+              var newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true);
+
+              if (newElem !== ogElemScroller) {
+                ogElemScroller = newElem;
+                clearAutoScrolls();
+              }
+
+              autoScroll(evt, _this.options, newElem, fallback);
+            }, 10);
+            lastAutoScrollX = x;
+            lastAutoScrollY = y;
+          }
+        } else {
+          // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll
+          if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) {
+            clearAutoScrolls();
+            return;
+          }
+
+          autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false);
+        }
+      }
+    };
+    return _extends(AutoScroll, {
+      pluginName: 'scroll',
+      initializeByDefault: true
+    });
+  }
+
+  function clearAutoScrolls() {
+    autoScrolls.forEach(function (autoScroll) {
+      clearInterval(autoScroll.pid);
+    });
+    autoScrolls = [];
+  }
+
+  function clearPointerElemChangedInterval() {
+    clearInterval(pointerElemChangedInterval);
+  }
+
+  var autoScroll = throttle(function (evt, options, rootEl, isFallback) {
+    // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+    if (!options.scroll) return;
+    var x = (evt.touches ? evt.touches[0] : evt).clientX,
+        y = (evt.touches ? evt.touches[0] : evt).clientY,
+        sens = options.scrollSensitivity,
+        speed = options.scrollSpeed,
+        winScroller = getWindowScrollingElement();
+    var scrollThisInstance = false,
+        scrollCustomFn; // New scroll root, set scrollEl
+
+    if (scrollRootEl !== rootEl) {
+      scrollRootEl = rootEl;
+      clearAutoScrolls();
+      scrollEl = options.scroll;
+      scrollCustomFn = options.scrollFn;
+
+      if (scrollEl === true) {
+        scrollEl = getParentAutoScrollElement(rootEl, true);
+      }
+    }
+
+    var layersOut = 0;
+    var currentParent = scrollEl;
+
+    do {
+      var el = currentParent,
+          rect = getRect(el),
+          top = rect.top,
+          bottom = rect.bottom,
+          left = rect.left,
+          right = rect.right,
+          width = rect.width,
+          height = rect.height,
+          canScrollX = void 0,
+          canScrollY = void 0,
+          scrollWidth = el.scrollWidth,
+          scrollHeight = el.scrollHeight,
+          elCSS = css(el),
+          scrollPosX = el.scrollLeft,
+          scrollPosY = el.scrollTop;
+
+      if (el === winScroller) {
+        canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll' || elCSS.overflowX === 'visible');
+        canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll' || elCSS.overflowY === 'visible');
+      } else {
+        canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll');
+        canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll');
+      }
+
+      var vx = canScrollX && (Math.abs(right - x) <= sens && scrollPosX + width < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX);
+      var vy = canScrollY && (Math.abs(bottom - y) <= sens && scrollPosY + height < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY);
+
+      if (!autoScrolls[layersOut]) {
+        for (var i = 0; i <= layersOut; i++) {
+          if (!autoScrolls[i]) {
+            autoScrolls[i] = {};
+          }
+        }
+      }
+
+      if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) {
+        autoScrolls[layersOut].el = el;
+        autoScrolls[layersOut].vx = vx;
+        autoScrolls[layersOut].vy = vy;
+        clearInterval(autoScrolls[layersOut].pid);
+
+        if (vx != 0 || vy != 0) {
+          scrollThisInstance = true;
+          /* jshint loopfunc:true */
+
+          autoScrolls[layersOut].pid = setInterval(function () {
+            // emulate drag over during autoscroll (fallback), emulating native DnD behaviour
+            if (isFallback && this.layer === 0) {
+              Sortable.active._onTouchMove(touchEvt$1); // To move ghost if it is positioned absolutely
+
+            }
+
+            var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0;
+            var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0;
+
+            if (typeof scrollCustomFn === 'function') {
+              if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt$1, autoScrolls[this.layer].el) !== 'continue') {
+                return;
+              }
+            }
+
+            scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY);
+          }.bind({
+            layer: layersOut
+          }), 24);
+        }
+      }
+
+      layersOut++;
+    } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false)));
+
+    scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not
+  }, 30);
+
+  var drop = function drop(_ref) {
+    var originalEvent = _ref.originalEvent,
+        putSortable = _ref.putSortable,
+        dragEl = _ref.dragEl,
+        activeSortable = _ref.activeSortable,
+        dispatchSortableEvent = _ref.dispatchSortableEvent,
+        hideGhostForTarget = _ref.hideGhostForTarget,
+        unhideGhostForTarget = _ref.unhideGhostForTarget;
+    if (!originalEvent) return;
+    var toSortable = putSortable || activeSortable;
+    hideGhostForTarget();
+    var touch = originalEvent.changedTouches && originalEvent.changedTouches.length ? originalEvent.changedTouches[0] : originalEvent;
+    var target = document.elementFromPoint(touch.clientX, touch.clientY);
+    unhideGhostForTarget();
+
+    if (toSortable && !toSortable.el.contains(target)) {
+      dispatchSortableEvent('spill');
+      this.onSpill({
+        dragEl: dragEl,
+        putSortable: putSortable
+      });
+    }
+  };
+
+  function Revert() {}
+
+  Revert.prototype = {
+    startIndex: null,
+    dragStart: function dragStart(_ref2) {
+      var oldDraggableIndex = _ref2.oldDraggableIndex;
+      this.startIndex = oldDraggableIndex;
+    },
+    onSpill: function onSpill(_ref3) {
+      var dragEl = _ref3.dragEl,
+          putSortable = _ref3.putSortable;
+      this.sortable.captureAnimationState();
+
+      if (putSortable) {
+        putSortable.captureAnimationState();
+      }
+
+      var nextSibling = getChild(this.sortable.el, this.startIndex, this.options);
+
+      if (nextSibling) {
+        this.sortable.el.insertBefore(dragEl, nextSibling);
+      } else {
+        this.sortable.el.appendChild(dragEl);
+      }
+
+      this.sortable.animateAll();
+
+      if (putSortable) {
+        putSortable.animateAll();
+      }
+    },
+    drop: drop
+  };
+
+  _extends(Revert, {
+    pluginName: 'revertOnSpill'
+  });
+
+  function Remove() {}
+
+  Remove.prototype = {
+    onSpill: function onSpill(_ref4) {
+      var dragEl = _ref4.dragEl,
+          putSortable = _ref4.putSortable;
+      var parentSortable = putSortable || this.sortable;
+      parentSortable.captureAnimationState();
+      dragEl.parentNode && dragEl.parentNode.removeChild(dragEl);
+      parentSortable.animateAll();
+    },
+    drop: drop
+  };
+
+  _extends(Remove, {
+    pluginName: 'removeOnSpill'
+  });
+
+  var lastSwapEl;
+
+  function SwapPlugin() {
+    function Swap() {
+      this.defaults = {
+        swapClass: 'sortable-swap-highlight'
+      };
+    }
+
+    Swap.prototype = {
+      dragStart: function dragStart(_ref) {
+        var dragEl = _ref.dragEl;
+        lastSwapEl = dragEl;
+      },
+      dragOverValid: function dragOverValid(_ref2) {
+        var completed = _ref2.completed,
+            target = _ref2.target,
+            onMove = _ref2.onMove,
+            activeSortable = _ref2.activeSortable,
+            changed = _ref2.changed,
+            cancel = _ref2.cancel;
+        if (!activeSortable.options.swap) return;
+        var el = this.sortable.el,
+            options = this.options;
+
+        if (target && target !== el) {
+          var prevSwapEl = lastSwapEl;
+
+          if (onMove(target) !== false) {
+            toggleClass(target, options.swapClass, true);
+            lastSwapEl = target;
+          } else {
+            lastSwapEl = null;
+          }
+
+          if (prevSwapEl && prevSwapEl !== lastSwapEl) {
+            toggleClass(prevSwapEl, options.swapClass, false);
+          }
+        }
+
+        changed();
+        completed(true);
+        cancel();
+      },
+      drop: function drop(_ref3) {
+        var activeSortable = _ref3.activeSortable,
+            putSortable = _ref3.putSortable,
+            dragEl = _ref3.dragEl;
+        var toSortable = putSortable || this.sortable;
+        var options = this.options;
+        lastSwapEl && toggleClass(lastSwapEl, options.swapClass, false);
+
+        if (lastSwapEl && (options.swap || putSortable && putSortable.options.swap)) {
+          if (dragEl !== lastSwapEl) {
+            toSortable.captureAnimationState();
+            if (toSortable !== activeSortable) activeSortable.captureAnimationState();
+            swapNodes(dragEl, lastSwapEl);
+            toSortable.animateAll();
+            if (toSortable !== activeSortable) activeSortable.animateAll();
+          }
+        }
+      },
+      nulling: function nulling() {
+        lastSwapEl = null;
+      }
+    };
+    return _extends(Swap, {
+      pluginName: 'swap',
+      eventProperties: function eventProperties() {
+        return {
+          swapItem: lastSwapEl
+        };
+      }
+    });
+  }
+
+  function swapNodes(n1, n2) {
+    var p1 = n1.parentNode,
+        p2 = n2.parentNode,
+        i1,
+        i2;
+    if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return;
+    i1 = index(n1);
+    i2 = index(n2);
+
+    if (p1.isEqualNode(p2) && i1 < i2) {
+      i2++;
+    }
+
+    p1.insertBefore(n2, p1.children[i1]);
+    p2.insertBefore(n1, p2.children[i2]);
+  }
+
+  var multiDragElements = [],
+      multiDragClones = [],
+      lastMultiDragSelect,
+      // for selection with modifier key down (SHIFT)
+  multiDragSortable,
+      initialFolding = false,
+      // Initial multi-drag fold when drag started
+  folding = false,
+      // Folding any other time
+  dragStarted = false,
+      dragEl$1,
+      clonesFromRect,
+      clonesHidden;
+
+  function MultiDragPlugin() {
+    function MultiDrag(sortable) {
+      // Bind all private methods
+      for (var fn in this) {
+        if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+          this[fn] = this[fn].bind(this);
+        }
+      }
+
+      if (sortable.options.supportPointer) {
+        on(document, 'pointerup', this._deselectMultiDrag);
+      } else {
+        on(document, 'mouseup', this._deselectMultiDrag);
+        on(document, 'touchend', this._deselectMultiDrag);
+      }
+
+      on(document, 'keydown', this._checkKeyDown);
+      on(document, 'keyup', this._checkKeyUp);
+      this.defaults = {
+        selectedClass: 'sortable-selected',
+        multiDragKey: null,
+        setData: function setData(dataTransfer, dragEl) {
+          var data = '';
+
+          if (multiDragElements.length && multiDragSortable === sortable) {
+            multiDragElements.forEach(function (multiDragElement, i) {
+              data += (!i ? '' : ', ') + multiDragElement.textContent;
+            });
+          } else {
+            data = dragEl.textContent;
+          }
+
+          dataTransfer.setData('Text', data);
+        }
+      };
+    }
+
+    MultiDrag.prototype = {
+      multiDragKeyDown: false,
+      isMultiDrag: false,
+      delayStartGlobal: function delayStartGlobal(_ref) {
+        var dragged = _ref.dragEl;
+        dragEl$1 = dragged;
+      },
+      delayEnded: function delayEnded() {
+        this.isMultiDrag = ~multiDragElements.indexOf(dragEl$1);
+      },
+      setupClone: function setupClone(_ref2) {
+        var sortable = _ref2.sortable,
+            cancel = _ref2.cancel;
+        if (!this.isMultiDrag) return;
+
+        for (var i = 0; i < multiDragElements.length; i++) {
+          multiDragClones.push(clone(multiDragElements[i]));
+          multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex;
+          multiDragClones[i].draggable = false;
+          multiDragClones[i].style['will-change'] = '';
+          toggleClass(multiDragClones[i], this.options.selectedClass, false);
+          multiDragElements[i] === dragEl$1 && toggleClass(multiDragClones[i], this.options.chosenClass, false);
+        }
+
+        sortable._hideClone();
+
+        cancel();
+      },
+      clone: function clone(_ref3) {
+        var sortable = _ref3.sortable,
+            rootEl = _ref3.rootEl,
+            dispatchSortableEvent = _ref3.dispatchSortableEvent,
+            cancel = _ref3.cancel;
+        if (!this.isMultiDrag) return;
+
+        if (!this.options.removeCloneOnHide) {
+          if (multiDragElements.length && multiDragSortable === sortable) {
+            insertMultiDragClones(true, rootEl);
+            dispatchSortableEvent('clone');
+            cancel();
+          }
+        }
+      },
+      showClone: function showClone(_ref4) {
+        var cloneNowShown = _ref4.cloneNowShown,
+            rootEl = _ref4.rootEl,
+            cancel = _ref4.cancel;
+        if (!this.isMultiDrag) return;
+        insertMultiDragClones(false, rootEl);
+        multiDragClones.forEach(function (clone) {
+          css(clone, 'display', '');
+        });
+        cloneNowShown();
+        clonesHidden = false;
+        cancel();
+      },
+      hideClone: function hideClone(_ref5) {
+        var _this = this;
+
+        var sortable = _ref5.sortable,
+            cloneNowHidden = _ref5.cloneNowHidden,
+            cancel = _ref5.cancel;
+        if (!this.isMultiDrag) return;
+        multiDragClones.forEach(function (clone) {
+          css(clone, 'display', 'none');
+
+          if (_this.options.removeCloneOnHide && clone.parentNode) {
+            clone.parentNode.removeChild(clone);
+          }
+        });
+        cloneNowHidden();
+        clonesHidden = true;
+        cancel();
+      },
+      dragStartGlobal: function dragStartGlobal(_ref6) {
+        var sortable = _ref6.sortable;
+
+        if (!this.isMultiDrag && multiDragSortable) {
+          multiDragSortable.multiDrag._deselectMultiDrag();
+        }
+
+        multiDragElements.forEach(function (multiDragElement) {
+          multiDragElement.sortableIndex = index(multiDragElement);
+        }); // Sort multi-drag elements
+
+        multiDragElements = multiDragElements.sort(function (a, b) {
+          return a.sortableIndex - b.sortableIndex;
+        });
+        dragStarted = true;
+      },
+      dragStarted: function dragStarted(_ref7) {
+        var _this2 = this;
+
+        var sortable = _ref7.sortable;
+        if (!this.isMultiDrag) return;
+
+        if (this.options.sort) {
+          // Capture rects,
+          // hide multi drag elements (by positioning them absolute),
+          // set multi drag elements rects to dragRect,
+          // show multi drag elements,
+          // animate to rects,
+          // unset rects & remove from DOM
+          sortable.captureAnimationState();
+
+          if (this.options.animation) {
+            multiDragElements.forEach(function (multiDragElement) {
+              if (multiDragElement === dragEl$1) return;
+              css(multiDragElement, 'position', 'absolute');
+            });
+            var dragRect = getRect(dragEl$1, false, true, true);
+            multiDragElements.forEach(function (multiDragElement) {
+              if (multiDragElement === dragEl$1) return;
+              setRect(multiDragElement, dragRect);
+            });
+            folding = true;
+            initialFolding = true;
+          }
+        }
+
+        sortable.animateAll(function () {
+          folding = false;
+          initialFolding = false;
+
+          if (_this2.options.animation) {
+            multiDragElements.forEach(function (multiDragElement) {
+              unsetRect(multiDragElement);
+            });
+          } // Remove all auxiliary multidrag items from el, if sorting enabled
+
+
+          if (_this2.options.sort) {
+            removeMultiDragElements();
+          }
+        });
+      },
+      dragOver: function dragOver(_ref8) {
+        var target = _ref8.target,
+            completed = _ref8.completed,
+            cancel = _ref8.cancel;
+
+        if (folding && ~multiDragElements.indexOf(target)) {
+          completed(false);
+          cancel();
+        }
+      },
+      revert: function revert(_ref9) {
+        var fromSortable = _ref9.fromSortable,
+            rootEl = _ref9.rootEl,
+            sortable = _ref9.sortable,
+            dragRect = _ref9.dragRect;
+
+        if (multiDragElements.length > 1) {
+          // Setup unfold animation
+          multiDragElements.forEach(function (multiDragElement) {
+            sortable.addAnimationState({
+              target: multiDragElement,
+              rect: folding ? getRect(multiDragElement) : dragRect
+            });
+            unsetRect(multiDragElement);
+            multiDragElement.fromRect = dragRect;
+            fromSortable.removeAnimationState(multiDragElement);
+          });
+          folding = false;
+          insertMultiDragElements(!this.options.removeCloneOnHide, rootEl);
+        }
+      },
+      dragOverCompleted: function dragOverCompleted(_ref10) {
+        var sortable = _ref10.sortable,
+            isOwner = _ref10.isOwner,
+            insertion = _ref10.insertion,
+            activeSortable = _ref10.activeSortable,
+            parentEl = _ref10.parentEl,
+            putSortable = _ref10.putSortable;
+        var options = this.options;
+
+        if (insertion) {
+          // Clones must be hidden before folding animation to capture dragRectAbsolute properly
+          if (isOwner) {
+            activeSortable._hideClone();
+          }
+
+          initialFolding = false; // If leaving sort:false root, or already folding - Fold to new location
+
+          if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) {
+            // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible
+            var dragRectAbsolute = getRect(dragEl$1, false, true, true);
+            multiDragElements.forEach(function (multiDragElement) {
+              if (multiDragElement === dragEl$1) return;
+              setRect(multiDragElement, dragRectAbsolute); // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted
+              // while folding, and so that we can capture them again because old sortable will no longer be fromSortable
+
+              parentEl.appendChild(multiDragElement);
+            });
+            folding = true;
+          } // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out
+
+
+          if (!isOwner) {
+            // Only remove if not folding (folding will remove them anyways)
+            if (!folding) {
+              removeMultiDragElements();
+            }
+
+            if (multiDragElements.length > 1) {
+              var clonesHiddenBefore = clonesHidden;
+
+              activeSortable._showClone(sortable); // Unfold animation for clones if showing from hidden
+
+
+              if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) {
+                multiDragClones.forEach(function (clone) {
+                  activeSortable.addAnimationState({
+                    target: clone,
+                    rect: clonesFromRect
+                  });
+                  clone.fromRect = clonesFromRect;
+                  clone.thisAnimationDuration = null;
+                });
+              }
+            } else {
+              activeSortable._showClone(sortable);
+            }
+          }
+        }
+      },
+      dragOverAnimationCapture: function dragOverAnimationCapture(_ref11) {
+        var dragRect = _ref11.dragRect,
+            isOwner = _ref11.isOwner,
+            activeSortable = _ref11.activeSortable;
+        multiDragElements.forEach(function (multiDragElement) {
+          multiDragElement.thisAnimationDuration = null;
+        });
+
+        if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) {
+          clonesFromRect = _extends({}, dragRect);
+          var dragMatrix = matrix(dragEl$1, true);
+          clonesFromRect.top -= dragMatrix.f;
+          clonesFromRect.left -= dragMatrix.e;
+        }
+      },
+      dragOverAnimationComplete: function dragOverAnimationComplete() {
+        if (folding) {
+          folding = false;
+          removeMultiDragElements();
+        }
+      },
+      drop: function drop(_ref12) {
+        var evt = _ref12.originalEvent,
+            rootEl = _ref12.rootEl,
+            parentEl = _ref12.parentEl,
+            sortable = _ref12.sortable,
+            dispatchSortableEvent = _ref12.dispatchSortableEvent,
+            oldIndex = _ref12.oldIndex,
+            putSortable = _ref12.putSortable;
+        var toSortable = putSortable || this.sortable;
+        if (!evt) return;
+        var options = this.options,
+            children = parentEl.children; // Multi-drag selection
+
+        if (!dragStarted) {
+          if (options.multiDragKey && !this.multiDragKeyDown) {
+            this._deselectMultiDrag();
+          }
+
+          toggleClass(dragEl$1, options.selectedClass, !~multiDragElements.indexOf(dragEl$1));
+
+          if (!~multiDragElements.indexOf(dragEl$1)) {
+            multiDragElements.push(dragEl$1);
+            dispatchEvent({
+              sortable: sortable,
+              rootEl: rootEl,
+              name: 'select',
+              targetEl: dragEl$1,
+              originalEvt: evt
+            }); // Modifier activated, select from last to dragEl
+
+            if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) {
+              var lastIndex = index(lastMultiDragSelect),
+                  currentIndex = index(dragEl$1);
+
+              if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) {
+                // Must include lastMultiDragSelect (select it), in case modified selection from no selection
+                // (but previous selection existed)
+                var n, i;
+
+                if (currentIndex > lastIndex) {
+                  i = lastIndex;
+                  n = currentIndex;
+                } else {
+                  i = currentIndex;
+                  n = lastIndex + 1;
+                }
+
+                for (; i < n; i++) {
+                  if (~multiDragElements.indexOf(children[i])) continue;
+                  toggleClass(children[i], options.selectedClass, true);
+                  multiDragElements.push(children[i]);
+                  dispatchEvent({
+                    sortable: sortable,
+                    rootEl: rootEl,
+                    name: 'select',
+                    targetEl: children[i],
+                    originalEvt: evt
+                  });
+                }
+              }
+            } else {
+              lastMultiDragSelect = dragEl$1;
+            }
+
+            multiDragSortable = toSortable;
+          } else {
+            multiDragElements.splice(multiDragElements.indexOf(dragEl$1), 1);
+            lastMultiDragSelect = null;
+            dispatchEvent({
+              sortable: sortable,
+              rootEl: rootEl,
+              name: 'deselect',
+              targetEl: dragEl$1,
+              originalEvt: evt
+            });
+          }
+        } // Multi-drag drop
+
+
+        if (dragStarted && this.isMultiDrag) {
+          folding = false; // Do not "unfold" after around dragEl if reverted
+
+          if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) {
+            var dragRect = getRect(dragEl$1),
+                multiDragIndex = index(dragEl$1, ':not(.' + this.options.selectedClass + ')');
+            if (!initialFolding && options.animation) dragEl$1.thisAnimationDuration = null;
+            toSortable.captureAnimationState();
+
+            if (!initialFolding) {
+              if (options.animation) {
+                dragEl$1.fromRect = dragRect;
+                multiDragElements.forEach(function (multiDragElement) {
+                  multiDragElement.thisAnimationDuration = null;
+
+                  if (multiDragElement !== dragEl$1) {
+                    var rect = folding ? getRect(multiDragElement) : dragRect;
+                    multiDragElement.fromRect = rect; // Prepare unfold animation
+
+                    toSortable.addAnimationState({
+                      target: multiDragElement,
+                      rect: rect
+                    });
+                  }
+                });
+              } // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert
+              // properly they must all be removed
+
+
+              removeMultiDragElements();
+              multiDragElements.forEach(function (multiDragElement) {
+                if (children[multiDragIndex]) {
+                  parentEl.insertBefore(multiDragElement, children[multiDragIndex]);
+                } else {
+                  parentEl.appendChild(multiDragElement);
+                }
+
+                multiDragIndex++;
+              }); // If initial folding is done, the elements may have changed position because they are now
+              // unfolding around dragEl, even though dragEl may not have his index changed, so update event
+              // must be fired here as Sortable will not.
+
+              if (oldIndex === index(dragEl$1)) {
+                var update = false;
+                multiDragElements.forEach(function (multiDragElement) {
+                  if (multiDragElement.sortableIndex !== index(multiDragElement)) {
+                    update = true;
+                    return;
+                  }
+                });
+
+                if (update) {
+                  dispatchSortableEvent('update');
+                }
+              }
+            } // Must be done after capturing individual rects (scroll bar)
+
+
+            multiDragElements.forEach(function (multiDragElement) {
+              unsetRect(multiDragElement);
+            });
+            toSortable.animateAll();
+          }
+
+          multiDragSortable = toSortable;
+        } // Remove clones if necessary
+
+
+        if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') {
+          multiDragClones.forEach(function (clone) {
+            clone.parentNode && clone.parentNode.removeChild(clone);
+          });
+        }
+      },
+      nullingGlobal: function nullingGlobal() {
+        this.isMultiDrag = dragStarted = false;
+        multiDragClones.length = 0;
+      },
+      destroyGlobal: function destroyGlobal() {
+        this._deselectMultiDrag();
+
+        off(document, 'pointerup', this._deselectMultiDrag);
+        off(document, 'mouseup', this._deselectMultiDrag);
+        off(document, 'touchend', this._deselectMultiDrag);
+        off(document, 'keydown', this._checkKeyDown);
+        off(document, 'keyup', this._checkKeyUp);
+      },
+      _deselectMultiDrag: function _deselectMultiDrag(evt) {
+        if (typeof dragStarted !== "undefined" && dragStarted) return; // Only deselect if selection is in this sortable
+
+        if (multiDragSortable !== this.sortable) return; // Only deselect if target is not item in this sortable
+
+        if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return; // Only deselect if left click
+
+        if (evt && evt.button !== 0) return;
+
+        while (multiDragElements.length) {
+          var el = multiDragElements[0];
+          toggleClass(el, this.options.selectedClass, false);
+          multiDragElements.shift();
+          dispatchEvent({
+            sortable: this.sortable,
+            rootEl: this.sortable.el,
+            name: 'deselect',
+            targetEl: el,
+            originalEvt: evt
+          });
+        }
+      },
+      _checkKeyDown: function _checkKeyDown(evt) {
+        if (evt.key === this.options.multiDragKey) {
+          this.multiDragKeyDown = true;
+        }
+      },
+      _checkKeyUp: function _checkKeyUp(evt) {
+        if (evt.key === this.options.multiDragKey) {
+          this.multiDragKeyDown = false;
+        }
+      }
+    };
+    return _extends(MultiDrag, {
+      // Static methods & properties
+      pluginName: 'multiDrag',
+      utils: {
+        /**
+         * Selects the provided multi-drag item
+         * @param  {HTMLElement} el    The element to be selected
+         */
+        select: function select(el) {
+          var sortable = el.parentNode[expando];
+          if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return;
+
+          if (multiDragSortable && multiDragSortable !== sortable) {
+            multiDragSortable.multiDrag._deselectMultiDrag();
+
+            multiDragSortable = sortable;
+          }
+
+          toggleClass(el, sortable.options.selectedClass, true);
+          multiDragElements.push(el);
+        },
+
+        /**
+         * Deselects the provided multi-drag item
+         * @param  {HTMLElement} el    The element to be deselected
+         */
+        deselect: function deselect(el) {
+          var sortable = el.parentNode[expando],
+              index = multiDragElements.indexOf(el);
+          if (!sortable || !sortable.options.multiDrag || !~index) return;
+          toggleClass(el, sortable.options.selectedClass, false);
+          multiDragElements.splice(index, 1);
+        }
+      },
+      eventProperties: function eventProperties() {
+        var _this3 = this;
+
+        var oldIndicies = [],
+            newIndicies = [];
+        multiDragElements.forEach(function (multiDragElement) {
+          oldIndicies.push({
+            multiDragElement: multiDragElement,
+            index: multiDragElement.sortableIndex
+          }); // multiDragElements will already be sorted if folding
+
+          var newIndex;
+
+          if (folding && multiDragElement !== dragEl$1) {
+            newIndex = -1;
+          } else if (folding) {
+            newIndex = index(multiDragElement, ':not(.' + _this3.options.selectedClass + ')');
+          } else {
+            newIndex = index(multiDragElement);
+          }
+
+          newIndicies.push({
+            multiDragElement: multiDragElement,
+            index: newIndex
+          });
+        });
+        return {
+          items: _toConsumableArray(multiDragElements),
+          clones: [].concat(multiDragClones),
+          oldIndicies: oldIndicies,
+          newIndicies: newIndicies
+        };
+      },
+      optionListeners: {
+        multiDragKey: function multiDragKey(key) {
+          key = key.toLowerCase();
+
+          if (key === 'ctrl') {
+            key = 'Control';
+          } else if (key.length > 1) {
+            key = key.charAt(0).toUpperCase() + key.substr(1);
+          }
+
+          return key;
+        }
+      }
+    });
+  }
+
+  function insertMultiDragElements(clonesInserted, rootEl) {
+    multiDragElements.forEach(function (multiDragElement, i) {
+      var target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)];
+
+      if (target) {
+        rootEl.insertBefore(multiDragElement, target);
+      } else {
+        rootEl.appendChild(multiDragElement);
+      }
+    });
+  }
+  /**
+   * Insert multi-drag clones
+   * @param  {[Boolean]} elementsInserted  Whether the multi-drag elements are inserted
+   * @param  {HTMLElement} rootEl
+   */
+
+
+  function insertMultiDragClones(elementsInserted, rootEl) {
+    multiDragClones.forEach(function (clone, i) {
+      var target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)];
+
+      if (target) {
+        rootEl.insertBefore(clone, target);
+      } else {
+        rootEl.appendChild(clone);
+      }
+    });
+  }
+
+  function removeMultiDragElements() {
+    multiDragElements.forEach(function (multiDragElement) {
+      if (multiDragElement === dragEl$1) return;
+      multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement);
+    });
+  }
+
+  Sortable.mount(new AutoScrollPlugin());
+  Sortable.mount(Remove, Revert);
+
+  Sortable.mount(new SwapPlugin());
+  Sortable.mount(new MultiDragPlugin());
+
+  return Sortable;
+
+})));

File diff suppressed because it is too large
+ 1 - 0
js/Sortable.min.js


+ 12 - 7
js/custom.js

@@ -1064,12 +1064,6 @@ $(document).on("click", ".saveJava", function () {
 $(document).on("click", ".saveThemeJava", function () {
     $('.javaThemeTextarea').val(javaThemeEditor.getValue()).trigger('change');
 });
-$(document).on("click", ".savecustomHTMLoneTextarea", function () {
-    $('.customHTMLoneTextarea').val(customHTMLoneEditor.getValue()).trigger('change');
-});
-$(document).on("click", ".savecustomHTMLtwoTextarea", function () {
-    $('.customHTMLtwoTextarea').val(customHTMLtwoEditor.getValue()).trigger('change');
-});
 
 $(document).on('focusout', 'input.pick-a-color-custom-options', function(e) {
     var original = $(this).attr('data-original');
@@ -1796,7 +1790,7 @@ $(document).on('click', ".ipInfo", function(){
 });
 // set active for group list
 $(document).on('click', '.allGroupsList', function() {
-    $(this).toggleClass('active');
+    //$(this).toggleClass('active');
 });
 // Control init of custom netdata JSON editor
 $(document).on('click', 'li a[aria-controls="Custom data"]', function() {
@@ -1887,4 +1881,15 @@ $(document).on('change', '[id*=-form-chooseI]', function (e) {
 	if(newValue !== 'Select or type Icon'){
 		$('#'+pasteId).val(newValue);
 	}
+});
+// SETTINGS DROPDOWN CHANGE
+$(document).on("change", ".settings-dropdown-box", function () {
+	let id = $(this).val();
+	$(id).click();
+});
+$(document).on('click', '.nav-non-mobile li a', function() {
+	let id = $(this).attr('id');
+	let menu = $(this).parent().parent().attr('data-dropdown');
+	$('.' + menu).val('#' + id);
+
 });

File diff suppressed because it is too large
+ 0 - 0
js/custom.min.js


+ 53 - 41
js/functions.js

@@ -23,6 +23,7 @@ var tabInformation = {};
 var tabActionsList = [];
 tabActionsList['refresh'] = [];
 tabActionsList['close'] = [];
+var customHTMLEditorObject = [];
 $.xhrPool = [];
 // Add new jquery serializeObject function
 $.fn.serializeObject = function()
@@ -1211,12 +1212,17 @@ function buildPluginsItem(array){
 	activePlugins = (activePlugins.length !== 0) ? activePlugins : '<h2 class="text-center" lang="en">Nothing Active</h2>';
 	inactivePlugins = (inactivePlugins.length !== 0) ? inactivePlugins : '<h2 class="text-center" lang="en">Everything Active</h2>';
 	var panes = `
-	<ul class="nav customtab2 nav-tabs" role="tablist">
-		<li onclick="changeSettingsMenu('Settings::Plugins::Active')" role="presentation" class="active"><a href="#settings-plugins-active" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-file"></i></span><span class="hidden-xs" lang="en">Active</span></a>
+	<select class="form-control settings-dropdown-box plugin-menu w-100 visible-xs">
+		<option value="#settings-plugins-active-anchor" lang="en">Active</option>
+		<option value="#settings-plugins-inactive-anchor" lang="en">Inactive</option>
+		<option value="#settings-plugins-marketplace-anchor" lang="en">Marketplace</option>
+	</select>
+	<ul class="nav customtab2 nav-tabs nav-non-mobile hidden-xs" data-dropdown="plugin-menu" role="tablist">
+		<li onclick="changeSettingsMenu('Settings::Plugins::Active')" role="presentation" class="active"><a id="settings-plugins-active-anchor" href="#settings-plugins-active" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-file"></i></span><span class="hidden-xs" lang="en">Active</span></a>
 		</li>
-		<li onclick="changeSettingsMenu('Settings::Plugins::Inactive')" role="presentation" class=""><a href="#settings-plugins-inactive" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-zip"></i></span><span class="hidden-xs" lang="en">Inactive</span></a>
+		<li onclick="changeSettingsMenu('Settings::Plugins::Inactive')" role="presentation" class=""><a id="settings-plugins-inactive-anchor" href="#settings-plugins-inactive" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-zip"></i></span><span class="hidden-xs" lang="en">Inactive</span></a>
 		</li>
-		<li onclick="changeSettingsMenu('Settings::Plugins::Marketplace');loadMarketplace('plugins');" role="presentation" class=""><a href="#settings-plugins-marketplace" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-shopping-cart-full"></i></span><span class="hidden-xs" lang="en">Marketplace</span></a>
+		<li onclick="changeSettingsMenu('Settings::Plugins::Marketplace');loadMarketplace('plugins');" role="presentation" class=""><a id="settings-plugins-marketplace-anchor" href="#settings-plugins-marketplace" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-shopping-cart-full"></i></span><span class="hidden-xs" lang="en">Marketplace</span></a>
 		</li>
 	</ul>
 	<!-- Tab panes -->
@@ -1722,27 +1728,23 @@ function homepageItemFormHTML(v){
 function clearHomepageOriginal(){
 	$('#editHomepageItem').html('');
 }
-function completeHomepageLoad(item){
-	if(item == 'CustomHTML-1'){
-		customHTMLoneEditor = ace.edit("customHTMLoneEditor");
-		let HTMLMode = ace.require("ace/mode/html").Mode;
-		customHTMLoneEditor.session.setMode(new HTMLMode());
-		customHTMLoneEditor.setTheme("ace/theme/idle_fingers");
-		customHTMLoneEditor.setShowPrintMargin(false);
-		customHTMLoneEditor.session.on('change', function(delta) {
-			$('.customHTMLoneTextarea').val(customHTMLoneEditor.getValue());
-			$('#homepage-CustomHTML-1-form-save').removeClass('hidden');
-		});
-	}
-	if(item == 'CustomHTML-2'){
-		customHTMLtwoEditor = ace.edit("customHTMLtwoEditor");
-		let HTMLMode = ace.require("ace/mode/html").Mode;
-		customHTMLtwoEditor.session.setMode(new HTMLMode());
-		customHTMLtwoEditor.setTheme("ace/theme/idle_fingers");
-		customHTMLtwoEditor.setShowPrintMargin(false);
-		customHTMLtwoEditor.session.on('change', function(delta) {
-			$('.customHTMLtwoTextarea').val(customHTMLtwoEditor.getValue());
-			$('#homepage-CustomHTML-2-form-save').removeClass('hidden');
+function completeHomepageLoad(item, data){
+	if(item == 'CustomHTML'){
+		let iteration = 0;
+		$.each(data.settings, function(i,customItem) {
+			let iterationString = (parseInt(iteration, 10) + 101).toString().substr(1);
+			let customEditor = 'customHTML'+iterationString+'Editor';
+			let customTextarea = 'customHTML'+iterationString+'Textarea';
+			let HTMLMode = ace.require("ace/mode/html").Mode;
+			customHTMLEditorObject[iterationString] = ace.edit(customEditor);
+			customHTMLEditorObject[iterationString].session.setMode(new HTMLMode());
+			customHTMLEditorObject[iterationString].setTheme("ace/theme/idle_fingers");
+			customHTMLEditorObject[iterationString].setShowPrintMargin(false);
+			customHTMLEditorObject[iterationString].session.on('change', function(delta) {
+				$('.' + customTextarea).val(customHTMLEditorObject[iterationString].getValue());
+				$('#homepage-CustomHTML-form-save').removeClass('hidden');
+			});
+			iteration++;
 		});
 	}
 	pageLoad();
@@ -1775,7 +1777,7 @@ function editHomepageItem(item){
 					delay: 0,
 					fullscreen: true,
 					clone: false,
-					onComplete: completeHomepageLoad(item),
+					onComplete: completeHomepageLoad(item, response.data),
 					onClose: clearHomepageOriginal
 				},loader:{active:true}
 			}).open();
@@ -1796,15 +1798,15 @@ function buildHomepageItem(array){
 		$.each(array, function(i,v) {
 			if(v.enabled){
 				listing += `
-				<div class="col-lg-2 col-md-2 col-sm-4 col-xs-4">
+				<div class="col-lg-2 col-md-2 col-sm-6 col-xs-6">
 					<div class="white-box bg-org m-0">
 						<div class="el-card-item p-0 editHomepageItemBox-`+v.name+`">
 							<div class="el-card-avatar el-overlay-1">
 								<a onclick="editHomepageItem('`+v.name+`')"><img class="lazyload tabImages mouse" data-src="`+v.image+`"></a>
 							</div>
 							<div class="el-card-content">
-								<h3 class="box-title">`+v.name+`</h3>
-								<small class="elip text-uppercase">`+v.category+`</small><br>
+								<h3 class="box-title elip">`+v.name+`</h3>
+								<small class="elip text-uppercase elip">`+v.category+`</small><br>
 							</div>
 						</div>
 					</div>
@@ -3186,15 +3188,24 @@ function buildTabEditorItem(array){
 			<input type="hidden" class="form-control" name="tab[`+v.id+`].id" value="`+v.id+`">
 			<input type="hidden" class="form-control order" name="tab[`+v.id+`].order" value="`+v.order+`">
 			<input type="hidden" class="form-control" name="tab[`+v.id+`].originalOrder" value="`+v.order+`">
+			<td class="mouse sort-tabs-handle">
+				<i class="icon-options-vertical m-r-5"></i> 
+				<!-- May use later on
+				<div class="btn-group dropside visible-xs">
+					<button aria-expanded="false" data-toggle="dropdown" class="btn btn-default btn-outline dropdown-toggle waves-effect waves-light" type="button"> <i class="icon-options-vertical m-r-5"></i> <span class="caret"></span></button>
+					<ul role="menu" class="dropdown-menu">
+						<li><a href="#"><i class="fa fa-angle-double-up"></i></a></li>
+						<li><a href="#"><i class="fa fa-angle-up"></i></a></li>
+						<li><a href="#"><i class="fa fa-angle-double-down"></i></a></li>
+						<li><a href="#"><i class="fa fa-angle-down"></i></a></li>
+					</ul>
+				</div>
+				-->
+			</td>
 			<td style="text-align:center" class="text-center el-element-overlay">
 				<div class="el-card-item p-0">
 					<div class="el-card-avatar el-overlay-1 m-0">
 						<div class="tabEditorIcon">`+iconPrefix(v.image)+`</div>
-						<div class="el-overlay bg-org">
-							<ul class="el-info">
-								<i class="fa fa-bars"></i>
-							</ul>
-						</div>
 					</div>
 				</div>
 			</td>
@@ -4192,10 +4203,10 @@ function loadInternal(url,tabName, split = null){
 	organizrAPI2('get',url).success(function(data) {
 		try {
 			var html = data.response;
+			$('#internal-'+extra+tabName).html(html.data);
 		}catch(e) {
 			organizrCatchError(e,data);
 		}
-		$('#internal-'+extra+tabName).html(html.data);
 	}).fail(function(xhr) {
 		OrganizrApiError(xhr);
 	});
@@ -4931,7 +4942,7 @@ function buildStreamItem(array,source){
 						<h3 class="box-title pull-right vertical-middle" style="width:10%"><i class="icon-control-`+v.state+` fa-fw text-info" style=""></i></h3>
 						<div class="clearfix"></div>
 						<small class="pull-left p-l-10 w-50 elip"><span class="pull-left"><i class="`+icon+` fa-fw text-info"></i>`+v.nowPlayingBottom+`</span></small>
-						<small class="pull-right p-r-10 w-50 elip"><span class="pull-right">`+v.user+` <i class="icon-user"></i></span></small>
+						<small class="pull-right p-r-10 w-50"><span class="pull-right"><span class="">`+v.user+` <i class="icon-user"></i></span></span></small>
 						<br>
 					</div>
 				</div>
@@ -10058,7 +10069,7 @@ function clickPath(type,path=null){
             break;
         case 'update':
             $('#settings-main-system-settings-anchor').trigger('click');
-            $('#update-button').trigger('click');
+            $('#settings-settings-updates-anchor').trigger('click');
             break;
         case 'sso':
             $('#settings-main-system-settings-anchor').trigger('click');
@@ -10797,18 +10808,19 @@ function checkForUpdates(){
 
 function loadJavascript(script = null, defer = false){
 	if(script){
-		console.log(script);
-		console.log('checking if script is loaded...');
+		organizrConsole('JS Loader',script);
+		organizrConsole('JS Loader','Checking if script is loaded...');
 		let loaded = $('script[src="'+script+'"]').length;
-		///let loaded2 = document.querySelector('script[src="' + script + '"]');
 		if(!loaded){
-			console.log('script is NOT loaded... Loading now...');
+			organizrConsole('JS Loader','Script is NOT loaded... Loading now...');
 			let head = document.getElementsByTagName('head')[0];
 			let scriptEl = document.createElement('script');
 			scriptEl.type = 'text/javascript';
 			scriptEl.src = script;
 			scriptEl.defer = false;
 			head.appendChild(scriptEl);
+		}else{
+			organizrConsole('JS Loader','Script already loaded');
 		}
 	}
 }

+ 7 - 0
js/version.json

@@ -418,5 +418,12 @@
     "new": "Self Signed SSL Cert option to all homepage items (#1177)|shortcut function for menu clicking - WIP",
     "fixed": "move google js to load only when config value setup (#1699)|tooltips to homepage settings ui buttons|First IPA typo to Free IPA|typo and sort LDAP options|now playing spacing",
     "notes": ""
+  },
+  "2.1.525": {
+    "date": "2021-08-29 21:40",
+    "title": "Added more Custom HTML homepage items",
+    "new": "",
+    "fixed": "",
+    "notes": ""
   }
 }

BIN
plugins/images/tabs/HTML5.png


Some files were not shown because too many files changed in this diff