(function ($) {
	'use strict';

	$(function () {

		const i18n = mdspbeVars.i18n;
		const changes = {};
		const changeRecords = {};
		let currentRecord = 0;
		let timeRecording;
		let creatingProds = 0;
		let allowEdit = true;

		// We do this here instead of with php, because of the wp_kses() funcion that strips the style attr off
		$('.js-current-label').fadeOut();

		// Remove any previous action parameter and show notice
		function removePreviousParameters() {
			// Get the current URL
			let url = new URL(window.location.href);

			const success = url.searchParams.get('success');
			if (success) {
				url.searchParams.delete('success');

				showAlert(i18n[success + 'BulkSuccess'], 'success');

				// Update the browser's address bar with the new URL (without reloading the page)
				window.history.pushState({}, '', url.toString());
			}
		}
		removePreviousParameters();


		// Initialise Tom select for ajax options
		function initTomSelect(el, attrId) {

			// Destroy previus isntance if any
			const prevTomSelect = el.tomselect;
			if (prevTomSelect !== undefined) {
				prevTomSelect.destroy();
			}

			let settings = {
				plugins: ['remove_button'],
				valueField: 'id',       // use 'id' as the value
				labelField: 'name',     // show 'name' as the label
				searchField: 'name',    // searchable by 'name'
				onInitialize: function () {
					this.control.classList.add('rounded-3');
					this.control.classList.add('border-secondary');

					// Manually sync visibility after init
					if ($(el).hasClass('js-hidden')) {
						this.wrapper.style.display = 'none';
					}
				},
				// minimum query length
				shouldLoad: function (query) {
					return query.length > 2;
				},
				load: function (term, callback) {
					if ($(el).hasClass('js-remote-data')) {
						const url = mdspbeVars.restUrl + 'mdspbe/v1/get_ajax_options?term=' + encodeURIComponent(term) + '&attrId=' + encodeURIComponent(attrId);
						fetch(url, {
							method: 'GET',
							headers: {
								'X-WP-Nonce': mdspbeVars.nonce
							}
						})
							.then(response => response.json()) // return the promise
							.then(products => {
								callback(products);
							})
							.catch(() => {
								callback();
							});
					} else {
						callback();
					}
				},
				render: {
					option: function (item, escape) {
						return `<div>${escape(item.name)}</div>`;
					},
					item: function (item, escape) {
						return `<div>${escape(item.name)}</div>`;
					}
				}
			};
			new TomSelect(el, settings);
		}

		// Initialize WP editor
		function initWpEditor(textAreaIds) {
			if (typeof wp !== 'undefined' && wp.editor && wp.editor.initialize) {
				textAreaIds.forEach(function (id) {

					if (wp.editor.remove) {
						wp.editor.remove(id); // Clean up before reinitializing
					}

					wp.editor.initialize(id, {
						tinymce: {
							wpautop: true,
							toolbar1: 'bold italic underline | bullist numlist | link',
							plugins: 'lists link'
						},
						quicktags: true,
						mediaButtons: false
					});
				});
			}
		}


		function createAtumPurchaseOrderFields() {
			if (mdspbeVars.atumActive) {
				// Header cells
				const siblingHCells = $('#mdspbe-main-table thead').find('th.sticky-col');
				siblingHCells.each(function () {
					$(this).after(
						`<th class="table-warning js-purchase-order-cell" style="display: none;">
							<div class="th-content-wrapper">
								<div class="text-center text-nowrap">
									${i18n.purchaseOrder}
								</div>
							</div>
						</th>`
					);
				});


				// Body cells
				const siblingBCells = $('#mdspbe-main-table tbody').find('td.sticky-col');
				siblingBCells.each(function () {
					$(this).after(
						`<td class="table-warning js-purchase-order-cell" style="display: none;" data-id="atum_purchase_qty">
							<input type="number" class="form-control rounded-3 js-po-qty">
						</td>`
					);
				});
			}
		}
		createAtumPurchaseOrderFields();

		// Maybe disable or enable fields
		function maybeDisableToggleSiblings($input) {
			const attrId = $input.attr('data-id');
			const conditionals = mdspbeVars.conditionals;
			const attrConditionals = conditionals[attrId];

			if (!attrConditionals) {
				return;
			}

			attrConditionals.forEach(function (affectedAttrId) {
				maybeDisableToggleField(affectedAttrId, $input);
			});
		}

		function maybeDisableToggleField(affectedAttrId, $input) {

			const $row = $input.closest('tr');
			const affectedElement = $row.find(`td[data-id="${affectedAttrId}"]`).find('input, select, textarea');

			const attributes = mdspbeVars.attributes;
			const thisAttr = attributes[affectedAttrId];
			const conditionals = thisAttr.conditionals;

			if (!conditionals) {
				return;
			}

			let disabled = false;
			Object.keys(conditionals).some(function (attrId) {
				let attrValue = conditionals[attrId];

				// Convert true and false to yes and no
				if (attrValue === true) {
					attrValue = 'yes';
				} else if (attrValue === false) {
					attrValue = 'no';
				}

				const condElement = $row.find(`td[data-id="${attrId}"]`).find('input, select, textarea');
				if (condElement && condElement.val() !== attrValue) {
					disabled = true;
					return true;
				}
			});

			affectedElement.prop('disabled', disabled);
			if (disabled) {
				const defaultVal = thisAttr.default ?? '';
				updateInputValue(affectedElement, defaultVal);
			}
		}

		// Listen to changes in the PO quantity input
		$(document).on('change', '.js-po-qty', function () {
			const $this = $(this);
			if ($this.val() === '') {
				$this.closest('td').removeClass('table-danger').addClass('table-warning');
			} else {
				$this.closest('td').removeClass('table-warning').addClass('table-danger');
			}
		});

		// Listen to changes in any input of the main table
		$('#mdspbe-main-table').on('change', 'input:not(.js-element-selector, .js-select-all, .js-po-qty), select, textarea', function (e) {
			const $this = $(this);

			// Some fields require others to hace certain values in order to be enabled
			maybeDisableToggleSiblings($this);

			// Update the changes object and the DOM
			const prodId = Number($this.closest('tr').attr('data-id'));
			if (prodId === 0) { // We don't do anything with "still not created" products
				return;
			}

			const attrId = sanitizeValue($this.attr('data-id'));
			const td = $this.closest('td');
			const value = $this.val();

			const sanitizedValue = sanitizeValue(value);
			if (value !== sanitizedValue) {
				$this.val(sanitizedValue);
			}

			// Compare the new with the old values
			const val_1 = $this.val();
			let val_2 = sanitizeValue($this.attr('data-value'));

			if (Array.isArray(val_1)) {
				if (val_2 == '') {
					val_2 = '[]';
				}
			}

			//const val_2 = Array.isArray(val_1) ? JSON.parse(sanitizeValue($this.attr('data-value'))) : sanitizeValue($this.attr('data-value'));
			let areEqual;
			if (Array.isArray(val_1)) {
				areEqual = areArrayEqual(val_1, val_2);
			} else {
				areEqual = val_1 === val_2;
			}

			if (!areEqual) {
				td.addClass('table-danger');
				td.find('.js-current-label').fadeIn();

				if (changes[prodId] === undefined) {
					changes[prodId] = {};
				}
				changes[prodId][attrId] = sanitizedValue;
			} else {
				td.removeClass('table-danger');
				td.find('.js-current-label').fadeOut();

				if (changes[prodId]) {
					delete changes[prodId][attrId];
					if (Object.keys(changes[prodId]).length === 0) {
						delete changes[prodId];
					}
				}

			}

			// Toggle save button
			if (Object.keys(changes).length === 0 && creatingProds === 0) {
				$('.js-save-btn').fadeOut();
			} else {
				$('.js-save-btn').fadeIn();
			}

		});


		/**
		 * This is only for the inputs that are changed directly (not bulk actions or images)
		 * For bulk actions and others, we use updateInputValue() and updateElementHTML()
		 */

		let previousValue = null;
		$('#mdspbe-main-table').find('input:not(.js-element-selector, .js-select-all, .js-po-qty), select, textarea')
			.on('focus', function () {
				previousValue = sanitizeValue($(this).val());
			})
			.on('blur', function () {
				previousValue = null;
			})
			.on('change', function () {
				// Make sure the element was focused before
				if (previousValue === null) {
					return;
				}

				const $this = $(this);
				recordSingleChange($this);
			});

		function recordSingleChange($this) {
			const value = sanitizeValue($this.val());
			const prevVal = previousValue;

			if (prevVal !== value) {

				maybeRemoveLastRecords();

				// Set the current record state
				currentRecord++;

				// Record change
				const timeRecording = Date.now();
				if (changeRecords[timeRecording] === undefined) {
					changeRecords[timeRecording] = {};
				}
				if (changeRecords[timeRecording]['value'] === undefined) {
					changeRecords[timeRecording]['value'] = [];
				}
				changeRecords[timeRecording]['value'].push({ input: $this, old: prevVal, new: value });

				// Enable / disable btns
				$('.js-undo-changes').prop('disabled', false);
				$('.js-redo-changes').prop('disabled', true);
			}

			previousValue = value; // Global

		}

		// Undo changes
		$('.js-undo-changes').on('click', function () {
			undoRedoChanges('undo');
		});

		// Redo changes
		$('.js-redo-changes').on('click', function () {
			undoRedoChanges('redo');
		});

		$(document).on('keydown', function (e) {
			// Ctrl + Z
			if (e.ctrlKey && e.key.toLowerCase() === 'z' && !e.shiftKey) {
				e.preventDefault();
				$(document.activeElement).blur();
				undoRedoChanges('undo');
			}

			// Ctrl + Y or Ctrl + Shift + Z (both are common for redo)
			if (
				(e.ctrlKey && e.key.toLowerCase() === 'y') ||
				(e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'z')
			) {
				e.preventDefault();
				$(document.activeElement).blur();
				undoRedoChanges('redo');
			}
		});

		// Listen to the product / element selects
		$('.js-element-selector').on('change', function () {

			// Change row color
			$(this).closest('tr').toggleClass('table-primary', $(this).prop('checked'));

			toggleBulkButtons();

		});

		function toggleBulkButtons(toggle = null) {
			if (toggle === null) {
				toggle = $('.js-element-selector:checked').length > 0;
			}

			let removeClass; let addClass;
			if (!toggle) {
				removeClass = 'btn-warning';
				addClass = 'btn-light';
			} else {
				removeClass = 'btn-light';
				addClass = 'btn-warning';
			}


			$('.js-bulk-attribute-edit, .js-bulk-action-apply')
				.prop('disabled', !toggle)
				.removeClass(removeClass)
				.addClass(addClass);
		}

		// Select / unselect all products
		$('.js-select-all').on('change', function () {
			const isChecked = $(this).is(':checked');
			$('tbody .js-element-selector')
				.prop('checked', isChecked)
				.trigger('change');
		});

		// Toggle extra filters
		$('.js-toggle-extra-filters').on('click', function (e) {
			e.preventDefault();

			$('#mdspbe-hidden-filters').toggleClass('d-none');
		});

		// Bulk action option selected listener
		$('#bulk-action').on('change', function () {

			const isPO = ['atum-create-po', 'atum-update-po'].includes($(this).val());

			// Toggle the edit mode
			allowEdit = !isPO;

			if (isPO) {
				toggleBulkButtons(isPO);
			} else {
				toggleBulkButtons();
			}

			$('#mdspbe-main-table').find('td, th').toggle(!isPO);
			$('.js-bulk-attribute-edit, .js-no-bulk-action, .js-element-selector, .js-select-all').toggle(!isPO);

			const cells = $('#mdspbe-main-table td').not('.js-purchase-order-cell');
			cells.find('input').prop('readonly', isPO);
			cells.find('select').prop('disabled', isPO);

			if (isPO) {
				$('#mdspbe-main-table').find('.js-purchase-order-cell, .js-po-keep').toggle(isPO);
			} else {
				$('#mdspbe-main-table').find('.js-purchase-order-cell').toggle(isPO);
			}

			$('.float-div').toggle(!isPO);

		});

		// Make user select a bulk action
		$('.js-bulk-action-apply').on('click', function (e) {
			if ($('#bulk-action').val() === '') {
				e.preventDefault();
				showAlert(i18n.selectAnAction, 'warning');
			}
		});

		// Bulk action form submission
		let isSubmitting = false;
		$('#bulk-actions-form').on('submit', async function (e) {

			const action = $('#bulk-action').val();
			if (action === 'atum-create-po') {
				e.preventDefault();
				await showCreatePOForm();
			} else if (action === 'atum-update-po') {
				e.preventDefault();
				await showUpdatePOForm();
			} else {
				if (isSubmitting) return;
				e.preventDefault();

				const confirm = await showConfirm(i18n.wishToProceed);
				if (confirm) {

					blockUI();

					isSubmitting = true;

					// Add the selected product ids to a hidden form field
					const selected = $('.js-element-selector:checked');
					const prodIds = [];
					selected.each(function () {
						const prodId = Number($(this).attr('data-id'));
						prodIds.push(prodId);
					});

					$('#bulk-ids').val(JSON.stringify(prodIds));

					// Submit the form
					$(this).submit();
				}
			}

		});

		function getSupplierNameFromId(supplierId) {

			if (!supplierId) {
				return '';
			}

			const supplierName = sanitizeValue($('#atum-supplier').find(`option[value="${supplierId}"]`).text());
			return supplierName;
		}

		async function showCreatePOForm() {

			const lineItems = getPOLineItems();
			if (lineItems.length === 0) {
				showAlert(i18n.noProductSelected, 'warning');
				return;
			}

			const supplierId = parseInt($('#atum_supplier_id').val());
			const supplierName = getSupplierNameFromId(supplierId);

			// Get the raw value
			const args = {
				message: supplierName ? i18n.purchaseOrder + ' - ' + supplierName : i18n.purchaseOrder + ' - ' + i18n.multipleSuppliers,
				submitText: i18n.createPurchaseOrder,
				htmlCallback: function () {
					const html = `
						<div class="form-floating">
							<textarea class="form-control rounded-3 shadow-sm mt-3" name="description" required></textarea>
							<label>${i18n.description} <span class="required-mark">*</span></label>
						</div>
						<div class="form-floating">
							<input type="datetime-local" class="form-control rounded-3 shadow-sm mt-3" name="date_expected" required>
							<label>${i18n.expectedDate} <span class="required-mark">*</span></label>
						</div>
						<div class="form-floating">
							<select class="form-select rounded-3 shadow-sm mt-3" name="status" required>
								<option value="atum_pending" selected>${i18n.pending}</option>
								<option value="atum_ordered">${i18n.ordered}</option>
								<option value="atum_onthewayin">${i18n.onthewayin}</option>
								<option value="atum_receiving">${i18n.receiving}</option>
								<option value="atum_received ">${i18n.received}</option>
							</select>	
							<label>${i18n.poStatus} <span class="required-mark">*</span></label>
						</div>
						`;
					return html;
				}
			};

			const formData = await showFormPrompt(args);
			if (formData === null) {
				return;
			}

			const poData = {
				date_expected: formData.date_expected,
				description: formData.description,
				status: formData.status,
				line_items: lineItems
			};

			if (supplierId) {
				poData.supplier = supplierId;
				poData.multiple_suppliers = false;
			} else {
				poData.multiple_suppliers = true;
			}

			submitPO(poData);
		}


		async function showUpdatePOForm() {

			try {

				const lineItems = getPOLineItems();
				if (lineItems.length === 0) {
					showAlert(i18n.noProductSelected, 'warning');
					return;
				}

				const purchaseOrders = await getPOs();

				let supplierId = $('#atum_supplier_id').val();
				supplierId = supplierId ? parseInt($('#atum_supplier_id').val()) : null;
				const supplierName = getSupplierNameFromId(supplierId) ?? i18n.multipleSuppliers;

				// Get the raw value
				const args = {
					message: supplierName ? i18n.purchaseOrder + ' - ' + supplierName : i18n.purchaseOrder + ' - ' + i18n.multipleSuppliers,
					submitText: i18n.addToPurchaseOrder,
					htmlCallback: function () {

						let html = '';
						let options = '';
						purchaseOrders.forEach(function (purchaseOrder) {

							const poSupplierId = purchaseOrder.supplier;
							let poSupplierName = getSupplierNameFromId(poSupplierId);
							poSupplierName = poSupplierName ? poSupplierName : i18n.multipleSuppliers;

							const description = purchaseOrder.description ?? '';
							const total = purchaseOrder.total ?? '0';
							const currency = purchaseOrder.currency ?? '';
							const status = purchaseOrder.status.replace(/^atum_/, "");

							options += `<option value="${purchaseOrder.id}">${poSupplierName} - ${description} - ${currency} ${total} - ${i18n[status]}</option>`;

						});

						html = `
							<div class="form-floating">
								<select class="form-select rounded-3 shadow-sm mt-3" name="po_id" required>
									<option value="" selected disabled hidden></option>
									${options}
								</select>	
								<label>${i18n.purchaseOrder} <span class="required-mark">*</span></label>
							</div>
							`;

						return html;
					}
				};

				const formData = await showFormPrompt(args);
				if (formData === null) {
					return;
				}

				const poData = {
					line_items: lineItems
				};

				const poId = formData.po_id;

				submitPO(poData, poId);

			} catch (error) {
				// Nothing for now
			}

		}


		function getPOs() {
			blockUI();

			const supplierId = parseInt($('#atum_supplier_id').val());

			const data = {
				status: ['atum_pending', 'atum_ordered', 'atum_onthewayin', 'atum_receiving'],
				per_page: 50
			}

			if (supplierId) {
				data.supplier = supplierId;
			} else {
				data.multiple_suppliers = true;
			}

			return new Promise((resolve, reject) => {
				$.ajax({
					url: mdspbeVars.restUrl + 'wc/v3/atum/purchase-orders',
					method: 'GET',
					data: data,
					dataType: 'json',
					beforeSend: function (xhr) {
						xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
					},
					success: function (response) {
						resolve(response);
						unblockUI();
					},
					error: function (error) {
						reject(error);
						showAlert(i18n.errorGettingPO, 'danger');
						unblockUI();
					}
				});
			});
		}

		function submitPO(poData, poId = null) {
			blockUI();

			let url = mdspbeVars.restUrl + 'wc/v3/atum/purchase-orders';
			if (poId) {
				url += '/' + poId;
			}

			$.ajax({
				url,
				method: 'POST',
				contentType: 'application/json',
				data: JSON.stringify(poData),
				beforeSend: function (xhr) {
					xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
				},
				success: function (response) {
					$('.js-po-qty').val('').trigger('change').closest('td').removeClass('table-danger').addClass('table-warning');
					showAlert(`${poId ? i18n.poUpdatedSuccesfully : i18n.poCreatedSuccesfully}. <a href="${mdspbeVars.adminUrl}post.php?post=${response.id}&action=edit" target="_blank">${i18n.viewPurchaseOrder}</a>`, 'success');
					unblockUI();
				},
				error: function (error) {
					showAlert(i18n.errorCreatingPO, 'danger');
					unblockUI();
				}
			});
		}


		function getPOLineItems() {

			const lineItems = [];

			$('#mdspbe-main-table .js-po-qty').each(function () {
				const $this = $(this);
				const row = $this.closest('tr');
				const productId = isFinite(row.attr('data-id')) ? parseInt(row.attr('data-id')) : null;
				const quantity = isFinite($this.val()) ? parseInt($this.val()) : null;

				if (productId && quantity) {
					lineItems.push({
						product_id: productId, quantity,
					});
				}

			});

			return lineItems;
		}


		// Bulk action listener
		$('.js-bulk-attribute-edit').on('click', function (e) {
			const dataId = sanitizeValue($(this).attr('data-id'));
			const prototype = $('#mdspbe-main-table tfoot td').find(`[data-id="${dataId}"]`);

			changeInputsValues(prototype, dataId, null, true);
		});

		// Open input full view mode
		$('#mdspbe-main-table').on('dblclick', 'input:not(.js-element-selector, .js-select-all), textarea', async function (e) {
			const dataId = sanitizeValue($(this).attr('data-id'));
			changeInputsValues($(this), dataId, sanitizeValue($(this).val()));
		});

		// Add a select option form
		$('.js-add-option').on('click', async function () {

			const id = $(this).data('id');
			const name = $(this).data('name');
			const editOpts = mdspbeVars.editOpts[id];
			const fields = editOpts.add.fields;

			if (editOpts === undefined || editOpts.add === undefined) {
				return;
			}

			// Get the raw value
			const args = {
				message: name + ': ' + i18n.addNew,
				submitText: i18n.add,
				htmlCallback: function () {
					let html = '';

					Object.keys(fields).forEach(function (key) {
						const field = fields[key];
						const required = field.required ? 'required' : '';
						const requiredMark = field.required ? ' <span class="required-mark">*</span>' : '';
						let input;
						switch (field.type) {
							case 'input_text':
								input = `<input type="text" class="form-control rounded-3 shadow-sm mt-3" name="${field.name}" ${required}>`;
								break;
							case 'input_number':
								input = `<input type="number" class="form-control rounded-3 shadow-sm mt-3" name="${field.name}" ${required}>`;
								break;
							case 'textarea':
								input = `<textarea class="form-control rounded-3 shadow-sm mt-3" name="${field.name}" ${required}></textarea>`;
								break;
							case 'select':
								let opts = '<option value="0"></option>';
								Object.keys(field.options).forEach(function (optKey) {
									opts += `<option value="${optKey}">${field.options[optKey]}</option>`;
								});
								input = `<select class="form-select rounded-3 shadow-sm mt-3" id="add-field-${id}" name="${field.name}" ${required}>${opts}</select>`;
								break;
						}

						const htmlField = `
						<div class="form-floating">
							${input}
							<label for="add-field-${field.name}">${field.label}${requiredMark}</label>
						</div>`;

						html += htmlField;

					});

					return html;
				}
			};
			const formData = await showFormPrompt(args);

			if (formData !== null) {
				formData.id = id;
				addNewOption(formData, function (response) {
					// Update all the select inputs
					const selectElems = $(`select[data-id="${id}"]`);
					selectElems.prepend($('<option>', {
						value: response.id,
						text: response.name
					}));

					// Update the new-option form data
					Object.keys(fields).forEach(function (key) {
						const field = fields[key];
						if (field.type === 'select' && field.addNewOpts === true) {
							mdspbeVars.editOpts[id].add.fields[key].options[response.id] = response.name;
						}
					});

					// Maybe update the filter options
					$('#' + id).append($('<option>', {
						value: response.id,
						text: response.name
					}));

				});
			}
		});

		// Remove select options form
		$('.js-remove-options').on('click', async function () {

			const id = $(this).data('id');
			const name = $(this).data('name');
			const editOpts = mdspbeVars.editOpts[id];
			const fields = editOpts.add.fields;

			if (editOpts === undefined || editOpts.remove === undefined) {
				return;
			}

			const opts = sanitizeValue($('#mdspbe-main-table tfoot tr').find(`select[data-id="${id}"]`).html());

			// Get the raw value
			const args = {
				message: name + ': ' + i18n.remove,
				submitText: i18n.remove,
				type: 'tom-select-multiple',
				selectOpts: opts,
				showMagicSelector: false,
			};
			const formData = await showFormPrompt(args);

			if (formData !== null) {
				formData.id = id;
				removeOptions(formData, function (response) {
					const selectElems = $(`select[data-id="${id}"]`);
					const removedIds = formData['value_1'];
					removedIds.forEach(function (removedId) {
						selectElems.find(`option[value="${removedId}"]`).remove();

						// Update the new-option form data
						Object.keys(fields).forEach(function (key) {
							const field = fields[key];
							if (field.type === 'select' && field.addNewOpts === true) {
								delete mdspbeVars.editOpts[id].add.fields[key].options[removedId];
							}
						});
					});

				});
			}
		});

		// Add new product listener
		$('.js-new-btn').on('click', async function (e) {

			const prodTypes = mdspbeVars.productTypes;

			// Show form
			const args = {
				message: i18n.addProducts,
				submitText: i18n.add,
				htmlCallback: function () {
					let options = '';
					Object.keys(prodTypes).forEach(function (type) {
						options += `<option value="${type}">${prodTypes[type]}</option>`;
					});

					let html = `
						<div class="form-floating mb-3">
							<input type="number" value="1" class="form-control" name="quantity" id="prod-quantity">
							<label for="prod-quantity">${i18n.quantity}</label>
						</div>
						<div class="form-floating mb-3">
							<select class="form-select" name="type" id="prod-type">${options}</select>
							<label for="prod-type">${i18n.type}</label>
						</div>`;
					return html;
				},
				scrollable: true
			};
			const rawData = await showFormPrompt(args);
			if (rawData === null) {
				return;
			}

			// Find the right product prototype
			const prodType = rawData.type;
			const qty = rawData.quantity;

			const $tableBody = $('#mdspbe-main-table tbody');
			const $prototypeTr = $('#mdspbe-main-table tfoot').find(`tr[data-product-type="${prodType}"]`).first();
			if ($prototypeTr.length === 0) {
				return;
			}

			// Add the rows to the top
			const fragment = $(document.createDocumentFragment());
			for (let i = 0; i < qty; i++) {
				// Clone it
				const $newClone = $prototypeTr.clone(true, true);

				// Set a mark
				$newClone.addClass('js-just-added');

				// Enable form validation in all inputs and add the product type 
				$newClone.find('.js-will-required').prop('required', true);
				$newClone.find(`td[data-id="product_type"] span`).text(prodTypes[prodType]);

				// Append it to the fragment
				fragment.append($newClone);
			}
			// Prepend the fragment to the DOM
			$tableBody.prepend(fragment);

			// By triggering change event, we enable all disabled field if they meet the required conditions
			const $inputs = $tableBody.find('tr.js-just-added').find('input, select, textarea');
			$inputs.trigger('change').removeClass('js-just-added');

			// Scroll to top
			$('#mdspbe-main-form .table-responsive').scrollTop(0);

			creatingProds += qty;
			$('.js-save-btn').fadeIn();
		});

		// Save action listener
		$('.js-save-btn').on('click', function (e) {
			const form = document.querySelector('#mdspbe-main-form');
			if (!form.checkValidity()) {
				form.reportValidity();
			} else {
				$('.js-save-btn').fadeOut();

				const newProds = getNewProdsData();
				const data = { create: newProds, update: changes };

				blockUI();
				$.ajax({
					url: mdspbeVars.restUrl + 'mdspbe/v1/update_create',
					method: 'PUT',
					contentType: 'application/json',
					data: JSON.stringify({ data }),
					beforeSend: function (xhr) {
						xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
					},
					success: function (response) {
						processUpdateCreateResponse(response);
						unblockUI(false);
					},
					error: function (error) {
						showAlert(i18n.errorUpdated, 'danger');
						unblockUI();
					}
				});
			}
		});

		// Remove row if creating new product is cancelled
		$('.js-remove-row').on('click', function (e) {
			e.preventDefault();
			const tr = $(this).closest('tr');
			if (tr.attr('data-id') != 0) {
				return;
			}

			tr.remove();
			creatingProds--;
			if (creatingProds === 0 && Object.keys(changes).length === 0) {
				$('.js-save-btn').fadeOut();
			}
		});

		// Open tom select modal
		$(document).on('click', '.js-tom-select-edit', function (e) {
			const input = $(this).closest('td').find('select.js-tom-select');
			const dataId = sanitizeValue(input.attr('data-id'));
			const defaultValue = sanitizeValue(input.val())
			changeInputsValues(input, dataId, defaultValue);
		});

		// Open WP editor
		$(document).on('click', '.js-wp-edit', function (e) {
			$(this).closest('td').find('textarea.js-wp-editor').trigger('dblclick');
		});

		// Set an image
		$(document).on('click', '.js-set-image', function (e) {
			e.preventDefault();

			const $this = $(this);

			// Always create a new media uploader
			const mediaUploader = wp.media({
				multiple: false
			});

			mediaUploader.on('select', function () {
				const attachment = mediaUploader.state().get('selection').first().toJSON();

				const preview = $this.closest('.js-image-preview');
				const input = $this.closest('td').find('input');

				const html = `
					<div class="d-flex flex-column justify-content-center gap-1">
						<span class="cursor-pointer js-set-image dashicons dashicons-edit"></span>
						<span class="cursor-pointer js-remove-image dashicons dashicons-trash"></span>
					</div>
					<img src="${attachment.url}" style="height: 60px; width: auto;" class="cursor-pointer rounded-3 js-set-image">`;

				updateElementHTML(preview, html);
				updateInputValue(input, attachment.id);
			});

			mediaUploader.open();
		});

		// Set an image bulkly
		$(document).on('click', '.js-bulk-set-image', function (e) {
			e.preventDefault();

			const $this = $(this);

			// Always create a new media uploader
			const mediaUploader = wp.media({
				multiple: false
			});

			mediaUploader.on('select', function () {
				const attachment = mediaUploader.state().get('selection').first().toJSON();
				const preview = $this.closest('.js-image-preview');
				const input = $('#daFormPromptModal').find('.js-value-1');
				const src = $('#daFormPromptModal').find('.js-value-src');

				preview.html(`
					<div class="image-container cursor-pointer js-bulk-set-image mx-auto mt-3">
						<img src="${attachment.url}" style="height: 60px; width: auto;" class="rounded-3">
					</div>`);

				input.val(attachment.id);
				src.val(attachment.url);
			});

			mediaUploader.open();
		});

		// Remove an image
		$(document).on('click', '.js-remove-image', function (e) {

			const input = $(this).closest('td').find('input');
			const element = $(this).closest('.js-image-preview');
			const html = '<div class="d-flex flex-column justify-content-center"><span class="cursor-pointer js-set-image dashicons dashicons-plus-alt"></span></div>';

			updateElementHTML(element, html);
			updateInputValue(input, '');
		});

		// Show the bulk image selector
		$('#set-bulk-image').on('change', function () {
			$('.js-bulk-set-image').fadeIn();
		});

		// Hide the bulk image selector
		$('#remove-bulk-image').on('change', function () {
			$('.js-bulk-set-image').fadeOut();
		});

		// Magic selector listener
		$(document).on('change', '.js-magic-selector', function () {
			const value = sanitizeValue($(this).find('option:selected').val());
			const modal = $('#daFormPromptModal');
			if (value === 'clear-out') {
				modal.find('.js-value-1, .js-value-2, .js-round-selector, .js-image-preview, #wp-wp-editor-1-wrap').hide();
			} else if (value === 'replace') {
				modal.find('.js-value-1').attr('placeholder', i18n.replaceThis).show();
				modal.find('.js-value-2').attr('placeholder', i18n.withThis).show();
			} else {
				modal.find('.js-value-1, .js-round-selector').attr('placeholder', '').show();
				modal.find('.js-value-2').hide();
				modal.find('.js-image-preview, #wp-wp-editor-1-wrap').show();
			}
		});

		// Toggle fields modal
		$('.js-toggle-fields').on('click', async function (e) {

			e.preventDefault();
			const args = {
				message: i18n.toggleFields,
				submitText: i18n.save,
				htmlCallback: function () {
					let html = `
						<div class="form-floating mb-3">
							<input type="text" class="form-control" id="toggle-field-search">
							<label for="toggle-field-search">${i18n.search}</label>
						</div>
						<div class="js-toggle-fields-wrapper">`;

					const fields = mdspbeVars.toggleFields;

					fields.forEach(function (field) {
						const checked = field.active ? 'checked' : '';
						html += `
							<div class="js-toggle-field-wrapper">
								<input type="checkbox" class="js-toggle-attribute" id="toggle-${field.id}" name="${field.id}" value="yes" ${checked}>
								<label for="toggle-${field.id}" title="${field.description}">
									${field.name}
								</label>
							</div>`;
					});

					html += '</div>';
					return html;
				},
				scrollable: true
			};
			const rawData = await showFormPrompt(args);
			if (rawData !== null) {
				toggleFields(rawData, async function (response) {
					// Reload the page
					const confirm = await showConfirm(i18n.realoadToApplyChanges);
					if (confirm) {
						blockUI();
						window.location.href = window.location.href;
					}
				});
			}
		});

		// Toggle necesary attributes on / off acordenly to what the toggled attribute needs
		$(document).on('click', '.js-toggle-attribute', function () {
			const attrName = $(this).attr('name');
			const attrActive = $(this).prop('checked');

			const attributes = mdspbeVars.attributes;
			const attrsNeeded = attributes[attrName] && attributes[attrName]['attrs_needed'] ? attributes[attrName]['attrs_needed'] : [];

			attrsNeeded.forEach(function (attrNeeded) {
				if (attrActive) {
					$('#toggle-' + attrNeeded).prop('checked', attrActive);
				} else {
					const secAttrsNeeded = attributes[attrNeeded] && attributes[attrNeeded]['attrs_needed'] ? attributes[attrNeeded]['attrs_needed'] : [];
					if (secAttrsNeeded.includes(attrName)) {
						$('#toggle-' + attrNeeded).prop('checked', attrActive);
					}
				}

			});

		});

		// Toggle field searcher
		$(document).on('input', '#toggle-field-search', function () {

			// Hide all fields
			$('.js-toggle-field-wrapper').hide();

			// Show matching fields
			const search = normalize($(this).val());
			const fieldWrapper = $('.js-toggle-fields-wrapper');

			fieldWrapper.find('label').filter(function () {
				return normalize($(this).text()).includes(search);
			}).closest('.js-toggle-field-wrapper').show();

		});

		// Go to trash
		$('.js-go-to-trash').on('click', function (e) {
			trashSwitch('trash');
		});

		// Back from trash
		$('.js-back-from-trash').on('click', function (e) {
			trashSwitch('');
		});

		function trashSwitch(postStatus) {
			$('#query-search').val('');
			$('#categories').val('all');
			$('#tags').val('all');
			$('#stock-status').val('');
			$('#visibility').val('');
			$('#post-status').val(postStatus);
			$('#atum-supplier').val('');
			$('#orderby').val('ID');
			$('#order').val('DESC');
			$('#query-limit').val('50');

			$('#mdspbe-hidden-filters').find('select').val('');

			$('#post-status').closest('form').submit();
		}

		// Show print tags dialog
		$('.js-create-tags-dialog').on('click', async function (e) {

			e.preventDefault();
			const args = {
				message: i18n.createPrintTags,
				submitText: i18n.create,
				htmlCallback: function () {
					let html = `
						<div class="row g-3 mb-3">
							<div class="col form-floating">
								<select class="form-select" id="tag-product-selection" name="product-selection" required>
									<option value="all" selected>${i18n.allProductsOnPage}</option>
									<option value="selected">${i18n.selectedProducts}</option>
								</select>
								<label for="tag-product-selection">${i18n.productSelection}</label>
							</div>
							
							<div class="col form-floating">
								<input type="number" class="form-control" id="tag-quantity" name="quantity" value="1" min="1" max="100" required>
								<label for="tag-quantity">${i18n.tagQuantity}</label>
							</div>
						</div>
						<div class="row g-3 mb-3">
							<div class="col form-floating">
								<input type="number" class="form-control" id="tag-text-scale" name="scale" value="100" min="0" placeholder="%">
								<label for="tag-text-scale">${i18n.textScale}</label>
							</div>
							<div class="col form-floating">
								<select class="form-select" id="tag-size" name="size" required>
									<option value="50x30" selected>${i18n.s50x30}</option>
									<option value="63x22">${i18n.s63x22}</option>
									<option value="custom">${i18n.customSize}</option>
								</select>
								<label for="tag-size">${i18n.tagSize}</label>
							</div>
						</div>
						<div class="row g-3 mb-3" id="tag-custom-size-cont" style="display: none">
							<div class="col form-floating">
								<input type="number" class="form-control" id="tag-width" name="width">
								<label for="tag-width">${i18n.witdhInMm}</label>
							</div>
							<div class="col form-floating">
								<input type="number" class="form-control" id="tag-height" name="height">
								<label for="tag-height">${i18n.heightInMm}</label>
							</div>
						</div>
						<div class="mb-3">
							<h6>${i18n.fieldsToShow}</h6>
							<div>
								<input type="checkbox" id="tag-show-name" name="show-name" value="yes" checked>
								<label class="form-check-label" for="tag-show-name">
									${i18n.name}
								</label>
							</div>
							<div>
								<input type="checkbox" id="tag-show-sku" name="show-sku" value="yes" checked>
								<label class="form-check-label" for="tag-show-sku">
									${i18n.sku}
								</label>
							</div>
							<div>
								<input type="checkbox" id="tag-show-price" name="show-price" value="yes" checked>
								<label class="form-check-label" for="tag-show-price">
									${i18n.price}
								</label>
							</div>
							<div>
								<input type="checkbox" id="tag-show-global-id" name="show-global-id" value="yes" checked>
								<label class="form-check-label" for="tag-show-global-id">
									${i18n.globalUniqueId}
								</label>
							</div>
							<div>
								<input type="checkbox" id="tag-show-current-date" name="show-current-date" value="yes" checked>
								<label class="form-check-label" for="tag-show-current-date">
									${i18n.currentDate}
								</label>
							</div>
						</div>`;

					return html;
				},
				scrollable: true
			};
			const data = await showFormPrompt(args);
			if (data === null) return;

			// Get products ids
			const productsIds = [];
			if (data['product-selection'] === 'all') {
				const productsTrs = $('#mdspbe-main-table tbody tr');
				productsTrs.each(function () {
					productsIds.push($(this).attr('data-id'));
				});
			} else {
				const checkedCboxes = $('#mdspbe-main-table tbody').find('.js-element-selector:checked');
				checkedCboxes.each(function () {
					productsIds.push($(this).attr('data-id'));
				});
			}

			// Check some products are selected
			if (productsIds.length === 0) {
				showAlert(i18n.noSelectedProducts, 'warning');
				return;
			}

			data.prodsIds = productsIds;

			// Get products data
			getPrintTagData(data, function (productsData) {
				getPrintTagHtml(data, productsData, function (html) {
					printTags(html);
				});
			});

		});


		function getPrintTagHtml(data, productsData, callback = () => { }) {

			const qty = data.quantity ?? 1;
			const size = data.size;
			let width;
			let height;

			if (size === '50x30') {
				width = '50';
				height = '30';
			} else if (size === '63x22') {
				width = '63';
				height = '22';
			} else {
				width = data.width ?? '50';
				height = data.height ?? '30';
			}

			let currentDate = '';
			if (data['show-current-date'] === 'yes') {
				const today = new Date();

				const day = String(today.getDate()).padStart(2, '0');
				const month = String(today.getMonth() + 1).padStart(2, '0');
				const year = String(today.getFullYear()).slice(-2);

				currentDate = `${day}-${month}-${year}`;
			}

			const scale = data.scale;
			const sizeS = parseInt(8 * scale / 100, 10);
			const sizeM = parseInt(12 * scale / 100, 10);
			const sizeL = parseInt(16 * scale / 100, 10);

			let gridItems = '';
			Object.keys(productsData).forEach(function (prodId) {
				const product = productsData[prodId];

				let newItem = '<div class="grid-item">';
				if (product.name) {
					newItem += `<div class="size-m t-bold">${product.name}</div>`;
				}
				if (product.sku) {
					newItem += `<div class="size-s">${product.sku}</div>`;
				}
				if (product.price) {
					newItem += `<div class="size-l t-bold">${product.price}</div>`;
				}

				newItem += `
					<div class="bottom-flex">
						<div class="size-s bottom-left">${product.global_unique_id ?? ''}</div>
						<div class="size-s bottom-right">${currentDate}</div>
					</div>
				</div>`;

				for (var i = 0; i < qty; i++) {
					gridItems += newItem;
				}
			});

			const html = `
				<html>
				<head>
					<style type="text/css" media="print">

					body {
						padding: 5mm;
						box-sizing: border-box;
    					font-family: Verdana, Geneva, sans-serif;
					}

					.grid-container {
						display: grid;
						grid-template-columns: repeat(auto-fill, ${width}mm);
						grid-auto-rows: ${height}mm;
						gap: 0mm;
					}

					.grid-item {
						width: ${width}mm;
						height: ${height}mm;
						padding: 3%;
						gap: 5%;
						box-sizing: border-box;
						border: 1px solid #ccc;
						display: flex;
						flex-direction: column;
						background: white;
						page-break-inside: avoid;
					}
					
					.bottom-flex {
						display: flex;
  						width: 100%;
						margin-top: auto;
					}

					.bottom-left,
					.bottom-right {
						flex: 1 1 auto;
					}

					.bottom-right {
						text-align: right;
					}

					.t-bold {
						font-weight: bold;
					}

					.size-s {
						font-size: ${sizeS}pt;
					}

					.size-m {
						font-size: ${sizeM}pt;
					}

					.size-l {
						font-size: ${sizeL}pt;
					}
					</style>
				</head>
				<body>
					<div class="grid-container">
					${gridItems}
					</div>
				</body>
				</html>
				`;


			callback(html);
		}


		function printTags(html) {

			const a = window.open('', '', 'height=1000, width=1000');
			a.document.write(html);
			a.document.close();
			//if (getDeviceOS() === 'Windows') {
			a.print();
			a.close();
			/*
			} else {
				a.onload = function () {
					a.print();
					// We can't close the popup automatically. 
					// Otherwise we will get an error
				};
			}
			*/
		}


		$(document).on('change', '#tag-size', function () {
			if ($(this).val() === 'custom') {
				$('#tag-custom-size-cont').show();
				$('#tag-width, #tag-height').prop('required', true);
			} else {
				$('#tag-custom-size-cont').hide();
				$('#tag-width, #tag-height').prop('required', false);
			}
		});

		// Ask user for confirmation before leaving
		window.addEventListener('beforeunload', function (e) {

			let unsavedPO = false;
			const poQtyInputs = $('#mdspbe-main-table .js-po-qty');
			poQtyInputs.each(function () {
				if ($(this).val() != '') {
					unsavedPO = true;
				}
			});

			if (unsavedPO || Object.keys(changes).length > 0 || creatingProds > 0) {
				e.preventDefault();
				e.returnValue = '';
			}
		});

		// Change input values bulkly
		async function changeInputsValues(mainInput, dataId, defaultValue = null, isBulk = false) {

			// Check no edit mode
			if (!allowEdit) {
				return;
			}

			const tag = mainInput[0].tagName.toLowerCase();
			const opts = tag === 'select' ? sanitizeValue(mainInput.html()) : '';

			// Get the field type
			let type;
			let attrId = '';
			if (mainInput.hasClass('js-wp-editor')) {
				type = 'wp-editor';
			} else if (mainInput.hasClass('js-tom-select')) {
				attrId = sanitizeValue(mainInput.attr('data-id'));
				if (mainInput.prop('multiple')) {
					type = 'tom-select-multiple';
				} else {
					type = 'tom-select';
				}
			} else if (mainInput.hasClass('js-input-image')) {
				type = 'image';
			} else if (tag === 'input') {
				type = mainInput.attr('type');
			} else if (tag === 'textarea') {
				type = tag;
			} else if (tag === 'select') {
				type = tag;
			}

			// Get the raw value
			const args = {
				message: mainInput.attr('data-name'),
				submitText: i18n.save,
				type: type,
				selectOpts: opts,
				defaultValue: defaultValue,
				attrId: attrId,
				remoteData: mainInput.hasClass('js-remote-data'),
			};
			const rawData = await showFormPrompt(args);
			if (rawData !== null) {

				const selectedTrs = isBulk === true ? $('.js-element-selector:checked').closest('tr') : mainInput.closest('tr');

				if (type === 'wp-editor') {
					selectedTrs.each(function () {
						const tr = $(this);
						const span = tr.find(`[data-id="${dataId}"]`).find(`.js-wp-edit-content`);
						const input = tr.find(`textarea[data-id="${dataId}"]`);
						const newValue = calculateNewValue(sanitizeValue(input.val()), rawData);

						updateElementHTML(span, newValue);
						updateInputValue(input, newValue);
					});
				} else if (type === 'tom-select') {

					const newValue = calculateNewValue(null, rawData);
					const newLabel = sanitizeValue($('#form-prompt .js-value-1').find(`option[value="${newValue}"]`).text());

					selectedTrs.each(function () {
						const tr = $(this);
						const span = tr.find(`[data-id="${dataId}"]`).find(`.js-tom-select-content`);
						const select = tr.find(`select[data-id="${dataId}"]`);
						const currentValue = select.val(); // clone it

						if (select.hasClass('js-remote-data')) {
							if (newValue !== '') { // Option with value=0 is important when we do select.val('')
								updateElementHTML(select, `<option value="0"></option><option value="${newValue}" selected>${newLabel}</option>`);
							} else {
								updateElementHTML(select, '<option value="0"></option>');
							}
						}

						updateElementHTML(span, newLabel);
						updateInputValue(select, newValue, currentValue);
					});
				} else if (type === 'tom-select-multiple') {
					selectedTrs.each(function () {
						const tr = $(this);
						const span = tr.find(`[data-id="${dataId}"]`).find(`.js-tom-select-content`);
						const select = tr.find(`select[data-id="${dataId}"]`);
						const currentValues = [...select.val()]; // clone it
						const newValues = calculateNewValue(select.val(), rawData);

						if (select.hasClass('js-remote-data')) {
							generateDynamicOpts(select, rawData);
						} else {
							updateTomSelectOpts(select, newValues);
						}

						let newLabel = [];
						select.find('option:selected').each(function () {
							const text = $(this).text();
							if (text !== '') {
								newLabel.push(text);
							}
						});
						newLabel = newLabel.join(', ');

						updateElementHTML(span, newLabel);
						updateInputValue(select, newValues, currentValues);

					});
				} else if (type === 'image') {
					selectedTrs.each(function () {
						const tr = $(this);
						const input = tr.find(`td [data-id="${dataId}"]`);
						const preview = tr.find('.js-image-preview');

						const src = rawData['src'];
						const newValue = rawData['value_1'];

						updateInputValue(input, newValue);

						if (src != '') {
							const html = `
								<div class="d-flex flex-column justify-content-center gap-1">
									<span class="cursor-pointer js-set-image dashicons dashicons-edit"></span>
									<span class="cursor-pointer js-remove-image dashicons dashicons-trash"></span>
								</div>
								<img src="${src}" style="height: 60px; width: auto;" class="cursor-pointer rounded-3 js-set-image">`;
							updateElementHTML(preview, html);
						} else {
							const html = '<div class="mt-1"><span class="cursor-pointer js-set-image dashicons dashicons-plus-alt"></span></div>';
							updateElementHTML(preview, html);
						}

					});
				} else {
					const inputs = selectedTrs.find(`td [data-id="${dataId}"]`);
					inputs.each(function () {
						const input = $(this);
						const currentValue = sanitizeValue(input.val());
						const newValue = calculateNewValue(currentValue, rawData);

						updateInputValue(input, newValue);
					});
				}

			}
		}

		// Get new products data (if any)
		function getNewProdsData() {
			const newProds = [];
			const newProdRows = $('#mdspbe-main-table tbody tr[data-id="0"]');
			newProdRows.each(function () {
				const row = $(this);
				const prodData = { prodType: row.attr('data-product-type') };
				const inputs = row.find('input:not(.js-element-selector), select, textarea');
				inputs.each(function () {
					const key = sanitizeValue($(this).attr('data-id'));
					const value = sanitizeValue($(this).val());
					prodData[key] = value;
				});
				newProds.push(prodData);
			});

			return newProds.reverse(); // We reverse it to create products from the bottom to top
		}

		// Highlight the updated properties and other DOM stuff. Also assign ids to the created products
		function processUpdateCreateResponse(response) {

			let withErrors = false;
			const tBody = $('#mdspbe-main-table tbody');

			// Update response
			Object.keys(response.update).forEach(function (prodId) {
				const prodData = response.update[prodId];
				const row = tBody.find(`[data-id='${prodId}']`);
				if (row.length > 0) {
					withErrors = updateProductRows(row, prodId, prodData);
				}
			});

			// Create response
			Object.keys(response.create).forEach(function (prodId) {
				const prodData = response.create[prodId];
				const lastNewProdRow = $('#mdspbe-main-table tbody tr[data-id="0"]').last();
				lastNewProdRow.attr('data-id', prodId).removeClass('table-danger');
				lastNewProdRow.find('.js-remove-row-cont').remove();

				const rowActionsCont = lastNewProdRow.find('.js-row-actions-cont');
				rowActionsCont.show();
				rowActionsCont.find('.js-row-view-cont').attr('href', prodData.permalink);

				const postlink = `${mdspbeVars.adminUrl}post.php?post=${prodId}&action=edit`;
				rowActionsCont.find('.js-row-edit-cont, .js-row-create-variation-cont').attr('href', postlink);

				lastNewProdRow.find('.js-element-selector').attr('data-id', prodId);

				withErrors = updateProductRows(lastNewProdRow, prodId, prodData, true, withErrors);
			});

			// Show notices
			if (withErrors) {
				showAlert(i18n.notAllProdsUpdated, 'warning');
			} else {
				showAlert(i18n.allProdsUpdated, 'success');
			}

		}

		// Update TD elements with the updated input values
		function updateProductRows(row, prodId, prodData, newProduct = false, withErrors = false) {

			const tds = row.find('td');
			tds.each(function () {
				// Update the current input value attribute
				const input = $(this).find('input:not(.js-element-selector, .js-select-all), select, textarea').first();

				const key = sanitizeValue(input.attr('data-id'));
				if (prodData['updated'][key] === true) {

					// If it is an array, we need to convert it into a string to output it properly in the html
					if (Array.isArray(input.val())) {
						const newValue = JSON.stringify(sanitizeValue(input.val())).replace(/"/g, "'");
						input.attr('data-value', sanitizeValue(newValue));
					} else {
						input.attr('data-value', sanitizeValue(input.val()));
					}


					// Update the current input value in the span element
					const tag = input[0].tagName.toLowerCase();
					let valToShow;
					if (tag === 'select') {
						if (input.hasClass('multiple')) {

							valToShow = input.find('option:selected').map(function () {
								return sanitizeValue($(this).text());
							}).get().join(', ');

						} else {
							valToShow = sanitizeValue(input.find('option:selected').text());
						}

					} else {
						valToShow = sanitizeValue(input.val());
					}
					$(this).find('.js-current-label').fadeOut();
					$(this).find('.js-current-label span').text(valToShow);

					$(this).removeClass('table-danger').addClass('table-success');

					// Remove elements from changes object (if not a new one)
					if (changes[prodId] !== undefined && changes[prodId][key] !== undefined) {
						delete changes[prodId][key];
					}

					if (newProduct) {
						creatingProds--;
					}
				} else if (prodData['updated'][key] === false) {
					$(this).addClass('table-danger');
					withErrors = true;
				}

			});

			// Clean-up
			if (changes[prodId] !== undefined && Object.keys(changes[prodId]).length === 0) {
				delete changes[prodId];
			}

			return withErrors;
		}

		// Add options to a select element
		function addNewOption(data, onSucces = () => { }) {

			blockUI();

			$.ajax({
				url: mdspbeVars.restUrl + 'mdspbe/v1/add_option',
				method: 'PUT',
				contentType: 'application/json',
				data: JSON.stringify({ data }),
				beforeSend: function (xhr) {
					xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
				},
				success: function (response) {
					onSucces(response);
					showAlert(i18n.optionAdded, 'success');
					unblockUI();
				},
				error: function (error) {
					showAlert(i18n.errorAddingOpt, 'danger');
					unblockUI();
				}
			});
		}

		// Remove options from a select element
		function removeOptions(data, onSuccces = () => { }) {

			blockUI();

			$.ajax({
				url: mdspbeVars.restUrl + 'mdspbe/v1/remove_options',
				method: 'PUT',
				contentType: 'application/json',
				data: JSON.stringify({ data }),
				beforeSend: function (xhr) {
					xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
				},
				success: function (response) {
					onSuccces(response);
					showAlert(i18n.optionsRemoved, 'success');
					unblockUI();
				},
				error: function (error) {
					showAlert(i18n.errorRemovingOpts, 'danger');
					unblockUI();
				}
			});
		}

		// Toggle fields
		function toggleFields(data, onSuccces = () => { }) {

			blockUI();

			$.ajax({
				url: mdspbeVars.restUrl + 'mdspbe/v1/toggle_fields',
				method: 'PUT',
				contentType: 'application/json',
				data: JSON.stringify({ data }),
				beforeSend: function (xhr) {
					xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
				},
				success: function (response) {
					onSuccces(response);
					unblockUI();
				},
				error: function (error) {
					showAlert(i18n.errorTogglingFields, 'danger');
					unblockUI();
				}
			});
		}

		// Get products data for print tag creation
		function getPrintTagData(data, onSuccces = () => { }) {

			blockUI();

			$.ajax({
				url: mdspbeVars.restUrl + 'mdspbe/v1/get_print_tag_data',
				method: 'POST',
				contentType: 'application/json',
				data: JSON.stringify({ data }),
				beforeSend: function (xhr) {
					xhr.setRequestHeader('X-WP-Nonce', mdspbeVars.nonce);
				},
				success: function (response) {
					onSuccces(response);
					unblockUI();
				},
				error: function (error) {
					showAlert(i18n.errorCreatingTags, 'danger');
					unblockUI();
				}
			});
		}

		// Custom alert
		function showAlert(message, type = "primary", timeout = 10000) {
			const alertContainer = document.getElementById("da-alert-container");

			// Create alert element
			const alert = document.createElement("div");
			const closeBtn = timeout > 0 ? '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : '';
			alert.className = `alert alert-${type} fade show shadow-lg`;
			alert.role = "alert";
			alert.innerHTML = `
				${message}
				${closeBtn}
			`;

			// Append to container
			alertContainer.appendChild(alert);

			// Auto-dismiss after timeout
			if (timeout >= 0) {
				setTimeout(() => {
					alert.classList.remove("show");
					setTimeout(() => alert.remove(), 150); // Wait for fade-out animation
				}, timeout);
			}
		}

		// Form prompt 
		function showFormPrompt(args) {

			return new Promise((resolve) => {

				// Set the modal label
				$('#customFormPromptLabel').text(args.message);
				$('#formPromptSubmit').text(args.submitText);

				const modal = $('#daFormPromptModal');
				const form = $('#form-prompt');

				// Set the form HTML
				const htmlCallback = args.htmlCallback;
				if (htmlCallback !== undefined) {
					form.html(htmlCallback());
				} else {

					const type = args.type;
					const defaultValue = args.defaultValue !== undefined ? args.defaultValue : '';
					const selectOpts = args.selectOpts !== undefined ? args.selectOpts : '';
					const remoteData = args.remoteData !== undefined ? args.remoteData : false;
					const showMagicSelector = args.showMagicSelector !== undefined ? args.showMagicSelector : true;

					form.html(getAttrEditFormHtml(type, selectOpts, remoteData, showMagicSelector));

					// Initialise inputs
					if (type === 'tom-select' || type === 'tom-select-multiple') {
						const attrId = args.attrId ? args.attrId : '';
						document.querySelectorAll('.js-value-1, .js-value-2').forEach((el) => {
							initTomSelect(el, attrId);
						});
					} else if (type === 'select') {
						if (defaultValue) {
							form.find('.js-value-1').val(defaultValue);
						} else {
							form.find('.js-value-1').val('');
						}
					} else if (type === 'wp-editor') {
						initWpEditor(['wp-editor-1']);
						if (defaultValue) {
							tinymce.get('wp-editor-1').setContent(defaultValue);
						}
					} else if (type === 'tom-select') {
						if (defaultValue) {
							form.find('.js-value-1').val(defaultValue);
						} else {
							form.find('.js-value-1').val('');
						}
					} else if (type === 'tom-select-multiple') {
						if (defaultValue) {
							form.find('.js-value-1').val(defaultValue);
						} else {
							form.find('.js-value-1').val([]);
						}
					} else {
						if (defaultValue) {
							form.find('.js-value-1').val(defaultValue);
						}
					}
				}

				// Make modal body scrollable
				if (args.scrollable === true) {
					modal.find('.modal-dialog').addClass('modal-dialog-scrollable');
				} else {
					modal.find('.modal-dialog').removeClass('modal-dialog-scrollable');
				}

				// Show the modal
				modal.modal('show');

				// Submit the form
				function submitPrompt() {

					if (!form[0].checkValidity()) {
						form[0].reportValidity();
					} else {
						if (args.type === 'wp-editor') {
							form.find('.js-value-1').val(getTinymceContent());
						}

						const val12Multiple = args.type === 'tom-select-multiple';
						const formData = arrayToObject(form.serializeArray(), val12Multiple);
						modal.modal('hide');
						resolve(formData);
					}
				}

				// Handle OK button click
				$('#formPromptSubmit').off("click").on("click", submitPrompt);

				// Prevent default form submition
				form.off("submit").on("submit", function (e) {
					e.preventDefault();
					$('#formPromptSubmit').trigger('click');
				});

				// Handle "Enter" key press
				modal.find('.js-magic-selector, .js-value-1, .js-value-2').off("keydown").on("keydown", function (e) {
					if (e.key === "Enter") {
						e.preventDefault(); // Prevent form submission
						submitPrompt();
					}
				});

				// Handle modal close (Cancel button or X)
				$("#daFormPromptModal").off("hidden.bs.modal").on("hidden.bs.modal", function () {
					resolve(null);
				});
			});

		}

		// Custom confirm
		function showConfirm(message) {
			return new Promise((resolve) => {
				const modal = $('#daConfirmModal');
				document.getElementById('daConfirmModalMessage').innerText = message;

				document.getElementById('daConfirmModalYes').onclick = function () {
					resolve(true);
					modal.modal('hide');
				};

				document.querySelector("#daConfirmModal .btn-close, #daConfirmModal .btn-secondary").onclick = function () {
					resolve(false);
					modal.modal('hide');
				};

				modal.modal('show');

				// Handle Enter key press inside the input field
				jQuery("#daConfirmModal").off("keydown").on("keydown", function (e) {
					if (e.key === "Enter") {
						e.preventDefault(); // Prevent form submission
						jQuery('#daConfirmModalYes').trigger('click');
					}
				});
			});
		}

		// Get attribute form edit HTML
		function getAttrEditFormHtml(type, selectOpts = '', remoteData = false, showMagicSelector = true) {

			const remoteDataClass = remoteData === true ? 'js-remote-data' : '';
			let html = '';
			switch (type) {
				case 'text':
					html = `
						${getMagicSelectHtml('text', showMagicSelector)}
						<input type="text" class="form-control rounded-3 shadow-sm mt-3 js-value-1" name="value_1">
						<input type="text" class="form-control rounded-3 shadow-sm mt-3 js-value-2" name="value_2" style="display: none;">`;
					break;
				case 'number':
					html = `
						${getMagicSelectHtml('number', showMagicSelector)}
						<input type="number" class="form-control rounded-3 shadow-sm mt-3 js-value-1" name="value_1" step="0.01">
						${getRoundSelectHtml()}`;
					break;
				case 'date':
					html = `
						${getMagicSelectHtml('date', showMagicSelector)}
						<input type="date" class="form-control rounded-3 shadow-sm mt-3 js-value-1" name="value_1">`;
					break;
				case 'textarea':
					html = `
						${getMagicSelectHtml('text', showMagicSelector)}
						<textarea class="form-control rounded-3 shadow-sm mt-3 js-value-1" name="value_1" rows="10"></textarea>
						<textarea class="form-control rounded-3 shadow-sm mt-3 js-value-2" name="value_2" rows="10" style="display: none;"></textarea>`;
					break;
				case 'wp-editor':
					html = `
						${getMagicSelectHtml('wp-editor', showMagicSelector)}
						<textarea id="wp-editor-1" class="js-value-1" name="value_1" rows="10" hidden></textarea>`;
					break;
				case 'tom-select':
					html = `
						${getMagicSelectHtml(type, showMagicSelector)}
						<select class="rounded-3 shadow-sm mt-3 js-value-1 ${remoteDataClass}" name="value_1">${selectOpts}</select>`;
					break;
				case 'tom-select-multiple':
					html = `
						${getMagicSelectHtml(type, showMagicSelector)}
						<select class="rounded-3 shadow-sm mt-3 js-value-1 ${remoteDataClass}" name="value_1" multiple>${selectOpts}</select>`;
					break;
				case 'select':
					html = `
						<select class="form-select rounded-3 shadow-sm mt-3 js-value-1" name="value_1">
							${selectOpts}
						</select>`;
					break;
				case 'image':
					html = `
					${getMagicSelectHtml('image', showMagicSelector)}
					<div class="js-image-preview">
						<div class="no-image-container cursor-pointer js-bulk-set-image mx-auto mt-3">
							<span class="dashicons dashicons-plus-alt"></span> 
						</div>
					</div>
					<input type="hidden" class="js-value-1" name="value_1">
					<input type="hidden" class="js-value-src" name="src">`;
					break;
			}

			return html;
		}

		// Get the magic select element html
		function getMagicSelectHtml(type, showMagicSelector = true) {

			if (!showMagicSelector) {
				return '';
			}

			let options = '';
			if (type === 'text') {
				options = `
					<option value="set">${i18n.setNew}</option>
					<option value="add-start">${i18n.addAtTheStart}</option>
					<option value="add-end">${i18n.addAtTheEnd}</option>
					<option value="substract">${i18n.substractValue}</option>
					<option value="replace">${i18n.replaceValue}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			} else if (type === 'number') {
				options = `
					<option value="set">${i18n.setAmount}</option>
					<option value="add-perc">${i18n.addPercentage}</option>
					<option value="subs-perc">${i18n.substractPercentage}</option>
					<option value="add-amount">${i18n.addAmount}</option>
					<option value="subs-amount">${i18n.substractAmount}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			} else if (type === 'image' || type === 'date' || type === 'tom-select') {
				options = `
					<option value="set">${i18n.setNew}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			} else if (type === 'multiple_images') {
				options = `
					<option value="set">${i18n.setNew}</option>
					<option value="add-start">${i18n.addAtTheStart}</option>
					<option value="add-end">${i18n.addAtTheEnd}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			} else if (type === 'array' || type === 'tom-select-multiple') {
				options = `
					<option value="set">${i18n.setNew}</option>
					<option value="add">${i18n.addValue}</option>
					<option value="remove">${i18n.removeValue}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			} else if (type === 'wp-editor') {
				options = `
					<option value="set">${i18n.setNew}</option>
					<option value="add-start">${i18n.addAtTheStart}</option>
					<option value="add-end">${i18n.addAtTheEnd}</option>
					<option value="clear-out">${i18n.clearOut}</option>`;
			}

			const html = `
				<select class="rounded-3 shadow-sm w-100 js-magic-selector" name="action">
					${options}
				</select>`;

			return html;
		}

		// Get the round select input html
		function getRoundSelectHtml() {
			return `
				<select class="rounded-3 shadow-sm mt-3 w-100 js-round-selector" name="round_to">
					<option value="0.01">${i18n.roundTo} 0.01</option>
					<option value="0.1">${i18n.roundTo} 0.1</option>
					<option value="" selected>${i18n.doNotRound}</option>
					<option value="10">${i18n.roundTo} 10</option>
					<option value="20">${i18n.roundTo} 20</option>
					<option value="50">${i18n.roundTo} 50</option>
					<option value="100">${i18n.roundTo} 100</option>
					<option value="200">${i18n.roundTo} 200</option>
					<option value="500">${i18n.roundTo} 500</option>
					<option value="1000">${i18n.roundTo} 1000</option>
				</select>`;
		}

		// Block UI
		function blockUI() {
			$('.js-save-btn, .js-new-btn, .js-undo-changes, .js-redo-changes').fadeOut();
			$('#mdspbe-container').addClass('blocked');
			$('.floating-loader').show();
		}

		// Unblock UI
		function unblockUI(maybeShowSaveBtn = true) {
			if (maybeShowSaveBtn) {
				if (Object.keys(changes).length > 0 || creatingProds > 0) {
					$('.js-save-btn').fadeIn();
				}
			}

			$('.js-new-btn, .js-undo-changes, .js-redo-changes').fadeIn();

			$('#mdspbe-container').removeClass('blocked');
			$('.floating-loader').hide();
		}

		// Get tinymce content
		function getTinymceContent() {
			const editor = tinymce.get('wp-editor-1');
			if (editor && !editor.isHidden()) {
				// Visual (TinyMCE) mode
				return editor.getContent();
			} else {
				// Text (HTML) mode
				return sanitizeValue($('#wp-editor-1').val());
			}
		}

		// Calculate new input value 
		function calculateNewValue(currentValue, rawData) {
			const action = rawData.action !== undefined ? rawData.action : 'set';
			let newValue;
			switch (action) {
				case 'set':
					newValue = rawData.value_1;
					break;
				case 'add':
					if (Array.isArray(currentValue)) {
						newValue = currentValue.concat(rawData.value_1);
					} else {
						newValue = rawData.value_1 + currentValue;
					}
					break;
				case 'add-start':
					if (Array.isArray(currentValue)) {
						newValue = currentValue.concat(rawData.value_1);
					} else {
						newValue = rawData.value_1 + currentValue;
					}
					break;
				case 'add-end':
					if (Array.isArray(currentValue)) {
						newValue = rawData.value_1.concat(currentValue);
					} else {
						newValue = currentValue + rawData.value_1;
					}
					break;
				case 'substract':
				case 'remove':
					if (Array.isArray(currentValue)) {
						newValue = currentValue.filter(item => !rawData.value_1.includes(item));
					} else {
						newValue = currentValue.replace(rawData.value_1, '');
					}
					break;
				case 'replace':
					if (Array.isArray(currentValue)) {
						newValue = currentValue.filter(item => !rawData.value_1.includes(item)); // remove old
						newValue = newValue.concat(rawData.value_2); // add new
					} else {
						newValue = currentValue.replace(rawData.value_1, rawData.value_2);
					}
					break;
				case 'add-perc':
					currentValue = currentValue == '' ? 0 : currentValue;
					newValue = parseFloat(currentValue) * (100 + parseFloat(rawData.value_1)) / 100;
					break;
				case 'subs-perc':
					currentValue = currentValue == '' ? 0 : currentValue;
					newValue = parseFloat(currentValue) * (100 - parseFloat(rawData.value_1)) / 100;
					break;
				case 'add-amount':
					currentValue = currentValue == '' ? 0 : currentValue;
					newValue = parseFloat(currentValue) + parseFloat(rawData.value_1);
					break;
				case 'subs-amount':
					currentValue = currentValue == '' ? 0 : currentValue;
					newValue = parseFloat(currentValue) - parseFloat(rawData.value_1);
					break;
				case 'clear-out':
					if (Array.isArray(currentValue)) {
						newValue = [];
					} else {
						newValue = '';
					}
					break;
			}

			if (Number.isFinite(parseFloat(newValue))) {
				const roundTo = rawData['round_to'] ? parseFloat(rawData['round_to']) : null;
				if (roundTo) {
					newValue = roundValue(newValue, roundTo);
				}
			}

			return newValue;
		}

		// Generate dynamic options
		function generateDynamicOpts(select, rawData) {
			const jsValue1 = $('#form-prompt .js-value-1');
			const values = rawData['value_1'];
			const action = rawData.action !== undefined ? rawData.action : 'set';
			let options;

			switch (action) {
				case 'set':
					options = '<option value="0"></option>';
					values.forEach(function (value) {
						const label = sanitizeValue(jsValue1.find(`option[value="${value}"]`).text());
						options += `<option value="${value}" selected>${label}</option>`;
					});
					updateElementHTML(select, options);
					break;

				case 'add':
					options = select.html();
					values.forEach(function (value) {
						if (select.find(`option[value="${value}"]`).length === 0) {
							const label = sanitizeValue(jsValue1.find(`option[value="${value}"]`).text());
							options += `<option value="${value}" selected>${label}</option>`;
						}
					});
					updateElementHTML(select, options);
					break;

				case 'remove':
					options = '';
					select.find('option').each(function () {
						const value = $(this).val();
						if (!values.includes(value)) {
							options += $(this)[0].outerHTML;
						}
					});
					updateElementHTML(select, options);
					break;
			}
		}

		// Update the selected property of tom select underlying options
		function updateTomSelectOpts(select, newValues) {
			let options = '';
			select.find('option').each(function () {
				const value = $(this).val();
				const selected = newValues && newValues.includes(value) ? 'selected' : '';
				options += `<option value="${value}" ${selected}>${$(this).text()}</option>`;
			});

			updateElementHTML(select, options);
		}

		// Update input value while keep a record
		function updateInputValue(input, value, currentValue = null) {

			if (!input.is('input, select, textarea')) {
				return;
			}

			currentValue = currentValue === null ? sanitizeValue(input.val()) : currentValue;
			if (currentValue === value) {
				return;
			}

			if (!timeRecording) {
				setGlobalTimeRecording();
			}

			// Record change
			if (changeRecords[timeRecording] === undefined) {
				changeRecords[timeRecording] = {};
			}
			if (changeRecords[timeRecording]['value'] === undefined) {
				changeRecords[timeRecording]['value'] = [];
			}
			changeRecords[timeRecording]['value'].push({ input, old: currentValue, new: value });

			// Update field
			input.val(value).trigger('change');

		}

		// Update element HTML while keep a record
		function updateElementHTML(element, html) {

			if (!timeRecording) {
				setGlobalTimeRecording();
			}

			const currentHTML = sanitizeValue(element.html());
			if (currentHTML !== html) {

				// Record change
				if (changeRecords[timeRecording] === undefined) {
					changeRecords[timeRecording] = {};
				}
				if (changeRecords[timeRecording]['html'] === undefined) {
					changeRecords[timeRecording]['html'] = [];
				}
				changeRecords[timeRecording]['html'].push({ element, old: currentHTML, new: html });

				// Update field
				element.html(html);
			}

		}

		// Undo - redo changes
		function undoRedoChanges(action = 'undo') {

			// Check no edit mode
			if (!allowEdit) {
				return;
			}

			const recordKeys = Object.keys(changeRecords);
			let recordKey;
			let oldNew;

			if (action === 'undo' && currentRecord > 0) {
				currentRecord--;
				recordKey = recordKeys[currentRecord];
				oldNew = 'old';

				// Enable / disable btns
				if (currentRecord === 0) {
					$('.js-undo-changes').prop('disabled', true);
				}
				$('.js-redo-changes').prop('disabled', false);

			} else if (action === 'redo' && currentRecord < recordKeys.length) {
				currentRecord++;
				recordKey = recordKeys[currentRecord - 1];
				oldNew = 'new';

				// Enable / disable btns
				if (currentRecord === recordKeys.length) {
					$('.js-redo-changes').prop('disabled', true);
				}
				$('.js-undo-changes').prop('disabled', false);

			} else {
				return;
			}

			const record = changeRecords[recordKey];

			//let scrolled = false; // to scroll to the first element of the list

			// Update elements html content
			const htmlChanges = record.html;
			if (htmlChanges !== undefined) {
				htmlChanges.forEach(function (change) {
					const element = change.element;
					const html = change[oldNew];
					element.html(html);
				});
			}

			// Update inputs
			const valueChanges = record.value;
			if (valueChanges !== undefined) {
				valueChanges.forEach(function (change) {

					const input = change.input;
					const value = change[oldNew];

					/*
					// Jump to the input position
					if (!scrolled) {
						scrolled = true;
	
						const container = $('#mdspbe-main-form .table-responsive');
						const inputPosition = input.position();
	
						container.scrollTop(inputPosition.top);
						container.scrollLeft(inputPosition.left);
					}
					*/

					// Trigger the changes now so it can be seen by the user
					input.val(value).trigger('change');
				});
			}

		}

		// Set global timestamp in order to group all the changes that were made together
		function setGlobalTimeRecording() {

			maybeRemoveLastRecords();

			currentRecord++;
			timeRecording = Date.now();
			setTimeout(() => {
				timeRecording = null;
			}, 200);

			// Enable / disable btns
			$('.js-undo-changes').prop('disabled', false);
			$('.js-redo-changes').prop('disabled', true);
		}

		// Maybe remove the last change records
		function maybeRemoveLastRecords() {
			// This means undo btn was pressed to we need to clean the last records
			const recordKeys = Object.keys(changeRecords);
			if (recordKeys.length > currentRecord) {
				const k = recordKeys.length - currentRecord;
				let i = 1;
				while (i <= k) {
					const recordKey = recordKeys[recordKeys.length - i];
					delete changeRecords[recordKey];
					i++;
				}
			}
		}

		// Round num to specific precision or to nearest multiple
		function roundValue(num, roundTo) {
			roundTo = Number.isFinite(roundTo) ? roundTo : 1;

			switch (roundTo) {
				case 0.01:
					num = Math.round(num * 100) / 100;
					break;
				case 0.1:
					num = Math.round(num * 10) / 10;
					break;
				case 1:
					num = Math.round(num);
					break;
				default:
					const allowedMultiples = [5, 10, 20, 50, 100, 200, 500, 1000];
					if (allowedMultiples.includes(roundTo)) {
						num = Math.round(num / roundTo) * roundTo;
					}
			}

			return num;
		}

		// Array to object
		function arrayToObject(array, value12Multiple = false) {
			const obj = array.reduce((acc, { name, value }) => {
				if (value12Multiple && ['value_1', 'value_2'].includes(name)) {
					if (!acc[name]) {
						acc[name] = [];
					}
					acc[name].push(value);
				} else {
					acc[name] = value;
				}

				return acc;
			}, {});

			return obj;
		}

		// Normalize text
		function normalize(str) {
			return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
		}

		// Compare values of 2 arrays
		function areArrayEqual(a, b) {
			return a.length === b.length && a.sort().join(',') === b.sort().join(',');
		}

		// Sanitize value
		function sanitizeValue(value) {
			if (typeof value === 'string') {
				return DOMPurify.sanitize(value);
			} else if (Array.isArray(value)) {
				return value.map(sanitizeValue); // recursively sanitize each item
			} else if (value !== null && typeof value === 'object') {
				const sanitizedObject = {};
				for (const key in value) {
					if (value.hasOwnProperty(key)) {
						sanitizedObject[key] = sanitizeValue(value[key]); // recursive call
					}
				}
				return sanitizedObject;
			}
			return value; // for numbers, booleans, null, undefined, etc.
		}


		// Make main table columns resizable
		document.querySelectorAll('.resizer').forEach(resizer => {
			let startX, startWidth, th;

			const mouseMoveHandler = function (e) {
				const newWidth = startWidth + (e.pageX - startX);
				th.style.width = newWidth + 'px';
			};

			const mouseUpHandler = function () {
				document.removeEventListener('mousemove', mouseMoveHandler);
				document.removeEventListener('mouseup', mouseUpHandler);
				document.body.classList.remove('resizing');
			};

			const mouseDownHandler = function (e) {
				th = resizer.parentElement;
				startX = e.pageX;
				startWidth = th.offsetWidth;
				document.addEventListener('mousemove', mouseMoveHandler);
				document.addEventListener('mouseup', mouseUpHandler);
				document.body.classList.add('resizing');
			};

			resizer.addEventListener('mousedown', mouseDownHandler);
		});

	});

})(jQuery);