From a702cbdf1090e2611f849078388fe8fe9f7cf00d Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Tue, 19 Apr 2022 19:24:22 -0500 Subject: [PATCH 01/15] add support for custom widgets registration in Python --- pyscriptjs/src/components/base.ts | 168 ++++++++++++++++++++++++++ pyscriptjs/src/components/pyscript.ts | 2 +- pyscriptjs/src/interpreter.ts | 19 +++ pyscriptjs/src/main.ts | 3 +- 4 files changed, 190 insertions(+), 2 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 0ec2a411..66037368 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -151,3 +151,171 @@ export class BaseEvalElement extends HTMLElement { } } } + + 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() { + console.log(this.name, 'connected!!!!') + this.eval(this.code).then(() => { + this.proxy = this.proxyClass(this); + console.log('proxy', this.proxy); + this.proxy.connect(); + this.registerWidget(); + }); + } + + 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); + + }); + + console.log('py-template connected'); + } + + 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"); + + // 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; + } + } + } + + 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/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index 4b0da39d..f302a596 100644 --- a/pyscriptjs/src/components/pyscript.ts +++ b/pyscriptjs/src/components/pyscript.ts @@ -296,7 +296,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/interpreter.ts b/pyscriptjs/src/interpreter.ts index c2dbfdd7..cc184771 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -58,6 +58,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 +104,17 @@ 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) + 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..1dfbf9ba 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -4,12 +4,13 @@ import { PyScript } from "./components/pyscript"; import { PyRepl } from "./components/pyrepl"; import { PyEnv } from "./components/pyenv"; import { PyBox } from "./components/pybox"; - +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 xPyWidget = customElements.define('py-register-widget', PyWidget); const app = new App({ From 280fd53aba78cdfe1178634d40a95649202efd9a Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Tue, 19 Apr 2022 19:24:47 -0500 Subject: [PATCH 02/15] add example --- pyscriptjs/examples/pylist.py | 88 ++++++++++++++++++++++++++++ pyscriptjs/examples/todo-pylist.html | 56 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 pyscriptjs/examples/pylist.py create mode 100644 pyscriptjs/examples/todo-pylist.html diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py new file mode 100644 index 00000000..32a0bcf0 --- /dev/null +++ b/pyscriptjs/examples/pylist.py @@ -0,0 +1,88 @@ +from datetime import datetime as dt +from xml.dom.pulldom import END_ELEMENT +from js import console, HTMLElement, document + + +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 PyList: + def __init__(self, parent): + self.parent = parent + self._children = [] + self.id = self.parent.id + + def connect(self): + self.md = main_div = document.createElement('div'); + main_div.id = self.id + "-list-tasks-container" + + for klass in "flex flex-col-reverse mt-4".split(' '): + main_div.classList.add(klass) + + self.parent.appendChild(main_div) + + def add(self, data, labels): + child = PyItem(self, data, labels) + return self._add(child) + + def _add(self, child_elem): + self._children.append(child_elem) + console.log("appending child", child_elem.element) + self.md.appendChild(child_elem.create().element) + return child_elem + +class PyItem(Element): + def __init__(self, parent, data, labels): + self._parent = parent + self.id = f"{self._parent.id}-c-{len(self._parent._children)}" + self.data = data + self.data['id'] = self.id + self.labels = labels + + super().__init__(self.id) + + def create(self): + console.log('creating section') + new_child = create('section', self.id, "task bg-white my-1") + console.log('creating values') + values = ' - '.join([self.data[f] for f in self.labels]) + console.log('creating innerHtml') + new_child._element.innerHTML = f""" + + """ + + check = new_child.select('input').element + check.onclick = self.check_task + console.log('returning') + return new_child + + def check_task(self, evt=None): + self.data['done'] = not self.data['done'] + if self.data['done']: + self.add_class("line-through") + else: + self.remove_class("line-through") + + +def add_task(*ags, **kws): + console.log(new_task_content.value) + console.log('adding', myList) + task = { + "content": new_task_content.value, + "done": False, + "created_at": dt.now() + } + myList.add(task, ['content']) + new_task_content.clear() diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html new file mode 100644 index 00000000..015b197e --- /dev/null +++ b/pyscriptjs/examples/todo-pylist.html @@ -0,0 +1,56 @@ + + + + + + + Todo App + + + + + + + - paths: + - /utils.py + + + + + + + + + +
+
+ + + +
+

To Do List

+
+
+ + +
+ + + + + + + +
+
+ + From a6d00318c39aff6a592e2f190e0d50a6db4ee187 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 12:48:25 -0500 Subject: [PATCH 03/15] add PyListTemplate and PyItemTemplate so that classes implementing list and list item widgets can subclass from it and hide complexity to users --- pyscriptjs/examples/pylist.py | 71 ++++++++++++++++++++++++++----- pyscriptjs/src/App.svelte | 2 +- pyscriptjs/src/components/base.ts | 3 +- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py index 32a0bcf0..53fa8455 100644 --- a/pyscriptjs/examples/pylist.py +++ b/pyscriptjs/examples/pylist.py @@ -14,8 +14,19 @@ def create(what, id_=None, classes=''): 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 PyListTemplate: + theme = PyWidgetTheme("flex flex-col-reverse mt-4") + -class PyList: def __init__(self, parent): self.parent = parent self._children = [] @@ -25,8 +36,8 @@ class PyList: self.md = main_div = document.createElement('div'); main_div.id = self.id + "-list-tasks-container" - for klass in "flex flex-col-reverse mt-4".split(' '): - main_div.classList.add(klass) + if self.theme: + self.theme.theme_it(main_div) self.parent.appendChild(main_div) @@ -35,12 +46,24 @@ class PyList: return self._add(child) def _add(self, child_elem): - self._children.append(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 -class PyItem(Element): + def pre_child_append(self, child): + pass + + def child_appended(self, child): + """Overwrite me to define logic""" + pass + + +class PyItemTemplate(Element): def __init__(self, parent, data, labels): self._parent = parent self.id = f"{self._parent.id}-c-{len(self._parent._children)}" @@ -63,22 +86,48 @@ class PyItem(Element): """ - check = new_child.select('input').element - check.onclick = self.check_task + # check = new_child.select('input').element + # check.onclick = self.check_task console.log('returning') return new_child - def check_task(self, evt=None): - self.data['done'] = not self.data['done'] - if self.data['done']: + 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): + if value: self.add_class("line-through") else: self.remove_class("line-through") + +class PyList(PyListTemplate): + pass + + +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'] + + def add_task(*ags, **kws): console.log(new_task_content.value) - console.log('adding', myList) + console.log('adding1', myList) task = { "content": new_task_content.value, "done": False, 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 66037368..dd8cdd44 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -176,13 +176,14 @@ export class BaseEvalElement extends HTMLElement { } connectedCallback() { - console.log(this.name, 'connected!!!!') + console.log(this.name, 'OOOOOOO connected!!!!') this.eval(this.code).then(() => { this.proxy = this.proxyClass(this); console.log('proxy', this.proxy); this.proxy.connect(); this.registerWidget(); }); + console.log(this.name, 'DOOOOONE connected!!!!') } async registerWidget(){ From ee1db16bcfdad55c928883984c718e4c7be3f99e Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 13:30:08 -0500 Subject: [PATCH 04/15] move templates to interpreter --- pyscriptjs/examples/pylist.py | 123 ++------------------------------ pyscriptjs/src/interpreter.ts | 129 ++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 118 deletions(-) diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py index 53fa8455..cfb6cdf4 100644 --- a/pyscriptjs/examples/pylist.py +++ b/pyscriptjs/examples/pylist.py @@ -3,116 +3,6 @@ from xml.dom.pulldom import END_ELEMENT from js import console, HTMLElement, document -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 PyListTemplate: - theme = PyWidgetTheme("flex flex-col-reverse mt-4") - - - def __init__(self, parent): - self.parent = parent - self._children = [] - self.id = self.parent.id - - 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, data, labels): - child = PyItem(self, data, labels) - 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 PyItemTemplate(Element): - def __init__(self, parent, data, labels): - self._parent = parent - self.id = f"{self._parent.id}-c-{len(self._parent._children)}" - self.data = data - self.data['id'] = self.id - self.labels = labels - - super().__init__(self.id) - - def create(self): - console.log('creating section') - new_child = create('section', self.id, "task bg-white my-1") - console.log('creating values') - values = ' - '.join([self.data[f] for f in self.labels]) - console.log('creating innerHtml') - new_child._element.innerHTML = f""" - - """ - - # check = new_child.select('input').element - # check.onclick = self.check_task - 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): - if value: - self.add_class("line-through") - else: - self.remove_class("line-through") - - class PyList(PyListTemplate): pass @@ -124,14 +14,11 @@ class PyItem(PyItemTemplate): self.select('input').element.checked = self.data['done'] + def display(self): + return self.data['content'] + def add_task(*ags, **kws): - console.log(new_task_content.value) - console.log('adding1', myList) - task = { - "content": new_task_content.value, - "done": False, - "created_at": dt.now() - } - myList.add(task, ['content']) + task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } + myList.add(PyItem(task, labels=['content'], state_key="done")) new_task_content.clear() diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index cc184771..f2d368c8 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -51,6 +51,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""" @@ -115,6 +119,131 @@ class Element: 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 PyListTemplate: + theme = PyWidgetTheme("flex flex-col-reverse mt-4") + + + def __init__(self, parent): + self.parent = parent + self._children = [] + self._id = self.parent.id + + 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, child): + 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 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): + 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 OutputCtxManager: def __init__(self, out=None, output_to_console=True, append=True): self._out = out From ea0cddee963d92a2ebbeb1e30927d25ef0477ab6 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 13:48:11 -0500 Subject: [PATCH 05/15] simplify the example --- pyscriptjs/examples/pylist.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py index cfb6cdf4..48c948cc 100644 --- a/pyscriptjs/examples/pylist.py +++ b/pyscriptjs/examples/pylist.py @@ -13,10 +13,7 @@ class PyItem(PyItemTemplate): self.strike(self.data['done']) self.select('input').element.checked = self.data['done'] - - def display(self): - return self.data['content'] - + def add_task(*ags, **kws): task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } From 28efe8a1c9a8054399236467740ce360531c460b Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 13:52:13 -0500 Subject: [PATCH 06/15] clean html --- pyscriptjs/examples/todo-pylist.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html index 015b197e..9a8e2cb9 100644 --- a/pyscriptjs/examples/todo-pylist.html +++ b/pyscriptjs/examples/todo-pylist.html @@ -40,16 +40,6 @@ - - - From f5b168a45e917385bcdb3c04c46a2170a84e5095 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 15:41:28 -0500 Subject: [PATCH 07/15] add better support for new widgets --- pyscriptjs/src/components/base.ts | 18 +++++++++++++++++- pyscriptjs/src/components/pyscript.ts | 7 +------ pyscriptjs/src/interpreter.ts | 12 ++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index dd8cdd44..8f5f5667 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -149,7 +149,23 @@ 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){ diff --git a/pyscriptjs/src/components/pyscript.ts b/pyscriptjs/src/components/pyscript.ts index f302a596..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 diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index f2d368c8..2dc38fac 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -148,6 +148,18 @@ class PyListTemplate: 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] + + @property + def render_children(self): + return [c.element.innerHTML.replace("\\n", "") for c in self._children] + def connect(self): self.md = main_div = document.createElement('div'); main_div.id = self._id + "-list-tasks-container" From 3aa3ba02be47dbfcfb2b5e6eee5f4ef1922a97d2 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 15:41:51 -0500 Subject: [PATCH 08/15] add pybutton and pytitle --- pyscriptjs/examples/todo-pylist.html | 21 +++++---- pyscriptjs/src/components/pybutton.ts | 64 +++++++++++++++++++++++++++ pyscriptjs/src/components/pytitle.ts | 37 ++++++++++++++++ pyscriptjs/src/main.ts | 4 ++ pyscriptjs/src/utils.ts | 27 ++++++++++- 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 pyscriptjs/src/components/pybutton.ts create mode 100644 pyscriptjs/src/components/pytitle.ts diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html index 9a8e2cb9..e38be332 100644 --- a/pyscriptjs/examples/todo-pylist.html +++ b/pyscriptjs/examples/todo-pylist.html @@ -18,28 +18,27 @@ - - -
- - -
-

