Source code for wdom.web_node

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Base classes for web-synchronized Nodes."""

import logging
import re
from typing import Any, Awaitable, Dict, Tuple, Union
from typing import TYPE_CHECKING
import warnings
from weakref import WeakValueDictionary

from wdom import server
from wdom.event import create_event, WebEventTarget
from wdom.element import _AttrValueType, HTMLElement, ElementParser
from wdom.element import ElementMeta, DOMTokenList
from wdom.node import Node, CharacterData

if TYPE_CHECKING:
    from typing import Type  # noqa

logger = logging.getLogger(__name__)
_remove_id_re = re.compile(r' wdom_id="\d+"')
_WdomIdType = Union[int, str]


def remove_wdom_id(html: str) -> str:
    """Remove ``wdom_id`` attribute from html strings."""
    return _remove_id_re.sub('', html)


class WdomElementParser(ElementParser):
    """Parser class which generates WdomElement nodes."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: D102
        super().__init__(*args, **kwargs)
        self.default_class = WdomElement


class WdomElementMeta(ElementMeta):
    """Meta class to set default class variable of HTMLElement."""

    @classmethod
    def __prepare__(metacls, name: str, bases: Tuple[type], **kwargs: Any
                    ) -> Dict[str, bool]:
        return {'inherit_class': True}


