Files
jobtrackingapp/job-tracker-ui/src/components/RichTextField.tsx
T
2026-10-07 13:53:54 +02:00

82 lines
2.9 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,
helperText,
disabled = false,
}: {
value: string;
onChange: (v: string) => void;
label?: string;
placeholder?: string;
minRows?: number;
ariaLabel?: string;
helperText?: React.ReactNode;
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}
helperText={helperText}
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>
);
}