mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
* add pyweb * build * add test file * fix pydom example code * remove old reference to js * temporarily comment out query functions on BaseElement while rearranging code to reuse the same underlying logic accross PyDom and other elements * add temp TODO comment to content as it breaks with template elements * update pydom example to define code on external file * fix name type while renaming document -> dom * add real pydom test files * add classes to dom scope * __len__ to ElementCollection * fix some of the old tests * rename test from test_query_by_class to test_getitem_by_class * change test for read and write multiple elements * add find method to BaseElement * fix remaining tests * add Collection Tests * add equality to Collection * add test for collection style manipulation * fix getter for style property and rename style related attribute from pop to remove * add single element creation test * remove append on BaseElement and add body and head to dom * add test_create_element_child to verify child creation * add children getter property to Element * remove old code * remove more old code, change style attribute from visibility to visible and now default getters on collection to return a list with the value of an attribute for every element in the collection * remove more old code and add possibility to customize test flags via url * add support to pass Js and pydom.Element elements to when decorator * remove methods related to input type of elements until we have a better design for it * rename _element to _js * add test_when decorator with a ElementCollection input * when decorator now supporte pydom.ElementCollection as input * update pyscript.js * remove useless variable from when decorator * remove base.py from pyweb * add nodes for append collection test and add better feedback on successes vs failure * add tests and fix code for support of append Element and ElementCollection * manage access to content attribute when tagname is template * fix comment --------- Co-authored-by: Fabio Pliger <fpliger@anaconda.com>
315 lines
8.8 KiB
Python
315 lines
8.8 KiB
Python
import sys
|
|
import warnings
|
|
from functools import cached_property
|
|
from typing import Any
|
|
|
|
from pyodide.ffi import JsProxy
|
|
from pyscript import display, document, window
|
|
|
|
# from pyscript import when as _when
|
|
|
|
alert = window.alert
|
|
|
|
|
|
class BaseElement:
|
|
def __init__(self, js_element):
|
|
self._js = js_element
|
|
self._parent = None
|
|
self.style = StyleProxy(self)
|
|
|
|
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 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 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
|
|
|
|
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 when(self, event, handler):
|
|
document.when(event, selector=self)(handler)
|
|
|
|
|
|
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 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) -> Any:
|
|
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):
|
|
super().__init__(document)
|
|
self.ids = DomScope()
|
|
self.body = Element(document.body)
|
|
self.head = Element(document.head)
|
|
|
|
def create(self, type_, parent=None, classes=None, html=None):
|
|
return super().create(type_, is_child=False)
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, int):
|
|
indices = range(*key.indices(len(self.list)))
|
|
return [self.list[i] for i in indices]
|
|
|
|
elements = self._js.querySelectorAll(key)
|
|
if not elements:
|
|
return None
|
|
return ElementCollection([Element(el) for el in elements])
|
|
|
|
|
|
dom = PyDom()
|
|
|
|
sys.modules[__name__] = dom
|