diff --git a/examples/custom_vue_component/README.md b/examples/custom_vue_component/README.md new file mode 100644 index 000000000..820a26157 --- /dev/null +++ b/examples/custom_vue_component/README.md @@ -0,0 +1,21 @@ +# Custom Vue Component + +This example demonstrates how to create and use custom Vue components in NiceGUI. +One component is implemented using JavaScript, the other using Vue's Single-File Component (SFC) syntax. + +## Counter Component (counter.js) + +The `Counter` component is a simple counter that increments a value. +On change, it emits an event with the new value. +A reset method allows to reset the counter to 0. + +The JavaScript code in `counter.js` defines the front-end logic of the component using JavaScript. + +## OnOff Component (on_off.vue) + +The `OnOff` component is a simple toggle that switches between on and off. +On change, it emits an event with the new value. +A reset method is also provided to reset the toggle to off. + +The Single-File Component in `on_off.vue` defines the front-end logic of the component using Vue 2. +In contrast to the JavaScript code in `counter.js`, it splits the template, script, and style into separate sections. diff --git a/examples/custom_vue_component/counter.js b/examples/custom_vue_component/counter.js index 16b40fd16..f005a9920 100644 --- a/examples/custom_vue_component/counter.js +++ b/examples/custom_vue_component/counter.js @@ -1,9 +1,13 @@ // NOTE: Make sure to reload the browser with cache disabled after making changes to this file. export default { template: ` - `, + + `, + props: { + title: String, + }, data() { return { value: 0, @@ -18,7 +22,4 @@ export default { this.value = 0; }, }, - props: { - title: String, - }, }; diff --git a/examples/custom_vue_component/main.py b/examples/custom_vue_component/main.py index bd373eecf..f6b104629 100755 --- a/examples/custom_vue_component/main.py +++ b/examples/custom_vue_component/main.py @@ -1,17 +1,16 @@ #!/usr/bin/env python3 from counter import Counter +from on_off import OnOff from nicegui import ui -ui.markdown(''' -#### Try the new click counter! +with ui.row(align_items='center'): + counter = Counter('Count', on_change=lambda e: ui.notify(f'The value changed to {e.args}.')) + ui.button('Reset', on_click=counter.reset).props('outline') -Click to increment its value. -''') -with ui.card(): - counter = Counter('Clicks', on_change=lambda e: ui.notify(f'The value changed to {e.args}.')) +with ui.row(align_items='center'): + on_off = OnOff('State', on_change=lambda e: ui.notify(f'The value changed to {e.args}.')) + ui.button('Reset', on_click=on_off.reset).props('outline') -ui.button('Reset', on_click=counter.reset).props('small outline') - -ui.run() +ui.run(uvicorn_reload_includes='*.py,*.js,*.vue') diff --git a/examples/custom_vue_component/on_off.py b/examples/custom_vue_component/on_off.py new file mode 100644 index 000000000..90328b8a8 --- /dev/null +++ b/examples/custom_vue_component/on_off.py @@ -0,0 +1,14 @@ +from typing import Callable, Optional + +from nicegui.element import Element + + +class OnOff(Element, component='on_off.vue'): + + def __init__(self, title: str, *, on_change: Optional[Callable] = None) -> None: + super().__init__() + self._props['title'] = title + self.on('change', on_change) + + def reset(self) -> None: + self.run_method('reset') diff --git a/examples/custom_vue_component/on_off.vue b/examples/custom_vue_component/on_off.vue new file mode 100644 index 000000000..f5d91b1d5 --- /dev/null +++ b/examples/custom_vue_component/on_off.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/nicegui/elements/keyboard.js b/nicegui/elements/keyboard.js index ee9257e5e..f86c1bff4 100644 --- a/nicegui/elements/keyboard.js +++ b/nicegui/elements/keyboard.js @@ -2,10 +2,15 @@ export default { mounted() { for (const event of this.events) { document.addEventListener(event, (evt) => { + // https://github.com/zauberzeug/nicegui/issues/4290 + if (!(evt instanceof KeyboardEvent)) return; + // https://stackoverflow.com/a/36469636/3419103 const focus = document.activeElement; if (focus && this.ignore.includes(focus.tagName.toLowerCase())) return; + if (evt.repeat && !this.repeating) return; + this.$emit("key", { action: event, altKey: evt.altKey, diff --git a/nicegui/elements/leaflet.js b/nicegui/elements/leaflet.js index 1d554143f..cb1f3cb16 100644 --- a/nicegui/elements/leaflet.js +++ b/nicegui/elements/leaflet.js @@ -10,12 +10,14 @@ export default { draw_control: Object, resource_path: String, hide_drawn_items: Boolean, + additional_resources: Array, }, async mounted() { await this.$nextTick(); // NOTE: wait for window.path_prefix to be set await Promise.all([ loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.css`), loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.js`), + ...this.additional_resources.map((resource) => loadResource(resource)), ]); if (this.draw_control) { await Promise.all([ diff --git a/nicegui/elements/leaflet.py b/nicegui/elements/leaflet.py index c779d2041..5cf41e56a 100644 --- a/nicegui/elements/leaflet.py +++ b/nicegui/elements/leaflet.py @@ -1,6 +1,6 @@ import asyncio from pathlib import Path -from typing import Any, Dict, List, Tuple, Union, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast from typing_extensions import Self @@ -27,6 +27,7 @@ def __init__(self, options: Dict = {}, # noqa: B006 draw_control: Union[bool, Dict] = False, hide_drawn_items: bool = False, + additional_resources: Optional[List[str]] = None, ) -> None: """Leaflet map @@ -37,6 +38,7 @@ def __init__(self, :param draw_control: whether to show the draw toolbar (default: False) :param options: additional options passed to the Leaflet map (default: {}) :param hide_drawn_items: whether to hide drawn items on the map (default: False, *added in version 2.0.0*) + :param additional_resources: additional resources like CSS or JS files to load (default: None) """ super().__init__() self.add_resource(Path(__file__).parent / 'lib' / 'leaflet') @@ -51,6 +53,7 @@ def __init__(self, self._props['options'] = {**options} self._props['draw_control'] = draw_control self._props['hide_drawn_items'] = hide_drawn_items + self._props['additional_resources'] = additional_resources or [] self.on('init', self._handle_init) self.on('map-moveend', self._handle_moveend) diff --git a/poetry.lock b/poetry.lock index 95875eecb..6ea44956c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -281,13 +281,13 @@ files = [ [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] diff --git a/website/documentation/content/leaflet_documentation.py b/website/documentation/content/leaflet_documentation.py index d41b8e1f5..94b01c273 100644 --- a/website/documentation/content/leaflet_documentation.py +++ b/website/documentation/content/leaflet_documentation.py @@ -196,4 +196,18 @@ async def page(): m.run_map_method('fitBounds', [[bounds['_southWest'], bounds['_northEast']]]) ui.timer(0, page, once=True) # HIDE + +@doc.demo('Leaflet Plugins', ''' + You can add plugins to the map by passing the URLs of JS and CSS files to the `additional_resources` parameter. + This demo shows how to add the [Leaflet.RotatedMarker](https://github.com/bbecquet/Leaflet.RotatedMarker) plugin. + It allows you to rotate markers by a given `rotationAngle`. +''') +def leaflet_plugins() -> None: + m = ui.leaflet((51.51, -0.09), additional_resources=[ + 'https://unpkg.com/leaflet-rotatedmarker@0.2.0/leaflet.rotatedMarker.js', + ]) + m.marker(latlng=(51.51, -0.091), options={'rotationAngle': -30}) + m.marker(latlng=(51.51, -0.090), options={'rotationAngle': 30}) + + doc.reference(ui.leaflet)