Skip to content

Commit d231880

Browse files
authored
1 parent b7efc9c commit d231880

File tree

60 files changed

+2882
-2432
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2882
-2432
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const toggleButton = document.getElementById('options-toggle');
3+
const panel = document.getElementById('options-panel');
4+
5+
function togglePanel() {
6+
const isVisible = panel.classList.contains('show');
7+
panel.classList.toggle('show', !isVisible);
8+
panel.setAttribute('aria-hidden', isVisible ? 'true' : 'false');
9+
}
10+
11+
toggleButton.addEventListener('click', (e) => {
12+
e.stopPropagation();
13+
togglePanel();
14+
});
15+
16+
document.addEventListener('click', (e) => {
17+
if (!panel.contains(e.target) && e.target !== toggleButton) {
18+
panel.classList.remove('show');
19+
panel.setAttribute('aria-hidden', 'true');
20+
}
21+
});
22+
});
Lines changed: 113 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,143 @@
1-
(() => {
2-
'use strict';
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const languageSelect = document.getElementById('languageSelect');
3+
const versionSelect = document.getElementById('versionSelect');
34

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=';
96

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');
1310

14-
const versionElement = document.getElementById(SELECTOR_VERSION_ID);
15-
if (!versionElement) {
16-
return;
11+
if (overrideUrl) {
12+
currentURL = overrideUrl;
1713
}
1814

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);
2216

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 = {};
3818

19+
fetch(fetchUrl)
20+
.then(response => {
21+
if (!response.ok) throw new Error('Failed to fetch versions');
3922
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+
});
13131

132-
if (resolvedStaticLanguage) {
133-
language = resolvedStaticLanguage;
134-
}
32+
const languageKeys = Object.keys(versionsByLanguage);
33+
const currentLangFromUrl = getLanguageFromUrl(currentURL);
13534

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']);
14038
} 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+
});
16367
}
68+
})
69+
.catch(error => {
70+
console.error(error);
71+
versionSelect.innerHTML = '<option disabled>Error loading versions</option>';
72+
});
16473

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;
16778
}
79+
});
16880

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();
18684

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');
19786

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+
});
20295

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;
204103
}
104+
versionSelect.appendChild(option);
105+
seen.add(item.version);
205106
}
206-
}
107+
});
207108

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);
210114
}
211-
212-
return html;
213115
}
214116

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-
}
223117

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();
231126
}
232127

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() : '';
251132
}
252133

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;
257141
}
258-
});
259-
})();
142+
}
143+
});

0 commit comments

Comments
 (0)