diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts new file mode 100644 index 0000000..e6208f0 --- /dev/null +++ b/pyscriptjs/src/components/pyrepl.ts @@ -0,0 +1,253 @@ +import {EditorState, EditorView, basicSetup} from "@codemirror/basic-setup" +import { python } from "@codemirror/lang-python" +// @ts-ignore +import { StateCommand, Compartment } 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 } from '../stores'; +import { addClasses } from '../utils'; + +// 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 HTMLElement { + shadow: ShadowRoot; + wrapper: HTMLElement; + editor: EditorView; + editorNode: HTMLElement; + code: string; + cm: any; + btnConfig: HTMLElement; + btnRun: HTMLElement; + editorOut: HTMLElement; //HTMLTextAreaElement; + theme: string; + // editorState: EditorState; + + constructor() { + super(); + + // attach shadow so we can preserve the element original innerHtml content + this.shadow = this.attachShadow({ mode: 'open'}); + + this.wrapper = document.createElement('slot'); + + // add an extra div where we can attach the codemirror editor + this.editorNode = document.createElement('div'); + addClasses(this.editorNode, ["editor-box"]) + this.shadow.appendChild(this.wrapper); + } + + + connectedCallback() { + this.code = this.innerHTML; + this.innerHTML = ''; + + let 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); + + // } + // }) + ]; + + if (!this.hasAttribute('theme')) { + this.theme = this.getAttribute('theme'); + if (this.theme == 'dark'){ + extensions.push(oneDarkTheme); + } + } + + 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", "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:"target", 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('target')) { + this.editorOut = document.getElementById(this.getAttribute('target')); + + // 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{ + // Editor Output Div + this.editorOut = document.createElement('div'); + this.editorOut.classList.add("output"); + this.editorOut.hidden = true; + this.editorOut.id = this.id + "-" + this.getAttribute("exec-id"); + + // add the output div id there's not target + mainDiv.appendChild(this.editorOut); + } + + this.appendChild(mainDiv); + + console.log('connected'); + } + + addToOutput(s: string) { + this.editorOut.innerHTML += "
"+s+"
"; + this.editorOut.hidden = false; + } + + async evaluate() { + console.log('evaluate'); + let pyodide = await pyodideReadyPromise; + // debugger + try { + // @ts-ignore + let source = this.editor.state.doc.toString(); + let output; + if (source.includes("asyncio")){ + output = pyodide.runPythonAsync(source); + }else{ + output = pyodide.runPython(source); + } + + if (output !== undefined){ + let Element = pyodide.globals.get('Element'); + let out = Element(this.editorOut.id); + // @ts-ignore + out.write(output); + out.write.callKwargs(output, { append : false}); + + if (!this.hasAttribute('target')) { + this.editorOut.hidden = false; + } + // this.addToOutput(output); + } + + if (this.hasAttribute('auto-generate')) { + let 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('target')){ + newPyRepl.setAttribute('target', this.getAttribute('target')); + } + + newPyRepl.setAttribute('exec-id', nextExecId.toString()); + this.parentElement.appendChild(newPyRepl); + } + } catch (err) { + this.addToOutput(err); + } + } + + render(){ + console.log('rendered'); + + } + } + + \ No newline at end of file diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 0bf2e9c..86aca79 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -6,11 +6,11 @@ import { keymap } from "@codemirror/view"; import { defaultKeymap } from "@codemirror/commands"; import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { PyScript } from "./components/pyscript"; -import { pyodideLoaded } from './stores'; - +import { PyRepl } from "./components/pyrepl"; let xPyScript = customElements.define('py-script', PyScript); +let xPyRepl = customElements.define('py-repl', PyRepl); const app = new App({ diff --git a/pyscriptjs/src/stores.ts b/pyscriptjs/src/stores.ts index 43e14ed..36f5587 100644 --- a/pyscriptjs/src/stores.ts +++ b/pyscriptjs/src/stores.ts @@ -9,7 +9,7 @@ export const pyodideLoaded = writable({ }); export const loadedEnvironments = writable([{}]) - +export const DEFAULT_MODE = 'play'; export const pyodideReadyPromise = promisable( loadInterpreter, @@ -20,4 +20,16 @@ export const navBarOpen = writable(false); export const componentsNavOpen = writable(false); export const componentDetailsNavOpen = writable(false); export const mainDiv = writable(null); -export const currentComponentDetails = writable([]); \ No newline at end of file +export const currentComponentDetails = writable([]); +export const mode = writable(DEFAULT_MODE) +export const scriptsQueue = writable([]) + +let scriptsQueue_ = [] + +scriptsQueue.subscribe(value => { + scriptsQueue_ = value; +}); + +export const addToScriptsQueue = (script) => { + scriptsQueue.set([...scriptsQueue_, script]); +};