import React, { useRef, useEffect, useCallback, MouseEventHandler } from "react"; import ContentEditable, { ContentEditableEvent } from "react-contenteditable"; import "./Editable.scss"; import { EditableProps } from "./Editable.interface"; const Editable: React.FC = ({ className, placeholder, content, onContentChange, handleClick: handleContentClick, }) => { const editableRef = useRef(null); const lastCaretPosition = useRef(0); const getCaretPosition = (el: HTMLElement): number => { let caretPosition = 0; const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const preRange = range.cloneRange(); preRange.selectNodeContents(el); preRange.setEnd(range.endContainer, range.endOffset); caretPosition = preRange.toString().length; } return caretPosition; }; const setCaretPosition = useCallback((offset: number) => { const selection = window.getSelection(); const range = document.createRange(); const el = editableRef.current; let localOffset = offset; el?.childNodes.forEach((element) => { if (!element.textContent?.length) { element.textContent = ""; } if (localOffset <= element.textContent?.length && localOffset >= 0) { range.setStart(element.childNodes[0] ?? element, localOffset); range.collapse(true); selection?.removeAllRanges(); selection?.addRange(range); localOffset -= element.textContent?.length; return; } else if (localOffset >= 0) { localOffset -= element.textContent?.length; } }); }, []); const handleChange = (event: ContentEditableEvent) => { const newText = event.currentTarget.innerText; const currentCaretPosition = getCaretPosition(editableRef.current!); if (newText.length <= 500) { onContentChange(newText); lastCaretPosition.current = currentCaretPosition; } else { const trimmedText = newText.substring(0, 500); event.currentTarget.innerText = trimmedText; onContentChange(trimmedText); lastCaretPosition.current = currentCaretPosition; setCaretPosition(500); } }; const handleClick: MouseEventHandler = (event) => { lastCaretPosition.current = getCaretPosition(editableRef.current!); if (handleContentClick) handleContentClick(event); }; useEffect(() => { const contentEditable = editableRef.current; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.removedNodes.forEach((node) => { if (node instanceof HTMLElement && node.tagName === "A") { setCaretPosition(lastCaretPosition.current); } }); } }); }); if (contentEditable) { observer.observe(contentEditable, { childList: true, subtree: true }); } return () => { observer.disconnect(); }; }, [setCaretPosition]); return ( {}} html={content.replaceAll("@@@", "")} data-placeholder={placeholder} onClick={handleClick} /> ); }; export default Editable;