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";
7 $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
9 OSM.Element = type => function () {
11 let scrollStartObserver, scrollEndObserver;
13 page.pushstate = page.popstate = function (path, id, version) {
14 OSM.loadSidebarContent(path, function () {
15 initVersionsNavigation();
16 page._addObject(type, id, version);
17 abortController = new AbortController();
21 page.load = function (path, id, version) {
22 initVersionsNavigation();
23 page._addObject(type, id, version, true);
24 abortController = new AbortController();
27 page.unload = function () {
29 scrollStartObserver?.disconnect();
30 scrollStartObserver = null;
31 scrollEndObserver?.disconnect();
32 scrollEndObserver = null;
33 abortController?.abort();
36 page._addObject = function () {};
37 page._removeObject = function () {};
39 function initVersionsNavigation() {
40 $(document).trigger("numbered_pagination:center");
42 const $scrollableList = $("#versions-navigation-list-middle");
43 const [scrollableFirstItem] = $scrollableList.children().first();
44 const [scrollableLastItem] = $scrollableList.children().last();
46 if (scrollableFirstItem) {
47 scrollStartObserver = createScrollObserver("#versions-navigation-list-start", "2px 0px");
48 scrollStartObserver.observe(scrollableFirstItem);
51 if (scrollableLastItem) {
52 scrollEndObserver = createScrollObserver("#versions-navigation-list-end", "-2px 0px");
53 scrollEndObserver.observe(scrollableLastItem);
57 function createScrollObserver(shadowTarget, shadowOffset) {
58 const threshold = 0.95;
59 return new IntersectionObserver(([entry]) => {
60 const floating = entry.intersectionRatio < threshold;
62 .css("box-shadow", floating ? `rgba(0, 0, 0, 0.075) ${shadowOffset} 2px` : "")
63 .css("z-index", floating ? "5" : ""); // floating z-index should be larger than z-index of Bootstrap's .page-link:focus, which is 3
70 OSM.MappedElement = type => function (map) {
71 const page = OSM.Element(type)(map);
73 page._addObject = function (type, id, version, center) {
74 const hashParams = OSM.parseHash();
75 map.addObject({ type: type, id: parseInt(id, 10), version: version && parseInt(version, 10) }, function (bounds) {
76 if (!hashParams.center && bounds.isValid() &&
77 (center || !map.getBounds().contains(bounds))) {
78 OSM.router.withoutMoveListener(function () {
79 map.fitBounds(bounds);
85 page._removeObject = function () {
92 function previewWikidataValue($btn) {
93 if (!OSM.WIKIDATA_API_URL) return;
94 const items = $btn.data("qids");
95 if (!items?.length) return;
96 $btn.prop("disabled", true);
97 fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({
98 action: "wbgetentities",
101 ids: items.join("|"),
102 props: "labels|sitelinks/urls|claims|descriptions",
103 languages: languagesToRequest.join("|"),
105 sitefilter: wikisToRequest.join("|")
107 headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
108 signal: abortController?.signal
110 .then(response => response.ok ? response.json() : Promise.reject(response))
111 .then(({ entities }) => {
112 if (!entities) return Promise.reject(entities);
117 .filter(qid => entities[qid])
118 .map(qid => getLocalizedResponse(entities[qid]))
119 .filter(data => data.label || data.icon || data.description || data.article)
120 .map(data => renderWikidataResponse(data, $btn.siblings(`a[href*="wikidata.org/entity/${data.qid}"]`)))
123 .catch(() => $btn.prop("disabled", false));
126 function getLocalizedResponse(entity) {
127 const rank = ({ rank }) => ({ preferred: 1, normal: 0, deprecated: -1 })[rank] ?? 0;
128 const toBestClaim = (out, claim) => (rank(claim) > rank(out)) ? claim : out;
129 const toFirstOf = (property) => (out, localization) => out ?? property[localization];
132 label: languagesToRequest.reduce(toFirstOf(entity.labels), null),
134 "P8972", // small logo or icon
135 "P154", // logo image
136 "P14" // traffic sign
137 ].reduce((out, prop) => out ?? entity.claims[prop]?.reduce(toBestClaim)?.mainsnak?.datavalue?.value, null),
138 description: languagesToRequest.reduce(toFirstOf(entity.descriptions), null),
139 article: wikisToRequest.reduce(toFirstOf(entity.sitelinks), null)
141 if (data.article) data.article.language = data.article.site.replace("wiki", "");
145 function renderWikidataResponse({ icon, label, article, description }, $link) {
146 const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
147 const cell = $("<td>")
149 .addClass("bg-body-tertiary");
151 if (icon && OSM.WIKIMEDIA_COMMONS_URL) {
152 let src = OSM.WIKIMEDIA_COMMONS_URL + "Special:Redirect/file/" + encodeURIComponent(icon) + "?mobileaction=toggle_view_desktop";
153 if (!icon.endsWith(".svg")) src += "&width=128";
155 .attr("href", OSM.WIKIMEDIA_COMMONS_URL + "File:" + encodeURIComponent(icon) + `?uselang=${OSM.i18n.locale}`)
156 .append($("<img>").attr({ src, height: "32" }))
157 .addClass("float-end mb-1 ms-2")
161 const link = $link.clone()
165 if (!isOfExpectedLanguage(label)) {
166 link.attr("lang", label.language);
167 link.after($("<sup>").text(" " + localeName.of(label.language)));
171 const link = $("<a>")
172 .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
173 .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
180 if (!isOfExpectedLanguage(article)) {
181 link.attr("lang", article.language);
182 link.after($("<sup>").text(" " + localeName.of(article.language)));
186 const text = $("<div>")
187 .text(description.value)
191 if (!isOfExpectedLanguage(description)) {
192 text.attr("lang", description.language);
195 return $("<tr>").append(cell);