2 let abortController = null;
3 const languagesToRequest = [...new Set([...OSM.preferred_languages.map(l => l.toLowerCase()), "mul", "en"])];
4 const wikisToRequest = [...new Set(languagesToRequest.filter(l => l !== "mul").map(l => l.split("-")[0] + "wiki"))];
5 const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
6 const isOfExpectedLanguage = ({ language }) => languagesToRequest[0].startsWith(language) || language === "mul";
8 $(document).on("click", "a[href='#versions-navigation-active-page-item']", function (e) {
9 scrollToActiveVersion();
10 $("#versions-navigation-active-page-item a.page-link").trigger("focus");
14 $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
16 OSM.Element = function (map, type) {
18 let scrollStartObserver, scrollEndObserver;
20 page.pushstate = page.popstate = function (path, id, version) {
21 OSM.loadSidebarContent(path, function () {
22 initVersionsNavigation();
23 page._addObject(type, id, version);
24 abortController = new AbortController();
28 page.load = function (path, id, version) {
29 initVersionsNavigation();
30 page._addObject(type, id, version, true);
31 abortController = new AbortController();
34 page.unload = function () {
36 scrollStartObserver?.disconnect();
37 scrollStartObserver = null;
38 scrollEndObserver?.disconnect();
39 scrollEndObserver = null;
40 abortController?.abort();
43 page._addObject = function () {};
44 page._removeObject = function () {};
46 function initVersionsNavigation() {
47 scrollToActiveVersion();
49 const $scrollableList = $("#versions-navigation-list-middle");
50 const [scrollableFirstItem] = $scrollableList.children().first();
51 const [scrollableLastItem] = $scrollableList.children().last();
53 if (scrollableFirstItem) {
54 scrollStartObserver = createScrollObserver("#versions-navigation-list-start", "2px 0px");
55 scrollStartObserver.observe(scrollableFirstItem);
58 if (scrollableLastItem) {
59 scrollEndObserver = createScrollObserver("#versions-navigation-list-end", "-2px 0px");
60 scrollEndObserver.observe(scrollableLastItem);
64 function createScrollObserver(shadowTarget, shadowOffset) {
65 const threshold = 0.95;
66 return new IntersectionObserver(([entry]) => {
67 const floating = entry.intersectionRatio < threshold;
69 .css("box-shadow", floating ? `rgba(0, 0, 0, 0.075) ${shadowOffset} 2px` : "")
70 .css("z-index", floating ? "5" : ""); // floating z-index should be larger than z-index of Bootstrap's .page-link:focus, which is 3
77 OSM.MappedElement = function (map, type) {
78 const page = OSM.Element(map, type);
80 page._addObject = function (type, id, version, center) {
81 const hashParams = OSM.parseHash();
82 map.addObject({ type: type, id: parseInt(id, 10), version: version && parseInt(version, 10) }, function (bounds) {
83 if (!hashParams.center && bounds.isValid() &&
84 (center || !map.getBounds().contains(bounds))) {
85 OSM.router.withoutMoveListener(function () {
86 map.fitBounds(bounds);
92 page._removeObject = function () {
99 function scrollToActiveVersion() {
100 const [scrollableList] = $("#versions-navigation-list-middle");
102 if (!scrollableList) return;
104 const [activeStartItem] = $("#versions-navigation-list-start #versions-navigation-active-page-item");
105 const [activeScrollableItem] = $("#versions-navigation-list-middle #versions-navigation-active-page-item");
107 if (activeStartItem) {
108 scrollableList.scrollLeft = 0;
109 } else if (activeScrollableItem) {
110 scrollableList.scrollLeft = Math.round(activeScrollableItem.offsetLeft - (scrollableList.offsetWidth / 2) + (activeScrollableItem.offsetWidth / 2));
112 scrollableList.scrollLeft = scrollableList.scrollWidth - scrollableList.offsetWidth;
116 function previewWikidataValue($btn) {
117 if (!OSM.WIKIDATA_API_URL) return;
118 const items = $btn.data("qids");
119 if (!items?.length) return;
120 $btn.prop("disabled", true);
121 fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({
122 action: "wbgetentities",
125 ids: items.join("|"),
126 props: "labels|sitelinks/urls|claims|descriptions",
127 languages: languagesToRequest.join("|"),
128 sitefilter: wikisToRequest.join("|")
130 headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
131 signal: abortController?.signal
133 .then(response => response.ok ? response.json() : Promise.reject(response))
134 .then(({ entities }) => {
135 if (!entities) return Promise.reject(entities);
140 .filter(qid => entities[qid])
141 .map(qid => getLocalizedResponse(entities[qid]))
142 .filter(data => data.label || data.icon || data.description || data.article)
143 .map(data => renderWikidataResponse(data, $btn.siblings(`a[href*="wikidata.org/entity/${data.qid}"]`)))
146 .catch(() => $btn.prop("disabled", false));
149 function getLocalizedResponse(entity) {
150 const rank = ({ rank }) => ({ preferred: 1, normal: 0, deprecated: -1 })[rank] ?? 0;
151 const toBestClaim = (out, claim) => (rank(claim) > rank(out)) ? claim : out;
152 const toFirstOf = (property) => (out, localization) => out ?? entity[property][localization];
155 label: languagesToRequest.reduce(toFirstOf("labels"), null),
157 "P8972", // small logo or icon
158 "P154", // logo image
159 "P14" // traffic sign
160 ].reduce((out, prop) => out ?? entity.claims[prop]?.reduce(toBestClaim)?.mainsnak?.datavalue?.value, null),
161 description: languagesToRequest.reduce(toFirstOf("descriptions"), null),
162 article: wikisToRequest.reduce(toFirstOf("sitelinks"), null)
164 if (data.article) data.article.language = data.article.site.replace("wiki", "");
168 function renderWikidataResponse({ icon, label, article, description }, $link) {
169 const cell = $("<td>")
171 .addClass("bg-body-tertiary");
173 if (icon && OSM.WIKIMEDIA_COMMONS_URL) {
174 let src = OSM.WIKIMEDIA_COMMONS_URL + "Special:Redirect/file/" + encodeURIComponent(icon) + "?mobileaction=toggle_view_desktop";
175 if (!icon.endsWith(".svg")) src += "&width=128";
177 .attr("href", OSM.WIKIMEDIA_COMMONS_URL + "File:" + encodeURIComponent(icon) + `?uselang=${OSM.i18n.locale}`)
178 .append($("<img>").attr({ src, height: "32" }))
179 .addClass("float-end mb-1 ms-2")
183 const link = $link.clone()
187 if (!isOfExpectedLanguage(label)) {
188 link.attr("lang", label.language);
189 link.after($("<sup>").text(" " + localeName.of(label.language)));
193 const link = $("<a>")
194 .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
195 .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
202 if (!isOfExpectedLanguage(article)) {
203 link.attr("lang", article.language);
204 link.after($("<sup>").text(" " + localeName.of(article.language)));
208 const text = $("<div>")
209 .text(description.value)
213 if (!isOfExpectedLanguage(description)) {
214 text.attr("lang", description.language);
217 return $("<tr>").append(cell);