0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-22 08:58:11 +00:00

stable with basic suggestion

This commit is contained in:
Víctor Losada Hernández
2026-02-15 14:43:52 +01:00
parent d0119f351f
commit 46d9322b50
3 changed files with 149 additions and 82 deletions

View File

@@ -114,6 +114,11 @@
z-index : 200;
max-width : 150px;
}
&.tags .tagInput-dropdown {
z-index : 201;
max-width : 150px;
}
}

View File

@@ -1,100 +1,161 @@
import './tagInput.less';
import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import "./tagInput.less";
import React, { useState, useEffect, useMemo } from "react";
import Combobox from "../../../components/combobox.jsx";
const TagInput = ({ unique = true, values = [], ...props })=>{
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
import tagSuggestionList from "./tagSuggestionList.js";
useEffect(()=>{
handleChange(tagList.map((context)=>context.value));
const TagInput = ({ label, unique = true, values = [], placeholder = "", onChange }) => {
const [tagList, setTagList] = useState(
values.map((value) => ({
value,
display: value.trim(),
editing: false,
})),
);
// Keep in sync if parent updates values
useEffect(() => {
const incoming = values || [];
const current = tagList.map((t) => t.value);
const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]);
if (changed) {
setTagList(
incoming.map((value) => ({
value,
display: value.trim(),
editing: false,
})),
);
}
}, [values]);
// Emit changes upward
useEffect(() => {
onChange?.({
target: { value: tagList.map((t) => t.value) },
});
}, [tagList]);
const handleChange = (value)=>{
props.onChange({
target : { value }
// Canonical duplicate groups
const duplicateGroups = [
["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"],
["P2e", "p2e", "P2E", "Pathfinder 2e"],
];
const normalizeValue = (input) => {
const group = duplicateGroups.find((grp) => grp.some((tag) => tag.toLowerCase() === input.toLowerCase()));
return group ? group[0] : input;
};
const regexPattern = /^[-A-Za-z0-9&_.()]+$/;
const submitTag = (newValue, index = null) => {
const trimmed = newValue?.trim();
if (!trimmed) return;
if (!regexPattern.test(trimmed)) return;
const canonical = normalizeValue(trimmed);
setTagList((prev) => {
const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === canonical.toLowerCase());
if (unique && existsIndex !== -1) return prev;
if (index !== null) {
return prev.map((t, i) =>
i === index ? { ...t, value: canonical, display: canonical, editing: false } : t,
);
}
return [...prev, { value: canonical, display: canonical, editing: false }];
});
};
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{
if(_.includes(['Enter', ','], evt.key)) {
evt.preventDefault();
submitTag(evt.target.value, value, index);
if(options.clear) {
setTempInputText('');
}
}
const removeTag = (index) => {
setTagList((prev) => prev.filter((_, i) => i !== index));
};
const submitTag = (newValue, originalValue, index)=>{
setTagList((prevContext)=>{
// remove existing tag
if(newValue === null){
return [...prevContext].filter((context, i)=>i !== index);
}
// add new tag
if(originalValue === null){
return [...prevContext, { value: newValue, editing: false }];
}
// update existing tag
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, value: newValue, editing: false };
}
return context;
});
});
const editTag = (index) => {
setTagList((prev) => prev.map((t, i) => ({ ...t, editing: i === index })));
};
const editTag = (index)=>{
setTagList((prevContext)=>{
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
};
const renderReadTag = (context, index)=>{
return (
<li key={index}
data-value={context.value}
className='tag'
onClick={()=>editTag(index)}>
{context.value}
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
</li>
);
};
const renderWriteTag = (context, index)=>{
return (
<input type='text'
key={index}
defaultValue={context.value}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
autoFocus
/>
);
};
const suggestionOptions = tagSuggestionList.map((tag) => (
<div
className="item"
key={`tag-${tag}`} // unique key
value={tag}
data={tag}
title={tag}>
{tag}
</div>
));
return (
<div className='field'>
<label>{props.label}</label>
<div className='value'>
<ul className='list'>
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
<div className="field tags">
{label && <label>{label}</label>}
<div className="value">
<ul className="list">
{tagList.map((t, i) =>
t.editing ? (
<input
key={i}
type="text"
value={t.display}
pattern="[-A-Za-z0-9&_.()]+"
onChange={(e) => {
const val = e.target.value;
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, display: val } : tag)),
);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitTag(e.target.value, i);
}
}}
autoFocus
/>
) : (
<li key={i} className="tag" onClick={() => editTag(i)}>
{t.display}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
<i className="fa fa-times fa-fw" />
</button>
</li>
),
)}
</ul>
<input
type='text'
className='value'
placeholder={props.placeholder}
value={tempInputText}
onChange={(e)=>setTempInputText(e.target.value)}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
<Combobox
trigger="click"
className="tagInput-dropdown"
default=""
placeholder={placeholder}
options={suggestionOptions}
autoSuggest={{
suggestMethod: "startsWith",
clearAutoSuggestOnClick: true,
filterOn: ["value", "title"],
}}
onSelect={(value) => {
submitTag(value, null);
}}
onEntry={(e) => {
// Allow free typing + Enter
if (e.key === "Enter") {
e.preventDefault();
submitTag(e.target.value, null);
}
}}
/>
</div>
</div>

View File

@@ -0,0 +1 @@
export default ["D&D", "P2e", "VtM", "CoC"];