Browse Source

Refactor JS to use ES modules (#3476)

Alec Rust 2 years ago
parent
commit
50325c5cfc
72 changed files with 1000 additions and 1277 deletions
  1. 17 28
      build.js
  2. 7 5
      package.json
  3. 12 6
      postcss.config.js
  4. 2 16
      web/css/src/themes/default.css
  5. 6 4
      web/css/src/utilities.css
  6. 0 0
      web/css/themes/default.min.css
  7. 0 0
      web/js/dist/events.min.js
  8. 0 1
      web/js/dist/init.min.js
  9. 0 0
      web/js/dist/main.min.js
  10. 3 0
      web/js/dist/main.min.js.map
  11. 0 0
      web/js/dist/shortcuts.min.js
  12. 0 8
      web/js/pages/add_db.js
  13. 2 98
      web/js/pages/add_mail_acc.js
  14. 0 8
      web/js/pages/add_user.js
  15. 0 5
      web/js/pages/add_web.js
  16. 0 8
      web/js/pages/edit_db.js
  17. 2 94
      web/js/pages/edit_mail_acc.js
  18. 1 8
      web/js/pages/edit_server_mysql.js
  19. 1 8
      web/js/pages/edit_server_nginx.js
  20. 1 8
      web/js/pages/edit_server_php.js
  21. 0 7
      web/js/pages/edit_user.js
  22. 2 2
      web/js/pages/edit_web.js
  23. 49 0
      web/js/src/alpineInit.js
  24. 16 0
      web/js/src/confirmationDialog.js
  25. 0 337
      web/js/src/events.js
  26. 14 0
      web/js/src/focusFirstInput.js
  27. 125 0
      web/js/src/helpers.js
  28. 0 63
      web/js/src/init.js
  29. 49 0
      web/js/src/listSorting.js
  30. 18 0
      web/js/src/listeners.js
  31. 9 0
      web/js/src/loadingSpinner.js
  32. 31 281
      web/js/src/main.js
  33. 34 0
      web/js/src/nameServerInput.js
  34. 125 0
      web/js/src/navigation.js
  35. 43 0
      web/js/src/notifications.js
  36. 47 0
      web/js/src/passwordInput.js
  37. 19 0
      web/js/src/selectAll.js
  38. 26 27
      web/js/src/shortcuts.js
  39. 22 0
      web/js/src/stickyToolbar.js
  40. 43 0
      web/js/src/unlimitedInput.js
  41. 1 1
      web/templates/footer.php
  42. 1 4
      web/templates/includes/js.php
  43. 3 1
      web/templates/pages/add_db.php
  44. 4 2
      web/templates/pages/add_mail_acc.php
  45. 1 1
      web/templates/pages/add_package.php
  46. 3 1
      web/templates/pages/add_user.php
  47. 3 1
      web/templates/pages/edit_db.php
  48. 4 2
      web/templates/pages/edit_mail_acc.php
  49. 1 1
      web/templates/pages/edit_package.php
  50. 3 1
      web/templates/pages/edit_user.php
  51. 1 1
      web/templates/pages/list_access_keys.php
  52. 3 3
      web/templates/pages/list_backup.php
  53. 6 6
      web/templates/pages/list_backup_detail.php
  54. 3 3
      web/templates/pages/list_cron.php
  55. 4 4
      web/templates/pages/list_db.php
  56. 6 6
      web/templates/pages/list_dns.php
  57. 2 2
      web/templates/pages/list_dns_rec.php
  58. 3 3
      web/templates/pages/list_firewall.php
  59. 1 1
      web/templates/pages/list_firewall_banlist.php
  60. 1 1
      web/templates/pages/list_firewall_ipset.php
  61. 2 2
      web/templates/pages/list_ip.php
  62. 1 1
      web/templates/pages/list_key.php
  63. 1 1
      web/templates/pages/list_log.php
  64. 1 1
      web/templates/pages/list_log_auth.php
  65. 9 9
      web/templates/pages/list_mail.php
  66. 5 5
      web/templates/pages/list_mail_acc.php
  67. 3 3
      web/templates/pages/list_packages.php
  68. 4 4
      web/templates/pages/list_services.php
  69. 3 3
      web/templates/pages/list_user.php
  70. 6 6
      web/templates/pages/list_web.php
  71. 1 1
      web/templates/pages/setup_webapp.php
  72. 184 174
      yarn.lock

+ 17 - 28
build.js

@@ -1,37 +1,26 @@
 // Build JS and CSS using esbuild and PostCSS
-const esbuild = require('esbuild');
-const postcss = require('postcss');
-const fs = require('fs').promises;
-const path = require('path');
-const postcssConfig = require('./postcss.config.js');
+import esbuild from 'esbuild';
+import postcss from 'postcss';
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+import postcssConfig from './postcss.config.js';
 
-// Esbuild JavaScript configuration
 const esbuildConfig = {
-	outdir: './web/js/dist',
-	entryNames: '[dir]/[name].min',
+	outfile: './web/js/dist/main.min.js',
+	bundle: true,
 	minify: true,
+	sourcemap: true,
 };
 
 // Build JavaScript
 async function buildJS() {
-	const jsSrcPath = './web/js/src/';
-	const jsEntries = await fs.readdir(jsSrcPath);
-	const jsBuildPromises = jsEntries
-		.filter((entry) => path.extname(entry) === '.js')
-		.map((entry) => {
-			const inputPath = path.join(jsSrcPath, entry);
-			return esbuild
-				.build({
-					...esbuildConfig,
-					entryPoints: [inputPath],
-				})
-				.then(() => {
-					console.log('✅ JavaScript build completed for', inputPath);
-				});
-		});
-
+	const inputPath = './web/js/src/main.js';
 	try {
-		await Promise.all(jsBuildPromises);
+		await esbuild.build({
+			...esbuildConfig,
+			entryPoints: [inputPath],
+		});
+		console.log('✅ JavaScript build completed for', inputPath);
 	} catch (error) {
 		console.error('❌ Error building JavaScript:', error);
 		process.exit(1);
@@ -56,14 +45,14 @@ async function processCSS(inputFile, outputFile) {
 
 // Build CSS files
 async function buildCSS() {
-	const themesSrcPath = './web/css/src/themes/';
-	const cssEntries = await fs.readdir(themesSrcPath);
+	const themesSourcePath = './web/css/src/themes/';
+	const cssEntries = await fs.readdir(themesSourcePath);
 
 	const cssBuildPromises = cssEntries
 		.filter((entry) => path.extname(entry) === '.css')
 		.map(async (entry) => {
 			const entryName = entry.replace('.css', '.min.css');
-			const inputPath = path.join(themesSrcPath, entry);
+			const inputPath = path.join(themesSourcePath, entry);
 			const outputPath = `./web/css/themes/${entryName}`;
 			await processCSS(inputPath, outputPath);
 		});

+ 7 - 5
package.json

@@ -5,6 +5,7 @@
 	"description": "An open-source Linux web server control panel.",
 	"repository": "https://github.com/hestiacp/hestiacp",
 	"license": "GPL-3.0-or-later",
+	"type": "module",
 	"scripts": {
 		"docs:dev": "vitepress dev docs",
 		"docs:build": "vitepress build docs",
@@ -19,14 +20,15 @@
 	"packageManager": "yarn@3.5.0",
 	"dependencies": {
 		"@fortawesome/fontawesome-free": "^6.4.0",
+		"nanoid": "^4.0.2",
 		"normalize.css": "^8.0.1"
 	},
 	"devDependencies": {
 		"@prettier/plugin-php": "^0.19.4",
-		"@typescript-eslint/eslint-plugin": "^5.58.0",
-		"@typescript-eslint/parser": "^5.58.0",
+		"@typescript-eslint/eslint-plugin": "^5.59.0",
+		"@typescript-eslint/parser": "^5.59.0",
 		"cssnano": "^6.0.0",
-		"esbuild": "^0.17.16",
+		"esbuild": "^0.17.17",
 		"eslint": "^8.38.0",
 		"eslint-config-prettier": "^8.8.0",
 		"eslint-plugin-editorconfig": "^4.0.2",
@@ -36,7 +38,7 @@
 		"postcss": "^8.4.22",
 		"postcss-import": "^15.1.0",
 		"postcss-path-replace": "^1.0.4",
-		"postcss-preset-env": "^8.3.1",
+		"postcss-preset-env": "^8.3.2",
 		"postcss-size": "^4.0.1",
 		"prettier": "^2.8.7",
 		"prettier-plugin-nginx": "^1.0.3",
@@ -45,7 +47,7 @@
 		"stylelint": "^15.5.0",
 		"stylelint-config-standard": "^33.0.0",
 		"typescript": "^5.0.4",
-		"vitepress": "1.0.0-alpha.70",
+		"vitepress": "1.0.0-alpha.72",
 		"vue": "^3.2.47"
 	},
 	"browserslist": [

+ 12 - 6
postcss.config.js

@@ -1,14 +1,20 @@
-module.exports = {
+import postcssImport from 'postcss-import';
+import postcssPathReplace from 'postcss-path-replace';
+import postcssSize from 'postcss-size';
+import cssnano from 'cssnano';
+import postcssPresetEnv from 'postcss-preset-env';
+
+export default {
 	plugins: [
-		require('postcss-import'),
-		require('postcss-path-replace')({
+		postcssImport,
+		postcssPathReplace({
 			publicPath: '/webfonts/',
 			matched: '../webfonts/',
 			mode: 'replace',
 		}),
-		require('postcss-size'),
-		require('cssnano'),
-		require('postcss-preset-env')({
+		postcssSize,
+		cssnano,
+		postcssPresetEnv({
 			autoprefixer: {
 				flexbox: 'no-2009',
 			},

+ 2 - 16
web/css/src/themes/default.css

@@ -1588,20 +1588,6 @@
 	}
 }
 
-.toggle-password {
-	color: #aaa;
-	z-index: 1;
-	position: absolute;
-	top: 9px;
-	right: 12px;
-}
-
-.toggle-psw-visibility-icon {
-	cursor: pointer;
-	opacity: 1;
-	margin: -2px;
-}
-
 .password-meter {
 	height: 3px;
 	overflow: hidden;
@@ -1698,7 +1684,7 @@
 		opacity: 0.8;
 	}
 
-	&.is-active {
+	&.active {
 		opacity: 1;
 	}
 
@@ -2213,7 +2199,7 @@
 	opacity: 0;
 	transition: opacity 0.2s ease, visibility 0s 0.2s;
 
-	&.show {
+	&.active {
 		visibility: visible;
 		opacity: 1;
 		transition: opacity 0.2s ease, visibility 0s 0s;

+ 6 - 4
web/css/src/utilities.css

@@ -45,10 +45,6 @@
 	white-space: nowrap !important;
 }
 
-.u-opacity-50 {
-	opacity: 0.5 !important;
-}
-
 .u-mt15 {
 	margin-top: 15px !important;
 }
@@ -145,6 +141,12 @@
 	resize: both !important;
 }
 
+.u-unstyled-button {
+	border: 0 !important;
+	padding: 0 !important;
+	background-color: transparent !important;
+}
+
 .u-console {
 	font-family: var(--font-family-monospace) !important;
 	white-space: pre !important;

File diff suppressed because it is too large
+ 0 - 0
web/css/themes/default.min.css


File diff suppressed because it is too large
+ 0 - 0
web/js/dist/events.min.js


+ 0 - 1
web/js/dist/init.min.js

@@ -1 +0,0 @@
-document.addEventListener("DOMContentLoaded",()=>{function o(){document.querySelector(".fullscreen-loader").classList.add("show")}if(document.querySelector("#vstobjects")?.addEventListener("submit",o),document.querySelector('[x-bind="BulkEdit"]')?.addEventListener("submit",o),document.querySelectorAll(".toolbar-right .sort-by")?.forEach(t=>{t.addEventListener("click",()=>$(".context-menu.sort-order").toggle())}),document.querySelectorAll("dialog[open]").length==0){const t=document.querySelector("#vstobjects .form-control:not([disabled]),			#vstobjects .form-select:not([disabled])");t&&t.focus()}document.querySelectorAll(".toolbar-sorting-toggle").forEach(t=>{t.addEventListener("click",e=>{e.preventDefault(),document.querySelector(".toolbar-sorting-menu").classList.toggle("u-hidden")})}),$(".toolbar-sorting-menu span").click(function(){$(".toolbar-sorting-menu").toggleClass("u-hidden"),!$(this).hasClass("active")&&($(".toolbar-sorting-menu span").removeClass("active"),$(this).addClass("active"),VE.tmp.sort_par=$(this).parent("li").attr("entity"),VE.tmp.sort_as_int=!!$(this).parent("li").attr("sort_as_int"),VE.tmp.sort_direction=$(this).hasClass("up")*1||-1,$(".toolbar-sorting-toggle b").html($(this).parent("li").find(".name").html()),$(".toolbar-sorting-toggle .fas").removeClass("fa-arrow-up-a-z fa-arrow-down-a-z"),$(this).hasClass("up")?$(".toolbar-sorting-toggle .fas").addClass("fa-arrow-up-a-z"):$(".toolbar-sorting-toggle .fas").addClass("fa-arrow-down-a-z"),$(".units .l-unit").sort((t,e)=>VE.tmp.sort_as_int?parseInt($(t).attr(VE.tmp.sort_par))>=parseInt($(e).attr(VE.tmp.sort_par))?VE.tmp.sort_direction:VE.tmp.sort_direction*-1:$(t).attr(VE.tmp.sort_par)<=$(e).attr(VE.tmp.sort_par)?VE.tmp.sort_direction:VE.tmp.sort_direction*-1).appendTo(".units"))}),$(".button.cancel").attr("title","ctrl+Backspace"),VE.core.register()});

File diff suppressed because it is too large
+ 0 - 0
web/js/dist/main.min.js


File diff suppressed because it is too large
+ 3 - 0
web/js/dist/main.min.js.map


File diff suppressed because it is too large
+ 0 - 0
web/js/dist/shortcuts.min.js


+ 0 - 8
web/js/pages/add_db.js

@@ -65,11 +65,3 @@ App.Listeners.DB.keypress_db_databasename = function () {
 // Trigger listeners
 App.Listeners.DB.keypress_db_username();
 App.Listeners.DB.keypress_db_databasename();
-
-applyRandomPassword = function (min_length = 16) {
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomString(min_length);
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-	}
-};

+ 2 - 98
web/js/pages/add_mail_acc.js

@@ -8,102 +8,6 @@ $('#v_blackhole').on('click', function () {
 		$('#id_fwd_for').show();
 	}
 });
-$('form[name="v_quota"]').on('submit', function () {
-	$('input:disabled').each(function (i, elm) {
-		$(elm).attr('disabled', false);
-		if (Alpine.store('globals').isUnlimitedValue($(elm).val())) {
-			$(elm).val(Alpine.store('globals').UNLIM_VALUE);
-		}
-	});
-});
-
-applyRandomPassword = function (min_length = 16) {
-	const randomPassword = randomString(min_length);
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomPassword;
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-		const passwordOutput = document.querySelector('.js-password-output');
-		if (passwordOutput) {
-			if (passwordInput.getAttribute('type') === 'text') {
-				passwordOutput.textContent = randomPassword;
-			} else {
-				passwordOutput.textContent = Array(randomPassword.length + 1).join('*');
-			}
-		}
-		generate_mail_credentials();
-	}
-};
-
-generate_mail_credentials = function () {
-	var div = $('.js-mail-info').clone();
-	div.find('#mail_configuration').remove();
-	var output = div.text();
-	output = output.replace(/(?:\r\n|\r|\n|\t)/g, '|');
-	output = output.replace(/ {2}/g, '');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/^\|+/g, '');
-	output = output.replace(/\|$/, '');
-	output = output.replace(/ $/, '');
-	output = output.replace(/:\|/g, ': ');
-	output = output.replace(/\|/g, '\n');
-	$('.js-hidden-credentials').val(output);
-};
-
-$(document).ready(function () {
-	$('.js-account-output').text($('input[name=v_account]').val());
-	$('.js-password-output').text($('.js-password-input').val());
-	generate_mail_credentials();
-
-	$('input[name=v_account]').change(function () {
-		$('.js-account-output').text($(this).val());
-		generate_mail_credentials();
-	});
 
-	$('.js-password-input').change(function () {
-		if ($('.js-password-input').attr('type') == 'text')
-			$('.js-password-output').text($(this).val());
-		else $('.js-password-output').text(Array($(this).val().length + 1).join('*'));
-		generate_mail_credentials();
-	});
-
-	$('.toggle-psw-visibility-icon').click(function () {
-		$('.js-password-output').text($('.js-password-input').val());
-		generate_mail_credentials();
-	});
-
-	$('#mail_configuration').change(function (evt) {
-		var opt = $(evt.target).find('option:selected');
-
-		switch (opt.attr('v_type')) {
-			case 'hostname':
-				$('#td_imap_hostname').text(opt.attr('domain'));
-				$('#td_smtp_hostname').text(opt.attr('domain'));
-				break;
-			case 'starttls':
-				$('#td_imap_port').text('143');
-				$('#td_imap_encryption').text('STARTTLS');
-				$('#td_smtp_port').text('587');
-				$('#td_smtp_encryption').text('STARTTLS');
-				break;
-			case 'ssl':
-				$('#td_imap_port').text('993');
-				$('#td_imap_encryption').text('SSL / TLS');
-				$('#td_smtp_port').text('465');
-				$('#td_smtp_encryption').text('SSL / TLS');
-				break;
-			case 'no_encryption':
-				$('#td_imap_hostname').text(opt.attr('domain'));
-				$('#td_smtp_hostname').text(opt.attr('domain'));
-
-				$('#td_imap_port').text('143');
-				$('#td_imap_encryption').text(opt.attr('no_encryption'));
-				$('#td_smtp_port').text('25');
-				$('#td_smtp_encryption').text(opt.attr('no_encryption'));
-				break;
-		}
-		generate_mail_credentials();
-	});
-});
+VE.helpers.monitorAndUpdate('.js-account-input', '.js-account-output');
+VE.helpers.monitorAndUpdate('.js-password-input', '.js-password-output');

+ 0 - 8
web/js/pages/add_user.js

@@ -12,11 +12,3 @@ $(function () {
 		}
 	});
 });
-
-applyRandomPassword = function (min_length = 16) {
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomString(min_length);
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-	}
-};

+ 0 - 5
web/js/pages/add_web.js

@@ -93,11 +93,6 @@ $(function () {
 	});
 });
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function WEBrandom() {
-	document.v_add_web.v_stats_password.value = randomString(16);
-}
-
 document.getElementById('vstobjects').addEventListener('submit', function () {
 	$('input[disabled]').each(function (i, elm) {
 		$(elm).removeAttr('disabled');

+ 0 - 8
web/js/pages/edit_db.js

@@ -72,11 +72,3 @@ App.Listeners.DB.keypress_db_databasename = function () {
 // Trigger listeners
 App.Listeners.DB.keypress_db_username();
 App.Listeners.DB.keypress_db_databasename();
-
-applyRandomPassword = function (min_length = 16) {
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomString(min_length);
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-	}
-};

+ 2 - 94
web/js/pages/edit_mail_acc.js

@@ -9,97 +9,5 @@ $('#v_blackhole').on('click', function () {
 	}
 });
 
-applyRandomPassword = function (min_length = 16) {
-	const randomPassword = randomString(min_length);
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomPassword;
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-		const passwordOutput = document.querySelector('.js-password-output');
-		if (passwordOutput) {
-			if (passwordInput.getAttribute('type') === 'text') {
-				passwordOutput.textContent = randomPassword;
-			} else {
-				passwordOutput.textContent = Array(randomPassword.length + 1).join('*');
-			}
-		}
-		generate_mail_credentials();
-	}
-};
-
-generate_mail_credentials = function () {
-	var div = $('.js-mail-info').clone();
-	div.find('#mail_configuration').remove();
-	var pass = div.find('.js-password-output').text();
-	if (pass == '') div.find('.js-password-output').text(' ');
-	var output = div.text();
-	output = output.replace(/(?:\r\n|\r|\n|\t)/g, '|');
-	output = output.replace(/ {2}/g, '');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/\|\|/g, '|');
-	output = output.replace(/^\|+/g, '');
-	output = output.replace(/\|$/, '');
-	output = output.replace(/ $/, '');
-	output = output.replace(/:\|/g, ': ');
-	output = output.replace(/\|/g, '\n');
-	$('.js-hidden-credentials').val(output);
-};
-
-$(document).ready(function () {
-	$('.js-account-output').text($('input[name=v_account]').val());
-	$('.js-password-output').text($('.js-password-input').val());
-	generate_mail_credentials();
-
-	$('input[name=v_account]').change(function () {
-		$('.js-account-output').text($(this).val());
-		generate_mail_credentials();
-	});
-
-	$('.js-password-input').change(function () {
-		if ($('.js-password-input').attr('type') == 'text')
-			$('.js-password-output').text($(this).val());
-		else $('.js-password-output').text(Array($(this).val().length + 1).join('*'));
-		generate_mail_credentials();
-	});
-
-	$('.toggle-psw-visibility-icon').click(function () {
-		if ($('.js-password-input').attr('type') == 'text')
-			$('.js-password-output').text($('.js-password-input').val());
-		else $('.js-password-output').text(Array($('.js-password-input').val().length + 1).join('*'));
-		generate_mail_credentials();
-	});
-
-	$('#mail_configuration').change(function (evt) {
-		var opt = $(evt.target).find('option:selected');
-
-		switch (opt.attr('v_type')) {
-			case 'hostname':
-				$('#td_imap_hostname').text(opt.attr('domain'));
-				$('#td_smtp_hostname').text(opt.attr('domain'));
-				break;
-			case 'starttls':
-				$('#td_imap_port').text('143');
-				$('#td_imap_encryption').text('STARTTLS');
-				$('#td_smtp_port').text('587');
-				$('#td_smtp_encryption').text('STARTTLS');
-				break;
-			case 'ssl':
-				$('#td_imap_port').text('993');
-				$('#td_imap_encryption').text('SSL / TLS');
-				$('#td_smtp_port').text('465');
-				$('#td_smtp_encryption').text('SSL / TLS');
-				break;
-			case 'no_encryption':
-				$('#td_imap_hostname').text(opt.attr('domain'));
-				$('#td_smtp_hostname').text(opt.attr('domain'));
-
-				$('#td_imap_port').text('143');
-				$('#td_imap_encryption').text(opt.attr('no_encryption'));
-				$('#td_smtp_port').text('25');
-				$('#td_smtp_encryption').text(opt.attr('no_encryption'));
-				break;
-		}
-		generate_mail_credentials();
-	});
-});
+VE.helpers.monitorAndUpdate('.js-account-input', '.js-account-output');
+VE.helpers.monitorAndUpdate('.js-password-input', '.js-password-output');

+ 1 - 8
web/js/pages/edit_server_mysql.js

@@ -1,10 +1,9 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 function toggleOptions() {
 	if ($('#advanced-options').is(':visible')) {
-		Cookies.remove('advanced');
 		$('#advanced-options').hide();
 		$('#basic-options').show();
 	} else {
-		Cookies.set('advanced', 1);
 		$('#advanced-options').show();
 		$('#basic-options').hide();
 
@@ -33,9 +32,3 @@ $('#vstobjects').submit(function () {
 		});
 	}
 });
-
-$(document).ready(function () {
-	if (Cookies.read('advanced')) {
-		toggleOptions();
-	}
-});

+ 1 - 8
web/js/pages/edit_server_nginx.js

@@ -1,10 +1,9 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 function toggleOptions() {
 	if ($('#advanced-options').is(':visible')) {
-		Cookies.remove('advanced');
 		$('#advanced-options').hide();
 		$('#basic-options').show();
 	} else {
-		Cookies.set('advanced', 1);
 		$('#advanced-options').show();
 		$('#basic-options').hide();
 
@@ -33,9 +32,3 @@ $('#vstobjects').submit(function () {
 		});
 	}
 });
-
-$(document).ready(function () {
-	if (Cookies.read('advanced')) {
-		toggleOptions();
-	}
-});

+ 1 - 8
web/js/pages/edit_server_php.js

@@ -1,10 +1,9 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 function toggleOptions() {
 	if ($('#advanced-options').is(':visible')) {
-		Cookies.remove('advanced');
 		$('#advanced-options').hide();
 		$('#basic-options').show();
 	} else {
-		Cookies.set('advanced', 1);
 		$('#advanced-options').show();
 		$('#basic-options').hide();
 
@@ -33,9 +32,3 @@ $('#vstobjects').submit(function () {
 		});
 	}
 });
-
-$(document).ready(function () {
-	if (Cookies.read('advanced')) {
-		toggleOptions();
-	}
-});

+ 0 - 7
web/js/pages/edit_user.js

@@ -1,7 +0,0 @@
-applyRandomPassword = function (min_length = 16) {
-	const passwordInput = document.querySelector('.js-password-input');
-	if (passwordInput) {
-		passwordInput.value = randomString(min_length);
-		VE.helpers.recalculatePasswordStrength(passwordInput);
-	}
-};

+ 2 - 2
web/js/pages/edit_web.js

@@ -271,12 +271,12 @@ $(function () {
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 function WEBrandom() {
-	document.v_edit_web.v_stats_password.value = randomString(16);
+	document.v_edit_web.v_stats_password.value = VE.helpers.randomPassword();
 }
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 function FTPrandom(elm) {
-	$(elm).parents('.js-ftp-account').find('.v-ftp-user-psw').val(randomString(16));
+	$(elm).parents('.js-ftp-account').find('.v-ftp-user-psw').val(VE.helpers.randomPassword());
 	App.Actions.WEB.randomPasswordGenerated && App.Actions.WEB.randomPasswordGenerated(elm);
 }
 

+ 49 - 0
web/js/src/alpineInit.js

@@ -0,0 +1,49 @@
+import notificationMethods from './notifications.js';
+import initUnlimitedInput from './unlimitedInput.js';
+import initShortcuts from './shortcuts.js';
+
+// Set up various Alpine things, loads after Alpine is initialized
+export default function alpineInit() {
+	// Bulk edit forms
+	Alpine.bind('BulkEdit', () => ({
+		/** @param {SubmitEvent} evt */
+		'@submit'(evt) {
+			evt.preventDefault();
+			document.querySelectorAll('.ch-toggle').forEach((el) => {
+				if (el.checked) {
+					const input = document.createElement('input');
+					input.type = 'hidden';
+					input.name = el.name;
+					input.value = el.value;
+					evt.target.appendChild(input);
+				}
+			});
+
+			evt.target.submit();
+		},
+	}));
+
+	// Form state
+	Alpine.store('form', {
+		dirty: false,
+		makeDirty() {
+			this.dirty = true;
+		},
+	});
+	document
+		.querySelectorAll('#vstobjects input, #vstobjects select, #vstobjects textarea')
+		.forEach((el) => {
+			el.addEventListener('change', () => {
+				Alpine.store('form').makeDirty();
+			});
+		});
+
+	// Register Alpine notifications methods
+	Alpine.data('notifications', notificationMethods);
+	initAlpineDependentModules();
+}
+
+function initAlpineDependentModules() {
+	initUnlimitedInput();
+	initShortcuts();
+}

+ 16 - 0
web/js/src/confirmationDialog.js

@@ -0,0 +1,16 @@
+import { createConfirmationDialog } from './helpers.js';
+
+// Adds listeners to .js-confirm-action links and intercepts them with a confirmation dialog
+export default function initConfirmationDialogs() {
+	document.querySelectorAll('.js-confirm-action').forEach((triggerLink) => {
+		triggerLink.addEventListener('click', (evt) => {
+			evt.preventDefault();
+
+			const title = triggerLink.dataset.confirmTitle;
+			const message = triggerLink.dataset.confirmMessage;
+			const targetUrl = triggerLink.getAttribute('href');
+
+			createConfirmationDialog({ title, message, targetUrl });
+		});
+	});
+}

+ 0 - 337
web/js/src/events.js

@@ -1,337 +0,0 @@
-const VE = {
-	core: {
-		/**
-		 * Main method that invokes further event processing
-		 * @param root is root HTML DOM element that. Pass HTML DOM Element or css selector
-		 * @param event_type (eg: click, mouseover etc..)
-		 */
-		register: (root, event_type) => {
-			root = !root ? 'body' : root; // if elm is not passed just bind events to body DOM Element
-			event_type = !event_type ? 'click' : event_type; // set event type to "click" by default
-			$(root).bind(event_type, (evt) => {
-				VE.core.dispatch(evt, $(evt.target), event_type); // dispatch captured event
-			});
-		},
-		/**
-		 * Dispatch event that was previously registered
-		 * @param evt related event object
-		 * @param elm that was catched
-		 * @param event_type (eg: click, mouseover etc..)
-		 */
-		dispatch: (evt, elm, event_type) => {
-			if ('undefined' == typeof VE.callbacks[event_type]) {
-				return VE.helpers.warn(
-					'There is no corresponding object that should contain event callbacks for "' +
-						event_type +
-						'" event type'
-				);
-			}
-			// get class of element
-			const classes = $(elm).attr('class');
-			// if no classes are attached, then just stop any further processings
-			if (!classes) {
-				return; // no classes assigned
-			}
-			// split the classes and check if it related to function
-			$(classes.split(/\s/)).each((i, key) => {
-				VE.callbacks[event_type][key] && VE.callbacks[event_type][key](evt, elm);
-			});
-		},
-	},
-	navigation: {
-		state: {
-			active_menu: 1,
-			menu_selector: '.main-menu-item',
-			menu_active_selector: '.active',
-		},
-		enter_focused: () => {
-			if ($('.units').hasClass('active')) {
-				location.href = $(
-					'.units.active .l-unit.focus .actions-panel__col.actions-panel__edit a'
-				).attr('href');
-			} else {
-				if ($(VE.navigation.state.menu_selector + '.focus a').attr('href')) {
-					location.href = $(VE.navigation.state.menu_selector + '.focus a').attr('href');
-				}
-			}
-		},
-		move_focus_left: () => {
-			let index = parseInt(
-				$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_selector + '.focus'))
-			);
-			if (index == -1)
-				index = parseInt(
-					$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_active_selector))
-				);
-
-			if ($('.units').hasClass('active')) {
-				$('.units').removeClass('active');
-				index++;
-			}
-
-			$(VE.navigation.state.menu_selector).removeClass('focus');
-
-			if (index > 0) {
-				$($(VE.navigation.state.menu_selector)[index - 1]).addClass('focus');
-			} else {
-				VE.navigation.switch_menu('last');
-			}
-		},
-		move_focus_right: () => {
-			const max_index = $(VE.navigation.state.menu_selector).length - 1;
-			let index = parseInt(
-				$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_selector + '.focus'))
-			);
-			if (index == -1)
-				index =
-					parseInt(
-						$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_active_selector))
-					) || 0;
-			$(VE.navigation.state.menu_selector).removeClass('focus');
-
-			if ($('.units').hasClass('active')) {
-				$('.units').removeClass('active');
-				index--;
-			}
-
-			if (index < max_index) {
-				$($(VE.navigation.state.menu_selector)[index + 1]).addClass('focus');
-			} else {
-				VE.navigation.switch_menu('first');
-			}
-		},
-		move_focus_down: () => {
-			const max_index = $('.units .l-unit:not(.header)').length - 1;
-			let index = parseInt($('.units .l-unit').index($('.units .l-unit.focus')));
-
-			if (index < max_index) {
-				$('.units .l-unit.focus').removeClass('focus');
-				$($('.units .l-unit:not(.header)')[index + 1]).addClass('focus');
-
-				$('html, body').animate({ scrollTop: $('.units .l-unit.focus').offset().top - 200 }, 200);
-			}
-		},
-		move_focus_up: () => {
-			let index = parseInt($('.units .l-unit:not(.header)').index($('.units .l-unit.focus')));
-
-			if (index == -1) index = 0;
-
-			if (index > 0) {
-				$('.units .l-unit.focus').removeClass('focus');
-				$($('.units .l-unit:not(.header)')[index - 1]).addClass('focus');
-
-				$('html, body').animate({ scrollTop: $('.units .l-unit.focus').offset().top - 200 }, 200);
-			}
-		},
-		switch_menu: (position) => {
-			position = position || 'first'; // last
-
-			if (VE.navigation.state.active_menu == 0) {
-				VE.navigation.state.active_menu = 1;
-				VE.navigation.state.menu_selector = '.main-menu-item';
-				VE.navigation.state.menu_active_selector = '.active';
-
-				if (position == 'first') {
-					$($(VE.navigation.state.menu_selector)[0]).addClass('focus');
-				} else {
-					const max_index = $(VE.navigation.state.menu_selector).length - 1;
-					$($(VE.navigation.state.menu_selector)[max_index]).addClass('focus');
-				}
-			}
-		},
-		shortcut: (elm) => {
-			/** @type {'js' | 'href'} */
-			const action = elm.attr('key-action');
-
-			switch (action) {
-				case 'js':
-					VE.core.dispatch(true, elm.find('.data-controls'), 'click');
-					break;
-
-				case 'href':
-					location.href = elm.find('a').attr('href');
-					break;
-
-				default:
-					break;
-			}
-		},
-	},
-	callbacks: {
-		click: {
-			// Usage: <a class="data-controls do_something" title="Something">Do something</a>
-			// do_something: (evt, elm) => {
-			// 	const ref = elm.hasClass('actions-panel') ? elm : elm.parents('.actions-panel');
-			// 	const title = $(elm).parent().attr('title') || $(elm).attr('title');
-			// 	const targetUrl = $('input[name="suspend_url"]', ref).val();
-			// 	VE.helpers.createConfirmationDialog({ title, targetUrl });
-			// },
-		},
-	},
-	helpers: {
-		/**
-		 * Create confirmation <dialog> on the fly
-		 * @param title The title of the dialog displayed in the header
-		 * @param message The message displayed in the body of the dialog
-		 * @param targetUrl URL that will be redirected to if user clicks "OK"
-		 */
-		createConfirmationDialog: ({ title, message = 'Are you sure?', targetUrl }) => {
-			// Create the dialog
-			const dialog = document.createElement('dialog');
-			dialog.classList.add('modal');
-
-			// Create and insert the title
-			if (title) {
-				const titleElem = document.createElement('h2');
-				titleElem.innerHTML = title;
-				titleElem.classList.add('modal-title');
-				dialog.appendChild(titleElem);
-			}
-
-			// Create and insert the message
-			const messageElem = document.createElement('p');
-			messageElem.innerHTML = message;
-			messageElem.classList.add('modal-message');
-			dialog.appendChild(messageElem);
-
-			// Create and insert the options
-			const optionsElem = document.createElement('div');
-			optionsElem.classList.add('modal-options');
-
-			const confirmButton = document.createElement('button');
-			confirmButton.type = 'submit';
-			confirmButton.classList.add('button');
-			confirmButton.textContent = 'OK';
-			optionsElem.appendChild(confirmButton);
-
-			const cancelButton = document.createElement('button');
-			cancelButton.type = 'button';
-			cancelButton.classList.add('button', 'button-secondary', 'cancel', 'u-ml5');
-			cancelButton.textContent = 'Cancel';
-			if (targetUrl) {
-				optionsElem.appendChild(cancelButton);
-			}
-
-			dialog.appendChild(optionsElem);
-
-			// Define named functions to handle the event listeners
-			const handleConfirm = () => {
-				if (targetUrl) {
-					window.location.href = targetUrl;
-				}
-				handleClose();
-			};
-			const handleCancel = () => handleClose();
-			const handleClose = () => {
-				confirmButton.removeEventListener('click', handleConfirm);
-				cancelButton.removeEventListener('click', handleCancel);
-				dialog.removeEventListener('close', handleClose);
-				document.removeEventListener('keydown', handleKeydown);
-				document.body.removeChild(dialog);
-			};
-			const handleKeydown = ({ key }) => {
-				if (key === 'Escape') {
-					handleClose();
-				}
-			};
-
-			// Add event listeners
-			confirmButton.addEventListener('click', handleConfirm);
-			cancelButton.addEventListener('click', handleCancel);
-			dialog.addEventListener('close', handleClose);
-			document.addEventListener('keydown', handleKeydown);
-
-			// Add to DOM and show
-			document.body.appendChild(dialog);
-			dialog.showModal();
-		},
-		recalculatePasswordStrength: (input) => {
-			const password = input.value;
-			const meter = input.parentNode.querySelector('.js-password-meter');
-			if (meter) {
-				// TODO: Switch to zxcvbn or something when we can load modules
-				const validations = [
-					password.length >= 8, // Min length of 8
-					password.search(/[a-z]/) > -1, // Contains 1 lowercase letter
-					password.search(/[A-Z]/) > -1, // Contains 1 uppercase letter
-					password.search(/[0-9]/) > -1, // Contains 1 number
-				];
-				const strength = validations.reduce((acc, cur) => acc + cur, 0);
-				meter.value = strength;
-			}
-		},
-		enableInputUnlimited: (input, toggleButton) => {
-			toggleButton.classList.add('is-active');
-			input.dataset.prevValue = input.value;
-			input.value = Alpine.store('globals').UNLIM_TRANSLATED_VALUE;
-			input.disabled = true;
-		},
-		disableInputUnlimited: (input, toggleButton) => {
-			toggleButton.classList.remove('is-active');
-			const prevValue = input.dataset.prevValue?.trim();
-			if (prevValue) {
-				input.value = prevValue;
-			}
-			if (Alpine.store('globals').isUnlimitedValue(input.value)) {
-				input.value = '0';
-			}
-			input.disabled = false;
-		},
-		toggleInputUnlimited: (input, toggleButton) => {
-			if (toggleButton.classList.contains('is-active')) {
-				VE.helpers.disableInputUnlimited(input, toggleButton);
-			} else {
-				VE.helpers.enableInputUnlimited(input, toggleButton);
-			}
-		},
-		warn: (msg) => {
-			alert('WARNING: ' + msg);
-		},
-		extendPasswordFields: () => {
-			const references = ['.js-password-input'];
-
-			$(document).ready(() => {
-				$(references).each((i, ref) => {
-					VE.helpers.initAdditionalPasswordFieldElements(ref);
-				});
-			});
-		},
-		initAdditionalPasswordFieldElements: (ref) => {
-			const enabled = Cookies.read('hide_passwords') == 1 ? true : false;
-			if (enabled) {
-				Cookies.set('hide_passwords', 1, 365);
-				$(ref).prop('type', 'password');
-			}
-
-			$(ref).prop('autocomplete', 'off');
-
-			const html =
-				'<span class="toggle-password"><i class="toggle-psw-visibility-icon fas fa-eye-slash ' +
-				enabled
-					? ''
-					: 'u-opacity-50' +
-					  '" onclick="VE.helpers.toggleHiddenPasswordText(\'' +
-					  ref +
-					  '\', this)"></i></span>';
-			$(ref).after(html);
-		},
-		toggleHiddenPasswordText: (ref, triggering_elm) => {
-			$(triggering_elm).toggleClass('u-opacity-50');
-
-			if ($(ref).prop('type') == 'text') {
-				Cookies.set('hide_passwords', 1, 365);
-				$(ref).prop('type', 'password');
-			} else {
-				Cookies.set('hide_passwords', 0, 365);
-				$(ref).prop('type', 'text');
-			}
-		},
-	},
-	tmp: {
-		sort_par: 'sort-name',
-		sort_direction: -1,
-		sort_as_int: false,
-	},
-};
-
-VE.helpers.extendPasswordFields();

