This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.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>
)
});