Spaces:
Sleeping
Sleeping
| 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; | |