1
0
mirror of https://github.com/pyscript/pyscript.git synced 2022-05-01 19:47:48 +03:00

Merge pull request #36 from anaconda/pys-18/add-pylist

[PYS-18] Add pylist
This commit is contained in:
Fabio Pliger
2022-04-20 20:29:33 -05:00
committed by GitHub
13 changed files with 599 additions and 14 deletions

View File

@@ -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()

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Todo App</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="/build/pyscript.css" />
<script defer src="/build/pyscript.js"></script>
<py-env>
- paths:
- /utils.py
</py-env>
<py-register-widget src="/pylist.py" name="py-list" klass="PyList"></py-register-widget>
</head>
<body>
<py-title>To Do List</py-title>
<py-box widths="4/5;1/5">
<py-inputbox id="new-task-content">
def on_keypress(e):
if (e.code == "Enter"):
add_task()
</py-inputbox>
<py-button id="new-task-btn" label="Add Task!">
def on_click(evt):
add_task()
</button>
</py-box>
<py-list id="myList"></py-list>
<py-repl id="my-repl" auto-generate="true"> </py-repl>
</body>
</html>

View File

@@ -53,7 +53,7 @@
for (let initializer of $postInitializers){ for (let initializer of $postInitializers){
initializer(); initializer();
} }
}, 5000); }, 3000);
} }

View File

@@ -149,5 +149,174 @@ export class BaseEvalElement extends HTMLElement {
this.errorElement.hidden = false; this.errorElement.hidden = false;
this.errorElement.style.display = 'block'; this.errorElement.style.display = 'block';
} }
} // end evaluate
async eval(source: string): Promise<void> {
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<void> {
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<string>{
let pyodide = await pyodideReadyPromise;
let response = await fetch(s);
return await response.text();
}
async eval(source: string): Promise<void> {
let output;
let pyodide = await pyodideReadyPromise;
try{
output = await pyodide.runPythonAsync(source);
if (output !== undefined){
console.log(output);
}
} catch (err) {
console.log(err);
}
} }
} }

View File

@@ -19,7 +19,7 @@ export class PyBox extends HTMLElement {
connectedCallback() { connectedCallback() {
let mainDiv = document.createElement('div'); 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 // Hack: for some reason when moving children, the editor box duplicates children
// meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box> // meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box>

View File

@@ -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<string>;
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');
}
}

View File

@@ -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<string>;
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);
}
}

View File

@@ -98,7 +98,7 @@ export class PyRepl extends BaseEvalElement {
}) })
let mainDiv = document.createElement('div'); 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 // add Editor to main PyScript div
// Butons DIV // Butons DIV
@@ -199,6 +199,10 @@ export class PyRepl extends BaseEvalElement {
} }
postEvaluate(): void { postEvaluate(): void {
this.outputElement.hidden = false;
this.outputElement.style.display = 'block';
if (this.hasAttribute('auto-generate')) { if (this.hasAttribute('auto-generate')) {
let nextExecId = parseInt(this.getAttribute('exec-id')) + 1; let nextExecId = parseInt(this.getAttribute('exec-id')) + 1;
const newPyRepl = document.createElement("py-repl"); const newPyRepl = document.createElement("py-repl");

View File

@@ -7,7 +7,7 @@ import { defaultKeymap } from "@codemirror/commands";
import { oneDarkTheme } from "@codemirror/theme-one-dark"; import { oneDarkTheme } from "@codemirror/theme-one-dark";
import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, currentComponentDetails, mode, addToScriptsQueue, addInitializer, addPostInitializer } from '../stores'; import { pyodideLoaded, loadedEnvironments, componentDetailsNavOpen, currentComponentDetails, mode, addToScriptsQueue, addInitializer, addPostInitializer } from '../stores';
import { addClasses } from '../utils'; import { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
// Premise used to connect to the first available pyodide interpreter // Premise used to connect to the first available pyodide interpreter
@@ -41,11 +41,6 @@ function createCmdHandler(el){
return toggleCheckbox return toggleCheckbox
} }
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
// TODO: use type declaractions // TODO: use type declaractions
type PyodideInterface = { type PyodideInterface = {
registerJsModule(name: string, module: object): void registerJsModule(name: string, module: object): void
@@ -296,7 +291,7 @@ async function mountElements() {
for (var el of matches) { for (var el of matches) {
let mountName = el.getAttribute('py-mount'); let mountName = el.getAttribute('py-mount');
if (!mountName){ if (!mountName){
mountName = el.id.replace("-", "_"); mountName = el.id.split("-").join("_");
} }
source += `\n${ mountName } = Element("${ el.id }")`; source += `\n${ mountName } = Element("${ el.id }")`;
} }

View File

@@ -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<string>;
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);
}
}

View File

@@ -6,8 +6,9 @@ let pyodideReadyPromise;
let pyodide; let pyodide;
let additional_definitions = ` let additional_definitions = `
from js import document, setInterval, console from js import document, setInterval, console, setTimeout
import micropip import micropip
import time
import asyncio import asyncio
import io, base64, sys import io, base64, sys
@@ -51,6 +52,10 @@ class Element:
self._id = element_id self._id = element_id
self._element = element self._element = element
@property
def id(self):
return self._id
@property @property
def element(self): def element(self):
"""Return the dom element""" """Return the dom element"""
@@ -58,6 +63,14 @@ class Element:
self._element = document.querySelector(f'#{self._id}'); self._element = document.querySelector(f'#{self._id}');
return self._element 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): def write(self, value, append=False):
console.log(f"Element.write: {value} --> {append}") console.log(f"Element.write: {value} --> {append}")
# TODO: it should be the opposite... pyscript.write should use the Element.write # TODO: it should be the opposite... pyscript.write should use the Element.write
@@ -96,6 +109,176 @@ class Element:
return Element(clone.id, clone) 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"""
<label for="flex items-center p-2 ">
<input class="mr-2" type="checkbox" class="task-check">
<p class="m-0 inline">{self.render_content()}</p>
</label>
"""
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: class OutputCtxManager:
def __init__(self, out=None, output_to_console=True, append=True): def __init__(self, out=None, output_to_console=True, append=True):
self._out = out self._out = out

View File

@@ -4,12 +4,19 @@ import { PyScript } from "./components/pyscript";
import { PyRepl } from "./components/pyrepl"; import { PyRepl } from "./components/pyrepl";
import { PyEnv } from "./components/pyenv"; import { PyEnv } from "./components/pyenv";
import { PyBox } from "./components/pybox"; 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 xPyScript = customElements.define('py-script', PyScript);
let xPyRepl = customElements.define('py-repl', PyRepl); let xPyRepl = customElements.define('py-repl', PyRepl);
let xPyEnv = customElements.define('py-env', PyEnv); let xPyEnv = customElements.define('py-env', PyEnv);
let xPyBox = customElements.define('py-box', PyBox); 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({ const app = new App({

View File

@@ -9,4 +9,29 @@ const getLastPath = function (str) {
return str.split('\\').pop().split('/').pop(); 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}