Просмотр исходного кода

added typedefs for events, and improved formatting per suggestions.

Steve Jackson 2 лет назад
Родитель
Сommit
d570f8b30f
2 измененных файлов с 143 добавлено и 70 удалено
  1. 110 68
      nicegui/element.py
  2. 33 2
      nicegui/typedefs.py

+ 110 - 68
nicegui/element.py

@@ -11,22 +11,23 @@ from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .event_listener import EventListener
 from .events import handle_event
 from .events import handle_event
 from .slot import Slot
 from .slot import Slot
-from .typedefs import ElementAsDict
+from .typedefs import ElementAsDict, AnyHTMLEvent
+from typing_extensions import Self
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .client import Client
     from .client import Client
 
 
 
 
-PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
+PROPS_PATTERN = re.compile(
+    r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)'
+)
 
 
 
 
-# noinspection PySingleQuotedDocstring
 class Element(ABC, Visibility):
 class Element(ABC, Visibility):
-    '''
-    Define the base class for all html elements represented as Python objects.
+    """Define the base class for all HTML elements represented as Python objects.
 
 
-    :param tag: The html tag (e.g. div, p, etc. of the element.)
-    '''
+    :param tag: The HTML tag (e.g. div, p, etc. of the element.)
+    """
 
 
     def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
     def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
         super().__init__()
         super().__init__()
@@ -38,9 +39,9 @@ class Element(ABC, Visibility):
         self._style: Dict[str, str] = {}
         self._style: Dict[str, str] = {}
         self._props: Dict[str, Any] = {}
         self._props: Dict[str, Any] = {}
         self._event_listeners: List[EventListener] = []
         self._event_listeners: List[EventListener] = []
-        self._text: str = ''
+        self._text: str = ""
         self.slots: Dict[str, Slot] = {}
         self.slots: Dict[str, Slot] = {}
-        self.default_slot = self.add_slot('default')
+        self.default_slot = self.add_slot("default")
 
 
         self.client.elements[self.id] = self
         self.client.elements[self.id] = self
         self.parent_slot: Optional[Slot] = None
         self.parent_slot: Optional[Slot] = None
@@ -53,8 +54,8 @@ class Element(ABC, Visibility):
         self.slots[name] = Slot(self, name)
         self.slots[name] = Slot(self, name)
         return self.slots[name]
         return self.slots[name]
 
 
-    def __enter__(self) -> 'Element':
-        '''Allow element to be used as a context manager (with statement.)'''
+    def __enter__(self) -> Self:
+        """Allow the element to be used as a context manager (with statement.)"""
         self.default_slot.__enter__()
         self.default_slot.__enter__()
         return self
         return self
 
 
@@ -62,38 +63,50 @@ class Element(ABC, Visibility):
         self.default_slot.__exit__(*_)
         self.default_slot.__exit__(*_)
 
 
     def to_dict(self) -> ElementAsDict:
     def to_dict(self) -> ElementAsDict:
-        '''Get important attributes of an element as a dictionary.'''
+        """Get important attributes of an element as a dictionary."""
         events: Dict[str, Dict] = {}
         events: Dict[str, Dict] = {}
         for listener in self._event_listeners:
         for listener in self._event_listeners:
-            words = listener.type.split('.')
+            words = listener.type.split(".")
             type = words.pop(0)
             type = words.pop(0)
-            specials = [w for w in words if w in {'capture', 'once', 'passive'}]
-            modifiers = [w for w in words if w in {'stop', 'prevent', 'self', 'ctrl', 'shift', 'alt', 'meta'}]
+            specials = [w for w in words if w in {"capture", "once", "passive"}]
+            modifiers = [w for w in words if w in {"stop", "prevent", "self", "ctrl", "shift", "alt", "meta"}]
             keys = [w for w in words if w not in specials + modifiers]
             keys = [w for w in words if w not in specials + modifiers]
             events[listener.type] = {
             events[listener.type] = {
-                'listener_type': listener.type,
-                'type': type,
-                'specials': specials,
-                'modifiers': modifiers,
-                'keys': keys,
-                'args': list(set(events.get(listener.type, {}).get('args', []) + listener.args)),
-                'throttle': min(events.get(listener.type, {}).get('throttle', float('inf')), listener.throttle),
+                "listener_type": listener.type,
+                "type": type,
+                "specials": specials,
+                "modifiers": modifiers,
+                "keys": keys,
+                "args": list(
+                    set(events.get(listener.type, {}).get("args", []) + listener.args)
+                ),
+                "throttle": min(
+                    events.get(listener.type, {}).get("throttle", float("inf")),
+                    listener.throttle,
+                ),
             }
             }
         return {
         return {
-            'id': self.id,
-            'tag': self.tag,
-            'class': self._classes,
-            'style': self._style,
-            'props': self._props,
-            'events': events,
-            'text': self._text,
-            'slots': {name: [child.id for child in slot.children] for name, slot in self.slots.items()},
+            "id": self.id,
+            "tag": self.tag,
+            "class": self._classes,
+            "style": self._style,
+            "props": self._props,
+            "events": events,
+            "text": self._text,
+            "slots": {
+                name: [child.id for child in slot.children]
+                for name, slot in self.slots.items()
+            },
         }
         }
 
 
-    def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
-            -> 'Element':
-        '''
-        Apply, remove, or replace HTML classes.
+    def classes(
+        self,
+        add: Optional[str] = None,
+        *,
+        remove: Optional[str] = None,
+        replace: Optional[str] = None,
+    ) -> "Element":
+        """Apply, remove, or replace HTML classes.
 
 
         Generally, this is for the purpose of modifying the look of the element or its layout, based on the
         Generally, this is for the purpose of modifying the look of the element or its layout, based on the
         Quasar framework.
         Quasar framework.
