|
1 |
| -(() => { |
2 |
| - 'use strict'; |
| 1 | +document.addEventListener('DOMContentLoaded', () => { |
| 2 | + const languageSelect = document.getElementById('languageSelect'); |
| 3 | + const versionSelect = document.getElementById('versionSelect'); |
3 | 4 |
|
4 |
| - const SELECTOR_VERSION_ID = 'toc-version'; |
5 |
| - const SELECTOR_VERSION_OPTIONS_ID = 'toc-version-options'; |
6 |
| - const SELECTOR_VERSION_WRAPPER_ID = 'toc-version-wrapper'; |
7 |
| - const SELECTOR_VERSION_WRAPPER_ACTIVE_CLASS = 'toc-version-wrapper-active'; |
8 |
| - const LANGUAGE_DEFAULT = 'A_Default'; |
| 5 | + const DEFAULT_URL = 'https://docs.typo3.org/services/versionsJson.php?url='; |
9 | 6 |
|
10 |
| - // it should have at least one more digit than the largest number part in |
11 |
| - // version strings |
12 |
| - const VERSION_SORT_BASE = 100000; |
| 7 | + let currentURL = document.URL; |
| 8 | + const overrideUrl = versionSelect.getAttribute('data-override-url-self'); |
| 9 | + const proxyUrl = versionSelect.getAttribute('data-override-url-proxy'); |
13 | 10 |
|
14 |
| - const versionElement = document.getElementById(SELECTOR_VERSION_ID); |
15 |
| - if (!versionElement) { |
16 |
| - return; |
| 11 | + if (overrideUrl) { |
| 12 | + currentURL = overrideUrl; |
17 | 13 | }
|
18 | 14 |
|
19 |
| - async function retrieveListOfVersions() { |
20 |
| - let urlSelf = document.URL; |
21 |
| - let URL_TEMPLATE = 'https://docs.typo3.org/services/versionsJson.php?url='; |
| 15 | + const fetchUrl = (proxyUrl || DEFAULT_URL) + encodeURIComponent(currentURL); |
22 | 16 |
|
23 |
| - if (versionElement.getAttribute('data-override-url-self')) { |
24 |
| - urlSelf = versionElement.getAttribute('data-override-url-self'); |
25 |
| - URL_TEMPLATE = versionElement.getAttribute('data-override-url-proxy'); |
26 |
| - console.log('AJAX version selector API: Developer mode enabled. Adjust data-override-url-self to simulate different menus. More information: https://docs.typo3.org/other/t3docs/render-guides/main/en-us/Developer/AjaxVersions.html'); |
27 |
| - console.log('Currently: ' + urlSelf); |
28 |
| - console.log('The API PROXY is currently served from: ' + URL_TEMPLATE); |
29 |
| - } |
30 |
| - const url = URL_TEMPLATE + encodeURI(urlSelf); |
31 |
| - |
32 |
| - try { |
33 |
| - const response = await fetch(url); |
34 |
| - if (!response.ok) { |
35 |
| - console.log('AJAX version selector API: Request failure or empty response.'); |
36 |
| - return ''; |
37 |
| - } |
| 17 | + let versionsByLanguage = {}; |
38 | 18 |
|
| 19 | + fetch(fetchUrl) |
| 20 | + .then(response => { |
| 21 | + if (!response.ok) throw new Error('Failed to fetch versions'); |
39 | 22 | return response.json();
|
40 |
| - } catch (e) { |
41 |
| - console.log('AJAX version selector API: Request failed, likely CORS issue. Read the documentation to configure a proxy.'); |
42 |
| - return ''; |
43 |
| - } |
44 |
| - } |
45 |
| - |
46 |
| - function setVersionContent(parentElement, jsonData) { |
47 |
| - const options = document.createElement('dl'); |
48 |
| - |
49 |
| - let defaultLanguage = 'en-us'; |
50 |
| - |
51 |
| - // This is a list of "known" languages. We cannot execute |
52 |
| - // server-side language list parsing, because the versionJson.php |
53 |
| - // file is not under the TYPO3 Documentation Team's direct control. |
54 |
| - // If a language does not match the list defined here it falls back |
55 |
| - // to just using the language key like before. |
56 |
| - // Currently, only german, french and russian translations are used |
57 |
| - // for existing projects. |
58 |
| - let staticLanguages = { |
59 |
| - 'de-de': 'German', |
60 |
| - 'de-at': 'German (Austria)', |
61 |
| - 'de-ch': 'German (Switzerland)', |
62 |
| - 'en-gb': 'English', |
63 |
| - 'fr-fr': 'French', |
64 |
| - 'ru-ru': 'Russian', |
65 |
| - }; |
66 |
| - staticLanguages[defaultLanguage] = LANGUAGE_DEFAULT; // Underscore ensures alphabetical sorted first |
67 |
| - |
68 |
| - if (typeof jsonData !== 'object') { |
69 |
| - console.log('AJAX version selector API: Request failed, no JSON returned.'); |
70 |
| - parentElement.innerHTML = '<p>Versions unavailable.</p>'; |
71 |
| - return; |
72 |
| - } |
73 |
| - |
74 |
| - let unsortedOutput = {'currentfile': {}, 'singlefile': {}}; |
75 |
| - |
76 |
| - // We sort by: |
77 |
| - // - First, english language links: |
78 |
| - // - "main" |
79 |
| - // - then all version numbers, with highest first (12.4, 11.5, 10.5, 9.7, 8.6, ...) |
80 |
| - // - then all "named versions", alphabetically sorted ("alpha", "draft", "testing", "verified") |
81 |
| - // - Then all languages other than english, alphabetically sorted by their name |
82 |
| - // - "main" |
83 |
| - // - "named versions" |
84 |
| - // - "version numbers" |
85 |
| - // - Then all links to "in one file" with the same sorting |
86 |
| - // |
87 |
| - // Thus the array is multidimensional: |
88 |
| - // unsortedOutput[singlefile|currentfile][language][1_main|2_numeric|3_named][...] = [url, title] |
89 |
| - // |
90 |
| - // Example: |
91 |
| - // |
92 |
| - // main |
93 |
| - // 12.4 |
94 |
| - // 11.5 |
95 |
| - // 10.4 |
96 |
| - // 9.5 |
97 |
| - // 8.7 |
98 |
| - // 7.6 |
99 |
| - // draft |
100 |
| - // |
101 |
| - // French: |
102 |
| - // 7.6 |
103 |
| - // |
104 |
| - // Russian: |
105 |
| - // main |
106 |
| - // 12.4 |
107 |
| - // |
108 |
| - // In one file: |
109 |
| - // main |
110 |
| - // 12.4 |
111 |
| - // 11.5 |
112 |
| - // 10.4 |
113 |
| - // 9.5 |
114 |
| - // 8.7 |
115 |
| - // 7.6 |
116 |
| - // draft |
117 |
| - // |
118 |
| - // French: |
119 |
| - // 7.6 |
120 |
| - // |
121 |
| - // Russian: |
122 |
| - // main |
123 |
| - // 12.4 |
124 |
| - |
125 |
| - for (let linkList in jsonData) { |
126 |
| - let currentItem = jsonData[linkList]; |
127 |
| - |
128 |
| - let language = currentItem.language; |
129 |
| - let version = currentItem.version; |
130 |
| - let resolvedStaticLanguage = staticLanguages[language.toLocaleLowerCase()] |
| 23 | + }) |
| 24 | + .then(data => { |
| 25 | + // Group by language |
| 26 | + data.forEach(item => { |
| 27 | + const lang = item.language.toLowerCase(); |
| 28 | + if (!versionsByLanguage[lang]) versionsByLanguage[lang] = []; |
| 29 | + versionsByLanguage[lang].push(item); |
| 30 | + }); |
131 | 31 |
|
132 |
| - if (resolvedStaticLanguage) { |
133 |
| - language = resolvedStaticLanguage; |
134 |
| - } |
| 32 | + const languageKeys = Object.keys(versionsByLanguage); |
| 33 | + const currentLangFromUrl = getLanguageFromUrl(currentURL); |
135 | 34 |
|
136 |
| - // The "1_", "2_", "3_" ensures proper sortability. |
137 |
| - let versionType = '3_named'; |
138 |
| - if (version === 'main') { |
139 |
| - versionType = '1_main'; |
| 35 | + if (languageKeys.length <= 1 && versionsByLanguage['en-us']) { |
| 36 | + // ✅ English only |
| 37 | + renderVersionSelect(versionsByLanguage['en-us']); |
140 | 38 | } else {
|
141 |
| - let versionTrimmed = version.trim(); |
142 |
| - let versionAsFloat = parseFloat(versionTrimmed); |
143 |
| - if (!isNaN(versionAsFloat) && Number(versionTrimmed) === versionAsFloat) { |
144 |
| - // make each number part have the same digit count, allowing to |
145 |
| - // properly sort as a string |
146 |
| - version = version.split('.').map(n => +n + VERSION_SORT_BASE).join('.') |
147 |
| - versionType = '2_numeric'; |
148 |
| - } |
149 |
| - } |
150 |
| - |
151 |
| - // We assume that currentfile and singlefile are always filled in parallel. |
152 |
| - if (!unsortedOutput['currentfile'][language]) { |
153 |
| - unsortedOutput['currentfile'][language] = {}; |
154 |
| - unsortedOutput['singlefile'][language] = {}; |
155 |
| - } |
156 |
| - if (!unsortedOutput['singlefile'][language][versionType]) { |
157 |
| - unsortedOutput['currentfile'][language][versionType] = {}; |
158 |
| - unsortedOutput['singlefile'][language][versionType] = {}; |
159 |
| - } |
160 |
| - if (!unsortedOutput['singlefile'][language][versionType][version]) { |
161 |
| - unsortedOutput['currentfile'][language][versionType][version] = {}; |
162 |
| - unsortedOutput['singlefile'][language][versionType][version] = {}; |
| 39 | + // ✅ Multiple languages |
| 40 | + languageSelect.classList.remove('d-none'); |
| 41 | + versionSelect.innerHTML = '<option disabled selected>Select a version</option>'; |
| 42 | + |
| 43 | + languageKeys.sort().forEach(lang => { |
| 44 | + const option = document.createElement('option'); |
| 45 | + option.value = lang; |
| 46 | + option.textContent = humanizeLanguage(lang); |
| 47 | + languageSelect.appendChild(option); |
| 48 | + }); |
| 49 | + |
| 50 | + // ✅ Preselect based on URL or fallback to English |
| 51 | + const defaultLang = languageKeys.includes(currentLangFromUrl) ? currentLangFromUrl : 'en-us'; |
| 52 | + languageSelect.value = defaultLang; |
| 53 | + |
| 54 | + renderVersionSelect(versionsByLanguage[defaultLang]); |
| 55 | + |
| 56 | + // ✅ Language switch triggers navigation immediately |
| 57 | + languageSelect.addEventListener('change', () => { |
| 58 | + const selectedLang = languageSelect.value; |
| 59 | + const versions = versionsByLanguage[selectedLang]; |
| 60 | + |
| 61 | + if (versions && versions.length > 0) { |
| 62 | + renderVersionSelect(versions); |
| 63 | + const firstVersionUrl = toAbsoluteUrl(versions[0].url); |
| 64 | + window.location.href = firstVersionUrl; |
| 65 | + } |
| 66 | + }); |
163 | 67 | }
|
| 68 | + }) |
| 69 | + .catch(error => { |
| 70 | + console.error(error); |
| 71 | + versionSelect.innerHTML = '<option disabled>Error loading versions</option>'; |
| 72 | + }); |
164 | 73 |
|
165 |
| - unsortedOutput['currentfile'][language][versionType][version] = currentItem.url; |
166 |
| - unsortedOutput['singlefile'][language][versionType][version] = currentItem.singleUrl; |
| 74 | + // ✅ Manual version change |
| 75 | + versionSelect.addEventListener('change', (e) => { |
| 76 | + if (e.target.value) { |
| 77 | + window.location.href = e.target.value; |
167 | 78 | }
|
| 79 | + }); |
168 | 80 |
|
169 |
| - // Leeloo multisort |
170 |
| - let sortedOutput = sortObjectByKey(unsortedOutput); |
171 |
| - |
172 |
| - let content = ''; |
173 |
| - content += addHtmlFromType('currentfile', sortedOutput); |
174 |
| - content += addHtmlFromType('singlefile', sortedOutput); |
175 |
| - |
176 |
| - options.innerHTML = content; |
177 |
| - parentElement.innerHTML = ''; |
178 |
| - parentElement.appendChild(options); |
179 |
| - } |
180 |
| - |
181 |
| - function addHtmlFromType(baseIndexKey, sortedOutput) { |
182 |
| - let html = ''; |
183 |
| - if (baseIndexKey === 'singlefile') { |
184 |
| - html += '<dd><p><details><summary><strong>In one file:</strong></summary>'; |
185 |
| - } |
| 81 | + function renderVersionSelect(versionData) { |
| 82 | + versionSelect.innerHTML = ''; |
| 83 | + const seen = new Set(); |
186 | 84 |
|
187 |
| - for (let language in sortedOutput[baseIndexKey]) { |
188 |
| - if (language != LANGUAGE_DEFAULT) { |
189 |
| - let firstVersionTypeKey = Object.keys(sortedOutput[baseIndexKey][language])[0]; |
190 |
| - let firstVersionKey = Object.keys(sortedOutput[baseIndexKey][language][firstVersionTypeKey])[0]; |
191 |
| - html += '<dd><strong><a href="' + sortedOutput[baseIndexKey][language][firstVersionTypeKey][firstVersionKey] + '">' + language + '</a></strong></dd>'; |
192 |
| - } |
193 |
| - for (let versionType in sortedOutput[baseIndexKey][language]) { |
194 |
| - // versionType: 1_main, 2_numeric, 3_named |
195 |
| - for (let version in sortedOutput[baseIndexKey][language][versionType]) { |
196 |
| - let parsedVersion = version |
| 85 | + const currentVersion = versionSelect.getAttribute('data-current-version'); |
197 | 86 |
|
198 |
| - if (versionType === '2_numeric') { |
199 |
| - // restore version string from before sorting |
200 |
| - parsedVersion = version.split('.').map(n => +n - VERSION_SORT_BASE).join('.') |
201 |
| - } |
| 87 | + const sortedData = versionData.sort((a, b) => { |
| 88 | + const priority = (v) => { |
| 89 | + if (v === 'main') return Infinity; |
| 90 | + const num = parseFloat(v); |
| 91 | + return isNaN(num) ? -1 : num; |
| 92 | + }; |
| 93 | + return priority(b.version) - priority(a.version); |
| 94 | + }); |
202 | 95 |
|
203 |
| - html += '<dd><a href="' + sortedOutput[baseIndexKey][language][versionType][version] + '">' + parsedVersion + '</a></dd>'; |
| 96 | + sortedData.forEach(item => { |
| 97 | + if (!seen.has(item.version)) { |
| 98 | + const option = document.createElement('option'); |
| 99 | + option.value = toAbsoluteUrl(item.url); |
| 100 | + option.textContent = item.version; |
| 101 | + if (item.version === currentVersion) { |
| 102 | + option.selected = true; |
204 | 103 | }
|
| 104 | + versionSelect.appendChild(option); |
| 105 | + seen.add(item.version); |
205 | 106 | }
|
206 |
| - } |
| 107 | + }); |
207 | 108 |
|
208 |
| - if (baseIndexKey === 'singlefile') { |
209 |
| - html += '</details></p></dd>'; |
| 109 | + if (versionSelect.options.length === 0) { |
| 110 | + const option = document.createElement('option'); |
| 111 | + option.textContent = 'No versions available'; |
| 112 | + option.disabled = true; |
| 113 | + versionSelect.appendChild(option); |
210 | 114 | }
|
211 |
| - |
212 |
| - return html; |
213 | 115 | }
|
214 | 116 |
|
215 |
| - function addListOfVersions() { |
216 |
| - const versionWrapperElement = document.getElementById(SELECTOR_VERSION_WRAPPER_ID); |
217 |
| - const versionOptions = document.getElementById(SELECTOR_VERSION_OPTIONS_ID); |
218 |
| - |
219 |
| - versionWrapperElement.classList.toggle(SELECTOR_VERSION_WRAPPER_ACTIVE_CLASS); |
220 |
| - if (versionOptions.dataset.ready) { |
221 |
| - return; |
222 |
| - } |
223 | 117 |
|
224 |
| - retrieveListOfVersions().then(data => { |
225 |
| - if (data === '') { |
226 |
| - data = '<p>No data available.</p>'; |
227 |
| - } |
228 |
| - setVersionContent(versionOptions, data); |
229 |
| - versionOptions.dataset.ready = 'true'; |
230 |
| - }); |
| 118 | + function humanizeLanguage(langCode) { |
| 119 | + const map = { |
| 120 | + 'en-us': 'English', |
| 121 | + 'de-de': 'German', |
| 122 | + 'fr-fr': 'French', |
| 123 | + 'ru-ru': 'Russian' |
| 124 | + }; |
| 125 | + return map[langCode] || langCode.toUpperCase(); |
231 | 126 | }
|
232 | 127 |
|
233 |
| - function sortObjectByKey(obj) { |
234 |
| - if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) { |
235 |
| - const sortedObj = {}; |
236 |
| - |
237 |
| - // Separate numeric and string keys |
238 |
| - const numericKeys = Object.keys(obj).filter(key => !isNaN(key)).sort((a, b) => b - a); |
239 |
| - const stringKeys = Object.keys(obj).filter(key => isNaN(key)).sort(); |
240 |
| - |
241 |
| - // Combine the sorted keys |
242 |
| - const keys = [...numericKeys, ...stringKeys]; |
243 |
| - |
244 |
| - keys.forEach(key => { |
245 |
| - sortedObj[key] = sortObjectByKey(obj[key]); |
246 |
| - }); |
247 |
| - return sortedObj; |
248 |
| - } else { |
249 |
| - return obj; |
250 |
| - } |
| 128 | + function getLanguageFromUrl(url) { |
| 129 | + const langRegex = /\/([a-z]{2}-[a-z]{2})\//i; |
| 130 | + const match = url.match(langRegex); |
| 131 | + return match ? match[1].toLowerCase() : ''; |
251 | 132 | }
|
252 | 133 |
|
253 |
| - versionElement.addEventListener('click', addListOfVersions); |
254 |
| - versionElement.addEventListener('keypress', (e) => { |
255 |
| - if (e.key === 'Enter') { |
256 |
| - addListOfVersions() |
| 134 | + function toAbsoluteUrl(url) { |
| 135 | + try { |
| 136 | + const link = document.createElement('a'); |
| 137 | + link.href = url; |
| 138 | + return link.href; |
| 139 | + } catch { |
| 140 | + return url; |
257 | 141 | }
|
258 |
| - }); |
259 |
| -})(); |
| 142 | + } |
| 143 | +}); |
0 commit comments