Rodja Trappe преди 2 години
родител
ревизия
ac1a0948cd
променени са 5 файла, в които са добавени 138 реда и са изтрити 41 реда
  1. 79 0
      nicegui/elements/tabs.py
  2. 4 0
      nicegui/ui.py
  3. 20 0
      tests/test_tabs.py
  4. 15 41
      website/example.py
  5. 20 0
      website/reference.py

+ 79 - 0
nicegui/elements/tabs.py

@@ -0,0 +1,79 @@
+from typing import Any, Callable, Optional
+
+from .. import globals
+from ..element import Element
+from .mixins.value_element import ValueElement
+
+
+class Tabs(ValueElement):
+
+    def __init__(self, *,
+                 value: Any = None,
+                 on_change: Optional[Callable] = None) -> None:
+        """Tabs
+
+        This element represents `Quasar's QTabs <https://quasar.dev/vue-components/tabs#qtabs-api>`_ component.
+        It contains individual tabs.
+
+        :param value: name of the tab to be initially selected
+        :param on_change: callback to be executed when the selected tab changes
+        """
+        super().__init__(tag='q-tabs', value=value, on_value_change=on_change)
+        self.panels: Optional[TabPanels] = None
+
+
+class Tab(Element):
+
+    def __init__(self, name: str, label: Optional[str] = None, icon: Optional[str] = None) -> None:
+        """Tab
+
+        This element represents `Quasar's QTab <https://quasar.dev/vue-components/tabs#qtab-api>`_ component.
+        It is a child of a `Tabs` element.
+
+        :param name: name of the tab (the value of the `Tabs` element)
+        :param label: label of the tab (default: `None`, meaning the same as `name`)
+        :param icon: icon of the tab (default: `None`)
+        """
+        super().__init__('q-tab')
+        self._props['name'] = name
+        self._props['label'] = label if label is not None else name
+        if icon:
+            self._props['icon'] = icon
+        self.tabs = globals.get_slot().parent
+
+
+class TabPanels(ValueElement):
+
+    def __init__(self,
+                 tabs: Tabs, *,
+                 value: Any = None,
+                 on_change: Optional[Callable] = None,
+                 animated: bool = True,
+                 ) -> None:
+        """Tab Panels
+
+        This element represents `Quasar's QTabPanels <https://quasar.dev/vue-components/tab-panels#qtabpanels-api>`_ component.
+        It contains individual tab panels.
+
+        :param tabs: the `Tabs` element that controls this element
+        :param value: name of the tab panel to be initially visible
+        :param on_change: callback to be executed when the visible tab panel changes
+        :param animated: whether the tab panels should be animated (default: `True`)
+        """
+        super().__init__(tag='q-tab-panels', value=value, on_value_change=on_change)
+        tabs.bind_value(self, 'value')
+        self._props['animated'] = animated
+
+
+class TabPanel(Element):
+
+    def __init__(self, name: str) -> None:
+        """Tab Panel
+
+        This element represents `Quasar's QTabPanel <https://quasar.dev/vue-components/tab-panels#qtabpanel-api>`_ component.
+        It is a child of a `TabPanels` element.
+
+        :param name: name of the tab panel (the value of the `TabPanels` element)
+        """
+        super().__init__('q-tab-panel')
+        self._props['name'] = name

+ 4 - 0
nicegui/ui.py

@@ -44,6 +44,10 @@ from .elements.slider import Slider as slider
 from .elements.spinner import Spinner as spinner
 from .elements.switch import Switch as switch
 from .elements.table import Table as table
+from .elements.tabs import Tab as tab
+from .elements.tabs import TabPanel as tab_panel
+from .elements.tabs import TabPanels as tab_panels
+from .elements.tabs import Tabs as tabs
 from .elements.time import Time as time
 from .elements.toggle import Toggle as toggle
 from .elements.tooltip import Tooltip as tooltip

+ 20 - 0
tests/test_tabs.py

@@ -0,0 +1,20 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_tabs(screen: Screen):
+    with ui.tabs() as tabs:
+        ui.tab('One')
+        ui.tab('Two')
+
+    with ui.tab_panels(tabs, value='One'):
+        with ui.tab_panel('One'):
+            ui.label('First tab')
+        with ui.tab_panel('Two'):
+            ui.label('Second tab')
+
+    screen.open('/')
+    screen.should_contain('First tab')
+    screen.click('Two')
+    screen.should_contain('Second tab')

