]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/element.js
Move breadcrumbs version click 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", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
8
9   OSM.Element = type => function () {
10     const page = {};
11     let scrollStartObserver, scrollEndObserver;
12
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();
18       });
19     };
20
21     page.load = function (path, id, version) {
22       initVersionsNavigation();
23       page._addObject(type, id, version, true);
24       abortController = new AbortController();
25     };
26
27     page.unload = function () {
28       page._removeObject();
29       scrollStartObserver?.disconnect();
30       scrollStartObserver = null;
31       scrollEndObserver?.disconnect();
32       scrollEndObserver = null;
33       abortController?.abort();
34     };
35
36     page._addObject = function () {};
37     page._removeObject = function () {};
38
39     function initVersionsNavigation() {
40       $(document).trigger("numbered_pagination:center");
41
42       const $scrollableList = $("#versions-navigation-list-middle");
43       const [scrollableFirstItem] = $scrollableList.children().first();
44       const [scrollableLastItem] = $scrollableList.children().last();
45
46       if (scrollableFirstItem) {
47         scrollStartObserver = createScrollObserver("#versions-navigation-list-start", "2px 0px");
48         scrollStartObserver.observe(scrollableFirstItem);
49       }
50
51       if (scrollableLastItem) {
52         scrollEndObserver = createScrollObserver("#versions-navigation-list-end", "-2px 0px");
53         scrollEndObserver.observe(scrollableLastItem);
54       }
55     }
56
57     function createScrollObserver(shadowTarget, shadowOffset) {
58       const threshold = 0.95;
59       return new IntersectionObserver(([entry]) => {
60         const floating = entry.intersectionRatio < threshold;
61         $(shadowTarget)
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
64       }, { threshold });
65     }
66
67     return page;
68   };
69
70   OSM.MappedElement = type => function (map) {
71     const page = OSM.Element(type)(map);
72
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);
80           });
81         }
82       });
83     };
84
85     page._removeObject = function () {
86       map.removeObject();
87     };
88
89     return page;
90   };
91
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",
99       format: "json",
100       origin: "*",
101       ids: items.join("|"),
102       props: "labels|sitelinks/urls|claims|descriptions",
103       languages: languagesToRequest.join("|"),
104       languagefallback: 1,
105       sitefilter: wikisToRequest.join("|")
106     }), {
107       headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
108       signal: abortController?.signal
109     })
110       .then(response => response.ok ? response.json() : Promise.reject(response))
111       .then(({ entities }) => {
112         if (!entities) return Promise.reject(entities);
113         $btn
114           .closest("tr")
115           .after(
116             items
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}"]`)))
121           );
122       })
123       .catch(() => $btn.prop("disabled", false));
124   }
125
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];
130     const data = {
131       qid: entity.id,
132       label: languagesToRequest.reduce(toFirstOf(entity.labels), null),
133       icon: [
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)
140     };
141     if (data.article) data.article.language = data.article.site.replace("wiki", "");
142     return data;
143   }
144
145   function renderWikidataResponse({ icon, label, article, description }, $link) {
146     const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
147     const cell = $("<td>")
148       .attr("colspan", 2)
149       .addClass("bg-body-tertiary");
150
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";
154       $("<a>")
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")
158         .appendTo(cell);
159     }
160     if (label) {
161       const link = $link.clone()
162         .text(label.value)
163         .attr("dir", "auto")
164         .appendTo(cell);
165       if (!isOfExpectedLanguage(label)) {
166         link.attr("lang", label.language);
167         link.after($("<sup>").text(" " + localeName.of(label.language)));
168       }
169     }
170     if (article) {
171       const link = $("<a>")
172         .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
173         .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
174         .attr("dir", "auto")
175         .appendTo(cell);
176       if (label) {
177         link.before(" (");
178         link.after(")");
179       }
180       if (!isOfExpectedLanguage(article)) {
181         link.attr("lang", article.language);
182         link.after($("<sup>").text(" " + localeName.of(article.language)));
183       }
184     }
185     if (description) {
186       const text = $("<div>")
187         .text(description.value)
188         .addClass("small")
189         .attr("dir", "auto")
190         .appendTo(cell);
191       if (!isOfExpectedLanguage(description)) {
192         text.attr("lang", description.language);
193       }
194     }
195     return $("<tr>").append(cell);
196   }
197 }());