Files
diplom-frontend/app/components/custom-ui/text-box.tsx
2025-05-20 04:16:03 +03:00

124 lines
4.2 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(
"max-h-96 w-full overflow-y-auto min-h-6",
wrapperClassName
)}
onClick={() => localRef.current?.focus()}
>
<div
ref={localRef}
contentEditable={!disabled}
onInput={handleInput}
onPaste={handlePaste}
onBlur={onBlur}
onFocus={onFocus}
className={cn(
"break-words whitespace-pre-wrap outline-none w-full",
"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;