page_layout.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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. DrawerSides = Literal['left', 'right']
  7. PageStickyPositions = Literal[
  8. 'top-right',
  9. 'top-left',
  10. 'bottom-right',
  11. 'bottom-left',
  12. 'top',
  13. 'right',
  14. 'bottom',
  15. 'left',
  16. ]
  17. class Header(ValueElement, default_classes='nicegui-header'):
  18. def __init__(self, *,
  19. value: bool = True,
  20. fixed: bool = True,
  21. bordered: bool = False,
  22. elevated: bool = False,
  23. wrap: bool = True,
  24. add_scroll_padding: bool = True,
  25. ) -> None:
  26. """Header
  27. This element is based on Quasar's `QHeader <https://quasar.dev/layout/header-and-footer#qheader-api>`_ component.
  28. Like other layout elements, the header can not be nested inside other elements.
  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._props['bordered'] = bordered
  42. self._props['elevated'] = elevated
  43. if wrap:
  44. self._classes.append('wrap')
  45. code = list(self.client.layout.props['view'])
  46. code[1] = 'H' if fixed else 'h'
  47. self.client.layout.props['view'] = ''.join(code)
  48. self.move(target_index=0)
  49. if add_scroll_padding:
  50. add_body_html(f'''
  51. <script>
  52. window.onload = () => {{
  53. const header = getHtmlElement({self.id});
  54. new ResizeObserver(() => {{
  55. document.documentElement.style.scrollPaddingTop = `${{header.offsetHeight}}px`;
  56. }}).observe(header);
  57. }};
  58. </script>
  59. ''')
  60. def toggle(self):
  61. """Toggle the header"""
  62. self.value = not self.value
  63. def show(self):
  64. """Show the header"""
  65. self.value = True
  66. def hide(self):
  67. """Hide the header"""
  68. self.value = False
  69. class Drawer(ValueElement, default_classes='nicegui-drawer'):
  70. def __init__(self,
  71. side: DrawerSides, *,
  72. value: Optional[bool] = None,
  73. fixed: bool = True,
  74. bordered: bool = False,
  75. elevated: bool = False,
  76. top_corner: bool = False,
  77. bottom_corner: bool = False) -> None:
  78. """Drawer
  79. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  80. Like other layout elements, a drawer can not be nested inside other elements.
  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. A value of ``None`` will automatically open or close the drawer depending on the current layout width (breakpoint: >=1024 px).
  84. On the auto-index page, the value will remain ``None`` until the drawer is opened, closed or toggled.
  85. On other pages, the value will be requested from the client when the websocket connection is established.
  86. :param side: side of the page where the drawer should be placed (`left` or `right`)
  87. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  88. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  89. :param bordered: whether the drawer should have a border (default: `False`)
  90. :param elevated: whether the drawer should have a shadow (default: `False`)
  91. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  92. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  93. """
  94. _check_current_slot(self)
  95. with context.client.layout:
  96. super().__init__(tag='q-drawer', value=value, on_value_change=None)
  97. self._props['show-if-above'] = value is None
  98. self._props['side'] = side
  99. self._props['bordered'] = bordered
  100. self._props['elevated'] = elevated
  101. code = list(self.client.layout.props['view'])
  102. code[0 if side == 'left' else 2] = side[0].lower() if top_corner else 'h'
  103. code[4 if side == 'left' else 6] = side[0].upper() if fixed else side[0].lower()
  104. code[8 if side == 'left' else 10] = side[0].lower() if bottom_corner else 'f'
  105. self.client.layout.props['view'] = ''.join(code)
  106. page_container_index = self.client.layout.default_slot.children.index(self.client.page_container)
  107. self.move(target_index=page_container_index if side == 'left' else page_container_index + 1)
  108. if value is None and not self.client.is_auto_index_client:
  109. async def _request_value() -> None:
  110. js_code = f'!getHtmlElement({self.id}).parentElement.classList.contains("q-layout--prevent-focus")'
  111. self.value = await context.client.run_javascript(js_code)
  112. self.client.on_connect(_request_value)
  113. def toggle(self) -> None:
  114. """Toggle the drawer"""
  115. if self.value is None:
  116. self.run_method('toggle')
  117. else:
  118. self.value = not self.value
  119. def show(self) -> None:
  120. """Show the drawer"""
  121. self.value = True
  122. def hide(self) -> None:
  123. """Hide the drawer"""
  124. self.value = False
  125. def _handle_value_change(self, value: bool) -> None:
  126. super()._handle_value_change(value)
  127. self._props['show-if-above'] = value is None
  128. class LeftDrawer(Drawer):
  129. def __init__(self, *,
  130. value: Optional[bool] = None,
  131. fixed: bool = True,
  132. bordered: bool = False,
  133. elevated: bool = False,
  134. top_corner: bool = False,
  135. bottom_corner: bool = False) -> None:
  136. """Left drawer
  137. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  138. Like other layout elements, the left drawer can not be nested inside other elements.
  139. Note: The left drawer is automatically placed above the main page container in the DOM to improve accessibility.
  140. To change the order, use the `move` method.
  141. A value of ``None`` will automatically open or close the drawer depending on the current layout width (breakpoint: >=1024 px).
  142. On the auto-index page, the value will remain ``None`` until the drawer is opened, closed or toggled.
  143. On other pages, the value will be requested from the client when the websocket connection is established.
  144. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  145. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  146. :param bordered: whether the drawer should have a border (default: `False`)
  147. :param elevated: whether the drawer should have a shadow (default: `False`)
  148. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  149. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  150. """
  151. super().__init__('left',
  152. value=value,
  153. fixed=fixed,
  154. bordered=bordered,
  155. elevated=elevated,
  156. top_corner=top_corner,
  157. bottom_corner=bottom_corner)
  158. class RightDrawer(Drawer):
  159. def __init__(self, *,
  160. value: Optional[bool] = None,
  161. fixed: bool = True,
  162. bordered: bool = False,
  163. elevated: bool = False,
  164. top_corner: bool = False,
  165. bottom_corner: bool = False) -> None:
  166. """Right drawer
  167. This element is based on Quasar's `QDrawer <https://quasar.dev/layout/drawer>`_ component.
  168. Like other layout elements, the right drawer can not be nested inside other elements.
  169. Note: The right drawer is automatically placed below the main page container in the DOM to improve accessibility.
  170. To change the order, use the `move` method.
  171. A value of ``None`` will automatically open or close the drawer depending on the current layout width (breakpoint: >=1024 px).
  172. On the auto-index page, the value will remain ``None`` until the drawer is opened, closed or toggled.
  173. On other pages, the value will be requested from the client when the websocket connection is established.
  174. :param value: whether the drawer is already opened (default: `None`, i.e. if layout width is above threshold)
  175. :param fixed: whether the drawer is fixed or scrolls with the content (default: `True`)
  176. :param bordered: whether the drawer should have a border (default: `False`)
  177. :param elevated: whether the drawer should have a shadow (default: `False`)
  178. :param top_corner: whether the drawer expands into the top corner (default: `False`)
  179. :param bottom_corner: whether the drawer expands into the bottom corner (default: `False`)
  180. """
  181. super().__init__('right',
  182. value=value,
  183. fixed=fixed,
  184. bordered=bordered,
  185. elevated=elevated,
  186. top_corner=top_corner,
  187. bottom_corner=bottom_corner)
  188. class Footer(ValueElement, default_classes='nicegui-footer'):
  189. def __init__(self, *,
  190. value: bool = True,
  191. fixed: bool = True,
  192. bordered: bool = False,
  193. elevated: bool = False,
  194. wrap: bool = True,
  195. ) -> None:
  196. """Footer
  197. This element is based on Quasar's `QFooter <https://quasar.dev/layout/header-and-footer#qfooter-api>`_ component.
  198. Like other layout elements, the footer can not be nested inside other elements.
  199. Note: The footer is automatically placed below other layout elements in the DOM to improve accessibility.
  200. To change the order, use the `move` method.
  201. :param value: whether the footer is already opened (default: `True`)
  202. :param fixed: whether the footer is fixed or scrolls with the content (default: `True`)
  203. :param bordered: whether the footer should have a border (default: `False`)
  204. :param elevated: whether the footer should have a shadow (default: `False`)
  205. :param wrap: whether the footer should wrap its content (default: `True`)
  206. """
  207. _check_current_slot(self)
  208. with context.client.layout:
  209. super().__init__(tag='q-footer', value=value, on_value_change=None)
  210. self._props['bordered'] = bordered
  211. self._props['elevated'] = elevated
  212. if wrap:
  213. self._classes.append('wrap')
  214. code = list(self.client.layout.props['view'])
  215. code[9] = 'F' if fixed else 'f'
  216. self.client.layout.props['view'] = ''.join(code)
  217. self.move(target_index=-1)
  218. def toggle(self) -> None:
  219. """Toggle the footer"""
  220. self.value = not self.value
  221. def show(self) -> None:
  222. """Show the footer"""
  223. self.value = True
  224. def hide(self) -> None:
  225. """Hide the footer"""
  226. self.value = False
  227. class PageSticky(Element):
  228. def __init__(self,
  229. position: PageStickyPositions = 'bottom-right',
  230. x_offset: float = 0,
  231. y_offset: float = 0,
  232. *,
  233. expand: bool = False) -> None:
  234. """Page sticky
  235. This element is based on Quasar's `QPageSticky <https://quasar.dev/layout/page-sticky>`_ component.
  236. :param position: position on the screen (default: "bottom-right")
  237. :param x_offset: horizontal offset (default: 0)
  238. :param y_offset: vertical offset (default: 0)
  239. :param expand: whether to fully expand instead of shrinking to fit the content (default: ``False``, *added in version 2.1.0*)
  240. """
  241. super().__init__('q-page-sticky')
  242. self._props['position'] = position
  243. self._props['offset'] = [x_offset, y_offset]
  244. if expand:
  245. self._props['expand'] = True
  246. def _check_current_slot(element: Element) -> None:
  247. parent = context.slot.parent
  248. if parent != parent.client.content:
  249. raise RuntimeError(f'Found top level layout element "{element.__class__.__name__}" inside element "{parent.__class__.__name__}". '
  250. 'Top level layout elements can not be nested but must be direct children of the page content.')