0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-26 21:18:12 +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; z-index : 200;
max-width : 150px; max-width : 150px;
} }
&.tags .tagInput-dropdown {
z-index : 201;
max-width : 150px;
}
} }

View File

@@ -1,100 +1,161 @@
import './tagInput.less'; import "./tagInput.less";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from "react";
import _ from 'lodash'; import Combobox from "../../../components/combobox.jsx";
const TagInput = ({ unique = true, values = [], ...props })=>{ import tagSuggestionList from "./tagSuggestionList.js";
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
useEffect(()=>{ const TagInput = ({ label, unique = true, values = [], placeholder = "", onChange }) => {
handleChange(tagList.map((context)=>context.value)); 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]); }, [tagList]);
const handleChange = (value)=>{ // Canonical duplicate groups
props.onChange({ const duplicateGroups = [
target : { value } ["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 = {} })=>{ const removeTag = (index) => {
if(_.includes(['Enter', ','], evt.key)) { setTagList((prev) => prev.filter((_, i) => i !== index));
evt.preventDefault();
submitTag(evt.target.value, value, index);
if(options.clear) {
setTempInputText('');
}
}
}; };
const submitTag = (newValue, originalValue, index)=>{ const editTag = (index) => {
setTagList((prevContext)=>{ setTagList((prev) => prev.map((t, i) => ({ ...t, editing: i === index })));
// 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)=>{ const suggestionOptions = tagSuggestionList.map((tag) => (
setTagList((prevContext)=>{ <div
return prevContext.map((context, i)=>{ className="item"
if(i === index) { key={`tag-${tag}`} // unique key
return { ...context, editing: true }; value={tag}
} data={tag}
return { ...context, editing: false }; title={tag}>
}); {tag}
}); </div>
}; ));
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
/>
);
};
return ( return (
<div className='field'> <div className="field tags">
<label>{props.label}</label> {label && <label>{label}</label>}
<div className='value'>
<ul className='list'> <div className="value">
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} <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> </ul>
<input <Combobox
type='text' trigger="click"
className='value' className="tagInput-dropdown"
placeholder={props.placeholder} default=""
value={tempInputText} placeholder={placeholder}
onChange={(e)=>setTempInputText(e.target.value)} options={suggestionOptions}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })} 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>
</div> </div>

View File

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