+ 14 - 0
web/js/src/focusFirstInput.js

@@ -0,0 +1,14 @@
+// If no dialog is open, focus first input in main content form
+// TODO: Replace this with autofocus attributes in the HTML
+export default function focusFirstInput() {
+	const openDialogs = document.querySelectorAll('dialog[open]');
+	if (openDialogs.length === 0) {
+		const input = document.querySelector(
+			'#vstobjects .form-control:not([disabled]),\
+		#vstobjects .form-select:not([disabled])'
+		);
+		if (input) {
+			input.focus();
+		}
+	}
+}

+ 125 - 0
web/js/src/helpers.js

@@ -0,0 +1,125 @@
+import { nanoid } from 'nanoid';
+
+// Generates a random password using and ensures a number is included
+export function randomPassword(length = 16) {
+	return nanoid(length) + Math.floor(Math.random() * 10);
+}
+
+// Creates a confirmation <dialog> on the fly
+export function createConfirmationDialog({ title, message = 'Are you sure?', targetUrl }) {
+	// Create the dialog
+	const dialog = document.createElement('dialog');
+	dialog.classList.add('modal');
+
+	// Create and insert the title
+	if (title) {
+		const titleElement = document.createElement('h2');
+		titleElement.textContent = title;
+		titleElement.classList.add('modal-title');
+		dialog.append(titleElement);
+	}
+
+	// Create and insert the message
+	const messageElement = document.createElement('p');
+	messageElement.textContent = message;
+	messageElement.classList.add('modal-message');
+	dialog.append(messageElement);
+
+	// Create and insert the options
+	const optionsElement = document.createElement('div');
+	optionsElement.classList.add('modal-options');
+
+	const confirmButton = document.createElement('button');
+	confirmButton.type = 'submit';
+	confirmButton.classList.add('button');
+	confirmButton.textContent = 'OK';
+	optionsElement.append(confirmButton);
+
+	const cancelButton = document.createElement('button');
+	cancelButton.type = 'button';
+	cancelButton.classList.add('button', 'button-secondary', 'u-ml5');
+	cancelButton.textContent = 'Cancel';
+	if (targetUrl) {
+		optionsElement.append(cancelButton);
+	}
+
+	dialog.append(optionsElement);
+
+	// Define named functions to handle the event listeners
+	const handleConfirm = () => {
+		if (targetUrl) {
+			window.location.href = targetUrl;
+		}
+		handleClose();
+	};
+	const handleCancel = () => handleClose();
+	const handleClose = () => {
+		confirmButton.removeEventListener('click', handleConfirm);
+		cancelButton.removeEventListener('click', handleCancel);
+		dialog.removeEventListener('close', handleClose);
+		dialog.remove();
+	};
+
+	// Add event listeners
+	confirmButton.addEventListener('click', handleConfirm);
+	cancelButton.addEventListener('click', handleCancel);
+	dialog.addEventListener('close', handleClose);
+
+	// Add to DOM and show
+	document.body.append(dialog);
+	dialog.showModal();
+}
+
+// Monitors an input field for change and updates another selector with the value
+export function monitorAndUpdate(inputSelector, outputSelector) {
+	const inputElement = document.querySelector(inputSelector);
+	const outputElement = document.querySelector(outputSelector);
+
+	if (!inputElement || !outputElement) {
+		return;
+	}
+
+	function updateOutput(value) {
+		outputElement.textContent = value;
+		VE.helpers.generateMailCredentials();
+	}
+
+	inputElement.addEventListener('input', (event) => {
+		updateOutput(event.target.value);
+	});
+	updateOutput(inputElement.value);
+}
+
+// Updates hidden input field with values from cloned email info panel
+export function generateMailCredentials() {
+	const mailInfoPanel = document.querySelector('.js-mail-info');
+	if (!mailInfoPanel) return;
+	const formattedCredentials = emailCredentialsAsPlainText(mailInfoPanel.cloneNode(true));
+	document.querySelector('.js-hidden-credentials').value = formattedCredentials;
+}
+
+// Reformats cloned DOM email credentials into plain text
+export function emailCredentialsAsPlainText(element) {
+	const headings = [...element.querySelectorAll('h2')];
+	const lists = [...element.querySelectorAll('ul')];
+
+	return headings
+		.map((heading, index) => {
+			const items = [...lists[index].querySelectorAll('li')];
+
+			const itemText = items
+				.map((item) => {
+					const label = item.querySelector('.values-list-label');
+					const value = item.querySelector('.values-list-value');
+					const valueLink = value.querySelector('a');
+
+					const valueText = valueLink ? valueLink.href : value.textContent;
+
+					return `${label.textContent}: ${valueText}`;
+				})
+				.join('\n');
+
+			return `${heading.textContent}\n${itemText}\n`;
+		})
+		.join('\n');
+}

