Newer
Older
gnexus-ui-kit / src / js / components / input-patterns.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 13 hours ago 5 KB Add date time form controls
const initializedRoots = new WeakSet();
const fileUploadState = new WeakMap();

function getFileKey(file) {
	return `${file.name}:${file.size}:${file.lastModified}`;
}

function clearFilePreviews(previewNode) {
	if(!previewNode) {
		return;
	}

	previewNode.querySelectorAll("img[data-object-url]").forEach(image => {
		URL.revokeObjectURL(image.dataset.objectUrl);
	});
	previewNode.innerHTML = "";
	previewNode.hidden = true;
}

function getStoredFiles(input) {
	return fileUploadState.get(input) || [];
}

function setStoredFiles(input, files) {
	fileUploadState.set(input, files);

	const transfer = new DataTransfer();
	files.forEach(file => transfer.items.add(file));
	input.files = transfer.files;
}

function addStoredFiles(input, files) {
	const storedFiles = getStoredFiles(input);
	const knownKeys = new Set(storedFiles.map(getFileKey));
	const nextFiles = [...storedFiles];

	files.forEach(file => {
		const key = getFileKey(file);

		if(!knownKeys.has(key)) {
			knownKeys.add(key);
			nextFiles.push(file);
		}
	});

	setStoredFiles(input, nextFiles);
	return nextFiles;
}

function removeStoredFile(input, index) {
	const nextFiles = getStoredFiles(input).filter((file, fileIndex) => fileIndex !== index);
	setStoredFiles(input, nextFiles);
	return nextFiles;
}

function getFileType(file) {
	const nameParts = file.name.split(".");
	const extension = nameParts.length > 1 ? nameParts.pop().trim() : "";

	if(extension) {
		return extension.slice(0, 6).toUpperCase();
	}

	if(file.type) {
		return file.type.split("/").pop().slice(0, 6).toUpperCase();
	}

	return "FILE";
}

function formatBytes(bytes) {
	if(!Number.isFinite(bytes)) {
		return "";
	}

	if(bytes === 0) {
		return "0 B";
	}

	const units = ["B", "KB", "MB", "GB"];
	const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
	const value = bytes / Math.pow(1024, index);

	return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}

function updateFileUpload(input) {
	const container = input.closest(".file-upload-panel, .file-upload");
	const previewNode = container?.querySelector("[data-file-upload-preview]");

	if(!container || !previewNode) {
		return;
	}

	const files = getStoredFiles(input);

	if(!files.length) {
		clearFilePreviews(previewNode);
		return;
	}

	updateFilePreviews(previewNode, files);
}

function updateFilePreviews(previewNode, files) {
	if(!previewNode) {
		return;
	}

	clearFilePreviews(previewNode);

	files.forEach((file, index) => {
		const figure = document.createElement("figure");
		figure.className = "file-upload-preview-item";
		figure.dataset.fileUploadIndex = String(index);

		const preview = document.createElement("div");
		preview.className = "file-upload-preview-visual";

		if(file.type.startsWith("image/")) {
			const image = document.createElement("img");
			const objectUrl = URL.createObjectURL(file);
			image.src = objectUrl;
			image.dataset.objectUrl = objectUrl;
			image.alt = "";
			image.loading = "lazy";
			preview.append(image);
		} else {
			const type = document.createElement("span");
			type.className = "file-upload-preview-type";
			type.textContent = getFileType(file);
			preview.append(type);
		}

		const caption = document.createElement("figcaption");

		const name = document.createElement("span");
		name.className = "file-upload-preview-name";
		name.textContent = file.name;

		const meta = document.createElement("span");
		meta.className = "file-upload-preview-meta";
		meta.textContent = `${getFileType(file)} / ${formatBytes(file.size)}`;

		const remove = document.createElement("button");
		remove.className = "file-upload-preview-remove";
		remove.type = "button";
		remove.dataset.fileUploadRemove = String(index);
		remove.setAttribute("aria-label", `Remove ${file.name}`);
		remove.innerHTML = `<i class="ph ph-x"></i>`;

		caption.append(name, meta);
		figure.append(remove, preview, caption);
		previewNode.append(figure);
	});

	previewNode.hidden = false;
}

function init(root = document) {
	if(initializedRoots.has(root)) {
		return;
	}

	root.addEventListener("click", event => {
		const clearButton = event.target.closest("[data-input-clear]");

		if(!clearButton) {
			return;
		}

		const group = clearButton.closest(".input-group");
		const input = group?.querySelector("input, textarea");

		if(!input) {
			return;
		}

		input.value = "";
		input.dispatchEvent(new Event("input", { bubbles: true }));
		input.focus();
	});

	root.addEventListener("click", event => {
		const removeButton = event.target.closest("[data-file-upload-remove]");

		if(!removeButton) {
			return;
		}

		const container = removeButton.closest(".file-upload-panel, .file-upload");
		const input = container?.querySelector("[data-file-upload-input]");

		if(!input) {
			return;
		}

		removeStoredFile(input, Number(removeButton.dataset.fileUploadRemove));
		updateFileUpload(input);
		input.dispatchEvent(new Event("change", { bubbles: true }));
	});

	root.addEventListener("click", event => {
		const input = event.target.closest("[data-date-picker]");

		if(!input) {
			return;
		}

		input.focus();

		if(typeof input.showPicker === "function") {
			try {
				input.showPicker();
			} catch(error) {
				// Some browsers restrict showPicker() to direct user gestures or supported input types.
			}
		}
	});

	root.addEventListener("change", event => {
		const input = event.target.closest("[data-file-upload-input]");

		if(!input) {
			return;
		}

		addStoredFiles(input, Array.from(input.files || []));
		updateFileUpload(input);
	});

	root.addEventListener("reset", event => {
		const form = event.target.closest("form");

		if(!form) {
			return;
		}

		setTimeout(() => {
			form.querySelectorAll("[data-file-upload-input]").forEach(input => {
				setStoredFiles(input, []);
				updateFileUpload(input);
			});
		}, 0);
	});

	initializedRoots.add(root);
}

export default {
	init,
	updateFileUpload
};