862 lines
		
	
	
		
			No EOL
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			862 lines
		
	
	
		
			No EOL
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| let Utils = {};
 | |
| 
 | |
| let locale;
 | |
| let format;
 | |
| Utils.translate = function (key) {
 | |
| 	if (!Lang.hasOwnProperty(locale)) {
 | |
| 		console.warn(`Language '${locale}' is not available. Using default 'en'.`);
 | |
| 		locale = "en";
 | |
| 	}
 | |
| 
 | |
| 	let langObj = Lang[locale];
 | |
| 	const keys = key.split(".");
 | |
| 
 | |
| 	for (const k of keys) {
 | |
| 		if (!langObj.hasOwnProperty(k)) {
 | |
| 			console.warn(`Translation key '${key}' not found for language '${locale}'.`);
 | |
| 			return "missing_translation";
 | |
| 		}
 | |
| 		langObj = langObj[k];
 | |
| 	}
 | |
| 
 | |
| 	return langObj;
 | |
| };
 | |
| 
 | |
| Utils.setLocale = function (current_locale) {
 | |
| 	locale = current_locale;
 | |
| };
 | |
| 
 | |
| Utils.setFormat = function (current_format) {
 | |
| 	format = current_format;
 | |
| };
 | |
| 
 | |
| Utils.loadLanguageFile = async function () {
 | |
| 	try {
 | |
| 		await new Promise((resolve, reject) => {
 | |
| 			let fileUrl = `lang/${locale}.js`;
 | |
| 			const script = document.createElement("script");
 | |
| 			script.src = fileUrl;
 | |
| 
 | |
| 			script.onload = () => {
 | |
| 				resolve();
 | |
| 			};
 | |
| 
 | |
| 			script.onerror = (event) => {
 | |
| 				reject(new Error("Failed to load language file: " + fileUrl));
 | |
| 			};
 | |
| 
 | |
| 			document.body.appendChild(script);
 | |
| 
 | |
| 			const timeoutDuration = 10000; // 10 seconds
 | |
| 			setTimeout(() => {
 | |
| 				reject(new Error("Timeout: The script took too long to load the language file: " + fileUrl));
 | |
| 			}, timeoutDuration);
 | |
| 		});
 | |
| 	} catch (error) {
 | |
| 		if (locale !== "en") {
 | |
| 			console.warn(`Language '${locale}' is not available. Using default 'en'.`);
 | |
| 			Utils.setLocale("en");
 | |
| 			await Utils.loadLanguageFile();
 | |
| 		} else {
 | |
| 			throw error;
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| Utils.loadLanguageModules = async function (utils_module) {
 | |
| 	Utils.setLocale(utils_module.config.locale);
 | |
| 	Utils.setFormat(utils_module.config.format);
 | |
| 	await Utils.loadLanguageFile();
 | |
| 	Utils.deepMerge(Lang,utils_module.lang);
 | |
| };
 | |
| 
 | |
| Utils.timeConverter = function (UNIX_timestamp, options = {}) {
 | |
| 	const timestampMillis = UNIX_timestamp * 1000;
 | |
| 	const formattedTime = new Date(timestampMillis).toLocaleString(locale, options);
 | |
| 
 | |
| 	return formattedTime;
 | |
| };
 | |
| 
 | |
| Utils.currencyFormat = function (number, decimalPlaces = null) {
 | |
| 	const options = {
 | |
| 		style: "currency",
 | |
| 		currency: format.currency,
 | |
| 	};
 | |
| 
 | |
| 	if (decimalPlaces != null) {
 | |
| 		options.minimumFractionDigits = decimalPlaces;
 | |
| 		options.maximumFractionDigits = decimalPlaces;
 | |
| 	}
 | |
| 
 | |
| 	return new Intl.NumberFormat(format.location, options).format(number);
 | |
| };
 | |
| 
 | |
| Utils.numberFormat = function (number, decimalPlaces = null) {
 | |
| 	const options = {};
 | |
| 
 | |
| 	if (decimalPlaces != null) {
 | |
| 		options.minimumFractionDigits = decimalPlaces;
 | |
| 		options.maximumFractionDigits = decimalPlaces;
 | |
| 	}
 | |
| 
 | |
| 	return new Intl.NumberFormat(format.location, options).format(number);
 | |
| };
 | |
| 
 | |
| Utils.getCurrencySymbol = function () {
 | |
| 	const options = {
 | |
| 		style: "currency",
 | |
| 		currency: format.currency,
 | |
| 		minimumFractionDigits: 0,
 | |
| 		maximumFractionDigits: 0,
 | |
| 	};
 | |
| 
 | |
| 	return new Intl.NumberFormat(locale, options).format(0).replace(/\d/g, "").trim();
 | |
| };
 | |
| 
 | |
| const requestQueue = [];
 | |
| let isProcessing = false;
 | |
| 
 | |
| const processQueue = async () => {
 | |
| 	if (!isProcessing && requestQueue.length > 0) {
 | |
| 		isProcessing = true;
 | |
| 		const { event, data, route, cb } = requestQueue.shift();
 | |
| 		try {
 | |
| 			const response = await fetch(Utils.getRoute(route), {
 | |
| 				method: "POST",
 | |
| 				headers: {
 | |
| 					"Content-Type": "application/json",
 | |
| 				},
 | |
| 				body: JSON.stringify({ event, data }),
 | |
| 			});
 | |
| 
 | |
| 			if (!response.ok) {
 | |
| 				throw new Error(`Request failed with status: ${response.status}`);
 | |
| 			}
 | |
| 
 | |
| 			const responseData = await response.json();
 | |
| 
 | |
| 			if (cb) {
 | |
| 				cb(responseData);
 | |
| 			} else {
 | |
| 				if (responseData !== 200) {
 | |
| 					console.log(responseData);
 | |
| 				}
 | |
| 			}
 | |
| 		} catch (error) {
 | |
| 			console.error(`Error occurred while making a POST request with event: "${event}", data: "${JSON.stringify(data)}", and route: "${Utils.getRoute(route)}": ${error.message}`);
 | |
| 		} finally {
 | |
| 			isProcessing = false;
 | |
| 			setTimeout(function() {
 | |
| 				processQueue();
 | |
| 			}, 200);
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| Utils.post = function (event, data, route = "post", cb) {
 | |
| 	requestQueue.push({ event, data, route, cb });
 | |
| 	processQueue();
 | |
| };
 | |
| 
 | |
| let resource_name;
 | |
| Utils.getRoute = function (name) {
 | |
| 	return `https://${resource_name}/${name}`;
 | |
| };
 | |
| 
 | |
| Utils.setResourceName = function (current_resource_name) {
 | |
| 	resource_name = current_resource_name;
 | |
| };
 | |
| 
 | |
| const modalTemplate = `
 | |
| 	<div id="confirmation-modal" class="modal fade">
 | |
| 		<div class="modal-dialog modal-dialog-centered">
 | |
| 			<div class="modal-content">
 | |
| 				<div class="modal-header">
 | |
| 					<h5 class="modal-title"></h5>
 | |
| 					<button type="button" class="close" data-dismiss="modal" aria-label="Close">
 | |
| 						<span aria-hidden="true">×</span>
 | |
| 					</button>
 | |
| 				</div>
 | |
| 				<form id="form-confirmation-modal" style="margin: 0;">
 | |
| 					<div class="modal-body">
 | |
| 						
 | |
| 					</div>
 | |
| 					<div class="modal-footer">
 | |
| 						
 | |
| 					</div>
 | |
| 				</form>
 | |
| 			</div>
 | |
| 		</div>
 | |
| 	</div>
 | |
| `;
 | |
| 
 | |
| Utils.showDefaultModal = function (action, body = Utils.translate("confirmation_modal_body")) {
 | |
| 	Utils.showCustomModal({
 | |
| 		title: Utils.translate("confirmation_modal_title"),
 | |
| 		body,
 | |
| 		buttons: [
 | |
| 			{ text: Utils.translate("confirmation_modal_cancel_button"), class: "btn btn-outline-primary", dismiss: true },
 | |
| 			{ text: Utils.translate("confirmation_modal_confirm_button"), class: "btn btn-primary", dismiss: true, action },
 | |
| 		],
 | |
| 	});
 | |
| };
 | |
| 
 | |
| Utils.showDefaultDangerModal = function (action, body = Utils.translate("confirmation_modal_body")) {
 | |
| 	Utils.showCustomModal({
 | |
| 		title: Utils.translate("confirmation_modal_title"),
 | |
| 		body,
 | |
| 		buttons: [
 | |
| 			{ text: Utils.translate("confirmation_modal_cancel_button"), class: "btn btn-outline-danger", dismiss: true },
 | |
| 			{ text: Utils.translate("confirmation_modal_confirm_button"), class: "btn btn-danger", dismiss: true, action },
 | |
| 		],
 | |
| 	});
 | |
| };
 | |
| /*
 | |
| const exampleConfig = {
 | |
| 	title: 'Custom Modal Title',
 | |
| 	body: 'Custom Modal Body Text',
 | |
| 	bodyHtml: '<p>Custom Modal Body Text that accept HTML</p>',
 | |
| 	bodyImage: "https://shuffle.dev/randomizer/saas/bootstrap-pstls/1.0.0/static_elements/footer/10_awz.jpg",
 | |
| 	footerText: "Custom Footer Text",
 | |
| 	buttons: [
 | |
| 		{ text: Utils.translate('confirmation_modal_cancel_button'), class: 'btn btn-outline-primary', dismiss: true },
 | |
| 		{ text: Utils.translate('confirmation_modal_confirm_button'), class: 'btn btn-primary', dismiss: false, action: () => console.log('Confirmed') }
 | |
| 		{ text: 'Submit', class: 'btn btn-primary', dismiss: false, type: 'submit' }
 | |
| 	],
 | |
| 	inputs: [
 | |
| 		{
 | |
| 			type: 'text', // Input type: text
 | |
| 			label: 'Text Input:',
 | |
| 			small: 'Small text',
 | |
| 			id: 'text-input-id',
 | |
| 			name: 'text-input-name',
 | |
| 			value: 'xxxx',
 | |
| 			required: true,
 | |
| 			placeholder: 'Enter text here'
 | |
| 		},
 | |
| 		{
 | |
| 			type: 'number', // Input type: number
 | |
| 			label: 'Number Input:',
 | |
| 			small: 'Small text',
 | |
| 			id: 'number-input-id',
 | |
| 			name: 'number-input-name',
 | |
| 			value: 10,
 | |
| 			min: 0,
 | |
| 			max: 10,
 | |
| 			required: true,
 | |
| 			placeholder: 'Enter a number'
 | |
| 		},
 | |
| 		{
 | |
| 			type: 'custom',
 | |
| 			html: `
 | |
| 				<div class="d-flex>
 | |
| 					// custom html
 | |
| 				</div>
 | |
| 			`
 | |
| 		},
 | |
| 		{
 | |
| 			type: 'select', // Input type: select
 | |
| 			label: 'Select Input:',
 | |
| 			id: 'select-input-id',
 | |
| 			name: 'select-input-name',
 | |
| 			required: true,
 | |
| 			options: [
 | |
| 				{ value: 'option1', text: 'Option 1' },
 | |
| 				{ value: 'option2', text: 'Option 2' },
 | |
| 				{ value: 'option3', text: 'Option 3' }
 | |
| 			]
 | |
| 		}
 | |
| 	],
 | |
| 	onSubmit: function(formData) {
 | |
| 		console.log("Form submitted with input values:", [...formData]);
 | |
| 		let amount = formData.get("number-input-name"); // Get by element name
 | |
| 		// You can perform further actions with the input values here
 | |
| 	}
 | |
| };
 | |
| */
 | |
| Utils.showCustomModal = function (config) {
 | |
| 	// Check if the modal already exists
 | |
| 	const $existingModal = $("#confirmation-modal");
 | |
| 	if ($existingModal.length > 0) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const modalConfig = {
 | |
| 		title: Utils.translate("confirmation_modal_title"),
 | |
| 		buttons: [],
 | |
| 		inputs: [],
 | |
| 	};
 | |
| 
 | |
| 	// Merge the provided config with the default modalConfig
 | |
| 	const mergedConfig = { ...modalConfig };
 | |
| 	Utils.deepMerge(mergedConfig, config);
 | |
| 
 | |
| 	// Append the modal HTML to the body
 | |
| 	$("body").append(modalTemplate);
 | |
| 
 | |
| 	// Cache the modal element
 | |
| 	const $modal = $("#confirmation-modal");
 | |
| 	const $modalBody = $modal.find(".modal-body");
 | |
| 
 | |
| 	// Set modal content
 | |
| 	$modal.find(".modal-title").text(mergedConfig.title);
 | |
| 
 | |
| 	if (mergedConfig.bodyImage) {
 | |
| 		const $imageContainer = $("<div>", { class: "d-flex justify-content-center m-2" });
 | |
| 		const $image = $("<img>", { src: mergedConfig.bodyImage, class: "w-50" });
 | |
| 		$imageContainer.append($image);
 | |
| 		$modalBody.append($imageContainer);
 | |
| 	}
 | |
| 
 | |
| 	if (mergedConfig.body) {
 | |
| 		const $p = $("<p>", { id: "modal-body-text", text: mergedConfig.body });
 | |
| 		$modalBody.append($p);
 | |
| 	}
 | |
| 
 | |
| 	if (mergedConfig.bodyHtml) {
 | |
| 		$modalBody.append(mergedConfig.bodyHtml);
 | |
| 	}
 | |
| 
 | |
| 	// Set modal inputs
 | |
| 	const $form = $modal.find("#form-confirmation-modal");
 | |
| 	mergedConfig.inputs.forEach(inputConfig => {
 | |
| 		const $inputContainer = $("<div>", { class: "form-group mx-2" });
 | |
| 
 | |
| 		if (inputConfig.type === "select") {
 | |
| 			const $label = $("<label>", { text: inputConfig.label, for: inputConfig.id });
 | |
| 			const $select = $("<select>", { class: "form-control", id: inputConfig.id, name: inputConfig.name, required: inputConfig.required });
 | |
| 			if (!Array.isArray(inputConfig.options)) {
 | |
| 				inputConfig.options = Object.values(inputConfig.options);
 | |
| 			}
 | |
| 			inputConfig.options.forEach(option => {
 | |
| 				const $option = $("<option>", { value: option.value, text: option.text });
 | |
| 				$select.append($option);
 | |
| 			});
 | |
| 			$inputContainer.append($label, $select);
 | |
| 		} else if (inputConfig.type === "checkbox") {
 | |
| 			const $checkboxContainer = $("<div>", { class: "form-check" });
 | |
| 			const $label = $("<label>", { for: inputConfig.id, class: "form-check-label" });
 | |
| 			const $input = $("<input>", { type: "checkbox", class: "form-check-input", id: inputConfig.id, name: inputConfig.name, required: inputConfig.required });
 | |
| 			$label.text(inputConfig.label);
 | |
| 			$inputContainer.append($checkboxContainer.append($input, $label));
 | |
| 		} else if (inputConfig.type === "slider" || inputConfig.type === "range") {
 | |
| 			const $label = $("<label>", { text: inputConfig.label, for: inputConfig.id });
 | |
| 			if (!inputConfig.default) {
 | |
| 				inputConfig.default = inputConfig.max ?? 100;
 | |
| 			}
 | |
| 			let range_slider = `
 | |
| 				<div class="range-slider mt-2" style='--min:${inputConfig.min || 0}; --max:${inputConfig.max || 100}; --step:${inputConfig.step || 1}; --value:${inputConfig.default}; --text-value:"${inputConfig.default}"; --prefix:"${inputConfig.isCurrency ? Utils.getCurrencySymbol() : ""} ";'>
 | |
| 					<input id="${inputConfig.id}" name="${inputConfig.name}" type="range" min="${inputConfig.min || 0}" max="${inputConfig.max || 100}" step="${inputConfig.step || 1}" value="${inputConfig.default}" oninput="this.parentNode.style.setProperty('--value',this.value); this.parentNode.style.setProperty('--text-value', JSON.stringify(this.value))">
 | |
| 					<output></output>
 | |
| 					<div class='range-slider__progress'></div>
 | |
| 				</div>`;
 | |
| 			$inputContainer.append($label, range_slider);
 | |
| 		} else if (inputConfig.type === "custom") {
 | |
| 			const $customInput = $(inputConfig.html);
 | |
| 			$inputContainer.append($customInput);
 | |
| 		} else {
 | |
| 			const $label = $("<label>", { text: inputConfig.label, for: inputConfig.id });
 | |
| 			const $input = $("<input>", { type: inputConfig.type, class: "form-control", id: inputConfig.id, name: inputConfig.name, required: inputConfig.required, placeholder: inputConfig.placeholder, min: inputConfig.min, max: inputConfig.max, value: inputConfig.value });
 | |
| 			$inputContainer.append($label, $input);
 | |
| 		}
 | |
| 		if (inputConfig.small) {
 | |
| 			const $small = $("<small>", { text: inputConfig.small, class: "text-muted", style: "font-size: 12px;" });
 | |
| 			$inputContainer.append($small);
 | |
| 		}
 | |
| 		$modalBody.append($inputContainer);
 | |
| 	});
 | |
| 
 | |
| 	if (mergedConfig.footerText) {
 | |
| 		const $p = $("<p>", { id: "modal-footer-text", text: mergedConfig.footerText });
 | |
| 		$modalBody.append($p);
 | |
| 	}
 | |
| 
 | |
| 	// Set modal buttons
 | |
| 	const $footer = $modal.find(".modal-footer");
 | |
| 	$footer.empty();
 | |
| 	mergedConfig.buttons.forEach(button => {
 | |
| 		const $button = $("<button>", { class: button.class, text: button.text, type: button.type ?? "button" });
 | |
| 		if (button.dismiss) {
 | |
| 			$button.attr("data-dismiss", "modal");
 | |
| 		}
 | |
| 		if (button.action) {
 | |
| 			$button.on("click", button.action);
 | |
| 		}
 | |
| 		$footer.append($button);
 | |
| 	});
 | |
| 
 | |
| 	// Set modal form submit
 | |
| 	$form.on("submit", function (e) {
 | |
| 		e.preventDefault();
 | |
| 
 | |
| 		if (config.onSubmit) {
 | |
| 			config.onSubmit(new FormData(e.target));
 | |
| 		}
 | |
| 		$modal.modal("hide");
 | |
| 	});
 | |
| 
 | |
| 	// Show the modal
 | |
| 	$modal.modal({ show: true });
 | |
| 
 | |
| 	// Remove the modal from the DOM when hidden
 | |
| 	$modal.on("hidden.bs.modal", function () {
 | |
| 		setTimeout(() => {
 | |
| 			$(this).remove();
 | |
| 		}, 50);
 | |
| 	});
 | |
| };
 | |
| 
 | |
| Utils.deepMerge = function (target, source) {
 | |
| 	for (const key in source) {
 | |
| 		if (source.hasOwnProperty(key)) {
 | |
| 			if (typeof source[key] === "function") {
 | |
| 				target[key] = source[key];
 | |
| 			} else if (source[key] instanceof Object && source[key] !== null) {
 | |
| 				if (!target.hasOwnProperty(key)) {
 | |
| 					target[key] = {};
 | |
| 				}
 | |
| 				Utils.deepMerge(target[key], source[key]);
 | |
| 			} else {
 | |
| 				target[key] = source[key];
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| if (!String.prototype.format) {
 | |
| 	String.prototype.format = function() {
 | |
| 		let args = arguments;
 | |
| 		return this.replace(/{(\d+)}/g, function(match, number) {
 | |
| 			return typeof args[number] != "undefined"
 | |
| 				? args[number]
 | |
| 				: match
 | |
| 			;
 | |
| 		});
 | |
| 	};
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * DEPRECATED: Use Utils.onInvalidInput(this) on input events instead.
 | |
|  */
 | |
| Utils.invalidMsg = function (textbox, min = null, max = null) {
 | |
| 	textbox.setCustomValidity("");
 | |
| 	if (textbox.value === "") {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.fill_field"));
 | |
| 	}
 | |
| 	else if (textbox.validity.typeMismatch) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.invalid_value"));
 | |
| 	}
 | |
| 	else if (textbox.validity.rangeUnderflow && min !== null) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.more_than").format(min));
 | |
| 	}
 | |
| 	else if (textbox.validity.rangeOverflow && max !== null) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.less_than").format(max));
 | |
| 	}
 | |
| 	else if (textbox.validity.stepMismatch) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.invalid_value"));
 | |
| 	}
 | |
| 	else if (textbox.validity.patternMismatch) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.pattern_mismatch"));
 | |
| 	}
 | |
| 	else if (textbox.validity.tooLong) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.too_long"));
 | |
| 	}
 | |
| 	else if (textbox.validity.tooShort) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.too_short"));
 | |
| 	}
 | |
| 	textbox.reportValidity();
 | |
| 	return true;
 | |
| };
 | |
| 
 | |
| Utils.onInvalidInput = function (textbox) { // oninvalid="Utils.onInvalidInput(this)"
 | |
| 	textbox.setCustomValidity("");
 | |
| 
 | |
| 	const elementType = textbox.tagName.toLowerCase(); // 'input', 'select', 'textarea'
 | |
| 	const inputType = elementType === "input" ? textbox.type.toLowerCase() : elementType; // 'text', 'email', 'select', etc.
 | |
| 
 | |
| 	if (textbox.validity.valueMissing || textbox.value === "") {
 | |
| 		if (inputType == "select") {
 | |
| 			textbox.setCustomValidity(Utils.translate("custom_validity.select_fill_field"));
 | |
| 		} else {
 | |
| 			textbox.setCustomValidity(Utils.translate("custom_validity.fill_field"));
 | |
| 		}
 | |
| 	}
 | |
| 	else if (textbox.validity.typeMismatch || textbox.validity.badInput) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.invalid_value"));
 | |
| 	}
 | |
| 	else if (textbox.validity.rangeUnderflow) {
 | |
| 		const min = $(textbox).attr("min");
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.more_than").format(Utils.numberFormat(min)));
 | |
| 	}
 | |
| 	else if (textbox.validity.rangeOverflow) {
 | |
| 		const max = $(textbox).attr("max");
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.less_than").format(Utils.numberFormat(max)));
 | |
| 	}
 | |
| 	else if (textbox.validity.stepMismatch) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.invalid_value"));
 | |
| 	}
 | |
| 	else if (textbox.validity.patternMismatch) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.pattern_mismatch"));
 | |
| 	}
 | |
| 	else if (textbox.validity.tooLong) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.too_long"));
 | |
| 	}
 | |
| 	else if (textbox.validity.tooShort) {
 | |
| 		textbox.setCustomValidity(Utils.translate("custom_validity.too_short"));
 | |
| 	}
 | |
| 	return true;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Sorts an object or an array of objects based on specified property paths.
 | |
|  * If the input is an object, it converts it to an array of objects, including the original object's key as an `.id` property.
 | |
|  * @param {Object|Array} input - The object or array to sort.
 | |
|  * @param {Array|String} propertyPath - The path(s) to the property for sorting, can be a single path or an array of paths for multiple criteria.
 | |
|  * @param {Boolean} ascending - Whether the sorting should be in ascending order. Defaults to true.
 | |
|  * @returns {Array} - The sorted array of objects.
 | |
|  */
 | |
| Utils.sortElement = function(input, propertyPath, ascending = true) {
 | |
| 	let arrayToSort;
 | |
| 
 | |
| 	// Convert input to an array of objects if it's not already one, including `.id`
 | |
| 	if (!Array.isArray(input)) {
 | |
| 		arrayToSort = Object.entries(input).map(([index, item]) => {
 | |
| 			if (item !== null && (typeof item === "object" || Array.isArray(item))) {
 | |
| 				return { ...item, id: item.id || index };
 | |
| 			} else {
 | |
| 				return { value: item, id: index };
 | |
| 			}
 | |
| 		});
 | |
| 	} else {
 | |
| 		arrayToSort = input.map((item, index) => ({
 | |
| 			...item,
 | |
| 			id: item.id || index,
 | |
| 		}));
 | |
| 	}
 | |
| 
 | |
| 	// Convert propertyPath to an array if it's not already one
 | |
| 	if (!Array.isArray(propertyPath)) {
 | |
| 		propertyPath = [propertyPath];
 | |
| 	}
 | |
| 
 | |
| 	// A helper function to safely access nested properties
 | |
| 	const resolvePath = (object, path) => {
 | |
| 		return path.split(".").reduce((accumulator, currentValue) => {
 | |
| 			return accumulator ? accumulator[currentValue] : undefined;
 | |
| 		}, object);
 | |
| 	};
 | |
| 
 | |
| 	// The sorting function
 | |
| 	return arrayToSort.sort((a, b) => {
 | |
| 		for (let i = 0; i < propertyPath.length; i++) {
 | |
| 			const aValue = resolvePath(a, propertyPath[i]);
 | |
| 			const bValue = resolvePath(b, propertyPath[i]);
 | |
| 
 | |
| 			if (typeof aValue === "string" && typeof bValue === "string") {
 | |
| 				const comparison = aValue.localeCompare(bValue);
 | |
| 				if (comparison !== 0) return ascending ? comparison : -comparison;
 | |
| 			} else {
 | |
| 				if (aValue < bValue) return ascending ? -1 : 1;
 | |
| 				if (aValue > bValue) return ascending ? 1 : -1;
 | |
| 			}
 | |
| 		}
 | |
| 		return 0; // if all criteria are equal
 | |
| 	});
 | |
| };
 | |
| 
 | |
| Utils.convertFileToBase64 = function (file, callback) {
 | |
| 	const reader = new FileReader();
 | |
| 	reader.onload = function(e) {
 | |
| 		callback(e.target.result);
 | |
| 	};
 | |
| 	reader.readAsDataURL(file);
 | |
| };
 | |
| 
 | |
| $(function () {
 | |
| 	window.addEventListener("message", function (event) {
 | |
| 		let item = event.data;
 | |
| 		if (item.notification) {
 | |
| 			vt.showNotification(item.notification, {
 | |
| 				position: item.position,
 | |
| 				duration: item.duration,
 | |
| 
 | |
| 				title: item.title,
 | |
| 				closable: true,
 | |
| 				focusable: false,
 | |
| 				callback: undefined,
 | |
| 			}, item.notification_type);
 | |
| 		}
 | |
| 		if (item.dark_theme != undefined) {
 | |
| 			if(item.dark_theme == 0){
 | |
| 				// Light theme
 | |
| 				$("#utils-css-light").prop("disabled", false);
 | |
| 				$("#utils-css-dark").prop("disabled", true);
 | |
| 			} else if(item.dark_theme == 1){
 | |
| 				// Dark theme
 | |
| 				$("#utils-css-dark").prop("disabled", false);
 | |
| 				$("#utils-css-light").prop("disabled", true);
 | |
| 			}
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	document.onkeyup = function(data){
 | |
| 		if (data.key == "Escape"){
 | |
| 			if ($("#confirmation-modal").is(":visible")){
 | |
| 				$("#confirmation-modal").modal("hide");
 | |
| 			} else if ($(".main").is(":visible")){
 | |
| 				$(".modal").modal("hide");
 | |
| 				Utils.post("close","");
 | |
| 			}
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	// Notification
 | |
| 	(() => {
 | |
| 		const toastPosition = {
 | |
| 			TopLeft: "top-left",
 | |
| 			TopCenter: "top-center",
 | |
| 			TopRight: "top-right",
 | |
| 			MiddleLeft: "middle-left",
 | |
| 			MiddleRight: "middle-right",
 | |
| 			BottomLeft: "bottom-left",
 | |
| 			BottomCenter: "bottom-center",
 | |
| 			BottomRight: "bottom-right",
 | |
| 		};
 | |
| 
 | |
| 		const toastPositionIndex = [
 | |
| 			[toastPosition.TopLeft, toastPosition.TopCenter, toastPosition.TopRight],
 | |
| 			[toastPosition.MiddleLeft, "", toastPosition.MiddleRight],
 | |
| 			[toastPosition.BottomLeft, toastPosition.BottomCenter, toastPosition.BottomRight],
 | |
| 		];
 | |
| 
 | |
| 		const svgs = {
 | |
| 			success: "<svg viewBox=\"0 0 426.667 426.667\" width=\"18\" height=\"18\"><path d=\"M213.333 0C95.518 0 0 95.514 0 213.333s95.518 213.333 213.333 213.333c117.828 0 213.333-95.514 213.333-213.333S331.157 0 213.333 0zm-39.134 322.918l-93.935-93.931 31.309-31.309 62.626 62.622 140.894-140.898 31.309 31.309-172.203 172.207z\" fill=\"#6ac259\"></path></svg>",
 | |
| 			warning: "<svg viewBox=\"0 0 310.285 310.285\" width=18 height=18> <path d=\"M264.845 45.441C235.542 16.139 196.583 0 155.142 0 113.702 0 74.743 16.139 45.44 45.441 16.138 74.743 0 113.703 0 155.144c0 41.439 16.138 80.399 45.44 109.701 29.303 29.303 68.262 45.44 109.702 45.44s80.399-16.138 109.702-45.44c29.303-29.302 45.44-68.262 45.44-109.701.001-41.441-16.137-80.401-45.439-109.703zm-132.673 3.895a12.587 12.587 0 0 1 9.119-3.873h28.04c3.482 0 6.72 1.403 9.114 3.888 2.395 2.485 3.643 5.804 3.514 9.284l-4.634 104.895c-.263 7.102-6.26 12.933-13.368 12.933H146.33c-7.112 0-13.099-5.839-13.345-12.945L128.64 58.594c-.121-3.48 1.133-6.773 3.532-9.258zm23.306 219.444c-16.266 0-28.532-12.844-28.532-29.876 0-17.223 12.122-30.211 28.196-30.211 16.602 0 28.196 12.423 28.196 30.211.001 17.591-11.456 29.876-27.86 29.876z\" fill=\"#FFDA44\" /> </svg>",
 | |
| 			info: "<svg viewBox=\"0 0 23.625 23.625\" width=18 height=18> <path d=\"M11.812 0C5.289 0 0 5.289 0 11.812s5.289 11.813 11.812 11.813 11.813-5.29 11.813-11.813S18.335 0 11.812 0zm2.459 18.307c-.608.24-1.092.422-1.455.548a3.838 3.838 0 0 1-1.262.189c-.736 0-1.309-.18-1.717-.539s-.611-.814-.611-1.367c0-.215.015-.435.045-.659a8.23 8.23 0 0 1 .147-.759l.761-2.688c.067-.258.125-.503.171-.731.046-.23.068-.441.068-.633 0-.342-.071-.582-.212-.717-.143-.135-.412-.201-.813-.201-.196 0-.398.029-.605.09-.205.063-.383.12-.529.176l.201-.828c.498-.203.975-.377 1.43-.521a4.225 4.225 0 0 1 1.29-.218c.731 0 1.295.178 1.692.53.395.353.594.812.594 1.376 0 .117-.014.323-.041.617a4.129 4.129 0 0 1-.152.811l-.757 2.68a7.582 7.582 0 0 0-.167.736 3.892 3.892 0 0 0-.073.626c0 .356.079.599.239.728.158.129.435.194.827.194.185 0 .392-.033.626-.097.232-.064.4-.121.506-.17l-.203.827zm-.134-10.878a1.807 1.807 0 0 1-1.275.492c-.496 0-.924-.164-1.28-.492a1.57 1.57 0 0 1-.533-1.193c0-.465.18-.865.533-1.196a1.812 1.812 0 0 1 1.28-.497c.497 0 .923.165 1.275.497.353.331.53.731.53 1.196 0 .467-.177.865-.53 1.193z\" fill=\"#006DF0\" /> </svg>",
 | |
| 			error: "<svg viewBox=\"0 0 51.976 51.976\" width=18 height=18> <path d=\"M44.373 7.603c-10.137-10.137-26.632-10.138-36.77 0-10.138 10.138-10.137 26.632 0 36.77s26.632 10.138 36.77 0c10.137-10.138 10.137-26.633 0-36.77zm-8.132 28.638a2 2 0 0 1-2.828 0l-7.425-7.425-7.778 7.778a2 2 0 1 1-2.828-2.828l7.778-7.778-7.425-7.425a2 2 0 1 1 2.828-2.828l7.425 7.425 7.071-7.071a2 2 0 1 1 2.828 2.828l-7.071 7.071 7.425 7.425a2 2 0 0 1 0 2.828z\" fill=\"#D80027\" /> </svg>",
 | |
| 		};
 | |
| 
 | |
| 		const styles = `
 | |
| 			.vt-container {
 | |
| 				position: fixed;
 | |
| 				width: 100%;
 | |
| 				height: 100vh;
 | |
| 				top: 0;
 | |
| 				left: 0;
 | |
| 				z-index: 9999;
 | |
| 				display: flex;
 | |
| 				flex-direction: column;
 | |
| 				justify-content: space-between;
 | |
| 				pointer-events: none;
 | |
| 			}
 | |
| 
 | |
| 			.vt-row {
 | |
| 				display: flex;
 | |
| 				justify-content: space-between;
 | |
| 			}
 | |
| 
 | |
| 			.vt-col {
 | |
| 				flex: 1;
 | |
| 				margin: 10px 20px;
 | |
| 				display: flex;
 | |
| 				flex-direction: column;
 | |
| 				align-items: center;
 | |
| 			}
 | |
| 
 | |
| 			.vt-col.top-left,
 | |
| 			.vt-col.bottom-left {
 | |
| 				align-items: flex-start;
 | |
| 			}
 | |
| 
 | |
| 			.vt-col.top-right,
 | |
| 			.vt-col.bottom-right {
 | |
| 				align-items: flex-end;
 | |
| 			}
 | |
| 			
 | |
| 			.vt-col.middle-left {
 | |
| 				justify-content: center;
 | |
| 				align-items: flex-start;
 | |
| 			}
 | |
| 
 | |
| 			.vt-col.middle-right {
 | |
| 				justify-content: center;
 | |
| 				align-items: flex-end;
 | |
| 			}
 | |
| 
 | |
| 
 | |
| 			.vt-card {
 | |
| 				display: flex;
 | |
| 				justify-content: center;
 | |
| 				align-items: center;
 | |
| 				padding: 12px 20px;
 | |
| 				box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 | |
| 				border-radius: 4px;
 | |
| 				margin: 0px;
 | |
| 				transition: 0.3s all ease-in-out;
 | |
| 				pointer-events: all;
 | |
| 				border-left: 3px solid #8b8b8b;
 | |
| 				cursor: pointer;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card.success {
 | |
| 				border-left: 3px solid #6ec05f;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card.warning {
 | |
| 				border-left: 3px solid #fed953;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card.info {
 | |
| 				border-left: 3px solid #1271ec;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card.error {
 | |
| 				border-left: 3px solid #d60a2e;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card .text-group {
 | |
| 				margin-left: 15px;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card h4 {
 | |
| 				margin: 0;
 | |
| 				margin-bottom: 2px;
 | |
| 				font-size: 16px;
 | |
| 				font-weight: 600;
 | |
| 			}
 | |
| 
 | |
| 			.vt-card p {
 | |
| 				margin: 0;
 | |
| 				font-size: 14px;
 | |
| 			}
 | |
| 		`;
 | |
| 
 | |
| 		const styleSheet = document.createElement("style");
 | |
| 		styleSheet.innerText = styles.replace((/  |\r\n|\n|\r/gm), "");
 | |
| 		document.head.appendChild(styleSheet);
 | |
| 
 | |
| 		const vtContainer = document.createElement("div");
 | |
| 		vtContainer.className = "vt-container";
 | |
| 
 | |
| 		for (const ri of [0, 1, 2]) {
 | |
| 			const row = document.createElement("div");
 | |
| 			row.className = "vt-row";
 | |
| 
 | |
| 			for (const ci of [0, 1, 2]) {
 | |
| 				const col = document.createElement("div");
 | |
| 				col.className = `vt-col ${toastPositionIndex[ri][ci]}`;
 | |
| 
 | |
| 				row.appendChild(col);
 | |
| 			}
 | |
| 
 | |
| 			vtContainer.appendChild(row);
 | |
| 		}
 | |
| 
 | |
| 		document.body.appendChild(vtContainer);
 | |
| 
 | |
| 		window.vt = {
 | |
| 			options: {
 | |
| 				title: undefined,
 | |
| 				position: toastPosition.TopCenter,
 | |
| 				duration: 10000,
 | |
| 				closable: true,
 | |
| 				focusable: true,
 | |
| 				callback: undefined,
 | |
| 			},
 | |
| 			showNotification(message, options, type) {
 | |
| 				show(message, options, type);
 | |
| 			},
 | |
| 		};
 | |
| 
 | |
| 		function show(message = "", options, type) {
 | |
| 			options = { ...window.vt.options, ...options };
 | |
| 
 | |
| 			const col = document.getElementsByClassName(options.position)[0];
 | |
| 
 | |
| 			const vtCard = document.createElement("div");
 | |
| 			vtCard.className = `vt-card ${type}`;
 | |
| 			vtCard.innerHTML += svgs[type];
 | |
| 			vtCard.options = {
 | |
| 				...options, ...{
 | |
| 					message,
 | |
| 					type: type,
 | |
| 					yPos: options.position.indexOf("top") > -1 ? "top" : "bottom",
 | |
| 					isFocus: false,
 | |
| 				},
 | |
| 			};
 | |
| 
 | |
| 			setVTCardContent(vtCard);
 | |
| 			setVTCardIntroAnim(vtCard);
 | |
| 			setVTCardBindEvents(vtCard);
 | |
| 			autoDestroy(vtCard);
 | |
| 
 | |
| 			col.appendChild(vtCard);
 | |
| 		}
 | |
| 
 | |
| 		function setVTCardContent(vtCard) {
 | |
| 			const textGroupDiv = document.createElement("div");
 | |
| 
 | |
| 			textGroupDiv.className = "text-group";
 | |
| 
 | |
| 			if (vtCard.options.title) {
 | |
| 				textGroupDiv.innerHTML = `<h4>${vtCard.options.title}</h4>`;
 | |
| 			}
 | |
| 
 | |
| 			textGroupDiv.innerHTML += `<p>${vtCard.options.message}</p>`;
 | |
| 
 | |
| 			vtCard.appendChild(textGroupDiv);
 | |
| 		}
 | |
| 
 | |
| 		function setVTCardIntroAnim(vtCard) {
 | |
| 			vtCard.style.setProperty(`margin-${vtCard.options.yPos}`, "-15px");
 | |
| 			vtCard.style.setProperty("opacity", "0");
 | |
| 
 | |
| 			setTimeout(() => {
 | |
| 				vtCard.style.setProperty(`margin-${vtCard.options.yPos}`, "15px");
 | |
| 				vtCard.style.setProperty("opacity", "1");
 | |
| 			}, 50);
 | |
| 		}
 | |
| 
 | |
| 		function setVTCardBindEvents(vtCard) {
 | |
| 			vtCard.addEventListener("click", () => {
 | |
| 				if (vtCard.options.closable) {
 | |
| 					destroy(vtCard);
 | |
| 				}
 | |
| 			});
 | |
| 
 | |
| 			vtCard.addEventListener("mouseover", () => {
 | |
| 				vtCard.options.isFocus = vtCard.options.focusable;
 | |
| 			});
 | |
| 
 | |
| 			vtCard.addEventListener("mouseout", () => {
 | |
| 				vtCard.options.isFocus = false;
 | |
| 				autoDestroy(vtCard, vtCard.options.duration);
 | |
| 			});
 | |
| 		}
 | |
| 
 | |
| 		function destroy(vtCard) {
 | |
| 			vtCard.style.setProperty(`margin-${vtCard.options.yPos}`, `-${vtCard.offsetHeight}px`);
 | |
| 			vtCard.style.setProperty("opacity", "0");
 | |
| 
 | |
| 			setTimeout(() => {
 | |
| 				if(typeof x !== "undefined"){
 | |
| 					vtCard.parentNode.removeChild(v);
 | |
| 
 | |
| 					if (typeof vtCard.options.callback === "function") {
 | |
| 						vtCard.options.callback();
 | |
| 					}
 | |
| 				}
 | |
| 			}, 500);
 | |
| 		}
 | |
| 
 | |
| 		function autoDestroy(vtCard) {
 | |
| 			if (vtCard.options.duration !== 0) {
 | |
| 				setTimeout(() => {
 | |
| 					if (!vtCard.options.isFocus) {
 | |
| 						destroy(vtCard);
 | |
| 					}
 | |
| 				}, vtCard.options.duration);
 | |
| 			}
 | |
| 		}
 | |
| 	})();
 | |
| }); | 
