@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
168 lines • 6.11 kB
JavaScript
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