|
<!-- original from: https://github.com/touchifyapp/svelte-codemirror-editor/blob/main/src/lib/CodeMirror.svelte --> |
|
<script lang="ts"> |
|
import type { ViewUpdate } from '@codemirror/view'; |
|
|
|
import { createEventDispatcher, onMount } from 'svelte'; |
|
import { EditorView, keymap, placeholder as placeholderExt } from '@codemirror/view'; |
|
import { StateEffect, EditorState, type Extension } from '@codemirror/state'; |
|
import { indentWithTab } from '@codemirror/commands'; |
|
import { oneDark } from '@codemirror/theme-one-dark'; |
|
|
|
import IconSpin from '../Icons/IconSpin.svelte'; |
|
|
|
import { basicSetup } from './basicSetup'; |
|
import CodeMirrorSearch from '$lib/CodeMirrorSearch/CodeMirrorSearch.svelte'; |
|
|
|
export let classNames = ''; |
|
export let loaderClassNames = ''; |
|
export let value = ''; |
|
export let fontSize: string | undefined = undefined; |
|
|
|
export let basic = true; |
|
export let extensions: Extension[] = []; |
|
|
|
export let useTab = true; |
|
|
|
export let editable = true; |
|
export let readonly = false; |
|
export let placeholder: string | HTMLElement | null | undefined = undefined; |
|
export let focusOnMount = false; |
|
export let view: EditorView | undefined = undefined; |
|
|
|
const isBrowser = typeof window !== 'undefined'; |
|
const dispatch = createEventDispatcher<{ change: string }>(); |
|
let element: HTMLDivElement; |
|
let isSearchOpen = false; |
|
|
|
$: reconfigure(), extensions; |
|
$: setDoc(value); |
|
|
|
function setDoc(newDoc: string) { |
|
if (view && newDoc !== view.state.doc.toString()) { |
|
view.dispatch({ |
|
changes: { |
|
from: 0, |
|
to: view.state.doc.length, |
|
insert: newDoc |
|
} |
|
}); |
|
} |
|
} |
|
|
|
function createEditorView(): EditorView { |
|
return new EditorView({ |
|
parent: element, |
|
state: createEditorState(value) |
|
}); |
|
} |
|
|
|
function handleChange(vu: ViewUpdate): void { |
|
if (vu.docChanged) { |
|
const doc = vu.state.doc; |
|
const text = doc.toString(); |
|
dispatch('change', text); |
|
} |
|
} |
|
|
|
function getExtensions() { |
|
const stateExtensions = [ |
|
...getBaseExtensions(basic, useTab, placeholder, editable, readonly), |
|
...getTheme(), |
|
...extensions |
|
]; |
|
return stateExtensions; |
|
} |
|
|
|
function createEditorState(value: string | null | undefined): EditorState { |
|
return EditorState.create({ |
|
doc: value ?? undefined, |
|
extensions: getExtensions() |
|
}); |
|
} |
|
|
|
function getBaseExtensions( |
|
basic: boolean, |
|
useTab: boolean, |
|
placeholder: string | HTMLElement | null | undefined, |
|
editable: boolean, |
|
readonly: boolean |
|
): Extension[] { |
|
const extensions: Extension[] = [ |
|
EditorView.editable.of(editable), |
|
EditorState.readOnly.of(readonly) |
|
]; |
|
|
|
if (basic) { |
|
extensions.push(basicSetup); |
|
} |
|
if (useTab) { |
|
extensions.push(keymap.of([indentWithTab])); |
|
} |
|
if (placeholder) { |
|
extensions.push(placeholderExt(placeholder)); |
|
} |
|
if (fontSize) { |
|
extensions.push( |
|
EditorView.theme({ |
|
'&': { |
|
fontSize: fontSize |
|
} |
|
}) |
|
); |
|
} |
|
|
|
extensions.push(EditorView.updateListener.of(handleChange)); |
|
return extensions; |
|
} |
|
|
|
function getTheme(): Extension[] { |
|
const extensions: Extension[] = []; |
|
const isDarkMode = document.querySelector('body')?.classList.contains('dark') ?? false; |
|
if (isDarkMode) { |
|
extensions.push(oneDark); |
|
} |
|
return extensions; |
|
} |
|
|
|
function reconfigure(): void { |
|
view?.dispatch({ |
|
effects: StateEffect.reconfigure.of(getExtensions()) |
|
}); |
|
} |
|
|
|
function onKeyDown(e: KeyboardEvent) { |
|
const { ctrlKey, metaKey, key } = e; |
|
const isOpenShortcut = key === 'f3' || ((metaKey || ctrlKey) && key === 'f'); |
|
if (isOpenShortcut) { |
|
isSearchOpen = true; |
|
e.preventDefault(); |
|
} |
|
} |
|
|
|
onMount(() => { |
|
view = createEditorView(); |
|
if (view && focusOnMount) { |
|
const tr = view.state.update({ |
|
selection: { anchor: view.state.doc.length } |
|
}); |
|
view.dispatch(tr); |
|
view.focus(); |
|
} |
|
return () => view?.destroy(); |
|
}); |
|
</script> |
|
|
|
{#if isBrowser} |
|
<div class="relative"> |
|
<div class="codemirror-wrapper {classNames}" bind:this={element} on:keydown={onKeyDown} /> |
|
{#if isSearchOpen && view} |
|
<CodeMirrorSearch {view} on:close={() => (isSearchOpen = false)} /> |
|
{/if} |
|
</div> |
|
{:else} |
|
<div class="flex h-64 items-center justify-center {loaderClassNames}"> |
|
<IconSpin classNames="animate-spin text-xs" /> |
|
</div> |
|
{/if} |
|
|