import { AppLayout } from "components/AppLayout"; import { Button } from "components/Inputs/Button"; import { Markdawn } from "components/Markdown/Markdawn"; import { ContentPanel, ContentPanelWidthSizes, } from "components/Panels/ContentPanel"; import { Popup } from "components/Popup"; import { ToolTip } from "components/ToolTip"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { Immutable } from "helpers/types"; import { GetStaticPropsContext } from "next"; import { useCallback, useState } from "react"; import TurndownService from "turndown"; import { Icon } from "components/Ico"; import { TOC } from "components/Markdown/TOC"; interface Props extends AppStaticProps {} export default function Editor(props: Immutable<Props>): JSX.Element { const handleInput = useCallback((text: string) => { setMarkdown(text); }, []); const [markdown, setMarkdown] = useState(""); const [converterOpened, setConverterOpened] = useState(false); function wrap( wrapper: string, properties?: Record<string, string>, addInnerNewLines?: boolean ) { transformationWrapper((value, selectionStart, selectionEnd) => { let prepend = wrapper; let append = wrapper; if (properties) { prepend = `<${wrapper}${Object.entries(properties).map( ([propertyName, propertyValue]) => ` ${propertyName}="${propertyValue}"` )}>`; append = `</${wrapper}>`; } if (addInnerNewLines) { prepend = `${prepend}\n`; append = `\n${append}`; } let newValue = ""; newValue += value.slice(0, selectionStart); newValue += prepend; newValue += value.slice(selectionStart, selectionEnd); newValue += append; newValue += value.slice(selectionEnd); return { prependLength: prepend.length, transformedValue: newValue }; }); } function toggleWrap( wrapper: string, properties?: Record<string, string>, addInnerNewLines?: boolean ) { const textarea = document.querySelector( "#editorTextArea" ) as HTMLTextAreaElement; const { value, selectionStart, selectionEnd } = textarea; if ( value.slice(selectionStart - wrapper.length, selectionStart) === wrapper && value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper ) { unwrap(wrapper); } else { wrap(wrapper, properties, addInnerNewLines); } } function unwrap(wrapper: string) { transformationWrapper((value, selectionStart, selectionEnd) => { let newValue = ""; newValue += value.slice(0, selectionStart - wrapper.length); newValue += value.slice(selectionStart, selectionEnd); newValue += value.slice(wrapper.length + selectionEnd); return { prependLength: -wrapper.length, transformedValue: newValue }; }); } function preline(prepend: string) { transformationWrapper((value, selectionStart) => { const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1; let newValue = ""; newValue += value.slice(0, lastNewLine); newValue += prepend; newValue += value.slice(lastNewLine); return { prependLength: prepend.length, transformedValue: newValue }; }); } function insert(prepend: string) { transformationWrapper((value, selectionStart) => { let newValue = ""; newValue += value.slice(0, selectionStart); newValue += prepend; newValue += value.slice(selectionStart); return { prependLength: prepend.length, transformedValue: newValue }; }); } function appendDoc(append: string) { transformationWrapper((value) => { const newValue = value + append; return { prependLength: 0, transformedValue: newValue }; }); } function transformationWrapper( transformation: ( value: string, selectionStart: number, selectedEnd: number ) => { prependLength: number; transformedValue: string } ) { const textarea = document.querySelector( "#editorTextArea" ) as HTMLTextAreaElement; const { value, selectionStart, selectionEnd } = textarea; const { prependLength, transformedValue } = transformation( value, selectionStart, selectionEnd ); textarea.value = transformedValue; handleInput(textarea.value); textarea.focus(); textarea.selectionStart = selectionStart + prependLength; textarea.selectionEnd = selectionEnd + prependLength; } const contentPanel = ( <ContentPanel width={ContentPanelWidthSizes.Large}> <Popup setState={setConverterOpened} state={converterOpened}> <div className="text-center"> <h2 className="mt-4">Convert HTML to markdown</h2> <p> Copy and paste any HTML content (content from web pages) here.{" "} <br /> The text will immediatly be converted to valid Markdown. <br /> You can then copy the converted text and paste it anywhere you want in the editor </p> </div> <textarea readOnly id="htmlMdTextArea" title="Ouput textarea" onPaste={(event) => { const turndownService = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced", bulletListMarker: "-", emDelimiter: "_", strongDelimiter: "**", }); let paste = event.clipboardData.getData("text/html"); paste = paste.replace(/<!--.*?-->/u, ""); paste = turndownService.turndown(paste); paste = paste.replace(/<!--.*?-->/u, ""); const target = event.target as HTMLTextAreaElement; target.value = paste; target.select(); event.preventDefault(); }} className="h-[50vh] w-[50vw] font-monospace mobile:w-[75vw]" /> </Popup> <div className="mb-4 flex flex-row gap-2"> <ToolTip 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"} /> </div> } > <Button icon={Icon.Title} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Bold</h3>} > <Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Italic</h3>} > <Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} /> </ToolTip> <ToolTip placement="bottom" content={ <> <h3 className="text-lg">Toggle Inline Code</h3> <p> Makes the text monospace (like text from a computer terminal). Usually used for stylistic purposes in transcripts. </p> </> } > <Button onClick={() => toggleWrap("`")} icon={Icon.Code} /> </ToolTip> <ToolTip placement="bottom" content={ <> <h3 className="text-lg">Insert footnote</h3> <p>When inserted “x”</p> </> } > <Button onClick={() => { insert("[^x]"); appendDoc("\n\n[^x]: This is a footnote."); }} icon={Icon.Superscript} /> </ToolTip> <ToolTip placement="bottom" content={ <> <h3 className="text-lg">Transcripts</h3> <p> Use this to create dialogues and transcripts. Start by adding a container, then add transcript speech line within. </p> <div className="grid gap-2"> <ToolTip placement="right" content={ <> <h3 className="text-lg">Transcript container</h3> </> } > <Button onClick={() => wrap("Transcript", {}, true)} icon={Icon.AddBox} /> </ToolTip> <ToolTip placement="right" content={ <> <h3 className="text-lg">Transcript speech line</h3> <p> Use to add a dialogue/transcript line. Change the{" "} <kbd>name</kbd> property to chang the name of the speaker </p> </> } > <Button onClick={() => wrap("Line", { name: "speaker" })} icon={Icon.RecordVoiceOver} /> </ToolTip> </div> </> } > <Button icon={Icon.RecordVoiceOver} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Inset box</h3>} > <Button onClick={() => wrap("InsetBox", {}, true)} icon={Icon.CheckBoxOutlineBlank} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>} > <Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} /> </ToolTip> <ToolTip content={ <div className="flex flex-col place-items-center gap-2"> <h3 className="text-lg">Links</h3> <ToolTip placement="right" content={ <> <h3 className="text-lg">External Link</h3> <p className="text-xs"> Provides a link to another webpage / website </p> </> } > <Button onClick={() => insert("[Link name](https://domain.com)")} icon={Icon.Link} text={"External"} /> </ToolTip> <ToolTip placement="right" content={ <> <h3 className="text-lg">Intralink</h3> <p className="text-xs"> Interlinks are used to add links to a header within the same document </p> </> } > <Button onClick={() => wrap("IntraLink", {})} icon={Icon.Link} text={"Internal"} /> </ToolTip> <ToolTip placement="right" content={ <> <h3 className="text-lg">Intralink (with target)</h3>{" "} <p className="text-xs"> Use this one if you want the intralink text to be different from the target header’s name. </p> </> } > <Button onClick={() => wrap("IntraLink", { target: "target" })} icon={Icon.Link} text="Internal (w/ target)" /> </ToolTip> </div> } > <Button icon={Icon.Link} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Player’s name placeholder</h3>} > <Button onClick={() => insert("<player>")} icon={Icon.Person} /> </ToolTip> <ToolTip placement="bottom" content={<h3 className="text-lg">Open HTML Converter</h3>} > <Button onClick={() => { setConverterOpened(true); }} icon={Icon.Html} /> </ToolTip> </div> <div className="grid grid-cols-2 gap-8"> <div> <h2>Editor</h2> <textarea id="editorTextArea" onInput={(event) => { const textarea = event.target as HTMLTextAreaElement; handleInput(textarea.value); }} className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8 font-monospace text-black outline-none" value={markdown} title="Input textarea" /> </div> <div> <h2>Preview</h2> <div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8"> <Markdawn className="w-full" text={markdown} /> </div> </div> </div> <div className="mt-8"> <TOC text={markdown} /> </div> </ContentPanel> ); return ( <AppLayout navTitle="Markdawn Editor" contentPanel={contentPanel} {...props} /> ); } export async function getStaticProps( context: GetStaticPropsContext ): Promise<{ notFound: boolean } | { props: Props }> { const props: Props = { ...(await getAppStaticProps(context)), }; return { props: props, }; }