+ 0 - 63
web/js/src/init.js

@@ -1,63 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
-	function showLoader() {
-		document.querySelector('.fullscreen-loader').classList.add('show');
-	}
-	document.querySelector('#vstobjects')?.addEventListener('submit', showLoader);
-	document.querySelector('[x-bind="BulkEdit"]')?.addEventListener('submit', showLoader);
-
-	document.querySelectorAll('.toolbar-right .sort-by')?.forEach((el) => {
-		el.addEventListener('click', () => $('.context-menu.sort-order').toggle());
-	});
-
-	// TODO: Replace with autofocus
-	if (document.querySelectorAll('dialog[open]').length == 0) {
-		const input = document.querySelector(
-			'#vstobjects .form-control:not([disabled]),\
-			#vstobjects .form-select:not([disabled])'
-		);
-		if (input) {
-			input.focus();
-		}
-	}
-
-	// SORTING
-	document.querySelectorAll('.toolbar-sorting-toggle').forEach((toggle) => {
-		toggle.addEventListener('click', (evt) => {
-			evt.preventDefault();
-			document.querySelector('.toolbar-sorting-menu').classList.toggle('u-hidden');
-		});
-	});
-
-	$('.toolbar-sorting-menu span').click(function () {
-		$('.toolbar-sorting-menu').toggleClass('u-hidden');
-		if ($(this).hasClass('active')) return;
-
-		$('.toolbar-sorting-menu span').removeClass('active');
-		$(this).addClass('active');
-		VE.tmp.sort_par = $(this).parent('li').attr('entity');
-		VE.tmp.sort_as_int = !!$(this).parent('li').attr('sort_as_int');
-		VE.tmp.sort_direction = $(this).hasClass('up') * 1 || -1;
-
-		$('.toolbar-sorting-toggle b').html($(this).parent('li').find('.name').html());
-		$('.toolbar-sorting-toggle .fas').removeClass('fa-arrow-up-a-z fa-arrow-down-a-z');
-		$(this).hasClass('up')
-			? $('.toolbar-sorting-toggle .fas').addClass('fa-arrow-up-a-z')
-			: $('.toolbar-sorting-toggle .fas').addClass('fa-arrow-down-a-z');
-		$('.units .l-unit')
-			.sort((a, b) => {
-				if (VE.tmp.sort_as_int)
-					return parseInt($(a).attr(VE.tmp.sort_par)) >= parseInt($(b).attr(VE.tmp.sort_par))
-						? VE.tmp.sort_direction
-						: VE.tmp.sort_direction * -1;
-				else
-					return $(a).attr(VE.tmp.sort_par) <= $(b).attr(VE.tmp.sort_par)
-						? VE.tmp.sort_direction
-						: VE.tmp.sort_direction * -1;
-			})
-			.appendTo('.units');
-	});
-
-	$('.button.cancel').attr('title', 'ctrl+Backspace');
-
-	VE.core.register();
-});

+ 49 - 0
web/js/src/listSorting.js

@@ -0,0 +1,49 @@
+// List view sorting dropdown
+export default function initSorting() {
+	document.querySelectorAll('.toolbar-sorting-toggle').forEach((toggle) => {
+		toggle.addEventListener('click', (evt) => {
+			evt.preventDefault();
+			document.querySelector('.toolbar-sorting-menu').classList.toggle('u-hidden');
+		});
+	});
+
+	document.querySelectorAll('.toolbar-sorting-menu span').forEach((span) => {
+		span.addEventListener('click', function () {
+			const menu = document.querySelector('.toolbar-sorting-menu');
+			menu.classList.toggle('u-hidden');
+
+			if (this.classList.contains('active')) return;
+
+			document
+				.querySelectorAll('.toolbar-sorting-menu span')
+				.forEach((s) => s.classList.remove('active'));
+			this.classList.add('active');
+			const parentLi = this.closest('li');
+			VE.tmp.sort_par = parentLi.getAttribute('entity');
+			VE.tmp.sort_as_int = !!parentLi.getAttribute('sort_as_int');
+			VE.tmp.sort_direction = this.classList.contains('up') ? 1 : -1;
+
+			const toggle = document.querySelector('.toolbar-sorting-toggle');
+			toggle.querySelector('b').innerHTML = parentLi.querySelector('.name').innerHTML;
+			const fas = toggle.querySelector('.fas');
+			fas.classList.remove('fa-arrow-up-a-z', 'fa-arrow-down-a-z');
+			fas.classList.add(this.classList.contains('up') ? 'fa-arrow-up-a-z' : 'fa-arrow-down-a-z');
+
+			const units = Array.from(document.querySelectorAll('.units .l-unit')).sort((a, b) => {
+				const aAttr = a.getAttribute(VE.tmp.sort_par);
+				const bAttr = b.getAttribute(VE.tmp.sort_par);
+
+				if (VE.tmp.sort_as_int) {
+					const aInt = parseInt(aAttr);
+					const bInt = parseInt(bAttr);
+					return aInt >= bInt ? VE.tmp.sort_direction : VE.tmp.sort_direction * -1;
+				} else {
+					return aAttr <= bAttr ? VE.tmp.sort_direction : VE.tmp.sort_direction * -1;
+				}
+			});
+
+			const unitsContainer = document.querySelector('.units');
+			units.forEach((unit) => unitsContainer.appendChild(unit));
+		});
+	});
+}

+ 18 - 0
web/js/src/listeners.js

@@ -0,0 +1,18 @@
+import initConfirmationDialogs from './confirmationDialog.js';
+import initListSelectAll from './selectAll.js';
+import initListSorting from './listSorting.js';
+import initLoadingSpinner from './loadingSpinner.js';
+import initNameServerInput from './nameServerInput.js';
+import initPasswordInput from './passwordInput.js';
+import initStickyToolbar from './stickyToolbar.js';
+
+// Attaches generic page listeners
+export default function initPageListeners() {
+	initConfirmationDialogs();
+	initListSelectAll();
+	initListSorting();
+	initLoadingSpinner();
+	initNameServerInput();
+	initPasswordInput();
+	initStickyToolbar();
+}

+ 9 - 0
web/js/src/loadingSpinner.js

@@ -0,0 +1,9 @@
+// Attaches listeners to various events and shows loading spinner overlay
+export default function initLoadingSpinner() {
+	document.querySelector('#vstobjects')?.addEventListener('submit', showLoader);
+	document.querySelector('[x-bind="BulkEdit"]')?.addEventListener('submit', showLoader);
+}
+
+function showLoader() {
+	document.querySelector('.js-fullscreen-loader').classList.add('active');
+}

+ 31 - 281
web/js/src/main.js

@@ -1,287 +1,37 @@
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Cookies = {
-	/**
-	 * Creates a cookie.
-	 *
-	 * @param {string} name The name of the cookie.
-	 * @param {any} value The value to assign the cookie. It will be JSON encoded using JSON.stringify(...).
-	 * @param {number} days The number of days in which the cookie will expire. If none is provided,
-	 * it will create a session cookie.
-	 */
-	set(name, value, days = null) {
-		let expires = '';
-		if (days && !isNaN(days)) {
-			const date = new Date();
-			date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
-			expires = `; expires=${date.toUTCString()}`;
-		}
-
-		document.cookie =
-			`${name}=${JSON.stringify(value)}` + expires + '; path="/"; SameSite=None; Secure';
-	},
-
-	/**
-	 * Reads a cookie.
-	 *
-	 * @param {string} name The name of the cookie.
-	 * @returns {string} The value of the cookie, decoded with JSON.parse(...).
-	 */
-	read(name) {
-		const value = document.cookie
-			.split('; ')
-			.find((row) => row.startsWith(`${name}=`))
-			?.split('=')[1];
-
-		return value ? JSON.parse(value) : undefined;
-	},
-
-	/**
-	 * Removes a cookie.
-	 *
-	 * @param {string} name The name of the cookie.
-	 */
-	remove(name) {
-		this.set(name, '', -1);
-	},
-};
-
-/**
- * generates a random string using a cryptographically secure rng,
- * and ensuring it contains at least 1 lowercase, 1 uppercase, and 1 number.
- *
- * @param {int} [length=16]
- * @throws {Error} if length is too small to create a "sufficiently secure" string
- * @returns {string}
- */
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function randomString(length = 16) {
-	const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-
-	const rng = (min, max) => {
-		if (min < 0 || min > 0xffff) {
-			throw new Error(
-				'minimum supported number is 0, this generator can only make numbers between 0-65535 inclusive.'
-			);
-		}
-		if (max > 0xffff || max < 0) {
-			throw new Error(
-				'max supported number is 65535, this generator can only make numbers between 0-65535 inclusive.'
-			);
-		}
-		if (min > max) {
-			throw new Error('dude min>max wtf');
-		}
-		// micro-optimization
-		const randArr = max > 255 ? new Uint16Array(1) : new Uint8Array(1);
-		let result;
-		let attempts = 0;
-
-		// eslint-disable-next-line no-constant-condition
-		while (true) {
-			crypto.getRandomValues(randArr);
-			result = randArr[0];
-			if (result >= min && result <= max) {
-				return result;
-			}
-			++attempts;
-			if (attempts > 1000000) {
-				// should basically never happen with max 0xFFFF/Uint16Array.
-				throw new Error('tried a million times, something is wrong');
-			}
-		}
-	};
-
-	let attempts = 0;
-	const minimumStrengthRegex = new RegExp(
-		/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\d)[a-zA-Z\d]{8,}$/
-	);
-	const randmax = chars.length - 1;
-
-	// eslint-disable-next-line no-constant-condition
-	while (true) {
-		let result = '';
-		for (let i = 0; i < length; ++i) {
-			result += chars[rng(0, randmax)];
-		}
-		if (minimumStrengthRegex.test(result)) {
-			return result;
-		}
-		++attempts;
-		if (attempts > 1000000) {
-			throw new Error('tried a million times, something is wrong');
-		}
-	}
-}
-
-document.addEventListener('alpine:init', () => {
-	// Sticky class helper
-	window.addEventListener('scroll', () => {
-		const toolbar = document.querySelector('.toolbar');
-		const toolbarOffset =
-			toolbar.getBoundingClientRect().top + (window.scrollY - document.documentElement.clientTop);
-		const headerHeight = document.querySelector('.top-bar').offsetHeight;
-		const isActive = window.scrollY > toolbarOffset - headerHeight;
-
-		toolbar.classList.toggle('active', isActive);
-	});
-
-	// Select all helper
-	const toggleAll = document.querySelector('.js-toggle-all');
-	if (toggleAll) {
-		toggleAll.addEventListener('change', (evt) => {
-			document.querySelectorAll('.ch-toggle').forEach((el) => (el.checked = evt.target.checked));
-			document
-				.querySelectorAll('.l-unit')
-				.forEach((el) => el.classList.toggle('selected', evt.target.checked));
-		});
-	}
-
-	// Unlimited input toggle
-	document.querySelectorAll('.js-unlimited-toggle').forEach((toggleButton) => {
-		const input = toggleButton.parentElement.querySelector('input');
-
-		if (Alpine.store('globals').isUnlimitedValue(input.value)) {
-			VE.helpers.enableInputUnlimited(input, toggleButton);
-		} else {
-			VE.helpers.disableInputUnlimited(input, toggleButton);
-		}
-
-		toggleButton.addEventListener('click', () => {
-			VE.helpers.toggleInputUnlimited(input, toggleButton);
-		});
-	});
-
-	// Bulk edit forms
-	Alpine.bind('BulkEdit', () => ({
-		/** @param {SubmitEvent} evt */
-		'@submit'(evt) {
-			evt.preventDefault();
-			document.querySelectorAll('.ch-toggle').forEach((el) => {
-				if (el.checked) {
-					const input = document.createElement('input');
-					input.type = 'hidden';
-					input.name = el.name;
-					input.value = el.value;
-					evt.target.appendChild(input);
-				}
-			});
-
-			evt.target.submit();
+import alpineInit from './alpineInit.js';
+import focusFirstInput from './focusFirstInput.js';
+import initListeners from './listeners.js';
+import navigationMethods from './navigation.js';
+import {
+	randomPassword,
+	createConfirmationDialog,
+	generateMailCredentials,
+	monitorAndUpdate,
+} from './helpers.js';
+
+function initializeApp() {
+	window.VE = {
+		// List view sorting state
+		tmp: {
+			sort_par: 'sort-name',
+			sort_direction: -1,
+			sort_as_int: false,
 		},
-	}));
-
-	// Form state
-	Alpine.store('form', {
-		dirty: false,
-		makeDirty() {
-			this.dirty = true;
-		},
-	});
-	document
-		.querySelectorAll('#vstobjects input, #vstobjects select, #vstobjects textarea')
-		.forEach((el) => {
-			el.addEventListener('change', () => {
-				Alpine.store('form').makeDirty();
-			});
-		});
-
-	// Notifications data
-	Alpine.data('notifications', () => ({
-		initialized: false,
-		open: false,
-		notifications: [],
-		toggle() {
-			this.open = !this.open;
-			if (!this.initialized) {
-				this.list();
-			}
-		},
-		async list() {
-			const token = document.querySelector('#token').getAttribute('token');
-			const res = await fetch(`/list/notifications/?ajax=1&token=${token}`);
-			this.initialized = true;
-			if (!res.ok) {
-				throw new Error('An error occurred while listing notifications.');
-			}
-
-			this.notifications = Object.entries(await res.json()).reduce(
-				(acc, [_id, notification]) => [...acc, notification],
-				[]
-			);
-		},
-		async remove(id) {
-			const token = document.querySelector('#token').getAttribute('token');
-			await fetch(`/delete/notification/?delete=1&notification_id=${id}&token=${token}`);
-
-			this.notifications = this.notifications.filter((notification) => notification.ID != id);
-			if (this.notifications.length == 0) {
-				this.open = false;
-			}
-		},
-		async removeAll() {
-			const token = document.querySelector('#token').getAttribute('token');
-			await fetch(`/delete/notification/?delete=1&token=${token}`);
-
-			this.notifications = [];
-			this.open = false;
+		// Page navigation methods called by shortcuts
+		navigation: navigationMethods(),
+		// Helpers exposed for page-specific JS and inline <script> usage
+		helpers: {
+			createConfirmationDialog,
+			randomPassword,
+			generateMailCredentials,
+			monitorAndUpdate,
 		},
-	}));
-});
-
-// Add new name server input
-const addNsButton = document.querySelector('.js-add-ns');
-if (addNsButton) {
-	addNsButton.addEventListener('click', () => {
-		const currentNsInputs = document.querySelectorAll('input[name^=v_ns]');
-		const inputCount = currentNsInputs.length;
-
-		if (inputCount < 8) {
-			const template = currentNsInputs[0].parentElement.cloneNode(true);
-			const templateNsInput = template.querySelector('input');
-
-			templateNsInput.removeAttribute('value');
-			templateNsInput.name = `v_ns${inputCount + 1}`;
-			addNsButton.insertAdjacentElement('beforebegin', template);
-		}
+	};
 
-		if (inputCount === 7) {
-			addNsButton.classList.add('u-hidden');
-		}
-	});
+	initListeners();
+	focusFirstInput();
 }
 
-// Remove name server input
-document.querySelectorAll('.js-remove-ns').forEach((removeNsButton) => {
-	removeNsButton.addEventListener('click', () => {
-		removeNsButton.parentElement.remove();
-		const currentNsInputs = document.querySelectorAll('input[name^=v_ns]');
-		currentNsInputs.forEach((input, index) => (input.name = `v_ns${index + 1}`));
-		document.querySelector('.js-add-ns').classList.remove('u-hidden');
-	});
-});
-
-// Intercept clicks on .js-confirm-action links and display dialog
-document.querySelectorAll('.js-confirm-action').forEach((triggerLink) => {
-	triggerLink.addEventListener('click', (evt) => {
-		evt.preventDefault();
-
-		const title = triggerLink.dataset.confirmTitle;
-		const message = triggerLink.dataset.confirmMessage;
-		const targetUrl = triggerLink.getAttribute('href');
-
-		VE.helpers.createConfirmationDialog({ title, message, targetUrl });
-	});
-});
-
-// Listen for changes to password inputs and update the password strength
-document.querySelectorAll('.js-password-input').forEach((passwordInput) => {
-	const updateTimeout = (evt) => {
-		clearTimeout(window.frp_usr_tmt);
-		window.frp_usr_tmt = setTimeout(() => {
-			VE.helpers.recalculatePasswordStrength(evt.target);
-		}, 100);
-	};
+initializeApp();
 
-	passwordInput.addEventListener('keypress', updateTimeout);
-	passwordInput.addEventListener('input', updateTimeout);
-});
+document.addEventListener('alpine:init', () => alpineInit());

+ 34 - 0
web/js/src/nameServerInput.js

@@ -0,0 +1,34 @@
+// Attaches listeners to nameserver add and remove links to clone or remove the input
+export default function initNameServerInput() {
+	// Add new name server input
+	const addNsButton = document.querySelector('.js-add-ns');
+	if (addNsButton) {
+		addNsButton.addEventListener('click', () => {
+			const currentNsInputs = document.querySelectorAll('input[name^=v_ns]');
+			const inputCount = currentNsInputs.length;
+
+			if (inputCount < 8) {
+				const template = currentNsInputs[0].parentElement.cloneNode(true);
+				const templateNsInput = template.querySelector('input');
+
+				templateNsInput.removeAttribute('value');
+				templateNsInput.name = `v_ns${inputCount + 1}`;
+				addNsButton.before(template);
+			}
+
+			if (inputCount === 7) {
+				addNsButton.classList.add('u-hidden');
+			}
+		});
+	}
+
+	// Remove name server input
+	document.querySelectorAll('.js-remove-ns').forEach((removeNsButton) => {
+		removeNsButton.addEventListener('click', () => {
+			removeNsButton.parentElement.remove();
+			const currentNsInputs = document.querySelectorAll('input[name^=v_ns]');
+			currentNsInputs.forEach((input, index) => (input.name = `v_ns${index + 1}`));
+			document.querySelector('.js-add-ns').classList.remove('u-hidden');
+		});
+	});
+}

+ 125 - 0
web/js/src/navigation.js

@@ -0,0 +1,125 @@
+// Page navigation methods called by shortcuts
+export default function navigationMethods() {
+	return {
+		state: {
+			active_menu: 1,
+			menu_selector: '.main-menu-item',
+			menu_active_selector: '.active',
+		},
+		enterFocused: () => {
+			if ($('.units').hasClass('active')) {
+				location.href = $(
+					'.units.active .l-unit.focus .actions-panel__col.actions-panel__edit a'
+				).attr('href');
+			} else {
+				if ($(VE.navigation.state.menu_selector + '.focus a').attr('href')) {
+					location.href = $(VE.navigation.state.menu_selector + '.focus a').attr('href');
+				}
+			}
+		},
+		moveFocusLeft: () => {
+			let index = Number.parseInt(
+				$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_selector + '.focus'))
+			);
+			if (index == -1)
+				index = Number.parseInt(
+					$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_active_selector))
+				);
+
+			if ($('.units').hasClass('active')) {
+				$('.units').removeClass('active');
+				index++;
+			}
+
+			$(VE.navigation.state.menu_selector).removeClass('focus');
+
+			if (index > 0) {
+				$($(VE.navigation.state.menu_selector)[index - 1]).addClass('focus');
+			} else {
+				VE.navigation.switchMenu('last');
+			}
+		},
+		moveFocusRight: () => {
+			const max_index = $(VE.navigation.state.menu_selector).length - 1;
+			let index = Number.parseInt(
+				$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_selector + '.focus'))
+			);
+			if (index == -1)
+				index =
+					Number.parseInt(
+						$(VE.navigation.state.menu_selector).index($(VE.navigation.state.menu_active_selector))
+					) || 0;
+			$(VE.navigation.state.menu_selector).removeClass('focus');
+
+			if ($('.units').hasClass('active')) {
+				$('.units').removeClass('active');
+				index--;
+			}
+
+			if (index < max_index) {
+				$($(VE.navigation.state.menu_selector)[index + 1]).addClass('focus');
+			} else {
+				VE.navigation.switchMenu('first');
+			}
+		},
+		moveFocusDown: () => {
+			const max_index = $('.units .l-unit:not(.header)').length - 1;
+			const index = Number.parseInt($('.units .l-unit').index($('.units .l-unit.focus')));
+
+			if (index < max_index) {
+				$('.units .l-unit.focus').removeClass('focus');
+				$($('.units .l-unit:not(.header)')[index + 1]).addClass('focus');
+
+				$('html, body').animate({ scrollTop: $('.units .l-unit.focus').offset().top - 200 }, 200);
+			}
+		},
+		moveFocusUp: () => {
+			let index = Number.parseInt(
+				$('.units .l-unit:not(.header)').index($('.units .l-unit.focus'))
+			);
+
+			if (index == -1) index = 0;
+
+			if (index > 0) {
+				$('.units .l-unit.focus').removeClass('focus');
+				$($('.units .l-unit:not(.header)')[index - 1]).addClass('focus');
+
+				$('html, body').animate({ scrollTop: $('.units .l-unit.focus').offset().top - 200 }, 200);
+			}
+		},
+		switchMenu: (position) => {
+			position = position || 'first'; // last
+
+			if (VE.navigation.state.active_menu == 0) {
+				VE.navigation.state.active_menu = 1;
+				VE.navigation.state.menu_selector = '.main-menu-item';
+				VE.navigation.state.menu_active_selector = '.active';
+
+				if (position == 'first') {
+					$($(VE.navigation.state.menu_selector)[0]).addClass('focus');
+				} else {
+					const max_index = $(VE.navigation.state.menu_selector).length - 1;
+					$($(VE.navigation.state.menu_selector)[max_index]).addClass('focus');
+				}
+			}
+		},
+		shortcut: (elm) => {
+			const action = elm[0].dataset.keyAction;
+			switch (action) {
+				case 'js': {
+					elm[0].querySelector('.data-controls').click();
+					break;
+				}
+
+				case 'href': {
+					location.href = elm.find('a').attr('href');
+					break;
+				}
+
+				default: {
+					break;
+				}
+			}
+		},
+	};
+}

+ 43 - 0
web/js/src/notifications.js

@@ -0,0 +1,43 @@
+// Returns methods for handling notifications with Alpine.js
+export default function notificationMethods() {
+	return {
+		initialized: false,
+		open: false,
+		notifications: [],
+		toggle() {
+			this.open = !this.open;
+			if (!this.initialized) {
+				this.list();
+			}
+		},
+		async list() {
+			const token = document.querySelector('#token').getAttribute('token');
+			const res = await fetch(`/list/notifications/?ajax=1&token=${token}`);
+			this.initialized = true;
+			if (!res.ok) {
+				throw new Error('An error occurred while listing notifications.');
+			}
+
+			this.notifications = Object.entries(await res.json()).reduce(
+				(accumulator, [_id, notification]) => [...accumulator, notification],
+				[]
+			);
+		},
+		async remove(id) {
+			const token = document.querySelector('#token').getAttribute('token');
+			await fetch(`/delete/notification/?delete=1&notification_id=${id}&token=${token}`);
+
+			this.notifications = this.notifications.filter((notification) => notification.ID != id);
+			if (this.notifications.length === 0) {
+				this.open = false;
+			}
+		},
+		async removeAll() {
+			const token = document.querySelector('#token').getAttribute('token');
+			await fetch(`/delete/notification/?delete=1&token=${token}`);
+
+			this.notifications = [];
+			this.open = false;
+		},
+	};
+}

