Переглянути джерело

Merge remote-tracking branch 'origin/events'

Rodja Trappe 2 роки тому
батько
коміт
21c22828d9
4 змінених файлів з 69 додано та 65 видалено
  1. 20 58
      nicegui/element.py
  2. 23 2
      nicegui/event_listener.py
  3. 9 5
      nicegui/templates/index.html
  4. 17 0
      tests/test_events.py

+ 20 - 58
nicegui/element.py

@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import re
+import warnings
 from copy import deepcopy
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 
@@ -38,7 +39,7 @@ class Element(Visibility):
         self._classes: List[str] = []
         self._style: Dict[str, str] = {}
         self._props: Dict[str, Any] = {}
-        self._event_listeners: List[EventListener] = []
+        self._event_listeners: Dict[str, EventListener] = {}
         self._text: str = ''
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
@@ -71,64 +72,23 @@ class Element(Visibility):
     def __exit__(self, *_):
         self.default_slot.__exit__(*_)
 
-    def _collect_event_dict(self) -> Dict[str, Dict]:
-        events: Dict[str, Dict] = {}
-        for listener in self._event_listeners:
-            words = listener.type.split('.')
-            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'}]
-            keys = [w for w in words if w not in specials + modifiers]
-            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),
-            }
-        return events
-
     def _collect_slot_dict(self) -> Dict[str, List[int]]:
         return {
             name: {'template': slot.template, 'ids': [child.id for child in slot.children]}
             for name, slot in self.slots.items()
         }
 
-    def _to_dict(self, *keys: str) -> Dict:
-        if not keys:
-            return {
-                'id': self.id,
-                'tag': self.tag,
-                'class': self._classes,
-                'style': self._style,
-                'props': self._props,
-                'text': self._text,
-                'slots': self._collect_slot_dict(),
-                'events': self._collect_event_dict(),
-            }
-        dict_: Dict[str, Any] = {}
-        for key in keys:
-            if key == 'id':
-                dict_['id'] = self.id
-            elif key == 'tag':
-                dict_['tag'] = self.tag
-            elif key == 'class':
-                dict_['class'] = self._classes
-            elif key == 'style':
-                dict_['style'] = self._style
-            elif key == 'props':
-                dict_['props'] = self._props
-            elif key == 'text':
-                dict_['text'] = self._text
-            elif key == 'slots':
-                dict_['slots'] = self._collect_slot_dict()
-            elif key == 'events':
-                dict_['events'] = self._collect_event_dict()
-            else:
-                raise ValueError(f'Unknown key {key}')
-        return dict_
+    def _to_dict(self) -> Dict[str, Any]:
+        return {
+            'id': self.id,
+            'tag': self.tag,
+            'class': self._classes,
+            'style': self._style,
+            'props': self._props,
+            'text': self._text,
+            'slots': self._collect_slot_dict(),
+            'events': [listener.to_dict() for listener in self._event_listeners.values()],
+        }
 
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
             -> Self:
@@ -237,15 +197,17 @@ class Element(Visibility):
         :param throttle: minimum time (in seconds) between event occurrences (default: 0.0)
         """
         if handler:
-            args = args if args is not None else ['*']
+            if args and '*' in args:
+                url = f'https://github.com/zauberzeug/nicegui/issues/644'
+                warnings.warn(DeprecationWarning(f'Event args "*" is deprecated, omit this parameter instead ({url})'))
+                args = None
             listener = EventListener(element_id=self.id, type=type, args=args, handler=handler, throttle=throttle)
-            self._event_listeners.append(listener)
+            self._event_listeners[listener.id] = listener
         return self
 
     def _handle_event(self, msg: Dict) -> None:
-        for listener in self._event_listeners:
-            if listener.type == msg['type']:
-                events.handle_event(listener.handler, msg, sender=self)
+        listener = self._event_listeners[msg['listener_id']]
+        events.handle_event(listener.handler, msg, sender=self)
 
     def update(self) -> None:
         """Update the element on the client side."""

+ 23 - 2
nicegui/event_listener.py

@@ -1,11 +1,32 @@
-from dataclasses import dataclass
-from typing import Callable, List
+import uuid
+from dataclasses import dataclass, field
+from typing import Any, Callable, Dict, List
 
 
 @dataclass
 class EventListener:
+    id: str = field(init=False)
     element_id: int
     type: str
     args: List[str]
     handler: Callable
     throttle: float
+
+    def __post_init__(self) -> None:
+        self.id = str(uuid.uuid4())
+
+    def to_dict(self) -> Dict[str, Any]:
+        words = self.type.split('.')
+        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'}]
+        keys = [w for w in words if w not in specials + modifiers]
+        return {
+            'listener_id': self.id,
+            'type': type,
+            'specials': specials,
+            'modifiers': modifiers,
+            'keys': keys,
+            'args': self.args,
+            'throttle': self.throttle,
+        }

+ 9 - 5
nicegui/templates/index.html

@@ -51,21 +51,25 @@
           style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, '') || undefined,
           ...element.props,
         };
-        Object.values(element.events).forEach((event) => {
+        element.events.forEach((event) => {
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
           let handler = (e) => {
-            const all = typeof e !== 'object' || event.args.includes('*');
+            const all = typeof e !== 'object' || !event.args;
             const args = all ? e : Object.fromEntries(event.args.map(a => [a, e[a]]));
-            const emitter = () => window.socket.emit("event", {id: element.id, type: event.listener_type, args});
-            throttle(emitter, event.throttle, event.listener_type);
+            const emitter = () => window.socket.emit("event", {id: element.id, listener_id: event.listener_id, args});
+            throttle(emitter, event.throttle, event.listener_id);
             if (element.props["loopback"] === False && event.type == "update:model-value") {
               element.props["model-value"] = args;
             }
           };
           handler = Vue.withModifiers(handler, event.modifiers);
           handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler;
-          props[event_name] = handler;
+          if (props[event_name]) {
+            props[event_name].push(handler)
+          } else {
+            props[event_name] = [handler];
+          }
         });
         const slots = {};
         Object.entries(element.slots).forEach(([name, data]) => {

+ 17 - 0
tests/test_events.py

@@ -90,3 +90,20 @@ def test_event_modifiers(screen: Screen):
     screen.selenium.find_element(By.XPATH, '//*[@aria-label="C"]').send_keys('xx')
     screen.selenium.find_element(By.XPATH, '//*[@aria-label="D"]').send_keys('Xx')
     assert events == ['A', 'B', 'C', 'D']
+
+
+def test_throttling(screen: Screen):
+    events = []
+    ui.button('Test', on_click=lambda: events.append(1)).on('click', lambda: events.append(2), throttle=1)
+
+    screen.open('/')
+    screen.click('Test')
+    screen.click('Test')
+    screen.click('Test')
+    assert events == [1, 2, 1, 1]
+
+    screen.wait(1.1)
+    screen.click('Test')
+    screen.click('Test')
+    screen.click('Test')
+    assert events == [1, 2, 1, 1, 1, 2, 1, 1]