element.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import shlex
  2. from abc import ABC
  3. from typing import Callable, Dict, List, Optional
  4. from . import globals
  5. from .elements.mixins.visibility import Visibility
  6. from .event_listener import EventListener
  7. from .slot import Slot
  8. from .task_logger import create_task
  9. class Element(ABC, Visibility):
  10. def __init__(self, tag: str) -> None:
  11. self.client = globals.client_stack[-1]
  12. self.id = self.client.next_element_id
  13. self.client.next_element_id += 1
  14. self.tag = tag
  15. self._classes: List[str] = []
  16. self._style: Dict[str, str] = {}
  17. self._props: Dict[str, str] = {}
  18. self._event_listeners: List[EventListener] = []
  19. self._text: str = ''
  20. self.slots: Dict[str, Slot] = {}
  21. self.default_slot = self.add_slot('default')
  22. self.client.elements[self.id] = self
  23. if self.client.slot_stack:
  24. self.client.slot_stack[-1].children.append(self)
  25. self.init_visibility()
  26. def add_slot(self, name: str) -> Slot:
  27. self.slots[name] = Slot(self, name)
  28. return self.slots[name]
  29. def __enter__(self):
  30. self.client.slot_stack.append(self.default_slot)
  31. return self
  32. def __exit__(self, *_):
  33. self.client.slot_stack.pop()
  34. def to_dict(self) -> Dict:
  35. events: Dict[str, List[str]] = {}
  36. for listener in self._event_listeners:
  37. events[listener.type] = events.get(listener.type, []) + listener.args
  38. return {
  39. 'id': self.id,
  40. 'tag': self.tag,
  41. 'class': self._classes,
  42. 'style': self._style,
  43. 'props': self._props,
  44. 'events': events,
  45. 'text': self._text,
  46. 'slots': {name: [child.id for child in slot.children] for name, slot in self.slots.items()},
  47. }
  48. def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
  49. '''HTML classes to modify the look of the element.
  50. Every class in the `remove` parameter will be removed from the element.
  51. Classes are separated with a blank space.
  52. This can be helpful if the predefined classes by NiceGUI are not wanted in a particular styling.
  53. '''
  54. class_list = self._classes if replace is None else []
  55. class_list = [c for c in class_list if c not in (remove or '').split()]
  56. class_list += (add or '').split()
  57. class_list += (replace or '').split()
  58. new_classes = list(dict.fromkeys(class_list)) # NOTE: remove duplicates while preserving order
  59. if self._classes != new_classes:
  60. self._classes = new_classes
  61. self.update()
  62. return self
  63. def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
  64. '''CSS style sheet definitions to modify the look of the element.
  65. Every style in the `remove` parameter will be removed from the element.
  66. Styles are separated with a semicolon.
  67. This can be helpful if the predefined style sheet definitions by NiceGUI are not wanted in a particular styling.
  68. '''
  69. def parse_style(text: Optional[str]) -> Dict[str, str]:
  70. return dict((word.strip() for word in part.split(':')) for part in text.strip('; ').split(';')) if text else {}
  71. style_dict = self._style if replace is None else {}
  72. for key in parse_style(remove):
  73. del style_dict[key]
  74. style_dict.update(parse_style(add))
  75. style_dict.update(parse_style(replace))
  76. if self._style != style_dict:
  77. self._style = style_dict
  78. self.update()
  79. return self
  80. def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
  81. '''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
  82. Boolean props will automatically activated if they appear in the list of the `add` property.
  83. Props are separated with a blank space. String values must be quoted.
  84. Every prop passed to the `remove` parameter will be removed from the element.
  85. This can be helpful if the predefined props by NiceGUI are not wanted in a particular styling.
  86. '''
  87. def parse_props(text: Optional[str]) -> Dict[str, str]:
  88. if not text:
  89. return {}
  90. lexer = shlex.shlex(text, posix=True)
  91. lexer.whitespace = ' '
  92. lexer.wordchars += '=-.%'
  93. return dict(word.split('=', 1) if '=' in word else (word, True) for word in lexer)
  94. needs_update = False
  95. for key in parse_props(remove):
  96. if key in self._props:
  97. needs_update = True
  98. del self._props[key]
  99. for key, value in parse_props(add).items():
  100. if self._props.get(key) != value:
  101. needs_update = True
  102. self._props[key] = value
  103. if needs_update:
  104. self.update()
  105. return self
  106. def on(self, type: str, handler: Optional[Callable], args: List[str] = []):
  107. if handler:
  108. self._event_listeners.append(EventListener(element_id=self.id, type=type, args=args, handler=handler))
  109. return self
  110. def handle_event(self, msg: Dict) -> None:
  111. for listener in self._event_listeners:
  112. if listener.type == msg['type']:
  113. listener.handler(msg)
  114. def update(self) -> None:
  115. if not globals.loop:
  116. return
  117. ids: List[int] = []
  118. def collect_ids(id: str):
  119. for slot in self.client.elements[id].slots.values():
  120. for child in slot.children:
  121. collect_ids(child.id)
  122. ids.append(id)
  123. collect_ids(self.id)
  124. elements = {id: self.client.elements[id].to_dict() for id in ids}
  125. create_task(globals.sio.emit('update', {'elements': elements}, room=str(self.client.id)))