+ 47 - 0
web/js/src/passwordInput.js

@@ -0,0 +1,47 @@
+import { randomPassword, generateMailCredentials } from './helpers.js';
+
+// Adds listeners to password inputs (to monitor strength) and generate password buttons
+export default function initPasswordInput() {
+	// Listen for changes to password inputs and update the password strength
+	document.querySelectorAll('.js-password-input').forEach((passwordInput) => {
+		const updateTimeout = (evt) => {
+			clearTimeout(window.frp_usr_tmt);
+			window.frp_usr_tmt = setTimeout(() => {
+				recalculatePasswordStrength(evt.target);
+			}, 100);
+		};
+
+		passwordInput.addEventListener('keypress', updateTimeout);
+		passwordInput.addEventListener('input', updateTimeout);
+	});
+
+	// Listen for clicks on all js-generate-password buttons and generate a password
+	document.querySelectorAll('.js-generate-password').forEach((generatePasswordButton) => {
+		generatePasswordButton.addEventListener('click', () => {
+			const passwordInput =
+				generatePasswordButton.parentNode.nextElementSibling.querySelector('.js-password-input');
+			if (passwordInput) {
+				passwordInput.value = randomPassword();
+				passwordInput.dispatchEvent(new Event('input'));
+				recalculatePasswordStrength(passwordInput);
+				generateMailCredentials();
+			}
+		});
+	});
+}
+
+// TODO: Switch to zxcvbn module or something to determine password strength?
+function recalculatePasswordStrength(input) {
+	const password = input.value;
+	const meter = input.parentNode.querySelector('.js-password-meter');
+	if (meter) {
+		const validations = [
+			password.length >= 8, // Min length of 8
+			password.search(/[a-z]/) > -1, // Contains 1 lowercase letter
+			password.search(/[A-Z]/) > -1, // Contains 1 uppercase letter
+			password.search(/\d/) > -1 || password.search(/[^\dA-Za-z]/) > -1, // Contains 1 number or special character
+		];
+		const strength = validations.reduce((acc, cur) => acc + cur, 0);
+		meter.value = strength;
+	}
+}

+ 19 - 0
web/js/src/selectAll.js

@@ -0,0 +1,19 @@
+// Select all checkbox on list view pages
+export default function initListSelectAll() {
+	const toggleAll = document.querySelector('.js-toggle-all');
+	if (toggleAll) {
+		toggleAll.addEventListener('change', handleToggleAllChange);
+	}
+}
+
+function handleToggleAllChange(evt) {
+	const isChecked = evt.target.checked;
+
+	document.querySelectorAll('.ch-toggle').forEach((el) => {
+		el.checked = isChecked;
+	});
+
+	document.querySelectorAll('.l-unit').forEach((el) => {
+		el.classList.toggle('selected', isChecked);
+	});
+}

+ 26 - 27
web/js/src/shortcuts.js

@@ -1,11 +1,13 @@
+import { createConfirmationDialog } from './helpers.js';
+
 /**
+ * Shortcuts
  * @typedef {{ key: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} KeyCombination
  * @typedef {{ code: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} CodeCombination
  * @typedef {{ combination: KeyCombination, event: 'keydown' | 'keyup', callback: (evt: KeyboardEvent) => void, target: EventTarget }} RegisteredShortcut
  * @typedef {{ type?: 'keydown' | 'keyup', propagate?: boolean, disabledInInput?: boolean, target?: EventTarget }} ShortcutOptions
  */
