Skip to content

Commit f482fb0

Browse files
Add export_position option to the Draw plugin (#2271)
The export button was pinned to the top-right corner with hard-coded CSS, so it could sit on top of other controls -- a LayerControl in the same corner, for example (#1806). Add an export_position parameter taking the four Leaflet corner names. It defaults to 'topright', which reproduces the previous placement exactly (top: 90px; right: 10px), so existing maps render unchanged. An unknown value raises a ValueError listing the valid corners. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent b972da7 commit f482fb0

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

folium/plugins/draw.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@
33
from folium.elements import JSCSSMixin
44
from folium.template import Template
55

6+
# Each corner maps to the pair of CSS edge offsets that pins the export button
7+
# there. "topright" reproduces the historical hard-coded placement exactly.
8+
_EXPORT_POSITION_CSS = {
9+
"topright": "top: 90px;\n right: 10px;",
10+
"topleft": "top: 90px;\n left: 10px;",
11+
"bottomright": "bottom: 20px;\n right: 10px;",
12+
"bottomleft": "bottom: 20px;\n left: 10px;",
13+
}
14+
15+
16+
def _export_position_to_css(export_position):
17+
try:
18+
return _EXPORT_POSITION_CSS[export_position]
19+
except KeyError:
20+
raise ValueError(
21+
"export_position must be one of "
22+
f"{sorted(_EXPORT_POSITION_CSS)}, not {export_position!r}"
23+
) from None
24+
625

726
class Draw(JSCSSMixin, MacroElement):
827
'''
@@ -20,6 +39,10 @@ class Draw(JSCSSMixin, MacroElement):
2039
position : {'topleft', 'toprigth', 'bottomleft', 'bottomright'}
2140
Position of control.
2241
See https://leafletjs.com/reference.html#control
42+
export_position : {'topright', 'topleft', 'bottomright', 'bottomleft'}
43+
Corner of the map to place the export button in, when ``export``
44+
is True. Defaults to 'topright'. Use this to keep the button clear
45+
of other controls such as a LayerControl.
2346
show_geometry_on_click : bool, default True
2447
When True, opens an alert with the geometry description on click.
2548
draw_options : dict, optional
@@ -60,7 +83,7 @@ class Draw(JSCSSMixin, MacroElement):
6083
<style>
6184
#export_{{ this.get_name() }} {
6285
position: absolute;
63-
right: 10px;
86+
{{ this.export_position_css }}
6487
z-index: 999;
6588
background: white;
6689
color: black;
@@ -70,7 +93,6 @@ class Draw(JSCSSMixin, MacroElement):
7093
cursor: pointer;
7194
font-size: 12px;
7295
text-decoration: none;
73-
top: 90px;
7496
}
7597
</style>
7698
<a href='#' id='export_{{ this.get_name() }}'>Export</a>
@@ -156,6 +178,7 @@ def __init__(
156178
feature_group=None,
157179
filename="data.geojson",
158180
position="topleft",
181+
export_position="topright",
159182
show_geometry_on_click=True,
160183
draw_options=None,
161184
edit_options=None,
@@ -167,6 +190,7 @@ def __init__(
167190
self.feature_group = feature_group
168191
self.filename = filename
169192
self.position = position
193+
self.export_position_css = _export_position_to_css(export_position)
170194
self.show_geometry_on_click = show_geometry_on_click
171195
self.draw_options = draw_options or {}
172196
self.edit_options = edit_options or {}

tests/plugins/test_draw.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import re
77

8+
import pytest
9+
810
import folium
911
from folium import plugins
1012
from folium.template import Template
@@ -68,3 +70,42 @@ def test_two_draw_controls_get_unique_export_ids():
6870
handler = out[start : out.index("}", start) + 1]
6971
assert f"drawnItems_{draw.get_name()}.toGeoJSON()" in handler
7072
assert filename in handler
73+
74+
75+
def test_draw_export_position_default():
76+
"""The default export button keeps its historical top-right placement."""
77+
m = folium.Map([45.0, 3.0], zoom_start=4)
78+
draw = plugins.Draw(export=True)
79+
m.add_child(draw)
80+
81+
out = normalize(m._parent.render())
82+
83+
block = out[out.index(f"#export_{draw.get_name()}") :]
84+
block = block[: block.index("}")]
85+
assert "position: absolute;" in block
86+
assert "top: 90px;" in block
87+
assert "right: 10px;" in block
88+
89+
90+
def test_draw_export_position_corners():
91+
expected = {
92+
"topright": ("top: 90px;", "right: 10px;"),
93+
"topleft": ("top: 90px;", "left: 10px;"),
94+
"bottomright": ("bottom: 20px;", "right: 10px;"),
95+
"bottomleft": ("bottom: 20px;", "left: 10px;"),
96+
}
97+
for position, edges in expected.items():
98+
m = folium.Map([45.0, 3.0], zoom_start=4)
99+
draw = plugins.Draw(export=True, export_position=position)
100+
m.add_child(draw)
101+
102+
out = normalize(m._parent.render())
103+
block = out[out.index(f"#export_{draw.get_name()}") :]
104+
block = block[: block.index("}")]
105+
for edge in edges:
106+
assert edge in block, f"{position}: {edge!r} missing from {block!r}"
107+
108+
109+
def test_draw_export_position_invalid():
110+
with pytest.raises(ValueError, match="export_position must be one of"):
111+
plugins.Draw(export=True, export_position="middle")

0 commit comments

Comments
 (0)