Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Evented base class #1959

Merged
merged 9 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Utilities
---------------------

.. autoclass:: folium.utilities.JsCode
.. autoclass:: folium.elements.EventHandler


Plugins
Expand Down
76 changes: 76 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,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."""

Expand Down
6 changes: 3 additions & 3 deletions folium/folium.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(<Object> 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.
Expand Down
19 changes: 19 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down