'init'
This commit is contained in:
223
app/components/RichTextInput/RichTextInput.tsx
Normal file
223
app/components/RichTextInput/RichTextInput.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
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<HTMLDivElement>(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(/'/g, "'")
|
||||
.replace(/\n/g, "<br>");
|
||||
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() == '<br>'){
|
||||
editableDivRef.current.innerHTML = '';
|
||||
}
|
||||
if(props.onChange){
|
||||
props.onChange(getValue());
|
||||
}
|
||||
});
|
||||
if(props.onKeyDown){
|
||||
props.onKeyDown(event);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: props.style?.width || '100%',
|
||||
maxWidth: props.style?.width || '100%',
|
||||
display: 'inline-block',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div
|
||||
onDrag={e => 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%',
|
||||
}}
|
||||
></div>
|
||||
{getValue() == "" && props.placeholder && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
pointerEvents: 'none',
|
||||
color: '#888',
|
||||
userSelect: 'none',
|
||||
fontSize: props.style?.fontSize || 14,
|
||||
}}>
|
||||
{props.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
Reference in New Issue
Block a user