TCP 서버 모드 추가
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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 (
|
||||
<div
|
||||
ref={contentRef}
|
||||
contentEditable
|
||||
className={cn(
|
||||
"min-h-[40px] rounded-md border border-input bg-background px-3 py-2",
|
||||
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"transition-colors",
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
data-value={internalValue}
|
||||
data-name={name}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableInput;
|
||||
Reference in New Issue
Block a user