import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; export interface RichTextInputProps { style?: React.CSSProperties; onChange?: (value: string) => void; onKeyDown?: (event: React.KeyboardEvent) => void; onPaste?: (event: React.ClipboardEvent) => void; placeholder?: string; autoFocus?: boolean; } export const RichTextInput = forwardRef((props : any, ref) => { const editableDivRef = useRef(null); useImperativeHandle(ref, () => ({ getValue, insertHTML, focus, clear, insertHTMLInCurrentCarretPosition })); useEffect(() => { if(props.autoFocus && editableDivRef.current){ focusEditableElement(editableDivRef.current); } }, [props.autoFocus]); const focus = () => { if(editableDivRef.current){ focusEditableElement(editableDivRef.current); } } const insertHTMLInCurrentCarretPosition = (html: string) => { if(!editableDivRef.current){ return; } if(document.activeElement !== editableDivRef.current){ focusEditableElement(editableDivRef.current); } document.execCommand('insertHTML', false, html); } const clear = () => { if(editableDivRef.current){ editableDivRef.current.innerHTML = ""; } if(props.onChange){ props.onChange(""); } } const focusEditableElement = (element: HTMLElement) => { /** * Focus to the end of the contenteditable element */ const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel?.removeAllRanges(); sel?.addRange(range); element.focus(); } const insertHTML = (html: string) => { if(!editableDivRef.current) return; let div = document.createElement("div"); div.innerHTML = html; if(props.style && props.style.fontSize){ /** * Prepare all elements to have the same font size as the editable div */ div.querySelectorAll('*').forEach(el => { el.setAttribute('style', ` font-size: ${props.style?.fontSize}px; vertical-align: sub; display: inline-block; margin-left: 1px; margin-right: 1px; user-select: none; `); el.setAttribute('width', '17'); el.setAttribute('height', '17'); }); } let preparedHtml = div.innerHTML; //let carret = saveCarretPosition(editableDivRef.current); editableDivRef.current.innerHTML += preparedHtml; //editableDivRef.current.focus(); //insertHtmlAtCarretPosition(preparedHtml); if(props.onChange){ props.onChange(getValue()); } //focusEditableElement(editableDivRef.current, carret); } const getValue = () : string => { if(!editableDivRef.current) return ""; editableDivRef.current.querySelectorAll('*').forEach(el => { if(el.tagName === 'BR'){ el.textContent = '\n'; return; } if(!el.hasAttribute("data")){ el.textContent = ''; return; } let text = el.getAttribute("data") || ''; el.textContent = text; }); let content = editableDivRef.current.textContent || ""; if(content.endsWith('\n') && content.length == 1){ /** * Remove trailing new line added by contenteditable div * (bug in some browsers includes electron) */ content = content.slice(0, -1); } return content || ""; } // const onCopy = (event : React.ClipboardEvent) => { // //event.preventDefault(); // //console.info("COPY EVENT", event); // //let value = getValue(); // //console.info("COPY VALUE", value); // //event.clipboardData.setData('text/plain', value); // } const onPaste = (event: React.ClipboardEvent) => { event.preventDefault(); let text = event.clipboardData.getData('text/plain'); if(text.trim() != '' && editableDivRef.current){ const html = text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") .replace(/\n/g, "
"); document.execCommand('insertHTML', false, html); if(props.onChange){ props.onChange(getValue()); } event.preventDefault(); return; } if(event.clipboardData.items[0].kind !== 'string'){ event.preventDefault(); } if(props.onPaste){ props.onPaste(event); } } const onKeyPress = (event : React.KeyboardEvent) => { if (event.keyCode == 13 && event.shiftKey == true) { //event.preventDefault(); //addLineBreak(); //return; } requestAnimationFrame(() => { if(editableDivRef.current?.innerHTML.trim() == '
'){ editableDivRef.current.innerHTML = ''; } if(props.onChange){ props.onChange(getValue()); } }); if(props.onKeyDown){ props.onKeyDown(event); } } return (
e.preventDefault()} onPaste={onPaste} //onCopy={onCopy} onKeyDown={onKeyPress} ref={editableDivRef} contentEditable={true} suppressContentEditableWarning style={{ overflowX: 'hidden', maxHeight: 150, maxWidth: '100%', overflowY: 'auto', wordWrap: 'break-word', whiteSpace: 'pre-wrap', verticalAlign: 'middle', boxSizing: 'border-box', outline: 'none', ...props.style, width: '100%', }} >
{getValue() == "" && props.placeholder && (
{props.placeholder}
)}
) });