import { basicSetup, EditorState, EditorView } from '@codemirror/basic-setup';
import { python } from '@codemirror/lang-python';
import { Compartment, StateCommand } from '@codemirror/state';
import { keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { oneDarkTheme } from '@codemirror/theme-one-dark';
import { componentDetailsNavOpen, currentComponentDetails, loadedEnvironments, mode, pyodideLoaded } from '../stores';
import { addClasses } from '../utils';
import { BaseEvalElement } from './base';
// Premise used to connect to the first available pyodide interpreter
let pyodideReadyPromise;
let environments;
let currentMode;
pyodideLoaded.subscribe(value => {
pyodideReadyPromise = value;
});
loadedEnvironments.subscribe(value => {
environments = value;
});
let propertiesNavOpen;
componentDetailsNavOpen.subscribe(value => {
propertiesNavOpen = value;
});
mode.subscribe(value => {
currentMode = value;
});
const languageConf = new Compartment();
function createCmdHandler(el) {
// Creates a codemirror cmd handler that calls the el.evaluate when an event
// triggers that specific cmd
const toggleCheckbox: StateCommand = ({ state, dispatch }) => {
return el.evaluate(state);
};
return toggleCheckbox;
}
export class PyRepl extends BaseEvalElement {
editor: EditorView;
editorNode: HTMLElement;
constructor() {
super();
// add an extra div where we can attach the codemirror editor
this.editorNode = document.createElement('div');
addClasses(this.editorNode, ["editor-box", "border", "border-gray-300"]);
this.shadow.appendChild(this.wrapper);
}
connectedCallback() {
this.checkId();
this.code = this.innerHTML;
this.innerHTML = '';
const extensions = [
basicSetup,
languageConf.of(python()),
keymap.of([
...defaultKeymap,
{ key: "Ctrl-Enter", run: createCmdHandler(this) },
{ key: "Shift-Enter", run: createCmdHandler(this) }
]),
// Event listener function that is called every time an user types something on this editor
// EditorView.updateListener.of((v:ViewUpdate) => {
// if (v.docChanged) {
// console.log(v.changes);
// }
// })
];
const customTheme = EditorView.theme({
'&.cm-focused .cm-editor': { outline: '0px' },
'.cm-scroller': { lineHeight: 2.5 },
'.cm-activeLine': { backgroundColor: '#fff' },
'.cm-content': { padding: 0, backgroundColor: '#f5f5f5' },
'&.cm-focused .cm-content': { border: '1px solid #1876d2' }
});
if (!this.hasAttribute('theme')) {
this.theme = this.getAttribute('theme');
if (this.theme == 'dark'){
extensions.push(oneDarkTheme);
}
extensions.push(customTheme);
}
let startState = EditorState.create({
doc: this.code.trim(),
extensions: extensions
})
this.editor = new EditorView({
state: startState,
parent: this.editorNode
})
let mainDiv = document.createElement('div');
addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "mx-8"])
// add Editor to main PyScript div
// Butons DIV
var eDiv = document.createElement('div');
addClasses(eDiv, "buttons-box opacity-0 group-hover:opacity-100 relative top-0 right-0 flex flex-row-reverse space-x-reverse space-x-4 font-mono text-white text-sm font-bold leading-6 dev-buttons-group -mt-2 first-of-type:m-0".split(" "))
eDiv.setAttribute("role", "group");
// Play Button
this.btnRun = document.createElement('button');
this.btnRun.innerHTML = '';
let buttonClasses = ["mr-2", "block", "py-2", "px-4", "rounded-full"];
addClasses(this.btnRun, buttonClasses);
addClasses(this.btnRun, ["bg-green-500"])
eDiv.appendChild(this.btnRun);
this.btnRun.onclick = wrap(this);
function wrap(el: any){
async function evaluatePython() {
el.evaluate()
}
// Settings button
this.btnConfig = document.createElement('button');
this.btnConfig.innerHTML =
'';
this.btnConfig.onclick = function toggleNavBar(evt) {
console.log('clicked');
componentDetailsNavOpen.set(!propertiesNavOpen);
currentComponentDetails.set([
{ key: 'auto-generate', value: true },
{ key: 'output', value: 'default' },
{ key: 'source', value: 'self' },
{ key: 'output-mode', value: 'clear' },
]);
};
addClasses(this.btnConfig, buttonClasses);
addClasses(this.btnConfig, ['bg-blue-500']);
eDiv.appendChild(this.btnConfig);
mainDiv.appendChild(eDiv);
mainDiv.appendChild(this.editorNode);
if (!this.id) {
console.log(
'WARNING: define with an id. should always have an id. More than one on a page won\'t work otherwise!',
);
}
if (!this.hasAttribute('exec-id')) {
this.setAttribute('exec-id', '1');
}
if (!this.hasAttribute('root')) {
this.setAttribute('root', this.id);
}
if (this.hasAttribute('output')) {
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
// in this case, the default output-mode is append, if hasn't been specified
if (!this.hasAttribute('output-mode')) {
this.setAttribute('output-mode', 'append');
}
} else {
if (this.hasAttribute('std-out')) {
this.outputElement = document.getElementById(this.getAttribute('std-out'));
} else {
// In this case neither output or std-out have been provided so we need
// to create a new output div to output to
this.outputElement = document.createElement('div');
this.outputElement.classList.add('output', 'font-mono', 'ml-8', 'mt-4','text-sm');
this.outputElement.hidden = true;
this.outputElement.id = this.id + '-' + this.getAttribute('exec-id');
// add the output div id if there's not output pre-defined
mainDiv.appendChild(this.outputElement);
}
if (this.hasAttribute('std-err')) {
this.errorElement = document.getElementById(this.getAttribute('std-err'));
} else {
this.errorElement = this.outputElement;
}
}
this.appendChild(mainDiv);
this.editor.focus();
console.log('connected');
}
addToOutput(s: string): void {
this.outputElement.innerHTML += '' + s + '
';
this.outputElement.hidden = false;
}
postEvaluate(): void {
this.outputElement.hidden = false;
this.outputElement.style.display = 'block';
if (this.hasAttribute('auto-generate')) {
const nextExecId = parseInt(this.getAttribute('exec-id')) + 1;
const newPyRepl = document.createElement('py-repl');
newPyRepl.setAttribute('root', this.getAttribute('root'));
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
newPyRepl.setAttribute('auto-generate', null);
if (this.hasAttribute('output')) {
newPyRepl.setAttribute('output', this.getAttribute('output'));
}
if (this.hasAttribute('std-out')) {
newPyRepl.setAttribute('std-out', this.getAttribute('std-out'));
}
if (this.hasAttribute('std-err')) {
newPyRepl.setAttribute('std-err', this.getAttribute('std-err'));
}
newPyRepl.setAttribute('exec-id', nextExecId.toString());
this.parentElement.appendChild(newPyRepl);
}
}
getSourceFromElement(): string {
const sourceStrings = [
`output_manager.change("` + this.outputElement.id + `")`,
...this.editor.state.doc.toString().split('\n'),
];
return sourceStrings.join('\n');
}
render() {
console.log('rendered');
}
}