157 lines
6.1 KiB
TypeScript
157 lines
6.1 KiB
TypeScript
import * as React from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu";
|
|
import { Scissors, Copy, Clipboard, Type } from "lucide-react";
|
|
export interface InputWithContextProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
onValueChange?: (value: string) => void;
|
|
}
|
|
const InputWithContext = React.forwardRef<HTMLInputElement, InputWithContextProps>(({ className, type, onValueChange, onChange, ...props }, ref) => {
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
const [hasSelection, setHasSelection] = React.useState(false);
|
|
const [canPaste, setCanPaste] = React.useState(false);
|
|
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
|
const updateSelectionState = () => {
|
|
const input = inputRef.current;
|
|
if (!input)
|
|
return;
|
|
const start = input.selectionStart ?? 0;
|
|
const end = input.selectionEnd ?? 0;
|
|
setHasSelection(start !== end);
|
|
};
|
|
const checkClipboard = async () => {
|
|
try {
|
|
const text = await navigator.clipboard.readText();
|
|
setCanPaste(text.length > 0);
|
|
}
|
|
catch {
|
|
setCanPaste(false);
|
|
}
|
|
};
|
|
const handleCut = async () => {
|
|
const input = inputRef.current;
|
|
if (!input)
|
|
return;
|
|
const start = input.selectionStart ?? 0;
|
|
const end = input.selectionEnd ?? 0;
|
|
const selectedText = input.value.substring(start, end);
|
|
if (selectedText) {
|
|
try {
|
|
await navigator.clipboard.writeText(selectedText);
|
|
const newValue = input.value.substring(0, start) + input.value.substring(end);
|
|
input.value = newValue;
|
|
input.setSelectionRange(start, start);
|
|
if (onChange) {
|
|
const event = {
|
|
target: input,
|
|
currentTarget: input,
|
|
} as React.ChangeEvent<HTMLInputElement>;
|
|
onChange(event);
|
|
}
|
|
if (onValueChange) {
|
|
onValueChange(newValue);
|
|
}
|
|
input.focus();
|
|
}
|
|
catch (err) {
|
|
console.error("Failed to cut:", err);
|
|
}
|
|
}
|
|
};
|
|
const handleCopy = async () => {
|
|
const input = inputRef.current;
|
|
if (!input)
|
|
return;
|
|
const start = input.selectionStart ?? 0;
|
|
const end = input.selectionEnd ?? 0;
|
|
const selectedText = input.value.substring(start, end);
|
|
if (selectedText) {
|
|
try {
|
|
await navigator.clipboard.writeText(selectedText);
|
|
input.focus();
|
|
}
|
|
catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
}
|
|
}
|
|
};
|
|
const handlePaste = async () => {
|
|
const input = inputRef.current;
|
|
if (!input)
|
|
return;
|
|
try {
|
|
const text = await navigator.clipboard.readText();
|
|
const start = input.selectionStart ?? 0;
|
|
const end = input.selectionEnd ?? 0;
|
|
const newValue = input.value.substring(0, start) + text + input.value.substring(end);
|
|
input.value = newValue;
|
|
const newPosition = start + text.length;
|
|
input.setSelectionRange(newPosition, newPosition);
|
|
if (onChange) {
|
|
const event = {
|
|
target: input,
|
|
currentTarget: input,
|
|
} as React.ChangeEvent<HTMLInputElement>;
|
|
onChange(event);
|
|
}
|
|
if (onValueChange) {
|
|
onValueChange(newValue);
|
|
}
|
|
input.focus();
|
|
await checkClipboard();
|
|
}
|
|
catch (err) {
|
|
console.error("Failed to paste:", err);
|
|
}
|
|
};
|
|
const handleSelectAll = () => {
|
|
const input = inputRef.current;
|
|
if (!input)
|
|
return;
|
|
input.select();
|
|
input.focus();
|
|
updateSelectionState();
|
|
};
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (onChange) {
|
|
onChange(e);
|
|
}
|
|
if (onValueChange) {
|
|
onValueChange(e.target.value);
|
|
}
|
|
};
|
|
return (<ContextMenu onOpenChange={(open) => {
|
|
if (open) {
|
|
checkClipboard();
|
|
}
|
|
}}>
|
|
<ContextMenuTrigger asChild>
|
|
<Input ref={inputRef} type={type} className={className} onChange={handleInputChange} onSelect={updateSelectionState} onMouseUp={updateSelectionState} onKeyUp={updateSelectionState} {...props}/>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent className="w-48">
|
|
<ContextMenuItem onSelect={handleCut} disabled={!hasSelection || props.disabled || props.readOnly}>
|
|
<Scissors className="mr-2 h-4 w-4"/>
|
|
Cut
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+X</span>
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onSelect={handleCopy} disabled={!hasSelection || props.disabled}>
|
|
<Copy className="mr-2 h-4 w-4"/>
|
|
Copy
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+C</span>
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onSelect={handlePaste} disabled={!canPaste || props.disabled || props.readOnly}>
|
|
<Clipboard className="mr-2 h-4 w-4"/>
|
|
Paste
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+V</span>
|
|
</ContextMenuItem>
|
|
<ContextMenuSeparator />
|
|
<ContextMenuItem onSelect={handleSelectAll} disabled={!inputRef.current?.value || props.disabled}>
|
|
<Type className="mr-2 h-4 w-4"/>
|
|
Select All
|
|
<span className="ml-auto text-xs text-muted-foreground">Ctrl+A</span>
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>);
|
|
});
|
|
InputWithContext.displayName = "InputWithContext";
|
|
export { InputWithContext };
|