Greums's picture
major improvements to the app
a417977
raw
history blame
10.9 kB
import style from "./style.module.scss";
import {Preview} from "@/components/preview";
import cn from "classnames";
import {useState, useRef, MutableRef, useEffect} from "preact/hooks";
import {JSX} from "preact";
import {
User,
Type,
Bold,
Italic,
Underline,
ChevronRight,
EyeOff,
Smile,
Image,
} from "preact-feather";
import {smileysMap} from "@/utils/smileys";
import {Button} from "@/components/button";
import {Input} from "@/components/input";
import {Alert} from "@/components/alert";
const iconSize = 14;
// const initialText = "> zefgze https://image.noelshack.com/fichiers/2018/13/4/1522325846-jesusopti.png\n" +
// "> zgfzreg :-)))\n" +
// "> > ergerg\n" +
// "zezefzef\n" +
// "<spoil>loool</spoil> AHI";
export function Wysiwyg(props: {
showTitle: boolean,
onSubmit: (user: string, title: string, text: string) => void,
disabled?: boolean,
initialText?: string,
text?: string,
setText?: (text: string) => void,
}) {
const [errors, setErrors] = useState<string[]>([]);
const [title, setTitle] = useState("");
const [user, setUser] = useState("");
const [text, setText] = props.text !== undefined && props.setText !== undefined ? [props.text, props.setText] : useState(props.initialText || "");
const [accordion, setAccordion] = useState<"smileys"|"stickers"|false>(false);
// Create a ref for the textarea
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const selectionRange = useRef<[start: number, end: number]|null>(null);
// // Example function to demonstrate interaction with the ref
// const focusTextarea = () => {
// if (textareaRef.current) {
// textareaRef.current.focus();
// }
// };
useEffect(() => {
if (textareaRef.current && selectionRange.current) {
const [start, end] = selectionRange.current;
textareaRef.current.setSelectionRange(start, end);
selectionRange.current = null;
}
}, [text])
const bold = createAction(textareaRef, (start, end) => {
const tag = "'''";
setText(insertAt(insertAt(text, tag, start), tag, end + tag.length));
selectionRange.current = [start + tag.length, end + tag.length];
});
const italic = createAction(textareaRef, (start, end) => {
const tag = "''";
setText(insertAt(insertAt(text, tag, start), tag, end + tag.length));
selectionRange.current = [start + tag.length, end + tag.length];
});
const underline = createAction(textareaRef, (start, end) => {
const startTag = "<u>";
const endTag = "</u>";
setText(insertAt(insertAt(text, startTag, start), endTag, end + startTag.length));
selectionRange.current = [start + startTag.length, end + startTag.length];
});
const quote = createAction(textareaRef, (start, end) => {
const lines = text.split("\n");
const newLines: string[] = [];
let charCount = 0;
let quoteEnd = 0;
let addedChars = 0;
for (const line of lines) {
charCount += line.length + 1; // Add "\n"
if (charCount > start && (charCount - line.length) <= end + 1) {
newLines.push(`> ${line}`);
addedChars += 2;
quoteEnd = charCount;
} else {
newLines.push(line);
}
}
setText(newLines.join("\n"));
selectionRange.current = [quoteEnd + addedChars - 1, quoteEnd + addedChars - 1];
});
const spoil = createAction(textareaRef, (start, end) => {
const startTag = "<spoil>";
const endTag = "</spoil>";
setText(insertAt(insertAt(text, startTag, start), endTag, end + startTag.length));
selectionRange.current = [start + startTag.length, end + startTag.length];
});
return (
<div>
<Alert lines={errors}/>
<Input
type="text"
icon={User}
value={user}
onChange={v => setUser(v as string)}
placeholder="Pseudo"
/>
{
props.showTitle ? (
<Input
type="text"
icon={Type}
value={title}
onChange={v => setTitle(v as string)}
placeholder="Titre du sujet"
/>
) : null
}
<div className={style.container}>
<div className={style.toolbar}>
<div className={style.toolbarGroup}>
<div className={style.buttonContainer} title="Gras" onMouseDown={bold}>
<Bold size={iconSize}/>
</div>
<div className={style.buttonContainer} title="Italique" onMouseDown={italic}>
<Italic size={iconSize}/>
</div>
<div className={style.buttonContainer} title="Souligné" onMouseDown={underline}>
<Underline size={iconSize}/>
</div>
</div>
<div className={style.toolbarGroup}>
<div className={style.buttonContainer} title="Citation" onMouseDown={quote}>
<ChevronRight size={iconSize}/>
</div>
<div className={style.buttonContainer} title="Spoil" onMouseDown={spoil}>
<EyeOff size={iconSize}/>
</div>
</div>
<div className={style.toolbarGroup}>
<div
className={style.buttonContainer}
title="Smileys"
onClick={() => {
setAccordion(accordion ? false : "smileys")
}}
>
<Smile size={iconSize}/>
</div>
<div className={style.buttonContainer} title="Stickers">
<Image size={iconSize}/>
</div>
</div>
</div>
<div className={cn(style.accordion, {[style.accordionOpened]: accordion})}>
<SmileyList
text={text}
setText={setText}
textareaRef={textareaRef}
selectionRange={selectionRange}
/>
</div>
<div className={style.editor}>
<textarea
className={style.textarea}
placeholder="Saisissez un message…"
name="wysiwyg"
id="wysiwyg"
cols="30"
rows="10"
value={text}
onInput={(e) => setText((e.target as HTMLTextAreaElement).value)}
ref={textareaRef} // Attach ref to textarea
/>
</div>
</div>
<div className={cn(style.container, style.previewContainer)}>
<Preview raw={text}/>
</div>
<Button
secondary={true}
onClick={() => {
const errors: string[] = [];
if(user.length < 2) {
errors.push("Le pseudo doit se composer au minimum de 3 caractères");
}
if(user.match(/[^a-zA-Z0-9_-]/)) {
errors.push("Le pseudo doit uniquement contenir les caractères suivant: a-z, A-Z, 0-9, _ et -");
}
if(props.showTitle && title.length < 2) {
errors.push("Le titre du sujet doit se composer au minimum de 3 caractères");
}
if(text.length < 2) {
errors.push("Le text du sujet doit se composer au minimum de 3 caractères");
}
setErrors(errors);
if(errors.length > 0) {
return;
}
props.onSubmit(user, title, text);
setUser("");
setTitle("");
setText("");
}}
disabled={props.disabled}
>
Poster
</Button>
</div>
);
}
function SmileyList(props: {
text: string,
setText: (text: string) => void,
textareaRef: MutableRef<HTMLTextAreaElement | null>,
selectionRange: MutableRef<[start: number, end: number]|null>,
}) {
// const addSmiley = createAction(props.textareaRef, (start, end) => {
// const startTag = "<u>";
// const endTag = "</u>";
//
// props.setText(insertAt(insertAt(props.text, startTag, start), endTag, end + startTag.length));
// props.selectionRange.current = [start + startTag.length, end + startTag.length];
// });
const addSmiley: (smiley: string) => JSX.MouseEventHandler<HTMLDivElement> = (smiley) => (e) => {
e.preventDefault(); // Do not lose the focus on textarea
if (props.textareaRef.current) {
props.setText(insertAt(props.text, smiley, props.textareaRef.current.selectionStart));
props.selectionRange.current = [props.textareaRef.current.selectionStart + smiley.length, props.textareaRef.current.selectionStart + smiley.length];
props.textareaRef.current.focus();
}
}
return (
<div className={style.smileyList}>
{smileysMap.map((smiley) => (
<div onMouseDown={addSmiley(` ${smiley[0]}`)}>
<img
src={`https://image.jeuxvideo.com/smileys_img/${smiley[1]}.gif`}
alt={smiley[0]}
/>
</div>
))}
</div>
)
}
function createAction(
ref: MutableRef<HTMLTextAreaElement | null>,
callback: (start: number, end: number) => void
): JSX.MouseEventHandler<HTMLDivElement> {
return (e) => {
e.preventDefault(); // Do not lose the focus on textarea
if (ref.current) {
callback(ref.current.selectionStart, ref.current.selectionEnd);
ref.current.focus();
}
}
}
function insertAt(originalString: string, substring: string, position: number): string {
return originalString.slice(0, position) + substring + originalString.slice(position);
}