171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
|
|
export interface ProjectPathMatch {
|
|||
|
|
name: string;
|
|||
|
|
path: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ProjectPathInputProps {
|
|||
|
|
value: string;
|
|||
|
|
onChange: (value: string) => void;
|
|||
|
|
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|||
|
|
suggestionTail: string;
|
|||
|
|
matchList: ProjectPathMatch[];
|
|||
|
|
selectedMatch: number;
|
|||
|
|
onSelectMatch: (index: number) => void;
|
|||
|
|
onAcceptMatch: (path: string) => void;
|
|||
|
|
onCloseSuggestions: () => void;
|
|||
|
|
currentPartial: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderHighlightedMatch(text: string, query: string) {
|
|||
|
|
if (!query) return text;
|
|||
|
|
let qIndex = 0;
|
|||
|
|
const lowerQuery = query.toLowerCase();
|
|||
|
|
const counts = new Map<string, number>();
|
|||
|
|
return text.split("").map((char) => {
|
|||
|
|
const isMatch =
|
|||
|
|
qIndex < lowerQuery.length && char.toLowerCase() === lowerQuery[qIndex];
|
|||
|
|
if (isMatch) {
|
|||
|
|
qIndex += 1;
|
|||
|
|
}
|
|||
|
|
const count = counts.get(char) ?? 0;
|
|||
|
|
counts.set(char, count + 1);
|
|||
|
|
return (
|
|||
|
|
<span
|
|||
|
|
key={`${char}-${count}`}
|
|||
|
|
style={isMatch ? { fontWeight: 600, color: "#222" } : undefined}
|
|||
|
|
>
|
|||
|
|
{char}
|
|||
|
|
</span>
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function ProjectPathInput({
|
|||
|
|
value,
|
|||
|
|
onChange,
|
|||
|
|
onKeyDown,
|
|||
|
|
suggestionTail,
|
|||
|
|
matchList,
|
|||
|
|
selectedMatch,
|
|||
|
|
onSelectMatch,
|
|||
|
|
onAcceptMatch,
|
|||
|
|
onCloseSuggestions,
|
|||
|
|
currentPartial,
|
|||
|
|
}: ProjectPathInputProps) {
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: "relative",
|
|||
|
|
marginTop: "12px",
|
|||
|
|
marginBottom: "170px",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: "absolute",
|
|||
|
|
inset: 0,
|
|||
|
|
padding: "10px",
|
|||
|
|
color: "#aaa",
|
|||
|
|
fontFamily: "monospace",
|
|||
|
|
whiteSpace: "pre",
|
|||
|
|
overflow: "hidden",
|
|||
|
|
textOverflow: "ellipsis",
|
|||
|
|
pointerEvents: "none",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{value}
|
|||
|
|
{suggestionTail}
|
|||
|
|
</div>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={value}
|
|||
|
|
placeholder="/path/to/project"
|
|||
|
|
onChange={(event) => onChange(event.target.value)}
|
|||
|
|
onKeyDown={onKeyDown}
|
|||
|
|
style={{
|
|||
|
|
width: "100%",
|
|||
|
|
padding: "10px",
|
|||
|
|
fontFamily: "monospace",
|
|||
|
|
background: "transparent",
|
|||
|
|
position: "relative",
|
|||
|
|
zIndex: 1,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
{matchList.length > 0 && (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: "absolute",
|
|||
|
|
top: "100%",
|
|||
|
|
left: 0,
|
|||
|
|
right: 0,
|
|||
|
|
marginTop: "6px",
|
|||
|
|
border: "1px solid #ddd",
|
|||
|
|
borderRadius: "6px",
|
|||
|
|
overflow: "hidden",
|
|||
|
|
background: "#fff",
|
|||
|
|
fontFamily: "monospace",
|
|||
|
|
height: "160px",
|
|||
|
|
overflowY: "auto",
|
|||
|
|
boxSizing: "border-box",
|
|||
|
|
zIndex: 2,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: "flex",
|
|||
|
|
justifyContent: "flex-end",
|
|||
|
|
alignItems: "center",
|
|||
|
|
padding: "4px 6px",
|
|||
|
|
borderBottom: "1px solid #eee",
|
|||
|
|
background: "#fafafa",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
aria-label="Close suggestions"
|
|||
|
|
onClick={onCloseSuggestions}
|
|||
|
|
style={{
|
|||
|
|
width: "24px",
|
|||
|
|
height: "24px",
|
|||
|
|
borderRadius: "4px",
|
|||
|
|
border: "1px solid #ddd",
|
|||
|
|
background: "#fff",
|
|||
|
|
cursor: "pointer",
|
|||
|
|
lineHeight: 1,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{matchList.map((match, index) => {
|
|||
|
|
const isSelected = index === selectedMatch;
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={match.path}
|
|||
|
|
type="button"
|
|||
|
|
onMouseEnter={() => onSelectMatch(index)}
|
|||
|
|
onMouseDown={(event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
onSelectMatch(index);
|
|||
|
|
onAcceptMatch(match.path);
|
|||
|
|
}}
|
|||
|
|
style={{
|
|||
|
|
width: "100%",
|
|||
|
|
textAlign: "left",
|
|||
|
|
padding: "6px 8px",
|
|||
|
|
border: "none",
|
|||
|
|
background: isSelected ? "#f0f0f0" : "transparent",
|
|||
|
|
cursor: "pointer",
|
|||
|
|
fontFamily: "inherit",
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{renderHighlightedMatch(match.name, currentPartial)}/
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|