]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/element.js
Remove inkscape:collect attributes from osm forum icon
[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()), "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";
7
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");
11     e.preventDefault();
12   });
13
14   $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
15
16   OSM.Element = function (map, type) {
17     const page = {};
18     let scrollStartObserver, scrollEndObserver;
19
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();
25       });
26     };
27
28     page.load = function (path, id, version) {
29       initVersionsNavigation();
30       page._addObject(type, id, version, true);
31       abortController = new AbortController();
32     };
33
34     page.unload = function () {
35       page._removeObject();
36       scrollStartObserver?.disconnect();
37       scrollStartObserver = null;
38       scrollEndObserver?.disconnect();
39       scrollEndObserver = null;
40       abortController?.abort();
41     };
42
43     page._addObject = function () {};
44     page._removeObject = function () {};
45
46     function initVersionsNavigation() {
47       scrollToActiveVersion();
48
49       const $scrollableList = $("#versions-navigation-list-middle");
50       const [scrollableFirstItem] = $scrollableList.children().first();
51       const [scrollableLastItem] = $scrollableList.children().last();
52
53       if (scrollableFirstItem) {
54         scrollStartObserver = createScrollObserver("#versions-navigation-list-start", "2px 0px");
55         scrollStartObserver.observe(scrollableFirstItem);
56       }
57
58       if (scrollableLastItem) {
59         scrollEndObserver = createScrollObserver("#versions-navigation-list-end", "-2px 0px");
60         scrollEndObserver.observe(scrollableLastItem);
61       }
62     }
63
64     function createScrollObserver(shadowTarget, shadowOffset) {
65       const threshold = 0.95;
66       return new IntersectionObserver(([entry]) => {
67         const floating = entry.intersectionRatio < threshold;
68         $(shadowTarget)
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
71       }, { threshold });
72     }
73
74     return page;
75   };
76
77   OSM.MappedElement = function (map, type) {
78     const page = OSM.Element(map, type);
79
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);
87           });
88         }
89       });
90     };
91
92     page._removeObject = function () {
93       map.removeObject();
94     };
95
96     return page;
97   };
98
99   function scrollToActiveVersion() {
100     const [scrollableList] = $("#versions-navigation-list-middle");
101
102     if (!scrollableList) return;
103
104     const [activeStartItem] = $("#versions-navigation-list-start #versions-navigation-active-page-item");
105     const [activeScrollableItem] = $("#versions-navigation-list-middle #versions-navigation-active-page-item");
106
107     if (activeStartItem) {
108       scrollableList.scrollLeft = 0;
109     } else if (activeScrollableItem) {
110       scrollableList.scrollLeft = Math.round(activeScrollableItem.offsetLeft - (scrollableList.offsetWidth / 2) + (activeScrollableItem.offsetWidth / 2));
111     } else {
112       scrollableList.scrollLeft = scrollableList.scrollWidth - scrollableList.offsetWidth;
113     }
114   }
115
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",
123       format: "json",
124       origin: "*",
125       ids: items.join("|"),
126       props: "labels|sitelinks/urls|claims|descriptions",
127       languages: languagesToRequest.join("|"),
128       sitefilter: wikisToRequest.join("|")
129     }), {
130       headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
131       signal: abortController?.signal
132     })
133       .then(response => response.ok ? response.json() : Promise.reject(response))
134       .then(({ entities }) => {
135         if (!entities) return Promise.reject(entities);
136         $btn
137           .closest("tr")
138           .after(
139             items
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}"]`)))
144           );
145       })
146       .catch(() => $btn.prop("disabled", false));
147   }
148
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];
153     const data = {
154       qid: entity.id,
155       label: languagesToRequest.reduce(toFirstOf("labels"), null),
156       icon: [
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)
163     };
164     if (data.article) data.article.language = data.article.site.replace("wiki", "");
165     return data;
166   }
167
168   function renderWikidataResponse({ icon, label, article, description }, $link) {
169     const cell = $("<td>")
170       .attr("colspan", 2)
171       .addClass("bg-body-tertiary");
172
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";
176       $("<a>")
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")
180         .appendTo(cell);
181     }
182     if (label) {
183       const link = $link.clone()
184         .text(label.value)
185         .attr("dir", "auto")
186         .appendTo(cell);
187       if (!isOfExpectedLanguage(label)) {
188         link.attr("lang", label.language);
189         link.after($("<sup>").text(" " + localeName.of(label.language)));
190       }
191     }
192     if (article) {
193       const link = $("<a>")
194         .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
195         .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
196         .attr("dir", "auto")
197         .appendTo(cell);
198       if (label) {
199         link.before(" (");
200         link.after(")");
201       }
202       if (!isOfExpectedLanguage(article)) {
203         link.attr("lang", article.language);
204         link.after($("<sup>").text(" " + localeName.of(article.language)));
205       }
206     }
207     if (description) {
208       const text = $("<div>")
209         .text(description.value)
210         .addClass("small")
211         .attr("dir", "auto")
212         .appendTo(cell);
213       if (!isOfExpectedLanguage(description)) {
214         text.attr("lang", description.language);
215       }
216     }
217     return $("<tr>").append(cell);
218   }
219 }());