Source code for toui.elements

"""
A module that creates HTML elements.
"""
from bs4 import BeautifulSoup
from bs4.element import Tag
from copy import copy
from typing import List
import tinycss
from toui._signals import Signal, File
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.get_unique_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._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: new = tags[0] else: return if self._element.parent is None: self._element = tags[0] else: self._element.replace_with(new)
[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 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 elements_list.append(element) return elements_list
[docs] def get_parent(self, do_copy=False) -> 'Element': """ Gets the parent element. Parameters ---------- do_copy: bool, default = False If ``True``, the element will be copied. Returns ------- element: Element If a parent was found, an `Element` object will be returned. None If a parent was not found. """ bs4_tag = self._element.parent if not isinstance(bs4_tag, Tag): return None 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 return element
[docs] def get_selector(self) -> str: """ Gets the CSS selector of an element. Returns ------- str None: If the element is not part of a page. """ selector = self._element.name + ''.join([f'[{attr}="{value}"]' for attr, value in self._element.attrs.items()]) return selector
[docs] def get_unique_selector(self): """ Gets the unique CSS selector of an element. Returns ------- str None: If the element is not part of a page. """ element = self._element path = [] while element is not None and element.name != "[document]": selector = element.name.lower() if element.get('id'): selector += '#' + element['id'] path.insert(0, selector) break else: index = [child for child in element.parent.children if child.name == element.name].index(element) + 1 if index != 1: selector += ":nth-of-type(" + str(index) + ")" path.insert(0, selector) element = element.parent return ' > '.join(path)
[docs] def get_attr(self, name, default=None): """ Gets the value of an HTML element attribute. Parameters ---------- name: str The name of the attribute. default: Any, default=None The value to return if the attribute does not exist. Returns ------- str If the attribute exists. None, Any ``None`` will be returned the attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ value = self._element.attrs.get(name) if type(value) == list: value = ' '.join(value) if value is None: value = default return value
[docs] def get_class_list(self): """ Gets all classes of the HTML element as a list. """ values = self._element.attrs.get("class") if values is None: return [] return values
[docs] def remove_from_class_list(self, cls): """ Removes all occurences of the specified class from the HTML element. """ values = self._element.attrs.get("class") if values is None: return values = [v for v in values if v != cls] self.set_attr("class", " ".join(values))
[docs] def add_to_class_list(self, cls): """ Adds the specified class to the HTML element if it does not exist. """ values = self._element.attrs.get("class") if values is None: self.set_attr("class", cls) return if not cls in values: values.append(cls) self.set_attr("class", " ".join(values))
[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, default=None): """ Gets the ``id`` attribute of the HTML element. Parameters ---------- default: Any, default=None The value to return if the attribute does not exist. Returns ------- str If the attribute exists. None, Any Retuns ``None`` if the attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ return self.get_attr("id", default=default)
[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, default=None): """ Gets the ``value`` attribute of the HTML element. Parameters ---------- default: Any, default=None The value to return if the attribute does not exist. Returns ------- str If the attribute exists. None, Any Retuns ``None`` if the attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ return self.get_attr("value", default=default)
[docs] def get_selected(self) -> 'Element': """ Gets the selected option of the HTML element ``<select>``. This method is used for ``<select>`` elements only. Returns ------- Element If it has a selected option. None If it does not have a selected option. """ for element in self.get_elements(): if element.has_attr("selected"): return element
[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) -> List[File]: """ Gets uploaded files from element. This method is useful when uploading files using ``<input type="file">`` element. Note ---- Remember to validate the names, types, and sizes of the uploaded files before using them. Parameters ---------- with_content: bool, default=False If ``True``, the contents of the files will be included in the output. It is recommended to keep this parameter as ``False`` and use :py:meth:`File.save() <toui._signals.File.save>` instead after validating the files. 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) soap = BeautifulSoup(str(content), features="html.parser") self._element.append(soap) if isinstance(content, Element): content._signal_mode = self._signal_mode content._parent_page = self._parent_page
[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) soap = BeautifulSoup(str(content), features="html.parser") self._element.append(soap) if isinstance(content, Element): content._signal_mode = self._signal_mode content._parent_page = self._parent_page
[docs] def get_style_property(self, property, default=None): """ Gets the value of a CSS property inside the ``style`` attribute. Parameters ---------- property: str The name of the property default: Any, default=None The value to return if the property does not exist or the ``style`` attribute does not exist. Returns ------- str If the property exists. None, Any Retuns ``None`` if the property does not exist or the ``style`` attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ if not self.has_attr("style"): return default 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 += v.as_css() return property_value return default
[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 += v.as_css() 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, default=None): """ Gets the value of the CSS property `width` inside the ``style`` attribute. Parameters ---------- default: Any, default=None The value to return if the property does not exist or the ``style`` attribute does not exist. Returns ------- str If the property exists. None, Any Retuns ``None`` if the property does not exist or the ``style`` attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ return self.get_style_property("width", default=default)
[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, default=None): """ Gets the value of the CSS property `height` inside the ``style`` attribute. Parameters ---------- default: Any, default=None The value to return if the property does not exist or the ``style`` attribute does not exist. Returns ------- str If the property exists. None, Any Retuns ``None`` if the property does not exist or the ``style`` attribute does not exist. However, if a `default` value is specified, it will be returned instead. """ return self.get_style_property("height", default=default)
[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, return_itself=False): """ 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. Warning ------- If you added a Python function, users might be able to call this function from the client-side. Choose wisely the functions you add. 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. return_itsef: bool, default=False If ``True``, the first argument of the function will be the element itself. 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 return_itself: args = "this, " + 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, return_itself=False): """ 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. Warning ------- If you added a Python function, users might be able to call this function from the client-side. Choose wisely the functions you add. 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. return_itsef: bool, default=False If ``True``, the first argument of the function will be the element itself. See Also -------- Element.on """ self.on('click', func_or_name, *func_args, quotes=quotes, return_itself=return_itself)
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)