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'); } }