Sfoglia il codice sorgente

Merge branch 'background'

# Conflicts:
#	website/documentation.py
Falko Schindler 2 anni fa
parent
commit
5455de5c8d

+ 11 - 9
main.py

@@ -314,27 +314,29 @@ def documentation_page():
 
 
 @ui.page('/documentation/{name}')
 @ui.page('/documentation/{name}')
 def documentation_page_more(name: str):
 def documentation_page_more(name: str):
+    if not hasattr(ui, name):
+        name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
+    module = importlib.import_module(f'website.more_documentation.{name}_documentation')
+    api = getattr(ui, name)
+    more = getattr(module, 'more', None)
+    back_link_target = str(api.__doc__ or api.__init__.__doc__).splitlines()[0].strip()
+
     add_head_html()
     add_head_html()
     add_header()
     add_header()
     with side_menu() as menu:
     with side_menu() as menu:
-        ui.markdown(f'[← back](/documentation#{create_anchor_name(name)})').classes('bold-links')
+        ui.markdown(f'[← back](/documentation#{create_anchor_name(back_link_target)})').classes('bold-links')
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
-        if not hasattr(ui, name):
-            name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
         section_heading('Documentation', f'ui.*{name}*')
         section_heading('Documentation', f'ui.*{name}*')
-        module = importlib.import_module(f'website.more_documentation.{name}_documentation')
-        element_class = getattr(ui, name)
-        more = getattr(module, 'more', None)
         with menu:
         with menu:
             ui.markdown('**Demos**' if more else '**Demo**').classes('mt-4')
             ui.markdown('**Demos**' if more else '**Demo**').classes('mt-4')
-        element_demo(element_class)(getattr(module, 'main_demo'))
+        element_demo(api)(getattr(module, 'main_demo'))
         if more:
         if more:
             more()
             more()
-        if inspect.isclass(element_class):
+        if inspect.isclass(api):
             with menu:
             with menu:
                 ui.markdown('**Reference**').classes('mt-4')
                 ui.markdown('**Reference**').classes('mt-4')
             ui.markdown('## Reference').classes('mt-16')
             ui.markdown('## Reference').classes('mt-16')
-            generate_class_doc(element_class)
+            generate_class_doc(api)
 
 
 
 
 ui.run(uvicorn_reload_includes='*.py, *.css, *.html')
 ui.run(uvicorn_reload_includes='*.py, *.css, *.html')

+ 13 - 8
nicegui/element.py

@@ -93,6 +93,16 @@ class Element(Visibility):
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
         }
         }
 
 
