From 9fb5f2a31968a6135dafb9f0bce1d9fdf039b322 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 2 Jun 2024 22:19:12 +0200 Subject: [PATCH] Add Evented base class (#1959) * 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. * Make realtime inherit from Layer * Changes after review comments by conengmo * Updates after review comments * Add extra docstring line * Add Evented class In Leaflet Evented is the parent class of both `L.Map` and `L.Layer`. It adds the `on` method which can be used to add event handlers to a leaflet object. * Update folium/map.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> * Update folium/map.py Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> * As requested in review comment --------- Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com> --- docs/reference.rst | 1 + folium/elements.py | 76 ++++++++++++++++++++++++++++++++++++++++++ folium/folium.py | 6 ++-- folium/map.py | 21 ++++++++++-- tests/test_features.py | 19 +++++++++++ 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 5ed5f7c32..da20b3377 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -35,6 +35,7 @@ Utilities --------------------- .. autoclass:: folium.utilities.JsCode +.. autoclass:: folium.elements.EventHandler Plugins diff --git a/folium/elements.py b/folium/elements.py index 9c41e66fa..3d4498517 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,80 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]): default_list.append((name, url)) +class EventHandler(MacroElement): + ''' + Add javascript event handlers. + + 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.add_child(EventHandler("mouseover", highlight)) + >>> g.add_child(EventHandler("mouseout", reset)) + ''' + + _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/folium.py b/folium/folium.py index 6eb557685..0b7b23887 100644 --- a/folium/folium.py +++ b/folium/folium.py @@ -7,11 +7,11 @@ import webbrowser from typing import Any, List, Optional, Sequence, Union -from branca.element import Element, Figure, MacroElement +from branca.element import Element, Figure from jinja2 import Template from folium.elements import JSCSSMixin -from folium.map import FitBounds, Layer +from folium.map import Evented, FitBounds, Layer from folium.raster_layers import TileLayer from folium.utilities import ( TypeBounds, @@ -79,7 +79,7 @@ def __init__(self, no_touch=False, disable_3d=False): self.disable_3d = disable_3d -class Map(JSCSSMixin, MacroElement): +class Map(JSCSSMixin, Evented): """Create a Map with Folium and Leaflet.js Generate a base map of given width and height with either default diff --git a/folium/map.py b/folium/map.py index 01ac7d2ed..26a333ca1 100644 --- a/folium/map.py +++ b/folium/map.py @@ -10,8 +10,9 @@ from branca.element import Element, Figure, Html, MacroElement from jinja2 import Template -from folium.elements import ElementAddToElement +from folium.elements import ElementAddToElement, EventHandler from folium.utilities import ( + JsCode, TypeBounds, TypeJsonValue, camelize, @@ -21,7 +22,23 @@ ) -class Layer(MacroElement): +class Evented(MacroElement): + """The base class for Layer and Map + + Adds the `on` method for event handling capabilities. + + See https://leafletjs.com/reference.html#evented for + more in depth documentation. Please note that we have + only added the `on( eventMap)` variant of this + method using python keyword arguments. + """ + + def on(self, **event_map: JsCode): + for event_type, handler in event_map.items(): + self.add_child(EventHandler(event_type, handler)) + + +class Layer(Evented): """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 c879ad119..94d27d329 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -13,6 +13,8 @@ import folium from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup +from folium.elements import EventHandler +from folium.utilities import JsCode @pytest.fixture @@ -283,6 +285,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.add_child(EventHandler("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 = {