Files
diplom-frontend/app/components/custom-ui/text-box.tsx
2025-05-21 08:52:33 +03:00

122 lines
4.0 KiB
TypeScript

import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import { cn } from "~/lib/utils";
export interface TextBoxProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "value"> {
value: string;
onChange: (value: string) => void;
placeholder?: string;
wrapperClassName?: string;
inputClassName?: string;
disabled?: boolean;
autoFocus?: boolean;
spellCheck?: boolean;
}
export const TextBox = forwardRef<HTMLDivElement, TextBoxProps>(
(
{
value,
onChange,
placeholder,
wrapperClassName,
inputClassName,
disabled = false,
autoFocus = false,
spellCheck = true,
onInput,
onBlur,
onFocus,
...rest
},
ref,
) => {
const localRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
// Function to handle DOM updates
const updateDOM = (newValue: string) => {
if (localRef.current) {
// Only update if different to avoid selection issues
if (localRef.current.textContent !== newValue) {
localRef.current.textContent = newValue;
// Clear any <br> elements if the content is empty
if (!newValue && localRef.current.innerHTML.includes("<br>")) {
localRef.current.innerHTML = "";
}
}
}
};
// Update DOM when value prop changes
useEffect(() => {
updateDOM(value);
}, [value]);
useEffect(() => {
if (autoFocus && localRef.current) {
localRef.current.focus();
}
}, [autoFocus]);
const handleInput = (event: React.FormEvent<HTMLDivElement>) => {
const newValue = event.currentTarget.textContent || "";
// Handle the case where the content is empty but contains a <br>
if (!newValue && event.currentTarget.innerHTML.includes("<br>")) {
event.currentTarget.innerHTML = "";
}
onChange(newValue);
onInput?.(event);
};
const handlePaste = (event: React.ClipboardEvent<HTMLDivElement>) => {
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
// Use document.execCommand to maintain undo stack
document.execCommand("insertText", false, text);
// Manually trigger input event
const inputEvent = new Event("input", { bubbles: true });
event.currentTarget.dispatchEvent(inputEvent);
};
return (
<div
className={cn("overflow-y-auto overflow-x-hidden w-full min-h-6", wrapperClassName)}
onClick={() => localRef.current?.focus()}
>
<div
ref={localRef}
contentEditable={!disabled}
onInput={handleInput}
onPaste={handlePaste}
onBlur={onBlur}
onFocus={onFocus}
className={cn(
"outline-none break-all",
"empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:cursor-text",
disabled && "cursor-not-allowed opacity-50",
inputClassName,
)}
data-placeholder={placeholder}
role="textbox"
aria-multiline="true"
aria-disabled={disabled}
aria-placeholder={placeholder}
spellCheck={spellCheck}
suppressContentEditableWarning
tabIndex={0}
{...rest}
/>
</div>
);
},
);
TextBox.displayName = "TextBox";
export default TextBox;