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