79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import React, { useRef } from "react";
|
|
|
|
import { Box, IconButton, TextField, Tooltip } from "@mui/material";
|
|
import FormatBoldIcon from "@mui/icons-material/FormatBold";
|
|
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
|
|
import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined";
|
|
import LinkIcon from "@mui/icons-material/Link";
|
|
|
|
import { wrapSelection } from "../cvBuilder";
|
|
import { useI18n } from "../i18n/I18nProvider";
|
|
|
|
// Lightweight rich text: a plain textarea plus a markdown toolbar. Storage stays plain text
|
|
// (**bold**, *italic*, __underline__, [text](url)); the server renderer converts the same whitelist
|
|
// to safe HTML. No RTE dependency, no HTML sanitisation surface. Bullet lists are the field itself —
|
|
// one line per bullet — so there is no separate list button.
|
|
export default function RichTextField({
|
|
value,
|
|
onChange,
|
|
label,
|
|
placeholder,
|
|
minRows = 2,
|
|
ariaLabel,
|
|
disabled = false,
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
label?: string;
|
|
placeholder?: string;
|
|
minRows?: number;
|
|
ariaLabel?: string;
|
|
disabled?: boolean;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
|
|
|
const apply = (before: string, after: string, placeholderText: string) => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const { text, selStart, selEnd } = wrapSelection(value, el.selectionStart, el.selectionEnd, before, after, placeholderText);
|
|
onChange(text);
|
|
requestAnimationFrame(() => {
|
|
el.focus();
|
|
el.setSelectionRange(selStart, selEnd);
|
|
});
|
|
};
|
|
|
|
const btn = (title: string, icon: React.ReactNode, before: string, after: string, ph: string) => (
|
|
<Tooltip title={title}>
|
|
<IconButton size="small" disabled={disabled} aria-label={title} onMouseDown={(e) => e.preventDefault()} onClick={() => apply(before, after, ph)}>
|
|
{icon}
|
|
</IconButton>
|
|
</Tooltip>
|
|
);
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: "flex", gap: 0.25, mb: 0.25 }}>
|
|
{btn(t("richTextBold"), <FormatBoldIcon fontSize="inherit" />, "**", "**", t("richTextBoldPlaceholder"))}
|
|
{btn(t("richTextItalic"), <FormatItalicIcon fontSize="inherit" />, "*", "*", t("richTextItalicPlaceholder"))}
|
|
{btn(t("richTextUnderline"), <FormatUnderlinedIcon fontSize="inherit" />, "__", "__", t("richTextUnderlinePlaceholder"))}
|
|
{btn(t("richTextLink"), <LinkIcon fontSize="inherit" />, "[", "](https://)", t("richTextLinkPlaceholder"))}
|
|
</Box>
|
|
<TextField
|
|
inputRef={ref}
|
|
label={label}
|
|
placeholder={placeholder}
|
|
multiline
|
|
minRows={minRows}
|
|
fullWidth
|
|
size="small"
|
|
value={value}
|
|
disabled={disabled}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
slotProps={{ htmlInput: { "aria-label": ariaLabel ?? label } }}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|