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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint