feat: tag autocomplete functionality in ItemRow component
This commit is contained in:
parent
005a1c08fa
commit
c16fa32af2
1 changed files with 91 additions and 19 deletions
|
|
@ -25,6 +25,7 @@ export default function ItemRow({
|
|||
const [hover, setHover] = useState(false);
|
||||
const [addingTag, setAddingTag] = useState(false);
|
||||
const [newTagInput, setNewTagInput] = useState("");
|
||||
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
|
||||
|
||||
const filteredSuggestions = allTags.filter(
|
||||
(t) =>
|
||||
|
|
@ -132,32 +133,103 @@ export default function ItemRow({
|
|||
className="border rounded px-1 py-0.5"
|
||||
placeholder="Tag..."
|
||||
value={newTagInput}
|
||||
onChange={(e) => setNewTagInput(e.target.value)}
|
||||
onChange={e => {
|
||||
setNewTagInput(e.target.value);
|
||||
setAutocompleteIndex(0);
|
||||
}}
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown={e => {
|
||||
// Autocomplete navigation
|
||||
const match = newTagInput.match(/^(\w*)$/);
|
||||
const suggestions = match
|
||||
? allTags.filter(
|
||||
tag =>
|
||||
tag.name.toLowerCase().startsWith(match[1].toLowerCase()) &&
|
||||
!item.tags.some((it) => it.id === tag.id)
|
||||
)
|
||||
: [];
|
||||
const showDropdown = suggestions.length > 0 && match && match[1].length >= 0;
|
||||
|
||||
if (showDropdown) {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setAutocompleteIndex(i => (i + 1) % suggestions.length);
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setAutocompleteIndex(i => (i - 1 + suggestions.length) % suggestions.length);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const tag = suggestions[autocompleteIndex];
|
||||
if (tag) {
|
||||
onAddTag(item.id, tag.id);
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
const tag = suggestions[autocompleteIndex];
|
||||
if (tag) {
|
||||
onAddTag(item.id, tag.id);
|
||||
setNewTagInput("");
|
||||
setAutocompleteIndex(0);
|
||||
// keep addingTag true so you can add another tag
|
||||
setTimeout(() => {
|
||||
e.target.focus();
|
||||
}, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{newTagInput && filteredSuggestions.length > 0 && (
|
||||
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10">
|
||||
{filteredSuggestions.map((tag) => (
|
||||
{/* Autocomplete-Dropdown */}
|
||||
{(() => {
|
||||
const match = newTagInput.match(/^(\w*)$/);
|
||||
const suggestions = match
|
||||
? allTags.filter(
|
||||
tag =>
|
||||
tag.name.toLowerCase().startsWith(match[1].toLowerCase()) &&
|
||||
!item.tags.some((it) => it.id === tag.id)
|
||||
)
|
||||
: [];
|
||||
const showDropdown = suggestions.length > 0 && match && match[1].length >= 0;
|
||||
if (showDropdown && autocompleteIndex >= suggestions.length) setAutocompleteIndex(0);
|
||||
|
||||
return (
|
||||
showDropdown && (
|
||||
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10 w-full max-h-40 overflow-auto">
|
||||
{suggestions.map((tag, idx) => (
|
||||
<li
|
||||
key={tag.id}
|
||||
className="px-2 py-1 hover:bg-gray-100 cursor-pointer"
|
||||
className={
|
||||
"px-2 py-1 cursor-pointer flex items-center " +
|
||||
(idx === autocompleteIndex ? "bg-blue-100 font-bold" : "")
|
||||
}
|
||||
onMouseDown={() => {
|
||||
onAddTag(item.id, tag.id);
|
||||
setAddingTag(false);
|
||||
setNewTagInput("");
|
||||
}}
|
||||
onMouseEnter={() => setAutocompleteIndex(idx)}
|
||||
>
|
||||
#{tag.name}
|
||||
{tag.mandatory && <span className="text-red-500 font-bold ml-1">!</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
)
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue