Improve editor and no longer crash if markdawn Line has bad parameters
This commit is contained in:
parent
b6882cd1e5
commit
d68e238b00
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) => (
|
||||
<>
|
||||
<strong
|
||||
className={cJoin("!my-0 text-dark/60", cIf(!isContentPanelAtLeastLg, "!-mb-4"))}>
|
||||
<Markdawn text={compProps.name} />
|
||||
</strong>
|
||||
<p className="whitespace-pre-line">{compProps.children}</p>
|
||||
</>
|
||||
),
|
||||
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={safeProps.name} />
|
||||
</strong>
|
||||
<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>;
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue