Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| import { createEventDispatcher, onMount } from "svelte"; | |
| import { | |
| Decoration, | |
| EditorView, | |
| ViewUpdate, | |
| keymap, | |
| placeholder as placeholderExt | |
| } from "@codemirror/view"; | |
| import { StateEffect, EditorState, type Extension } from "@codemirror/state"; | |
| import { indentWithTab } from "@codemirror/commands"; | |
| import { basicDark } from "cm6-theme-basic-dark"; | |
| import { basicLight } from "cm6-theme-basic-light"; | |
| import { basicSetup } from "./extensions"; | |
| import { getLanguageExtension } from "./language"; | |
| export let classNames = ""; | |
| export let value = ""; | |
| export let dark_mode: boolean; | |
| export let basic = true; | |
| export let language: string; | |
| export let lines = 5; | |
| export let extensions: Extension[] = []; | |
| export let highlights: [number, string][] = []; | |
| export let useTab = true; | |
| export let readonly = false; | |
| export let placeholder: string | HTMLElement | null | undefined = undefined; | |
| const dispatch = createEventDispatcher<{ | |
| change: string; | |
| blur: undefined; | |
| focus: undefined; | |
| }>(); | |
| let lang_extension: Extension | undefined; | |
| let element: HTMLDivElement; | |
| let view: EditorView; | |
| $: get_lang(language); | |
| async function get_lang(val: string): Promise<void> { | |
| const ext = await getLanguageExtension(val); | |
| lang_extension = ext; | |
| } | |
| $: reconfigure(), lang_extension; | |
| $: setDoc(value); | |
| $: updateLines(); | |
| function highlight_lines(root_element: HTMLDivElement) { | |
| const lines = root_element.querySelectorAll('.cm-line'); | |
| let old_color = 'transparent'; | |
| lines.forEach((e, index) => { | |
| const line_number = index + 1; | |
| let new_color = highlights.find(([l, c]) => l === line_number)?.[1] ?? old_color; | |
| e.style.backgroundColor = new_color; | |
| old_color = new_color; | |
| }); | |
| } | |
| function setDoc(newDoc: string): void { | |
| if (view && newDoc !== view.state.doc.toString()) { | |
| view.dispatch({ | |
| changes: { | |
| from: 0, | |
| to: view.state.doc.length, | |
| insert: newDoc | |
| } | |
| }); | |
| } | |
| } | |
| function updateLines(): void { | |
| if (view) { | |
| view.requestMeasure({ read: updateGutters }); | |
| } | |
| } | |
| function createEditorView(): EditorView { | |
| const editorView = new EditorView({ | |
| parent: element, | |
| state: createEditorState(value) | |
| }); | |
| editorView.dom.addEventListener("focus", handleFocus, true); | |
| editorView.dom.addEventListener("blur", handleBlur, true); | |
| return editorView; | |
| } | |
| function handleFocus(): void { | |
| dispatch("focus"); | |
| } | |
| function handleBlur(): void { | |
| dispatch("blur"); | |
| } | |
| function getGutterLineHeight(_view: EditorView): string | null { | |
| let elements = _view.dom.querySelectorAll<HTMLElement>(".cm-gutterElement"); | |
| if (elements.length === 0) { | |
| return null; | |
| } | |
| for (var i = 0; i < elements.length; i++) { | |
| let node = elements[i]; | |
| let height = getComputedStyle(node)?.height ?? "0px"; | |
| if (height != "0px") { | |
| return height; | |
| } | |
| } | |
| return null; | |
| } | |
| function updateGutters(_view: EditorView): any { | |
| let gutters = _view.dom.querySelectorAll<HTMLElement>(".cm-gutter"); | |
| let _lines = lines + 1; | |
| let lineHeight = getGutterLineHeight(_view); | |
| if (!lineHeight) { | |
| return null; | |
| } | |
| for (var i = 0; i < gutters.length; i++) { | |
| let node = gutters[i]; | |
| node.style.minHeight = `calc(${lineHeight} * ${_lines})`; | |
| } | |
| highlight_lines(element); | |
| return null; | |
| } | |
| function handleChange(vu: ViewUpdate): void { | |
| if (vu.docChanged) { | |
| const doc = vu.state.doc; | |
| const text = doc.toString(); | |
| value = text; | |
| dispatch("change", text); | |
| } | |
| view.requestMeasure({ read: updateGutters }); | |
| } | |
| function getExtensions(): Extension[] { | |
| const stateExtensions = [ | |
| ...getBaseExtensions( | |
| basic, | |
| useTab, | |
| placeholder, | |
| readonly, | |
| lang_extension | |
| ), | |
| FontTheme, | |
| ...getTheme(), | |
| ...extensions | |
| ]; | |
| return stateExtensions; | |
| } | |
| const FontTheme = EditorView.theme({ | |
| "&": { | |
| fontSize: "var(--text-sm)", | |
| backgroundColor: "var(--border-color-secondary)" | |
| }, | |
| ".cm-content": { | |
| paddingTop: "5px", | |
| paddingBottom: "5px", | |
| color: "var(--body-text-color)", | |
| fontFamily: "var(--font-mono)", | |
| minHeight: "100%" | |
| }, | |
| ".cm-gutters": { | |
| marginRight: "1px", | |
| borderRight: "1px solid var(--border-color-primary)", | |
| backgroundColor: "transparent", | |
| color: "var(--body-text-color-subdued)" | |
| }, | |
| ".cm-focused": { | |
| outline: "none" | |
| }, | |
| ".cm-scroller": { | |
| height: "auto" | |
| }, | |
| ".cm-cursor": { | |
| borderLeftColor: "var(--body-text-color)" | |
| } | |
| }); | |
| 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, | |
| readonly: boolean, | |
| lang: Extension | null | undefined | |
| ): Extension[] { | |
| const extensions: Extension[] = [ | |
| EditorView.editable.of(!readonly), | |
| EditorState.readOnly.of(readonly), | |
| EditorView.contentAttributes.of({ "aria-label": "Code input container" }) | |
| ]; | |
| if (basic) { | |
| extensions.push(basicSetup); | |
| } | |
| if (useTab) { | |
| extensions.push(keymap.of([indentWithTab])); | |
| } | |
| if (placeholder) { | |
| extensions.push(placeholderExt(placeholder)); | |
| } | |
| if (lang) { | |
| extensions.push(lang); | |
| } | |
| extensions.push(EditorView.updateListener.of(handleChange)); | |
| return extensions; | |
| } | |
| function getTheme(): Extension[] { | |
| const extensions: Extension[] = []; | |
| if (dark_mode) { | |
| extensions.push(basicDark); | |
| } else { | |
| extensions.push(basicLight); | |
| } | |
| return extensions; | |
| } | |
| function reconfigure(): void { | |
| view?.dispatch({ | |
| effects: StateEffect.reconfigure.of(getExtensions()) | |
| }); | |
| } | |
| onMount(() => { | |
| view = createEditorView(); | |
| return () => view?.destroy(); | |
| }); | |
| </script> | |
| <div class="wrap"> | |
| <div class="codemirror-wrapper {classNames}" bind:this={element} /> | |
| </div> | |
| <style> | |
| .wrap { | |
| display: flex; | |
| flex-direction: column; | |
| flex-flow: column; | |
| margin: 0; | |
| padding: 0; | |
| height: 100%; | |
| } | |
| .codemirror-wrapper { | |
| height: 100%; | |
| overflow: auto; | |
| } | |
| :global(.cm-editor) { | |
| height: 100%; | |
| } | |
| /* Dunno why this doesn't work through the theme API -- don't remove*/ | |
| :global(.cm-selectionBackground) { | |
| background-color: #b9d2ff30 ; | |
| } | |
| :global(.cm-focused) { | |
| outline: none ; | |
| } | |
| </style> | |