Skip to content

Commit

Permalink
feat: code mirror with custom themes and results window
Browse files Browse the repository at this point in the history
  • Loading branch information
ashfidable committed Aug 31, 2024
1 parent e5158ad commit 1a901c9
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 0 deletions.
253 changes: 253 additions & 0 deletions src/components/code-editor/code-editor.svelte
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>
17 changes: 17 additions & 0 deletions src/components/code-editor/icons/loading-spinner.svelte
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>
18 changes: 18 additions & 0 deletions src/components/code-editor/icons/prettier-icon.svelte
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>
6 changes: 6 additions & 0 deletions src/components/code-editor/icons/reset-icon.svelte
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>
6 changes: 6 additions & 0 deletions src/components/code-editor/icons/two-column-icon.svelte
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>
6 changes: 6 additions & 0 deletions src/components/code-editor/icons/two-row-icon.svelte
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>

0 comments on commit 1a901c9

Please sign in to comment.