import React, { useState, useRef, useEffect } from 'react'; import { cn } from "@/lib/utils"; const EditableInput = ({ name = '', value = '', className = '', style = {}, onChange = () => {}, onVariableHover = () => {} }) => { const contentRef = useRef(null); const [internalValue, setInternalValue] = useState(value); const [selection, setSelection] = useState({ start: 0, end: 0 }); // Get current cursor position const saveSelection = () => { const sel = window.getSelection(); if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(contentRef.current); preCaretRange.setEnd(range.endContainer, range.endOffset); const start = preCaretRange.toString().length; preCaretRange.setEnd(range.startContainer, range.startOffset); const end = preCaretRange.toString().length; return { start, end }; } return null; }; // Restore cursor position const restoreSelection = (pos) => { if (!pos || !contentRef.current) return; const sel = window.getSelection(); const range = document.createRange(); let currentPos = 0; let startNode = null; let startOffset = 0; let endNode = null; let endOffset = 0; const traverse = (node) => { if (node.nodeType === 3) { // Text node const nextPos = currentPos + node.length; if (!startNode && pos.start >= currentPos && pos.start <= nextPos) { startNode = node; startOffset = pos.start - currentPos; } if (!endNode && pos.end >= currentPos && pos.end <= nextPos) { endNode = node; endOffset = pos.end - currentPos; } currentPos = nextPos; } else { for (const child of node.childNodes) { traverse(child); } } }; traverse(contentRef.current); if (startNode && endNode) { range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); sel.removeAllRanges(); sel.addRange(range); } }; // Parse content to find variables const parseContent = (text) => { const regex = /{{.*?}}/g; const parts = []; let lastIndex = 0; let match; while ((match = regex.exec(text)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', content: text.slice(lastIndex, match.index) }); } parts.push({ type: 'variable', content: match[0] }); lastIndex = regex.lastIndex; } if (lastIndex < text.length) { parts.push({ type: 'text', content: text.slice(lastIndex) }); } return parts; }; const handleInput = (e) => { const newValue = e.target.textContent; const newSelection = saveSelection(); setInternalValue(newValue); setSelection(newSelection); onChange(newValue); }; const handlePaste = (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); const processedContent = parseContent(text); const fragment = document.createDocumentFragment(); processedContent.forEach(({ type, content }) => { const span = document.createElement('span'); span.textContent = content; if (type === 'variable') { span.className = 'bg-blue-100 text-blue-800 rounded px-1'; } fragment.appendChild(span); }); range.insertNode(fragment); handleInput({ target: contentRef.current }); } }; useEffect(() => { if (!contentRef.current) return; const parts = parseContent(internalValue); const currentSelection = saveSelection() || selection; contentRef.current.innerHTML = ''; parts.forEach(({ type, content }) => { const span = document.createElement('span'); span.textContent = content; if (type === 'variable') { span.className = 'bg-blue-100 text-blue-800 rounded px-1'; span.onmouseover = () => onVariableHover(content); } contentRef.current.appendChild(span); }); restoreSelection(currentSelection); }, [internalValue]); // Update internal value when prop value changes useEffect(() => { if (value !== internalValue) { setInternalValue(value); } }, [value]); return (
); }; export default EditableInput;