+ 15 - 41
website/example.py

@@ -7,7 +7,7 @@ import docutils.core
 import isort
 
 from nicegui import ui
-from nicegui.elements.markdown import apply_tailwind
+from nicegui.elements.markdown import apply_tailwind, prepare_content
 
 from .intersection_observer import IntersectionObserver as intersection_observer
 
@@ -40,8 +40,7 @@ class example:
     def __call__(self, f: Callable) -> Callable:
         with ui.column().classes('w-full mb-8'):
             if isinstance(self.content, str):
-                documentation = ui.markdown(self.content)
-                _add_markdown_anchor(documentation, self.menu)
+                html = prepare_content(self.content, 'fenced-code-blocks tables')
             else:
                 doc = self.content.__doc__ or self.content.__init__.__doc__
                 html: str = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
@@ -49,8 +48,19 @@ class example:
                 html = html.replace('</p>', '</h4>', 1)
                 html = html.replace('param ', '')
                 html = apply_tailwind(html)
-                documentation = ui.html(html)
-                _add_html_anchor(documentation.classes('documentation bold-links arrow-links'), self.menu)
+
+            match = REGEX_H4.search(html)
+            headline = match.groups()[0].strip()
+            headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
+            icon = '<span class="material-icons">link</span>'
+            link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
+            target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
+            html = html.replace('<h4', f'{target}<h4', 1)
+            html = html.replace('</h4>', f' {link}</h4>', 1)
+
+            ui.html(html).classes('documentation bold-links arrow-links')
+            with self.menu or contextlib.nullcontext():
+                ui.link(headline, f'#{headline_id}')
 
             with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
                 code = inspect.getsource(f).split('# END OF EXAMPLE')[0].strip().splitlines()
@@ -74,42 +84,6 @@ class example:
         return f
 
 
-def _add_markdown_anchor(element: ui.markdown, menu: Optional[ui.element]) -> None:
-    first_line, _ = element.content.split('\n', 1)
-    assert first_line.startswith('#### ')
-    headline = first_line[5:].strip()
-    headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
-    icon = '<span class="material-icons">link</span>'
-    link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
-    target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
-    title = f'{target}<h4>{headline} {link}</h4>'
-    element.content = title + '\n' + element.content.split('\n', 1)[1]
-
-    with menu or contextlib.nullcontext():
-        ui.link(headline, f'#{headline_id}')
-
-
-def _add_html_anchor(element: ui.html, menu: Optional[ui.element]) -> None:
-    html = element.content
-    match = REGEX_H4.search(html)
-    if not match:
-        return
-    headline = match.groups()[0].strip()
-    headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
-    if not headline_id:
-        return
-
-    icon = '<span class="material-icons">link</span>'
-    link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
-    target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
-    html = html.replace('<h4', f'{target}<h4', 1)
-    html = html.replace('</h4>', f' {link}</h4>', 1)
-    element.content = html
-
-    with menu or contextlib.nullcontext():
-        ui.link(headline, f'#{headline_id}')
-
-
 def _window_header(bgcolor: str) -> ui.row():
     return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
 

+ 20 - 0
website/reference.py

@@ -438,6 +438,26 @@ Alternatively, you can remove individual elements with `remove(element)`, where
         with ui.expansion('Expand!', icon='work').classes('w-full'):
             ui.label('inside the expansion')
 
+    @example('''#### Tabs
+
+The elements `ui.tabs`, `ui.tab`, `ui.tab_panels`, and `ui.tab_panel` resemble
+[Quasar's tabs](https://quasar.dev/vue-components/tabs)
+and [tab panels](https://quasar.dev/vue-components/tab-panels) API.
+
+`ui.tabs` creates a container for the tabs. This could be placed in a `ui.header` for example.
+`ui.tab_panels` creates a container for the tab panels with the actual content.
+''', menu)
+    def tabs_example():
+        with ui.tabs() as tabs:
+            ui.tab('Home', icon='home')
+            ui.tab('About', icon='info')
+
+        with ui.tab_panels(tabs, value='Home'):
+            with ui.tab_panel('Home'):
+                ui.label('This is the first tab')
+            with ui.tab_panel('About'):
+                ui.label('This is the second tab')
+
     @example(ui.menu, menu)
     def menu_example():
         choice = ui.label('Try the menu.')