mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
move pydom and elements from pyweb to pyscript.web (#2092)
* change pydom example to use new pyscript.web namespace * change tests to use new pyscript.web namespace * create new pyscript.web package and move pydom to pyscript.web.dom * add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module * move elements from pyweb.ui to pyscript.web and temp fix pydom import * moved of elements file completed * moved media from pyweb to pyscript.web * RIP pyweb * move JSProperty from pyscript.web.dom to pyscript.web.elements * move element classes from pyscript.web.dom to pyscript.web.elements * first round of fixes while running tests * fix test typo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW * lint * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * clean up dom.py from dead commented code and osbolete comments * bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection * additional cleanup in tests * lint --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -19,16 +19,17 @@ def when(event_type=None, selector=None):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
|
||||||
|
from pyscript.web.elements import Element, ElementCollection
|
||||||
|
|
||||||
if isinstance(selector, str):
|
if isinstance(selector, str):
|
||||||
elements = document.querySelectorAll(selector)
|
elements = document.querySelectorAll(selector)
|
||||||
else:
|
else:
|
||||||
# TODO: This is a hack that will be removed when pyscript becomes a package
|
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||||
# and we can better manage the imports without circular dependencies
|
# and we can better manage the imports without circular dependencies
|
||||||
from pyweb import pydom
|
if isinstance(selector, Element):
|
||||||
|
|
||||||
if isinstance(selector, pydom.Element):
|
|
||||||
elements = [selector._js]
|
elements = [selector._js]
|
||||||
elif isinstance(selector, pydom.ElementCollection):
|
elif isinstance(selector, ElementCollection):
|
||||||
elements = [el._js for el in selector]
|
elements = [el._js for el in selector]
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
5
pyscript.core/src/stdlib/pyscript/web/__init__.py
Normal file
5
pyscript.core/src/stdlib/pyscript/web/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from . import elements
|
||||||
|
|
||||||
|
# Ugly trick to hide the dom module in the web package since we want the module
|
||||||
|
# to allow querying right away.
|
||||||
|
from .dom import dom
|
||||||
21
pyscript.core/src/stdlib/pyscript/web/dom.py
Normal file
21
pyscript.core/src/stdlib/pyscript/web/dom.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from pyscript import document
|
||||||
|
from pyscript.web.elements import Element, ElementCollection
|
||||||
|
|
||||||
|
|
||||||
|
class PyDom:
|
||||||
|
# Add objects we want to expose to the DOM namespace since this class instance is being
|
||||||
|
# remapped as "the module" itself
|
||||||
|
ElementCollection = ElementCollection
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._js = document
|
||||||
|
|
||||||
|
self.body = Element(document.body)
|
||||||
|
self.head = Element(document.head)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
elements = self._js.querySelectorAll(key)
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
|
||||||
|
dom = PyDom()
|
||||||
@@ -1,8 +1,40 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pyscript import document, when, window
|
try:
|
||||||
from pyweb import JSProperty, pydom
|
from typing import Any
|
||||||
|
except ImportError:
|
||||||
|
Any = "Any"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import warnings
|
||||||
|
except ImportError:
|
||||||
|
# TODO: For now it probably means we are in MicroPython. We should figure
|
||||||
|
# out the "right" way to handle this. For now we just ignore the warning
|
||||||
|
# and logging to console
|
||||||
|
class warnings:
|
||||||
|
@staticmethod
|
||||||
|
def warn(*args, **kwargs):
|
||||||
|
print("WARNING: ", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import cached_property
|
||||||
|
except ImportError:
|
||||||
|
# TODO: same comment about micropython as above
|
||||||
|
cached_property = property
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyodide.ffi import JsProxy
|
||||||
|
except ImportError:
|
||||||
|
# TODO: same comment about micropython as above
|
||||||
|
def JsProxy(obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
from pyscript import document, window
|
||||||
|
|
||||||
|
# from pyscript.web import dom as pydom
|
||||||
|
|
||||||
#: A flag to show if MicroPython is the current Python interpreter.
|
#: A flag to show if MicroPython is the current Python interpreter.
|
||||||
is_micropython = "MicroPython" in sys.version
|
is_micropython = "MicroPython" in sys.version
|
||||||
@@ -17,7 +49,428 @@ def getmembers_static(cls):
|
|||||||
return inspect.getmembers_static(cls)
|
return inspect.getmembers_static(cls)
|
||||||
|
|
||||||
|
|
||||||
class ElementBase(pydom.Element):
|
class JSProperty:
|
||||||
|
"""JS property descriptor that directly maps to the property with the same
|
||||||
|
name in the underlying JS component."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, allow_nones: bool = False):
|
||||||
|
self.name = name
|
||||||
|
self.allow_nones = allow_nones
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return getattr(obj._js, self.name)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
if not self.allow_nones and value is None:
|
||||||
|
return
|
||||||
|
setattr(obj._js, self.name, value)
|
||||||
|
|
||||||
|
|
||||||
|
# ------ TODO: REMOVE!!!! pydom elements
|
||||||
|
|
||||||
|
|
||||||
|
class BaseElement:
|
||||||
|
def __init__(self, js_element):
|
||||||
|
self._js = js_element
|
||||||
|
self._parent = None
|
||||||
|
self.style = StyleProxy(self)
|
||||||
|
self._proxies = {}
|
||||||
|
|
||||||
|
def __eq__(self, obj):
|
||||||
|
"""Check if the element is the same as the other element by comparing
|
||||||
|
the underlying JS element"""
|
||||||
|
return isinstance(obj, BaseElement) and obj._js == self._js
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
if self._parent:
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
if self._js.parentElement:
|
||||||
|
# TODO: This should actually return the correct class (== to tagName)
|
||||||
|
self._parent = Element(self._js.parentElement)
|
||||||
|
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def __class(self):
|
||||||
|
# return self.__class__ if self.__class__ != PyDom else Element
|
||||||
|
|
||||||
|
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
||||||
|
js_el = document.createElement(type_)
|
||||||
|
element = self.__class(js_el)
|
||||||
|
|
||||||
|
if classes:
|
||||||
|
for class_ in classes:
|
||||||
|
element.add_class(class_)
|
||||||
|
|
||||||
|
if html is not None:
|
||||||
|
element.html = html
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
element.label = label
|
||||||
|
|
||||||
|
if is_child:
|
||||||
|
self.append(element)
|
||||||
|
|
||||||
|
return element
|
||||||
|
|
||||||
|
def find(self, selector):
|
||||||
|
"""Return an ElementCollection representing all the child elements that
|
||||||
|
match the specified selector.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
selector (str): A string containing a selector expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ElementCollection: A collection of elements matching the selector
|
||||||
|
"""
|
||||||
|
elements = self._js.querySelectorAll(selector)
|
||||||
|
if not elements:
|
||||||
|
return None
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
|
||||||
|
class Element(BaseElement):
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return [self.__class__(el) for el in self._js.children]
|
||||||
|
|
||||||
|
def append(self, child):
|
||||||
|
# TODO: this is Pyodide specific for now!!!!!!
|
||||||
|
# if we get passed a JSProxy Element directly we just map it to the
|
||||||
|
# higher level Python element
|
||||||
|
if inspect.isclass(JsProxy) and isinstance(child, JsProxy):
|
||||||
|
return self.append(Element(child))
|
||||||
|
|
||||||
|
elif isinstance(child, Element):
|
||||||
|
self._js.appendChild(child._js)
|
||||||
|
|
||||||
|
return child
|
||||||
|
|
||||||
|
elif isinstance(child, ElementCollection):
|
||||||
|
for el in child:
|
||||||
|
self.append(el)
|
||||||
|
|
||||||
|
# -------- Pythonic Interface to Element -------- #
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._js.innerHTML
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._js.innerHTML = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._js.textContent
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, value):
|
||||||
|
self._js.textContent = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self):
|
||||||
|
# TODO: This breaks with with standard template elements. Define how to best
|
||||||
|
# handle this specifica use case. Just not support for now?
|
||||||
|
if self._js.tagName == "TEMPLATE":
|
||||||
|
warnings.warn(
|
||||||
|
"Content attribute not supported for template elements.", stacklevel=2
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return self._js.innerHTML
|
||||||
|
|
||||||
|
@content.setter
|
||||||
|
def content(self, value):
|
||||||
|
# TODO: (same comment as above)
|
||||||
|
if self._js.tagName == "TEMPLATE":
|
||||||
|
warnings.warn(
|
||||||
|
"Content attribute not supported for template elements.", stacklevel=2
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
display(value, target=self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._js.id
|
||||||
|
|
||||||
|
@id.setter
|
||||||
|
def id(self, value):
|
||||||
|
self._js.id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
if "options" in self._proxies:
|
||||||
|
return self._proxies["options"]
|
||||||
|
|
||||||
|
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no options attribute."
|
||||||
|
)
|
||||||
|
self._proxies["options"] = OptionsProxy(self)
|
||||||
|
return self._proxies["options"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._js.value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
# in order to avoid confusion to the user, we don't allow setting the
|
||||||
|
# value of elements that don't have a value attribute
|
||||||
|
if not hasattr(self._js, "value"):
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||||
|
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||||
|
"javascript API attribute instead."
|
||||||
|
)
|
||||||
|
self._js.value = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
return self._js.selected
|
||||||
|
|
||||||
|
@selected.setter
|
||||||
|
def selected(self, value):
|
||||||
|
# in order to avoid confusion to the user, we don't allow setting the
|
||||||
|
# value of elements that don't have a value attribute
|
||||||
|
if not hasattr(self._js, "selected"):
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||||
|
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||||
|
"javascript API attribute instead."
|
||||||
|
)
|
||||||
|
self._js.selected = value
|
||||||
|
|
||||||
|
def clone(self, new_id=None):
|
||||||
|
clone = Element(self._js.cloneNode(True))
|
||||||
|
clone.id = new_id
|
||||||
|
|
||||||
|
return clone
|
||||||
|
|
||||||
|
def remove_class(self, classname):
|
||||||
|
classList = self._js.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.remove(*classname)
|
||||||
|
else:
|
||||||
|
classList.remove(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_class(self, classname):
|
||||||
|
classList = self._js.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.add(*classname)
|
||||||
|
else:
|
||||||
|
self._js.classList.add(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def classes(self):
|
||||||
|
classes = self._js.classList.values()
|
||||||
|
return [x for x in classes]
|
||||||
|
|
||||||
|
def show_me(self):
|
||||||
|
self._js.scrollIntoView()
|
||||||
|
|
||||||
|
def snap(
|
||||||
|
self,
|
||||||
|
to: BaseElement | str = None,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Captures a snapshot of a video element. (Only available for video elements)
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
* to: element where to save the snapshot of the video frame to
|
||||||
|
* width: width of the image
|
||||||
|
* height: height of the image
|
||||||
|
|
||||||
|
Output:
|
||||||
|
(Element) canvas element where the video frame snapshot was drawn into
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "VIDEO":
|
||||||
|
raise AttributeError("Snap method is only available for video Elements")
|
||||||
|
|
||||||
|
if to is None:
|
||||||
|
canvas = self.create("canvas")
|
||||||
|
if width is None:
|
||||||
|
width = self._js.width
|
||||||
|
if height is None:
|
||||||
|
height = self._js.height
|
||||||
|
canvas._js.width = width
|
||||||
|
canvas._js.height = height
|
||||||
|
|
||||||
|
elif isinstance(to, Element):
|
||||||
|
if to._js.tagName != "CANVAS":
|
||||||
|
raise TypeError("Element to snap to must a canvas.")
|
||||||
|
canvas = to
|
||||||
|
elif getattr(to, "tagName", "") == "CANVAS":
|
||||||
|
canvas = Element(to)
|
||||||
|
elif isinstance(to, str):
|
||||||
|
# TODO (fpliger): This needs a better fix but doing a local import here for a quick fix
|
||||||
|
from pyscript.web import dom
|
||||||
|
|
||||||
|
canvas = dom[to][0]
|
||||||
|
if canvas._js.tagName != "CANVAS":
|
||||||
|
raise TypeError("Element to snap to must a be canvas.")
|
||||||
|
|
||||||
|
canvas.draw(self, width, height)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
def download(self, filename: str = "snapped.png") -> None:
|
||||||
|
"""Download the current element (only available for canvas elements) with the filename
|
||||||
|
provided in input.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
* filename (str): name of the file being downloaded
|
||||||
|
|
||||||
|
Output:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "CANVAS":
|
||||||
|
raise AttributeError(
|
||||||
|
"The download method is only available for canvas Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
link = self.create("a")
|
||||||
|
link._js.download = filename
|
||||||
|
link._js.href = self._js.toDataURL()
|
||||||
|
link._js.click()
|
||||||
|
|
||||||
|
def draw(self, what, width, height):
|
||||||
|
"""Draw `what` on the current element (only available for canvas elements).
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
* what (canvas image source): An element to draw into the context. The specification permits any canvas
|
||||||
|
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
|
||||||
|
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "CANVAS":
|
||||||
|
raise AttributeError(
|
||||||
|
"The draw method is only available for canvas Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(what, Element):
|
||||||
|
what = what._js
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
|
||||||
|
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsProxy:
|
||||||
|
"""This class represents the options of a select element. It
|
||||||
|
allows to access to add and remove options by using the `add` and `remove` methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, element: Element) -> None:
|
||||||
|
self._element = element
|
||||||
|
if self._element._js.tagName.lower() != "select":
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._element._js.tagName} has no options attribute."
|
||||||
|
)
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
value: Any = None,
|
||||||
|
html: str = None,
|
||||||
|
text: str = None,
|
||||||
|
before: Element | int = None,
|
||||||
|
**kws,
|
||||||
|
) -> None:
|
||||||
|
"""Add a new option to the select element"""
|
||||||
|
# create the option element and set the attributes
|
||||||
|
option = document.createElement("option")
|
||||||
|
if value is not None:
|
||||||
|
kws["value"] = value
|
||||||
|
if html is not None:
|
||||||
|
option.innerHTML = html
|
||||||
|
if text is not None:
|
||||||
|
kws["text"] = text
|
||||||
|
|
||||||
|
for key, value in kws.items():
|
||||||
|
option.setAttribute(key, value)
|
||||||
|
|
||||||
|
if before:
|
||||||
|
if isinstance(before, Element):
|
||||||
|
before = before._js
|
||||||
|
|
||||||
|
self._element._js.add(option, before)
|
||||||
|
|
||||||
|
def remove(self, item: int) -> None:
|
||||||
|
"""Remove the option at the specified index"""
|
||||||
|
self._element._js.remove(item)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Remove all the options"""
|
||||||
|
for i in range(len(self)):
|
||||||
|
self.remove(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
"""Return the list of options"""
|
||||||
|
return [Element(opt) for opt in self._element._js.options]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
"""Return the selected option"""
|
||||||
|
return self.options[self._element._js.selectedIndex]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self.options
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.options)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.options[key]
|
||||||
|
|
||||||
|
|
||||||
|
class StyleProxy: # (dict):
|
||||||
|
def __init__(self, element: Element) -> None:
|
||||||
|
self._element = element
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _style(self):
|
||||||
|
return self._element._js.style
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._style.getPropertyValue(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._style.setProperty(key, value)
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
self._style.removeProperty(key)
|
||||||
|
|
||||||
|
def set(self, **kws):
|
||||||
|
for k, v in kws.items():
|
||||||
|
self._element._js.style.setProperty(k, v)
|
||||||
|
|
||||||
|
# CSS Properties
|
||||||
|
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
||||||
|
# Following prperties automatically generated from the above reference using
|
||||||
|
# tools/codegen_css_proxy.py
|
||||||
|
@property
|
||||||
|
def visible(self):
|
||||||
|
return self._element._js.style.visibility
|
||||||
|
|
||||||
|
@visible.setter
|
||||||
|
def visible(self, value):
|
||||||
|
self._element._js.style.visibility = value
|
||||||
|
|
||||||
|
|
||||||
|
# --------- END OF PYDOM STUFF ------
|
||||||
|
|
||||||
|
|
||||||
|
class ElementBase(Element):
|
||||||
tag = "div"
|
tag = "div"
|
||||||
|
|
||||||
# GLOBAL ATTRIBUTES
|
# GLOBAL ATTRIBUTES
|
||||||
@@ -83,7 +536,7 @@ class TextElementBase(ElementBase):
|
|||||||
super().__init__(style=style, **kwargs)
|
super().__init__(style=style, **kwargs)
|
||||||
|
|
||||||
# If it's an element, append the element
|
# If it's an element, append the element
|
||||||
if isinstance(content, pydom.Element):
|
if isinstance(content, Element):
|
||||||
self.append(content)
|
self.append(content)
|
||||||
# If it's a list of elements
|
# If it's a list of elements
|
||||||
elif isinstance(content, list):
|
elif isinstance(content, list):
|
||||||
@@ -945,3 +1398,87 @@ class grid(TextElementBase):
|
|||||||
# TODO: This should be a property
|
# TODO: This should be a property
|
||||||
if not gap is None:
|
if not gap is None:
|
||||||
self.style["gap"] = gap
|
self.style["gap"] = gap
|
||||||
|
|
||||||
|
|
||||||
|
class StyleCollection:
|
||||||
|
def __init__(self, collection: "ElementCollection") -> None:
|
||||||
|
self._collection = collection
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return obj._get_attribute("style")
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._collection._get_attribute("style")[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style[key] = value
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style.remove(key)
|
||||||
|
|
||||||
|
|
||||||
|
class ElementCollection:
|
||||||
|
def __init__(self, elements: [Element]) -> None:
|
||||||
|
self._elements = elements
|
||||||
|
self.style = StyleCollection(self)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
# If it's an integer we use it to access the elements in the collection
|
||||||
|
if isinstance(key, int):
|
||||||
|
return self._elements[key]
|
||||||
|
# If it's a slice we use it to support slice operations over the elements
|
||||||
|
# in the collection
|
||||||
|
elif isinstance(key, slice):
|
||||||
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
|
# If it's anything else (basically a string) we use it as a selector
|
||||||
|
# TODO: Write tests!
|
||||||
|
elements = self._element.querySelectorAll(key)
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._elements)
|
||||||
|
|
||||||
|
def __eq__(self, obj):
|
||||||
|
"""Check if the element is the same as the other element by comparing
|
||||||
|
the underlying JS element"""
|
||||||
|
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
||||||
|
|
||||||
|
def _get_attribute(self, attr, index=None):
|
||||||
|
if index is None:
|
||||||
|
return [getattr(el, attr) for el in self._elements]
|
||||||
|
|
||||||
|
# As JQuery, when getting an attr, only return it for the first element
|
||||||
|
return getattr(self._elements[index], attr)
|
||||||
|
|
||||||
|
def _set_attribute(self, attr, value):
|
||||||
|
for el in self._elements:
|
||||||
|
setattr(el, attr, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._get_attribute("html")
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._set_attribute("html", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._get_attribute("value")
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
self._set_attribute("value", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return self._elements
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self._elements
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .pydom import JSProperty
|
|
||||||
from .pydom import dom as pydom
|
|
||||||
@@ -1,569 +0,0 @@
|
|||||||
import inspect
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Any
|
|
||||||
except ImportError:
|
|
||||||
Any = "Any"
|
|
||||||
|
|
||||||
try:
|
|
||||||
import warnings
|
|
||||||
except ImportError:
|
|
||||||
# TODO: For now it probably means we are in MicroPython. We should figure
|
|
||||||
# out the "right" way to handle this. For now we just ignore the warning
|
|
||||||
# and logging to console
|
|
||||||
class warnings:
|
|
||||||
@staticmethod
|
|
||||||
def warn(*args, **kwargs):
|
|
||||||
print("WARNING: ", *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from functools import cached_property
|
|
||||||
except ImportError:
|
|
||||||
# TODO: same comment about micropython as above
|
|
||||||
cached_property = property
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pyodide.ffi import JsProxy
|
|
||||||
except ImportError:
|
|
||||||
# TODO: same comment about micropython as above
|
|
||||||
def JsProxy(obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
from pyscript import display, document, window
|
|
||||||
|
|
||||||
alert = window.alert
|
|
||||||
|
|
||||||
|
|
||||||
class JSProperty:
|
|
||||||
"""JS property descriptor that directly maps to the property with the same
|
|
||||||
name in the underlying JS component."""
|
|
||||||
|
|
||||||
def __init__(self, name: str, allow_nones: bool = False):
|
|
||||||
self.name = name
|
|
||||||
self.allow_nones = allow_nones
|
|
||||||
|
|
||||||
def __get__(self, obj, objtype=None):
|
|
||||||
return getattr(obj._js, self.name)
|
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
|
||||||
if not self.allow_nones and value is None:
|
|
||||||
return
|
|
||||||
setattr(obj._js, self.name, value)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseElement:
|
|
||||||
def __init__(self, js_element):
|
|
||||||
self._js = js_element
|
|
||||||
self._parent = None
|
|
||||||
self.style = StyleProxy(self)
|
|
||||||
self._proxies = {}
|
|
||||||
|
|
||||||
def __eq__(self, obj):
|
|
||||||
"""Check if the element is the same as the other element by comparing
|
|
||||||
the underlying JS element"""
|
|
||||||
return isinstance(obj, BaseElement) and obj._js == self._js
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
if self._parent:
|
|
||||||
return self._parent
|
|
||||||
|
|
||||||
if self._js.parentElement:
|
|
||||||
self._parent = self.__class__(self._js.parentElement)
|
|
||||||
|
|
||||||
return self._parent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def __class(self):
|
|
||||||
return self.__class__ if self.__class__ != PyDom else Element
|
|
||||||
|
|
||||||
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
|
||||||
js_el = document.createElement(type_)
|
|
||||||
element = self.__class(js_el)
|
|
||||||
|
|
||||||
if classes:
|
|
||||||
for class_ in classes:
|
|
||||||
element.add_class(class_)
|
|
||||||
|
|
||||||
if html is not None:
|
|
||||||
element.html = html
|
|
||||||
|
|
||||||
if label is not None:
|
|
||||||
element.label = label
|
|
||||||
|
|
||||||
if is_child:
|
|
||||||
self.append(element)
|
|
||||||
|
|
||||||
return element
|
|
||||||
|
|
||||||
def find(self, selector):
|
|
||||||
"""Return an ElementCollection representing all the child elements that
|
|
||||||
match the specified selector.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
selector (str): A string containing a selector expression
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ElementCollection: A collection of elements matching the selector
|
|
||||||
"""
|
|
||||||
elements = self._js.querySelectorAll(selector)
|
|
||||||
if not elements:
|
|
||||||
return None
|
|
||||||
return ElementCollection([Element(el) for el in elements])
|
|
||||||
|
|
||||||
|
|
||||||
class Element(BaseElement):
|
|
||||||
@property
|
|
||||||
def children(self):
|
|
||||||
return [self.__class__(el) for el in self._js.children]
|
|
||||||
|
|
||||||
def append(self, child):
|
|
||||||
# TODO: this is Pyodide specific for now!!!!!!
|
|
||||||
# if we get passed a JSProxy Element directly we just map it to the
|
|
||||||
# higher level Python element
|
|
||||||
if inspect.isclass(JsProxy) and isinstance(child, JsProxy):
|
|
||||||
return self.append(Element(child))
|
|
||||||
|
|
||||||
elif isinstance(child, Element):
|
|
||||||
self._js.appendChild(child._js)
|
|
||||||
|
|
||||||
return child
|
|
||||||
|
|
||||||
elif isinstance(child, ElementCollection):
|
|
||||||
for el in child:
|
|
||||||
self.append(el)
|
|
||||||
|
|
||||||
# -------- Pythonic Interface to Element -------- #
|
|
||||||
@property
|
|
||||||
def html(self):
|
|
||||||
return self._js.innerHTML
|
|
||||||
|
|
||||||
@html.setter
|
|
||||||
def html(self, value):
|
|
||||||
self._js.innerHTML = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
return self._js.textContent
|
|
||||||
|
|
||||||
@text.setter
|
|
||||||
def text(self, value):
|
|
||||||
self._js.textContent = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self):
|
|
||||||
# TODO: This breaks with with standard template elements. Define how to best
|
|
||||||
# handle this specifica use case. Just not support for now?
|
|
||||||
if self._js.tagName == "TEMPLATE":
|
|
||||||
warnings.warn(
|
|
||||||
"Content attribute not supported for template elements.", stacklevel=2
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
return self._js.innerHTML
|
|
||||||
|
|
||||||
@content.setter
|
|
||||||
def content(self, value):
|
|
||||||
# TODO: (same comment as above)
|
|
||||||
if self._js.tagName == "TEMPLATE":
|
|
||||||
warnings.warn(
|
|
||||||
"Content attribute not supported for template elements.", stacklevel=2
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
display(value, target=self.id)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self):
|
|
||||||
return self._js.id
|
|
||||||
|
|
||||||
@id.setter
|
|
||||||
def id(self, value):
|
|
||||||
self._js.id = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self):
|
|
||||||
if "options" in self._proxies:
|
|
||||||
return self._proxies["options"]
|
|
||||||
|
|
||||||
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
|
|
||||||
raise AttributeError(
|
|
||||||
f"Element {self._js.tagName} has no options attribute."
|
|
||||||
)
|
|
||||||
self._proxies["options"] = OptionsProxy(self)
|
|
||||||
return self._proxies["options"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self._js.value
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value):
|
|
||||||
# in order to avoid confusion to the user, we don't allow setting the
|
|
||||||
# value of elements that don't have a value attribute
|
|
||||||
if not hasattr(self._js, "value"):
|
|
||||||
raise AttributeError(
|
|
||||||
f"Element {self._js.tagName} has no value attribute. If you want to "
|
|
||||||
"force a value attribute, set it directly using the `_js.value = <value>` "
|
|
||||||
"javascript API attribute instead."
|
|
||||||
)
|
|
||||||
self._js.value = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def selected(self):
|
|
||||||
return self._js.selected
|
|
||||||
|
|
||||||
@selected.setter
|
|
||||||
def selected(self, value):
|
|
||||||
# in order to avoid confusion to the user, we don't allow setting the
|
|
||||||
# value of elements that don't have a value attribute
|
|
||||||
if not hasattr(self._js, "selected"):
|
|
||||||
raise AttributeError(
|
|
||||||
f"Element {self._js.tagName} has no value attribute. If you want to "
|
|
||||||
"force a value attribute, set it directly using the `_js.value = <value>` "
|
|
||||||
"javascript API attribute instead."
|
|
||||||
)
|
|
||||||
self._js.selected = value
|
|
||||||
|
|
||||||
def clone(self, new_id=None):
|
|
||||||
clone = Element(self._js.cloneNode(True))
|
|
||||||
clone.id = new_id
|
|
||||||
|
|
||||||
return clone
|
|
||||||
|
|
||||||
def remove_class(self, classname):
|
|
||||||
classList = self._js.classList
|
|
||||||
if isinstance(classname, list):
|
|
||||||
classList.remove(*classname)
|
|
||||||
else:
|
|
||||||
classList.remove(classname)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def add_class(self, classname):
|
|
||||||
classList = self._js.classList
|
|
||||||
if isinstance(classname, list):
|
|
||||||
classList.add(*classname)
|
|
||||||
else:
|
|
||||||
self._js.classList.add(classname)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@property
|
|
||||||
def classes(self):
|
|
||||||
classes = self._js.classList.values()
|
|
||||||
return [x for x in classes]
|
|
||||||
|
|
||||||
def show_me(self):
|
|
||||||
self._js.scrollIntoView()
|
|
||||||
|
|
||||||
def snap(
|
|
||||||
self,
|
|
||||||
to: BaseElement | str = None,
|
|
||||||
width: int | None = None,
|
|
||||||
height: int | None = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Captures a snapshot of a video element. (Only available for video elements)
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
|
|
||||||
* to: element where to save the snapshot of the video frame to
|
|
||||||
* width: width of the image
|
|
||||||
* height: height of the image
|
|
||||||
|
|
||||||
Output:
|
|
||||||
(Element) canvas element where the video frame snapshot was drawn into
|
|
||||||
"""
|
|
||||||
if self._js.tagName != "VIDEO":
|
|
||||||
raise AttributeError("Snap method is only available for video Elements")
|
|
||||||
|
|
||||||
if to is None:
|
|
||||||
canvas = self.create("canvas")
|
|
||||||
if width is None:
|
|
||||||
width = self._js.width
|
|
||||||
if height is None:
|
|
||||||
height = self._js.height
|
|
||||||
canvas._js.width = width
|
|
||||||
canvas._js.height = height
|
|
||||||
|
|
||||||
elif isinstance(to, Element):
|
|
||||||
if to._js.tagName != "CANVAS":
|
|
||||||
raise TypeError("Element to snap to must a canvas.")
|
|
||||||
canvas = to
|
|
||||||
elif getattr(to, "tagName", "") == "CANVAS":
|
|
||||||
canvas = Element(to)
|
|
||||||
elif isinstance(to, str):
|
|
||||||
canvas = pydom[to][0]
|
|
||||||
if canvas._js.tagName != "CANVAS":
|
|
||||||
raise TypeError("Element to snap to must a be canvas.")
|
|
||||||
|
|
||||||
canvas.draw(self, width, height)
|
|
||||||
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def download(self, filename: str = "snapped.png") -> None:
|
|
||||||
"""Download the current element (only available for canvas elements) with the filename
|
|
||||||
provided in input.
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
* filename (str): name of the file being downloaded
|
|
||||||
|
|
||||||
Output:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
if self._js.tagName != "CANVAS":
|
|
||||||
raise AttributeError(
|
|
||||||
"The download method is only available for canvas Elements"
|
|
||||||
)
|
|
||||||
|
|
||||||
link = self.create("a")
|
|
||||||
link._js.download = filename
|
|
||||||
link._js.href = self._js.toDataURL()
|
|
||||||
link._js.click()
|
|
||||||
|
|
||||||
def draw(self, what, width, height):
|
|
||||||
"""Draw `what` on the current element (only available for canvas elements).
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
|
|
||||||
* what (canvas image source): An element to draw into the context. The specification permits any canvas
|
|
||||||
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
|
|
||||||
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
|
|
||||||
"""
|
|
||||||
if self._js.tagName != "CANVAS":
|
|
||||||
raise AttributeError(
|
|
||||||
"The draw method is only available for canvas Elements"
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(what, Element):
|
|
||||||
what = what._js
|
|
||||||
|
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
|
|
||||||
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionsProxy:
|
|
||||||
"""This class represents the options of a select element. It
|
|
||||||
allows to access to add and remove options by using the `add` and `remove` methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, element: Element) -> None:
|
|
||||||
self._element = element
|
|
||||||
if self._element._js.tagName.lower() != "select":
|
|
||||||
raise AttributeError(
|
|
||||||
f"Element {self._element._js.tagName} has no options attribute."
|
|
||||||
)
|
|
||||||
|
|
||||||
def add(
|
|
||||||
self,
|
|
||||||
value: Any = None,
|
|
||||||
html: str = None,
|
|
||||||
text: str = None,
|
|
||||||
before: Element | int = None,
|
|
||||||
**kws,
|
|
||||||
) -> None:
|
|
||||||
"""Add a new option to the select element"""
|
|
||||||
# create the option element and set the attributes
|
|
||||||
option = document.createElement("option")
|
|
||||||
if value is not None:
|
|
||||||
kws["value"] = value
|
|
||||||
if html is not None:
|
|
||||||
option.innerHTML = html
|
|
||||||
if text is not None:
|
|
||||||
kws["text"] = text
|
|
||||||
|
|
||||||
for key, value in kws.items():
|
|
||||||
option.setAttribute(key, value)
|
|
||||||
|
|
||||||
if before:
|
|
||||||
if isinstance(before, Element):
|
|
||||||
before = before._js
|
|
||||||
|
|
||||||
self._element._js.add(option, before)
|
|
||||||
|
|
||||||
def remove(self, item: int) -> None:
|
|
||||||
"""Remove the option at the specified index"""
|
|
||||||
self._element._js.remove(item)
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
"""Remove all the options"""
|
|
||||||
for i in range(len(self)):
|
|
||||||
self.remove(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self):
|
|
||||||
"""Return the list of options"""
|
|
||||||
return [Element(opt) for opt in self._element._js.options]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def selected(self):
|
|
||||||
"""Return the selected option"""
|
|
||||||
return self.options[self._element._js.selectedIndex]
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
yield from self.options
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.options)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self.options[key]
|
|
||||||
|
|
||||||
|
|
||||||
class StyleProxy: # (dict):
|
|
||||||
def __init__(self, element: Element) -> None:
|
|
||||||
self._element = element
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def _style(self):
|
|
||||||
return self._element._js.style
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self._style.getPropertyValue(key)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self._style.setProperty(key, value)
|
|
||||||
|
|
||||||
def remove(self, key):
|
|
||||||
self._style.removeProperty(key)
|
|
||||||
|
|
||||||
def set(self, **kws):
|
|
||||||
for k, v in kws.items():
|
|
||||||
self._element._js.style.setProperty(k, v)
|
|
||||||
|
|
||||||
# CSS Properties
|
|
||||||
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
|
||||||
# Following prperties automatically generated from the above reference using
|
|
||||||
# tools/codegen_css_proxy.py
|
|
||||||
@property
|
|
||||||
def visible(self):
|
|
||||||
return self._element._js.style.visibility
|
|
||||||
|
|
||||||
@visible.setter
|
|
||||||
def visible(self, value):
|
|
||||||
self._element._js.style.visibility = value
|
|
||||||
|
|
||||||
|
|
||||||
class StyleCollection:
|
|
||||||
def __init__(self, collection: "ElementCollection") -> None:
|
|
||||||
self._collection = collection
|
|
||||||
|
|
||||||
def __get__(self, obj, objtype=None):
|
|
||||||
return obj._get_attribute("style")
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self._collection._get_attribute("style")[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
for element in self._collection._elements:
|
|
||||||
element.style[key] = value
|
|
||||||
|
|
||||||
def remove(self, key):
|
|
||||||
for element in self._collection._elements:
|
|
||||||
element.style.remove(key)
|
|
||||||
|
|
||||||
|
|
||||||
class ElementCollection:
|
|
||||||
def __init__(self, elements: [Element]) -> None:
|
|
||||||
self._elements = elements
|
|
||||||
self.style = StyleCollection(self)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
# If it's an integer we use it to access the elements in the collection
|
|
||||||
if isinstance(key, int):
|
|
||||||
return self._elements[key]
|
|
||||||
# If it's a slice we use it to support slice operations over the elements
|
|
||||||
# in the collection
|
|
||||||
elif isinstance(key, slice):
|
|
||||||
return ElementCollection(self._elements[key])
|
|
||||||
|
|
||||||
# If it's anything else (basically a string) we use it as a selector
|
|
||||||
# TODO: Write tests!
|
|
||||||
elements = self._element.querySelectorAll(key)
|
|
||||||
return ElementCollection([Element(el) for el in elements])
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self._elements)
|
|
||||||
|
|
||||||
def __eq__(self, obj):
|
|
||||||
"""Check if the element is the same as the other element by comparing
|
|
||||||
the underlying JS element"""
|
|
||||||
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
|
||||||
|
|
||||||
def _get_attribute(self, attr, index=None):
|
|
||||||
if index is None:
|
|
||||||
return [getattr(el, attr) for el in self._elements]
|
|
||||||
|
|
||||||
# As JQuery, when getting an attr, only return it for the first element
|
|
||||||
return getattr(self._elements[index], attr)
|
|
||||||
|
|
||||||
def _set_attribute(self, attr, value):
|
|
||||||
for el in self._elements:
|
|
||||||
setattr(el, attr, value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def html(self):
|
|
||||||
return self._get_attribute("html")
|
|
||||||
|
|
||||||
@html.setter
|
|
||||||
def html(self, value):
|
|
||||||
self._set_attribute("html", value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
return self._get_attribute("value")
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value):
|
|
||||||
self._set_attribute("value", value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def children(self):
|
|
||||||
return self._elements
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
yield from self._elements
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
|
||||||
|
|
||||||
|
|
||||||
class DomScope:
|
|
||||||
def __getattr__(self, __name: str):
|
|
||||||
element = document[f"#{__name}"]
|
|
||||||
if element:
|
|
||||||
return element[0]
|
|
||||||
|
|
||||||
|
|
||||||
class PyDom(BaseElement):
|
|
||||||
# Add objects we want to expose to the DOM namespace since this class instance is being
|
|
||||||
# remapped as "the module" itself
|
|
||||||
BaseElement = BaseElement
|
|
||||||
Element = Element
|
|
||||||
ElementCollection = ElementCollection
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# PyDom is a special case of BaseElement where we don't want to create a new JS element
|
|
||||||
# and it really doesn't have a need for styleproxy or parent to to call to __init__
|
|
||||||
# (which actually fails in MP for some reason)
|
|
||||||
self._js = document
|
|
||||||
self._parent = None
|
|
||||||
self._proxies = {}
|
|
||||||
self.ids = DomScope()
|
|
||||||
self.body = Element(document.body)
|
|
||||||
self.head = Element(document.head)
|
|
||||||
|
|
||||||
def create(self, type_, classes=None, html=None):
|
|
||||||
return super().create(type_, is_child=False, classes=classes, html=html)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
elements = self._js.querySelectorAll(key)
|
|
||||||
if not elements:
|
|
||||||
return None
|
|
||||||
return ElementCollection([Element(el) for el in elements])
|
|
||||||
|
|
||||||
|
|
||||||
dom = PyDom()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import elements
|
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<script type="mpy" src="pydom.py"></script>
|
<script type="mpy" src="pydom.py"></script>
|
||||||
|
|
||||||
|
<div id="system-info"></div>
|
||||||
|
|
||||||
<button id="just-a-button">Click For Time</button>
|
<button id="just-a-button">Click For Time</button>
|
||||||
<button id="color-button">Click For Color</button>
|
<button id="color-button">Click For Color</button>
|
||||||
<button id="color-reset-button">Reset Color</button>
|
<button id="color-reset-button">Reset Color</button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import time
|
|||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
|
|
||||||
from pyscript import display, when
|
from pyscript import display, when
|
||||||
from pyweb import pydom
|
from pyscript.web import dom
|
||||||
|
|
||||||
display(sys.version, target="system-info")
|
display(sys.version, target="system-info")
|
||||||
|
|
||||||
@@ -19,18 +19,15 @@ def on_click():
|
|||||||
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
|
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
|
||||||
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:])
|
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:])
|
||||||
|
|
||||||
display(f"Hello from PyScript, time is: {timenow}", append=False, target="result")
|
display(f"Hello from PyScript, time is: {timenow}", append=False, target="#result")
|
||||||
|
|
||||||
|
|
||||||
@when("click", "#color-button")
|
@when("click", "#color-button")
|
||||||
def on_color_click(event):
|
def on_color_click(event):
|
||||||
btn = pydom["#result"]
|
btn = dom["#result"]
|
||||||
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
|
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
|
||||||
|
|
||||||
|
|
||||||
@when("click", "#color-reset-button")
|
@when("click", "#color-reset-button")
|
||||||
def reset_color(*args, **kwargs):
|
def reset_color(*args, **kwargs):
|
||||||
pydom["#result"].style["background-color"] = "white"
|
dom["#result"].style["background-color"] = "white"
|
||||||
|
|
||||||
|
|
||||||
# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color)
|
|
||||||
|
|||||||
@@ -2,22 +2,13 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pyscript import document, when
|
from pyscript import document, when
|
||||||
from pyweb import pydom
|
from pyscript.web import dom
|
||||||
|
from pyscript.web import elements as el
|
||||||
|
|
||||||
|
|
||||||
class TestDocument:
|
class TestDocument:
|
||||||
def test__element(self):
|
def test__element(self):
|
||||||
assert pydom._js == document
|
assert dom._js == document
|
||||||
|
|
||||||
def test_no_parent(self):
|
|
||||||
assert pydom.parent is None
|
|
||||||
|
|
||||||
def test_create_element(self):
|
|
||||||
new_el = pydom.create("div")
|
|
||||||
assert isinstance(new_el, pydom.BaseElement)
|
|
||||||
assert new_el._js.tagName == "DIV"
|
|
||||||
# EXPECT the new element to be associated with the document
|
|
||||||
assert new_el.parent == None
|
|
||||||
|
|
||||||
|
|
||||||
def test_getitem_by_id():
|
def test_getitem_by_id():
|
||||||
@@ -26,14 +17,14 @@ def test_getitem_by_id():
|
|||||||
txt = "You found test_id_selector"
|
txt = "You found test_id_selector"
|
||||||
selector = f"#{id_}"
|
selector = f"#{id_}"
|
||||||
# EXPECT the element to be found by id
|
# EXPECT the element to be found by id
|
||||||
result = pydom[selector]
|
result = dom[selector]
|
||||||
div = result[0]
|
div = result[0]
|
||||||
# EXPECT the element text value to match what we expect and what
|
# EXPECT the element text value to match what we expect and what
|
||||||
# the JS document.querySelector API would return
|
# the JS document.querySelector API would return
|
||||||
assert document.querySelector(selector).innerHTML == div.html == txt
|
assert document.querySelector(selector).innerHTML == div.html == txt
|
||||||
# EXPECT the results to be of the right types
|
# EXPECT the results to be of the right types
|
||||||
assert isinstance(div, pydom.BaseElement)
|
assert isinstance(div, el.BaseElement)
|
||||||
assert isinstance(result, pydom.ElementCollection)
|
assert isinstance(result, dom.ElementCollection)
|
||||||
|
|
||||||
|
|
||||||
def test_getitem_by_class():
|
def test_getitem_by_class():
|
||||||
@@ -43,7 +34,7 @@ def test_getitem_by_class():
|
|||||||
"test_selector_w_children_child_1",
|
"test_selector_w_children_child_1",
|
||||||
]
|
]
|
||||||
expected_class = "a-test-class"
|
expected_class = "a-test-class"
|
||||||
result = pydom[f".{expected_class}"]
|
result = dom[f".{expected_class}"]
|
||||||
div = result[0]
|
div = result[0]
|
||||||
|
|
||||||
# EXPECT to find exact number of elements with the class in the page (== 3)
|
# EXPECT to find exact number of elements with the class in the page (== 3)
|
||||||
@@ -54,7 +45,7 @@ def test_getitem_by_class():
|
|||||||
|
|
||||||
|
|
||||||
def test_read_n_write_collection_elements():
|
def test_read_n_write_collection_elements():
|
||||||
elements = pydom[".multi-elems"]
|
elements = dom[".multi-elems"]
|
||||||
|
|
||||||
for element in elements:
|
for element in elements:
|
||||||
assert element.html == f"Content {element.id.replace('#', '')}"
|
assert element.html == f"Content {element.id.replace('#', '')}"
|
||||||
@@ -69,15 +60,15 @@ class TestElement:
|
|||||||
def test_query(self):
|
def test_query(self):
|
||||||
# GIVEN an existing element on the page, with at least 1 child element
|
# GIVEN an existing element on the page, with at least 1 child element
|
||||||
id_ = "test_selector_w_children"
|
id_ = "test_selector_w_children"
|
||||||
parent_div = pydom[f"#{id_}"][0]
|
parent_div = dom[f"#{id_}"][0]
|
||||||
|
|
||||||
# EXPECT it to be able to query for the first child element
|
# EXPECT it to be able to query for the first child element
|
||||||
div = parent_div.find("div")[0]
|
div = parent_div.find("div")[0]
|
||||||
|
|
||||||
# EXPECT the new element to be associated with the parent
|
# EXPECT the new element to be associated with the parent
|
||||||
assert div.parent == parent_div
|
assert div.parent == parent_div
|
||||||
# EXPECT the new element to be a BaseElement
|
# EXPECT the new element to be a el.BaseElement
|
||||||
assert isinstance(div, pydom.BaseElement)
|
assert isinstance(div, el.BaseElement)
|
||||||
# EXPECT the div attributes to be == to how they are configured in the page
|
# EXPECT the div attributes to be == to how they are configured in the page
|
||||||
assert div.html == "Child 1"
|
assert div.html == "Child 1"
|
||||||
assert div.id == "test_selector_w_children_child_1"
|
assert div.id == "test_selector_w_children_child_1"
|
||||||
@@ -86,8 +77,8 @@ class TestElement:
|
|||||||
# GIVEN 2 different Elements pointing to the same underlying element
|
# GIVEN 2 different Elements pointing to the same underlying element
|
||||||
id_ = "test_id_selector"
|
id_ = "test_id_selector"
|
||||||
selector = f"#{id_}"
|
selector = f"#{id_}"
|
||||||
div = pydom[selector][0]
|
div = dom[selector][0]
|
||||||
div2 = pydom[selector][0]
|
div2 = dom[selector][0]
|
||||||
|
|
||||||
# EXPECT them to be equal
|
# EXPECT them to be equal
|
||||||
assert div == div2
|
assert div == div2
|
||||||
@@ -102,27 +93,27 @@ class TestElement:
|
|||||||
|
|
||||||
def test_append_element(self):
|
def test_append_element(self):
|
||||||
id_ = "element-append-tests"
|
id_ = "element-append-tests"
|
||||||
div = pydom[f"#{id_}"][0]
|
div = dom[f"#{id_}"][0]
|
||||||
len_children_before = len(div.children)
|
len_children_before = len(div.children)
|
||||||
new_el = div.create("p")
|
new_el = el.p("new element")
|
||||||
div.append(new_el)
|
div.append(new_el)
|
||||||
assert len(div.children) == len_children_before + 1
|
assert len(div.children) == len_children_before + 1
|
||||||
assert div.children[-1] == new_el
|
assert div.children[-1] == new_el
|
||||||
|
|
||||||
def test_append_js_element(self):
|
def test_append_js_element(self):
|
||||||
id_ = "element-append-tests"
|
id_ = "element-append-tests"
|
||||||
div = pydom[f"#{id_}"][0]
|
div = dom[f"#{id_}"][0]
|
||||||
len_children_before = len(div.children)
|
len_children_before = len(div.children)
|
||||||
new_el = div.create("p")
|
new_el = el.p("new element")
|
||||||
div.append(new_el._js)
|
div.append(new_el._js)
|
||||||
assert len(div.children) == len_children_before + 1
|
assert len(div.children) == len_children_before + 1
|
||||||
assert div.children[-1] == new_el
|
assert div.children[-1] == new_el
|
||||||
|
|
||||||
def test_append_collection(self):
|
def test_append_collection(self):
|
||||||
id_ = "element-append-tests"
|
id_ = "element-append-tests"
|
||||||
div = pydom[f"#{id_}"][0]
|
div = dom[f"#{id_}"][0]
|
||||||
len_children_before = len(div.children)
|
len_children_before = len(div.children)
|
||||||
collection = pydom[".collection"]
|
collection = dom[".collection"]
|
||||||
div.append(collection)
|
div.append(collection)
|
||||||
assert len(div.children) == len_children_before + len(collection)
|
assert len(div.children) == len_children_before + len(collection)
|
||||||
|
|
||||||
@@ -132,16 +123,16 @@ class TestElement:
|
|||||||
def test_read_classes(self):
|
def test_read_classes(self):
|
||||||
id_ = "test_class_selector"
|
id_ = "test_class_selector"
|
||||||
expected_class = "a-test-class"
|
expected_class = "a-test-class"
|
||||||
div = pydom[f"#{id_}"][0]
|
div = dom[f"#{id_}"][0]
|
||||||
assert div.classes == [expected_class]
|
assert div.classes == [expected_class]
|
||||||
|
|
||||||
def test_add_remove_class(self):
|
def test_add_remove_class(self):
|
||||||
id_ = "div-no-classes"
|
id_ = "div-no-classes"
|
||||||
classname = "tester-class"
|
classname = "tester-class"
|
||||||
div = pydom[f"#{id_}"][0]
|
div = dom[f"#{id_}"][0]
|
||||||
assert not div.classes
|
assert not div.classes
|
||||||
div.add_class(classname)
|
div.add_class(classname)
|
||||||
same_div = pydom[f"#{id_}"][0]
|
same_div = dom[f"#{id_}"][0]
|
||||||
assert div.classes == [classname] == same_div.classes
|
assert div.classes == [classname] == same_div.classes
|
||||||
div.remove_class(classname)
|
div.remove_class(classname)
|
||||||
assert div.classes == [] == same_div.classes
|
assert div.classes == [] == same_div.classes
|
||||||
@@ -149,7 +140,7 @@ class TestElement:
|
|||||||
def test_when_decorator(self):
|
def test_when_decorator(self):
|
||||||
called = False
|
called = False
|
||||||
|
|
||||||
just_a_button = pydom["#a-test-button"][0]
|
just_a_button = dom["#a-test-button"][0]
|
||||||
|
|
||||||
@when("click", just_a_button)
|
@when("click", just_a_button)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -157,7 +148,7 @@ class TestElement:
|
|||||||
called = True
|
called = True
|
||||||
|
|
||||||
# Now let's simulate a click on the button (using the low level JS API)
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
# so we don't risk pydom getting in the way
|
# so we don't risk dom getting in the way
|
||||||
assert not called
|
assert not called
|
||||||
just_a_button._js.click()
|
just_a_button._js.click()
|
||||||
|
|
||||||
@@ -165,7 +156,7 @@ class TestElement:
|
|||||||
|
|
||||||
def test_html_attribute(self):
|
def test_html_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = pydom["#element_attribute_tests"][0]
|
div = dom["#element_attribute_tests"][0]
|
||||||
|
|
||||||
# WHEN we set the html attribute
|
# WHEN we set the html attribute
|
||||||
div.html = "<b>New Content</b>"
|
div.html = "<b>New Content</b>"
|
||||||
@@ -177,7 +168,7 @@ class TestElement:
|
|||||||
|
|
||||||
def test_text_attribute(self):
|
def test_text_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = pydom["#element_attribute_tests"][0]
|
div = dom["#element_attribute_tests"][0]
|
||||||
|
|
||||||
# WHEN we set the html attribute
|
# WHEN we set the html attribute
|
||||||
div.text = "<b>New Content</b>"
|
div.text = "<b>New Content</b>"
|
||||||
@@ -190,12 +181,12 @@ class TestElement:
|
|||||||
|
|
||||||
class TestCollection:
|
class TestCollection:
|
||||||
def test_iter_eq_children(self):
|
def test_iter_eq_children(self):
|
||||||
elements = pydom[".multi-elems"]
|
elements = dom[".multi-elems"]
|
||||||
assert [el for el in elements] == [el for el in elements.children]
|
assert [el for el in elements] == [el for el in elements.children]
|
||||||
assert len(elements) == 3
|
assert len(elements) == 3
|
||||||
|
|
||||||
def test_slices(self):
|
def test_slices(self):
|
||||||
elements = pydom[".multi-elems"]
|
elements = dom[".multi-elems"]
|
||||||
assert elements[0]
|
assert elements[0]
|
||||||
_slice = elements[:2]
|
_slice = elements[:2]
|
||||||
assert len(_slice) == 2
|
assert len(_slice) == 2
|
||||||
@@ -205,26 +196,26 @@ class TestCollection:
|
|||||||
|
|
||||||
def test_style_rule(self):
|
def test_style_rule(self):
|
||||||
selector = ".multi-elems"
|
selector = ".multi-elems"
|
||||||
elements = pydom[selector]
|
elements = dom[selector]
|
||||||
for el in elements:
|
for el in elements:
|
||||||
assert el.style["background-color"] != "red"
|
assert el.style["background-color"] != "red"
|
||||||
|
|
||||||
elements.style["background-color"] = "red"
|
elements.style["background-color"] = "red"
|
||||||
|
|
||||||
for i, el in enumerate(pydom[selector]):
|
for i, el in enumerate(dom[selector]):
|
||||||
assert elements[i].style["background-color"] == "red"
|
assert elements[i].style["background-color"] == "red"
|
||||||
assert el.style["background-color"] == "red"
|
assert el.style["background-color"] == "red"
|
||||||
|
|
||||||
elements.style.remove("background-color")
|
elements.style.remove("background-color")
|
||||||
|
|
||||||
for i, el in enumerate(pydom[selector]):
|
for i, el in enumerate(dom[selector]):
|
||||||
assert el.style["background-color"] != "red"
|
assert el.style["background-color"] != "red"
|
||||||
assert elements[i].style["background-color"] != "red"
|
assert elements[i].style["background-color"] != "red"
|
||||||
|
|
||||||
def test_when_decorator(self):
|
def test_when_decorator(self):
|
||||||
called = False
|
called = False
|
||||||
|
|
||||||
buttons_collection = pydom["button"]
|
buttons_collection = dom["button"]
|
||||||
|
|
||||||
@when("click", buttons_collection)
|
@when("click", buttons_collection)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -232,7 +223,7 @@ class TestCollection:
|
|||||||
called = True
|
called = True
|
||||||
|
|
||||||
# Now let's simulate a click on the button (using the low level JS API)
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
# so we don't risk pydom getting in the way
|
# so we don't risk dom getting in the way
|
||||||
assert not called
|
assert not called
|
||||||
for button in buttons_collection:
|
for button in buttons_collection:
|
||||||
button._js.click()
|
button._js.click()
|
||||||
@@ -242,32 +233,32 @@ class TestCollection:
|
|||||||
|
|
||||||
class TestCreation:
|
class TestCreation:
|
||||||
def test_create_document_element(self):
|
def test_create_document_element(self):
|
||||||
new_el = pydom.create("div")
|
# TODO: This test should probably be removed since it's testing the elements module
|
||||||
|
new_el = el.div("new element")
|
||||||
new_el.id = "new_el_id"
|
new_el.id = "new_el_id"
|
||||||
assert isinstance(new_el, pydom.BaseElement)
|
assert isinstance(new_el, el.BaseElement)
|
||||||
assert new_el._js.tagName == "DIV"
|
assert new_el._js.tagName == "DIV"
|
||||||
# EXPECT the new element to be associated with the document
|
# EXPECT the new element to be associated with the document
|
||||||
assert new_el.parent == None
|
assert new_el.parent == None
|
||||||
pydom.body.append(new_el)
|
dom.body.append(new_el)
|
||||||
|
|
||||||
assert pydom["#new_el_id"][0].parent == pydom.body
|
assert dom["#new_el_id"][0].parent == dom.body
|
||||||
|
|
||||||
def test_create_element_child(self):
|
def test_create_element_child(self):
|
||||||
selector = "#element-creation-test"
|
selector = "#element-creation-test"
|
||||||
parent_div = pydom[selector][0]
|
parent_div = dom[selector][0]
|
||||||
|
|
||||||
# Creating an element from another element automatically creates that element
|
# Creating an element from another element automatically creates that element
|
||||||
# as a child of the original element
|
# as a child of the original element
|
||||||
new_el = parent_div.create(
|
new_el = el.p("a div", classes=["code-description"], html="Ciao PyScripters!")
|
||||||
"p", classes=["code-description"], html="Ciao PyScripters!"
|
parent_div.append(new_el)
|
||||||
)
|
|
||||||
|
|
||||||
assert isinstance(new_el, pydom.BaseElement)
|
assert isinstance(new_el, el.BaseElement)
|
||||||
assert new_el._js.tagName == "P"
|
assert new_el._js.tagName == "P"
|
||||||
|
|
||||||
# EXPECT the new element to be associated with the document
|
# EXPECT the new element to be associated with the document
|
||||||
assert new_el.parent == parent_div
|
assert new_el.parent == parent_div
|
||||||
|
assert dom[selector][0].children[0] == new_el
|
||||||
assert pydom[selector][0].children[0] == new_el
|
|
||||||
|
|
||||||
|
|
||||||
class TestInput:
|
class TestInput:
|
||||||
@@ -281,7 +272,7 @@ class TestInput:
|
|||||||
def test_value(self):
|
def test_value(self):
|
||||||
for id_ in self.input_ids:
|
for id_ in self.input_ids:
|
||||||
expected_type = id_.split("_")[-1]
|
expected_type = id_.split("_")[-1]
|
||||||
result = pydom[f"#{id_}"]
|
result = dom[f"#{id_}"]
|
||||||
input_el = result[0]
|
input_el = result[0]
|
||||||
assert input_el._js.type == expected_type
|
assert input_el._js.type == expected_type
|
||||||
assert input_el.value == f"Content {id_}" == input_el._js.value
|
assert input_el.value == f"Content {id_}" == input_el._js.value
|
||||||
@@ -299,7 +290,7 @@ class TestInput:
|
|||||||
|
|
||||||
def test_set_value_collection(self):
|
def test_set_value_collection(self):
|
||||||
for id_ in self.input_ids:
|
for id_ in self.input_ids:
|
||||||
input_el = pydom[f"#{id_}"]
|
input_el = dom[f"#{id_}"]
|
||||||
|
|
||||||
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
|
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
|
||||||
|
|
||||||
@@ -308,35 +299,35 @@ class TestInput:
|
|||||||
assert input_el.value[0] == new_value == input_el[0].value
|
assert input_el.value[0] == new_value == input_el[0].value
|
||||||
|
|
||||||
def test_element_without_value(self):
|
def test_element_without_value(self):
|
||||||
result = pydom[f"#tests-terminal"][0]
|
result = dom[f"#tests-terminal"][0]
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
result.value = "some value"
|
result.value = "some value"
|
||||||
|
|
||||||
def test_element_without_collection(self):
|
def test_element_without_collection(self):
|
||||||
result = pydom[f"#tests-terminal"]
|
result = dom[f"#tests-terminal"]
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
result.value = "some value"
|
result.value = "some value"
|
||||||
|
|
||||||
def test_element_without_collection(self):
|
def test_element_without_collection(self):
|
||||||
result = pydom[f"#tests-terminal"]
|
result = dom[f"#tests-terminal"]
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
result.value = "some value"
|
result.value = "some value"
|
||||||
|
|
||||||
|
|
||||||
class TestSelect:
|
class TestSelect:
|
||||||
def test_select_options_iter(self):
|
def test_select_options_iter(self):
|
||||||
select = pydom[f"#test_select_element_w_options"][0]
|
select = dom[f"#test_select_element_w_options"][0]
|
||||||
|
|
||||||
for i, option in enumerate(select.options, 1):
|
for i, option in enumerate(select.options, 1):
|
||||||
assert option.value == f"{i}"
|
assert option.value == f"{i}"
|
||||||
assert option.html == f"Option {i}"
|
assert option.html == f"Option {i}"
|
||||||
|
|
||||||
def test_select_options_len(self):
|
def test_select_options_len(self):
|
||||||
select = pydom[f"#test_select_element_w_options"][0]
|
select = dom[f"#test_select_element_w_options"][0]
|
||||||
assert len(select.options) == 2
|
assert len(select.options) == 2
|
||||||
|
|
||||||
def test_select_options_clear(self):
|
def test_select_options_clear(self):
|
||||||
select = pydom[f"#test_select_element_to_clear"][0]
|
select = dom[f"#test_select_element_to_clear"][0]
|
||||||
assert len(select.options) == 3
|
assert len(select.options) == 3
|
||||||
|
|
||||||
select.options.clear()
|
select.options.clear()
|
||||||
@@ -345,7 +336,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_element_add(self):
|
def test_select_element_add(self):
|
||||||
# GIVEN the existing select element with no options
|
# GIVEN the existing select element with no options
|
||||||
select = pydom[f"#test_select_element"][0]
|
select = dom[f"#test_select_element"][0]
|
||||||
|
|
||||||
# EXPECT the select element to have no options
|
# EXPECT the select element to have no options
|
||||||
assert len(select.options) == 0
|
assert len(select.options) == 0
|
||||||
@@ -426,7 +417,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_options_remove(self):
|
def test_select_options_remove(self):
|
||||||
# GIVEN the existing select element with 3 options
|
# GIVEN the existing select element with 3 options
|
||||||
select = pydom[f"#test_select_element_to_remove"][0]
|
select = dom[f"#test_select_element_to_remove"][0]
|
||||||
|
|
||||||
# EXPECT the select element to have 3 options
|
# EXPECT the select element to have 3 options
|
||||||
assert len(select.options) == 4
|
assert len(select.options) == 4
|
||||||
@@ -448,7 +439,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_get_selected_option(self):
|
def test_select_get_selected_option(self):
|
||||||
# GIVEN the existing select element with one selected option
|
# GIVEN the existing select element with one selected option
|
||||||
select = pydom[f"#test_select_element_w_options"][0]
|
select = dom[f"#test_select_element_w_options"][0]
|
||||||
|
|
||||||
# WHEN we get the selected option
|
# WHEN we get the selected option
|
||||||
selected_option = select.options.selected
|
selected_option = select.options.selected
|
||||||
|
|||||||
@@ -101,10 +101,11 @@ class TestElements(PyScriptTest):
|
|||||||
code_ = f"""
|
code_ = f"""
|
||||||
from pyscript import when
|
from pyscript import when
|
||||||
<script type="{interpreter}">
|
<script type="{interpreter}">
|
||||||
from pyweb import pydom
|
from pyscript.web import dom
|
||||||
from pyweb.ui.elements import {el_type}
|
from pyscript.web.elements import {el_type}
|
||||||
|
|
||||||
el = {el_type}({attributes})
|
el = {el_type}({attributes})
|
||||||
pydom.body.append(el)
|
dom.body.append(el)
|
||||||
</script>
|
</script>
|
||||||
"""
|
"""
|
||||||
self.pyscript_run(code_)
|
self.pyscript_run(code_)
|
||||||
|
|||||||
16
pyscript.core/types/stdlib/pyscript.d.ts
vendored
16
pyscript.core/types/stdlib/pyscript.d.ts
vendored
@@ -7,16 +7,14 @@ declare namespace _default {
|
|||||||
"ffi.py": string;
|
"ffi.py": string;
|
||||||
"magic_js.py": string;
|
"magic_js.py": string;
|
||||||
"util.py": string;
|
"util.py": string;
|
||||||
|
web: {
|
||||||
|
"__init__.py": string;
|
||||||
|
"dom.py": string;
|
||||||
|
"elements.py": string;
|
||||||
|
"media.py": string;
|
||||||
|
};
|
||||||
"websocket.py": string;
|
"websocket.py": string;
|
||||||
};
|
};
|
||||||
let pyweb: {
|
let pyweb: {};
|
||||||
"__init__.py": string;
|
|
||||||
"media.py": string;
|
|
||||||
"pydom.py": string;
|
|
||||||
ui: {
|
|
||||||
"__init__.py": string;
|
|
||||||
"elements.py": string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
Reference in New Issue
Block a user