import {EditorState, EditorView, basicSetup} from "@codemirror/basic-setup" import { python } from "@codemirror/lang-python" // @ts-ignore import { StateCommand } from '@codemirror/state'; import { keymap, ViewUpdate } from "@codemirror/view"; import { defaultKeymap } from "@codemirror/commands"; import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, currentComponentDetails, mode, addToScriptsQueue, addInitializer, addPostInitializer } 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; let handlersCollected = false; pyodideLoaded.subscribe(value => { pyodideReadyPromise = value; }); loadedEnvironments.subscribe(value => { environments = value; }); let propertiesNavOpen; componentDetailsNavOpen.subscribe(value => { propertiesNavOpen = value; }); mode.subscribe(value => { currentMode = value; }); 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 } function htmlDecode(input) { var doc = new DOMParser().parseFromString(input, "text/html"); return doc.documentElement.textContent; } // TODO: use type declaractions type PyodideInterface = { registerJsModule(name: string, module: object): void } // TODO: This should be used as base for generic scripts that need exectutoin // from PyScript to initializers, etc... class Script { source: string; state: string; output: string; constructor(source: string, output: string) { this.output = output; this.source = source; this.state = 'waiting'; } async evaluate() { console.log('evaluate'); let pyodide = await pyodideReadyPromise; // debugger try { // @ts-ignore // let source = this.editor.state.doc.toString(); let output; if (this.source.includes("asyncio")){ output = await pyodide.runPythonAsync(this.source); }else{ output = pyodide.runPython(this.source); } if (this.output){ // this.editorOut.innerHTML = s; } // if (output !== undefined){ // this.addToOutput(output); // } } catch (err) { console.log("OOOPS, this happened: " + err); // this.addToOutput(err); } } } export class PyScript extends BaseEvalElement { // editorState: EditorState; constructor() { super(); // add an extra div where we can attach the codemirror editor this.shadow.appendChild(this.wrapper); } connectedCallback() { this.code = this.innerHTML; this.innerHTML = ''; let startState = EditorState.create({ doc: this.code, extensions: [ keymap.of([ ...defaultKeymap, { key: "Ctrl-Enter", run: createCmdHandler(this) }, { key: "Shift-Enter", run: createCmdHandler(this) } ]), oneDarkTheme, python(), // 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); // } // }) ] }) let mainDiv = document.createElement('div'); addClasses(mainDiv, ["parentBox", "flex", "flex-col", "border-4", "border-dashed", "border-gray-200", "rounded-lg"]) // add Editor to main PyScript div // Butons DIV var eDiv = document.createElement('div'); addClasses(eDiv, "buttons-box 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".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() } return evaluatePython; } // 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"} ]) } addClasses(this.btnConfig, buttonClasses); addClasses(this.btnConfig, ["bg-blue-500"]) eDiv.appendChild(this.btnConfig); mainDiv.appendChild(eDiv); 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"); 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.outputElement = document.getElementById(this.getAttribute('std-err')); }else{ this.errorElement = this.outputElement; } } if (currentMode=="edit"){ this.appendChild(mainDiv); }else{ addToScriptsQueue(this); } console.log('connected'); if (this.hasAttribute('src')) { this.source = this.getAttribute('src'); } } protected async _register_esm(pyodide: PyodideInterface): Promise { const imports: {[key: string]: unknown} = {} for (const node of document.querySelectorAll("script[type='importmap']")) { const importmap = (() => { try { return JSON.parse(node.textContent) } catch { return null } })() if (importmap?.imports == null) continue for (const [name, url] of Object.entries(importmap.imports)) { if (typeof name != "string" || typeof url != "string") continue try { // XXX: pyodide doesn't like Module(), failing with // "can't read 'name' of undefined" at import time imports[name] = {...await import(url)} } catch { console.error(`failed to fetch '${url}' for '${name}'`) } } } pyodide.registerJsModule("esm", imports) } getSourceFromElement(): string { return this.code; } } /** Initialize all elements with py-onClick handlers attributes */ async function initHandlers() { console.log('Collecting nodes...'); let pyodide = await pyodideReadyPromise; let matches : NodeListOf = document.querySelectorAll('[pys-onClick]'); let output; let source; for (var el of matches) { let handlerCode = el.getAttribute('pys-onClick'); source = `Element("${ el.id }").element.onclick = ${ handlerCode }`; output = await pyodide.runPythonAsync(source); // TODO: Should we actually map handlers in JS instaed of Python? // el.onclick = (evt: any) => { // console.log("click"); // new Promise((resolve, reject) => { // setTimeout(() => { // console.log('Inside') // }, 300); // }).then(() => { // console.log("resolved") // }); // // let handlerCode = el.getAttribute('pys-onClick'); // // pyodide.runPython(handlerCode); // } } handlersCollected = true; matches = document.querySelectorAll('[pys-onKeyDown]'); for (var el of matches) { let handlerCode = el.getAttribute('pys-onKeyDown'); source = `Element("${ el.id }").element.addEventListener("keydown", ${ handlerCode })`; output = await pyodide.runPythonAsync(source); } } /** Mount all elements with attribute py-mount into the Python namespace */ async function mountElements() { console.log('Collecting nodes to be mounted into python namespace...'); let pyodide = await pyodideReadyPromise; let matches : NodeListOf = document.querySelectorAll('[py-mount]'); let output; let source = ""; for (var el of matches) { let mountName = el.getAttribute('py-mount'); if (!mountName){ mountName = el.id.replace("-", "_"); } source += `\n${ mountName } = Element("${ el.id }")`; } await pyodide.runPythonAsync(source); } addInitializer(mountElements); addPostInitializer(initHandlers);