Skip to content

Commit

Permalink
Add EventHandler to layer object
Browse files Browse the repository at this point in the history
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 python-visualization#1866 by @yschopfer19.
The PR was not accepted yet, because of concerns with code
duplication. In the approach taken in the current PR, python-visualization#1866 would
not be necessary anymore, as the requested changes could be added
completely in client code space.
  • Loading branch information
hansthen committed May 18, 2024
1 parent 5086929 commit ebc34c1
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 2 deletions.
83 changes: 83 additions & 0 deletions folium/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""

Expand Down
4 changes: 2 additions & 2 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import folium
from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup
from folium.utilities import JsCode


@pytest.fixture
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit ebc34c1

Please sign in to comment.