new example, fix input bug

This commit is contained in:
Jonas Weinz 2022-10-16 18:41:50 +02:00
parent c236959c01
commit c51c1ba64a
22 changed files with 28776 additions and 14 deletions

View File

@ -1,5 +1,5 @@
const pwa_version = "01_hello_world_202210161053"
const pwa_version = "01_hello_world_202210161638"
const assets = ["./index.html",
"./main.py",
"./resources/bootstrap.css",

View File

@ -1,5 +1,5 @@
const pwa_version = "02_image_filter_202210161053"
const pwa_version = "02_image_filter_202210161638"
const assets = ["./index.html",
"./main.py",
"./resources/bootstrap.css",

View File

@ -0,0 +1,16 @@
{
"root_folder": "03_numpy_grid_demo",
"title": "03_numpy_grid_demo",
"packages": [
"numpy"
],
"paths": [],
"pyscript_css_url": "https://pyscript.net/latest/pyscript.css",
"pyscript_js_url": "https://pyscript.net/latest/pyscript.js",
"pyscript_py_url": "https://pyscript.net/latest/pyscript.py",
"bootstrap_css_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
"bootstrap_js_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
"pyscript_bootstrap_templates_wheel_url": "../dist/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl",
"pwa_bg_color": "#000000",
"pwa_theme_color": "#ffffff"
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%;height: 100%">
<head id="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="manifest.json" />
<script src="./resources/pwa.js"></script>
<link rel="stylesheet" href="./resources/pyscript.css" />
<script defer src="./resources/pyscript.js"></script>
<!-- Bootstrap CSS -->
<link href="./resources/bootstrap.css" rel="stylesheet">
<title>03_numpy_grid_demo</title>
</head>
<body style="width: 100%; height: 100%">
<py-config type="json">
{
"autoclose_loader": true,
"packages": [
"./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl",
"numpy"
],
"paths": []
}
</py-config>
<div id="pyscript_app" style="height: 100%; min-height: 100%"></div>
<py-script src="./main.py"></py-script>
<script src="./resources/bootstrap.js"></script>
</body>
</html>

View File

@ -0,0 +1,114 @@
from pyscript_bootstrap_templates import bootstrap_templates
from pyscript_bootstrap_templates import bootstrap_HTML as bHTML
from pyscript_bootstrap_templates import HTML as HTML
from pyscript_bootstrap_templates import bootstrap_inputs as bInputs
import numpy as np
# wrapper class to create editable numpy matrices:
class NumpyGrid2D(HTML.Table, bHTML.BootstrapContainer):
def __init__(self, numpy_grid: np.ndarray, parent: HTML.Element = None):
# store numpy grid
self._numpy_grid = numpy_grid.copy()
m,n = self._numpy_grid.shape
super().__init__(parent=parent)
self.m = 0
self.p = 0
# create a grid of inputs
self._inputs = []
for i in range(m):
row = []
# to keep it simple: use plain html table elements
tr = HTML.Tr(parent=self)
for j in range(n):
th = HTML.Th(parent=tr)
cell = bInputs.InputFloat(parent=th)
cell.p = 0
cell.m = 0
cell.value = self._numpy_grid[i,j]
row.append(cell)
self._inputs.append(row)
def _update_ndarray(self):
for i, row in enumerate(self._inputs):
for j, cell in enumerate(row):
self._numpy_grid[i,j] = float(cell.value)
@property
def numpy_grid(self) -> np.ndarray:
self._update_ndarray()
return self._numpy_grid.copy()
@numpy_grid.setter
def numpy_grid(self, value:np.ndarray):
assert value.shape == self._numpy_grid.shape
self._numpy_grid = value.copy()
# update input grid
for i, row in enumerate(self._inputs):
for j, cell in enumerate(row):
cell.value = self._numpy_grid[i,j]
# create default values
matrix_a = np.array([[1,2,3],[4,5,6],[7,8,9]])
matrix_b = np.array([[1,2,3],[4,5,6],[7,8,9]])
matrix_result = np.zeros((3,3))
app = bootstrap_templates.PyScriptBootstrapDashboard(
parent_element="pyscript_app", brand_name="03_numpy_grid_demo")
div = bHTML.BootstrapContainer("Matrix operations", parent=app.sidebar)
div.font_size = 4
matrix_row = bHTML.Row(parent=app.main)
matrix_row.p = 1
matrix_row.m = 1
matrix_row.display_property = bHTML.DisplayProperty.INLINE_FLEX
# create row of three matrices
mat_widget_a = NumpyGrid2D(matrix_a)
mat_widget_b = NumpyGrid2D(matrix_b)
mat_widget_result = NumpyGrid2D(matrix_result)
# just for fanciness: wrap the matrices in bootstrap cards
col_a = bHTML.Col(parent=matrix_row)
col_b = bHTML.Col(parent=matrix_row)
col_result = bHTML.Col(parent=matrix_row)
card_a = bHTML.Card(mat_widget_a, card_header=bHTML.Div("Matrix A"), parent=col_a)
card_b = bHTML.Card(mat_widget_b, card_header=bHTML.Div("Matrix B"), parent=col_b)
card_result = bHTML.Card(mat_widget_result, card_header=bHTML.Div("Result"), parent=col_result)
card_a.shadow = bHTML.Shadow.MEDIUM
card_b.shadow = bHTML.Shadow.MEDIUM
card_result.shadow = bHTML.Shadow.MEDIUM
operations = {
'+': np.add,
'-': np.subtract,
'*': np.multiply,
'/': np.divide,
'dot product': np.dot
}
for op in operations:
btn = bHTML.ButtonPrimary(op, parent=app.sidebar)
btn.w = 100
btn.m = 1
def onclick(event, numpy_func=operations[op]):
mat_widget_result.numpy_grid = numpy_func(mat_widget_a.numpy_grid, mat_widget_b.numpy_grid)
btn.onclick = onclick

View File

@ -0,0 +1 @@
{"name": "03_numpy_grid_demo", "short_name": "03_numpy_grid_demo", "start_url": "./index.html", "scope": ".", "display": "standalone", "background_color": "#000000", "theme_color": "#ffffff", "icons": [{"src": "resources/icon-512x512.png", "type": "image/png", "sizes": "512x512"}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,10 @@
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("./serviceWorker.js")
.then(res => console.log("service worker registered", res))
.catch(err => console.log("service worker not registered", err))
})
}

View File

@ -0,0 +1,244 @@
:not(:defined) {
display: none
}
html{
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
}
.spinner::after {
content: '';
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1);
border-left: 4px solid rgba(255, 255, 255, 1);
border-right: 4px solid rgba(255, 255, 255, 0);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
/* Pop-up second layer begin */
.py-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
transition: opacity 500ms;
visibility: hidden;
color: visible;
opacity: 1;
}
.py-overlay {
visibility: visible;
opacity: 1;
}
.py-pop-up {
text-align: center;
width: 600px;
}
.py-pop-up p {
margin: 5px;
}
.py-pop-up a {
position: absolute;
color: white;
text-decoration: none;
font-size: 200%;
top: 3.5%;
right: 5%;
}
.py-box{
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.py-box div.py-box-child *
{
max-width: 100%;
}
.py-repl-box{
flex-direction: column;
}
.editor-box{
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
position: relative;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
position: relative;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: rgb(209, 213, 219)
}
.editor-box:hover button{
opacity: 1;
}
.repl-play-button{
opacity: 0;
bottom: 0.25rem;
right: 0.25rem;
position: absolute;
padding: 0;
line-height: inherit;
color: inherit;
cursor: pointer;
background-color: transparent;
background-image: none;
-webkit-appearance: button;
text-transform: none;
font-family: inherit;
font-size: 100%;
margin: 0;
text-rendering: auto;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: center;
align-items: flex-start;
cursor: default;
box-sizing: border-box;
background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));
margin: 0em;
padding: 1px 6px;
border: 0;
}
.repl-play-button:hover{
opacity: 1;
}
.py-title{
text-transform: uppercase;
text-align: center;
}
.py-title h1{
font-weight: 700;
font-size: 1.875rem;
}
.py-input{
padding: 0.5rem;
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
margin-right: 0.75rem;
border-style: solid;
width: auto;
}
.py-box input.py-input{
width: -webkit-fill-available;
}
.central-content{
max-width: 20rem;
margin-left: auto;
margin-right: auto;
}
input {
text-rendering: auto;
color: -internal-light-dark(black, white);
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: start;
appearance: auto;
-webkit-rtl-ordering: logical;
cursor: text;
background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
margin: 0em;
padding: 1px 2px;
border-width: 2px;
border-style: inset;
border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
border-image: initial;
}
.py-button{
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
padding: 0.5rem;
--tw-bg-opacity: 1;
background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
--tw-border-opacity: 1;
border-color: rgba(37, 99, 235, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
}
.py-li-element p{
margin: 5px;
}
.py-li-element p{
display: inline;
}
button, input, optgroup, select, textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
.line-through {
text-decoration: line-through;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,424 @@
import asyncio
import base64
import io
import sys
import time
from textwrap import dedent
import micropip # noqa: F401
from js import console, document
loop = asyncio.get_event_loop()
MIME_METHODS = {
"__repr__": "text/plain",
"_repr_html_": "text/html",
"_repr_markdown_": "text/markdown",
"_repr_svg_": "image/svg+xml",
"_repr_png_": "image/png",
"_repr_pdf_": "application/pdf",
"_repr_jpeg_": "image/jpeg",
"_repr_latex": "text/latex",
"_repr_json_": "application/json",
"_repr_javascript_": "application/javascript",
"savefig": "image/png",
}
def render_image(mime, value, meta):
data = f"data:{mime};charset=utf-8;base64,{value}"
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
return f'<img src="{data}" {attrs}</img>'
def identity(value, meta):
return value
MIME_RENDERERS = {
"text/plain": identity,
"text/html": identity,
"image/png": lambda value, meta: render_image("image/png", value, meta),
"image/jpeg": lambda value, meta: render_image("image/jpeg", value, meta),
"image/svg+xml": identity,
"application/json": identity,
"application/javascript": lambda value, meta: f"<script>{value}</script>",
}
def eval_formatter(obj, print_method):
"""
Evaluates a formatter method.
"""
if print_method == "__repr__":
return repr(obj)
elif hasattr(obj, print_method):
if print_method == "savefig":
buf = io.BytesIO()
obj.savefig(buf, format="png")
buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8")
return getattr(obj, print_method)()
elif print_method == "_repr_mimebundle_":
return {}, {}
return None
def format_mime(obj):
"""
Formats object using _repr_x_ methods.
"""
if isinstance(obj, str):
return obj, "text/plain"
mimebundle = eval_formatter(obj, "_repr_mimebundle_")
if isinstance(mimebundle, tuple):
format_dict, _ = mimebundle
else:
format_dict = mimebundle
output, not_available = None, []
for method, mime_type in reversed(MIME_METHODS.items()):
if mime_type in format_dict:
output = format_dict[mime_type]
else:
output = eval_formatter(obj, method)
if output is None:
continue
elif mime_type not in MIME_RENDERERS:
not_available.append(mime_type)
continue
break
if output is None:
if not_available:
console.warning(
f"Rendered object requested unavailable MIME renderers: {not_available}"
)
output = repr(output)
mime_type = "text/plain"
elif isinstance(output, tuple):
output, meta = output
else:
meta = {}
return MIME_RENDERERS[mime_type](output, meta), mime_type
class PyScript:
loop = loop
@staticmethod
def write(element_id, value, append=False, exec_id=0):
"""Writes value to the element with id "element_id"""
console.log(f"APPENDING: {append} ==> {element_id} --> {value}")
if append:
child = document.createElement("div")
element = document.querySelector(f"#{element_id}")
if not element:
return
exec_id = exec_id or element.childElementCount + 1
element_id = child.id = f"{element_id}-{exec_id}"
element.appendChild(child)
element = document.getElementById(element_id)
html, mime_type = format_mime(value)
if mime_type in ("application/javascript", "text/html"):
script_element = document.createRange().createContextualFragment(html)
element.appendChild(script_element)
else:
element.innerHTML = html
@staticmethod
def run_until_complete(f):
_ = loop.run_until_complete(f)
class Element:
def __init__(self, element_id, element=None):
self._id = element_id
self._element = element
@property
def id(self):
return self._id
@property
def element(self):
"""Return the dom element"""
if not self._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
# so we can consolidate on how we write depending on the element type
pyscript.write(self._id, value, append=append)
def clear(self):
if hasattr(self.element, "value"):
self.element.value = ""
else:
self.write("", append=False)
def select(self, query, from_content=False):
el = self.element
if from_content:
el = el.content
_el = el.querySelector(query)
if _el:
return Element(_el.id, _el)
else:
console.log(f"WARNING: can't find element matching query {query}")
def clone(self, new_id=None, to=None):
if new_id is None:
new_id = self.element.id
clone = self.element.cloneNode(True)
clone.id = new_id
if to:
to.element.appendChild(clone)
# Inject it into the DOM
self.element.after(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 = dedent(
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):
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 in binds:
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
self._prev = out
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, err=None, output_to_console=True, append=True):
self._prev = self._out
self._out = out
self.output_to_console = output_to_console
self._append = append
console.log("----> changed out to", self._out, self._append)
def revert(self):
console.log("----> reverted")
self._out = self._prev
def write(self, txt):
console.log("writing to", self._out, txt, self._append)
if self._out:
pyscript.write(self._out, txt, append=self._append)
if self.output_to_console:
console.log(self._out, txt)
class OutputManager:
def __init__(self, out=None, err=None, output_to_console=True, append=True):
sys.stdout = self._out_manager = OutputCtxManager(
out, output_to_console, append
)
sys.stderr = self._err_manager = OutputCtxManager(
err, output_to_console, append
)
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, err=None, output_to_console=True, append=True):
self._out_manager.change(out, output_to_console, append)
sys.stdout = self._out_manager
self._err_manager.change(err, output_to_console, append)
sys.stderr = self._err_manager
self.output_to_console = output_to_console
self.append = append
def revert(self):
self._out_manager.revert()
self._err_manager.revert()
sys.stdout = self._out_manager
sys.stderr = self._err_manager
console.log("----> reverted")
pyscript = PyScript()
output_manager = OutputManager()

View File

@ -0,0 +1,51 @@
const pwa_version = "03_numpy_grid_demo_202210161638"
const assets = ["./index.html",
"./main.py",
"./resources/bootstrap.css",
"./resources/bootstrap.js",
"./resources/pyscript.css",
"./resources/pyscript.js",
"./resources/pyscript.py",
"./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl",
"./resources/pwa.js",
"./site.js",
]
self.addEventListener("install", installEvent => {
installEvent.waitUntil(
caches.open(pwa_version).then(cache => {
cache.addAll(assets).then(r => {
console.log("Cache assets downloaded");
}).catch(err => console.log("Error caching item", err))
console.log(`Cache ${pwa_version} opened.`);
}).catch(err => console.log("Error opening cache", err))
)
})
self.addEventListener('activate', e => {
console.log('Service Worker: Activated');
});
self.addEventListener('fetch', event => {
if (event.request.method != 'GET')
return;
event.respondWith((async () => {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
const response = await fetch(event.request);
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
const cache = await caches.open(pwa_version)
await cache.put(event.request, response.clone());
return response;
})());
});

View File

@ -0,0 +1,2 @@

View File

@ -1282,11 +1282,11 @@ class Input(Element):
@property
def value(self) -> str:
return self.get_attribute("value")
return self._element.value
@value.setter
def value(self, value: str) -> None:
self.set_attribute("value", value)
self._element.value = value
@property
def placeholder(self) -> str:

View File

@ -516,9 +516,11 @@ class Card(BootstrapContainer):
self._title_image = title_image
self._card_header = card_header
self.append_child(card_body)
self.append_child(title_image)
self.append_child(card_header)
self.append_child(title_image)
self.append_child(card_body)
@property
def card_body(self) -> HTML.Div:

View File

@ -23,14 +23,6 @@ class InputHelp(BootstrapContainer):
class InputFormControl(HTML.Input, BootstrapContainer):
_default_class_name: str = "form-control"
@property
def value(self) -> str:
return self._parse_value()
@value.setter
def value(self, value: str):
self._set_value(value)
class InputFormControlColor(InputFormControl):
_default_class_name: str = "form-control form-control-color"