page_layout.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. from typing import Literal, Optional
  2. from .context import context
  3. from .element import Element
  4. from .elements.mixins.value_element import ValueElement
  5. from .functions.html import add_body_html
  6. from .logging import log
  7. DrawerSides = Literal['left', 'right']
  8. PageStickyPositions = Literal[
  9. 'top-right',
  10. 'top-left',
  11. 'bottom-right',
  12. 'bottom-left',
  13. 'top',
  14. 'right',
  15. 'bottom',
  16. 'left',
  17. ]
  18. class Header(ValueElement):
  19. def __init__(self, *,
  20. value: bool = True,
  21. fixed: bool = True,
  22. bordered: bool = False,
  23. elevated: bool = False,
  24. wrap: bool = True,
  25. add_scroll_padding: bool = True,
  26. ) -> None:
  27. """Header
  28. This element is based on Quasar's `QHeader <https://quasar.dev/layout/header-and-footer#qheader-api>`_ component.
  29. Note: The header is automatically placed above other layout elements in the DOM to improve accessibility.
  30. To change the order, use the `move` method.
  31. :param value: whether the header is already opened (default: `True`)
  32. :param fixed: whether the header should be fixed to the top of the page (default: `True`)
  33. :param bordered: whether the header should have a border (default: `False`)
  34. :param elevated: whether the header should have a shadow (default: `False`)
  35. :param wrap: whether the header should wrap its content (default: `True`)
  36. :param add_scroll_padding: whether to automatically prevent link targets from being hidden behind the header (default: `True`)
  37. """
  38. _check_current_slot(self)
  39. with context.client.layout:
  40. super().__init__(tag='q-header', value=value, on_value_change=None)
  41. self._classes.append('nicegui-header')
  42. self._props['bordered'] = bordered
  43. self._props['elevated'] = elevated
  44. if wrap:
  45. self._classes.append('wrap')
  46. code = list(self.client.layout._props['view'])
  47. code[1] = 'H' if fixed else 'h'
  48. self.client.layout._props['view'] = ''.join(code)
  49. self.move(target_index=0)
  50. if add_scroll_padding:
  51. add_body_html(f'''
  52. <script>
  53. window.onload = () => {{
  54. const header = getElement({self.id}).$el;
  55. new ResizeObserver(() => {{
  56. document.documentElement.style.scrollPaddingTop = `${{header.offsetHeight}}px`;
  57. }}).observe(header);
  58. }};
  59. </script>
  60. ''')
  61. def toggle(self):
  62. """Toggle the header"""
  63. self.value = not self.value
  64. def show(self):
  65. """Show the header"""
  66. self.value = True
  67. def hide(self):
  68. """Hide the header"""
  69. self.value = False
  70. class Drawer(Element):
  71. def __init__(self,
  72. side: DrawerSides, *,
  73. value: Optional[bool] = None,
  74. fixed: bool = True,
  75. bordered: bool = False,
  76. elevated: bool = False,
  77. top_corner: bool = False,
  78. bottom_corner: bool = False) -> None:
  79. """Drawer
  80. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  81. Note: Depending on the side, the drawer is automatically placed above or below the main page container in the DOM to improve accessibility.
  82. To change the order, use the `move` method.
  83. :param side: side of the page where the drawer should be placed (`left` or `right`)
  84. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  85. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  86. :param bordered: whether the drawer should have a border (default: `False`)
  87. :param elevated: whether the drawer should have a shadow (default: `False`)
  88. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  89. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  90. """
  91. _check_current_slot(self)
  92. with context.client.layout:
  93. super().__init__('q-drawer')
  94. if value is None:
  95. self._props['show-if-above'] = True
  96. else:
  97. self._props['model-value'] = value
  98. self._props['side'] = side
  99. self._props['bordered'] = bordered
  100. self._props['elevated'] = elevated
  101. self._classes.append('nicegui-drawer')
  102. code = list(self.client.layout._props['view'])
  103. code[0 if side == 'left' else 2] = side[0].lower() if top_corner else 'h'
  104. code[4 if side == 'left' else 6] = side[0].upper() if fixed else side[0].lower()
  105. code[8 if side == 'left' else 10] = side[0].lower() if bottom_corner else 'f'
  106. self.client.layout._props['view'] = ''.join(code)
  107. page_container_index = self.client.layout.default_slot.children.index(self.client.page_container)
  108. self.move(target_index=page_container_index if side == 'left' else page_container_index + 1)
  109. def toggle(self) -> None:
  110. """Toggle the drawer"""
  111. self.run_method('toggle')
  112. def show(self) -> None:
  113. """Show the drawer"""
  114. self.run_method('show')
  115. def hide(self) -> None:
  116. """Hide the drawer"""
  117. self.run_method('hide')
  118. class LeftDrawer(Drawer):
  119. def __init__(self, *,
  120. value: Optional[bool] = None,
  121. fixed: bool = True,
  122. bordered: bool = False,
  123. elevated: bool = False,
  124. top_corner: bool = False,
  125. bottom_corner: bool = False) -> None:
  126. """Left drawer
  127. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  128. Note: The left drawer is automatically placed above the main page container in the DOM to improve accessibility.
  129. To change the order, use the `move` method.
  130. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  131. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  132. :param bordered: whether the drawer should have a border (default: `False`)
  133. :param elevated: whether the drawer should have a shadow (default: `False`)
  134. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  135. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  136. """
  137. super().__init__('left',
  138. value=value,
  139. fixed=fixed,
  140. bordered=bordered,
  141. elevated=elevated,
  142. top_corner=top_corner,
  143. bottom_corner=bottom_corner)
  144. class RightDrawer(Drawer):
  145. def __init__(self, *,
  146. value: Optional[bool] = None,
  147. fixed: bool = True,
  148. bordered: bool = False,
  149. elevated: bool = False,
  150. top_corner: bool = False,
  151. bottom_corner: bool = False) -> None:
  152. """Right drawer
  153. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  154. Note: The right drawer is automatically placed below the main page container in the DOM to improve accessibility.
  155. To change the order, use the `move` method.
  156. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  157. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  158. :param bordered: whether the drawer should have a border (default: `False`)
  159. :param elevated: whether the drawer should have a shadow (default: `False`)
  160. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  161. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  162. """
  163. super().__init__('right',
  164. value=value,
  165. fixed=fixed,
  166. bordered=bordered,
  167. elevated=elevated,
  168. top_corner=top_corner,
  169. bottom_corner=bottom_corner)
  170. class Footer(ValueElement):
  171. def __init__(self, *,
  172. value: bool = True,
  173. fixed: bool = True,
  174. bordered: bool = False,
  175. elevated: bool = False,
  176. wrap: bool = True,
  177. ) -> None:
  178. """Footer
  179. This element is based on Quasar's `QFooter <https://quasar.dev/layout/header-and-footer#qfooter-api>`_ component.
  180. Note: The footer is automatically placed below other layout elements in the DOM to improve accessibility.
  181. To change the order, use the `move` method.
  182. :param value: whether the footer is already opened (default: `True`)
  183. :param fixed: whether the footer is fixed or scrolls with the content (default: `True`)
  184. :param bordered: whether the footer should have a border (default: `False`)
  185. :param elevated: whether the footer should have a shadow (default: `False`)
  186. :param wrap: whether the footer should wrap its content (default: `True`)
  187. """
  188. _check_current_slot(self)
  189. with context.client.layout:
  190. super().__init__(tag='q-footer', value=value, on_value_change=None)
  191. self.classes('nicegui-footer')
  192. self._props['bordered'] = bordered
  193. self._props['elevated'] = elevated
  194. if wrap:
  195. self._classes.append('wrap')
  196. code = list(self.client.layout._props['view'])
  197. code[9] = 'F' if fixed else 'f'
  198. self.client.layout._props['view'] = ''.join(code)
  199. self.move(target_index=-1)
  200. def toggle(self) -> None:
  201. """Toggle the footer"""
  202. self.value = not self.value
  203. def show(self) -> None:
  204. """Show the footer"""
  205. self.value = True
  206. def hide(self) -> None:
  207. """Hide the footer"""
  208. self.value = False
  209. class PageSticky(Element):
  210. def __init__(self, position: PageStickyPositions = 'bottom-right', x_offset: float = 0, y_offset: float = 0) -> None:
  211. """Page sticky
  212. A sticky element that is always visible at the bottom of the page.
  213. :param position: position of the sticky element (default: `'bottom-right'`)
  214. :param x_offset: horizontal offset of the sticky element (default: `0`)
  215. :param y_offset: vertical offset of the sticky element (default: `0`)
  216. """
  217. super().__init__('q-page-sticky')
  218. self._props['position'] = position
  219. self._props['offset'] = [x_offset, y_offset]
  220. def _check_current_slot(element: Element) -> None:
  221. parent = context.slot.parent
  222. if parent != parent.client.content:
  223. log.warning(f'Found top level layout element "{element.__class__.__name__}" inside element "{parent.__class__.__name__}". '
  224. 'Top level layout elements should not be nested but must be direct children of the page content. '
  225. 'This will be raising an exception in NiceGUI 1.5') # DEPRECATED