210 lines
5.9 KiB
TypeScript
210 lines
5.9 KiB
TypeScript
import {
|
|
ChangeEventHandler, Dispatch,
|
|
KeyboardEventHandler,
|
|
MouseEventHandler,
|
|
ReactNode, SetStateAction,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState
|
|
} from "react";
|
|
import {FixedSizeList as List} from "react-window";
|
|
import Select, {createFilter, MenuListProps} from "react-select";
|
|
import * as R from 'ramda';
|
|
|
|
import {useQuery} from "@tanstack/react-query";
|
|
|
|
// TODO: Fix this for wrapping items, esp on phones
|
|
const height = 35;
|
|
|
|
function MenuList(props: MenuListProps) {
|
|
const {
|
|
options,
|
|
children,
|
|
maxHeight,
|
|
getValue
|
|
} = props as Omit<MenuListProps, 'children'> & {
|
|
children: ReactNode[]
|
|
};
|
|
|
|
const [value] = getValue();
|
|
const initialOffset = options.indexOf(value) * height;
|
|
|
|
return (<List
|
|
width={'100%'}
|
|
height={maxHeight}
|
|
itemCount={children.length}
|
|
itemSize={height}
|
|
initialScrollOffset={initialOffset}
|
|
>
|
|
{({
|
|
index,
|
|
style
|
|
}) => <div style={style}>{children[index]}</div>}
|
|
</List>);
|
|
}
|
|
|
|
interface Option {
|
|
label: string,
|
|
value: string,
|
|
data: {
|
|
backlinks: unknown[]
|
|
aliases?: string[]
|
|
}
|
|
}
|
|
|
|
export function LargeSelect() {
|
|
const {
|
|
data: options,
|
|
isLoading,
|
|
} = useQuery({
|
|
queryKey: ['obsidian-metadata'],
|
|
initialData: [],
|
|
queryFn: async () => {
|
|
const response = await fetch("/metadata")
|
|
const fullData: any[] = await response.json();
|
|
|
|
return R.sortBy(v => -(v.data.backlinks?.length ?? 0), fullData.map(md => ({
|
|
value: md.relativePath,
|
|
label: md.fileName,
|
|
data: md,
|
|
}) as Option));
|
|
},
|
|
});
|
|
|
|
const [selected, setSelected] = useState<Option | null>(null);
|
|
|
|
const onChange = useCallback((value: Option) => {
|
|
setSelected(value);
|
|
navigator.clipboard.writeText(`[[${value.label}]]`)
|
|
}, []);
|
|
|
|
return (
|
|
<div className={"m-5 text-lg"}>
|
|
<Select
|
|
onChange={onChange as any}
|
|
components={{MenuList}}
|
|
isLoading={isLoading}
|
|
options={options}
|
|
filterOption={createFilter({ignoreAccents: false})}
|
|
/>
|
|
|
|
{selected && <div className={"mt-2"}>
|
|
<div className="py-1">
|
|
<Alias original={selected.label}/>
|
|
</div>
|
|
|
|
{selected.data.aliases?.map(alias => (
|
|
<div className="py-1">
|
|
<Alias original={selected.label} alias={alias}/>
|
|
</div>
|
|
))}
|
|
|
|
<div className="py-1">
|
|
<CustomAlias selected={selected}/>
|
|
</div>
|
|
</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CustomAlias({selected}: {
|
|
selected: Option
|
|
}) {
|
|
const [alias, setAlias] = useState('');
|
|
|
|
// Reset when selection changes
|
|
useEffect(() => {
|
|
setAlias('');
|
|
}, [selected.value]);
|
|
|
|
const onClick: MouseEventHandler = useCallback((e) => {
|
|
if ((e.target as HTMLElement).tagName == 'SPAN' && alias.length > 0) {
|
|
navigator.clipboard.writeText(`[[${selected!.label}|${alias}]]`);
|
|
}
|
|
}, [selected.value, selected.label, alias]);
|
|
|
|
return (
|
|
<span onClick={onClick} className={"rounded-md p-1 hover:bg-slate-100"}>
|
|
<span className={"text-slate-300 p-0.5"}>[[</span>
|
|
<span>{selected.label}</span>
|
|
<span className={"text-slate-300 p-0.5"}>|</span>
|
|
<span>
|
|
<CustomAliasField content={alias} setContent={setAlias} selected={selected}/>
|
|
</span>
|
|
<span className={"text-slate-300 p-0.5"}>]]</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function CustomAliasField({
|
|
selected,
|
|
content,
|
|
setContent
|
|
}: {
|
|
selected: Option,
|
|
content: string,
|
|
setContent: Dispatch<SetStateAction<string>>,
|
|
}) {
|
|
const [width, setWidth] = useState<any>(0);
|
|
const span = useRef<HTMLSpanElement>(null);
|
|
|
|
// Resize on change of content
|
|
useEffect(() => {
|
|
setWidth(span.current!.offsetWidth);
|
|
// setWidth(content.length + 'ch');
|
|
}, [content]);
|
|
|
|
const changeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(evt => {
|
|
setContent(evt.target.value);
|
|
}, []);
|
|
|
|
const onCustomElementKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
|
if (e.key == 'Enter') {
|
|
navigator.clipboard.writeText(`[[${selected!.label}|${content}]]`);
|
|
(e.target as HTMLInputElement).blur()
|
|
}
|
|
}, [selected.value, selected.label, content]);
|
|
|
|
return (
|
|
<span>
|
|
<span style={{
|
|
position: 'absolute',
|
|
opacity: 0,
|
|
zIndex: -100,
|
|
whiteSpace: 'pre',
|
|
}} ref={span}>{content}</span>
|
|
<input
|
|
className={"border-none p-0 px-1"}
|
|
type="text" style={{width: `calc(${width}px + 0.25rem)`}} autoFocus onChange={changeHandler}
|
|
onKeyDown={onCustomElementKeyDown}/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function Alias({
|
|
original,
|
|
alias
|
|
}: {
|
|
original: string,
|
|
alias?: string
|
|
}) {
|
|
const onClick = useCallback(() => {
|
|
if (alias) {
|
|
navigator.clipboard.writeText(`[[${original}|${alias}]]`)
|
|
} else {
|
|
navigator.clipboard.writeText(`[[${original}]]`)
|
|
}
|
|
}, [original, alias]);
|
|
|
|
return (
|
|
<span className={"rounded-md p-1 hover:bg-slate-100"} onClick={onClick}>
|
|
<span className={"text-slate-300 p-0.5"}>[[</span>
|
|
<span>{original}</span>
|
|
{alias && <><span className={"text-slate-300 p-0.5"}>|</span>
|
|
<span>{alias}</span></>}
|
|
<span className={"text-slate-300 p-0.5"}>]]</span>
|
|
</span>
|
|
);
|
|
}
|