Improved editor

This commit is contained in:
DrMint 2022-05-27 13:10:19 +02:00
parent cedc25862d
commit 2a799cf9e0
3 changed files with 251 additions and 167 deletions

View File

@ -38,8 +38,8 @@ export function Button(props: Immutable<Props>): JSX.Element {
draggable={draggable} draggable={draggable}
id={id} id={id}
onClick={onClick} onClick={onClick}
className={`--opacityBadge:100 grid select-none place-content-center className={`--opacityBadge:100 grid select-none grid-flow-col place-content-center
place-items-center rounded-full border-[1px] border-dark px-4 pt-[0.4rem] place-items-center gap-2 rounded-full border-[1px] border-dark px-4 pt-[0.4rem]
pb-[0.5rem] text-dark transition-all hover:[--opacityBadge:0] ${className} ${ pb-[0.5rem] text-dark transition-all hover:[--opacityBadge:0] ${className} ${
active active
? "cursor-not-allowed !border-black bg-black text-light drop-shadow-black-lg" ? "cursor-not-allowed !border-black bg-black text-light drop-shadow-black-lg"
@ -56,7 +56,7 @@ export function Button(props: Immutable<Props>): JSX.Element {
</div> </div>
)} )}
{icon && <Ico icon={icon} />} {icon && <Ico icon={icon} />}
<p>{text}</p> {text && <p>{text}</p>}
</div> </div>
); );

View File

