element.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. from __future__ import annotations
  2. import shlex
  3. from abc import ABC
  4. from copy import deepcopy
  5. from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
  6. from . import binding, globals
  7. from .elements.mixins.visibility import Visibility
  8. from .event_listener import EventListener
  9. from .slot import Slot
  10. from .task_logger import create_task
  11. if TYPE_CHECKING:
  12. from .client import Client
  13. class Element(ABC, Visibility):
  14. def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
  15. super().__init__()
  16. self.client = _client or globals.get_client()
  17. self.id = self.client.next_element_id
  18. self.client.next_element_id += 1
  19. self.tag = tag
  20. self._classes: List[str] = []
  21. self._style: Dict[str, str] = {}
  22. self._props: Dict[str, str] = {}
  23. self._event_listeners: List[EventListener] = []
  24. self._text: str = ''
  25. self.slots: Dict[str, Slot] = {}
  26. self.default_slot = self.add_slot('default')
  27. self.client.elements[self.id] = self
  28. self.parent_slot: Optional[Slot] = None
  29. slot_stack = globals.get_slot_stack()
  30. if slot_stack:
  31. self.parent_slot = slot_stack[-1]
  32. self.parent_slot.children.append(self)
  33. def add_slot(self, name: str) -> Slot:
  34. self.slots[name] = Slot(self, name)
  35. return self.slots[name]
  36. def __enter__(self):
  37. self.default_slot.__enter__()
  38. return self
  39. def __exit__(self, *_):
  40. self.default_slot.__exit__(*_)
  41. def to_dict(self) -> Dict:
  42. events: Dict[str, Dict] = {}
  43. for listener in self._event_listeners:
  44. events[listener.type] = {
  45. 'type': listener.type.split('.')[0],
  46. 'modifiers': listener.type.split('.')[1:],
  47. 'args': list(set(events.get(listener.type, {}).get('args', []) + listener.args)),
  48. 'throttle': min(events.get(listener.type, {}).get('throttle', float('inf')), listener.throttle),
  49. }
  50. return {
  51. 'id': self.id,
  52. 'tag': self.tag,
  53. 'class': self._classes,
  54. 'style': self._style,
  55. 'props': self._props,
  56. 'events': events,
  57. 'text': self._text,
  58. 'slots': {name: [child.id for child in slot.children] for name, slot in self.slots.items()},
  59. }
  60. def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
  61. '''HTML classes to modify the look of the element.
  62. Every class in the `remove` parameter will be removed from the element.
  63. Classes are separated with a blank space.
  64. This can be helpful if the predefined classes by NiceGUI are not wanted in a particular styling.
  65. '''
  66. class_list = self._classes if replace is None else []
  67. class_list = [c for c in class_list if c not in (remove or '').split()]
  68. class_list += (add or '').split()
  69. class_list += (replace or '').split()
  70. new_classes = list(dict.fromkeys(class_list)) # NOTE: remove duplicates while preserving order
  71. if self._classes != new_classes:
  72. self._classes = new_classes
  73. self.update()
  74. return self
  75. @staticmethod
  76. def _parse_style(text: Optional[str]) -> Dict[str, str]:
  77. return dict((word.strip() for word in part.split(':')) for part in text.strip('; ').split(';')) if text else {}
  78. def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
  79. '''CSS style sheet definitions to modify the look of the element.
  80. Every style in the `remove` parameter will be removed from the element.
  81. Styles are separated with a semicolon.
  82. This can be helpful if the predefined style sheet definitions by NiceGUI are not wanted in a particular styling.
  83. '''
  84. style_dict = deepcopy(self._style) if replace is None else {}
  85. for key in self._parse_style(remove):
  86. del style_dict[key]
  87. style_dict.update(self._parse_style(add))
  88. style_dict.update(self._parse_style(replace))
  89. if self._style != style_dict:
  90. self._style = style_dict
  91. self.update()
  92. return self
  93. @staticmethod
  94. def _parse_props(text: Optional[str]) -> Dict[str, str]:
  95. if not text:
  96. return {}
  97. lexer = shlex.shlex(text, posix=True)
  98. lexer.whitespace = ' '
  99. lexer.wordchars += '=-.%'
  100. return dict(word.split('=', 1) if '=' in word else (word, True) for word in lexer)
  101. def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
  102. '''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
  103. Boolean props will automatically activated if they appear in the list of the `add` property.
  104. Props are separated with a blank space. String values must be quoted.
  105. Every prop passed to the `remove` parameter will be removed from the element.
  106. This can be helpful if the predefined props by NiceGUI are not wanted in a particular styling.
  107. '''
  108. needs_update = False
  109. for key in self._parse_props(remove):
  110. if key in self._props:
  111. needs_update = True
  112. del self._props[key]
  113. for key, value in self._parse_props(add).items():
  114. if self._props.get(key) != value:
  115. needs_update = True
  116. self._props[key] = value
  117. if needs_update:
  118. self.update()
  119. return self
  120. def tooltip(self, text: str):
  121. with self:
  122. tooltip = Element('q-tooltip')
  123. tooltip._text = text
  124. return self
  125. def on(self, type: str, handler: Optional[Callable], args: Optional[List[str]] = None, *, throttle: float = 0.0):
  126. if handler:
  127. args = args if args is not None else ['*']
  128. listener = EventListener(element_id=self.id, type=type, args=args, handler=handler, throttle=throttle)
  129. self._event_listeners.append(listener)
  130. return self
  131. def handle_event(self, msg: Dict) -> None:
  132. for listener in self._event_listeners:
  133. if listener.type == msg['type']:
  134. result = listener.handler(msg)
  135. if isinstance(result, Awaitable):
  136. create_task(result)
  137. def collect_descendant_ids(self) -> List[int]:
  138. '''includes own ID as first element'''
  139. ids: List[int] = [self.id]
  140. for slot in self.slots.values():
  141. for child in slot.children:
  142. ids.extend(child.collect_descendant_ids())
  143. return ids
  144. def update(self) -> None:
  145. if not globals.loop:
  146. return
  147. ids = self.collect_descendant_ids()
  148. elements = {id: self.client.elements[id].to_dict() for id in ids}
  149. create_task(globals.sio.emit('update', {'elements': elements}, room=str(self.client.id)))
  150. def run_method(self, name: str, *args: Any) -> None:
  151. if globals.loop is None:
  152. return
  153. data = {'id': self.id, 'name': name, 'args': args}
  154. create_task(globals.sio.emit('run_method', data, room=str(self.client.id)))
  155. def clear(self) -> None:
  156. descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]
  157. binding.remove(descendants, Element)
  158. for element in descendants:
  159. del self.client.elements[element.id]
  160. for slot in self.slots.values():
  161. slot.children.clear()
  162. self.update()
  163. def remove(self, element: Union[Element, int]) -> None:
  164. if isinstance(element, int):
  165. children = [child for slot in self.slots.values() for child in slot.children]
  166. element = children[element]
  167. binding.remove([element], Element)
  168. del self.client.elements[element.id]
  169. for slot in self.slots.values():
  170. slot.children[:] = [e for e in slot.children if e.id != element.id]
  171. self.update()