mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-26 03:58:11 +00:00
stable with basic suggestion
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
1
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default ["D&D", "P2e", "VtM", "CoC"];
|
||||||
Reference in New Issue
Block a user