-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: code mirror with custom themes and results window
- Loading branch information
1 parent
e5158ad
commit 1a901c9
Showing
6 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,253 @@ | ||
<script lang="ts"> | ||
import { EditorView, basicSetup } from 'codemirror' | ||
import { html } from '@codemirror/lang-html' | ||
import { css } from '@codemirror/lang-css' | ||
import { LanguageSupport } from '@codemirror/language' | ||
import { javascript } from '@codemirror/lang-javascript' | ||
import { EditorState } from '@codemirror/state' | ||
import prettier from 'prettier' | ||
import pluginHtml from 'prettier/plugins/html' | ||
import pluginCss from 'prettier/plugins/postcss' | ||
import pluginJS from 'prettier/plugins/babel' | ||
import pluginESTree from 'prettier/plugins/estree' | ||
import { myTheme } from './ashfid-theme.ts' | ||
import { onMount } from 'svelte' | ||
import PrettierIcon from './icons/prettier-icon.svelte' | ||
import LoadingSpinner from './icons/loading-spinner.svelte' | ||
import TwoColumnIcon from './icons/two-column-icon.svelte' | ||
import TwoRowIcon from './icons/two-row-icon.svelte' | ||
import ResetIcon from './icons/reset-icon.svelte' | ||
interface Props { | ||
title?: string | ||
baseHtml?: string | ||
baseStyles?: string | ||
baseScript?: string | ||
} | ||
let { | ||
title = 'Code Editor', | ||
baseHtml = $bindable(`<button>Click </button>`), | ||
baseStyles = $bindable(`body { background: #2d303e; }`), | ||
baseScript = $bindable(`const buttonReference = document.querySelector('button');buttonReference.addEventListener('click', () => alert("Hello")) | ||
`) | ||
}: Props = $props() | ||
const resetHTML = baseHtml | ||
const resetStyle = baseStyles | ||
const resetScript = baseScript | ||
// svelte-ignore non_reactive_update | ||
enum CodeType { | ||
HTML, | ||
CSS, | ||
JS | ||
} | ||
let editorElement: HTMLElement | ||
let editorView: EditorView | ||
let isFormatting = $state(false) | ||
let isResetting = $state(false) | ||
let isTwoColumn = $state(true) | ||
let debounceTimeout: number = -1 | ||
let debounceSeconds: number = 250 | ||
let currentCodeType = $state(CodeType.HTML) | ||
let src = $derived( | ||
`<html> | ||
<head> | ||
<${''}style>${baseStyles}<${''}/style> | ||
</head> | ||
<body>${baseHtml}</body> | ||
<${''}script>${baseScript}<${''}/script> | ||
</html>` | ||
) | ||
const createEditorState = ( | ||
doc: string, | ||
language: LanguageSupport, | ||
updateDoc: (doc: string) => {} | ||
) => | ||
EditorState.create({ | ||
doc, | ||
extensions: [ | ||
basicSetup, | ||
language, | ||
myTheme, | ||
EditorView.lineWrapping, | ||
EditorView.updateListener.of((v) => { | ||
if (v.docChanged) { | ||
clearTimeout(debounceTimeout) | ||
debounceTimeout = setTimeout(() => { | ||
updateDoc(v.state.doc.toString()) | ||
}, debounceSeconds) | ||
} | ||
}) | ||
] | ||
}) | ||
let editorState: EditorState = $derived.by(() => { | ||
switch (currentCodeType) { | ||
case CodeType.HTML: | ||
return createEditorState(baseHtml, html(), (doc) => (baseHtml = doc)) | ||
case CodeType.CSS: | ||
return createEditorState(baseStyles, css(), (doc) => (baseStyles = doc)) | ||
case CodeType.JS: | ||
return createEditorState(baseScript, javascript(), (doc) => (baseScript = doc)) | ||
} | ||
}) | ||
const format = async () => { | ||
switch (currentCodeType) { | ||
case CodeType.HTML: | ||
return await prettier.format(baseHtml, { | ||
parser: 'html', | ||
plugins: [pluginHtml, pluginCss, pluginESTree, pluginJS] | ||
}) | ||
case CodeType.CSS: | ||
return await prettier.format(baseStyles, { | ||
parser: 'css', | ||
plugins: [pluginCss] | ||
}) | ||
case CodeType.JS: | ||
return await prettier.format(baseScript, { | ||
parser: 'babel', | ||
plugins: [pluginESTree, pluginJS] | ||
}) | ||
} | ||
} | ||
const changeCodeType = async (newCodeType: CodeType) => { | ||
currentCodeType = newCodeType | ||
editorView.setState(editorState) | ||
editorView?.dispatch({ | ||
changes: { from: 0, to: editorView.state.doc.toString().length, insert: await format() } | ||
}) | ||
} | ||
const resetEditor = async () => { | ||
if (isResetting) return | ||
isResetting = true | ||
await new Promise((r) => setTimeout(r, 150)) | ||
baseHtml = resetHTML | ||
baseStyles = resetStyle | ||
baseScript = resetScript | ||
changeCodeType(currentCodeType) | ||
isResetting = false | ||
} | ||
onMount(async () => { | ||
editorView = new EditorView({ | ||
state: createEditorState(baseHtml, html(), (doc) => (baseHtml = doc)), | ||
parent: editorElement | ||
}) | ||
changeCodeType(CodeType.HTML) | ||
}) | ||
</script> | ||
|
||
<div class="rounded-md overflow-hidden bg-card pb-2 border border-b-4 border-highlight"> | ||
<header | ||
class="font-heading flex items-center justify-between px-4 py-2 border-b-2 border-b-highlight-hover" | ||
> | ||
<span class="font-semibold">{title}</span> | ||
<div class="flex gap-2 text-lg"> | ||
<button | ||
class="hover:text-xl" | ||
onclick={async () => { | ||
if (isFormatting) return | ||
isFormatting = true | ||
const string = await format() | ||
|
||
editorView?.dispatch({ | ||
changes: { from: 0, to: editorView.state.doc.toString().length, insert: string } | ||
}) | ||
await new Promise((r) => setTimeout(r, 500)) | ||
isFormatting = false | ||
}} | ||
> | ||
<span class="sr-only">{isFormatting ? 'Formatting in Progress' : 'Format'}</span> | ||
{#if !isFormatting} | ||
<PrettierIcon /> | ||
{:else} | ||
<LoadingSpinner /> | ||
{/if} | ||
</button> | ||
|
||
<button | ||
class="hover:text-xl" | ||
onclick={() => (isTwoColumn = !isTwoColumn)} | ||
class:text-icon-hover={isTwoColumn} | ||
> | ||
<span class="sr-only">{isTwoColumn ? 'Two Column Layout' : 'One Column Layout'}</span> | ||
{#if isTwoColumn} | ||
<TwoColumnIcon /> | ||
{:else} | ||
<TwoRowIcon /> | ||
{/if} | ||
</button> | ||
|
||
<button class="hover:text-xl" onclick={resetEditor}> | ||
<span class="sr-only">Reset</span> | ||
{#if isResetting} | ||
<LoadingSpinner /> | ||
{:else} | ||
<ResetIcon /> | ||
{/if} | ||
</button> | ||
</div> | ||
</header> | ||
<!-- Editor and Result --> | ||
<div class="grid bg-card px-2 overflow-hidden" class:grid-cols-2={isTwoColumn}> | ||
<!-- Editor --> | ||
<section> | ||
<header class="flex gap-4 py-2"> | ||
<button | ||
class:font-bold={currentCodeType === CodeType.HTML} | ||
class:border-b={currentCodeType === CodeType.HTML} | ||
class="uppercase font-heading border-highlight-hover" | ||
onclick={() => changeCodeType(CodeType.HTML)}>html</button | ||
> | ||
<button | ||
class:font-bold={currentCodeType === CodeType.CSS} | ||
class:border-b={currentCodeType === CodeType.CSS} | ||
class="uppercase font-heading border-highlight-hover" | ||
onclick={() => changeCodeType(CodeType.CSS)}>css</button | ||
> | ||
<button | ||
class:font-bold={currentCodeType === CodeType.JS} | ||
class:border-b={currentCodeType === CodeType.JS} | ||
class="uppercase font-heading border-highlight-hover" | ||
onclick={() => changeCodeType(CodeType.JS)}>javascript</button | ||
> | ||
</header> | ||
<div> | ||
<div bind:this={editorElement}></div> | ||
</div> | ||
</section> | ||
<!-- Result --> | ||
<section> | ||
<header class="font-bold uppercase font-heading tracking-wide py-2 pl-2">Output</header> | ||
<iframe | ||
title="code-preview" | ||
srcdoc={src} | ||
height="100%" | ||
width="100%" | ||
class="h-[400px] border-l-highlight-hover block" | ||
class:border-l-2={isTwoColumn} | ||
sandbox="allow-scripts allow-modals allow-same-origin" | ||
></iframe> | ||
</section> | ||
</div> | ||
</div> | ||
|
||
<style> | ||
:global(.cm-editor) { | ||
height: 400px; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}> | ||
<path | ||
fill="currentColor" | ||
d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" | ||
opacity="0.5" | ||
/> | ||
<path fill="currentColor" d="M20 12h2A10 10 0 0 0 12 2V4A8 8 0 0 1 20 12Z"> | ||
<animateTransform | ||
attributeName="transform" | ||
dur="1s" | ||
from="0 12 12" | ||
repeatCount="indefinite" | ||
to="360 12 12" | ||
type="rotate" | ||
/> | ||
</path> | ||
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<script lang="ts"> | ||
let { class: classname }: {class?: string} = $props() | ||
</script> | ||
|
||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="1em" | ||
height="1em" | ||
viewBox="0 0 16 16" | ||
class={classname as string} | ||
> | ||
<g fill="none" stroke-linecap="round" stroke-linejoin="round"> | ||
<path stroke="#7dc4e4" d="M1.5 2.5h11m-11 6h5" /> | ||
<path stroke="#eed49f" d="M1.5 4.5h5m3 4h5" /> | ||
<path stroke="#c6a0f6" d="M9.5 4.5h5m-13 2h5m-5 6h5" /> | ||
<path stroke="#ed8796" d="M9.5 6.5h5m-13 4h11m-11 4h5" /> | ||
</g> | ||
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32" {...$$props}> | ||
<path | ||
fill="currentColor" | ||
d="M18 28A12 12 0 1 0 6 16v6.2l-3.6-3.6L1 20l6 6l6-6l-1.4-1.4L8 22.2V16a10 10 0 1 1 10 10Z" | ||
/> | ||
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...$$props}> | ||
<path | ||
fill="currentColor" | ||
d="M6.25 3A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h11.5A3.25 3.25 0 0 0 21 17.75V6.25A3.25 3.25 0 0 0 17.75 3zM4.5 6.25c0-.966.784-1.75 1.75-1.75h5v15h-5a1.75 1.75 0 0 1-1.75-1.75zm8.25 13.25v-15h5c.966 0 1.75.784 1.75 1.75v11.5a1.75 1.75 0 0 1-1.75 1.75z" | ||
/> | ||
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32" {...$$props}> | ||
<path | ||
fill="currentColor" | ||
d="M3 7.5A4.5 4.5 0 0 1 7.5 3h17A4.5 4.5 0 0 1 29 7.5v17a4.5 4.5 0 0 1-4.5 4.5h-17A4.5 4.5 0 0 1 3 24.5zM7.5 5A2.5 2.5 0 0 0 5 7.5V15h22V7.5A2.5 2.5 0 0 0 24.5 5zM27 17H5v7.5A2.5 2.5 0 0 0 7.5 27h17a2.5 2.5 0 0 0 2.5-2.5z" | ||
/> | ||
</svg> |