import { defineComponent, h, ref, computed } from "vue";
import { cx } from "../utils.js";
export default defineComponent({
name: "GnTagInput",
inheritAttrs: false,
props: {
modelValue: { type: Array, default: () => [] },
label: { type: String, default: "" },
placeholder: { type: String, default: "Add item…" },
help: { type: String, default: "" },
disabled: { type: Boolean, default: false },
separator: { type: String, default: "," },
unique: { type: Boolean, default: true },
maxItems: { type: Number, default: 0 }
},
emits: ["update:modelValue", "add", "remove"],
setup(props, { emit, attrs, slots }) {
const inputRef = ref(null);
const focused = ref(false);
const rawValue = ref("");
const canAdd = computed(() => {
if(props.disabled) return false;
if(props.maxItems > 0 && props.modelValue.length >= props.maxItems) return false;
return true;
});
function normalizeText(text) {
return text.trim().replace(/\s+/g, " ");
}
function addValue(text) {
const value = normalizeText(text);
if(!value) return;
if(props.unique && props.modelValue.includes(value)) return;
if(props.maxItems > 0 && props.modelValue.length >= props.maxItems) return;
const next = [...props.modelValue, value];
emit("update:modelValue", next);
emit("add", value);
}
function removeValue(index) {
const removed = props.modelValue[index];
const next = props.modelValue.filter((_, i) => i !== index);
emit("update:modelValue", next);
emit("remove", removed);
}
function onKeydown(event) {
if(event.key === "Enter") {
event.preventDefault();
if(rawValue.value) {
const parts = props.separator
? rawValue.value.split(props.separator).map(s => s.trim()).filter(Boolean)
: [rawValue.value];
parts.forEach(addValue);
rawValue.value = "";
}
return;
}
if(event.key === "Backspace" && !rawValue.value && props.modelValue.length > 0) {
removeValue(props.modelValue.length - 1);
return;
}
}
function onPaste(event) {
const paste = event.clipboardData.getData("text");
if(!paste || !props.separator) return;
event.preventDefault();
const parts = paste.split(props.separator).map(s => s.trim()).filter(Boolean);
const appended = [];
for(const part of parts) {
const v = normalizeText(part);
if(!v) continue;
if(props.unique && props.modelValue.includes(v)) continue;
if(props.maxItems > 0 && props.modelValue.length + appended.length >= props.maxItems) break;
appended.push(v);
}
if(appended.length) {
const next = [...props.modelValue, ...appended];
emit("update:modelValue", next);
appended.forEach(v => emit("add", v));
}
}
function onWrapClick() {
inputRef.value?.focus();
}
return () => {
const labelNode = props.label || slots.label
? h("label", { class: "label" }, [
slots.label?.() || props.label,
props.disabled && h("span", { class: "label-disabled-hint" }, "Disabled")
])
: null;
const chips = props.modelValue.map((item, index) =>
h("span", {
class: cx("chip", "chip-secondary"),
key: `${item}-${index}`
}, [
item,
!props.disabled && h("button", {
class: "chip-remove",
type: "button",
"aria-label": `Remove ${item}`,
onClick: (e) => {
e.stopPropagation();
removeValue(index);
}
}, [h("i", { class: "ph ph-x" })])
])
);
const field = h("input", {
ref: inputRef,
class: "tag-input-field",
type: "text",
placeholder: canAdd.value ? props.placeholder : "",
value: rawValue.value,
disabled: props.disabled,
onInput: (e) => { rawValue.value = e.target.value; },
onKeydown,
onPaste,
onFocus: () => focused.value = true,
onBlur: () => focused.value = false
});
const wrap = h("div", {
class: cx("tag-input-wrap"),
onClick: onWrapClick
}, [
...chips,
field
]);
const meta = props.help
? h("div", { class: "input-info" }, [
h("i", { class: "ph ph-info" }),
" " + props.help
])
: null;
return h("div", {
...attrs,
class: cx("tag-input", {
"tag-input-focused": focused.value,
"tag-input-disabled": props.disabled
}, attrs.class)
}, [
labelNode,
wrap,
meta
]);
};
}
});