diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py
new file mode 100644
index 00000000..0dfa781d
--- /dev/null
+++ b/pyscriptjs/examples/pylist.py
@@ -0,0 +1,22 @@
+from datetime import datetime as dt
+
+class PyItem(PyItemTemplate):
+ def on_click(self, evt=None):
+ self.data['done'] = not self.data['done']
+ self.strike(self.data['done'])
+
+ self.select('input').element.checked = self.data['done']
+
+class PyList(PyListTemplate):
+ item_class = PyItem
+
+def add_task(*ags, **kws):
+ # create a new dictionary representing the new task
+ task = { "content": new_task_content.value, "done": False, "created_at": dt.now() }
+
+ # add a new task to the list and tell it to use the `content` key to show in the UI
+ # and to use the key `done` to sync the task status with a checkbox element in the UI
+ myList.add(task, labels=['content'], state_key="done")
+
+ # clear the inputbox element used to create the new task
+ new_task_content.clear()
diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html
new file mode 100644
index 00000000..49c02b15
--- /dev/null
+++ b/pyscriptjs/examples/todo-pylist.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Todo App
+
+
+
+
+
+
+ - paths:
+ - /utils.py
+
+
+
+
+
+ To Do List
+
+
+ def on_keypress(e):
+ if (e.code == "Enter"):
+ add_task()
+
+
+ def on_click(evt):
+ add_task()
+
+
+
+
+
+
+
diff --git a/pyscriptjs/src/App.svelte b/pyscriptjs/src/App.svelte
index 05a5a65f..7122ca7b 100644
--- a/pyscriptjs/src/App.svelte
+++ b/pyscriptjs/src/App.svelte
@@ -53,7 +53,7 @@
for (let initializer of $postInitializers){
initializer();
}
- }, 5000);
+ }, 3000);
}
diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts
index 0ec2a411..8c2861db 100644
--- a/pyscriptjs/src/components/base.ts
+++ b/pyscriptjs/src/components/base.ts
@@ -149,5 +149,174 @@ export class BaseEvalElement extends HTMLElement {
this.errorElement.hidden = false;
this.errorElement.style.display = 'block';
}
- }
+ } // end evaluate
+
+ async eval(source: string): Promise {
+ let output;
+ let pyodide = await pyodideReadyPromise;
+
+ try{
+ output = await pyodide.runPythonAsync(source);
+ if (output !== undefined){ console.log(output); }
+ } catch (err) {
+ console.log(err);
+ }
+ } // end eval
+ }
+
+ function createWidget(name: string, code: string, klass: string){
+
+ class CustomWidget extends HTMLElement{
+ shadow: ShadowRoot;
+ wrapper: HTMLElement;
+
+ name: string = name;
+ klass: string = klass;
+ code: string = code;
+ proxy: any;
+ proxyClass: any;
+
+ constructor() {
+ super();
+
+ // attach shadow so we can preserve the element original innerHtml content
+ this.shadow = this.attachShadow({ mode: 'open'});
+
+ this.wrapper = document.createElement('slot');
+ this.shadow.appendChild(this.wrapper);
+ }
+
+ connectedCallback() {
+ // TODO: we are calling with a 2secs delay to allow pyodide to load
+ // ideally we can just wait for it to load and then run. To do
+ // so we need to replace using the promise and actually using
+ // the interpreter after it loads completely
+ setTimeout(() => {
+ this.eval(this.code).then(() => {
+ this.proxy = this.proxyClass(this);
+ console.log('proxy', this.proxy);
+ this.proxy.connect();
+ this.registerWidget();
+ });
+ }, 2000);
+ }
+
+ async registerWidget(){
+ let pyodide = await pyodideReadyPromise;
+ console.log('new widget registered:', this.name);
+ pyodide.globals.set(this.id, this.proxy);
+ }
+
+ async eval(source: string): Promise {
+ let output;
+ let pyodide = await pyodideReadyPromise;
+ try{
+ output = await pyodide.runPythonAsync(source);
+ this.proxyClass = pyodide.globals.get(this.klass);
+ if (output !== undefined){
+ console.log(output);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+ }
+ let xPyWidget = customElements.define(name, CustomWidget);
+ }
+
+ export class PyWidget extends HTMLElement {
+ shadow: ShadowRoot;
+ name: string;
+ klass: string;
+ outputElement: HTMLElement;
+ errorElement: HTMLElement;
+ wrapper: HTMLElement;
+ theme: string;
+ source: string;
+ code: string;
+
+ constructor() {
+ super();
+
+ // attach shadow so we can preserve the element original innerHtml content
+ this.shadow = this.attachShadow({ mode: 'open'});
+
+ this.wrapper = document.createElement('slot');
+ this.shadow.appendChild(this.wrapper);
+
+ if (this.hasAttribute('src')) {
+ this.source = this.getAttribute('src');
+ }
+
+ if (this.hasAttribute('name')) {
+ this.name = this.getAttribute('name');
+ }
+
+ if (this.hasAttribute('klass')) {
+ this.klass = this.getAttribute('klass');
+ }
+ }
+
+ connectedCallback() {
+ if (this.id === undefined){
+ throw new ReferenceError(`No id specified for component. Components must have an explicit id. Please use id="" to specify your component id.`)
+ return;
+ }
+
+ let mainDiv = document.createElement('div');
+ mainDiv.id = this.id + '-main';
+ this.appendChild(mainDiv);
+ console.log('reading source')
+ this.getSourceFromFile(this.source).then((code:string) => {
+ this.code = code;
+ createWidget(this.name, code, this.klass);
+ });
+ }
+
+ initOutErr(): void {
+ 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");
+ }
+
+ if (this.hasAttribute('std-err')){
+ this.outputElement = document.getElementById(this.getAttribute('std-err'));
+ }else{
+ this.errorElement = this.outputElement;
+ }
+ }
+ }
+
+ async getSourceFromFile(s: string): Promise{
+ let pyodide = await pyodideReadyPromise;
+ let response = await fetch(s);
+ return await response.text();
+ }
+
+ async eval(source: string): Promise {
+ let output;
+ let pyodide = await pyodideReadyPromise;
+ try{
+ output = await pyodide.runPythonAsync(source);
+ if (output !== undefined){
+ console.log(output);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
}
diff --git a/pyscriptjs/src/components/pybox.ts b/pyscriptjs/src/components/pybox.ts
index c92845c1..acd27e57 100644
--- a/pyscriptjs/src/components/pybox.ts
+++ b/pyscriptjs/src/components/pybox.ts
@@ -19,7 +19,7 @@ export class PyBox extends HTMLElement {
connectedCallback() {
let mainDiv = document.createElement('div');
- addClasses(mainDiv, ["flex"])
+ addClasses(mainDiv, ["flex", "mx-8"])
// Hack: for some reason when moving children, the editor box duplicates children
// meaning that we end up with 2 editors, if there's a inside the
diff --git a/pyscriptjs/src/components/pybutton.ts b/pyscriptjs/src/components/pybutton.ts
new file mode 100644
index 00000000..426b537b
--- /dev/null
+++ b/pyscriptjs/src/components/pybutton.ts
@@ -0,0 +1,56 @@
+import { BaseEvalElement } from './base';
+import { addClasses, ltrim, htmlDecode } from '../utils';
+
+export class PyButton extends BaseEvalElement {
+ shadow: ShadowRoot;
+ wrapper: HTMLElement;
+ theme: string;
+ widths: Array;
+ label: string;
+ mount_name: string;
+ constructor() {
+ super();
+
+ if (this.hasAttribute('label')) {
+ this.label = this.getAttribute('label');
+ }
+ }
+
+ connectedCallback() {
+ this.code = htmlDecode(this.innerHTML);
+ this.mount_name = this.id.split("-").join("_");
+ this.innerHTML = '';
+
+ let mainDiv = document.createElement('button');
+ mainDiv.innerHTML = this.label;
+ addClasses(mainDiv, ["p-2", "text-white", "bg-blue-600", "border", "border-blue-600", "rounded"]);
+
+ mainDiv.id = this.id;
+ this.id = `${this.id}-container`;
+
+ this.appendChild(mainDiv);
+ this.code = this.code.split("self").join(this.mount_name);
+ let registrationCode = `${this.mount_name} = Element("${ mainDiv.id }")`;
+ if (this.code.includes("def on_focus")){
+ this.code = this.code.replace("def on_focus", `def on_focus_${this.mount_name}`);
+ registrationCode += `\n${this.mount_name}.element.onfocus = on_focus_${this.mount_name}`
+ }
+
+ if (this.code.includes("def on_click")){
+ this.code = this.code.replace("def on_click", `def on_click_${this.mount_name}`);
+ registrationCode += `\n${this.mount_name}.element.onclick = on_click_${this.mount_name}`
+ }
+
+ // now that we appended and the element is attached, lets connect with the event handlers
+ // defined for this widget
+ setTimeout(() => {
+ this.eval(this.code).then(() => {
+ this.eval(registrationCode).then(() => {
+ console.log('registered handlers');
+ });
+ });
+ }, 4000);
+
+ console.log('py-button connected');
+ }
+ }
diff --git a/pyscriptjs/src/components/pyinputbox.ts b/pyscriptjs/src/components/pyinputbox.ts
new file mode 100644
index 00000000..58fec149
--- /dev/null
+++ b/pyscriptjs/src/components/pyinputbox.ts
@@ -0,0 +1,54 @@
+import { BaseEvalElement } from './base';
+import { addClasses, ltrim, htmlDecode } from '../utils';
+
+export class PyInputBox extends BaseEvalElement {
+ shadow: ShadowRoot;
+ wrapper: HTMLElement;
+ theme: string;
+ widths: Array;
+ label: string;
+ mount_name: string;
+ constructor() {
+ super();
+
+ if (this.hasAttribute('label')) {
+ this.label = this.getAttribute('label');
+ }
+ }
+
+ connectedCallback() {
+ this.code = htmlDecode(this.innerHTML);
+ this.mount_name = this.id.split("-").join("_");
+ this.innerHTML = '';
+
+ let mainDiv = document.createElement('input');
+ mainDiv.type = "text";
+ addClasses(mainDiv, ["border", "flex-1", "w-full", "mr-3", "border-gray-300", "p-2", "rounded"]);
+
+ mainDiv.id = this.id;
+ this.id = `${this.id}-container`;
+ this.appendChild(mainDiv);
+
+ // now that we appended and the element is attached, lets connect with the event handlers
+ // defined for this widget
+ this.appendChild(mainDiv);
+ this.code = this.code.split("self").join(this.mount_name);
+ let registrationCode = `${this.mount_name} = Element("${ mainDiv.id }")`;
+ if (this.code.includes("def on_keypress")){
+ this.code = this.code.replace("def on_keypress", `def on_keypress_${this.mount_name}`);
+ registrationCode += `\n${this.mount_name}.element.onkeypress = on_keypress_${this.mount_name}`
+ }
+
+ // TODO: For now we delay execution to allow pyodide to load but in the future this
+ // should really wait for it to load..
+ setTimeout(() => {
+ this.eval(this.code).then(() => {
+ this.eval(registrationCode).then(() => {
+ console.log('registered handlers');
+ });
+ });
+ }, 4000);
+ }
+ }
+
+
\ No newline at end of file
diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts
index 5a9f9c18..2f9cd61e 100644
--- a/pyscriptjs/src/components/pyrepl.ts
+++ b/pyscriptjs/src/components/pyrepl.ts
@@ -98,7 +98,7 @@ export class PyRepl extends BaseEvalElement {
})
let mainDiv = document.createElement('div');
- addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "border-2", "border-gray-200", "rounded-lg"])
+ addClasses(mainDiv, ["parentBox", "group", "flex", "flex-col", "mt-2", "border-2", "border-gray-200", "rounded-lg", "mx-8"])
// add Editor to main PyScript div
// Butons DIV
@@ -199,6 +199,10 @@ export class PyRepl extends BaseEvalElement {
}
postEvaluate(): void {
+
+ this.outputElement.hidden = false;
+ this.outputElement.style.display = 'block';
+
if (this.hasAttribute('auto-generate')) {
let nextExecId = parseInt(this.getAttribute('exec-id')) + 1;
const newPyRepl = document.createElement("py-repl");
diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts
index 4b0da39d..b1d1f6e6 100644
--- a/pyscriptjs/src/components/pyscript.ts
+++ b/pyscriptjs/src/components/pyscript.ts
@@ -7,7 +7,7 @@ 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 { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base';
// Premise used to connect to the first available pyodide interpreter
@@ -41,11 +41,6 @@ function createCmdHandler(el){
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
@@ -296,7 +291,7 @@ async function mountElements() {
for (var el of matches) {
let mountName = el.getAttribute('py-mount');
if (!mountName){
- mountName = el.id.replace("-", "_");
+ mountName = el.id.split("-").join("_");
}
source += `\n${ mountName } = Element("${ el.id }")`;
}
diff --git a/pyscriptjs/src/components/pytitle.ts b/pyscriptjs/src/components/pytitle.ts
new file mode 100644
index 00000000..de4890c4
--- /dev/null
+++ b/pyscriptjs/src/components/pytitle.ts
@@ -0,0 +1,34 @@
+import { BaseEvalElement } from './base';
+import { addClasses, ltrim, htmlDecode } from '../utils';
+
+export class PyTitle extends BaseEvalElement {
+ shadow: ShadowRoot;
+ wrapper: HTMLElement;
+ theme: string;
+ widths: Array;
+ label: string;
+ mount_name: string;
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.label = htmlDecode(this.innerHTML);
+ this.mount_name = this.id.split("-").join("_");
+ this.innerHTML = '';
+
+ let mainDiv = document.createElement('div');
+ let divContent = document.createElement('h1')
+
+ addClasses(mainDiv, ["text-center", "w-full", "mb-8"]);
+ addClasses(divContent, ["text-3xl", "font-bold", "text-gray-800", "uppercase", "tracking-tight"]);
+ divContent.innerHTML = this.label;
+
+ mainDiv.id = this.id;
+ this.id = `${this.id}-container`;
+ mainDiv.appendChild(divContent);
+ this.appendChild(mainDiv);
+ }
+}
+
+
\ No newline at end of file
diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts
index c2dbfdd7..48ddd620 100644
--- a/pyscriptjs/src/interpreter.ts
+++ b/pyscriptjs/src/interpreter.ts
@@ -6,8 +6,9 @@ let pyodideReadyPromise;
let pyodide;
let additional_definitions = `
-from js import document, setInterval, console
+from js import document, setInterval, console, setTimeout
import micropip
+import time
import asyncio
import io, base64, sys
@@ -51,6 +52,10 @@ class Element:
self._id = element_id
self._element = element
+ @property
+ def id(self):
+ return self._id
+
@property
def element(self):
"""Return the dom element"""
@@ -58,6 +63,14 @@ class Element:
self._element = document.querySelector(f'#{self._id}');
return self._element
+ @property
+ def value(self):
+ return self.element.value
+
+ @property
+ def innerHtml(self):
+ return self.element.innerHtml
+
def write(self, value, append=False):
console.log(f"Element.write: {value} --> {append}")
# TODO: it should be the opposite... pyscript.write should use the Element.write
@@ -96,6 +109,176 @@ class Element:
return Element(clone.id, clone)
+
+ def remove_class(self, classname):
+ if isinstance(classname, list):
+ for cl in classname:
+ self.remove_class(cl)
+ else:
+ self.element.classList.remove(classname)
+
+ def add_class(self, classname):
+ self.element.classList.add(classname)
+
+def add_classes(element, class_list):
+ for klass in class_list.split(' '):
+ element.classList.add(klass)
+
+def create(what, id_=None, classes=''):
+ element = document.createElement(what)
+ if id_:
+ element.id = id_
+ add_classes(element, classes)
+ return Element(id_, element)
+
+
+class PyWidgetTheme:
+ def __init__(self, main_style_classes):
+ self.main_style_classes = main_style_classes
+
+ def theme_it(self, widget):
+ for klass in self.main_style_classes.split(' '):
+ widget.classList.add(klass)
+
+
+class PyItemTemplate(Element):
+ label_fields = None
+
+ def __init__(self, data, labels=None, state_key=None, parent=None):
+ self.data = data
+
+ self.register_parent(parent)
+
+ if not labels:
+ labels = list(self.data.keys())
+ self.labels = labels
+
+ self.state_key = state_key
+
+ super().__init__(self._id)
+
+ def register_parent(self, parent):
+ self._parent = parent
+ if parent:
+ self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
+ self.data['id'] = self._id
+ else:
+ self._id = None
+
+ def create(self):
+ console.log('creating section')
+ new_child = create('section', self._id, "task bg-white my-1")
+ console.log('creating values')
+
+ console.log('creating innerHtml')
+ new_child._element.innerHTML = f"""
+
+ """
+
+ console.log('returning')
+ return new_child
+
+ def on_click(self, evt):
+ pass
+
+ def pre_append(self):
+ pass
+
+ def post_append(self):
+ self.element.click = self.on_click
+ self.element.onclick = self.on_click
+
+ self._post_append()
+
+ def _post_append(self):
+ pass
+
+ def strike(self, value, extra=None):
+ if value:
+ self.add_class("line-through")
+ else:
+ self.remove_class("line-through")
+
+ def render_content(self):
+ return ' - '.join([self.data[f] for f in self.labels])
+
+class PyListTemplate:
+ theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-8")
+ item_class = PyItemTemplate
+
+ def __init__(self, parent):
+ self.parent = parent
+ self._children = []
+ self._id = self.parent.id
+
+ @property
+ def children(self):
+ return self._children
+
+ @property
+ def data(self):
+ return [c.data for c in self._children]
+
+ def render_children(self):
+ out = []
+ binds = {}
+ for i, c in enumerate(self._children):
+ txt = c.element.innerHTML
+ rnd = str(time.time()).replace(".", "")[-5:]
+ new_id = f"{c.element.id}-{i}-{rnd}"
+ binds[new_id] = c.element.id
+ txt = txt.replace(">", f" id='{new_id}'>")
+ print(txt)
+
+ def foo(evt):
+ console.log(evt)
+ evtEl = evt.srcElement
+ srcEl = Element(binds[evtEl.id])
+ srcEl.element.onclick()
+ evtEl.classList = srcEl.element.classList
+
+ for new_id, old_id in binds.items():
+ Element(new_id).element.onclick = foo
+
+ def connect(self):
+ self.md = main_div = document.createElement('div')
+ main_div.id = self._id + "-list-tasks-container"
+
+ if self.theme:
+ self.theme.theme_it(main_div)
+
+ self.parent.appendChild(main_div)
+
+ def add(self, *args, **kws):
+ if not isinstance(args[0], self.item_class):
+ child = self.item_class(*args, **kws)
+ else:
+ child = args[0]
+ child.register_parent(self)
+ return self._add(child)
+
+ def _add(self, child_elem):
+ console.log("appending child", child_elem.element)
+ self.pre_child_append(child_elem)
+ child_elem.pre_append()
+ self._children.append(child_elem)
+ self.md.appendChild(child_elem.create().element)
+ child_elem.post_append()
+ self.child_appended(child_elem)
+ return child_elem
+
+ def pre_child_append(self, child):
+ pass
+
+ def child_appended(self, child):
+ """Overwrite me to define logic"""
+ pass
+
+
+
class OutputCtxManager:
def __init__(self, out=None, output_to_console=True, append=True):
self._out = out
diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts
index bd4f69d6..321a79ee 100644
--- a/pyscriptjs/src/main.ts
+++ b/pyscriptjs/src/main.ts
@@ -4,12 +4,19 @@ import { PyScript } from "./components/pyscript";
import { PyRepl } from "./components/pyrepl";
import { PyEnv } from "./components/pyenv";
import { PyBox } from "./components/pybox";
-
+import { PyButton } from "./components/pybutton";
+import { PyTitle } from "./components/pytitle";
+import { PyInputBox } from "./components/pyinputbox";
+import { PyWidget } from "./components/base";
let xPyScript = customElements.define('py-script', PyScript);
let xPyRepl = customElements.define('py-repl', PyRepl);
let xPyEnv = customElements.define('py-env', PyEnv);
let xPyBox = customElements.define('py-box', PyBox);
+let xPyButton = customElements.define('py-button', PyButton);
+let xPyTitle = customElements.define('py-title', PyTitle);
+let xPyInputBox = customElements.define('py-inputbox', PyInputBox);
+let xPyWidget = customElements.define('py-register-widget', PyWidget);
const app = new App({
diff --git a/pyscriptjs/src/utils.ts b/pyscriptjs/src/utils.ts
index d9912bff..ac9f83d5 100644
--- a/pyscriptjs/src/utils.ts
+++ b/pyscriptjs/src/utils.ts
@@ -9,4 +9,29 @@ const getLastPath = function (str) {
return str.split('\\').pop().split('/').pop();
}
-export {addClasses, getLastPath}
+function htmlDecode(input) {
+ var doc = new DOMParser().parseFromString(input, "text/html");
+ return ltrim(doc.documentElement.textContent);
+}
+
+function ltrim(code: string): string {
+ const lines = code.split("\n")
+ if (lines.length == 0)
+ return code
+
+ const lengths = lines
+ .filter((line) => line.trim().length != 0)
+ .map((line) => {
+ const [prefix] = line.match(/^\s*/)
+ return prefix.length
+ })
+
+ const k = Math.min(...lengths)
+
+ if (k != 0)
+ return lines.map((line) => line.substring(k)).join("\n")
+ else
+ return code
+}
+
+export {addClasses, getLastPath, ltrim, htmlDecode}