diff options
Diffstat (limited to 'src/engine/utils')
-rw-r--r-- | src/engine/utils/CodeEditor.ts | 200 | ||||
-rw-r--r-- | src/engine/utils/Modal.ts | 45 | ||||
-rw-r--r-- | src/engine/utils/index.ts | 4 | ||||
-rw-r--r-- | src/engine/utils/modal.ts | 41 | ||||
-rw-r--r-- | src/engine/utils/tryWrap.ts | 7 |
5 files changed, 255 insertions, 42 deletions
diff --git a/src/engine/utils/CodeEditor.ts b/src/engine/utils/CodeEditor.ts new file mode 100644 index 0000000..e13d1d4 --- /dev/null +++ b/src/engine/utils/CodeEditor.ts @@ -0,0 +1,200 @@ +import { + EditorState, + StateField, + StateEffect, + Range, + Extension, +} from "@codemirror/state"; +import { Decoration, EditorView, keymap } from "@codemirror/view"; +import { defaultKeymap } from "@codemirror/commands"; +import rainbowBrackets from "rainbowbrackets"; +import { basicSetup } from "codemirror"; +import { ModalInstance, ModalSingleton } from "."; +import { EditorSave, Failure, ModalOpen, SOUNDS } from "../config"; + +export interface CodeEditorError extends Error { + location: { + start: { offset: number }; + end: { offset: number }; + }; +} + +export type CodeConsumer = (code: string) => { + consumed?: boolean; + error?: CodeEditorError; +}; + +interface CodeEditorState { + view: EditorView; + editorElement: HTMLElement; + errorElement: HTMLElement; + resultElement: HTMLElement; + closeButton: HTMLButtonElement; + codeConsumer: CodeConsumer; +} + +export class CodeEditorInstance { + constructor( + private modalInstance: ModalInstance = ModalSingleton, + private codeEditorState: CodeEditorState | null = null + ) {} + + public close() { + if (!this.codeEditorState) { + return; + } + + this.codeEditorState.view.destroy(); + this.codeEditorState = null; + + SOUNDS.get(EditorSave.name)!.play(); + } + + public open( + initCode: string, + codeConsumer: CodeEditorState["codeConsumer"], + readonly: boolean = false, + initResult: { data?: string; error?: string } = {}, + ) { + if (this.codeEditorState) { + throw new Error("code editor instance is already owned."); + } + const modalContent = ` + <div class='code'> + <div id='code'></div> + <br> + <p id='error' class='error'></p> + <p id='result' class='result'></p> + <button id='close-modal'>Done</button> + </div> + `; + this.modalInstance.open(modalContent); + + const startState = EditorState.create({ + doc: initCode, + extensions: [ + basicSetup, + keymap.of(defaultKeymap), + rainbowBrackets(), + highlightExtension, + FontSizeThemeExtension, + EditorState.readOnly.of(readonly), + ], + }); + + const codeBox = document.getElementById("code")!; + const errorElement = document.getElementById("error")!; + const resultElement = document.getElementById("result")!; + + const closeButton = document.getElementById( + "close-modal" + ) as HTMLButtonElement; + closeButton.addEventListener("click", () => this.onSave()); + + const editorView = new EditorView({ + state: startState, + parent: codeBox, + }); + editorView.focus(); + + this.codeEditorState = { + view: editorView, + editorElement: codeBox, + errorElement, + resultElement, + closeButton, + codeConsumer, + }; + this.setResult(initResult); + } + + private refreshCodeEditorText(text: string) { + if (!this.codeEditorState) { + return; + } + const { view } = this.codeEditorState; + + view.dispatch({ + changes: { + from: 0, + to: text.length, + insert: "", + }, + }); + view.dispatch({ + changes: { + from: 0, + to: 0, + insert: text, + }, + }); + } + + private onSave() { + if (!this.codeEditorState) { + throw new Error(); + } + + const { view } = this.codeEditorState; + const code = view.state.doc.toString(); + this.refreshCodeEditorText(code); + + const valid = this.codeEditorState.codeConsumer(code); + if (!valid.error) { + return; + } + + const { + location: { + start: { offset: start }, + end: { offset: end }, + }, + message, + } = valid.error; + + view.dispatch({ + effects: highlightEffect.of([ + syntaxErrorDecoration.range(start === end ? start - 1 : start, end), + ]), + }); + + this.setResult({ error: message }); + SOUNDS.get(Failure.name)!.play(); + } + + private setResult(result: { data?: string; error?: string }) { + if (!this.codeEditorState) return; + this.codeEditorState.resultElement.innerText = result.data ?? ""; + this.codeEditorState.errorElement.innerText = result.error ?? ""; + } +} + +const highlightEffect = StateEffect.define<Range<Decoration>[]>(); +const highlightExtension = StateField.define({ + create() { + return Decoration.none; + }, + update(value, transaction) { + value = value.map(transaction.changes); + + for (let effect of transaction.effects) { + if (effect.is(highlightEffect)) + value = value.update({ add: effect.value, sort: true }); + } + + return value; + }, + provide: (f) => EditorView.decorations.from(f), +}); + +const FontSizeTheme = EditorView.theme({ + $: { + fontSize: "14pt", + }, +}); +const FontSizeThemeExtension: Extension = [FontSizeTheme]; +const syntaxErrorDecoration = Decoration.mark({ + class: "syntax-error", +}); + +export const CodeEditorSingleton = new CodeEditorInstance(); diff --git a/src/engine/utils/Modal.ts b/src/engine/utils/Modal.ts new file mode 100644 index 0000000..d46cade --- /dev/null +++ b/src/engine/utils/Modal.ts @@ -0,0 +1,45 @@ +import { Miscellaneous } from "../config"; + +export class ModalInstance { + private modalOpen: boolean = false; + private elementId: string = Miscellaneous.MODAL_ID; + private contentId: string = Miscellaneous.MODAL_CONTENT_ID; + + public open(content: string) { + const modal = document.getElementById(this.elementId); + const modalContent = document.getElementById(this.contentId); + if (!modal || this.modalOpen) { + return; + } + + this.modalOpen = true; + modal.style.display = "flex"; + modal.style.animation = "fadeIn 0.25s"; + + modalContent!.innerHTML = content; + modalContent!.style.animation = "scaleUp 0.25s"; + } + + public vanish(): Promise<void> { + const modal = document.getElementById(this.elementId); + const modalContent = document.getElementById(this.contentId); + return new Promise((res, _rej) => { + if (!(modal && this.modalOpen && modalContent)) { + res(); + return; + } + modal.style.animation = "fadeOut 0.25s"; + modalContent.style.animation = "scaleDown 0.25s"; + + setTimeout(() => { + modalContent.innerHTML = ""; + modal.style.display = "none"; + + this.modalOpen = false; + res(); + }, 200); + }); + } +} + +export const ModalSingleton = new ModalInstance();
\ No newline at end of file diff --git a/src/engine/utils/index.ts b/src/engine/utils/index.ts index 48b94b8..ffeef7a 100644 --- a/src/engine/utils/index.ts +++ b/src/engine/utils/index.ts @@ -1,6 +1,8 @@ export * from "./clamp"; export * from "./dotProduct"; export * from "./rotateVector"; -export * from "./modal"; export * from "./colors"; export * from "./random"; +export * from './tryWrap'; +export * from "./Modal"; +export * from "./CodeEditor";
\ No newline at end of file diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts deleted file mode 100644 index a378821..0000000 --- a/src/engine/utils/modal.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Miscellaneous } from "../config"; - -let modalOpen = false; - -export const openModal = ( - content: string, - id = Miscellaneous.MODAL_ID, - contentId = Miscellaneous.MODAL_CONTENT_ID, -) => { - const modal = document.getElementById(id); - const modalContent = document.getElementById(contentId); - if (modal && !modalOpen && modalContent) { - modal.style.display = "flex"; - modal.style.animation = "fadeIn 0.25s"; - - modalContent.innerHTML = content; - modalContent.style.animation = "scaleUp 0.25s"; - - modalOpen = true; - } -}; - -export const closeModal = ( - id = Miscellaneous.MODAL_ID, - contentId = Miscellaneous.MODAL_CONTENT_ID, -) => { - const modal = document.getElementById(id); - const modalContent = document.getElementById(contentId); - - if (modal && modalOpen && modalContent) { - modal.style.animation = "fadeOut 0.25s"; - modalContent.style.animation = "scaleDown 0.25s"; - - setTimeout(() => { - modalContent.innerHTML = ""; - modal.style.display = "none"; - - modalOpen = false; - }, 200); - } -}; diff --git a/src/engine/utils/tryWrap.ts b/src/engine/utils/tryWrap.ts new file mode 100644 index 0000000..12e23ea --- /dev/null +++ b/src/engine/utils/tryWrap.ts @@ -0,0 +1,7 @@ +export const tryWrap = <T>(supplier: () => T): { data?: T; error?: any } => { + try { + return { data: supplier() }; + } catch (error) { + return { error: error as any }; + } +}; |