Spaces:
Running
Running
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<EditableProps> = ({ | |
className, | |
placeholder, | |
content, | |
onContentChange, | |
handleClick: handleContentClick, | |
}) => { | |
const editableRef = useRef<HTMLElement>(null); | |
const lastCaretPosition = useRef<number>(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<HTMLDivElement> = (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 ( | |
<ContentEditable | |
innerRef={editableRef} | |
className={`editable ${className}`} | |
onChange={handleChange} | |
onBlur={() => {}} | |
html={content.replaceAll("@@@", "")} | |
data-placeholder={placeholder} | |
onClick={handleClick} | |
/> | |
); | |
}; | |
export default Editable; | |