"""
A module that creates HTML elements.
"""
from bs4 import BeautifulSoup
from bs4.element import Tag
from copy import copy
import tinycss
from toui._signals import Signal
from toui._helpers import warn, debug, selector_to_str, obj_converter
class _ElementSignal(Signal):
"""
Creates signals that will be sent to JavaScript.
These signals are related the methods of the `Element` object.
"""
@staticmethod
def _default_kwargs(kwargs):
original_copy = kwargs['original_copy']
return {"selector": original_copy._selector}
@staticmethod
def from_str(**kwargs):
js_func = "_replaceElement"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['element'] = kwargs['element_as_str']
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def from_bs4_tag(**kwargs):
js_func = "_replaceElement"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['element'] = str(kwargs['bs4_tag'])
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def set_attr(**kwargs):
js_func = "_setAttr"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['name'] = kwargs['name']
js_kwargs['value'] = str(kwargs['value'])
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def del_attr(**kwargs):
js_func = "_delAttr"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['name'] = kwargs['name']
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def get_files(**kwargs):
js_func = "_getFiles"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['with_content'] = kwargs['with_content']
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def set_content(**kwargs):
js_func = "_setContent"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['content'] = str(kwargs['content'])
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
@staticmethod
def add_content(**kwargs):
js_func = "_addContent"
js_args = []
js_kwargs = _ElementSignal._default_kwargs(kwargs)
js_kwargs['content'] = str(kwargs['content'])
return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}
[docs]class Element:
"""
Creates an HTML element.
Examples
--------
Creating a ``<button>`` HTML element:
>>> button = Element("button")
>>> button
<button></button>
Setting the inner HTML content of the element:
>>> button.set_content("Click me")
>>> button
<button>Click me</button>
Setting the element's attributes:
>>> button.set_attr("name", "button-name")
>>> button
<button name="button-name">Click me</button>
"""
def __init__(self, tag_name="div"):
self._element = Tag(name=tag_name)
self._parent_page = None
self._parent_element =None
self._selector = {}
self._signal_mode = False
self._functions = {}
def __str__(self):
return self.to_str()
def __repr__(self):
return self.to_str()
@property
def _app(self):
return self._parent_page._app
[docs] @_ElementSignal()
def from_str(self, html_str):
"""
Converts HTML code to an `Element` object.
Parameters
----------
html_str: str
"""
soup = BeautifulSoup(html_str, features="html.parser")
tags = soup.find_all()
if len(tags) > 0:
tag = tags[0]
self._element = tag
else:
warn("No element found in string")
[docs] def to_str(self):
"""
Converts the `Element` object to HTML code.
Returns
-------
str
"""
return str(self._element)
[docs] @_ElementSignal()
def from_bs4_tag(self, bs4_tag):
"""
Converts a `bs4.element.Tag` object to an `Element` object.
Parameters
----------
bs4_tag: bs4.element.Tag
See Also
--------
bs4
"""
self._element = copy(bs4_tag)
[docs] def to_bs4_tag(self):
"""
Converts the `Element` object to a `bs4.element.Tag` object.
Returns
-------
bs4.element.Tag
See Also
--------
bs4
"""
return copy(self._element)
[docs] def get_element(self, element_id, do_copy=False):
"""
Gets a child element from its ``id`` attribute.
Parameters
----------
element_id: str
do_copy: bool, default = False
If ``True``, the element will be copied.
Returns
-------
element: Element
If the element was found, an `Element` object will be returned.
None
If the element was not found.
"""
bs4_tag = self._element.find(id=element_id)
element = Element()
if do_copy:
element.from_bs4_tag(bs4_tag)
else:
element._from_bs4_tag_no_copy(bs4_tag)
element._signal_mode = self._signal_mode
element._parent_page = self._parent_page
element._parent_element = self
element._selector = {"selector": f"[id={element_id}]",
"parent": self._selector}
return element
[docs] def get_elements(self, tag_name=None, class_name=None, name=None, do_copy=False, attrs=None):
"""
Get children elements by their tag name and attributes.
Parameters
----------
tag_name: str, default=None
The tag name of the elements.
class_name: str, default=None
The value of the ``class`` attribute of the elements.
name: str, default=None
The value of the ``name`` attribute of the elements.
do_copy: bool
If ``True``, the elements will be copied.
attrs: dict, default=None
Attributes other than ``class`` and ``name`` can be specified in this dictionary.
Returns
-------
elements_list: list(Element)
A list of `Element` objects that match the parameters.
"""
if attrs is None:
attrs = {}
if class_name:
attrs['class'] = class_name
if name:
attrs['name'] = name
bs4_tags = self._element.find_all(name=tag_name, attrs=attrs)
elements_list = []
for tag_num, bs4_tag in enumerate(bs4_tags):
element = Element()
if do_copy:
element.from_bs4_tag(bs4_tag)
else:
element._from_bs4_tag_no_copy(bs4_tag)
element._signal_mode = self._signal_mode
element._parent_page = self._parent_page
element._parent_element = self
element._selector = {"selector": selector_to_str(tag_name=tag_name, class_name=class_name,
name=name, attrs=attrs),
"number": tag_num,
"parent": self._selector}
elements_list.append(element)
return elements_list
[docs] def get_attr(self, name):
"""
Gets the value of an HTML element attribute.
Parameters
----------
name: str
The name of the attribute.
Returns
-------
str
If the attribute exists.
None
If the attribute does not exist.
"""
return self._element.attrs.get(name)
[docs] @_ElementSignal()
def set_attr(self, name, value):
"""
Sets the value of an HTML element attribute.
Parameters
----------
name: str
The name of the attribute.
value
The new value of the attribute.
"""
value = str(value)
self._element.attrs[name] = value
[docs] def has_attr(self, name):
"""
Checks if the HTML element has the specified attribute.
Parameters
----------
name: str
The name of the attribute.
Returns
-------
bool
"""
return self.get_attr(name) != None
[docs] @_ElementSignal()
def del_attr(self, name):
"""
Removes an HTML element attribute.
Parameters
----------
name: str
The name of the attribute.
"""
if self.has_attr(name):
del self._element.attrs[name]
[docs] def get_id(self):
"""
Gets the ``id`` attribute of the HTML element.
Returns
-------
str
If the attribute exists.
None
If the attribute does not exist.
"""
return self.get_attr("id")
[docs] def set_id(self, value):
"""
Sets the value of the ``id`` attribute.
Parameters
----------
value
The new value of the ``id`` attribute.
"""
self.set_attr("id", value)
[docs] def get_value(self):
"""
Gets the ``value`` attribute of the HTML element.
Returns
-------
str
If the attribute exists.
None
If the attribute does not exist.
"""
return self.get_attr("value")
[docs] def set_value(self, value):
"""
Sets the value of the ``value`` attribute.
Parameters
----------
value
The new value of the ``value`` attribute.
"""
self.set_attr("value", value)
[docs] @_ElementSignal(return_type="js")
def get_files(self, with_content=False):
"""
Gets uploaded files from element.
This method is useful when uploading files using ``<input type="file">`` element.
Parameters
----------
with_content: bool, default=False
If ``True``, the contents of the files will be included in the output.
Returns
-------
list(File)
A list of `File` objects.
See Also
--------
~toui._signals.File
"""
return []
[docs] def get_content(self):
"""
Gets the inner HTML content of the element.
It is similar to getting the ``Element.innerHTML`` property in JavaScript.
Returns
-------
str
"""
return self._element.decode_contents()
[docs] @_ElementSignal()
def set_content(self, content):
"""
Sets the inner HTML content of the element.
Parameters
----------
content
The new inner HTML content.
"""
self._element.clear()
self._manage_content_functions(content)
content = str(content)
content = BeautifulSoup(content, features="html.parser")
self._element.append(content)
[docs] @_ElementSignal()
def add_content(self, content):
"""
Adds to the inner HTML content of the element.
Parameters
----------
content
The added inner HTML content.
"""
self._manage_content_functions(content)
content = str(content)
content = BeautifulSoup(content, features="html.parser")
self._element.append(content)
[docs] def get_style_property(self, property):
"""
Gets the value of a CSS property inside the ``style`` attribute.
Parameters
----------
property: str
The name of the property
Returns
-------
str
"""
if not self.has_attr("style"):
return
style = self.get_attr('style')
parser = tinycss.make_parser("page3")
declarations = parser.parse_style_attr(style)[0]
for declaration in declarations:
if declaration.name == property:
property_value = ""
for v in declaration.value:
property_value += str(v.value)
if v.unit is not None:
property_value += str(v.unit)
return property_value
[docs] def set_style_property(self, property, value):
"""
Sets the value of a CSS property inside the ``style`` attribute.
Parameters
----------
property: str
The name of the property
value: str
The new value of the property.
"""
if self.has_attr("style"):
style = self.get_attr('style')
else:
style = ""
parser = tinycss.make_parser("page3")
declarations = parser.parse_style_attr(style)[0]
property_is_set = False
new_style = ""
for declaration in declarations:
new_style += declaration.name + ": "
if declaration.name == property:
property_value = value
property_is_set = True
else:
property_value = ""
for v in declaration.value:
property_value += str(v.value) + str(v.unit)
new_style += f"{property_value};"
if not property_is_set:
new_style += f"{property}: {value};"
self.set_attr(name="style", value=new_style)
[docs] def get_width_property(self):
"""
Gets the value of the CSS property `width` inside the ``style`` attribute.
Returns
-------
str
"""
return self.get_style_property("width")
[docs] def set_width_property(self, value):
"""
Sets the value of the CSS property `width` inside the ``style`` attribute.
Parameters
----------
value: str
"""
self.set_style_property("width", value)
[docs] def get_height_property(self):
"""
Gets the value of the CSS property `height` inside the ``style`` attribute.
Returns
-------
str
"""
return self.get_style_property("height")
[docs] def set_height_property(self, value):
"""
Sets the value of the CSS property `height` inside the ``style`` attribute.
Parameters
----------
value: str
"""
self.set_style_property("height", value)
def _manage_content_functions(self, content):
if type(content) is Element:
for func in content._functions.values():
if self._parent_page:
self._parent_page.add_function(func)
else:
self._functions[func.__name__] = func
[docs] def on(self, event, func_or_name, *func_args, quotes=True):
"""
Creates an HTML event attribute and adds a Python function to it.
If you want to add JavaScript code instead of a single function, use `Element.set_attr` method
instead.
Parameters
----------
event: str
Can be any HTML event, but without the "on" prefix.
For example: `click`, `load`, `mouseover`, etc.
func_or_name: Callable or str
The Python function to be called when the event is triggered. If the Python function
itself is added, the function will be automatically added to the parent `Page`. However,
if the function name is added, you need to add the Python function itself to the
parent `Page` manually using the method `Page.add_function`.
The `func_or_name` parameter can also be a JavaScript function. In this case, there is
no need to use the method `Page.add_function`.
func_args
The arguments of the function. Each argument will be automatically converted to a
string.
quotes: bool, default = True
If ``True``, each argument will be surrounded by double quotes.
Examples
--------
Adding a function to a button:
>>> def printValueType(value):
... value_type = type(value)
... print(value_type)
>>> button = Element("button")
>>> button.on("click", printValue, 10)
This function prints the type of the first argument.
If the button was clicked, the output will be:
>>> #<class 'str'>
The value ``10`` was converted to a string because the parameter `quotes` was ``True``.
However, if we change the `quotes` to ``False``:
>>> def printValueType(value):
... value_type = type(value)
... print(value_type)
>>> button = Element("button")
>>> button.on("click", printValue, 10, quotes=False)
The output will be:
>>> #<class 'int'>
"""
if quotes:
args = ",".join([f'"{arg}"' for arg in func_args])
else:
args = ",".join([f'{obj_converter(arg)}' for arg in func_args])
if callable(func_or_name):
name = func_or_name.__name__
if self._parent_page:
self._parent_page.add_function(func_or_name)
else:
self._functions[func_or_name.__name__] = func_or_name
else:
name = func_or_name
value = f"{name}({args})"
self.set_attr(name=f"on{event}", value=value)
[docs] def onclick(self, func_or_name, *func_args, quotes=True):
"""
Creates the HTML event attribute ``onclick`` and adds a Python function to it.
If you want to add JavaScript code instead of a single function, use `Element.set_attr` method
instead.
Parameters
----------
func_or_name: Callable or str
The Python function to be called when the event is triggered. If the Python function
itself is added, the function will be automatically added to the parent `Page`. However,
if the function name is added, you need to add the Python function itself to the
parent `Page` manually using the method `Page.add_function`.
The `func_or_name` parameter can also be a JavaScript function. In this case, there is
no need to use the method `Page.add_function`.
func_args
The arguments of the function. Each argument will be automatically converted to a
string.
quotes: bool, default = True
If ``True``, each argument will be surrounded by double quotes.
See Also
--------
Element.on
"""
self.on('click', func_or_name, *func_args, quotes=quotes)
def _from_bs4_tag_no_copy(self, bs4_tag):
self._element = bs4_tag
[docs]class IFrameElement(Element):
"""
An ``<iframe>`` element edited to fit the content within it.
"""
def __init__(self, src=None, borderless=True):
"""
Parameters
----------
src: str
The ``src`` attribute of the element.
borderless: bool, default = True
If ``True``, the border will be removed from the element.
"""
super().__init__(tag_name="iframe")
if borderless:
self.set_style_property("border", "none")
self.set_attr("marginwidth", "0")
self.set_attr("marginheight", "0")
self.set_attr("align", "center")
self.on('load', '_resizeEmbed', "this", quotes=False)
if src:
self.set_attr("src", src)
if __name__ == "__main__":
import doctest
results = doctest.testmod()
print(results)