To Do List

-
+ To Do List
-
+ + - +
diff --git a/pyscriptjs/src/components/pybutton.ts b/pyscriptjs/src/components/pybutton.ts new file mode 100644 index 00000000..452ee435 --- /dev/null +++ b/pyscriptjs/src/components/pybutton.ts @@ -0,0 +1,64 @@ +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(); + + // 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('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'); + } + } + + \ No newline at end of file diff --git a/pyscriptjs/src/components/pytitle.ts b/pyscriptjs/src/components/pytitle.ts new file mode 100644 index 00000000..633fff6f --- /dev/null +++ b/pyscriptjs/src/components/pytitle.ts @@ -0,0 +1,37 @@ +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); + + console.log('py-title connected'); + } + } + + \ No newline at end of file diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 1dfbf9ba..6eb56795 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -4,12 +4,16 @@ 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 { 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 xPyWidget = customElements.define('py-register-widget', PyWidget); 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} From 44afda7aa84b3f357cbac5f9e148467f3aa478bb Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 16:12:01 -0500 Subject: [PATCH 09/15] add inputbox and clearn todo html even more --- pyscriptjs/examples/todo-pylist.html | 39 ++++++++----------- pyscriptjs/src/components/pyinputbox.ts | 51 +++++++++++++++++++++++++ pyscriptjs/src/interpreter.ts | 1 - pyscriptjs/src/main.ts | 2 + 4 files changed, 68 insertions(+), 25 deletions(-) create mode 100644 pyscriptjs/src/components/pyinputbox.ts diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html index e38be332..11d54fa8 100644 --- a/pyscriptjs/examples/todo-pylist.html +++ b/pyscriptjs/examples/todo-pylist.html @@ -17,29 +17,20 @@ - - -
-
- - To Do List -
- - - - def on_click(evt): - task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } - myList.add(PyItem(task, labels=['content'], state_key="done")) - new_task_content.clear() - -
- - - + + To Do List + + + + def on_click(evt): + task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } + myList.add(PyItem(task, labels=['content'], state_key="done")) + new_task_content.clear() + + - - -
-
- + + + + diff --git a/pyscriptjs/src/components/pyinputbox.ts b/pyscriptjs/src/components/pyinputbox.ts new file mode 100644 index 00000000..0bdbd51d --- /dev/null +++ b/pyscriptjs/src/components/pyinputbox.ts @@ -0,0 +1,51 @@ +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(); + + // 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('label')) { + this.label = this.getAttribute('label'); + } + } + + + connectedCallback() { + this.label = 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.code = `${this.mount_name} = Element("${ mainDiv.id }")`; + setTimeout(() => { + this.eval(this.code).then(() => { + console.log('registered handlers'); + }); + }, 4000); + + console.log('py-title connected'); + } + } + + \ No newline at end of file diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 2dc38fac..3aedd3fb 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -156,7 +156,6 @@ class PyListTemplate: def data(self): return [c.data for c in self._children] - @property def render_children(self): return [c.element.innerHTML.replace("\\n", "") for c in self._children] diff --git a/pyscriptjs/src/main.ts b/pyscriptjs/src/main.ts index 6eb56795..321a79ee 100644 --- a/pyscriptjs/src/main.ts +++ b/pyscriptjs/src/main.ts @@ -6,6 +6,7 @@ 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); @@ -14,6 +15,7 @@ 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); From ac64b2aaa373e095553e4a1932e257a4182ebb44 Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 16:32:11 -0500 Subject: [PATCH 10/15] add handlers to input box as well --- pyscriptjs/examples/todo-pylist.html | 7 +++++-- pyscriptjs/src/components/pyinputbox.ts | 19 ++++++++++++++----- pyscriptjs/src/interpreter.ts | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html index 11d54fa8..a43b1040 100644 --- a/pyscriptjs/examples/todo-pylist.html +++ b/pyscriptjs/examples/todo-pylist.html @@ -20,7 +20,11 @@ To Do List - + + def on_keypress(e): + if (e.code == "Enter"): + add_task() + def on_click(evt): task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } @@ -30,7 +34,6 @@ - diff --git a/pyscriptjs/src/components/pyinputbox.ts b/pyscriptjs/src/components/pyinputbox.ts index 0bdbd51d..492bdc62 100644 --- a/pyscriptjs/src/components/pyinputbox.ts +++ b/pyscriptjs/src/components/pyinputbox.ts @@ -23,7 +23,7 @@ export class PyInputBox extends BaseEvalElement { connectedCallback() { - this.label = htmlDecode(this.innerHTML); + this.code = htmlDecode(this.innerHTML); this.mount_name = this.id.split("-").join("_"); this.innerHTML = ''; @@ -37,14 +37,23 @@ export class PyInputBox extends BaseEvalElement { // now that we appended and the element is attached, lets connect with the event handlers // defined for this widget - this.code = `${this.mount_name} = Element("${ mainDiv.id }")`; + 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}` + } + setTimeout(() => { this.eval(this.code).then(() => { - console.log('registered handlers'); + this.eval(registrationCode).then(() => { + console.log('registered handlers'); + }); }); }, 4000); - - console.log('py-title connected'); + + console.log('py-inputbox connected'); } } diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 3aedd3fb..99886de6 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -140,7 +140,7 @@ class PyWidgetTheme: class PyListTemplate: - theme = PyWidgetTheme("flex flex-col-reverse mt-4") + theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-4") def __init__(self, parent): From 019b7d145a02747dec75af9b4b98b3661a810b6d Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 18:16:26 -0500 Subject: [PATCH 11/15] add timer to register generic widgets --- pyscriptjs/src/components/base.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyscriptjs/src/components/base.ts b/pyscriptjs/src/components/base.ts index 8f5f5667..454e81fb 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -192,14 +192,18 @@ export class BaseEvalElement extends HTMLElement { } connectedCallback() { - console.log(this.name, 'OOOOOOO connected!!!!') - this.eval(this.code).then(() => { - this.proxy = this.proxyClass(this); - console.log('proxy', this.proxy); - this.proxy.connect(); - this.registerWidget(); - }); - console.log(this.name, 'DOOOOONE connected!!!!') + // 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(){ From 40c58c3cf1d3c170c461e34037f84a9720ddcdde Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 18:16:43 -0500 Subject: [PATCH 12/15] simplify example --- pyscriptjs/examples/todo-pylist.html | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyscriptjs/examples/todo-pylist.html b/pyscriptjs/examples/todo-pylist.html index a43b1040..49c02b15 100644 --- a/pyscriptjs/examples/todo-pylist.html +++ b/pyscriptjs/examples/todo-pylist.html @@ -2,7 +2,6 @@ - Todo App @@ -14,12 +13,12 @@ - paths: - /utils.py - + To Do List - + def on_keypress(e): if (e.code == "Enter"): @@ -27,12 +26,10 @@ def on_click(evt): - task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } - myList.add(PyItem(task, labels=['content'], state_key="done")) - new_task_content.clear() + add_task() - + From 7c71ba6fded72a81af5e3720920efe2db106105a Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 18:17:58 -0500 Subject: [PATCH 13/15] add margin to widgets and add item template to PyList --- pyscriptjs/src/components/pybox.ts | 2 +- pyscriptjs/src/components/pyrepl.ts | 2 +- pyscriptjs/src/interpreter.ts | 107 +++++++++++++++------------- 3 files changed, 58 insertions(+), 53 deletions(-) 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/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 5a9f9c18..9042d060 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 diff --git a/pyscriptjs/src/interpreter.ts b/pyscriptjs/src/interpreter.ts index 99886de6..26ace672 100644 --- a/pyscriptjs/src/interpreter.ts +++ b/pyscriptjs/src/interpreter.ts @@ -130,6 +130,7 @@ def create(what, id_=None, classes=''): add_classes(element, classes) return Element(id_, element) + class PyWidgetTheme: def __init__(self, main_style_classes): self.main_style_classes = main_style_classes @@ -139,57 +140,6 @@ class PyWidgetTheme: widget.classList.add(klass) -class PyListTemplate: - theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-4") - - - 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): - return [c.element.innerHTML.replace("\\n", "") for c in self._children] - - 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, child): - 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 PyItemTemplate(Element): label_fields = None @@ -253,6 +203,61 @@ class PyItemTemplate(Element): 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): + return [c.element.innerHTML.replace("\\n", "") for c in self._children] + + 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: From 71873b6b504a16a3816a698f0e8099dc3110298f Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 20:03:23 -0500 Subject: [PATCH 14/15] clean pylist.py and add a very hacky sync between list items and it's rendered versions --- pyscriptjs/examples/pylist.py | 11 +++-------- pyscriptjs/src/components/pyrepl.ts | 4 ++++ pyscriptjs/src/interpreter.ts | 29 ++++++++++++++++++++++++----- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py index 48c948cc..82a20ecc 100644 --- a/pyscriptjs/examples/pylist.py +++ b/pyscriptjs/examples/pylist.py @@ -1,11 +1,4 @@ from datetime import datetime as dt -from xml.dom.pulldom import END_ELEMENT -from js import console, HTMLElement, document - - -class PyList(PyListTemplate): - pass - class PyItem(PyItemTemplate): def on_click(self, evt=None): @@ -14,8 +7,10 @@ class PyItem(PyItemTemplate): self.select('input').element.checked = self.data['done'] +class PyList(PyListTemplate): + item_class = PyItem def add_task(*ags, **kws): task = { "content": new_task_content.value, "done": False, "created_at": dt.now() } - myList.add(PyItem(task, labels=['content'], state_key="done")) + myList.add(task, labels=['content'], state_key="done") new_task_content.clear() diff --git a/pyscriptjs/src/components/pyrepl.ts b/pyscriptjs/src/components/pyrepl.ts index 9042d060..2f9cd61e 100644 --- a/pyscriptjs/src/components/pyrepl.ts +++ b/pyscriptjs/src/components/pyrepl.ts @@ -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/interpreter.ts b/pyscriptjs/src/interpreter.ts index 26ace672..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 @@ -195,7 +196,7 @@ class PyItemTemplate(Element): def _post_append(self): pass - def strike(self, value): + def strike(self, value, extra=None): if value: self.add_class("line-through") else: @@ -222,14 +223,32 @@ class PyListTemplate: return [c.data for c in self._children] def render_children(self): - return [c.element.innerHTML.replace("\\n", "") for c in self._children] + 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'); + 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.theme.theme_it(main_div) self.parent.appendChild(main_div) From 4065c13d32d9e607c909c31f946ac77152f839dd Mon Sep 17 00:00:00 2001 From: Fabio Pliger Date: Wed, 20 Apr 2022 20:27:13 -0500 Subject: [PATCH 15/15] general cleanup before merging --- pyscriptjs/examples/pylist.py | 6 +++ pyscriptjs/src/components/base.ts | 24 +---------- pyscriptjs/src/components/pybutton.ts | 8 ---- pyscriptjs/src/components/pyinputbox.ts | 48 ++++++++++----------- pyscriptjs/src/components/pytitle.ts | 55 ++++++++++++------------- 5 files changed, 55 insertions(+), 86 deletions(-) diff --git a/pyscriptjs/examples/pylist.py b/pyscriptjs/examples/pylist.py index 82a20ecc..0dfa781d 100644 --- a/pyscriptjs/examples/pylist.py +++ b/pyscriptjs/examples/pylist.py @@ -11,6 +11,12 @@ 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/src/components/base.ts b/pyscriptjs/src/components/base.ts index 454e81fb..8c2861db 100644 --- a/pyscriptjs/src/components/base.ts +++ b/pyscriptjs/src/components/base.ts @@ -157,11 +157,7 @@ export class BaseEvalElement extends HTMLElement { try{ output = await pyodide.runPythonAsync(source); - - if (output !== undefined){ - console.log(output); - } - + if (output !== undefined){ console.log(output); } } catch (err) { console.log(err); } @@ -170,7 +166,6 @@ export class BaseEvalElement extends HTMLElement { function createWidget(name: string, code: string, klass: string){ - class CustomWidget extends HTMLElement{ shadow: ShadowRoot; wrapper: HTMLElement; @@ -208,10 +203,7 @@ export class BaseEvalElement extends HTMLElement { async registerWidget(){ let pyodide = await pyodideReadyPromise; - console.log('new widget registered:', this.name); - - pyodide.globals.set(this.id, this.proxy); } @@ -224,7 +216,6 @@ export class BaseEvalElement extends HTMLElement { if (output !== undefined){ console.log(output); } - } catch (err) { console.log(err); } @@ -266,7 +257,6 @@ export class BaseEvalElement extends HTMLElement { } } - 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.`) @@ -280,10 +270,7 @@ export class BaseEvalElement extends HTMLElement { this.getSourceFromFile(this.source).then((code:string) => { this.code = code; createWidget(this.name, code, this.klass); - }); - - console.log('py-template connected'); } initOutErr(): void { @@ -304,9 +291,6 @@ export class BaseEvalElement extends HTMLElement { 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')){ @@ -321,22 +305,18 @@ export class BaseEvalElement extends HTMLElement { 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/pybutton.ts b/pyscriptjs/src/components/pybutton.ts index 452ee435..426b537b 100644 --- a/pyscriptjs/src/components/pybutton.ts +++ b/pyscriptjs/src/components/pybutton.ts @@ -11,17 +11,11 @@ export class PyButton extends BaseEvalElement { 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('label')) { this.label = this.getAttribute('label'); } } - connectedCallback() { this.code = htmlDecode(this.innerHTML); this.mount_name = this.id.split("-").join("_"); @@ -60,5 +54,3 @@ export class PyButton extends BaseEvalElement { console.log('py-button connected'); } } - - \ No newline at end of file diff --git a/pyscriptjs/src/components/pyinputbox.ts b/pyscriptjs/src/components/pyinputbox.ts index 492bdc62..58fec149 100644 --- a/pyscriptjs/src/components/pyinputbox.ts +++ b/pyscriptjs/src/components/pyinputbox.ts @@ -9,32 +9,26 @@ export class PyInputBox extends BaseEvalElement { label: string; mount_name: 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('label')) { - this.label = this.getAttribute('label'); - } + 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); + 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); @@ -45,16 +39,16 @@ export class PyInputBox extends BaseEvalElement { 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); - - console.log('py-inputbox connected'); - } + }, 4000); + } } \ No newline at end of file diff --git a/pyscriptjs/src/components/pytitle.ts b/pyscriptjs/src/components/pytitle.ts index 633fff6f..de4890c4 100644 --- a/pyscriptjs/src/components/pytitle.ts +++ b/pyscriptjs/src/components/pytitle.ts @@ -2,36 +2,33 @@ 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); - - console.log('py-title connected'); + 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