@@ -103,12 +116,14 @@ class Element(ABC, Visibility):
         :param add: a white-space delimited string of classes
         :param add: a white-space delimited string of classes
         :param remove: A white-space delimited string of classes to remove from the element.
         :param remove: A white-space delimited string of classes to remove from the element.
         :param replace: A white-space delimited string of classes to use instead of existing.
         :param replace: A white-space delimited string of classes to use instead of existing.
-        '''
+        """
         class_list = self._classes if replace is None else []
         class_list = self._classes if replace is None else []
-        class_list = [c for c in class_list if c not in (remove or '').split()]
-        class_list += (add or '').split()
-        class_list += (replace or '').split()
-        new_classes = list(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+        class_list = [c for c in class_list if c not in (remove or "").split()]
+        class_list += (add or "").split()
+        class_list += (replace or "").split()
+        new_classes = list(
+            dict.fromkeys(class_list)
+        )  # NOTE: remove duplicates while preserving order
         if self._classes != new_classes:
         if self._classes != new_classes:
             self._classes = new_classes
             self._classes = new_classes
             self.update()
             self.update()
@@ -117,19 +132,24 @@ class Element(ABC, Visibility):
     @staticmethod
     @staticmethod
     def _parse_style(text: Optional[str]) -> Dict[str, str]:
     def _parse_style(text: Optional[str]) -> Dict[str, str]:
         result = {}
         result = {}
-        for word in (text or '').split(';'):
+        for word in (text or "").split(";"):
             word = word.strip()
             word = word.strip()
             if word:
             if word:
-                key, value = word.split(':', 1)
+                key, value = word.split(":", 1)
                 result[key.strip()] = value.strip()
                 result[key.strip()] = value.strip()
         return result
         return result
 
 
-    def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None)\
-            -> 'Element':
-        '''
-        Apply, remove, or replace CSS style sheet definitions to modify the look of the element.
+    def style(
+        self,
+        add: Optional[str] = None,
+        *,
+        remove: Optional[str] = None,
+        replace: Optional[str] = None,
+    ) -> "Element":
+        """Apply, remove, or replace CSS style sheet definitions to modify the look of the element.
 
 
         .. note::
         .. note::
+
             Removing styles can be helpful if the predefined style sheet definitions by NiceGUI are not wanted
             Removing styles can be helpful if the predefined style sheet definitions by NiceGUI are not wanted
             in a particular styling.
             in a particular styling.
 
 
@@ -140,7 +160,7 @@ class Element(ABC, Visibility):
         :param add: A semicolon separated list of styles to add to the element.
         :param add: A semicolon separated list of styles to add to the element.
         :param remove: A semicolon separated list of styles to remove from the element.
         :param remove: A semicolon separated list of styles to remove from the element.
         :param replace: Like add, but existing styles will be replaced by given.
         :param replace: Like add, but existing styles will be replaced by given.
-         '''
+        """
         style_dict = deepcopy(self._style) if replace is None else {}
         style_dict = deepcopy(self._style) if replace is None else {}
         for key in self._parse_style(remove):
         for key in self._parse_style(remove):
             if key in style_dict:
             if key in style_dict:
