MediaWiki:Common.js: Unterschied zwischen den Versionen
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Admin (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
| Zeile 608: | Zeile 608: | ||
/* --- Whisky Top-5 | /* --- Whisky Top-5 (rekursiv über Unterkategorien) ----------------------- */ | ||
mw.loader.using(['mediawiki.api']).then(function () { | mw.loader.using(['mediawiki.api']).then(function () { | ||
function get(obj, path) { var | // kleines Hilfs-get ohne optional chaining | ||
function get(obj, path) { | |||
var cur = obj, i; | |||
for (i = 0; i < path.length; i++) { | |||
if (!cur || typeof cur !== 'object') return; | |||
cur = cur[path[i]]; | |||
} | |||
return cur; | |||
} | |||
// Gewichte parsen, z. B. "NASE:1,GESCHMACK:2,ABGANG:1" | |||
function parseWeights(raw, contests) { | function parseWeights(raw, contests) { | ||
var map = {}, parts = (raw||'').split(','), i; | var map = {}, parts = (raw || '').split(','), i; | ||
for (i=0;i<parts.length;i++){ | for (i = 0; i < parts.length; i++) { | ||
var kv = parts[i].split(':'); if (kv.length | var kv = parts[i].split(':'); | ||
if (kv.length === 2) { | |||
var k = kv[0].replace(/^\s+|\s+$/g, ''); | |||
var v = parseFloat(kv[1]); | |||
if (!isNaN(v)) map[k] = v; | |||
} | |||
} | |||
for (i = 0; i < contests.length; i++) { | |||
if (typeof map[contests[i]] !== 'number') map[contests[i]] = 1; | |||
} | } | ||
return map; | return map; | ||
} | } | ||
function | // Holt Artikel (NS0) aus einer Kategorie inkl. ALLER Unterkategorien (BFS), bis limit erreicht ist | ||
var api = new mw.Api() | function fetchCategoryMembersRecursive(rootCat, limit) { | ||
function | var api = new mw.Api(); | ||
var | var visitedCats = {}; | ||
var queue = ['Kategorie:' + rootCat]; // Startkategorie (mit Präfix) | |||
if (cmcontinue) | var pages = []; // { pageid, title } | ||
return api.get( | |||
var | function fetchOneCat(catTitle, cmcontinue) { | ||
for (i=0;i< | var params = { | ||
var cont = get( | action: 'query', | ||
if (cont && | list: 'categorymembers', | ||
return | cmtitle: catTitle, | ||
cmnamespace: '0|14', // 0=Seiten, 14=Kategorien | |||
cmtype: 'page|subcat', | |||
cmlimit: Math.min(200, limit), | |||
format: 'json' | |||
}; | |||
if (cmcontinue) params.cmcontinue = cmcontinue; | |||
return api.get(params).then(function (data) { | |||
var cms = get(data, ['query', 'categorymembers']) || []; | |||
var i; | |||
for (i = 0; i < cms.length; i++) { | |||
var item = cms[i]; | |||
if (item.ns === 0) { | |||
if (pages.length < limit) pages.push({ pageid: String(item.pageid), title: item.title }); | |||
} else if (item.ns === 14) { | |||
var subcatTitle = item.title; // enthält schon "Kategorie:" | |||
if (!visitedCats[subcatTitle]) { | |||
visitedCats[subcatTitle] = true; | |||
queue.push(subcatTitle); | |||
} | |||
} | |||
} | |||
var cont = get(data, ['continue', 'cmcontinue']); | |||
if (cont && pages.length < limit) { | |||
return fetchOneCat(catTitle, cont); | |||
} | |||
return null; | |||
}); | }); | ||
} | } | ||
function loop() { | |||
if (pages.length >= limit || queue.length === 0) { | |||
return Promise.resolve(pages); | |||
} | |||
var nextCat = queue.shift(); | |||
if (visitedCats[nextCat]) return loop(); | |||
visitedCats[nextCat] = true; | |||
return fetchOneCat(nextCat).then(loop); | |||
} | |||
return loop(); | return loop(); | ||
} | } | ||
function fetchRatingsForContest(pageIds, contest) { | // Holt für pageIds die RatePage-Daten zu einem Contest | ||
var api = new mw.Api() | // includeHidden=true => zählt auch, wenn canSee=0 (nur für Ranking) | ||
for (i=0;i<pageIds.length;i+=chunk) chunks.push(pageIds.slice(i,i+chunk)); | function fetchRatingsForContest(pageIds, contest, includeHidden) { | ||
function step(idx){ | var api = new mw.Api(); | ||
if (idx>=chunks.length) return Promise.resolve(res); | var res = {}; // pageId -> { avg, total } | ||
var i, chunk = 50, chunks = []; | |||
for (i = 0; i < pageIds.length; i += chunk) chunks.push(pageIds.slice(i, i + chunk)); | |||
function step(idx) { | |||
if (idx >= chunks.length) return Promise.resolve(res); | |||
var ids = chunks[idx]; | var ids = chunks[idx]; | ||
return api.get({ | return api.get({ | ||
action:'query', prop:'pagerating', pageids: ids.join('|'), | action: 'query', | ||
prcontest: contest, format:'json' | prop: 'pagerating', | ||
}).then(function(d){ | pageids: ids.join('|'), | ||
var pages = get(d,['query','pages'])||{}; | prcontest: contest, | ||
for ( | format: 'json' | ||
}).then(function (d) { | |||
if (!res[pid]) res[pid] = { avg:null, total:0 }; | var pages = get(d, ['query', 'pages']) || {}; | ||
if (pr && (!('canSee' in pr) || pr.canSee!==0)) { | var pid, pr, hist, k, total, sum, s, c; | ||
for (pid in pages) if (Object.prototype.hasOwnProperty.call(pages, pid)) { | |||
for (k in hist) if (Object.prototype.hasOwnProperty.call(hist,k)) { | pr = pages[pid].pagerating; | ||
if (!res[pid]) res[pid] = { avg: null, total: 0 }; | |||
if (!isNaN(s)&&!isNaN(c)){ total+=c; sum+=s*c; } | if (pr && (includeHidden || !('canSee' in pr) || pr.canSee !== 0)) { | ||
hist = pr.pageRating || {}; | |||
total = 0; sum = 0; | |||
for (k in hist) if (Object.prototype.hasOwnProperty.call(hist, k)) { | |||
s = parseInt(k, 10); c = parseInt(hist[k], 10); | |||
if (!isNaN(s) && !isNaN(c)) { total += c; sum += s * c; } | |||
} | |||
if (total > 0) { | |||
res[pid] = { avg: Math.round((sum / total) * 10) / 10, total: total }; | |||
} | } | ||
} | } | ||
} | } | ||
return step(idx+1); | return step(idx + 1); | ||
}, function(){ return step(idx+1); }); | }, function () { | ||
// Fehler in diesem Chunk: einfach weitermachen | |||
return step(idx + 1); | |||
}); | |||
} | } | ||
return step(0); | return step(0); | ||
} | } | ||
// Gesamt berechnen (gewichtet) + Stimmen summieren | |||
function computeOverall(entry, contests, weights) { | function computeOverall(entry, contests, weights) { | ||
var wSum=0, wAvgSum=0, present=0, totalVotes=0, i; | var wSum = 0, wAvgSum = 0, present = 0, totalVotes = 0, i; | ||
for (i=0;i<contests.length;i++){ | for (i = 0; i < contests.length; i++) { | ||
var c = contests[i] | var c = contests[i]; | ||
if (sc && sc.avg!==null) { var w = weights[c] | var sc = entry.scores[c]; | ||
if (sc && sc.avg !== null) { | |||
var w = (typeof weights[c] === 'number') ? weights[c] : 1; | |||
wSum += w; | |||
wAvgSum += sc.avg * w; | |||
present++; | |||
} | |||
if (sc && sc.total) totalVotes += sc.total; | if (sc && sc.total) totalVotes += sc.total; | ||
} | } | ||
entry.totalVotes = totalVotes; | entry.totalVotes = totalVotes; | ||
entry.overall = (present>0 && wSum>0) ? Math.round((wAvgSum/wSum)*10)/10 : null; | entry.overall = (present > 0 && wSum > 0) ? Math.round((wAvgSum / wSum) * 10) / 10 : null; | ||
} | } | ||
function renderTopN(container, rows, N) { | // Rendering der Top-N Liste (kompakte Karte mit Mini-Balken) | ||
// nur Seiten mit Stimmen | function renderTopN(container, rows, N, minVotes) { | ||
rows = rows.filter(function(r){ return (r.overall!==null) && (r.totalVotes> | // nur Seiten mit Stimmen >= minVotes | ||
rows.sort(function(a,b){ | rows = rows.filter(function (r) { | ||
if (a.overall===null && b.overall!==null) return 1; | return (r.overall !== null) && (r.totalVotes >= minVotes); | ||
if (a.overall!==null && b.overall===null) return -1; | }); | ||
if (b.overall!==a.overall) return (b.overall - a.overall); | |||
if (b.totalVotes!==a.totalVotes) return (b.totalVotes - a.totalVotes); | // Sortierung: Gesamt desc, Stimmen desc, Titel asc | ||
rows.sort(function (a, b) { | |||
if (a.overall === null && b.overall !== null) return 1; | |||
if (a.overall !== null && b.overall === null) return -1; | |||
if (b.overall !== a.overall) return (b.overall - a.overall); | |||
if (b.totalVotes !== a.totalVotes) return (b.totalVotes - a.totalVotes); | |||
return a.title.localeCompare(b.title); | return a.title.localeCompare(b.title); | ||
}); | }); | ||
rows = rows.slice(0, N); | rows = rows.slice(0, N); | ||
var frag = document.createDocumentFragment(); | var frag = document.createDocumentFragment(); | ||
var i, r, item, rank, name, a, right, mini, track, fill, val, votes; | |||
for (i = 0; i < rows.length; i++) { | |||
r = rows[i]; | |||
item = document.createElement('div'); | |||
item.className = 'whisky-top5__item'; | |||
rank = document.createElement('div'); | |||
rank.className = 'whisky-top5__rank'; | |||
rank.textContent = (i + 1); | |||
name = document.createElement('div'); | |||
name.className = 'whisky-top5__name'; | |||
a = document.createElement('a'); | |||
a.href = mw.util.getUrl(r.title); | |||
a.textContent = r.title; | |||
name.appendChild(a); | |||
right = document.createElement('div'); | |||
right.style.minWidth = '160px'; | |||
mini = document.createElement('div'); | |||
mini.className = 'whisky-mini'; | |||
track = document.createElement('div'); | |||
track.className = 'whisky-mini__track'; | |||
fill = document.createElement('div'); | |||
fill.className = 'whisky-mini__fill'; | |||
fill.style.width = Math.max(0, Math.min(100, (r.overall / 10) * 100)) + '%'; | |||
val = document.createElement('span'); | |||
val.className = 'whisky-mini__val'; | |||
val.textContent = (r.overall.toFixed ? r.overall.toFixed(1) : (Math.round(r.overall * 10) / 10)); | |||
track.appendChild(fill); | |||
mini.appendChild(track); | |||
mini.appendChild(val); | |||
votes = document.createElement('div'); | |||
votes.className = 'whisky-top5__votes'; | |||
votes.textContent = r.totalVotes + ' Stimmen'; | votes.textContent = r.totalVotes + ' Stimmen'; | ||
| Zeile 724: | Zeile 825: | ||
} | } | ||
while (container.firstChild) container.removeChild(container.firstChild); | while (container.firstChild) container.removeChild(container.firstChild); | ||
if (rows.length) { | if (rows.length) { | ||
| Zeile 733: | Zeile 833: | ||
} | } | ||
function bootTop5(root){ | // Boot: findet .whisky-top5 Container und rendert sie | ||
var nodes = (root||document).querySelectorAll('.whisky-top5'); | function bootTop5(root) { | ||
var nodes = (root || document).querySelectorAll('.whisky-top5'); | |||
if (!nodes.length) return; | if (!nodes.length) return; | ||
var n; | |||
for (n = 0; n < nodes.length; n++) (function (container) { | |||
if (container.getAttribute('data-top5-init') === '1') return; | |||
container.setAttribute('data-top5-init', '1'); | |||
var cat = container.getAttribute('data-category') || 'Whisky'; // ohne "Kategorie:" | |||
var lim = parseInt(container.getAttribute('data-limit') || '300', 10); | |||
var cnt = parseInt(container.getAttribute('data-count') || '5', 10); | |||
var minVotes = parseInt(container.getAttribute('data-min-votes') || '1', 10); | |||
// Contests + Gewichte | |||
var rawC = container.getAttribute('data-contests') || 'NASE,GESCHMACK,ABGANG,GESAMTEINDRUCK'; | |||
var includeHidden = (container.getAttribute('data-include-hidden') === 'true'); // canSee=0 trotzdem zählen? | |||
var parts = rawC.split(','), contests = [], seen = {}, i; | |||
for (i = 0; i < parts.length; i++) { | |||
var c = parts[i].replace(/^\s+|\s+$/g, ''); | |||
if (c && !seen[c]) { contests.push(c); seen[c] = 1; } | |||
} | |||
var weights = parseWeights(container.getAttribute('data-weights') || '', contests); | |||
container.textContent = 'Lade Topliste …'; | |||
container.textContent = ' | // 1) Alle Artikel aus Kategorie + Subkategorien holen | ||
fetchCategoryMembersRecursive(cat, lim).then(function (members) { | |||
if (!members || !members.length) { | |||
container.textContent = 'Keine Seiten in Kategorie „' + cat + '“.'; | |||
return; | |||
} | |||
var pageIds = [], byId = {}, i; | |||
for (i = 0; i < members.length; i++) { | |||
pageIds.push(members[i].pageid); | |||
byId[members[i].pageid] = { pageid: members[i].pageid, title: members[i].title, scores: {} }; | |||
} | |||
// 2) Je Contest die Werte holen (nacheinander, um Last zu senken) | |||
if ( | function loopContest(idx) { | ||
var | if (idx >= contests.length) return Promise.resolve(); | ||
var contest = contests[idx]; | |||
return fetchRatingsForContest(pageIds, contest, includeHidden).then(function (map) { | |||
var pid; | |||
for (pid in map) if (Object.prototype.hasOwnProperty.call(map, pid)) { | |||
byId[pid].scores[contest] = map[pid]; | |||
} | |||
return loopContest(idx + 1); | |||
}); | |||
} | |||
loopContest(0).then(function () { | |||
// 3) Gesamt berechnen + Array bauen | |||
var rows = [], pid, e; | |||
for (pid in byId) if (Object.prototype.hasOwnProperty.call(byId, pid)) { | |||
e = byId[pid]; | |||
computeOverall(e, contests, weights); | |||
rows.push(e); | |||
} | } | ||
// 4) Rendern | |||
renderTopN(container, rows, cnt, minVotes); | |||
}).catch(function () { | |||
container.textContent = 'Topliste konnte nicht geladen werden.'; | |||
}); | |||
}).catch(function () { | |||
container.textContent = 'Topliste konnte nicht geladen werden.'; | |||
}); | |||
})(nodes[n]); | |||
} | } | ||
if (document.readyState==='loading') { | if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', function(){ bootTop5(document); }); | document.addEventListener('DOMContentLoaded', function () { bootTop5(document); }); | ||
} else { bootTop5(document); } | } else { | ||
mw.hook('wikipage.content').add(function($c){ if($c && $c[0]) bootTop5($c[0]); }); | bootTop5(document); | ||
} | |||
mw.hook('wikipage.content').add(function ($c) { if ($c && $c[0]) bootTop5($c[0]); }); | |||
}); | }); | ||