279 lines
7.2 KiB
Svelte
279 lines
7.2 KiB
Svelte
<script lang="ts" context="module">
|
|
type Attrs = {
|
|
[name: string]: string;
|
|
};
|
|
type AttrSource = Attrs | ((view: EditorView) => Attrs | null);
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
// look at https://github.com/sveltejs/learn.svelte.dev/blob/main/src/routes/tutorial/%5Bslug%5D/Editor.svelte
|
|
// import { javascript } from "@codemirror/lang-javascript";
|
|
import { onDestroy, onMount, createEventDispatcher } from "svelte";
|
|
import { theme } from "$lib/theme";
|
|
import { githubLight, githubDark } from "$lib/themes/github";
|
|
import { EditorView } from "@codemirror/view";
|
|
import {
|
|
EditorState,
|
|
StateEffect,
|
|
type Extension,
|
|
} from "@codemirror/state";
|
|
|
|
import { type LanguageSupport } from "@codemirror/language";
|
|
import { get_base_extensions } from "./editorExtensions";
|
|
|
|
export let value = "";
|
|
export let contentAttributes: AttrSource | null = null;
|
|
export let lang: LanguageSupport | null = null;
|
|
|
|
export let useTab = true;
|
|
export let tabSize = 2;
|
|
|
|
export let lineWrapping = false;
|
|
export let editable = true;
|
|
export let readonly = false;
|
|
export let placeholder: string | HTMLElement | null | undefined = undefined;
|
|
|
|
const is_browser = typeof window !== "undefined";
|
|
|
|
let element: HTMLDivElement;
|
|
let view: EditorView;
|
|
|
|
$: view && update(value);
|
|
$: view && state_extensions && reconfigure();
|
|
|
|
$: on_change = handle_change;
|
|
|
|
let update_from_prop = false;
|
|
let update_from_state = false;
|
|
let first_config = true;
|
|
let first_update = true;
|
|
|
|
let langPlugin = null;
|
|
|
|
let extensions: Extension[] = [];
|
|
|
|
$: state_extensions = [
|
|
...get_base_extensions(
|
|
useTab,
|
|
tabSize,
|
|
lineWrapping,
|
|
placeholder,
|
|
editable,
|
|
readonly,
|
|
lang,
|
|
),
|
|
$theme == "dark" ? githubDark : githubLight,
|
|
...extensions,
|
|
];
|
|
|
|
if (langPlugin !== null) extensions.push(langPlugin);
|
|
if (contentAttributes !== null)
|
|
extensions.push(EditorView.contentAttributes.of(contentAttributes));
|
|
|
|
onMount(() => {
|
|
view = create_editor_view();
|
|
dispatch("ready", view);
|
|
});
|
|
onDestroy(() => view?.destroy());
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
change: string;
|
|
ready: EditorView;
|
|
reconfigure: EditorView;
|
|
}>();
|
|
|
|
function create_editor_view(): EditorView {
|
|
return new EditorView({
|
|
parent: element,
|
|
state: create_editor_state(value),
|
|
dispatch(transaction) {
|
|
view.update([transaction]);
|
|
|
|
if (!update_from_prop && transaction.docChanged) {
|
|
on_change();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function reconfigure(): void {
|
|
if (first_config) {
|
|
first_config = false;
|
|
return;
|
|
}
|
|
|
|
view.dispatch({
|
|
effects: StateEffect.reconfigure.of(state_extensions),
|
|
});
|
|
|
|
dispatch("reconfigure", view);
|
|
}
|
|
|
|
function update(value: string | null | undefined): void {
|
|
if (first_update) {
|
|
first_update = false;
|
|
return;
|
|
}
|
|
|
|
if (update_from_state) {
|
|
update_from_state = false;
|
|
return;
|
|
}
|
|
|
|
update_from_prop = true;
|
|
|
|
if (value === undefined) {
|
|
return;
|
|
}
|
|
const currentValue = view ? view.state.doc.toString() : "";
|
|
if (view && value !== currentValue) {
|
|
view.dispatch({
|
|
changes: {
|
|
from: 0,
|
|
to: currentValue.length,
|
|
insert: value || "",
|
|
},
|
|
});
|
|
}
|
|
|
|
update_from_prop = false;
|
|
}
|
|
|
|
function handle_change(): void {
|
|
const new_value = view.state.doc.toString();
|
|
if (new_value === value) return;
|
|
|
|
update_from_state = true;
|
|
|
|
value = new_value;
|
|
dispatch("change", value);
|
|
}
|
|
|
|
function create_editor_state(
|
|
value: string | null | undefined,
|
|
): EditorState {
|
|
return EditorState.create({
|
|
doc: value ?? undefined,
|
|
extensions: state_extensions,
|
|
});
|
|
}
|
|
|
|
// $: console.log(value)
|
|
|
|
// import { linter, lintGutter } from "@codemirror/lint";
|
|
// import * as eslint from "eslint-linter-browserify";
|
|
|
|
// lintGutter(),
|
|
// linter(esLint(new eslint.Linter(), config)),
|
|
</script>
|
|
|
|
<div class="editor-wrapper card" class:no-header={!$$slots.header}>
|
|
{#if $$slots.header}
|
|
<div class="header">
|
|
<slot name="header" />
|
|
</div>
|
|
{/if}
|
|
{#if is_browser}
|
|
<div class="codemirror-wrapper editor" bind:this={element} />
|
|
{:else}
|
|
<div class="scm-waiting editor">
|
|
<div class="scm-waiting__loading scm-loading">
|
|
<div class="scm-loading__spinner" />
|
|
<p class="scm-loading__text">Loading editor...</p>
|
|
</div>
|
|
<div class="cm-editor"><pre class="scm-pre">{value}</pre></div>
|
|
|
|
</div>
|
|
{/if}
|
|
<!-- <CodeMirror
|
|
{value}
|
|
class="editor"
|
|
theme={$theme == "dark" ? githubDark : githubLight}
|
|
{extensions}
|
|
{readonly}
|
|
on:change
|
|
/> -->
|
|
</div>
|
|
|
|
<!-- <CodeMirror basic={true} bind:value lang={javascript({})} class="editor" /> -->
|
|
|
|
<style>
|
|
.editor-wrapper {
|
|
/* min-height: 200px; */
|
|
position: relative;
|
|
z-index: 1;
|
|
background-color: var(--surface-secondary-color);
|
|
}
|
|
|
|
.codemirror-wrapper :global(.cm-focused) {
|
|
outline: none;
|
|
}
|
|
|
|
.scm-waiting {
|
|
position: relative;
|
|
}
|
|
.scm-waiting__loading {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: 0;
|
|
background-color: var(--shadow-color);
|
|
}
|
|
|
|
.scm-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.scm-loading__spinner {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border-radius: 100%;
|
|
border: solid 2px var(--theme);
|
|
border-top-color: transparent;
|
|
margin-right: 0.75rem;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
.scm-loading__text {
|
|
font-family: sans-serif;
|
|
}
|
|
.scm-pre {
|
|
font-size: 0.85rem;
|
|
margin: 0;
|
|
padding: 4px 2px 4px 6px;
|
|
font-family: monospace;
|
|
tab-size: 2;
|
|
-moz-tab-size: 2;
|
|
resize: none;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
overflow: auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
:global(.editor-wrapper .cm-scroller, .editor-wrapper .cm-editor) {
|
|
min-height: 200px;
|
|
border-bottom-left-radius: var(--border-radius);
|
|
border-bottom-right-radius: var(--border-radius);
|
|
/* box-shadow: var(--shadow);
|
|
background-color: var(--surface-color); */
|
|
}
|
|
:global(
|
|
.editor-wrapper.no-header .cm-scroller,
|
|
.editor-wrapper.no-header .cm-editor
|
|
) {
|
|
border-radius: var(--border-radius);
|
|
}
|
|
:global(pre.cm-editor) {
|
|
margin: 0;
|
|
}
|
|
</style>
|