]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/history.js
Merge remote-tracking branch 'upstream/pull/5923'
[rails.git] / app / assets / javascripts / index / history.js
1 //= require jquery-simulate/jquery.simulate
2 //= require ./history-changesets-layer
3
4 OSM.History = function (map) {
5   const page = {};
6
7   $("#sidebar_content")
8     .on("click", ".changeset_more a", loadMoreChangesets)
9     .on("mouseover", "[data-changeset]", function () {
10       toggleChangesetHighlight($(this).data("changeset").id, true);
11     })
12     .on("mouseout", "[data-changeset]", function () {
13       toggleChangesetHighlight($(this).data("changeset").id, false);
14     });
15
16   const changesetsLayer = new OSM.HistoryChangesetsLayer()
17     .on("mouseover", function (e) {
18       toggleChangesetHighlight(e.layer.id, true);
19     })
20     .on("mouseout", function (e) {
21       toggleChangesetHighlight(e.layer.id, false);
22     })
23     .on("click", function (e) {
24       clickChangeset(e.layer.id, e.originalEvent);
25     });
26
27   let changesetIntersectionObserver;
28
29   function disableChangesetIntersectionObserver() {
30     if (changesetIntersectionObserver) {
31       changesetIntersectionObserver.disconnect();
32       changesetIntersectionObserver = null;
33     }
34   }
35
36   function enableChangesetIntersectionObserver() {
37     disableChangesetIntersectionObserver();
38     if (!window.IntersectionObserver) return;
39
40     let keepInitialLocation = true;
41
42     changesetIntersectionObserver = new IntersectionObserver((entries) => {
43       let closestTargetToTop,
44           closestDistanceToTop = Infinity,
45           closestTargetToBottom,
46           closestDistanceToBottom = Infinity;
47
48       for (const entry of entries) {
49         const id = $(entry.target).data("changeset")?.id;
50
51         if (entry.isIntersecting) {
52           if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, 0);
53           continue;
54         }
55
56         const distanceToTop = entry.rootBounds.top - entry.boundingClientRect.bottom;
57         const distanceToBottom = entry.boundingClientRect.top - entry.rootBounds.bottom;
58
59         if (id) changesetsLayer.setChangesetSidebarRelativePosition(id, distanceToTop >= 0 ? 1 : -1);
60
61         if (distanceToTop >= 0 && distanceToTop < closestDistanceToTop) {
62           closestDistanceToTop = distanceToTop;
63           closestTargetToTop = entry.target;
64         }
65         if (distanceToBottom >= 0 && distanceToBottom <= closestDistanceToBottom) {
66           closestDistanceToBottom = distanceToBottom;
67           closestTargetToBottom = entry.target;
68         }
69       }
70
71       changesetsLayer.reorderChangesets();
72
73       if (keepInitialLocation) {
74         keepInitialLocation = false;
75         return;
76       }
77
78       if (closestTargetToTop && closestDistanceToTop < closestDistanceToBottom) {
79         const id = $(closestTargetToTop).data("changeset")?.id;
80         if (id) {
81           OSM.router.replace(location.pathname + "?" + new URLSearchParams({ before: id }) + location.hash);
82         }
83       } else if (closestTargetToBottom) {
84         const id = $(closestTargetToBottom).data("changeset")?.id;
85         if (id) {
86           OSM.router.replace(location.pathname + "?" + new URLSearchParams({ after: id }) + location.hash);
87         }
88       }
89     }, { root: $("#sidebar")[0] });
90
91     $("#sidebar_content .changesets ol").children().each(function () {
92       changesetIntersectionObserver.observe(this);
93     });
94   }
95
96   function toggleChangesetHighlight(id, state) {
97     changesetsLayer.toggleChangesetHighlight(id, state);
98     $("#changeset_" + id).toggleClass("selected", state);
99   }
100
101   function clickChangeset(id, e) {
102     $("#changeset_" + id).find("a.changeset_id").simulate("click", e);
103   }
104
105   function displayFirstChangesets(html) {
106     $("#sidebar_content .changesets").html(html);
107
108     $("#sidebar_content .changesets ol")
109       .before($("<div class='changeset-color-hint-bar opacity-75 sticky-top changeset-above-sidebar-viewport'>"))
110       .after($("<div class='changeset-color-hint-bar opacity-75 sticky-bottom changeset-below-sidebar-viewport'>"));
111
112     if (location.pathname === "/history") {
113       setPaginationMapHashes();
114     }
115   }
116
117   function displayMoreChangesets(div, html) {
118     const sidebar = $("#sidebar")[0];
119     const previousScrollHeightMinusTop = sidebar.scrollHeight - sidebar.scrollTop;
120
121     const oldList = $("#sidebar_content .changesets ol");
122
123     div.replaceWith(html);
124
125     const prevNewList = oldList.prevAll("ol");
126     if (prevNewList.length) {
127       prevNewList.next(".changeset_more").remove();
128       prevNewList.children().prependTo(oldList);
129       prevNewList.remove();
130
131       // restore scroll position only if prepending
132       sidebar.scrollTop = sidebar.scrollHeight - previousScrollHeightMinusTop;
133     }
134
135     const nextNewList = oldList.nextAll("ol");
136     if (nextNewList.length) {
137       nextNewList.prev(".changeset_more").remove();
138       nextNewList.children().appendTo(oldList);
139       nextNewList.remove();
140     }
141
142     if (location.pathname === "/history") {
143       setPaginationMapHashes();
144     }
145   }
146
147   function setPaginationMapHashes() {
148     $("#sidebar .pagination a").each(function () {
149       $(this).prop("hash", OSM.formatHash({
150         center: map.getCenter(),
151         zoom: map.getZoom()
152       }));
153     });
154   }
155
156   function loadFirstChangesets() {
157     const data = new URLSearchParams();
158
159     disableChangesetIntersectionObserver();
160
161     if (location.pathname === "/history") {
162       setBboxFetchData(data);
163       const feedLink = $("link[type=\"application/atom+xml\"]"),
164             feedHref = feedLink.attr("href").split("?")[0];
165       feedLink.attr("href", feedHref + "?" + data);
166     }
167
168     setListFetchData(data, location);
169
170     fetch(location.pathname + "?" + data)
171       .then(response => response.text())
172       .then(function (html) {
173         displayFirstChangesets(html);
174         enableChangesetIntersectionObserver();
175
176         if (data.has("before")) {
177           const [firstItem] = $("#sidebar_content .changesets ol").children().first();
178           firstItem?.scrollIntoView();
179         } else if (data.has("after")) {
180           const [lastItem] = $("#sidebar_content .changesets ol").children().last();
181           lastItem?.scrollIntoView(false);
182         } else {
183           const [sidebar] = $("#sidebar");
184           sidebar.scrollTop = 0;
185         }
186
187         updateMap();
188       });
189   }
190
191   function loadMoreChangesets(e) {
192     e.preventDefault();
193     e.stopPropagation();
194
195     const div = $(this).parents(".changeset_more");
196
197     div.find(".pagination").addClass("invisible");
198     div.find("[hidden]").prop("hidden", false);
199
200     const data = new URLSearchParams();
201
202     if (location.pathname === "/history") {
203       setBboxFetchData(data);
204     }
205
206     const url = new URL($(this).attr("href"), location);
207     setListFetchData(data, url);
208
209     fetch(url.pathname + "?" + data)
210       .then(response => response.text())
211       .then(function (html) {
212         displayMoreChangesets(div, html);
213         enableChangesetIntersectionObserver();
214
215         updateMap();
216       });
217   }
218
219   function setBboxFetchData(data) {
220     const crs = map.options.crs;
221     const sw = map.getBounds().getSouthWest();
222     const ne = map.getBounds().getNorthEast();
223     const swClamped = crs.unproject(crs.project(sw));
224     const neClamped = crs.unproject(crs.project(ne));
225
226     if (sw.lat >= swClamped.lat || ne.lat <= neClamped.lat || ne.lng - sw.lng < 360) {
227       data.set("bbox", map.getBounds().toBBoxString());
228     }
229   }
230
231   function setListFetchData(data, url) {
232     const params = new URLSearchParams(url.search);
233
234     data.set("list", "1");
235
236     if (params.has("before")) {
237       data.set("before", params.get("before"));
238     }
239     if (params.has("after")) {
240       data.set("after", params.get("after"));
241     }
242   }
243
244   function moveEndListener() {
245     if (location.pathname === "/history") {
246       OSM.router.replace("/history" + window.location.hash);
247       loadFirstChangesets();
248     } else {
249       changesetsLayer.updateChangesetLocations(map);
250     }
251   }
252
253   function zoomEndListener() {
254     changesetsLayer.updateChangesetShapes(map);
255   }
256
257   function updateMap() {
258     const changesets = $("[data-changeset]").map(function (index, element) {
259       return $(element).data("changeset");
260     }).get().filter(function (changeset) {
261       return changeset.bbox;
262     });
263
264     changesetsLayer.updateChangesets(map, changesets);
265
266     if (location.pathname !== "/history") {
267       const bounds = changesetsLayer.getBounds();
268       if (bounds.isValid()) map.fitBounds(bounds);
269     }
270   }
271
272   page.pushstate = page.popstate = function (path) {
273     OSM.loadSidebarContent(path, page.load);
274   };
275
276   page.load = function () {
277     map.addLayer(changesetsLayer);
278     map.on("moveend", moveEndListener);
279     map.on("zoomend", zoomEndListener);
280     loadFirstChangesets();
281   };
282
283   page.unload = function () {
284     map.removeLayer(changesetsLayer);
285     map.off("moveend", moveEndListener);
286     map.off("zoomend", zoomEndListener);
287     disableChangesetIntersectionObserver();
288   };
289
290   return page;
291 };