[docs]class WdomElement(HTMLElement, WebEventTarget, metaclass=WdomElementMeta): """WdomElement class. This class provides main features to synchronously control browser DOM node. Additionally, this class provides shortcut properties to handle class attributes. """ _elements_with_wdom_id = WeakValueDictionary( ) # type: WeakValueDictionary[_WdomIdType, WdomElement] _parser_class = WdomElementParser # type: Type[ElementParser] #: str and list of strs are acceptale. class_ = '' #: Inherit classes defined in super class or not. #: By default, this variable is True. inherit_class = True @property def wdom_id(self) -> str: """Get wdom_id attribute. This attribute is used to relate python node and browser DOM node. """ return self.__wdom_id @property def rimo_id(self) -> str: """[Deprecated] Alias to `wdom_id`. rimo_id is renamed to `wdom_id`. """ warnings.warn('rimo_id is renamed to wdom_id.', DeprecationWarning) return self.wdom_id @property def connected(self) -> bool: """When this instance has any connection, return True.""" return bool(server.is_connected() and self.ownerDocument) def __init__(self, *args: Any, parent: 'WdomElement' = None, wdom_id: _WdomIdType = None, **kwargs: Any) -> None: # noqa: D102 if wdom_id is None: self.__wdom_id = str(id(self)) else: self.__wdom_id = str(wdom_id) super().__init__(*args, **kwargs) # use super class to set wdom_id self._elements_with_wdom_id[self.wdom_id] = self self.addEventListener('mount', self._on_mount) if parent: parent.appendChild(self) def _clone_node(self) -> HTMLElement: clone = super()._clone_node() for c in self.classList: clone.addClass(c) return clone # Hanlde attributes def _get_attrs_by_string(self) -> str: res = 'wdom_id="{}"'.format(self.wdom_id) attrs = super()._get_attrs_by_string() if attrs: return ' '.join([res, attrs]) return res def _set_attribute(self, attr: str, value: _AttrValueType) -> None: if attr == 'wdom_id': raise ValueError('Cannot change wdom_id') super()._set_attribute(attr, value) def __getitem__(self, attr: Union[str, int] ) -> Union[Node, _AttrValueType]: """Get/Set/Remove access by subscription (node['attr']).""" if isinstance(attr, int): return self.childNodes[attr] return self.getAttribute(attr) def __setitem__(self, attr: str, val: _AttrValueType) -> None: self.setAttribute(attr, val) def __delitem__(self, attr: str) -> None: self.removeAttribute(attr)
[docs] @classmethod def get_class_list(cls) -> DOMTokenList: """Get class-level class list, including all super class's.""" cl = [] cl.append(DOMTokenList(cls, cls.class_)) if cls.inherit_class: for base_cls in cls.__bases__: if issubclass(base_cls, WdomElement): cl.append(base_cls.get_class_list()) # Reverse order so that parent's class comes to front <- why? cl.reverse() return DOMTokenList(cls, *cl)
def getAttribute(self, attr: str) -> _AttrValueType: # noqa: D102 if attr == 'class': cls = self.get_class_list() cls._append(self.classList) return cls.toString() if cls else None return super().getAttribute(attr)
[docs] def addClass(self, *classes: str) -> None: """[Not Standard] Add classes to this node.""" self.classList.add(*classes)
[docs] def hasClass(self, class_: str) -> bool: # noqa: D102 """[Not Standard] Return if this node has ``class_`` class or not.""" return class_ in self.classList
[docs] def hasClasses(self) -> bool: # noqa: D102 """[Not Standard] Return if this node has any classes or not.""" return len(self.classList) > 0
[docs] def removeClass(self, *classes: str) -> None: """[Not Standard] Remove classes from this node.""" _remove_cl = [] for class_ in classes: if class_ not in self.classList: if class_ in self.get_class_list(): logger.warning( 'tried to remove class-level class: ' '{}'.format(class_) ) else: logger.warning( 'tried to remove non-existing class: {}'.format(class_) ) else: _remove_cl.append(class_) self.classList.remove(*_remove_cl)
# Handle child nodes def _remove_web(self) -> None: self.js_exec('remove')
[docs] def remove(self) -> None: """Remove this node from parent's DOM tree.""" if self.connected: self._remove_web() self._remove()
def _empty_web(self) -> None: self.js_exec('empty')
[docs] def empty(self) -> None: """Remove all child nodes from this node.""" if self.connected: self._empty_web() self._empty()
def _get_child_html(self, child: Node) -> str: if isinstance(child, CharacterData): # temparary become new parent # text node needs to know its parent to escape or not its content self._append_child(child) html = child.html self._remove_child(child) else: html = getattr(child, 'html', str(child)) return html def _append_child_web(self, child: 'WdomElement') -> Node: html = self._get_child_html(child) self.js_exec('insertAdjacentHTML', 'beforeend', html) return child
[docs] def appendChild(self, child: 'WdomElement') -> Node: """Append child node at the last of child nodes. If this instance is connected to the node on browser, the child node is also added to it. """ if self.connected: self._append_child_web(child) return self._append_child(child)
def _insert_before_web(self, child: Node, ref_node: Node) -> Node: html = self._get_child_html(child) if isinstance(ref_node, WdomElement): ref_node.js_exec('insertAdjacentHTML', 'beforebegin', html) else: index = self.index(ref_node) self.js_exec('insert', index, html) return child
[docs] def insertBefore(self, child: Node, ref_node: Node) -> Node: """Insert new child node before the reference child node. If the reference node is not a child of this node, raise ValueError. If this instance is connected to the node on browser, the child node is also added to it. """ if self.connected: self._insert_before_web(child, ref_node) return self._insert_before(child, ref_node)
def _remove_child_web(self, child: Node) -> Node: if child in self.childNodes: if isinstance(child, WdomElement): self.js_exec('removeChildById', child.wdom_id) else: self.js_exec('removeChildByIndex', self.index(child)) return child
[docs] def removeChild(self, child: Node) -> Node: """Remove the child node from this node. If the node is not a child of this node, raise ValueError. """ if self.connected: self._remove_child_web(child) return self._remove_child(child)
def _replace_child_web(self, new_child: Node, old_child: Node) -> None: html = self._get_child_html(new_child) if isinstance(old_child, WdomElement): self.js_exec('replaceChildById', html, old_child.wdom_id) elif old_child.parentNode is not None: # old_child will be Text Node index = old_child.parentNode.index(old_child) # Remove old_child before insert new child self._remove_child_web(old_child) self.js_exec('insert', index, html)
[docs] def replaceChild(self, new_child: 'WdomElement', old_child: 'WdomElement' ) -> Node: """Replace child nodes.""" if self.connected: self._replace_child_web(new_child, old_child) return self._replace_child(new_child, old_child)
[docs] async def getBoundingClientRect(self) -> None: """Get size of this node on browser.""" fut = await self.js_query('getBoundingClientRect') return fut
def _set_text_content_web(self, text: str) -> None: self.js_exec('textContent', self.textContent) @HTMLElement.textContent.setter # type: ignore def textContent(self, text: str) -> None: # type: ignore """Set textContent both on this node and related browser node.""" self._set_text_content(text) if self.connected: self._set_text_content_web(text) def _set_inner_html_web(self, html: str) -> None: self.js_exec('innerHTML', html) @HTMLElement.innerHTML.setter # type: ignore def innerHTML(self, html: str) -> None: # type: ignore """Set innerHTML both on this node and related browser node.""" df = self._parse_html(html) if self.connected: self._set_inner_html_web(df.html) self._empty() self._append_child(df) @property def html_noid(self) -> str: """Get html representation of this node without wdom_id.""" return remove_wdom_id(self.html)
[docs] def click(self) -> None: """Send click event.""" if self.connected: self.js_exec('click') else: # Web上に表示されてれば勝手にブラウザ側からクリックイベント発生する # のでローカルのクリックイベント不要 msg = {'proto': '', 'type': 'click', 'currentTarget': {'id': self.wdom_id}, 'target': {'id': self.wdom_id}} e = create_event(msg) self._dispatch_event(e)
[docs] def exec(self, script: str) -> None: """Execute JavaScript on the related browser node.""" self.js_exec('eval', script)
# Window controll def scroll(self, x: int, y: int) -> None: # noqa: D102 self.js_exec('scroll', x, y) def scrollTo(self, x: int, y: int) -> None: # noqa: D102 self.js_exec('scrollTo', x, y) def scrollBy(self, x: int, y: int) -> None: # noqa: D102 self.js_exec('scrollBy', x, y) def scrollX(self) -> Awaitable: # noqa: D102 return self.js_query('scrollX') def scrollY(self) -> Awaitable: # noqa: D102 return self.js_query('scrollY')
[docs] def show(self) -> None: """[Not Standard] Show this node on browser.""" self.hidden = False
[docs] def hide(self) -> None: """[Not Standard] Hide this node on browser.""" self.hidden = True