-
-document.addEventListener('alpine:init', () => {
+export default function initShortcuts() {
 	Alpine.store('shortcuts', {
 		/**
 		 * @type RegisteredShortcut[]
@@ -68,7 +70,7 @@ document.addEventListener('alpine:init', () => {
 			};
 
 			this.registeredShortcuts.push({
-				combination: combination,
+				combination,
 				callback: func,
 				target: options.target,
 				event: options.type,
@@ -133,15 +135,10 @@ document.addEventListener('alpine:init', () => {
 			}
 
 			if (Alpine.store('form').dirty && redirect) {
-				VE.helpers.createConfirmationDialog({
+				createConfirmationDialog({
 					message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 					targetUrl: redirect,
 				});
-			} else if (document.querySelector('form#vstobjects .button.cancel')) {
-				location.href = $('form#vstobjects input.cancel')
-					.attr('onclick')
-					.replace("location.href='", '')
-					.replace("'", '');
 			} else if (redirect) {
 				location.href = redirect;
 			}
@@ -150,7 +147,9 @@ document.addEventListener('alpine:init', () => {
 			{ key: 'F' },
 			(_evt) => {
 				const searchBox = document.querySelector('.js-search-input');
-				searchBox.focus();
+				if (searchBox) {
+					searchBox.focus();
+				}
 			},
 			{ disabledInInput: true }
 		)
@@ -162,7 +161,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -180,7 +179,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -198,7 +197,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -216,7 +215,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -234,7 +233,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -252,7 +251,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -270,7 +269,7 @@ document.addEventListener('alpine:init', () => {
 					return;
 				}
 				if (Alpine.store('form').dirty) {
-					VE.helpers.createConfirmationDialog({
+					createConfirmationDialog({
 						message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 						targetUrl: target.href,
 					});
@@ -293,37 +292,37 @@ document.addEventListener('alpine:init', () => {
 			{ disabledInInput: true }
 		)
 		.register({ code: 'Escape' }, (_evt) => {
-			const shortcutsDialog = document.querySelector('.shortcuts');
-			if (shortcutsDialog.open) {
-				shortcutsDialog.close();
+			const openDialog = document.querySelector('dialog[open]');
+			if (openDialog) {
+				openDialog.close();
 			}
 			document.querySelectorAll('input, checkbox, textarea, select').forEach((el) => el.blur());
 		})
 		.register(
 			{ code: 'ArrowLeft' },
 			(_evt) => {
-				VE.navigation.move_focus_left();
+				VE.navigation.moveFocusLeft();
 			},
 			{ disabledInInput: true }
 		)
 		.register(
 			{ code: 'ArrowRight' },
 			(_evt) => {
-				VE.navigation.move_focus_right();
+				VE.navigation.moveFocusRight();
 			},
 			{ disabledInInput: true }
 		)
 		.register(
 			{ code: 'ArrowDown' },
 			(_evt) => {
-				VE.navigation.move_focus_down();
+				VE.navigation.moveFocusDown();
 			},
 			{ disabledInInput: true }
 		)
 		.register(
 			{ code: 'ArrowUp' },
 			(_evt) => {
-				VE.navigation.move_focus_up();
+				VE.navigation.moveFocusUp();
 			},
 			{ disabledInInput: true }
 		)
@@ -419,7 +418,7 @@ document.addEventListener('alpine:init', () => {
 						const dialog = document.querySelector('dialog[open]');
 						dialog.querySelector('button[type="submit"]').click();
 					} else {
-						VE.helpers.createConfirmationDialog({
+						createConfirmationDialog({
 							message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
 							targetUrl: document.querySelector(`${VE.navigation.state.menu_selector}.focus a`)
 								.href,
@@ -434,11 +433,11 @@ document.addEventListener('alpine:init', () => {
 						if (el.length) {
 							VE.navigation.shortcut(el);
 						} else {
-							VE.navigation.enter_focused();
+							VE.navigation.enterFocused();
 						}
 					}
 				}
 			},
 			{ propagate: true }
 		);
-});
+}

+ 22 - 0
web/js/src/stickyToolbar.js

@@ -0,0 +1,22 @@
+// Add class to (sticky) toolbar on list view pages when scrolling
+export default function initStickyToolbar() {
+	const toolbar = document.querySelector('.toolbar');
+	const header = document.querySelector('.top-bar');
+
+	if (!toolbar || !header) {
+		return;
+	}
+
+	window.addEventListener('scroll', handleToolbarOnScroll);
+
+	function handleToolbarOnScroll() {
+		const toolbarRectTop = toolbar.getBoundingClientRect().top;
+		const scrolledDistance = window.scrollY;
+		const clientTop = document.documentElement.clientTop;
+		const toolbarOffsetTop = toolbarRectTop + scrolledDistance - clientTop;
+		const headerHeight = header.offsetHeight;
+		const isToolbarActive = scrolledDistance > toolbarOffsetTop - headerHeight;
+
+		toolbar.classList.toggle('active', isToolbarActive);
+	}
+}

+ 43 - 0
web/js/src/unlimitedInput.js

@@ -0,0 +1,43 @@
+// Adds listeners for "unlimited" input toggles
+export default function initUnlimitedInput() {
+	document.querySelectorAll('.js-unlimited-toggle').forEach((toggleButton) => {
+		const input = toggleButton.parentElement.querySelector('input');
+
+		if (Alpine.store('globals').isUnlimitedValue(input.value)) {
+			enableUnlimitedInput(input, toggleButton);
+		} else {
+			disableUnlimitedInput(input, toggleButton);
+		}
+
+		toggleButton.addEventListener('click', () => {
+			toggleUnlimitedInput(input, toggleButton);
+		});
+	});
+}
+
+function enableUnlimitedInput(input, toggleButton) {
+	toggleButton.classList.add('active');
+	input.dataset.prevValue = input.value;
+	input.value = Alpine.store('globals').UNLIM_TRANSLATED_VALUE;
+	input.disabled = true;
+}
+
+function disableUnlimitedInput(input, toggleButton) {
+	toggleButton.classList.remove('active');
+	const previousValue = input.dataset.prevValue?.trim();
+	if (previousValue) {
+		input.value = previousValue;
+	}
+	if (Alpine.store('globals').isUnlimitedValue(input.value)) {
+		input.value = '0';
+	}
+	input.disabled = false;
+}
+
+function toggleUnlimitedInput(input, toggleButton) {
+	if (toggleButton.classList.contains('active')) {
+		disableUnlimitedInput(input, toggleButton);
+	} else {
+		enableUnlimitedInput(input, toggleButton);
+	}
+}

+ 1 - 1
web/templates/footer.php

@@ -13,7 +13,7 @@
 		</button>)
 	</p>
 <?php } ?>
-	<div class="fullscreen-loader">
+	<div class="fullscreen-loader js-fullscreen-loader">
 		<i class="fas fa-circle-notch fa-spin"></i>
 	</div>
 

+ 1 - 4
web/templates/includes/js.php

@@ -1,8 +1,5 @@
-<script defer src="/js/dist/main.min.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/vendor/jquery-3.6.4.min.js?<?= JS_LATEST_UPDATE ?>"></script>
-<script defer src="/js/dist/shortcuts.min.js?<?= JS_LATEST_UPDATE ?>"></script>
-<script defer src="/js/dist/events.min.js?<?= JS_LATEST_UPDATE ?>"></script>
-<script defer src="/js/dist/init.min.js?<?= JS_LATEST_UPDATE ?>"></script>
+<script defer src="/js/dist/main.min.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script defer src="/js/vendor/alpine-3.10.5.min.js?<?= JS_LATEST_UPDATE ?>"></script>
 <script>
 	// TODO: REMOVE

+ 3 - 1
web/templates/pages/add_db.php

@@ -75,7 +75,9 @@
 				<div class="u-mb10">
 					<label for="v_password" class="form-label">
 						<?= _("Password") ?>
-						<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+						<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+							<i class="fas fa-arrows-rotate icon-green"></i>
+						</button>
 					</label>
 					<div class="u-pos-relative u-mb10">
 						<input type="text" class="form-control js-password-input" name="v_password" id="v_password">

+ 4 - 2
web/templates/pages/add_mail_acc.php

@@ -40,12 +40,14 @@
 					</div>
 					<div class="u-mb10">
 						<label for="v_account" class="form-label"><?= _("Account") ?></label>
-						<input type="text" class="form-control" name="v_account" id="v_account" value="<?= htmlentities(trim($v_account, "'")) ?>">
+						<input type="text" class="form-control js-account-input" name="v_account" id="v_account" value="<?= htmlentities(trim($v_account, "'")) ?>">
 					</div>
 					<div class="u-mb10">
 						<label for="v_password" class="form-label">
 							<?= _("Password") ?>
-							<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+							<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+								<i class="fas fa-arrows-rotate icon-green"></i>
+							</button>
 						</label>
 						<div class="u-pos-relative u-mb10">
 							<input type="text" class="form-control js-password-input" name="v_password" id="v_password">

+ 1 - 1
web/templates/pages/add_package.php

@@ -37,7 +37,7 @@
 			<?php show_alert_message($_SESSION); ?>
 			<div class="u-mb10">
 				<label for="v_package" class="form-label"><?= _("Package Name") ?></label>
-				<input type="text" class="form-control" name="v_package" id="v_package" value="<?= htmlentities(trim($v_package, "'")) ?>">
+				<input type="text" class="form-control" name="v_package" id="v_package" value="<?= htmlentities(trim($v_package, "'")) ?>" required>
 			</div>
 			<div class="u-mb10">
 				<label for="v_disk_quota" class="form-label">

+ 3 - 1
web/templates/pages/add_user.php

@@ -46,7 +46,9 @@
 			<div class="u-mb10">
 				<label for="v_password" class="form-label">
 					<?= _("Password") ?>
-					<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+					<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+						<i class="fas fa-arrows-rotate icon-green"></i>
+					</button>
 				</label>
 				<div class="u-pos-relative u-mb10">
 					<input type="text" class="form-control js-password-input" name="v_password" id="v_password" value="<?= htmlentities(trim($v_password, "'")) ?>" tabindex="4" required>

+ 3 - 1
web/templates/pages/edit_db.php

@@ -40,7 +40,9 @@
 			<div class="u-mb10">
 				<label for="v_password" class="form-label">
 					<?= _("Password") ?>
-					<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+					<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+						<i class="fas fa-arrows-rotate icon-green"></i>
+					</button>
 				</label>
 				<div class="u-pos-relative u-mb10">
 					<input type="text" class="form-control js-password-input" name="v_password" id="v_password" value="<?= htmlentities(trim($v_password, "'")) ?>">

+ 4 - 2
web/templates/pages/edit_mail_acc.php

@@ -38,12 +38,14 @@
 						<label for="v_email" class="form-label"><?= _("Account") ?></label>
 						<input type="text" class="form-control" name="v_email" id="v_email" value="<?= htmlentities($_GET["account"]) . "@" . htmlentities($_GET["domain"]) ?>" disabled>
 						<input type="hidden" name="v_domain" value="<?= htmlentities(trim($v_domain, "'")) ?>">
-						<input type="hidden" name="v_account" value="<?= htmlentities(trim($v_account, "'")) ?>">
+						<input type="hidden" name="v_account" value="<?= htmlentities(trim($v_account, "'")) ?>" class="js-account-input">
 					</div>
 					<div class="u-mb10">
 						<label for="v_password" class="form-label">
 							<?= _("Password") ?>
-							<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+							<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+								<i class="fas fa-arrows-rotate icon-green"></i>
+							</button>
 						</label>
 						<div class="u-pos-relative u-mb10">
 							<input type="text" class="form-control js-password-input" name="v_password" id="v_password" value="<?= htmlentities(trim($v_password, "'")) ?>">

+ 1 - 1
web/templates/pages/edit_package.php

@@ -39,7 +39,7 @@
 			<?php show_alert_message($_SESSION); ?>
 			<div class="u-mb10">
 				<label for="v_package_new" class="form-label"><?= _("Package Name") ?></label>
-				<input type="text" class="form-control" name="v_package_new" id="v_package_new" value="<?= htmlentities(trim($v_package_new, "'")) ?>">
+				<input type="text" class="form-control" name="v_package_new" id="v_package_new" value="<?= htmlentities(trim($v_package_new, "'")) ?>" required>
 				<input type="hidden" name="v_package" value="<?= htmlentities(trim($v_package, "'")) ?>">
 			</div>
 			<div class="u-mb10">

+ 3 - 1
web/templates/pages/edit_user.php

@@ -82,7 +82,9 @@
 			<div class="u-mb10">
 				<label for="v_password" class="form-label">
 					<?= _("Password") ?>
-					<a href="javascript:applyRandomPassword();" title="<?= _("generate") ?>" class="u-ml5"><i class="fas fa-arrows-rotate icon-green"></i></a>
+					<button type="button" title="<?= _("generate") ?>" class="u-unstyled-button u-ml5 js-generate-password">
+						<i class="fas fa-arrows-rotate icon-green"></i>
+					</button>
 				</label>
 				<div class="u-pos-relative u-mb10">
 					<input type="text" class="form-control js-password-input" name="v_password" id="v_password" value="<?= htmlentities(trim($v_password, "'")) ?>">

+ 1 - 1
web/templates/pages/list_access_keys.php

@@ -80,7 +80,7 @@
 				<div class="clearfix l-unit__stat-col--left compact u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/delete/access-key/?key=<?= $key ?>&token=<?= $_SESSION["token"] ?>"

+ 3 - 3
web/templates/pages/list_backup.php

@@ -92,10 +92,10 @@
 									<!-- Restrict ability to restore or delete backups when impersonating 'admin' account -->
 									&nbsp;
 								<?php } else { ?>
-									<div class="actions-panel__col actions-panel__download shortcut-d" key-action="href"><a href="/download/backup/?backup=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("download") ?>"><i class="fas fa-file-arrow-down icon-lightblue icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__download shortcut-d" data-key-action="href"><a href="/download/backup/?backup=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("download") ?>"><i class="fas fa-file-arrow-down icon-lightblue icon-dim"></i></a></div>
 									<?php if ($read_only !== 'true') {?>
-										<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href"><a href="/list/backup/?backup=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("restore") ?>"><i class="fas fa-arrow-rotate-left icon-green icon-dim"></i></a></div>
-										<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+										<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href"><a href="/list/backup/?backup=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("restore") ?>"><i class="fas fa-arrow-rotate-left icon-green icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 											<a
 												class="data-controls js-confirm-action"
 												href="/delete/backup/?backup=<?= $key ?>&token=<?= $_SESSION["token"] ?>"

+ 6 - 6
web/templates/pages/list_backup_detail.php

@@ -65,7 +65,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=web&object=<?= $key ?>&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim u-mr5"></i>
 								</a>
@@ -97,7 +97,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=mail&object=<?= $key ?>&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim"></i>
 								</a>
@@ -129,7 +129,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=dns&object=<?= $key ?>&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim"></i>
 								</a>
@@ -161,7 +161,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=db&object=<?= $key ?>&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim"></i>
 								</a>
@@ -190,7 +190,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=cron&object=records&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim"></i>
 								</a>
@@ -222,7 +222,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4 u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__list shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__list shortcut-enter" data-key-action="href">
 								<a href="/schedule/restore/?backup=<?= $backup ?>&type=udir&object=<?= $key ?>&token=<?= $_SESSION["token"] ?>" title="<?= _("Restore") ?>">
 									<i class="fas fa-arrow-rotate-left icon-green icon-dim"></i>
 								</a>

+ 3 - 3
web/templates/pages/list_cron.php

@@ -109,9 +109,9 @@
 								&nbsp;
 							<?php } else { ?>
 								<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-									<div class="actions-panel__col actions-panel__download shortcut-enter" key-action="href"><a href="/edit/cron/?job=<?=$data[$key]['JOB']?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Cron Job") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__download shortcut-enter" data-key-action="href"><a href="/edit/cron/?job=<?=$data[$key]['JOB']?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Cron Job") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 								<?php } ?>
-								<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+								<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/<?= $spnd_action ?>/cron/?job=<?= $data[$key]["JOB"] ?>&token=<?= $_SESSION["token"] ?>"
@@ -121,7 +121,7 @@
 										<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 									</a>
 								</div>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/cron/?job=<?= $data[$key]["JOB"] ?>&token=<?= $_SESSION["token"] ?>"

+ 4 - 4
web/templates/pages/list_db.php

@@ -149,12 +149,12 @@ if (!empty($_SESSION["DB_PGA_ALIAS"])) {
 									&nbsp;
 								<?php } else { ?>
 									<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-										<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/db/?database=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Database") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/db/?database=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Database") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 									<?php } ?>
 									<?php if ($data[$key]['TYPE'] == 'mysql' && isset($_SESSION['PHPMYADMIN_KEY']) && $_SESSION['PHPMYADMIN_KEY'] != '' && !ipUsed()) { $time = time(); ?>
-										<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a target="_blank" href="<?=$db_myadmin_link;?>/hestia-sso.php?database=<?=$key;?>&user=<?=$user_plain;?>&exp=<?=$time;?>&hestia_token=<?=password_hash($key.$user_plain.$_SESSION['user_combined_ip'].$time.$_SESSION['PHPMYADMIN_KEY'], PASSWORD_DEFAULT)?>" title="<?= _("phpMyAdmin") ?>"><i class="fas fa-right-to-bracket icon-orange icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a target="_blank" href="<?=$db_myadmin_link;?>/hestia-sso.php?database=<?=$key;?>&user=<?=$user_plain;?>&exp=<?=$time;?>&hestia_token=<?=password_hash($key.$user_plain.$_SESSION['user_combined_ip'].$time.$_SESSION['PHPMYADMIN_KEY'], PASSWORD_DEFAULT)?>" title="<?= _("phpMyAdmin") ?>"><i class="fas fa-right-to-bracket icon-orange icon-dim"></i></a></div>
 									<?php } ?>
-									<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+									<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/<?=$spnd_action?>/db/?database=<?=$key?>&token=<?=$_SESSION['token']?>"
@@ -164,7 +164,7 @@ if (!empty($_SESSION["DB_PGA_ALIAS"])) {
 											<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 										</a>
 									</div>
-									<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+									<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/delete/db/?database=<?= $key ?>&token=<?= $_SESSION["token"] ?>"

+ 6 - 6
web/templates/pages/list_dns.php

@@ -112,13 +112,13 @@
 								&nbsp;
 							<?php } else { ?>
 								<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-									<div class="actions-panel__col actions-panel__logs shortcut-n" key-action="href"><a href="/add/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("Add DNS Record") ?>"><i class="fas fa-circle-plus icon-green icon-dim"></i></a></div>
-									<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing DNS Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
-									<?php if($data[$key]['DNSSEC'] == "yes"){?><div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/list/dns/?domain=<?=htmlentities($key);?>&action=dnssec&token=<?=$_SESSION['token']?>" title="<?= _("View Public DNSSEC key") ?>"><i class="fas fa-key icon-orange icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__logs shortcut-n" data-key-action="href"><a href="/add/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("Add DNS Record") ?>"><i class="fas fa-circle-plus icon-green icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing DNS Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+									<?php if($data[$key]['DNSSEC'] == "yes"){?><div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/list/dns/?domain=<?=htmlentities($key);?>&action=dnssec&token=<?=$_SESSION['token']?>" title="<?= _("View Public DNSSEC key") ?>"><i class="fas fa-key icon-orange icon-dim"></i></a></div>
 									<?php } ?>
 								<?php } ?>
-								<div class="actions-panel__col actions-panel__edit shortcut-l" key-action="href"><a href="/list/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("DNS records") ?>"><i class="fas fa-list icon-lightblue icon-dim"></i></a></div>
-								<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+								<div class="actions-panel__col actions-panel__edit shortcut-l" data-key-action="href"><a href="/list/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?= _("DNS records") ?>"><i class="fas fa-list icon-lightblue icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/<?=$spnd_action?>/dns/?domain=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>"
@@ -128,7 +128,7 @@
 										<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 									</a>
 								</div>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/dns/?domain=<?= htmlentities($key) ?>&token=<?= $_SESSION["token"] ?>"

+ 2 - 2
web/templates/pages/list_dns_rec.php

@@ -116,9 +116,9 @@
 							&nbsp;
 						<?php } else { ?>
 							<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-								<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/dns/?domain=<?=htmlspecialchars($_GET['domain'])?>&record_id=<?=$data[$key]['ID']?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing DNS Record") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/dns/?domain=<?=htmlspecialchars($_GET['domain'])?>&record_id=<?=$data[$key]['ID']?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing DNS Record") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 							<?php } ?>
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/delete/dns/?domain=<?= htmlspecialchars($_GET["domain"]) ?>&record_id=<?= $data[$key]["ID"] ?>&token=<?= $_SESSION["token"] ?>"

+ 3 - 3
web/templates/pages/list_firewall.php

@@ -101,8 +101,8 @@
 					<div class="clearfix l-unit__stat-col--left compact-2 u-text-right">
 						<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 							<div class="actions-panel clearfix" style="padding-right: 10px;">
-								<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/firewall/?rule=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Firewall Rule") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
-								<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+								<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/firewall/?rule=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Firewall Rule") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/<?=$spnd_action?>/firewall/?rule=<?=$key?>&token=<?=$_SESSION['token']?>"
@@ -112,7 +112,7 @@
 										<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 									</a>
 								</div>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/firewall/?rule=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 1 - 1
web/templates/pages/list_firewall_banlist.php

@@ -55,7 +55,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/delete/firewall/banlist/?ip=<?= $ip ?>&chain=<?= $value["CHAIN"] ?>&token=<?= $_SESSION["token"] ?>"

+ 1 - 1
web/templates/pages/list_firewall_ipset.php

@@ -53,7 +53,7 @@
 				<div class="clearfix l-unit__stat-col--left compact-4">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/delete/firewall/ipset/?listname=<?= $listname ?>&token=<?= $_SESSION["token"] ?>"

+ 2 - 2
web/templates/pages/list_ip.php

@@ -77,8 +77,8 @@
 				<div class="clearfix l-unit__stat-col--left compact u-text-right">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/ip/?ip=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing IP Address") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/ip/?ip=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing IP Address") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/delete/ip/?ip=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 1 - 1
web/templates/pages/list_key.php

@@ -42,7 +42,7 @@
 				<div class="clearfix l-unit__stat-col--left text-left compact-2">
 					<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									<?php if ($_SESSION["userContext"] === "admin" && isset($_GET["user"]) && $_GET["user"] !== "admin") { ?>

+ 1 - 1
web/templates/pages/list_log.php

@@ -48,7 +48,7 @@
 				<!-- Hide delete buttons-->
 			<?php } else { ?>
 				<?php if ($_SESSION["userContext"] === "admin" || ($_SESSION["userContext"] === "user" && $_SESSION["POLICY_USER_DELETE_LOGS"] !== "no")) { ?>
-					<div class="actions-panel" key-action="js">
+					<div class="actions-panel" data-key-action="js">
 						<a
 							class="button button-secondary button-danger data-controls js-confirm-action"
 							<?php if ($_SESSION["userContext"] === "admin" && isset($_GET["user"])) { ?>

+ 1 - 1
web/templates/pages/list_log_auth.php

@@ -18,7 +18,7 @@
 				<!-- Hide delete buttons-->
 			<?php } else { ?>
 				<?php if ($_SESSION["userContext"] === "admin" || ($_SESSION["userContext"] === "user" && $_SESSION["POLICY_USER_DELETE_LOGS"] !== "no")) { ?>
-					<div class="actions-panel" key-action="js">
+					<div class="actions-panel" data-key-action="js">
 						<a
 							class="button button-secondary button-danger data-controls js-confirm-action"
 							<?php if ($_SESSION["userContext"] === "admin" && isset($_GET["user"])) { ?>

+ 9 - 9
web/templates/pages/list_mail.php

@@ -147,23 +147,23 @@
 							<div class="actions-panel clearfix">
 								<?php if ($read_only === 'true') {?>
 									<!-- Restrict ability to edit, delete, or suspend domain items when impersonating 'admin' account -->
-									<div class="actions-panel__col actions-panel__edit shortcut-l" key-action="href"><a href="?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("mail accounts") ?>"><i class="fas fa-users icon-blue icon-dim"></i></a></div>
-									<div class="actions-panel__col actions-panel__edit shortcut-l" key-action="href"><a href="?domain=<?=$key?>&dns=1&token=<?=$_SESSION['token']?>" title="<?= _("DNS records mail") ?>"><i class="fas fa-book-atlas icon-blue icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__edit shortcut-l" data-key-action="href"><a href="?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("mail accounts") ?>"><i class="fas fa-users icon-blue icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__edit shortcut-l" data-key-action="href"><a href="?domain=<?=$key?>&dns=1&token=<?=$_SESSION['token']?>" title="<?= _("DNS records mail") ?>"><i class="fas fa-book-atlas icon-blue icon-dim"></i></a></div>
 									<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-										<div class="actions-panel__col actions-panel__edit" key-action="href"><a href="http://<?=$webmail;?>.<?=$key?>/" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-paper-plane icon-lightblue icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__edit" data-key-action="href"><a href="http://<?=$webmail;?>.<?=$key?>/" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-paper-plane icon-lightblue icon-dim"></i></a></div>
 									<?php } ?>
 								<?php } else { ?>
 									<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-										<div class="actions-panel__col actions-panel__logs shortcut-n" key-action="href"><a href="/add/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Add Mail Account") ?>"><i class="fas fa-circle-plus icon-green icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__logs shortcut-n" data-key-action="href"><a href="/add/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Add Mail Account") ?>"><i class="fas fa-circle-plus icon-green icon-dim"></i></a></div>
 										<?php if($_SESSION['WEBMAIL_SYSTEM']){?>
 											<?php if (!empty($data[$key]['WEBMAIL'])) {?>
-												<div class="actions-panel__col actions-panel__edit" key-action="href"><a href="http://<?=$webmail;?>.<?=$key?>/" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-paper-plane icon-lightblue icon-dim"></i></a></div>
+												<div class="actions-panel__col actions-panel__edit" data-key-action="href"><a href="http://<?=$webmail;?>.<?=$key?>/" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-paper-plane icon-lightblue icon-dim"></i></a></div>
 											<?php } ?>
 										<?php } ?>
-										<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Mail Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Mail Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 									<?php } ?>
-									<div class="actions-panel__col actions-panel__edit shortcut-l" key-action="href"><a href="?domain=<?=$key?>&dns=1&token=<?=$_SESSION['token']?>" title="<?= _("DNS records") ?>"><i class="fas fa-book-atlas icon-blue icon-dim"></i></a></div>
-									<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+									<div class="actions-panel__col actions-panel__edit shortcut-l" data-key-action="href"><a href="?domain=<?=$key?>&dns=1&token=<?=$_SESSION['token']?>" title="<?= _("DNS records") ?>"><i class="fas fa-book-atlas icon-blue icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/<?=$spnd_action?>/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>"
@@ -173,7 +173,7 @@
 											<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 										</a>
 									</div>
-									<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+									<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/delete/mail/?domain=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 5 - 5
web/templates/pages/list_mail_acc.php

@@ -152,18 +152,18 @@ if (!empty($_SESSION["WEBMAIL_ALIAS"])) {
 								<?php if ($data[$key]['SUSPENDED'] == 'yes') { ?>
 									&nbsp;
 								<?php } else { ?>
-									<div class="actions-panel__col actions-panel__edit" key-action="href"><a href="http://<?=$v_webmail_alias;?>.<?=htmlspecialchars($_GET['domain'])?>/?_user=<?=$key?>@<?=htmlspecialchars($_GET['domain'])?>" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-envelope-open-text icon-maroon icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__edit" data-key-action="href"><a href="http://<?=$v_webmail_alias;?>.<?=htmlspecialchars($_GET['domain'])?>/?_user=<?=$key?>@<?=htmlspecialchars($_GET['domain'])?>" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-envelope-open-text icon-maroon icon-dim"></i></a></div>
 								<?php } ?>
 							<?php } else { ?>
 								<?php if ($data[$key]['SUSPENDED'] == 'no') { ?>
 									<?php if($_SESSION['WEBMAIL_SYSTEM']){?>
 										<?php if (!empty($data[$key]['WEBMAIL'])) { ?>
-											<div class="actions-panel__col actions-panel__edit" key-action="href"><a href="http://<?=$v_webmail_alias;?>.<?=htmlspecialchars($_GET['domain'])?>/?_user=<?=$key?>@<?=htmlspecialchars($_GET['domain'])?>" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-envelope-open-text icon-maroon icon-dim"></i></a></div>
+											<div class="actions-panel__col actions-panel__edit" data-key-action="href"><a href="http://<?=$v_webmail_alias;?>.<?=htmlspecialchars($_GET['domain'])?>/?_user=<?=$key?>@<?=htmlspecialchars($_GET['domain'])?>" target="_blank" title="<?= _("open webmail") ?>"><i class="fas fa-envelope-open-text icon-maroon icon-dim"></i></a></div>
 										<?php } ?>
 									<?php } ?>
-								<div class="actions-panel__col actions-panel__logs shortcut-enter" key-action="href"><a href="/edit/mail/?domain=<?=htmlspecialchars($_GET['domain'])?>&account=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Mail Account") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__logs shortcut-enter" data-key-action="href"><a href="/edit/mail/?domain=<?=htmlspecialchars($_GET['domain'])?>&account=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Mail Account") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 								<?php } ?>
-								<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+								<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/<?=$spnd_action?>/mail/?domain=<?=htmlspecialchars($_GET['domain'])?>&account=<?=$key?>&token=<?=$_SESSION['token']?>"
@@ -173,7 +173,7 @@ if (!empty($_SESSION["WEBMAIL_ALIAS"])) {
 										<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 									</a>
 								</div>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/mail/?domain=<?=htmlspecialchars($_GET['domain'])?>&account=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 3 - 3
web/templates/pages/list_packages.php

@@ -87,13 +87,13 @@
 							<?php if (($key == 'system')) { ?>
 								<!-- Restrict editing system package -->
 							<?php } else {?>
-								<div class="actions-panel__col actions-panel__edit shortcut-enter" key-action="href"><a href="/edit/package/?package=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Package") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__edit shortcut-enter" data-key-action="href"><a href="/edit/package/?package=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Package") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 							<?php } ?>
-							<div class="actions-panel__col actions-panel__edit" key-action="href"><a href="/copy/package/?package=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Copy") ?>"><i class="fas fa-clone icon-teal icon-dim"></i></a></div>
+							<div class="actions-panel__col actions-panel__edit" data-key-action="href"><a href="/copy/package/?package=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Copy") ?>"><i class="fas fa-clone icon-teal icon-dim"></i></a></div>
 							<?php if ($key == 'system') { ?>
 								<!-- Restrict deleting system package -->
 							<?php } else {?>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/package/?package=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 4 - 4
web/templates/pages/list_services.php

@@ -19,7 +19,7 @@
 			<a href="/list/log/?user=system&token=<?= $_SESSION["token"] ?>" class="button button-secondary">
 				<i class="fas fa-binoculars icon-orange"></i><?= _("Logs") ?>
 			</a>
-			<div class="actions-panel" key-action="js">
+			<div class="actions-panel" data-key-action="js">
 				<a
 					class="button button-secondary button-danger data-controls js-confirm-action"
 					href="/restart/system/?hostname=<?= $sys["sysinfo"]["HOSTNAME"] ?>&token=<?= $_SESSION["token"] ?>&system_reset_token=<?= time() ?>"
@@ -145,10 +145,10 @@
 					</div>
 					<div class="clearfix l-unit__stat-col--left u-text-center compact-2">
 						<div class="actions-panel clearfix">
-							<div class="actions-panel__col actions-panel__edit shortcut-enter" key-action="href">
+							<div class="actions-panel__col actions-panel__edit shortcut-enter" data-key-action="href">
 								<a href="/edit/server/<? echo $edit_url ?>/" title="<?= _("edit") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a>
 							</div>
-							<div class="actions-panel__col actions-panel__stop shortcut-s" key-action="js">
+							<div class="actions-panel__col actions-panel__stop shortcut-s" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/restart/service/?srv=<?= $key ?>&token=<?= $_SESSION["token"] ?>"
@@ -158,7 +158,7 @@
 									<i class="fas fa-arrow-rotate-left icon-highlight icon-dim"></i>
 								</a>
 							</div>
-							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 								<a
 									class="data-controls js-confirm-action"
 									href="/<?=$action ?>/service/?srv=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 3 - 3
web/templates/pages/list_user.php

@@ -123,7 +123,7 @@
 								<!-- Hide edit button from admin user when logged in with another admin user -->
 								&nbsp;
 							<?php } else { ?>
-								<div class="actions-panel__col actions-panel__edit shortcut-enter" key-action="href"><a href="/edit/user/?user=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing User") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+								<div class="actions-panel__col actions-panel__edit shortcut-enter" data-key-action="href"><a href="/edit/user/?user=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing User") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 							<?php } ?>
 							<?php if ($key == "admin") { ?>
 								<!-- Hide suspend and delete buttons in the user list for primary 'admin' account -->
@@ -131,7 +131,7 @@
 								<?php if ($key == $user_plain) { ?>
 									<!-- Hide suspend and delete buttons in the user list for current user -->
 								<?php } else { ?>
-								<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+								<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/<?= $spnd_action ?>/user/?user=<?= $key ?>&token=<?= $_SESSION["token"] ?>"
@@ -141,7 +141,7 @@
 										<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 									</a>
 								</div>
-								<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 									<a
 										class="data-controls js-confirm-action"
 										href="/delete/user/?user=<?= $key ?>&token=<?= $_SESSION["token"] ?>"

+ 6 - 6
web/templates/pages/list_web.php

@@ -192,18 +192,18 @@
 						<div class="l-unit-toolbar__col l-unit-toolbar__col--right u-noselect">
 							<div class="actions-panel clearfix">
 								<?php if (!empty($data[$key]['STATS'])) { ?>
-									<div class="actions-panel__col actions-panel__logs shortcut-w" key-action="href"><a href="http://<?=$key?>/vstats/" rel="noopener" target="_blank" rel="noopener" title="<?= _("Statistics") ?>"><i class="fas fa-chart-bar icon-maroon icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__logs shortcut-w" data-key-action="href"><a href="http://<?=$key?>/vstats/" rel="noopener" target="_blank" rel="noopener" title="<?= _("Statistics") ?>"><i class="fas fa-chart-bar icon-maroon icon-dim"></i></a></div>
 								<?php } ?>
-									<div class="actions-panel__col actions-panel__view" key-action="href"><a href="http://<?=$key?>/" rel="noopener" target="_blank"><i class="fas fa-square-up-right icon-lightblue icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__view" data-key-action="href"><a href="http://<?=$key?>/" rel="noopener" target="_blank"><i class="fas fa-square-up-right icon-lightblue icon-dim"></i></a></div>
 								<?php if ($read_only === 'true') {?>
 									<!-- Restrict ability to edit, delete, or suspend web domains when impersonating the 'admin' account -->
 									&nbsp;
 								<?php } else { ?>
 									<?php if ($data[$key]['SUSPENDED'] == 'no') {?>
-										<div class="actions-panel__col actions-panel__edit shortcut-enter" key-action="href"><a href="/edit/web/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
+										<div class="actions-panel__col actions-panel__edit shortcut-enter" data-key-action="href"><a href="/edit/web/?domain=<?=$key?>&token=<?=$_SESSION['token']?>" title="<?= _("Editing Domain") ?>"><i class="fas fa-pencil icon-orange icon-dim"></i></a></div>
 									<?php } ?>
-									<div class="actions-panel__col actions-panel__logs shortcut-l" key-action="href"><a href="/list/web-log/?domain=<?=$key?>&type=access#" title="<?= _("AccessLog") ?>"><i class="fas fa-binoculars icon-purple icon-dim"></i></a></div>
-									<div class="actions-panel__col actions-panel__suspend shortcut-s" key-action="js">
+									<div class="actions-panel__col actions-panel__logs shortcut-l" data-key-action="href"><a href="/list/web-log/?domain=<?=$key?>&type=access#" title="<?= _("AccessLog") ?>"><i class="fas fa-binoculars icon-purple icon-dim"></i></a></div>
+									<div class="actions-panel__col actions-panel__suspend shortcut-s" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/<?=$spnd_action?>/web/?domain=<?=$key?>&token=<?=$_SESSION['token']?>"
@@ -213,7 +213,7 @@
 											<i class="fas <?= $spnd_icon ?> icon-highlight icon-dim"></i>
 										</a>
 									</div>
-									<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+									<div class="actions-panel__col actions-panel__delete shortcut-delete" data-key-action="js">
 										<a
 											class="data-controls js-confirm-action"
 											href="/delete/web/?domain=<?=$key?>&token=<?=$_SESSION['token']?>"

+ 1 - 1
web/templates/pages/setup_webapp.php

@@ -69,7 +69,7 @@
 									<?php if ($field_type == "password") { ?>
 										/
 										<button
-											x-on:click="value = randomString()"
+											x-on:click="value = VE.helpers.randomPassword()"
 											class="form-link"
 											type="button"
 										>

+ 184 - 174
yarn.lock

@@ -336,9 +336,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-gradients-interpolation-method@npm:^3.0.3":
-  version: 3.0.3
-  resolution: "@csstools/postcss-gradients-interpolation-method@npm:3.0.3"
+"@csstools/postcss-gradients-interpolation-method@npm:^3.0.4":
+  version: 3.0.4
+  resolution: "@csstools/postcss-gradients-interpolation-method@npm:3.0.4"
   dependencies:
     "@csstools/css-color-parser": ^1.1.2
     "@csstools/css-parser-algorithms": ^2.1.1
@@ -346,7 +346,7 @@ __metadata:
     "@csstools/postcss-progressive-custom-properties": ^2.0.0
   peerDependencies:
     postcss: ^8.4
-  checksum: ee01775a94ec6e764388f25a554af5e64aab1fc3d2dccf421c2e9b90349a6e0cf6c927ff612467de9a834fa67e59aa1516dadd145bd309aa2fc24231c5b01b2a
+  checksum: 4a69c12d12b9ada23af55cb54a331d04e01c28e52f5f111e9229000b35c52d90515f90cdd854f896503b7d56e5130d67ca96bfd81f1491f5784eca1011a95c4c
   languageName: node
   linkType: hard
 
@@ -599,156 +599,156 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@esbuild/android-arm64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/android-arm64@npm:0.17.16"
+"@esbuild/android-arm64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/android-arm64@npm:0.17.17"
   conditions: os=android & cpu=arm64
   languageName: node
   linkType: hard
 
-"@esbuild/android-arm@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/android-arm@npm:0.17.16"
+"@esbuild/android-arm@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/android-arm@npm:0.17.17"
   conditions: os=android & cpu=arm
   languageName: node
   linkType: hard
 
-"@esbuild/android-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/android-x64@npm:0.17.16"
+"@esbuild/android-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/android-x64@npm:0.17.17"
   conditions: os=android & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/darwin-arm64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/darwin-arm64@npm:0.17.16"
+"@esbuild/darwin-arm64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/darwin-arm64@npm:0.17.17"
   conditions: os=darwin & cpu=arm64
   languageName: node
   linkType: hard
 
-"@esbuild/darwin-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/darwin-x64@npm:0.17.16"
+"@esbuild/darwin-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/darwin-x64@npm:0.17.17"
   conditions: os=darwin & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/freebsd-arm64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/freebsd-arm64@npm:0.17.16"
+"@esbuild/freebsd-arm64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/freebsd-arm64@npm:0.17.17"
   conditions: os=freebsd & cpu=arm64
   languageName: node
   linkType: hard
 
-"@esbuild/freebsd-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/freebsd-x64@npm:0.17.16"
+"@esbuild/freebsd-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/freebsd-x64@npm:0.17.17"
   conditions: os=freebsd & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/linux-arm64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-arm64@npm:0.17.16"
+"@esbuild/linux-arm64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-arm64@npm:0.17.17"
   conditions: os=linux & cpu=arm64
   languageName: node
   linkType: hard
 
-"@esbuild/linux-arm@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-arm@npm:0.17.16"
+"@esbuild/linux-arm@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-arm@npm:0.17.17"
   conditions: os=linux & cpu=arm
   languageName: node
   linkType: hard
 
-"@esbuild/linux-ia32@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-ia32@npm:0.17.16"
+"@esbuild/linux-ia32@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-ia32@npm:0.17.17"
   conditions: os=linux & cpu=ia32
   languageName: node
   linkType: hard
 
-"@esbuild/linux-loong64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-loong64@npm:0.17.16"
+"@esbuild/linux-loong64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-loong64@npm:0.17.17"
   conditions: os=linux & cpu=loong64
   languageName: node
   linkType: hard
 
-"@esbuild/linux-mips64el@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-mips64el@npm:0.17.16"
+"@esbuild/linux-mips64el@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-mips64el@npm:0.17.17"
   conditions: os=linux & cpu=mips64el
   languageName: node
   linkType: hard
 
-"@esbuild/linux-ppc64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-ppc64@npm:0.17.16"
+"@esbuild/linux-ppc64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-ppc64@npm:0.17.17"
   conditions: os=linux & cpu=ppc64
   languageName: node
   linkType: hard
 
-"@esbuild/linux-riscv64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-riscv64@npm:0.17.16"
+"@esbuild/linux-riscv64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-riscv64@npm:0.17.17"
   conditions: os=linux & cpu=riscv64
   languageName: node
   linkType: hard
 
-"@esbuild/linux-s390x@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-s390x@npm:0.17.16"
+"@esbuild/linux-s390x@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-s390x@npm:0.17.17"
   conditions: os=linux & cpu=s390x
   languageName: node
   linkType: hard
 
-"@esbuild/linux-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/linux-x64@npm:0.17.16"
+"@esbuild/linux-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/linux-x64@npm:0.17.17"
   conditions: os=linux & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/netbsd-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/netbsd-x64@npm:0.17.16"
+"@esbuild/netbsd-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/netbsd-x64@npm:0.17.17"
   conditions: os=netbsd & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/openbsd-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/openbsd-x64@npm:0.17.16"
+"@esbuild/openbsd-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/openbsd-x64@npm:0.17.17"
   conditions: os=openbsd & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/sunos-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/sunos-x64@npm:0.17.16"
+"@esbuild/sunos-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/sunos-x64@npm:0.17.17"
   conditions: os=sunos & cpu=x64
   languageName: node
   linkType: hard
 
-"@esbuild/win32-arm64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/win32-arm64@npm:0.17.16"
+"@esbuild/win32-arm64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/win32-arm64@npm:0.17.17"
   conditions: os=win32 & cpu=arm64
   languageName: node
   linkType: hard
 
-"@esbuild/win32-ia32@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/win32-ia32@npm:0.17.16"
+"@esbuild/win32-ia32@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/win32-ia32@npm:0.17.17"
   conditions: os=win32 & cpu=ia32
   languageName: node
   linkType: hard
 
-"@esbuild/win32-x64@npm:0.17.16":
-  version: 0.17.16
-  resolution: "@esbuild/win32-x64@npm:0.17.16"
+"@esbuild/win32-x64@npm:0.17.17":
+  version: 0.17.17
+  resolution: "@esbuild/win32-x64@npm:0.17.17"
   conditions: os=win32 & cpu=x64
   languageName: node
   linkType: hard
@@ -957,14 +957,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/eslint-plugin@npm:^5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/eslint-plugin@npm:5.58.0"
+"@typescript-eslint/eslint-plugin@npm:^5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/eslint-plugin@npm:5.59.0"
   dependencies:
     "@eslint-community/regexpp": ^4.4.0
-    "@typescript-eslint/scope-manager": 5.58.0
-    "@typescript-eslint/type-utils": 5.58.0
-    "@typescript-eslint/utils": 5.58.0
+    "@typescript-eslint/scope-manager": 5.59.0
+    "@typescript-eslint/type-utils": 5.59.0
+    "@typescript-eslint/utils": 5.59.0
     debug: ^4.3.4
     grapheme-splitter: ^1.0.4
     ignore: ^5.2.0
@@ -977,43 +977,43 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: e5d76d43c466ebd4b552e3307eff72ab5ae8a0c09a1d35fa13b62769ac3336df94d9281728ab5aafd2c14a0a644133583edcd708fce60a9a82df1db3ca3b8e14
+  checksum: 3b2582fe7baa9bf7733be79c6e35a390806f91c8d5ba5b604f71cb3635fb36abc975b926195c3ef5c6a4018bb94f66e009d727e3af2ce8b92c96aa3ee9ed194a
   languageName: node
   linkType: hard
 
-"@typescript-eslint/parser@npm:^5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/parser@npm:5.58.0"
+"@typescript-eslint/parser@npm:^5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/parser@npm:5.59.0"
   dependencies:
-    "@typescript-eslint/scope-manager": 5.58.0
-    "@typescript-eslint/types": 5.58.0
-    "@typescript-eslint/typescript-estree": 5.58.0
+    "@typescript-eslint/scope-manager": 5.59.0
+    "@typescript-eslint/types": 5.59.0
+    "@typescript-eslint/typescript-estree": 5.59.0
     debug: ^4.3.4
   peerDependencies:
     eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 38681da48a40132c0538579c818ceef9ba2793ab8f79236c3f64980ba1649bb87cb367cd79d37bf2982b8bfbc28f91846b8676f9bd333e8b691c9befffd8874a
+  checksum: 1a442d6b776fc1dca4fe104bac77eae0a59b807ba11cef00dec8f5dbbc0fb4e5fc10519eac03dd94d52e4dd6d814800d0e5c0a3bd43eefce80d829c65ba47ad0
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/scope-manager@npm:5.58.0"
+"@typescript-eslint/scope-manager@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/scope-manager@npm:5.59.0"
   dependencies:
-    "@typescript-eslint/types": 5.58.0
-    "@typescript-eslint/visitor-keys": 5.58.0
-  checksum: f0d3df5cc3c461fe63ef89ad886b53c239cc7c1d9061d83d8a9d9c8e087e5501eac84bebff8a954728c17ccea191f235686373d54d2b8b6370af2bcf2b18e062
+    "@typescript-eslint/types": 5.59.0
+    "@typescript-eslint/visitor-keys": 5.59.0
+  checksum: dd89cd34291f7674edcbe9628748faa61dbf7199f9776586167e81fd91b93ba3a7f0ddd493c559c0dbb805b58629858fae648d56550e8ac5330b2ed1802b0178
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/type-utils@npm:5.58.0"
+"@typescript-eslint/type-utils@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/type-utils@npm:5.59.0"
   dependencies:
-    "@typescript-eslint/typescript-estree": 5.58.0
-    "@typescript-eslint/utils": 5.58.0
+    "@typescript-eslint/typescript-estree": 5.59.0
+    "@typescript-eslint/utils": 5.59.0
     debug: ^4.3.4
     tsutils: ^3.21.0
   peerDependencies:
@@ -1021,23 +1021,23 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 803f24daed185152bf86952d4acebb5ea18ff03db5f28750368edf76fdea46b4b0f8803ae0b61c0282b47181c9977113457b16e33d5d2cb33b13855f55c5e5b2
+  checksum: 811981ea117808315fe37ce8489ae6e20979f588cf0fdef2bd969d58c505ececff0bccf7957f3b178933028433ce28764ebc9fea32a35a4c2da81b5b1e98b454
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/types@npm:5.58.0"
-  checksum: 8622a73d73220c4a7111537825f488c0271272032a1d4e129dc722bc6e8b3ec84f64469b2ca3b8dae7da3a9c18953ce1449af51f5f757dad60835eb579ad1d2c
+"@typescript-eslint/types@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/types@npm:5.59.0"
+  checksum: 5dc608a867b07b4262a236a264a65e894f841388b3aba461c4c1a30d76a2c3aed0c6a1e3d1ea2f64cce55e783091bafb826bf01a0ef83258820af63da860addf
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/typescript-estree@npm:5.58.0"
+"@typescript-eslint/typescript-estree@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/typescript-estree@npm:5.59.0"
   dependencies:
-    "@typescript-eslint/types": 5.58.0
-    "@typescript-eslint/visitor-keys": 5.58.0
+    "@typescript-eslint/types": 5.59.0
+    "@typescript-eslint/visitor-keys": 5.59.0
     debug: ^4.3.4
     globby: ^11.1.0
     is-glob: ^4.0.3
@@ -1046,35 +1046,35 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 51b668ec858db0c040a71dff526273945cee4ba5a9b240528d503d02526685882d900cf071c6636a4d9061ed3fd4a7274f7f1a23fba55c4b48b143344b4009c7
+  checksum: d80f2766e2830dc830b9f4f1b9e744e1e7a285ebe72babdf0970f75bfe26cb832c6623bb836a53c48f1e707069d1e407ac1ea095bd583807007f713ba6e2e0e1
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/utils@npm:5.58.0"
+"@typescript-eslint/utils@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/utils@npm:5.59.0"
   dependencies:
     "@eslint-community/eslint-utils": ^4.2.0
     "@types/json-schema": ^7.0.9
     "@types/semver": ^7.3.12
-    "@typescript-eslint/scope-manager": 5.58.0
-    "@typescript-eslint/types": 5.58.0
-    "@typescript-eslint/typescript-estree": 5.58.0
+    "@typescript-eslint/scope-manager": 5.59.0
+    "@typescript-eslint/types": 5.59.0
+    "@typescript-eslint/typescript-estree": 5.59.0
     eslint-scope: ^5.1.1
     semver: ^7.3.7
   peerDependencies:
     eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
-  checksum: c618ae67963ecf96b1492c09afaeb363f542f0d6780bcac4af3c26034e3b20034666b2d523aa94821df813aafb57a0b150a7d5c2224fe8257452ad1de2237a58
+  checksum: 228318df02f2381f859af184cafa5de4146a2e1518a5062444bf9bd7d468e058f9bd93a3e46cc4683d9bd02159648f416e5c7c539901ca16142456cae3c1af5f
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:5.58.0":
-  version: 5.58.0
-  resolution: "@typescript-eslint/visitor-keys@npm:5.58.0"
+"@typescript-eslint/visitor-keys@npm:5.59.0":
+  version: 5.59.0
+  resolution: "@typescript-eslint/visitor-keys@npm:5.59.0"
   dependencies:
-    "@typescript-eslint/types": 5.58.0
+    "@typescript-eslint/types": 5.59.0
     eslint-visitor-keys: ^3.3.0
-  checksum: ab2d1f37660559954c840429ef78bbf71834063557e3e68e435005b4987970b9356fdf217ead53f7a57f66f5488dc478062c5c44bf17053a8bf041733539b98f
+  checksum: e21656de02e221a27a5fe9f7fd34a1ca28530e47675134425f84fd0d1f276695fe39e35120837a491b02255d49aa2fd871e2c858ecccc66c687db972d057bd1c
   languageName: node
   linkType: hard
 
@@ -1595,9 +1595,9 @@ __metadata:
   linkType: hard
 
 "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001449, caniuse-lite@npm:^1.0.30001464":
-  version: 1.0.30001478
-  resolution: "caniuse-lite@npm:1.0.30001478"
-  checksum: 27a370dcb32a6a35e186307aabc570da1cd0fccc849913665e7df6822a87286de99509b163304e0586c23c539a991717fb68ed84b85bbd21b2cb86475ae5ffb2
+  version: 1.0.30001480
+  resolution: "caniuse-lite@npm:1.0.30001480"
+  checksum: c0b40f02f45ee99c73f732a3118028b2ab1544962d473d84f2afcb898a5e3099bd4c45f316ebc466fb1dbda904e86b72695578ca531a0bfa9d6337e7aad1ee2a
   languageName: node
   linkType: hard
 
@@ -2108,9 +2108,9 @@ __metadata:
   linkType: hard
 
 "electron-to-chromium@npm:^1.4.284":
-  version: 1.4.365
-  resolution: "electron-to-chromium@npm:1.4.365"
-  checksum: 900ac6afc3550f047d44278e1e415f04adb68003517287907e058c1ff066f5ab48a460339473d268b1e1dd006c7b7bb6100aa5d5f24bb823ad865fdaec14d793
+  version: 1.4.367
+  resolution: "electron-to-chromium@npm:1.4.367"
+  checksum: 721e4958945a16ff21a63896d4e112da7b8f3fd5540a27280d5777c196fe4e1f1b51359f86c6fe83a267e81f127e4753ec9171547f094f86481a421c809473b0
   languageName: node
   linkType: hard
 
@@ -2174,32 +2174,32 @@ __metadata:
   languageName: node
   linkType: hard
 
-"esbuild@npm:^0.17.16, esbuild@npm:^0.17.5":
-  version: 0.17.16
-  resolution: "esbuild@npm:0.17.16"
-  dependencies:
-    "@esbuild/android-arm": 0.17.16
-    "@esbuild/android-arm64": 0.17.16
-    "@esbuild/android-x64": 0.17.16
-    "@esbuild/darwin-arm64": 0.17.16
-    "@esbuild/darwin-x64": 0.17.16
-    "@esbuild/freebsd-arm64": 0.17.16
-    "@esbuild/freebsd-x64": 0.17.16
-    "@esbuild/linux-arm": 0.17.16
-    "@esbuild/linux-arm64": 0.17.16
-    "@esbuild/linux-ia32": 0.17.16
-    "@esbuild/linux-loong64": 0.17.16
-    "@esbuild/linux-mips64el": 0.17.16
-    "@esbuild/linux-ppc64": 0.17.16
-    "@esbuild/linux-riscv64": 0.17.16
-    "@esbuild/linux-s390x": 0.17.16
-    "@esbuild/linux-x64": 0.17.16
-    "@esbuild/netbsd-x64": 0.17.16
-    "@esbuild/openbsd-x64": 0.17.16
-    "@esbuild/sunos-x64": 0.17.16
-    "@esbuild/win32-arm64": 0.17.16
-    "@esbuild/win32-ia32": 0.17.16
-    "@esbuild/win32-x64": 0.17.16
+"esbuild@npm:^0.17.17, esbuild@npm:^0.17.5":
+  version: 0.17.17
+  resolution: "esbuild@npm:0.17.17"
+  dependencies:
+    "@esbuild/android-arm": 0.17.17
+    "@esbuild/android-arm64": 0.17.17
+    "@esbuild/android-x64": 0.17.17
+    "@esbuild/darwin-arm64": 0.17.17
+    "@esbuild/darwin-x64": 0.17.17
+    "@esbuild/freebsd-arm64": 0.17.17
+    "@esbuild/freebsd-x64": 0.17.17
+    "@esbuild/linux-arm": 0.17.17
+    "@esbuild/linux-arm64": 0.17.17
+    "@esbuild/linux-ia32": 0.17.17
+    "@esbuild/linux-loong64": 0.17.17
+    "@esbuild/linux-mips64el": 0.17.17
+    "@esbuild/linux-ppc64": 0.17.17
+    "@esbuild/linux-riscv64": 0.17.17
+    "@esbuild/linux-s390x": 0.17.17
+    "@esbuild/linux-x64": 0.17.17
+    "@esbuild/netbsd-x64": 0.17.17
+    "@esbuild/openbsd-x64": 0.17.17
+    "@esbuild/sunos-x64": 0.17.17
+    "@esbuild/win32-arm64": 0.17.17
+    "@esbuild/win32-ia32": 0.17.17
+    "@esbuild/win32-x64": 0.17.17
   dependenciesMeta:
     "@esbuild/android-arm":
       optional: true
@@ -2247,7 +2247,7 @@ __metadata:
       optional: true
   bin:
     esbuild: bin/esbuild
-  checksum: c9787d8e05b9c4f762761be31a7847b5b4492b9b997808b7098479fef9a3260f1b8ca01e9b38376b6698f4394bfe088acb4f797a697b45b965cd664e103aafa7
+  checksum: dbb803a7fc798755ffcc347fd4e83f33bdffb91b62ff14c41d858acacd60b2b74a9fbcfb54da2be7cc385bd99fc00f5a0cc1e80c7e5d501236f4fd39cf8c03d1
   languageName: node
   linkType: hard
 
@@ -2801,21 +2801,22 @@ __metadata:
   dependencies:
     "@fortawesome/fontawesome-free": ^6.4.0
     "@prettier/plugin-php": ^0.19.4
-    "@typescript-eslint/eslint-plugin": ^5.58.0
-    "@typescript-eslint/parser": ^5.58.0
+    "@typescript-eslint/eslint-plugin": ^5.59.0
+    "@typescript-eslint/parser": ^5.59.0
     cssnano: ^6.0.0
-    esbuild: ^0.17.16
+    esbuild: ^0.17.17
     eslint: ^8.38.0
     eslint-config-prettier: ^8.8.0
     eslint-plugin-editorconfig: ^4.0.2
     husky: ^8.0.3
     lint-staged: ^13.2.1
     markdownlint-cli2: ^0.6.0
+    nanoid: ^4.0.2
     normalize.css: ^8.0.1
     postcss: ^8.4.22
     postcss-import: ^15.1.0
     postcss-path-replace: ^1.0.4
-    postcss-preset-env: ^8.3.1
+    postcss-preset-env: ^8.3.2
     postcss-size: ^4.0.1
     prettier: ^2.8.7
     prettier-plugin-nginx: ^1.0.3
@@ -2824,7 +2825,7 @@ __metadata:
     stylelint: ^15.5.0
     stylelint-config-standard: ^33.0.0
     typescript: ^5.0.4
-    vitepress: 1.0.0-alpha.70
+    vitepress: 1.0.0-alpha.72
     vue: ^3.2.47
   languageName: unknown
   linkType: soft
@@ -3735,6 +3736,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nanoid@npm:^4.0.2":
+  version: 4.0.2
+  resolution: "nanoid@npm:4.0.2"
+  bin:
+    nanoid: bin/nanoid.js
+  checksum: 747c399cea4664dd0be1d0ec498ffd1ef8f1f5221676fc8b577e3f46f66d9afcddb9595d63d19a2e78d0bc6cc33984f65e66bf1682c850b9e26288883d96b53f
+  languageName: node
+  linkType: hard
+
 "natural-compare-lite@npm:^1.4.0":
   version: 1.4.0
   resolution: "natural-compare-lite@npm:1.4.0"
@@ -4657,15 +4667,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-preset-env@npm:^8.3.1":
-  version: 8.3.1
-  resolution: "postcss-preset-env@npm:8.3.1"
+"postcss-preset-env@npm:^8.3.2":
+  version: 8.3.2
+  resolution: "postcss-preset-env@npm:8.3.2"
   dependencies:
     "@csstools/postcss-cascade-layers": ^3.0.1
     "@csstools/postcss-color-function": ^2.2.1
     "@csstools/postcss-color-mix-function": ^1.0.1
     "@csstools/postcss-font-format-keywords": ^2.0.2
-    "@csstools/postcss-gradients-interpolation-method": ^3.0.3
+    "@csstools/postcss-gradients-interpolation-method": ^3.0.4
     "@csstools/postcss-hwb-function": ^2.2.1
     "@csstools/postcss-ic-unit": ^2.0.2
     "@csstools/postcss-is-pseudo-class": ^3.2.0
@@ -4718,7 +4728,7 @@ __metadata:
     postcss-value-parser: ^4.2.0
   peerDependencies:
     postcss: ^8.4
-  checksum: 8b91a9e83e872d987f6fb28d46de1c1d135dde3ee197d4d203a6e9175362da5b2e2f0360e910b0366bf484bc29c07d024f422709860323f2c18c9bbc298b8a62
+  checksum: d0ebfcc4f06a0f6ec2bdfbd2d19388e65c6eb0922e5b4638e32af013c84c955ad7b2702c24987b599ccc287af814f5fbd80df0c1ecff37e8c3e311e7c4c0785c
   languageName: node
   linkType: hard
 
@@ -5131,8 +5141,8 @@ __metadata:
   linkType: hard
 
 "rollup@npm:^3.18.0":
-  version: 3.20.3
-  resolution: "rollup@npm:3.20.3"
+  version: 3.20.6
+  resolution: "rollup@npm:3.20.6"
   dependencies:
     fsevents: ~2.3.2
   dependenciesMeta:
@@ -5140,7 +5150,7 @@ __metadata:
       optional: true
   bin:
     rollup: dist/bin/rollup
-  checksum: 90ab2e099535246bab6ddffb566f3b073073d82006ad5a52bf8687eb50c3b540879aae40693b8ffdc86d9bf6a083badd583677a227954687d5dde60661fe7d11
+  checksum: fa30f1e1d214b8c62e631d3c181a75d61bc9c20fca38220d6f938bb3bf734a874e407cd641c90f550dc2b127df5029dfb3108be08934a654f1f40b50f368b0c2
   languageName: node
   linkType: hard
 
@@ -5186,13 +5196,13 @@ __metadata:
   linkType: hard
 
 "semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7":
-  version: 7.4.0
-  resolution: "semver@npm:7.4.0"
+  version: 7.5.0
+  resolution: "semver@npm:7.5.0"
   dependencies:
     lru-cache: ^6.0.0
   bin:
     semver: bin/semver.js
-  checksum: debf7f4d6fa36fdc5ef82bd7fc3603b6412165c8a3963a30be0c45a587be1a49e7681e80aa109da1875765741af24edc6e021cee1ba16ae96f649d06c5df296d
+  checksum: 2d266937756689a76f124ffb4c1ea3e1bbb2b263219f90ada8a11aebebe1280b13bb76cca2ca96bdee3dbc554cbc0b24752eb895b2a51577aa644427e9229f2b
   languageName: node
   linkType: hard
 
@@ -5885,8 +5895,8 @@ __metadata:
   linkType: hard
 
 "vite@npm:^4.2.1":
-  version: 4.2.1
-  resolution: "vite@npm:4.2.1"
+  version: 4.2.2
+  resolution: "vite@npm:4.2.2"
   dependencies:
     esbuild: ^0.17.5
     fsevents: ~2.3.2
@@ -5918,13 +5928,13 @@ __metadata:
       optional: true
   bin:
     vite: bin/vite.js
-  checksum: 70eb162ffc299017a3c310e3adc95e9661def6b17aafd1f8e5e02e516766060435590dbe3df1e4e95acc3583c728a76e91f07c546221d1e701f1b2b021293f45
+  checksum: 7fff9d046f6091c02e030aa5f45e68939b9bec1dd15d4e2c3c084d82ec185300295f3db26f537daf2e19f9ad191be260bf70e5fe0e2d9054f174a7ad457623f8
   languageName: node
   linkType: hard
 
-"vitepress@npm:1.0.0-alpha.70":
-  version: 1.0.0-alpha.70
-  resolution: "vitepress@npm:1.0.0-alpha.70"
+"vitepress@npm:1.0.0-alpha.72":
+  version: 1.0.0-alpha.72
+  resolution: "vitepress@npm:1.0.0-alpha.72"
   dependencies:
     "@docsearch/css": ^3.3.3
     "@docsearch/js": ^3.3.3
@@ -5939,7 +5949,7 @@ __metadata:
     vue: ^3.2.47
   bin:
     vitepress: bin/vitepress.js
-  checksum: 1ee9816b5522b4eb1a7693fc0b6c0343d85f1ae46c8ecb1e2cabbb534b5fce5fb836f5a92b49b94c4684c3aff17a85d70e8fae63b9ea3d9e33fcf244a91e4a9c
+  checksum: d941b3e638fb7404eea57a461c3b5355d7840066c342f38c2faaf23b368391305561ffb579f22c3a205741ca043c1155f4a65992c24ce71a32fab3a90f3e024e
   languageName: node
   linkType: hard
 

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