]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/contextmenu.js
Update route markers and context menu icons to cursor/flag metaphor
[rails.git] / app / assets / javascripts / index / contextmenu.js
1 OSM.initializations.push(function (map) {
2   const $contextMenu = $("#map-context-menu");
3   map.osm_contextmenu = new OSM.ContextMenu(map, $contextMenu);
4
5   const toggleMenuItem = ($element, enable) => {
6     $element.toggleClass("disabled", !enable)
7       .attr("aria-disabled", enable ? null : "true");
8   };
9
10   const updateContextMenuState = () => {
11     const zoom = map.getZoom();
12     toggleMenuItem($("#menu-action-add-note"), zoom >= 12);
13     toggleMenuItem($("#menu-action-query-features"), zoom >= 14);
14   };
15
16   const getDirectionsCoordinates = ($input) => {
17     const lat = $input.attr("data-lat");
18     const lon = $input.attr("data-lon");
19     if (lat && lon) return `${lat},${lon}`;
20     return $input.val();
21   };
22
23   const latLngFromContext = () =>
24     L.latLng($contextMenu.data("lat"), $contextMenu.data("lng"));
25
26   const croppedLatLon = () =>
27     OSM.cropLocation(latLngFromContext(), map.getZoom());
28
29   const routeWithLatLon = (path, extraParams = {}) => {
30     const [lat, lon] = croppedLatLon();
31     OSM.router.route(`${path}?` + new URLSearchParams({ lat, lon, ...extraParams }));
32   };
33
34   const contextmenuItems = [
35     {
36       id: "menu-action-directions-from",
37       icon: "bi-cursor",
38       text: OSM.i18n.t("javascripts.context.directions_from"),
39       callback: () => {
40         const params = new URLSearchParams({
41           from: croppedLatLon().join(","),
42           to: getDirectionsCoordinates($("#route_to"))
43         });
44         OSM.router.route(`/directions?${params}`);
45       }
46     },
47     {
48       id: "menu-action-directions-to",
49       icon: "bi-flag",
50       text: OSM.i18n.t("javascripts.context.directions_to"),
51       callback: () => {
52         const params = new URLSearchParams({
53           from: getDirectionsCoordinates($("#route_from")),
54           to: croppedLatLon().join(",")
55         });
56         OSM.router.route(`/directions?${params}`);
57       }
58     },
59     {
60       separator: true
61     },
62     {
63       id: "menu-action-add-note",
64       icon: "bi-chat-text",
65       text: OSM.i18n.t("javascripts.context.add_note"),
66       callback: () => routeWithLatLon("/note/new")
67     },
68     {
69       separator: true
70     },
71     {
72       id: "menu-action-show-address",
73       icon: "bi-compass",
74       text: OSM.i18n.t("javascripts.context.show_address"),
75       callback: () => routeWithLatLon("/search", { zoom: map.getZoom() })
76     },
77     {
78       id: "menu-action-query-features",
79       icon: "bi-question",
80       text: OSM.i18n.t("javascripts.context.query_features"),
81       callback: () => routeWithLatLon("/query")
82     },
83     {
84       id: "menu-action-centre-map",
85       icon: "bi-crosshair",
86       text: OSM.i18n.t("javascripts.context.centre_map"),
87       callback: () => map.panTo(latLngFromContext())
88     }
89   ];
90
91   map.on("contextmenu", function (e) {
92     map.osm_contextmenu.show(e, contextmenuItems);
93     updateContextMenuState();
94   });
95
96   map.on("show-contextmenu", function (data) {
97     map.osm_contextmenu.show(data.event, data.items);
98   });
99
100   map.on("zoomend", updateContextMenuState);
101 });
102
103 class ContextMenu {
104   constructor(map, $element) {
105     this._map = map;
106     this._$element = $element;
107     this._popperInstance = null;
108
109     this._map.on("click movestart", this.hide, this);
110     $(document).on("click", (e) => {
111       if (!$(e.target).closest(this._$element).length) {
112         this.hide();
113       }
114     });
115   }
116
117   show(e, items) {
118     e.originalEvent.preventDefault();
119     e.originalEvent.stopPropagation();
120
121     this._render(items);
122     this._$element.removeClass("d-none");
123     this._updatePopper(e);
124
125     this._$element.data("lat", e.latlng.lat);
126     this._$element.data("lng", e.latlng.lng);
127   }
128
129   hide() {
130     this._$element.addClass("d-none");
131     if (this._popperInstance) {
132       this._popperInstance.destroy();
133       this._popperInstance = null;
134     }
135   }
136
137   _updatePopper(e) {
138     const getVirtualReference = (x, y) => ({
139       getBoundingClientRect: () => ({
140         width: 0, height: 0, top: y, left: x, right: x, bottom: y
141       })
142     });
143
144     if (this._popperInstance) {
145       this._popperInstance.destroy();
146       this._popperInstance = null;
147     }
148
149     const virtualReference = getVirtualReference(
150       e.originalEvent.clientX,
151       e.originalEvent.clientY
152     );
153
154     this._popperInstance = Popper.createPopper(virtualReference, this._$element.find(".dropdown-menu")[0], {
155       placement: "bottom-start",
156       strategy: "absolute",
157       modifiers: [
158         {
159           name: "offset",
160           options: {
161             offset: [0, 0]
162           }
163         },
164         {
165           name: "preventOverflow",
166           options: { boundary: document.getElementById("map") }
167         },
168         {
169           name: "flip",
170           options: {
171             fallbackPlacements: ["top-start", "bottom-end", "top-end"]
172           }
173         }
174       ]
175     });
176   }
177
178   _render(items) {
179     const $menuList = $("<ul>").addClass("dropdown-menu show shadow cm_dropdown_menu");
180
181     items.forEach((item) => {
182       const $menuItem = item.separator ?
183         this._createSeparator() :
184         this._createMenuItem(item);
185       $menuList.append($menuItem);
186     });
187
188     this._$element.empty().append($menuList);
189   }
190
191   _createMenuItem(item) {
192     const $icon = $("<i>").addClass(`bi ${item.icon}`).prop("ariaHidden", true);
193     const $label = $("<span>").text(item.text);
194
195     const $link = $("<a>")
196       .addClass("dropdown-item d-flex align-items-center gap-3")
197       .attr({ href: "#", id: item.id })
198       .append($icon, $label)
199       .on("click", (e) => {
200         e.preventDefault();
201         item.callback?.();
202         this.hide();
203       });
204
205     return $("<li>").append($link);
206   }
207
208   _createSeparator() {
209     return $("<li>").append($("<hr>").addClass("dropdown-divider"));
210   }
211 }
212
213 OSM.ContextMenu = ContextMenu;