Newer
Older
gnexus-ui-kit / src / vue / components / GnRepeater.js
import { defineComponent, h, computed } from "vue";
import { cx, iconNode } from "../utils.js";

let _id = 0;
function nextId() {
	return `repeater-item-${++_id}`;
}

export default defineComponent({
	name: "GnRepeater",
	inheritAttrs: false,
	props: {
		modelValue: { type: Array, default: () => [] },
		label: { type: String, default: "Items" },
		addLabel: { type: String, default: "Add" },
		addIcon: { type: String, default: "ph-plus" },
		removeLabel: { type: String, default: "Remove" },
		removeIcon: { type: String, default: "ph-trash" },
		minItems: { type: Number, default: 1 },
		maxItems: { type: Number, default: 0 },
		itemFactory: {
			type: Function,
			default: () => () => ({ id: nextId() })
		},
		disabled: { type: Boolean, default: false }
	},
	emits: ["update:modelValue", "add", "remove"],
	setup(props, { attrs, emit, slots }) {
		const canAdd = computed(() => {
			if(props.disabled) return false;
			if(props.maxItems > 0 && props.modelValue.length >= props.maxItems) return false;
			return true;
		});

		function canRemove(index) {
			if(props.disabled) return false;
			if(props.modelValue.length <= props.minItems) return false;
			return true;
		}

		function add() {
			if(!canAdd.value) return;
			const item = props.itemFactory();
			const next = [...props.modelValue, item];
			emit("update:modelValue", next);
			emit("add", item);
		}

		function remove(index) {
			if(!canRemove(index)) return;
			const removed = props.modelValue[index];
			const next = props.modelValue.filter((_, i) => i !== index);
			emit("update:modelValue", next);
			emit("remove", removed);
		}

		return () => {
			const headerSlot = slots.header?.({ add, canAdd: canAdd.value });
			const header = headerSlot || h("div", { class: "repeater-header" }, [
				h("span", { class: "repeater-title" }, props.label),
				h("button", {
					type: "button",
					class: cx("btn", "btn-secondary", "btn-small", "with-icon"),
					disabled: !canAdd.value,
					onClick: add
				}, [
					iconNode(props.addIcon),
					props.addLabel
				])
			]);

			const items = props.modelValue.map((item, index) => {
				const itemKey = item?.id ?? item?.key ?? index;
				const removable = canRemove(index);
				const scope = { item, index, remove: () => remove(index), canRemove: removable };

				const body = slots.item?.(scope);
				const actions = slots.actions?.(scope);

				const defaultActions = !slots.actions && h("button", {
					type: "button",
					class: cx("btn-icon", "btn-icon-sm"),
					"aria-label": props.removeLabel,
					disabled: !removable,
					onClick: (e) => {
						e.stopPropagation();
						remove(index);
					}
				}, [iconNode(props.removeIcon)]);

				return h("div", {
					class: "repeater-item",
					key: itemKey
				}, [
					h("div", { class: "repeater-item-body" }, body),
					(actions || defaultActions) && h("div", { class: "repeater-item-actions" }, actions || [defaultActions])
				]);
			});

			return h("div", {
				...attrs,
				class: cx("repeater", { "repeater-disabled": props.disabled }, attrs.class)
			}, [
				header,
				h("div", { class: "repeater-list" }, items)
			]);
		};
	}
});