Improve editor and no longer crash if markdawn Line has bad parameters

This commit is contained in:
DrMint 2023-04-08 16:34:27 +02:00
parent b6882cd1e5
commit d68e238b00
3 changed files with 221 additions and 48 deletions

View File

@ -1,8 +1,10 @@
// eslint-disable-next-line import/named
import { Placement } from "tippy.js";
import { Button } from "./Button";
import { ToolTip } from "components/ToolTip";
import { cJoin } from "helpers/className";
import { ConditionalWrapper, Wrapper } from "helpers/component";
import { isDefinedAndNotEmpty } from "helpers/asserts";
import { isDefined } from "helpers/asserts";
/*
*
@ -12,7 +14,8 @@ import { isDefinedAndNotEmpty } from "helpers/asserts";
interface Props {
className?: string;
buttonsProps: (Parameters<typeof Button>[0] & {
tooltip?: string | null | undefined;
tooltip?: React.ReactNode | null | undefined;
tooltipPlacement?: Placement;
})[];
}
@ -23,9 +26,9 @@ export const ButtonGroup = ({ buttonsProps, className }: Props): JSX.Element =>
{buttonsProps.map((buttonProps, index) => (
<ConditionalWrapper
key={index}
isWrapping={isDefinedAndNotEmpty(buttonProps.tooltip)}
isWrapping={isDefined(buttonProps.tooltip)}
wrapper={ToolTipWrapper}
wrapperProps={{ text: buttonProps.tooltip ?? "" }}>
wrapperProps={{ text: buttonProps.tooltip ?? "", placement: buttonProps.tooltipPlacement }}>
<Button
{...buttonProps}
className={
@ -47,11 +50,12 @@ export const ButtonGroup = ({ buttonsProps, className }: Props): JSX.Element =>
*/
interface ToolTipWrapperProps {
text: string;
text: React.ReactNode;
placement?: Placement;
}
const ToolTipWrapper = ({ text, children }: ToolTipWrapperProps & Wrapper) => (
<ToolTip content={text}>
const ToolTipWrapper = ({ text, children, placement }: ToolTipWrapperProps & Wrapper) => (
<ToolTip content={text} placement={placement}>
<>{children}</>
</ToolTip>
);

View File

@ -1,6 +1,7 @@
import Markdown from "markdown-to-jsx";
import React, { Fragment, MouseEventHandler, useMemo } from "react";
import ReactDOMServer from "react-dom/server";
import { z } from "zod";
import { HorizontalLine } from "components/HorizontalLine";
import { Img } from "components/Img";
import { InsetBox } from "components/Containers/InsetBox";
@ -35,7 +36,7 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
const { showLightBox } = useAtomGetter(atoms.lightBox);
/* eslint-disable no-irregular-whitespace */
const text = `${preprocessMarkDawn(rawText, playerName)}
const text = `${preprocessMarkDawn(rawText, playerName)}\n
`;
/* eslint-enable no-irregular-whitespace */
@ -117,15 +118,30 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
},
Line: {
component: (compProps) => (
component: (compProps) => {
const schema = z.object({ name: z.string(), children: z.any() });
if (!schema.safeParse(compProps).success) {
return (
<MarkdawnError
message={`Error while parsing a <Line/> tag. Here is the correct usage:
<Line name="John">Hello!</Line>`}
/>
);
}
const safeProps: z.infer<typeof schema> = compProps;
return (
<>
<strong
className={cJoin("!my-0 text-dark/60", cIf(!isContentPanelAtLeastLg, "!-mb-4"))}>
<Markdawn text={compProps.name} />
className={cJoin(
"!my-0 text-dark/60",
cIf(!isContentPanelAtLeastLg, "!-mb-4")
)}>
<Markdawn text={safeProps.name} />
</strong>
<p className="whitespace-pre-line">{compProps.children}</p>
<p className="whitespace-pre-line">{safeProps.children}</p>
</>
),
);
},
},
InsetBox: {
@ -215,6 +231,21 @@ export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Eleme
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface MarkdawnErrorProps {
message: string;
}
const MarkdawnError = ({ message }: MarkdawnErrorProps): JSX.Element => (
<div
className="flex place-items-center gap-4 whitespace-pre-line rounded-md
bg-[red]/10 px-4 text-[red]">
<Ico icon="error" isFilled={false} />
<p>{message}</p>
</div>
);
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface TableOfContentsProps {
toc: TocInterface;
onContentClicked?: MouseEventHandler<HTMLAnchorElement>;

View File

@ -12,6 +12,7 @@ import { getOpenGraph } from "helpers/openGraph";
import { getFormat } from "helpers/i18n";
import { isDefined } from "helpers/asserts";
import { atomPairing, useAtomPair } from "helpers/atoms";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
/*
*
@ -39,23 +40,23 @@ const Editor = (props: Props): JSX.Element => {
value: string,
selectionStart: number,
selectedEnd: number
) => { prependLength: number; transformedValue: string }
) => { prependLength: number; selectionLength?: number; transformedValue: string }
) => {
if (textAreaRef.current) {
const { value, selectionStart, selectionEnd } = textAreaRef.current;
const { prependLength, transformedValue } = transformation(
value,
selectionStart,
selectionEnd
);
const {
prependLength,
transformedValue,
selectionLength = selectionEnd - selectionStart,
} = transformation(value, selectionStart, selectionEnd);
textAreaRef.current.value = transformedValue;
setMarkdown(textAreaRef.current.value);
textAreaRef.current.focus();
textAreaRef.current.selectionStart = selectionStart + prependLength;
textAreaRef.current.selectionEnd = selectionEnd + prependLength;
textAreaRef.current.selectionEnd = selectionStart + selectionLength + prependLength;
}
},
[setMarkdown]
@ -92,13 +93,13 @@ const Editor = (props: Props): JSX.Element => {
);
const unwrap = useCallback(
(wrapper: string) => {
(openingWrapper: string, closingWrapper: string) => {
transformationWrapper((value, selectionStart, selectionEnd) => {
let newValue = "";
newValue += value.slice(0, selectionStart - wrapper.length);
newValue += value.slice(0, selectionStart - openingWrapper.length);
newValue += value.slice(selectionStart, selectionEnd);
newValue += value.slice(wrapper.length + selectionEnd);
return { prependLength: -wrapper.length, transformedValue: newValue };
newValue += value.slice(closingWrapper.length + selectionEnd);
return { prependLength: -openingWrapper.length, transformedValue: newValue };
});
},
[transformationWrapper]
@ -109,11 +110,16 @@ const Editor = (props: Props): JSX.Element => {
if (textAreaRef.current) {
const { value, selectionStart, selectionEnd } = textAreaRef.current;
const openingWrapper =
properties && Object.keys(properties).length === 0 ? `<${wrapper}>` : wrapper;
const closingWrapper =
properties && Object.keys(properties).length === 0 ? `</${wrapper}>` : wrapper;
if (
value.slice(selectionStart - wrapper.length, selectionStart) === wrapper &&
value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper
value.slice(selectionStart - openingWrapper.length, selectionStart) === openingWrapper &&
value.slice(selectionEnd, selectionEnd + closingWrapper.length) === closingWrapper
) {
unwrap(wrapper);
unwrap(openingWrapper, closingWrapper);
} else {
wrap(wrapper, properties, addInnerNewLines);
}
@ -124,20 +130,77 @@ const Editor = (props: Props): JSX.Element => {
const preline = useCallback(
(prepend: string) => {
transformationWrapper((value, selectionStart) => {
transformationWrapper((value, selectionStart, selectionEnd) => {
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1;
const nextNewLine = value.slice(selectionEnd).indexOf("\n") + selectionEnd;
const lines = value.slice(lastNewLine, nextNewLine).split("\n");
const processedLines = lines.map((line) => `${prepend}${line}`);
let newValue = "";
newValue += value.slice(0, lastNewLine);
newValue += prepend;
newValue += value.slice(lastNewLine);
newValue += processedLines.join("\n");
newValue += value.slice(nextNewLine);
return { prependLength: prepend.length, transformedValue: newValue };
return {
prependLength: prepend.length,
selectionLength: selectionEnd - selectionStart + (lines.length - 1) * prepend.length,
transformedValue: newValue,
};
});
},
[transformationWrapper]
);
const unpreline = useCallback(
(prepend: string) => {
transformationWrapper((value, selectionStart, selectionEnd) => {
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1;
const nextNewLine = value.slice(selectionEnd).indexOf("\n") + selectionEnd;
const lines = value.slice(lastNewLine, nextNewLine).split("\n");
const processedLines = lines.map((line) =>
line.startsWith(prepend) ? line.slice(prepend.length) : line
);
let newValue = "";
newValue += value.slice(0, lastNewLine);
newValue += processedLines.join("\n");
newValue += value.slice(nextNewLine);
return {
prependLength: -prepend.length,
selectionLength: selectionEnd - selectionStart + (lines.length - 1) * -prepend.length,
transformedValue: newValue,
};
});
},
[transformationWrapper]
);
const togglePreline = useCallback(
(prepend: string) => {
if (!textAreaRef.current) {
return;
}
const { value, selectionStart, selectionEnd } = textAreaRef.current;
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1;
const nextNewLine = value.slice(selectionEnd).indexOf("\n") + selectionEnd;
const lines = value.slice(lastNewLine, nextNewLine).split("\n");
if (lines.every((line) => line.startsWith(prepend))) {
unpreline(prepend);
} else {
preline(prepend);
}
},
[preline, unpreline]
);
const insert = useCallback(
(prepend: string) => {
transformationWrapper((value, selectionStart) => {
@ -208,23 +271,48 @@ const Editor = (props: Props): JSX.Element => {
content={
<div className="grid gap-2">
<h3 className="text-lg">Headers</h3>
<Button onClick={() => preline("# ")} text={"H1"} />
<Button onClick={() => preline("## ")} text={"H2"} />
<Button onClick={() => preline("### ")} text={"H3"} />
<Button onClick={() => preline("#### ")} text={"H4"} />
<Button onClick={() => preline("##### ")} text={"H5"} />
<Button onClick={() => preline("###### ")} text={"H6"} />
<Button onClick={() => togglePreline("# ")} text={"H1"} />
<Button onClick={() => togglePreline("## ")} text={"H2"} />
<Button onClick={() => togglePreline("### ")} text={"H3"} />
<Button onClick={() => togglePreline("#### ")} text={"H4"} />
<Button onClick={() => togglePreline("##### ")} text={"H5"} />
<Button onClick={() => togglePreline("###### ")} text={"H6"} />
</div>
}>
<Button icon="title" />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Bold</h3>}>
<Button onClick={() => toggleWrap("**")} icon="format_bold" />
</ToolTip>
<ButtonGroup
buttonsProps={[
{
onClick: () => toggleWrap("**"),
tooltip: <h3 className="text-lg">Toggle Bold</h3>,
tooltipPlacement: "bottom",
icon: "format_bold",
},
{
onClick: () => toggleWrap("_"),
tooltip: <h3 className="text-lg">Toggle Italic</h3>,
tooltipPlacement: "bottom",
icon: "format_italic",
},
{
onClick: () => toggleWrap("u", {}),
tooltip: <h3 className="text-lg">Toggle Underline</h3>,
tooltipPlacement: "bottom",
icon: "format_underlined",
},
{
onClick: () => toggleWrap("~~"),
tooltip: <h3 className="text-lg">Toggle Strikethrough</h3>,
tooltipPlacement: "bottom",
icon: "strikethrough_s",
},
]}
/>
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Italic</h3>}>
<Button onClick={() => toggleWrap("_")} icon="format_italic" />
<ToolTip placement="bottom" content={<h3 className="text-lg">Highlight</h3>}>
<Button onClick={() => toggleWrap("==")} icon="format_ink_highlighter" />
</ToolTip>
<ToolTip
@ -241,6 +329,40 @@ const Editor = (props: Props): JSX.Element => {
<Button onClick={() => toggleWrap("`")} icon="code" />
</ToolTip>
<ButtonGroup
buttonsProps={[
{
onClick: () => togglePreline("- "),
icon: "format_list_bulleted",
tooltip: <h3 className="text-lg">Bulleted List</h3>,
tooltipPlacement: "bottom",
},
{
onClick: () => togglePreline("1. "),
icon: "format_list_numbered",
tooltip: <h3 className="text-lg">Numbered List</h3>,
tooltipPlacement: "bottom",
},
]}
/>
<ButtonGroup
buttonsProps={[
{
onClick: () => preline("\t"),
icon: "format_indent_increase",
tooltip: <h3 className="text-lg">Increase Indent</h3>,
tooltipPlacement: "bottom",
},
{
onClick: () => unpreline("\t"),
icon: "format_indent_decrease",
tooltip: <h3 className="text-lg">Decrease Indent</h3>,
tooltipPlacement: "bottom",
},
]}
/>
<ToolTip
placement="bottom"
content={
@ -299,12 +421,28 @@ const Editor = (props: Props): JSX.Element => {
<Button icon="record_voice_over" />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Inset box</h3>}>
<Button onClick={() => wrap("InsetBox", {}, true)} icon="check_box_outline_blank" />
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Layouts</h3>
<div className="grid gap-2">
<Button
onClick={() => wrap("InsetBox", {}, true)}
icon="check_box_outline_blank"
text="InsetBox"
/>
<Button onClick={() => togglePreline("> ")} icon="format_quote" text="Blockquote" />
</div>
</>
}>
<Button icon="dashboard" />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>}>
<Button onClick={() => insert("\n* * *\n")} icon="more_horiz" />
</ToolTip>
<ToolTip
content={
<div className="flex flex-col place-items-center gap-2">