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 [hover, setHover] = useState(false);
|
||||||
const [addingTag, setAddingTag] = useState(false);
|
const [addingTag, setAddingTag] = useState(false);
|
||||||
const [newTagInput, setNewTagInput] = useState("");
|
const [newTagInput, setNewTagInput] = useState("");
|
||||||
|
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
|
||||||
|
|
||||||
const filteredSuggestions = allTags.filter(
|
const filteredSuggestions = allTags.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
|
|
@ -132,32 +133,103 @@ export default function ItemRow({
|
||||||
className="border rounded px-1 py-0.5"
|
className="border rounded px-1 py-0.5"
|
||||||
placeholder="Tag..."
|
placeholder="Tag..."
|
||||||
value={newTagInput}
|
value={newTagInput}
|
||||||
onChange={(e) => setNewTagInput(e.target.value)}
|
onChange={e => {
|
||||||
|
setNewTagInput(e.target.value);
|
||||||
|
setAutocompleteIndex(0);
|
||||||
|
}}
|
||||||
autoFocus
|
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") {
|
if (e.key === "Escape") {
|
||||||
setAddingTag(false);
|
setAddingTag(false);
|
||||||
setNewTagInput("");
|
setNewTagInput("");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{newTagInput && filteredSuggestions.length > 0 && (
|
{/* Autocomplete-Dropdown */}
|
||||||
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10">
|
{(() => {
|
||||||
{filteredSuggestions.map((tag) => (
|
const match = newTagInput.match(/^(\w*)$/);
|
||||||
<li
|
const suggestions = match
|
||||||
key={tag.id}
|
? allTags.filter(
|
||||||
className="px-2 py-1 hover:bg-gray-100 cursor-pointer"
|
tag =>
|
||||||
onMouseDown={() => {
|
tag.name.toLowerCase().startsWith(match[1].toLowerCase()) &&
|
||||||
onAddTag(item.id, tag.id);
|
!item.tags.some((it) => it.id === tag.id)
|
||||||
setAddingTag(false);
|
)
|
||||||
setNewTagInput("");
|
: [];
|
||||||
}}
|
const showDropdown = suggestions.length > 0 && match && match[1].length >= 0;
|
||||||
>
|
if (showDropdown && autocompleteIndex >= suggestions.length) setAutocompleteIndex(0);
|
||||||
#{tag.name}
|
|
||||||
</li>
|
return (
|
||||||
))}
|
showDropdown && (
|
||||||
</ul>
|
<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 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue