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