| <!-- 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} |
|
|