From 18927b68e79cabd57b44c38f3b7ee8ca7bcab1c8 Mon Sep 17 00:00:00 2001 From: hksamm Date: Sun, 23 Aug 2026 04:34:09 +0900 Subject: [PATCH] Add export_position option to the Draw plugin 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 --- folium/plugins/draw.py | 28 ++++++++++++++++++++++++-- tests/plugins/test_draw.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/folium/plugins/draw.py b/folium/plugins/draw.py index 2de7733bbd..648d553e77 100644 --- a/folium/plugins/draw.py +++ b/folium/plugins/draw.py @@ -3,6 +3,25 @@ from folium.elements import JSCSSMixin from folium.template import Template +# Each corner maps to the pair of CSS edge offsets that pins the export button +# there. "topright" reproduces the historical hard-coded placement exactly. +_EXPORT_POSITION_CSS = { + "topright": "top: 90px;\n right: 10px;", + "topleft": "top: 90px;\n left: 10px;", + "bottomright": "bottom: 20px;\n right: 10px;", + "bottomleft": "bottom: 20px;\n left: 10px;", +} + + +def _export_position_to_css(export_position): + try: + return _EXPORT_POSITION_CSS[export_position] + except KeyError: + raise ValueError( + "export_position must be one of " + f"{sorted(_EXPORT_POSITION_CSS)}, not {export_position!r}" + ) from None + class Draw(JSCSSMixin, MacroElement): ''' @@ -20,6 +39,10 @@ class Draw(JSCSSMixin, MacroElement): position : {'topleft', 'toprigth', 'bottomleft', 'bottomright'} Position of control. See https://leafletjs.com/reference.html#control + export_position : {'topright', 'topleft', 'bottomright', 'bottomleft'} + Corner of the map to place the export button in, when ``export`` + is True. Defaults to 'topright'. Use this to keep the button clear + of other controls such as a LayerControl. show_geometry_on_click : bool, default True When True, opens an alert with the geometry description on click. draw_options : dict, optional @@ -60,7 +83,7 @@ class Draw(JSCSSMixin, MacroElement): Export @@ -156,6 +178,7 @@ def __init__( feature_group=None, filename="data.geojson", position="topleft", + export_position="topright", show_geometry_on_click=True, draw_options=None, edit_options=None, @@ -167,6 +190,7 @@ def __init__( self.feature_group = feature_group self.filename = filename self.position = position + self.export_position_css = _export_position_to_css(export_position) self.show_geometry_on_click = show_geometry_on_click self.draw_options = draw_options or {} self.edit_options = edit_options or {} diff --git a/tests/plugins/test_draw.py b/tests/plugins/test_draw.py index 16889b1aea..507474de2a 100644 --- a/tests/plugins/test_draw.py +++ b/tests/plugins/test_draw.py @@ -5,6 +5,8 @@ import re +import pytest + import folium from folium import plugins from folium.template import Template @@ -68,3 +70,42 @@ def test_two_draw_controls_get_unique_export_ids(): handler = out[start : out.index("}", start) + 1] assert f"drawnItems_{draw.get_name()}.toGeoJSON()" in handler assert filename in handler + + +def test_draw_export_position_default(): + """The default export button keeps its historical top-right placement.""" + m = folium.Map([45.0, 3.0], zoom_start=4) + draw = plugins.Draw(export=True) + m.add_child(draw) + + out = normalize(m._parent.render()) + + block = out[out.index(f"#export_{draw.get_name()}") :] + block = block[: block.index("}")] + assert "position: absolute;" in block + assert "top: 90px;" in block + assert "right: 10px;" in block + + +def test_draw_export_position_corners(): + expected = { + "topright": ("top: 90px;", "right: 10px;"), + "topleft": ("top: 90px;", "left: 10px;"), + "bottomright": ("bottom: 20px;", "right: 10px;"), + "bottomleft": ("bottom: 20px;", "left: 10px;"), + } + for position, edges in expected.items(): + m = folium.Map([45.0, 3.0], zoom_start=4) + draw = plugins.Draw(export=True, export_position=position) + m.add_child(draw) + + out = normalize(m._parent.render()) + block = out[out.index(f"#export_{draw.get_name()}") :] + block = block[: block.index("}")] + for edge in edges: + assert edge in block, f"{position}: {edge!r} missing from {block!r}" + + +def test_draw_export_position_invalid(): + with pytest.raises(ValueError, match="export_position must be one of"): + plugins.Draw(export=True, export_position="middle")