+    @staticmethod
+    def _update_classes_list(
+            classes: List[str],
+            add: Optional[str] = None, remove: Optional[str] = None, replace: Optional[str] = None) -> List[str]:
+        class_list = classes if replace is None else []
+        class_list = [c for c in class_list if c not in (remove or '').split()]
+        class_list += (add or '').split()
+        class_list += (replace or '').split()
+        return list(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
             -> Self:
             -> Self:
         """Apply, remove, or replace HTML classes.
         """Apply, remove, or replace HTML classes.
@@ -105,11 +115,7 @@ class Element(Visibility):
         :param remove: whitespace-delimited string of classes to remove from the element
         :param remove: whitespace-delimited string of classes to remove from the element
         :param replace: whitespace-delimited string of classes to use instead of existing ones
         :param replace: whitespace-delimited string of classes to use instead of existing ones
         """
         """
-        class_list = self._classes if replace is None else []
-        class_list = [c for c in class_list if c not in (remove or '').split()]
-        class_list += (add or '').split()
-        class_list += (replace or '').split()
-        new_classes = list(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+        new_classes = self._update_classes_list(self._classes, add, remove, replace)
         if self._classes != new_classes:
         if self._classes != new_classes:
             self._classes = new_classes
             self._classes = new_classes
             self.update()
             self.update()
@@ -133,11 +139,10 @@ class Element(Visibility):
         :param add: semicolon-separated list of styles to add to the element
         :param add: semicolon-separated list of styles to add to the element
         :param remove: semicolon-separated list of styles to remove from the element
         :param remove: semicolon-separated list of styles to remove from the element
         :param replace: semicolon-separated list of styles to use instead of existing ones
         :param replace: semicolon-separated list of styles to use instead of existing ones
-         """
+        """
         style_dict = deepcopy(self._style) if replace is None else {}
         style_dict = deepcopy(self._style) if replace is None else {}
         for key in self._parse_style(remove):
         for key in self._parse_style(remove):
-            if key in style_dict:
-                del style_dict[key]
+            style_dict.pop(key, None)
         style_dict.update(self._parse_style(add))
         style_dict.update(self._parse_style(add))
         style_dict.update(self._parse_style(replace))
         style_dict.update(self._parse_style(replace))
         if self._style != style_dict:
         if self._style != style_dict:

+ 37 - 0
nicegui/elements/query.js

@@ -0,0 +1,37 @@
+export default {
+  mounted() {
+    this.add_classes(this.classes);
+    this.add_style(this.style);
+    this.add_props(this.props);
+  },
+  methods: {
+    add_classes(classes) {
+      document.querySelectorAll(this.selector).forEach((e) => e.classList.add(...classes));
+    },
+    remove_classes(classes) {
+      document.querySelectorAll(this.selector).forEach((e) => e.classList.remove(...classes));
+    },
+    add_style(style) {
+      Object.entries(style).forEach(([key, val]) =>
+        document.querySelectorAll(this.selector).forEach((e) => (e.style[key] = val))
+      );
+    },
+    remove_style(keys) {
+      keys.forEach((key) => document.querySelectorAll(this.selector).forEach((e) => e.style.removeProperty(key)));
+    },
+    add_props(props) {
+      Object.entries(props).forEach(([key, val]) =>
+        document.querySelectorAll(this.selector).forEach((e) => e.setAttribute(key, val))
+      );
+    },
+    remove_props(keys) {
+      keys.forEach((key) => document.querySelectorAll(this.selector).forEach((e) => e.removeAttribute(key)));
+    },
+  },
+  props: {
+    selector: String,
+    classes: Array,
+    style: Object,
+    props: Object,
+  },
+};

+ 69 - 0
nicegui/elements/query.py

@@ -0,0 +1,69 @@
+from typing import Optional
+
+from typing_extensions import Self
+
+from ..dependencies import register_component
+from ..element import Element
+from ..globals import get_client
+
+register_component('query', __file__, 'query.js')
+
+
+class Query(Element):
+
+    def __init__(self, selector: str) -> None:
+        super().__init__('query')
+        self._props['selector'] = selector
+        self._props['classes'] = []
+        self._props['style'] = {}
+        self._props['props'] = {}
+
+    def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
+            -> Self:
+        classes = self._update_classes_list(self._props['classes'], add, remove, replace)
+        new_classes = [c for c in classes if c not in self._props['classes']]
+        old_classes = [c for c in self._props['classes'] if c not in classes]
+        if new_classes:
+            self.run_method('add_classes', new_classes)
+        if old_classes:
+            self.run_method('remove_classes', old_classes)
+        self._props['classes'] = classes
+        return self
+
+    def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
+            -> Self:
+        old_style = Element._parse_style(remove)
+        for key in old_style:
+            self._props['style'].pop(key, None)
+        if old_style:
+            self.run_method('remove_style', list(old_style))
+        self._props['style'].update(Element._parse_style(add))
+        self._props['style'].update(Element._parse_style(replace))
+        if self._props['style']:
+            self.run_method('add_style', self._props['style'])
+        return self
+
+    def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:
+        old_props = self._parse_props(remove)
+        for key in old_props:
+            self._props['props'].pop(key, None)
+        if old_props:
+            self.run_method('remove_props', list(old_props))
+        new_props = self._parse_props(add)
+        self._props['props'].update(new_props)
+        if self._props['props']:
+            self.run_method('add_props', self._props['props'])
+        return self
+
+
+def query(selector: str) -> Query:
+    """Query Selector
+
+    To manipulate elements like the document body, you can use the `ui.query` function.
+    With the query result you can add classes, styles, and attributes like with every other UI element.
+    This can be useful for example to change the background color of the page (e.g. `ui.query('body').classes('bg-green')`).
+    """
+    for element in get_client().elements.values():
+        if isinstance(element, Query) and element._props['selector'] == selector:
+            return element
+    return Query(selector)

+ 1 - 0
nicegui/ui.py

@@ -39,6 +39,7 @@ from .elements.number import Number as number
 from .elements.plotly import Plotly as plotly
 from .elements.plotly import Plotly as plotly
 from .elements.progress import CircularProgress as circular_progress
 from .elements.progress import CircularProgress as circular_progress
 from .elements.progress import LinearProgress as linear_progress
 from .elements.progress import LinearProgress as linear_progress
+from .elements.query import query
 from .elements.radio import Radio as radio
 from .elements.radio import Radio as radio
 from .elements.row import Row as row
 from .elements.row import Row as row
 from .elements.scene import Scene as scene
 from .elements.scene import Scene as scene

+ 54 - 0
tests/test_query.py

@@ -0,0 +1,54 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_query_body(screen: Screen):
+    ui.label('Hello')
+    ui.query('body').classes('bg-orange-100')
+    ui.button('Red background', on_click=lambda: ui.query('body').classes(replace='bg-red-100'))
+    ui.button('Blue background', on_click=lambda: ui.query('body').classes(replace='bg-blue-100'))
+    ui.button('Small padding', on_click=lambda: ui.query('body').style('padding: 1px'))
+    ui.button('Large padding', on_click=lambda: ui.query('body').style('padding: 10px'))
+    ui.button('Data X = 1', on_click=lambda: ui.query('body').props('data-x=1'))
+    ui.button('Data X = 2', on_click=lambda: ui.query('body').props('data-x=2'))
+
+    screen.open('/')
+    screen.should_contain('Hello')
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-orange-100'
+
+    screen.click('Red background')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-red-100'
+
+    screen.click('Blue background')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-blue-100'
+
+    screen.click('Small padding')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').value_of_css_property('padding') == '1px'
+
+    screen.click('Large padding')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').value_of_css_property('padding') == '10px'
+
+    screen.click('Data X = 1')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('data-x') == '1'
+
+    screen.click('Data X = 2')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('data-x') == '2'
+
+
+def test_query_multiple_divs(screen: Screen):
+    ui.label('A')
+    ui.label('B')
+    ui.button('Add border', on_click=lambda: ui.query('div').style('border: 1px solid black'))
+
+    screen.open('/')
+    screen.click('Add border')
+    screen.wait(0.5)
+    assert screen.find('A').value_of_css_property('border') == '1px solid rgb(0, 0, 0)'
+    assert screen.find('B').value_of_css_property('border') == '1px solid rgb(0, 0, 0)'

+ 1 - 0
website/documentation.py

@@ -256,6 +256,7 @@ def create_full() -> None:
         red_style.apply(label_c)
         red_style.apply(label_c)
         ui.label('Label D').tailwind(red_style)
         ui.label('Label D').tailwind(red_style)
 
 
+    load_demo(ui.query)
     load_demo(ui.colors)
     load_demo(ui.colors)
 
 
     heading('Action')
     heading('Action')

+ 3 - 3
website/documentation_tools.py

@@ -98,13 +98,13 @@ class element_demo:
             return demo(browser_title=self.browser_title)(f)
             return demo(browser_title=self.browser_title)(f)
 
 
 
 
-def load_demo(element_class: type) -> None:
-    name = pascal_to_snake(element_class.__name__)
+def load_demo(api: Union[type, Callable]) -> None:
+    name = pascal_to_snake(api.__name__)
     try:
     try:
         module = importlib.import_module(f'website.more_documentation.{name}_documentation')
         module = importlib.import_module(f'website.more_documentation.{name}_documentation')
     except ModuleNotFoundError:
     except ModuleNotFoundError:
         module = importlib.import_module(f'website.more_documentation.{name.replace("_", "")}_documentation')
         module = importlib.import_module(f'website.more_documentation.{name.replace("_", "")}_documentation')
-    element_demo(element_class)(getattr(module, 'main_demo'), more_link=name)
+    element_demo(api)(getattr(module, 'main_demo'), more_link=name)
 
 
 
 
 def is_method_or_property(cls: type, attribute_name: str) -> bool:
 def is_method_or_property(cls: type, attribute_name: str) -> bool:

+ 12 - 0
website/more_documentation/query_documentation.py

@@ -0,0 +1,12 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    def set_background(color: str) -> None:
+        ui.query('body').style(f'background-color: {color}')
+
+    # ui.button('Blue', on_click=lambda: set_background(#ddeeff'))
+    # ui.button('Orange', on_click=lambda: set_background(#ffeedd'))
+    # END OF DEMO
+    ui.button('Blue', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ddeeff'))
+    ui.button('Orange', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ffeedd'))