UNPKG

@dodona/papyros

Version:

Scratchpad for multiple programming languages in the browser.

168 lines 6.11 kB
import { Compartment, EditorState, StateEffect } from "@codemirror/state"; import { EditorView, placeholder } from "@codemirror/view"; import { Renderable, renderWithOptions } from "../util/Rendering"; import { darkTheme } from "./DarkTheme"; import { CODE_MIRROR_TRANSLATIONS } from "../Translations"; import { cursorDocEnd } from "@codemirror/commands"; import { i18n } from "../util/Util"; /** * Base class for Editors implemented using CodeMirror 6 * https://codemirror.net/6/ */ export class CodeMirrorEditor extends Renderable { /** * @param {Set<string>} compartments Identifiers for configurable extensions * @param {EditorStyling} styling Data to style this editor */ constructor(compartments, styling) { super(); this.styling = styling; this.listenerTimeouts = new Map(); // Ensure default compartments are present compartments.add(CodeMirrorEditor.STYLE); compartments.add(CodeMirrorEditor.PLACEHOLDER); compartments.add(CodeMirrorEditor.THEME); compartments.add(CodeMirrorEditor.LANGUAGE); this.compartments = new Map(); const configurableExtensions = []; compartments.forEach(opt => { const compartment = new Compartment(); this.compartments.set(opt, compartment); configurableExtensions.push(compartment.of([])); }); this.editorView = new EditorView({ state: EditorState.create({ extensions: [ configurableExtensions, EditorView.updateListener.of(this.onViewUpdate.bind(this)) ] }) }); } onViewUpdate(v) { if (v.docChanged) { this.handleChange(); } } /** * @param {Extension} extension The extension to add to the Editor */ addExtension(extension) { this.editorView.dispatch({ effects: StateEffect.appendConfig.of(extension) }); } /** * @return {string} The text within the editor */ getText() { return this.editorView.state.doc.toString(); } /** * @param {string} text The new value to be shown in the editor */ setText(text) { this.editorView.dispatch({ changes: { from: 0, to: this.getText().length, insert: text } }); } /** * Helper method to dispatch configuration changes at runtime * @param {Array<[Option, Extension]>} items Array of items to reconfigure * The option indicates the relevant compartment * The extension indicates the new configuration */ reconfigure(...items) { this.editorView.dispatch({ effects: items.map(([opt, ext]) => this.compartments.get(opt).reconfigure(ext)) }); } /** * Apply focus to the Editor */ focus() { this.editorView.focus(); cursorDocEnd(this.editorView); } /** * @param {string} placeholderValue The contents of the placeholder */ setPlaceholder(placeholderValue) { this.reconfigure([ CodeMirrorEditor.PLACEHOLDER, placeholder(placeholderValue) ]); } /** * @param {boolean} darkMode Whether to use dark mode */ setDarkMode(darkMode) { let styleExtensions = []; if (darkMode) { styleExtensions = [darkTheme]; } this.reconfigure([CodeMirrorEditor.STYLE, styleExtensions]); } /** * Override the style used by this Editor * @param {Partial<EditorStyling>} styling Object with keys of EditorStyling to override styles */ setStyling(styling) { Object.assign(this.styling, styling); this.reconfigure([ CodeMirrorEditor.THEME, EditorView.theme(Object.assign({ ".cm-scroller": { overflow: "auto" }, "&": { "maxHeight": this.styling.maxHeight, "height": "100%", "font-size": "14px" // use proper size to align gutters with editor }, ".cm-gutter,.cm-content": { minHeight: this.styling.minHeight }, ".cm-button": { "background-color": "#455A64", "color": "white", "background-image": "none" } }, (this.styling.theme || {}))) ]); } _render(options) { this.setStyling(this.styling); this.setDarkMode(options.darkMode || false); this.reconfigure([ CodeMirrorEditor.LANGUAGE, EditorState.phrases.of(CODE_MIRROR_TRANSLATIONS[i18n.locale]) ]); const wrappingDiv = document.createElement("div"); wrappingDiv.classList.add(...this.styling.classes); wrappingDiv.replaceChildren(this.editorView.dom); renderWithOptions(options, wrappingDiv); } /** * Process the changes by informing the listeners of the new contents */ handleChange() { const currentDoc = this.getText(); const now = Date.now(); this.listenerTimeouts.forEach((timeoutData, listener) => { // Clear existing scheduled calls if (timeoutData.timeout !== null) { clearTimeout(timeoutData.timeout); } timeoutData.lastCalled = now; if (listener.delay && listener.delay > 0) { timeoutData.timeout = setTimeout(() => { timeoutData.timeout = null; listener.onChange(currentDoc); }, listener.delay); } else { listener.onChange(currentDoc); } timeoutData.lastCalled = now; }); } /** * @param {DocChangeListener} changeListener Listener that performs actions on the new contents */ onChange(changeListener) { this.listenerTimeouts.set(changeListener, { timeout: null, lastCalled: 0 }); } } CodeMirrorEditor.STYLE = "style"; CodeMirrorEditor.PLACEHOLDER = "placeholder"; CodeMirrorEditor.THEME = "theme"; CodeMirrorEditor.LANGUAGE = "language"; //# sourceMappingURL=CodeMirrorEditor.js.map