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.outputElement = document.getElementById(this.getAttribute('output'));
}else{
// Editor Output Div
this.outputElement = document.createElement('div');
this.outputElement.classList.add("output");
this.outputElement.hidden = true;
// add the output div id there's no output element
mainDiv.appendChild(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);