Newer
Older
gnexus-ui-kit / src / vue / components / GnToastProvider.js
import { defineComponent, h, provide, ref, nextTick } from "vue";
import { cx, iconNode, normalizeVariant } from "../utils.js";
import { toastKey } from "../composables/toast-context.js";

const iconByVariant = {
	info: "ph-info",
	success: "ph-check-circle",
	warning: "ph-warning",
	danger: "ph-warning-octagon",
	error: "ph-warning-octagon",
	primary: "ph-info",
	secondary: "ph-info"
};

export default defineComponent({
	name: "GnToastProvider",
	props: {
		lifetime: { type: Number, default: 4000 }
	},
	setup(props, { slots, expose }) {
		const toast = ref(null);
		const closing = ref(false);
		const showing = ref(false);
		let timer = null;
		let closeTimer = null;
		let progressTimer = null;
		const progress = ref(100);

		const dismiss = () => {
			window.clearTimeout(closeTimer);
			window.clearInterval(progressTimer);
			closing.value = true;
			showing.value = false;
			closeTimer = window.setTimeout(() => {
				toast.value = null;
				closing.value = false;
				progress.value = 100;
				window.clearTimeout(timer);
				timer = null;
			}, 300);
		};

		const close = () => {
			window.clearTimeout(closeTimer);
			window.clearTimeout(timer);
			window.clearInterval(progressTimer);
			closing.value = false;
			showing.value = false;
			progress.value = 100;
			toast.value = null;
		};

		const show = options => {
			window.clearTimeout(closeTimer);
			window.clearInterval(progressTimer);
			closing.value = false;
			showing.value = false;
			progress.value = 100;
			const variant = normalizeVariant(options.variant || options.type || "info", "info");
			const lifetime = options.lifetime !== undefined ? options.lifetime : props.lifetime;
			toast.value = {
				id: Date.now(),
				variant: variant === "error" ? "danger" : variant,
				title: options.title || "",
				text: options.text || options.message || "",
				icon: options.icon || iconByVariant[variant] || iconByVariant.info,
				lifetime
			};

			window.clearTimeout(timer);

			if(lifetime !== 0) {
				const step = 100;
				const totalSteps = lifetime / step;
				progress.value = 100;
				progressTimer = window.setInterval(() => {
					progress.value -= 100 / totalSteps;
					if(progress.value <= 0) {
						window.clearInterval(progressTimer);
					}
				}, step);

				timer = window.setTimeout(dismiss, lifetime);
			}

			nextTick(() => {
				requestAnimationFrame(() => {
					showing.value = true;
				});
			});
		};

		const api = {
			show,
			close,
			info: options => show({ ...options, variant: "info" }),
			success: options => show({ ...options, variant: "success" }),
			warning: options => show({ ...options, variant: "warning" }),
			danger: options => show({ ...options, variant: "danger" }),
			error: options => show({ ...options, variant: "danger" })
		};

		provide(toastKey, api);
		expose(api);

		const toastClass = () => {
			if (closing.value) return "a-hide";
			if (showing.value) return "a-show";
			return "";
		};

		return () => [
			slots.default?.(),
			toast.value && h("div", {
				class: cx("toast", toastClass(), `toast-${toast.value.variant}`),
				role: "alert"
			}, [
				h("div", { class: "toast-content" }, [
					h("div", { class: "toast-header" }, [
						iconNode(toast.value.icon),
						toast.value.title
					]),
					toast.value.text && h("p", { class: "toast-text" }, toast.value.text)
				]),
				h("button", {
					class: "btn-icon toast-close",
					type: "button",
					"aria-label": "Close",
					onClick: dismiss
				}, [iconNode("ph-x")]),
				toast.value.lifetime !== 0 && h("div", { class: "toast-progress" }, [
					h("div", {
						class: "toast-progress-bar",
						style: { transform: `scaleX(${Math.max(0, progress.value / 100)})` }
					})
				])
			])
		];
	}
});