@ -36,7 +36,6 @@ export function ReturnButton(props: Immutable<Props>): JSX.Element {
<Button <Button
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
href={props.href} href={props.href}
className="grid grid-flow-col gap-2"
text={`${props.langui.return_to} ${props.title}`} text={`${props.langui.return_to} ${props.title}`}
icon={Icon.NavigateBefore} icon={Icon.NavigateBefore}
/> />

View File

@ -13,6 +13,7 @@ import { GetStaticPropsContext } from "next";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import TurndownService from "turndown"; import TurndownService from "turndown";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { TOC } from "components/Markdown/TOC";
interface Props extends AppStaticProps {} interface Props extends AppStaticProps {}
@ -24,19 +25,124 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
const [markdown, setMarkdown] = useState(""); const [markdown, setMarkdown] = useState("");
const [converterOpened, setConverterOpened] = useState(false); const [converterOpened, setConverterOpened] = useState(false);
function insert( function wrap(
text: string, wrapper: string,
prepend: string, properties?: Record<string, string>,
append: 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) => {
let newValue = value + append;
return { prependLength: 0, transformedValue: newValue };
});
}
function transformationWrapper(
transformation: (
value: string,
selectionStart: number, selectionStart: number,
selectionEnd: number selectedEnd: number
): string { ) => { prependLength: number; transformedValue: string }
let newText = text.slice(0, selectionStart); ) {
newText += prepend; const textarea = document.querySelector(
newText += text.slice(selectionStart, selectionEnd); "#editorTextArea"
newText += append; ) as HTMLTextAreaElement;
newText += text.slice(selectionEnd); const { value, selectionStart, selectionEnd } = textarea;
return newText;
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 = ( const contentPanel = (
@ -79,90 +185,117 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
className="h-[50vh] w-[50vw] font-monospace mobile:w-[75vw]" className="h-[50vh] w-[50vw] font-monospace mobile:w-[75vw]"
/> />
</Popup> </Popup>
<div className="mb-4 flex flex-row gap-2"> <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 <ToolTip
placement="bottom" placement="bottom"
content={ content={
<> <>
<h3 className="text-lg">Transcript container</h3> <h3 className="text-lg">Toggle Inline Code</h3>
<p> <p>
Use this to create dialogues and transcripts. You can then add Makes the text monospace (like text from a computer terminal).
transcript speech line within ( Usually used for stylistic purposes in transcripts.
<Ico className="text-xs" icon={Icon.RecordVoiceOver} />)
</p> </p>
</> </>
} }
>
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Insert footnote</h3>
<p>When inserted &ldquo;x&rdquo;</p>
</>
}
> >
<Button <Button
onClick={() => { onClick={() => {
const textarea = document.querySelector( insert("[^x]");
"#editorTextArea" appendDoc("\n\n[^x]: This is a footnote.");
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"\n<Transcript>\n",
"\n</Transcript>\n",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}} }}
icon={Icon.QuestionAnswer} 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>
<ToolTip <ToolTip
placement="bottom" placement="right"
content={ content={
<> <>
<h3 className="text-lg">Transcript speech line</h3> <h3 className="text-lg">Transcript speech line</h3>
<p> <p>
Use to add a dialogue/transcript line. Change the{" "} Use to add a dialogue/transcript line. Change the{" "}
<kbd>name</kbd> property to chang the name of the speaker <kbd>name</kbd> property to chang the name of the
speaker
</p> </p>
</> </>
} }
> >
<Button <Button
onClick={() => { onClick={() => wrap("Line", { name: "speaker" })}
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
'<Line name="speaker">',
"</Line>\n",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.RecordVoiceOver} icon={Icon.RecordVoiceOver}
/> />
</ToolTip> </ToolTip>
<ToolTip </div>
placement="bottom" </>
content={<h3 className="text-lg">Vertical spacer</h3>} }
> >
<Button <Button icon={Icon.RecordVoiceOver} />
onClick={() => {
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"<Sep />",
"",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.DensityLarge}
/>
</ToolTip> </ToolTip>
<ToolTip <ToolTip
@ -170,20 +303,7 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
content={<h3 className="text-lg">Inset box</h3>} content={<h3 className="text-lg">Inset box</h3>}
> >
<Button <Button
onClick={() => { onClick={() => wrap("InsetBox", {}, true)}
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"\n<InsetBox>\n",
"\n</InsetBox>\n",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.CheckBoxOutlineBlank} icon={Icon.CheckBoxOutlineBlank}
/> />
</ToolTip> </ToolTip>
@ -191,28 +311,30 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
placement="bottom" placement="bottom"
content={<h3 className="text-lg">Scene break</h3>} content={<h3 className="text-lg">Scene break</h3>}
> >
<Button <Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
onClick={() => {
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"\n\n<SceneBreak />\n\n",
"",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.MoreHoriz}
/>
</ToolTip> </ToolTip>
<ToolTip <ToolTip
content={ content={
<div className="flex flex-col place-items-center gap-2"> <div className="flex flex-col place-items-center gap-2">
<h3 className="text-lg">Intralink</h3> <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 <ToolTip
placement="right" placement="right"
content={ content={
@ -226,21 +348,9 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
} }
> >
<Button <Button
onClick={() => { onClick={() => wrap("IntraLink", {})}
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"<IntraLink>",
"</IntraLink>",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.Link} icon={Icon.Link}
text={"Internal"}
/> />
</ToolTip> </ToolTip>
<ToolTip <ToolTip
@ -256,22 +366,9 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
} }
> >
<Button <Button
onClick={() => { onClick={() => wrap("IntraLink", { target: "target" })}
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
'<IntraLink target="target">',
"</IntraLink>",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.Link} icon={Icon.Link}
text="+ target" text="Internal (w/ target)"
/> />
</ToolTip> </ToolTip>
</div> </div>
@ -284,23 +381,7 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
placement="bottom" placement="bottom"
content={<h3 className="text-lg">Player&rsquo;s name placeholder</h3>} content={<h3 className="text-lg">Player&rsquo;s name placeholder</h3>}
> >
<Button <Button onClick={() => insert("<player>")} icon={Icon.Person} />
onClick={() => {
const textarea = document.querySelector(
"#editorTextArea"
) as HTMLTextAreaElement;
const { value, selectionStart, selectionEnd } = textarea;
textarea.value = insert(
value,
"<player>",
"",
selectionStart,
selectionEnd
);
handleInput(textarea.value);
}}
icon={Icon.Person}
/>
</ToolTip> </ToolTip>
<ToolTip <ToolTip
@ -325,8 +406,8 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
const textarea = event.target as HTMLTextAreaElement; const textarea = event.target as HTMLTextAreaElement;
handleInput(textarea.value); handleInput(textarea.value);
}} }}
className="h-[70vh] w-full rounded-xl className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8 font-monospace
bg-mid !bg-opacity-40 p-8 font-monospace text-black outline-none" text-black outline-none"
value={markdown} value={markdown}
title="Input textarea" title="Input textarea"
/> />
@ -338,6 +419,10 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-8">
<TOC text={markdown} />
</div>
</ContentPanel> </ContentPanel>
); );
return ( return (