]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/element.js
Move centering active version to numbered pagination js module
[rails.git] / app / assets / javascripts / index / element.js
1 (function () {
2   let abortController = null;
3   const languagesToRequest = [...new Set(OSM.preferred_languages.map(l => l.toLowerCase()))];
4   const wikisToRequest = [...new Set([...OSM.preferred_languages, "en"].map(l => l.split("-")[0] + "wiki"))];
5   const isOfExpectedLanguage = ({ language }) => languagesToRequest[0].startsWith(language) || language === "mul";
6
7   $(document).on("click", "a[href='#versions-navigation-active-page-item']", function (e) {
8     $(document).trigger("numbered_pagination:center");
9     $("#versions-navigation-active-page-item a.page-link").trigger("focus");
10     e.preventDefault();
11   });
12
13   $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
14
15   OSM.Element = type => function () {
16     const page = {};
17     let scrollStartObserver, scrollEndObserver;
18
19     page.pushstate = page.popstate = function (path, id, version) {
20       OSM.loadSidebarContent(path, function () {
21         initVersionsNavigation();
22         page._addObject(type, id, version);
23         abortController = new AbortController();
24       });
25     };
26
27     page.load = function (path, id, version) {
28       initVersionsNavigation();
29       page._addObject(type, id, version, true);
30       abortController = new AbortController();
31     };
32
33     page.unload = function () {
34       page._removeObject();
35       scrollStartObserver?.disconnect();
36       scrollStartObserver = null;
37       scrollEndObserver?.disconnect();
38       scrollEndObserver = null;
39       abortController?.abort();
40     };
41
42     page._addObject = function () {};
43     page._removeObject = function () {};
44
45     function initVersionsNavigation() {
46       $(document).trigger("numbered_pagination:center");
47
48       const $scrollableList = $("#versions-navigation-list-middle");
49       const [scrollableFirstItem] = $scrollableList.children().first();
50       const [scrollableLastItem] = $scrollableList.children().last();
51
52       if (scrollableFirstItem) {
53         scrollStartObserver = createScrollObserver("#versions-navigation-list-start", "2px 0px");
54         scrollStartObserver.observe(scrollableFirstItem);
55       }
56
57       if (scrollableLastItem) {
58         scrollEndObserver = createScrollObserver("#versions-navigation-list-end", "-2px 0px");
59         scrollEndObserver.observe(scrollableLastItem);
60       }
61     }
62
63     function createScrollObserver(shadowTarget, shadowOffset) {
64       const threshold = 0.95;
65       return new IntersectionObserver(([entry]) => {
66         const floating = entry.intersectionRatio < threshold;
67         $(shadowTarget)
68           .css("box-shadow", floating ? `rgba(0, 0, 0, 0.075) ${shadowOffset} 2px` : "")
69           .css("z-index", floating ? "5" : ""); // floating z-index should be larger than z-index of Bootstrap's .page-link:focus, which is 3
70       }, { threshold });
71     }
72
73     return page;
74   };
75
76   OSM.MappedElement = type => function (map) {
77     const page = OSM.Element(type)(map);
78
79     page._addObject = function (type, id, version, center) {
80       const hashParams = OSM.parseHash();
81       map.addObject({ type: type, id: parseInt(id, 10), version: version && parseInt(version, 10) }, function (bounds) {
82         if (!hashParams.center && bounds.isValid() &&
83             (center || !map.getBounds().contains(bounds))) {
84           OSM.router.withoutMoveListener(function () {
85             map.fitBounds(bounds);
86           });
87         }
88       });
89     };
90
91     page._removeObject = function () {
92       map.removeObject();
93     };
94
95     return page;
96   };
97
98   function previewWikidataValue($btn) {
99     if (!OSM.WIKIDATA_API_URL) return;
100     const items = $btn.data("qids");
101     if (!items?.length) return;
102     $btn.prop("disabled", true);
103     fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({
104       action: "wbgetentities",
105       format: "json",
106       origin: "*",
107       ids: items.join("|"),
108       props: "labels|sitelinks/urls|claims|descriptions",
109       languages: languagesToRequest.join("|"),
110       languagefallback: 1,
111       sitefilter: wikisToRequest.join("|")
112     }), {
113       headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
114       signal: abortController?.signal
115     })
116       .then(response => response.ok ? response.json() : Promise.reject(response))
117       .then(({ entities }) => {
118         if (!entities) return Promise.reject(entities);
119         $btn
120           .closest("tr")
121           .after(
122             items
123               .filter(qid => entities[qid])
124               .map(qid => getLocalizedResponse(entities[qid]))
125               .filter(data => data.label || data.icon || data.description || data.article)
126               .map(data => renderWikidataResponse(data, $btn.siblings(`a[href*="wikidata.org/entity/${data.qid}"]`)))
127           );
128       })
129       .catch(() => $btn.prop("disabled", false));
130   }
131
132   function getLocalizedResponse(entity) {
133     const rank = ({ rank }) => ({ preferred: 1, normal: 0, deprecated: -1 })[rank] ?? 0;
134     const toBestClaim = (out, claim) => (rank(claim) > rank(out)) ? claim : out;
135     const toFirstOf = (property) => (out, localization) => out ?? property[localization];
136     const data = {
137       qid: entity.id,
138       label: languagesToRequest.reduce(toFirstOf(entity.labels), null),
139       icon: [
140         "P8972", // small logo or icon
141         "P154", // logo image
142         "P14" // traffic sign
143       ].reduce((out, prop) => out ?? entity.claims[prop]?.reduce(toBestClaim)?.mainsnak?.datavalue?.value, null),
144       description: languagesToRequest.reduce(toFirstOf(entity.descriptions), null),
145       article: wikisToRequest.reduce(toFirstOf(entity.sitelinks), null)
146     };
147     if (data.article) data.article.language = data.article.site.replace("wiki", "");
148     return data;
149   }
150
151   function renderWikidataResponse({ icon, label, article, description }, $link) {
152     const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
153     const cell = $("<td>")
154       .attr("colspan", 2)
155       .addClass("bg-body-tertiary");
156
157     if (icon && OSM.WIKIMEDIA_COMMONS_URL) {
158       let src = OSM.WIKIMEDIA_COMMONS_URL + "Special:Redirect/file/" + encodeURIComponent(icon) + "?mobileaction=toggle_view_desktop";
159       if (!icon.endsWith(".svg")) src += "&width=128";
160       $("<a>")
161         .attr("href", OSM.WIKIMEDIA_COMMONS_URL + "File:" + encodeURIComponent(icon) + `?uselang=${OSM.i18n.locale}`)
162         .append($("<img>").attr({ src, height: "32" }))
163         .addClass("float-end mb-1 ms-2")
164         .appendTo(cell);
165     }
166     if (label) {
167       const link = $link.clone()
168         .text(label.value)
169         .attr("dir", "auto")
170         .appendTo(cell);
171       if (!isOfExpectedLanguage(label)) {
172         link.attr("lang", label.language);
173         link.after($("<sup>").text(" " + localeName.of(label.language)));
174       }
175     }
176     if (article) {
177       const link = $("<a>")
178         .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
179         .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
180         .attr("dir", "auto")
181         .appendTo(cell);
182       if (label) {
183         link.before(" (");
184         link.after(")");
185       }
186       if (!isOfExpectedLanguage(article)) {
187         link.attr("lang", article.language);
188         link.after($("<sup>").text(" " + localeName.of(article.language)));
189       }
190     }
191     if (description) {
192       const text = $("<div>")
193         .text(description.value)
194         .addClass("small")
195         .attr("dir", "auto")
196         .appendTo(cell);
197       if (!isOfExpectedLanguage(description)) {
198         text.attr("lang", description.language);
199       }
200     }
201     return $("<tr>").append(cell);
202   }
203 }());