2024-08-12 01:13:42 +00:00
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
import { forwardRef, useId, useMemo, useRef, useState} from "react";
|
2025-05-13 21:02:59 +00:00
|
|
|
import { useAtom, useAtomValue } from "jotai";
|
|
|
|
|
import { autoUpdate, flip, FloatingFocusManager, FloatingPortal, size, useDismiss, useFloating, useInteractions, useListNavigation, useRole } from "@floating-ui/react";
|
|
|
|
|
import Fuse from "fuse.js";
|
2025-05-25 05:17:41 +00:00
|
|
|
import { charactersAtom, selectedTargetInventoryAtom } from "@/state/atoms";
|
2024-08-12 01:13:42 +00:00
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
interface AccountInventorySelectorItemProps {
|
2025-05-13 21:02:59 +00:00
|
|
|
children: React.ReactNode;
|
|
|
|
|
active: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-25 05:17:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
const AccountInventorySelectorItem = forwardRef<
|
2025-05-13 21:02:59 +00:00
|
|
|
HTMLDivElement,
|
2025-05-25 05:17:41 +00:00
|
|
|
AccountInventorySelectorItemProps & React.HTMLProps<HTMLDivElement>
|
2025-05-13 21:02:59 +00:00
|
|
|
>(({ children, active, ...rest }, ref) => {
|
|
|
|
|
const id = useId();
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
role="option"
|
|
|
|
|
id={id}
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
{...rest}
|
|
|
|
|
style={{
|
|
|
|
|
background: active ? "lightblue" : "none",
|
|
|
|
|
padding: 4,
|
|
|
|
|
cursor: "default",
|
|
|
|
|
...rest.style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-05-25 05:17:41 +00:00
|
|
|
export const InventoryTargetSelector = () => {
|
2025-05-13 21:02:59 +00:00
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const [inputValue, setInputValue] = useState("");
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
|
|
|
|
|
|
|
|
const listRef = useRef<Array<HTMLElement | null>>([]);
|
|
|
|
|
|
|
|
|
|
const { refs, floatingStyles, context } = useFloating<HTMLInputElement>({
|
|
|
|
|
whileElementsMounted: autoUpdate,
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange: setOpen,
|
|
|
|
|
middleware: [
|
|
|
|
|
flip({ padding: 10 }),
|
|
|
|
|
size({
|
|
|
|
|
apply({ rects, availableHeight, elements }) {
|
|
|
|
|
Object.assign(elements.floating.style, {
|
|
|
|
|
width: `${rects.reference.width}px`,
|
|
|
|
|
maxHeight: `${availableHeight}px`,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
padding: 10,
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const role = useRole(context, { role: "listbox" });
|
|
|
|
|
const dismiss = useDismiss(context);
|
|
|
|
|
const listNav = useListNavigation(context, {
|
|
|
|
|
listRef,
|
|
|
|
|
activeIndex,
|
|
|
|
|
onNavigate: setActiveIndex,
|
|
|
|
|
virtual: true,
|
|
|
|
|
loop: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
|
|
|
|
[role, dismiss, listNav]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
const value = event.target.value;
|
|
|
|
|
setInputValue(value);
|
|
|
|
|
setSelectedTargetInventory(undefined)
|
|
|
|
|
|
|
|
|
|
if (value) {
|
|
|
|
|
setOpen(true);
|
|
|
|
|
setActiveIndex(0);
|
|
|
|
|
} else {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const { data: subaccounts } = useAtomValue(charactersAtom)
|
|
|
|
|
|
|
|
|
|
const [selectedTargetInventory, setSelectedTargetInventory] = useAtom(selectedTargetInventoryAtom)
|
|
|
|
|
|
|
|
|
|
const searcher = useMemo(()=>{
|
|
|
|
|
return new Fuse(subaccounts?.flatMap(x=>[
|
|
|
|
|
x.bank,
|
|
|
|
|
x.character,
|
|
|
|
|
])||[], {
|
|
|
|
|
keys:["path","name"],
|
|
|
|
|
findAllMatches: true,
|
|
|
|
|
threshold: 0.8,
|
|
|
|
|
useExtendedSearch: true,
|
|
|
|
|
})
|
|
|
|
|
}, [subaccounts])
|
|
|
|
|
|
|
|
|
|
const items = searcher.search(inputValue || "!-", {limit: 10}).map(x=>x.item)
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<input
|
|
|
|
|
className="border border-black-1 bg-gray-100 placeholder-gray-600"
|
|
|
|
|
{...getReferenceProps({
|
|
|
|
|
ref: refs.setReference,
|
|
|
|
|
onChange,
|
|
|
|
|
value: selectedTargetInventory !== undefined ? selectedTargetInventory.name : inputValue,
|
|
|
|
|
placeholder: "Target Inventory",
|
|
|
|
|
"aria-autocomplete": "list",
|
2025-05-25 05:17:41 +00:00
|
|
|
onFocus() {
|
|
|
|
|
setOpen(true);
|
|
|
|
|
},
|
2025-05-13 21:02:59 +00:00
|
|
|
onKeyDown(event) {
|
|
|
|
|
if (
|
|
|
|
|
event.key === "Enter" &&
|
|
|
|
|
activeIndex != null &&
|
|
|
|
|
items[activeIndex]
|
|
|
|
|
) {
|
|
|
|
|
setSelectedTargetInventory(items[activeIndex])
|
|
|
|
|
setInputValue(items[activeIndex].name);
|
|
|
|
|
setActiveIndex(null);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
{open && (
|
|
|
|
|
<FloatingPortal>
|
|
|
|
|
<FloatingFocusManager
|
|
|
|
|
context={context}
|
|
|
|
|
initialFocus={-1}
|
|
|
|
|
visuallyHiddenDismiss
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
{...getFloatingProps({
|
|
|
|
|
ref: refs.setFloating,
|
|
|
|
|
style: {
|
|
|
|
|
...floatingStyles,
|
|
|
|
|
background: "#eee",
|
|
|
|
|
color: "black",
|
|
|
|
|
overflowY: "auto",
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
{items.map((item, index) => (
|
2025-05-25 05:17:41 +00:00
|
|
|
<AccountInventorySelectorItem
|
2025-05-13 21:02:59 +00:00
|
|
|
{...getItemProps({
|
|
|
|
|
key: item.path,
|
|
|
|
|
ref(node) {
|
|
|
|
|
listRef.current[index] = node;
|
|
|
|
|
},
|
|
|
|
|
onClick() {
|
|
|
|
|
setInputValue(item.name);
|
|
|
|
|
setSelectedTargetInventory(item);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
refs.domReference.current?.focus();
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
active={activeIndex === index}
|
|
|
|
|
>
|
|
|
|
|
{item.name}
|
2025-05-25 05:17:41 +00:00
|
|
|
</AccountInventorySelectorItem>
|
2025-05-13 21:02:59 +00:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</FloatingFocusManager>
|
|
|
|
|
</FloatingPortal>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|