Newer
Older
vmk-ui-kit / src / vue / utils.js
import { h } from "vue";
import VmkIcon from "./components/Icon.js";

export const variants = new Set([
	"primary",
	"secondary",
	"accent",
	"success",
	"warning",
	"danger",
	"error",
	"info"
]);

export function cx(...items) {
	return items
		.flatMap(item => {
			if (!item) {
				return [];
			}

			if (Array.isArray(item)) {
				return item;
			}

			if (typeof item === "object") {
				return Object.entries(item)
					.filter(([, enabled]) => enabled)
					.map(([name]) => name);
			}

			return [item];
		})
		.filter(Boolean)
		.join(" ");
}

export function normalizeVariant(value, fallback = "primary") {
	return variants.has(value) ? value : fallback;
}

const SIZE_MAP = {
	xxs: "btn-xxs",
	xs: "btn-xs",
	sm: "btn-sm",
	md: "btn-md",
	lg: "btn-lg"
};

export function normalizeSize(value, fallback = "btn-md") {
	return SIZE_MAP[value] || fallback;
}

export const inputVariants = new Set(["error", "success", "warning"]);

export function normalizeInputState(value, fallback = "") {
	return inputVariants.has(value) ? value : fallback;
}

export function eventValue(event) {
	const target = event.target;
	if (target.type === "checkbox") {
		return target.checked;
	}
	return target.value;
}

/**
 * Keep keyboard focus inside a modal/drawer container when Tab is pressed.
 */
export function trapFocus(event, container) {
	if (!container || event.key !== "Tab") return;

	const focusable = Array.from(
		container.querySelectorAll(
			'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
		)
	).filter(el => el.offsetParent !== null);

	if (focusable.length === 0) {
		event.preventDefault();
		return;
	}

	const first = focusable[0];
	const last = focusable[focusable.length - 1];
	const active = document.activeElement;

	if (event.shiftKey) {
		if (active === first || !container.contains(active)) {
			event.preventDefault();
			last.focus();
		}
	} else {
		if (active === last || !container.contains(active)) {
			event.preventDefault();
			first.focus();
		}
	}
}

const PHOSPHOR_TO_VMK = {
	spinner: "essentials/loading",
	loader: "essentials/loading",
	loading: "essentials/loading",
	"arrow-right": "arrows/arrow/right",
	"arrow-left": "arrows/arrow/left",
	"arrow-up": "arrows/arrow/up",
	"arrow-down": "arrows/arrow/down",
	"chevron-right": "arrows/chevron/right",
	"chevron-left": "arrows/chevron/left",
	"chevron-up": "arrows/chevron/up",
	"chevron-down": "arrows/chevron/down",
	"magnifying-glass": "essentials/search",
	search: "essentials/search",
	"warning-circle": "essentials/alert/circle",
	plus: "essentials/plus",
	minus: "essentials/minus",
	x: "essentials/close-cross",
	close: "essentials/close-cross",
	check: "essentials/check",
	user: "essentials/user",
	trash: "essentials/trash",
	information: "essentials/information/circle",
	info: "essentials/information/circle",
	alert: "essentials/alert-sign",
	warning: "essentials/alert-sign",
	danger: "essentials/alert/circle",
	error: "essentials/alert/circle",
	success: "essentials/check/circle"
};

const PHOSPHOR_PREFIX_RE = /^ph[-\s]/;

export function iconNode(icon, extraClass = "") {
	if (!icon) {
		return null;
	}

	let name = icon.trim();

	// Accept legacy Phosphor prefixes (ph-plus, ph bold ph-spinner, etc).
	if (PHOSPHOR_PREFIX_RE.test(name)) {
		name = name.slice(3).trim();
	}

	// If the name already looks like a VMK key, use it directly.
	if (!name.includes("/")) {
		name = PHOSPHOR_TO_VMK[name] || `essentials/${name}`;
	}

	return h(VmkIcon, {
		name,
		class: extraClass
	});
}