Skip to content

Commit

Permalink
Resizable editor
Browse files Browse the repository at this point in the history
  • Loading branch information
x0k committed Jun 4, 2024
1 parent a8e1fde commit 09607a7
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 20 deletions.
20 changes: 20 additions & 0 deletions src/adapters/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { SyncStorage } from "@/shared";

export function createSyncStorage<T>(
storage: Storage,
key: string,
defaultValue: T
): SyncStorage<T> {
return {
load(): T {
const stored = storage.getItem(key);
if (stored === null) {
return defaultValue;
}
return JSON.parse(stored);
},
save(data: T): void {
storage.setItem(key, JSON.stringify(data));
},
};
}
5 changes: 3 additions & 2 deletions src/components/editor-testing-panel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@
<span class="loading loading-spinner"></span>
{:else}
<Icon class="w-6" icon="lucide:play" />
<span class="w-6 text-center"
>{Math.max(lastTestId, 0)}/{testData.length}</span

<span class="w-6 text-center" class:hidden={lastTestId === -1}
>{lastTestId}/{testData.length}</span
>
{/if}
</button>
74 changes: 63 additions & 11 deletions src/components/editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,28 @@
Language,
LANGUAGE_TITLE,
} from "@/lib/testing/languages";
import type { Position, SyncStorage } from '@/shared';
import { MONACO_LANGUAGE_ID } from "@/adapters/monaco";
import Resizer from './resizer.svelte';
interface Props {
languages: Lang[];
initialValue?: string;
defaultLanguage?: Lang;
children: Snippet<[Lang, editor.ITextModel]>;
onLanguageChange?: (lang: Lang, model: editor.ITextModel) => void;
widthStorage: SyncStorage<number>
}
const { initialValue = "", languages, defaultLanguage, children, onLanguageChange }: Props = $props();
const {
initialValue = "",
languages,
defaultLanguage,
children,
onLanguageChange,
widthStorage,
}: Props = $props();
let lang = $state(defaultLanguage ?? languages[0]);
Expand All @@ -32,24 +43,65 @@
editor.setModelLanguage(model, $state.snapshot(monacoLang));
});
let ed: editor.IStandaloneCodeEditor;
let editorElement: HTMLDivElement;
$effect(() => {
const ed = editor.create(editorElement, {
ed = editor.create(editorElement, {
model: model,
theme: "vs-dark",
automaticLayout: true,
minimap: {
enabled: false
}
});
return () => ed.dispose()
});
let windowWidth = $state(window.innerWidth)
function onWindowResize() {
windowWidth = window.innerWidth
}
$effect(() => {
window.addEventListener("resize", onWindowResize)
return () => {
window.removeEventListener("resize", onWindowResize)
}
})
function normalizeWidth(width: number) {
return Math.min(Math.max(width, 400), windowWidth)
}
let width = $state(normalizeWidth(widthStorage.load()))
let start: Position
let info: editor.EditorLayoutInfo
</script>

<div bind:this={editorElement} class="grow"></div>
<div class="p-4 border-t border-base-100 flex items-center gap-3">
{@render children(lang, model)}
<select class="select select-ghost select-sm ml-auto" bind:value={lang}>
{#each languages as lang (lang)}
<option value={lang}>{LANGUAGE_TITLE[lang]}</option>
{/each}
</select>
<div class="h-full flex flex-col relative bg-base-300">
<Resizer
onMoveStart={(e) => {
start = { x: e.clientX, y: e.clientY }
info = ed.getLayoutInfo();
}}
onMove={(e) => {
width = normalizeWidth(start.x - e.clientX + info.width)
}}
onMoveEnd={() => {
widthStorage.save(width)
ed.layout({
width: width,
height: info.height
})
}}
/>
<div bind:this={editorElement} class="grow" style="width: {width}px" ></div>
<div class="p-4 border-t border-base-100 flex items-center gap-3">
{@render children(lang, model)}
<select class="select select-ghost select-sm ml-auto" bind:value={lang}>
{#each languages as lang (lang)}
<option value={lang}>{LANGUAGE_TITLE[lang]}</option>
{/each}
</select>
</div>
</div>
37 changes: 37 additions & 0 deletions src/components/resizer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
interface Props {
onMove: (e: MouseEvent) => void;
onMoveStart?: (e: MouseEvent) => void;
onMoveEnd?: (e: MouseEvent) => void;
}
const { onMove, onMoveStart, onMoveEnd }: Props = $props();
let resizerElement: HTMLDivElement;
function onEndMove(e: MouseEvent) {
onMoveEnd?.(e);
window.removeEventListener("mouseup", onEndMove);
window.removeEventListener("mouseleave", onEndMove);
window.removeEventListener("mousemove", onMove);
}
function onStartMove(e: MouseEvent) {
onMoveStart?.(e);
window.addEventListener("mouseup", onEndMove);
window.addEventListener("mouseleave", onEndMove);
window.addEventListener("mousemove", onMove);
}
$effect(() => {
resizerElement.addEventListener("mousedown", onStartMove);
return () => {
resizerElement.removeEventListener("mousedown", onStartMove);
};
});
</script>

<div
bind:this={resizerElement}
class="absolute select-none w-1 z-50 h-full active:bg-primary hover:bg-primary/50 hover:cursor-col-resize"
></div>
11 changes: 9 additions & 2 deletions src/content/design-patterns/factory/editor.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script lang="ts">
import { Language, type TestRunnerFactory } from '@/lib/testing';
import { createSyncStorage } from '@/adapters/storage'
import Editor from '@/components/editor.svelte';
import EditorTestingPanel from '@/components/editor-testing-panel.svelte';
import { testsData, type Input, type Output } from './tests-data'
import { testRunnerFactory as phpTestRunnerFactory } from './php/test-runners'
import { jsTestRunnerFactory, tsTestRunnerFactory } from './js/test-runners'
Expand All @@ -22,14 +24,19 @@
[Language.JavaScript]: jsTestRunnerFactory,
[Language.Python]: pyTestRunnerFactory,
}
const defaultLanguage = Language.PHP;
const widthStorage = createSyncStorage(localStorage, "editor-width", window.innerWidth - 600)
const langStorage = createSyncStorage(localStorage, "editor-lang", Language.PHP)
const defaultLanguage = $state(langStorage.load());
</script>

<Editor
{defaultLanguage}
{widthStorage}
languages={Object.keys(TEST_RUNNER_FACTORIES) as Language[]}
onLanguageChange={async (lang, model) => {
model.setValue(await INITIAL_VALUES[lang])
langStorage.save(lang)
}}
>
{#snippet children(lang, model)}
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/problem.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const { title } = Astro.props;

<RootLayout title={title} class="h-screen">
<main class="h-full overflow-hidden flex">
<div class="h-full overflow-auto resize-x w-[800px]">
<div class="h-full overflow-auto grow">
<slot name="description" />
</div>
<div class="grow h-full overflow-auto">
<div class="h-full">
<EditorProvider>
<slot name="editor" />
</EditorProvider>
Expand Down
4 changes: 1 addition & 3 deletions src/pages/example.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,5 @@ import { Content as Description } from "@/content/design-patterns/factory/descri
</div>
</div>
</div>
<div slot="editor" class="flex flex-col h-full bg-base-300">
<Editor client:only="svelte" />
</div>
<Editor slot="editor" client:only="svelte" />
</Problem>
10 changes: 10 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ export enum Page {
}

export const TITLE = "Programming Patterns Practice";

export interface Position {
x: number;
y: number;
}

export interface SyncStorage<T> {
load(): T
save(data: T): void
}

0 comments on commit 09607a7

Please sign in to comment.