@@ -155,7 +175,7 @@ class Element(ABC, Visibility):
     @staticmethod
     @staticmethod
     def _parse_props(text: Optional[str]) -> Dict[str, Any]:
     def _parse_props(text: Optional[str]) -> Dict[str, Any]:
         dictionary = {}
         dictionary = {}
-        for match in PROPS_PATTERN.finditer(text or ''):
+        for match in PROPS_PATTERN.finditer(text or ""):
             key = match.group(1)
             key = match.group(1)
             value = match.group(2) or match.group(3)
             value = match.group(2) or match.group(3)
             if value and value.startswith('"') and value.endswith('"'):
             if value and value.startswith('"') and value.endswith('"'):
@@ -163,8 +183,10 @@ class Element(ABC, Visibility):
             dictionary[key] = value or True
             dictionary[key] = value or True
         return dictionary
         return dictionary
 
 
-    def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> 'Element':
-        '''Add or remove Quasar-specif properties to modify the look of the element.
+    def props(
+        self, add: Optional[str] = None, *, remove: Optional[str] = None
+    ) -> "Element":
+        """Add or remove Quasar-specif properties to modify the look of the element.
 
 
         see https://quasar.dev/vue-components/button#design
         see https://quasar.dev/vue-components/button#design
 
 
@@ -176,7 +198,7 @@ class Element(ABC, Visibility):
 
 
         :param add: A whitespace separated list of either boolean values or key=value pair to add
         :param add: A whitespace separated list of either boolean values or key=value pair to add
         :param remove: A whitespace separated list of property keys to remove.
         :param remove: A whitespace separated list of property keys to remove.
-        '''
+        """
         needs_update = False
         needs_update = False
         for key in self._parse_props(remove):
         for key in self._parse_props(remove):
             if key in self._props:
             if key in self._props:
@@ -192,29 +214,40 @@ class Element(ABC, Visibility):
 
 
     def tooltip(self, text: str):
     def tooltip(self, text: str):
         with self:
         with self:
-            tooltip = Element('q-tooltip')
+            tooltip = Element("q-tooltip")
             tooltip._text = text
             tooltip._text = text
         return self
         return self
 
 
-    def on(self, type: str, handler: Optional[Callable], args: Optional[List[str]] = None, *, throttle: float = 0.0) \
-            -> 'Element':
+    def on(
+        self,
+        type: AnyHTMLEvent,
+        handler: Optional[Callable],
+        args: Optional[List[str]] = None,
+        *,
+        throttle: float = 0.0,
+    ) -> "Element":
         if handler:
         if handler:
-            args = args if args is not None else ['*']
-            listener = EventListener(element_id=self.id, type=type, args=args, handler=handler, throttle=throttle)
+            args = args if args is not None else ["*"]
+            listener = EventListener(
+                element_id=self.id,
+                type=type,
+                args=args,
+                handler=handler,
+                throttle=throttle,
+            )
             self._event_listeners.append(listener)
             self._event_listeners.append(listener)
         return self
         return self
 
 
     def handle_event(self, msg: Dict) -> None:
     def handle_event(self, msg: Dict) -> None:
         for listener in self._event_listeners:
         for listener in self._event_listeners:
-            if listener.type == msg['type']:
+            if listener.type == msg["type"]:
                 handle_event(listener.handler, msg, sender=self)
                 handle_event(listener.handler, msg, sender=self)
 
 
     def collect_descendant_ids(self) -> List[int]:
     def collect_descendant_ids(self) -> List[int]:
-        '''
-        Return a list of ids of the element and each of its descendents.
+        """Return a list of ids of the element and each of its descendents.
 
 
         ... note:: The first id in the list is that of the element.
         ... note:: The first id in the list is that of the element.
