]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/contextmenu.js
Merge remote-tracking branch 'upstream/pull/7190'
[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 = () => L.latLng($contextMenu.data("lat"), $contextMenu.data("lng"));
24
25   const croppedLatLng = () => OSM.cropLocation(latLngFromContext(), map.getZoom());
26
27   const routeWithLatLon = (path, extraParams = {}) => {
28     const { lat, lng } = croppedLatLng();
29     OSM.router.route(`${path}?` + new URLSearchParams({ lat, lon: lng, ...extraParams }));
30   };
31
32   const contextmenuItems = [
33     {
34       id: "menu-action-directions-from",
35       icon: "bi-cursor",
36       text: OSM.i18n.t("javascripts.context.directions_from"),
37       callback: () => {
38         const { lat, lng } = croppedLatLng();
39         const params = new URLSearchParams({
40           from: `${lat},${lng}`,
41           to: getDirectionsCoordinates($("#route_to"))
42         });
43         OSM.router.route(`/directions?${params}`);
44       }
45     },
46     {
47       id: "menu-action-directions-to",
48       icon: "bi-flag",
49       text: OSM.i18n.t("javascripts.context.directions_to"),
50       callback: () => {
51         const { lat, lng } = croppedLatLng();
52         const params = new URLSearchParams({
53           from: getDirectionsCoordinates($("#route_from")),
54           to: `${lat},${lng}`
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-square-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-lg",
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 OSM.ContextMenu = class {
104   constructor(map, $element) {
105     this._map = map;
106     this._$element = $element;
107     this._popperInstance = null;
108
109     const hide = this.hide.bind(this);
110     this._map.on("click", hide);
111     this._map.on("movestart", hide);
112     $(document).on("click", (e) => {
113       if (!$(e.target).closest(this._$element).length) {
114         this.hide();
115       }
116     });
117   }
118
119   show(e, items) {
120     e.originalEvent.preventDefault();
121     e.originalEvent.stopPropagation();
122
123     this._render(items);
124     this._$element.removeClass("d-none");
125     this._updatePopper(e);
126
127     this._$element.data("lat", e.latlng.lat);
128     this._$element.data("lng", e.latlng.lng);
129   }
130
131   hide() {
132     this._$element.addClass("d-none");
133     if (this._popperInstance) {
134       this._popperInstance.destroy();
135       this._popperInstance = null;
136     }
137   }
138
139   _updatePopper(e) {
140     const getVirtualReference = (x, y) => ({
141       getBoundingClientRect: () => ({
142         width: 0, height: 0, top: y, left: x, right: x, bottom: y
143       })
144     });
145
146     if (this._popperInstance) {
147       this._popperInstance.destroy();
148       this._popperInstance = null;
149     }
150
151     const virtualReference = getVirtualReference(
152       e.originalEvent.clientX,
153       e.originalEvent.clientY
154     );
155
156     this._popperInstance = Popper.createPopper(virtualReference, this._$element.find(".dropdown-menu")[0], {
157       placement: "bottom-start",
158       strategy: "absolute",
159       modifiers: [
160         {
161           name: "offset",
162           options: {
163             offset: [0, 0]
164           }
165         },
166         {
167           name: "preventOverflow",
168           options: { boundary: document.getElementById("map") }
169         },
170         {
171           name: "flip",
172           options: {
173             fallbackPlacements: ["top-start", "bottom-end", "top-end"]
174           }
175         }
176       ]
177     });
178   }
179
180   _render(items) {
181     const $menuList = $("<ul>").addClass("dropdown-menu show shadow cm_dropdown_menu");
182
183     items.forEach((item) => {
184       const $menuItem = item.separator ?
185         this._createSeparator() :
186         this._createMenuItem(item);
187       $menuList.append($menuItem);
188     });
189
190     this._$element.empty().append($menuList);
191   }
192
193   _createMenuItem(item) {
194     const $icon = $("<i>").addClass(`bi ${item.icon}`).prop("ariaHidden", true);
195     const $label = $("<span>").text(item.text);
196
197     const $link = $("<a>")
198       .addClass("dropdown-item d-flex align-items-center gap-3")
199       .attr({ href: "#", id: item.id })
200       .append($icon, $label)
201       .on("click", (e) => {
202         e.preventDefault();
203         item.callback?.();
204         this.hide();
205       });
206
207     return $("<li>").append($link);
208   }
209
210   _createSeparator() {
211     return $("<li>").append($("<hr>").addClass("dropdown-divider"));
212   }
213 };