From ebc34c17cae611df33f7dc3eb4139346b6bf3a93 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 14 Mar 2024 11:39:55 +0100 Subject: [PATCH 1/5] Add EventHandler to layer object In combination with JsCode this makes it easier for users to add `on` method calls for event handling without extending Folium itself. The functionality was inspired by PR #1866 by @yschopfer19. The PR was not accepted yet, because of concerns with code duplication. In the approach taken in the current PR, #1866 would not be necessary anymore, as the requested changes could be added completely in client code space. --- folium/elements.py | 83 ++++++++++++++++++++++++++++++++++++++++++ folium/map.py | 4 +- tests/test_features.py | 18 +++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/folium/elements.py b/folium/elements.py index 9c41e66fab..56965b449c 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -3,6 +3,8 @@ from branca.element import CssLink, Element, Figure, JavascriptLink, MacroElement from jinja2 import Template +from folium.utilities import JsCode + class JSCSSMixin(Element): """Render links to external Javascript and CSS resources.""" @@ -46,6 +48,87 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): default_list.append((name, url)) +class EventTargetMixin(Element): + '''Add Event Handlers to an element. + + Examples + -------- + >>> import folium + >>> from folium.utilities import JsCode + + >>> m = folium.Map() + + >>> geo_json_data = { + ... "type": "FeatureCollection", + ... "features": [ + ... { + ... "type": "Feature", + ... "geometry": { + ... "type": "Polygon", + ... "coordinates": [ + ... [ + ... [100.0, 0.0], + ... [101.0, 0.0], + ... [101.0, 1.0], + ... [100.0, 1.0], + ... [100.0, 0.0], + ... ] + ... ], + ... }, + ... "properties": {"prop1": {"title": "Somewhere on Sumatra"}}, + ... } + ... ], + ... } + + >>> g = folium.GeoJson(geo_json_data).add_to(m) + >>> highlight = JsCode( + ... """ + ... function highlight(e) { + ... e.target.original_color = e.layer.options.color; + ... e.target.setStyle({ color: "green" }); + ... } + ... """ + ... ) + >>> reset = JsCode( + ... """ + ... function reset(e) { + ... e.target.setStyle({ color: e.target.original_color }); + ... } + ... """ + ... ) + >>> g.on(mouseover=highlight, mouseout=reset) + ''' + + def on(self, **kwargs: JsCode): + for event, handler in kwargs.items(): + self.add_child(EventHandler(event, handler)) + return self + + def render(self, **kwargs) -> None: + super().render(**kwargs) + + +class EventHandler(MacroElement): + """Render Event Handlers.""" + + _template = Template( + """ + {% macro script(this, kwargs) %} + {{ this._parent.get_name()}}.on( + {{ this.event|tojson}}, + {{ this.handler.js_code }} + ); + {% endmacro %} + """ + ) + + def __init__(self, event: str, handler: JsCode): + super().__init__() + self._name = "EventHandler" + self.event = event + self.handler = handler + + class ElementAddToElement(MacroElement): """Abstract class to add an element to another element.""" diff --git a/folium/map.py b/folium/map.py index 01ac7d2ede..fb00ee72d5 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,7 +10,7 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement +from folium.elements import ElementAddToElement, EventTargetMixin from folium.utilities import ( TypeBounds, TypeJsonValue, @@ -21,7 +21,7 @@ ) -class Layer(MacroElement): +class Layer(EventTargetMixin, MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. diff --git a/tests/test_features.py b/tests/test_features.py index c879ad119c..8dd51d61b7 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -13,6 +13,7 @@ import folium from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup +from folium.utilities import JsCode @pytest.fixture @@ -283,6 +284,23 @@ def test_geojson_empty_features_with_styling(): m.get_root().render() +def test_geojson_event_handler(): + """Test that event handlers are properly generated""" + m = Map() + data = {"type": "FeatureCollection", "features": []} + geojson = GeoJson(data, style_function=lambda x: {}).add_to(m) + fn = JsCode( + """ + function f(e) { + console.log("only for testing") + } + """ + ) + geojson.on(mouseover=fn) + rendered = m.get_root().render() + assert fn.js_code in rendered + + def test_geometry_collection_get_bounds(): """Assert #1599 is fixed""" geojson_data = { From 934ab68a50833c4d544bdf5816615db0957400b1 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Thu, 25 Apr 2024 22:41:00 +0200 Subject: [PATCH 2/5] Make realtime inherit from Layer --- docs/reference.rst | 1 + folium/elements.py | 9 ++++++--- folium/plugins/realtime.py | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 5ed5f7c323..bb4740d206 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -35,6 +35,7 @@ Utilities --------------------- .. autoclass:: folium.utilities.JsCode +.. autoclass:: folium.elements.EventTargetMixin Plugins diff --git a/folium/elements.py b/folium/elements.py index 56965b449c..d6f04dfe3b 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -55,9 +55,9 @@ class EventTargetMixin(Element): -------- >>> import folium >>> from folium.utilities import JsCode - + >>> >>> m = folium.Map() - + >>> >>> geo_json_data = { ... "type": "FeatureCollection", ... "features": [ @@ -79,8 +79,9 @@ class EventTargetMixin(Element): ... } ... ], ... } - + >>> >>> g = folium.GeoJson(geo_json_data).add_to(m) + >>> >>> highlight = JsCode( ... """ ... function highlight(e) { @@ -89,6 +90,7 @@ class EventTargetMixin(Element): ... } ... """ ... ) + >>> >>> reset = JsCode( ... """ ... function reset(e) { @@ -96,6 +98,7 @@ class EventTargetMixin(Element): ... } ... """ ... ) + >>> >>> g.on(mouseover=highlight, mouseout=reset) ''' diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index d7f99594da..145623973b 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,6 +1,5 @@ from typing import Optional, Union -from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin @@ -8,7 +7,7 @@ from folium.utilities import JsCode, camelize, parse_options -class Realtime(JSCSSMixin, MacroElement): +class Realtime(JSCSSMixin, Layer): """Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. From 1d1511b6983b5e068b73782fd869840232383db4 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 May 2024 16:37:40 +0200 Subject: [PATCH 3/5] Changes after review comments by conengmo --- docs/reference.rst | 2 +- folium/elements.py | 32 ++++++++++---------------------- folium/map.py | 4 ++-- tests/test_features.py | 3 ++- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index bb4740d206..da20b33775 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -35,7 +35,7 @@ Utilities --------------------- .. autoclass:: folium.utilities.JsCode -.. autoclass:: folium.elements.EventTargetMixin +.. autoclass:: folium.elements.EventHandler Plugins diff --git a/folium/elements.py b/folium/elements.py index d6f04dfe3b..d017f7589a 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -48,9 +48,8 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): default_list.append((name, url)) -class EventTargetMixin(Element): - '''Add Event Handlers to an element. - +class EventHandler(MacroElement): + ''' Examples -------- >>> import folium @@ -85,35 +84,24 @@ class EventTargetMixin(Element): >>> highlight = JsCode( ... """ ... function highlight(e) { - ... e.target.original_color = e.layer.options.color; - ... e.target.setStyle({ color: "green" }); - ... } + ... e.target.original_color = e.layer.options.color; + ... e.target.setStyle({ color: "green" }); + ... } ... """ ... ) >>> >>> reset = JsCode( ... """ - ... function reset(e) { - ... e.target.setStyle({ color: e.target.original_color }); - ... } + ... function reset(e) { + ... e.target.setStyle({ color: e.target.original_color }); + ... } ... """ ... ) >>> - >>> g.on(mouseover=highlight, mouseout=reset) + >>> g.add_child(EventHandler("mouseover", highlight)) + >>> g.add_child(EventHandler("mouseout", reset)) ''' - def on(self, **kwargs: JsCode): - for event, handler in kwargs.items(): - self.add_child(EventHandler(event, handler)) - return self - - def render(self, **kwargs) -> None: - super().render(**kwargs) - - -class EventHandler(MacroElement): - """Render Event Handlers.""" - _template = Template( """ {% macro script(this, kwargs) %} diff --git a/folium/map.py b/folium/map.py index fb00ee72d5..01ac7d2ede 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,7 +10,7 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement, EventTargetMixin +from folium.elements import ElementAddToElement from folium.utilities import ( TypeBounds, TypeJsonValue, @@ -21,7 +21,7 @@ ) -class Layer(EventTargetMixin, MacroElement): +class Layer(MacroElement): """An abstract class for everything that is a Layer on the map. It will be used to define whether an object will be included in LayerControls. diff --git a/tests/test_features.py b/tests/test_features.py index 8dd51d61b7..94d27d329b 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -13,6 +13,7 @@ import folium from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup +from folium.elements import EventHandler from folium.utilities import JsCode @@ -296,7 +297,7 @@ def test_geojson_event_handler(): } """ ) - geojson.on(mouseover=fn) + geojson.add_child(EventHandler("mouseover", fn)) rendered = m.get_root().render() assert fn.js_code in rendered From 5425f6d4432ef582ee3336cc160ef881189a5dfa Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 18 May 2024 17:03:14 +0200 Subject: [PATCH 4/5] Updates after review comments --- folium/plugins/realtime.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/folium/plugins/realtime.py b/folium/plugins/realtime.py index 145623973b..d7f99594da 100644 --- a/folium/plugins/realtime.py +++ b/folium/plugins/realtime.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from branca.element import MacroElement from jinja2 import Template from folium.elements import JSCSSMixin @@ -7,7 +8,7 @@ from folium.utilities import JsCode, camelize, parse_options -class Realtime(JSCSSMixin, Layer): +class Realtime(JSCSSMixin, MacroElement): """Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything. From 6690c5275b40b16c5eecc8f7f661b98f2ffb8df2 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Fri, 24 May 2024 18:42:52 +0200 Subject: [PATCH 5/5] Add extra docstring line --- folium/elements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/folium/elements.py b/folium/elements.py index d017f7589a..3d44985175 100644 --- a/folium/elements.py +++ b/folium/elements.py @@ -50,6 +50,8 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): class EventHandler(MacroElement): ''' + Add javascript event handlers. + Examples -------- >>> import folium