-        '''
+        """
         ids: List[int] = [self.id]
         ids: List[int] = [self.id]
         for slot in self.slots.values():
         for slot in self.slots.values():
             for child in slot.children:
             for child in slot.children:
@@ -226,17 +259,25 @@ class Element(ABC, Visibility):
             return
             return
         ids = self.collect_descendant_ids()
         ids = self.collect_descendant_ids()
         elements = {id: self.client.elements[id].to_dict() for id in ids}
         elements = {id: self.client.elements[id].to_dict() for id in ids}
-        background_tasks.create(globals.sio.emit('update', {'elements': elements}, room=self.client.id))
+        background_tasks.create(
+            globals.sio.emit("update", {"elements": elements}, room=self.client.id)
+        )
 
 
     def run_method(self, name: str, *args: Any) -> None:
     def run_method(self, name: str, *args: Any) -> None:
         if not globals.loop:
         if not globals.loop:
             return
             return
-        data = {'id': self.id, 'name': name, 'args': args}
-        background_tasks.create(globals.sio.emit('run_method', data, room=globals._socket_id or self.client.id))
+        data = {"id": self.id, "name": name, "args": args}
+        background_tasks.create(
+            globals.sio.emit(
+                "run_method", data, room=globals._socket_id or self.client.id
+            )
+        )
 
 
     def clear(self) -> None:
     def clear(self) -> None:
-        '''Remove all descendant (child) elements.'''
-        descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]
+        """Remove all descendant (child) elements."""
+        descendants = [
+            self.client.elements[id] for id in self.collect_descendant_ids()[1:]
+        ]
         binding.remove(descendants, Element)
         binding.remove(descendants, Element)
         for element in descendants:
         for element in descendants:
             del self.client.elements[element.id]
             del self.client.elements[element.id]
@@ -245,13 +286,14 @@ class Element(ABC, Visibility):
         self.update()
         self.update()
 
 
     def remove(self, element: Union[Element, int]) -> None:
     def remove(self, element: Union[Element, int]) -> None:
-        '''
-        Remove a descendant (child) element.
+        """Remove a descendant (child) element.
 
 
         :param element: Either the element instance or its id.
         :param element: Either the element instance or its id.
-        '''
+        """
         if isinstance(element, int):
         if isinstance(element, int):
-            children = [child for slot in self.slots.values() for child in slot.children]
+            children = [
+                child for slot in self.slots.values() for child in slot.children
+            ]
             element = children[element]
             element = children[element]
         binding.remove([element], Element)
         binding.remove([element], Element)
         del self.client.elements[element.id]
         del self.client.elements[element.id]

+ 33 - 2
nicegui/typedefs.py

@@ -1,9 +1,9 @@
 """
 """
 Define types used throughout to assist with auto-complete.
 Define types used throughout to assist with auto-complete.
 """
 """
-from typing import List, Dict, Any
+from typing import List, Dict, Any, Union
 
 
-from typing_extensions import TypedDict
+from typing_extensions import TypedDict, Literal
 
 
 # note: This element of typed dict is necessary due to the use of the "class" keyword.
 # note: This element of typed dict is necessary due to the use of the "class" keyword.
 # note: Ideally the dict types get better refined moving forward to give more clarity to the caller.
 # note: Ideally the dict types get better refined moving forward to give more clarity to the caller.
@@ -18,3 +18,34 @@ ElementAsDict = TypedDict('ElementAsDict', {
     'slots': Dict
     'slots': Dict
 })
 })
 
 
+WindowsEvents = Literal[
+    'afterprint', 'beforeprint', 'beforeunload', 'error', 'hashchange', 'load', 'message', 'offline', 'online',
+    'pagehide', 'pageshow', 'popstate', 'resize', 'storage', 'unload']
+FormEvents = Literal[
+    'blur', 'change', 'contextmenu', 'focus', 'input', 'invalid', 'reset', 'search', 'select',
+    'submit']
+KeyboardEvents = Literal[
+    'keydown', 'keypress', 'keyup'
+]
+MouseEvents = Literal[
+    'click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
+    'mousewheel', 'wheel'
+]
+DragEvents = Literal[
+    'drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'dragstart', 'drop', 'scroll'
+]
+ClipboardEvents = Literal[
+    'copy', 'cut', 'paste'
+]
+MediaEvents = Literal[
+    'abort', 'canplay', 'canplaythrough', 'cuechange', 'durationchange', 'emptied', 'ended', 'error', 
+    'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 
+    'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting'
+]
+MiscEvents = Literal['toggle']
+
+AnyHTMLEvent = Union[
+    WindowsEvents, FormEvents, KeyboardEvents, MouseEvents, DragEvents, ClipboardEvents, MediaEvents, MiscEvents]
+
+
+