MediaWiki:Common.js: Difference between revisions
Appearance
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
(function () { | (function () { | ||
var root = document.getElementById('pc-podcast'); | var root = document.getElementById('pc-podcast'); | ||
if (!root) return; | if (!root) return; | ||
var feed = root.dataset.feed, page = root.dataset.page; | var feed = root.dataset.feed, page = root.dataset.page; | ||
var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav'); | var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav'); | ||
| Line 132: | Line 13: | ||
return e; | return e; | ||
} | } | ||
function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; } | function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; } | ||
function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; } | function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; } | ||
| Line 151: | Line 33: | ||
var s = parseInt(v, 10); | var s = parseInt(v, 10); | ||
if (!s) return ''; | if (!s) return ''; | ||
var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60; | var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; | ||
return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2); | return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2); | ||
} | } | ||
function fromXml(xml) { | function fromXml(xml) { | ||
var doc = new DOMParser().parseFromString(xml, 'application/xml'); | var doc = new DOMParser().parseFromString(xml, 'application/xml'); | ||
var chImg = doc.getElementsByTagName('itunes:image')[0]; | var chImg = doc.getElementsByTagName('itunes:image')[0]; | ||
var chCover = chImg ? chImg.getAttribute('href') : ''; | var chCover = chImg ? chImg.getAttribute('href') : ''; | ||
return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) { | return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) { | ||
var img = it.getElementsByTagName('itunes:image')[0]; | var img = it.getElementsByTagName('itunes:image')[0]; | ||
var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode'); | var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode'); | ||
return { | return { | ||
title: txt(it, 'title'), link: txt(it, 'link'), date: txt(it, 'pubDate'), | title: txt(it, 'title'), | ||
link: txt(it, 'link'), | |||
date: txt(it, 'pubDate'), | |||
dur: fmtDur(txt(it, 'itunes:duration')), | dur: fmtDur(txt(it, 'itunes:duration')), | ||
tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '', | tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '', | ||
| Line 172: | Line 57: | ||
}); | }); | ||
} | } | ||
function fromJson(j) { | function fromJson(j) { | ||
var chCover = j.feed && j.feed.image; | var chCover = j.feed && j.feed.image; | ||
return (j.items || []).map(function (it) { | return (j.items || []).map(function (it) { | ||
return { title: it.title, link: it.link, date: it.pubDate, dur: '', tag: '', | return { | ||
title: it.title, | |||
link: it.link, | |||
date: it.pubDate, | |||
dur: '', | |||
tag: '', | |||
desc: plain(it.description), | |||
cover: it.thumbnail || chCover | |||
}; | |||
}); | }); | ||
} | } | ||
| Line 184: | Line 76: | ||
var c = el('article', 'pc-card'); | var c = el('article', 'pc-card'); | ||
var cover = el('div', 'pc-cover'); | var cover = el('div', 'pc-cover'); | ||
if (safeUrl(ep.cover)) { | if (safeUrl(ep.cover)) { | ||
var img = el('img'); img.alt = ''; img.loading = 'lazy'; | var img = el('img'); | ||
img.onerror = function () { | img.alt = ''; | ||
img.src = ep.cover; cover.appendChild(img); | img.loading = 'lazy'; | ||
} else cover.textContent = '🎙️'; | img.onerror = function () { cover.textContent = '🎙️'; }; | ||
img.src = ep.cover; | |||
cover.appendChild(img); | |||
} else { | |||
cover.textContent = '🎙️'; | |||
} | |||
var body = el('div', 'pc-body'); | var body = el('div', 'pc-body'); | ||
var meta = el('div', 'pc-meta'); | var meta = el('div', 'pc-meta'); | ||
if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag)); | if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag)); | ||
var ds = fmtDate(ep.date); if (ds) meta.appendChild(el('span', 'pc-pill', ds)); | var ds = fmtDate(ep.date); | ||
if (ds) meta.appendChild(el('span', 'pc-pill', ds)); | |||
if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur)); | if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur)); | ||
if (meta.children.length) body.appendChild(meta); | if (meta.children.length) body.appendChild(meta); | ||
var href = safeUrl(ep.link) || safeUrl(page); | var href = safeUrl(ep.link) || safeUrl(page); | ||
var title = el('div', 'pc-title'); title.setAttribute('role', 'heading'); title.setAttribute('aria-level', '3'); | var title = el('div', 'pc-title'); | ||
title.setAttribute('role', 'heading'); | |||
title.setAttribute('aria-level', '3'); | |||
var a = el('a', '', ep.title || 'Untitled episode'); | var a = el('a', '', ep.title || 'Untitled episode'); | ||
if (href) { a.href = href; a.target = '_blank'; a.rel = 'noopener'; } | if (href) { | ||
title.appendChild(a); body.appendChild(title); | a.href = href; | ||
a.target = '_blank'; | |||
a.rel = 'noopener'; | |||
} | |||
title.appendChild(a); | |||
body.appendChild(title); | |||
body.appendChild(el('p', 'pc-desc', ep.desc)); | body.appendChild(el('p', 'pc-desc', ep.desc)); | ||
c.appendChild(cover); c.appendChild(body); | c.appendChild(cover); | ||
c.appendChild(body); | |||
if (href) { | if (href) { | ||
var o = el('div', 'mc-open') | var o = el('div', 'mc-open'); | ||
oa.href = href; oa.target = '_blank'; oa.rel = 'noopener'; | var oa = el('a'); | ||
oa.title = 'Open episode'; oa.setAttribute('aria-label', 'Open episode'); | oa.href = href; | ||
oa.appendChild(el('span', 'mc-open-icon')); o.appendChild(oa); c.appendChild(o); | oa.target = '_blank'; | ||
oa.rel = 'noopener'; | |||
oa.title = 'Open episode'; | |||
oa.setAttribute('aria-label', 'Open episode'); | |||
oa.appendChild(el('span', 'mc-open-icon')); | |||
o.appendChild(oa); | |||
c.appendChild(o); | |||
} | } | ||
return c; | return c; | ||
} | } | ||
| Line 216: | Line 133: | ||
function layout() { | function layout() { | ||
var n = cards.length; | var n = cards.length; | ||
if (!n) return; | |||
cards.forEach(function (c, i) { | cards.forEach(function (c, i) { | ||
var pos = (i - active + n) % n; | var pos = (i - active + n) % n; | ||
| Line 222: | Line 141: | ||
c.setAttribute('aria-hidden', pos ? 'true' : 'false'); | c.setAttribute('aria-hidden', pos ? 'true' : 'false'); | ||
}); | }); | ||
dots.forEach(function (d, i) { d.setAttribute('aria-current', i === active ? 'true' : 'false'); }); | |||
dots.forEach(function (d, i) { | |||
d.setAttribute('aria-current', i === active ? 'true' : 'false'); | |||
}); | |||
} | |||
function go(delta) { | |||
active = (active + delta + cards.length) % cards.length; | |||
layout(); | |||
} | } | ||
function arrow(label, path, delta) { | function arrow(label, path, delta) { | ||
var b = el('button', 'pc-btn'); b.type = 'button'; b.setAttribute('aria-label', label); | var b = el('button', 'pc-btn'); | ||
b.type = 'button'; | |||
b.setAttribute('aria-label', label); | |||
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>'; | b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>'; | ||
b.addEventListener('click', function () { go(delta); }); | b.addEventListener('click', function () { go(delta); }); | ||
| Line 234: | Line 162: | ||
function render(list) { | function render(list) { | ||
if (!list || !list.length) return; | if (!list || !list.length) return; | ||
list = list.filter(function (e) { return e.title; }) | |||
.sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); }).slice(0, 3); | list = list | ||
.filter(function (e) { return e.title; }) | |||
.sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); }) | |||
.slice(0, 3); | |||
if (!list.length) return; | if (!list.length) return; | ||
list.forEach(function (ep) { | deck.innerHTML = ''; | ||
nav.innerHTML = ''; | |||
cards = []; | |||
dots = []; | |||
active = 0; | |||
}); | list.forEach(function (ep) { | ||
var card = buildCard(ep); | |||
cards.push(card); | |||
deck.appendChild(card); | |||
}); | |||
for (var k = cards.length; k < 3; k++) { | for (var k = cards.length; k < 3; k++) { | ||
var g = el('div', 'pc-card pc-ghost'); | |||
g.dataset.pos = k; | |||
g.setAttribute('aria-hidden', 'true'); | |||
deck.appendChild(g); | |||
} | } | ||
root.classList.add('is-loaded' | root.classList.add('is-loaded'); | ||
if (cards.length > 1) { | if (cards.length > 1) { | ||
var wrap = el('div', 'pc-dots'); | var wrap = el('div', 'pc-dots'); | ||
cards.forEach(function (_, i) { | cards.forEach(function (_, i) { | ||
var d = el('button', 'pc-dot'); d.type = 'button'; d.setAttribute('aria-label', 'Episode ' + (i + 1)); | var d = el('button', 'pc-dot'); | ||
d.addEventListener('click', function () { active = i; layout(); }); | d.type = 'button'; | ||
dots.push(d); wrap.appendChild(d); | d.setAttribute('aria-label', 'Episode ' + (i + 1)); | ||
d.addEventListener('click', function () { | |||
active = i; | |||
layout(); | |||
}); | |||
dots.push(d); | |||
wrap.appendChild(d); | |||
}); | }); | ||
nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1)); | nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1)); | ||
nav.appendChild(wrap); | nav.appendChild(wrap); | ||
nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1)); | nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1)); | ||
var sx = null; | var sx = null; | ||
deck.addEventListener('pointerdown', function (e) { sx = e.clientX; }); | deck.addEventListener('pointerdown', function (e) { sx = e.clientX; }); | ||
deck.addEventListener('pointerup', function (e) { | deck.addEventListener('pointerup', function (e) { | ||
if (sx === null) return; | if (sx === null) return; | ||
var dx = e.clientX - sx; sx = null; | var dx = e.clientX - sx; | ||
sx = null; | |||
if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1); | if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1); | ||
}); | }); | ||
} | } | ||
layout(); | layout(); | ||
} | } | ||
| Line 285: | Line 229: | ||
.catch(function () { | .catch(function () { | ||
return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed)) | return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed)) | ||
.then(function (r) { return r.json(); }).then(fromJson); | .then(function (r) { return r.json(); }) | ||
.then(fromJson); | |||
}) | }) | ||
.then(render) | .then(render) | ||
.catch(function () { /* leave | .catch(function () { /* leave empty */ }); | ||
})(); | })(); | ||
Revision as of 22:22, 3 October 2026
(function () {
var root = document.getElementById('pc-podcast');
if (!root) return;
var feed = root.dataset.feed, page = root.dataset.page;
var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav');
var cards = [], dots = [], active = 0;
function el(tag, cls, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (text) e.textContent = text;
return e;
}
function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; }
function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; }
function plain(html) {
return new DOMParser().parseFromString(html || '', 'text/html').body.textContent.replace(/\s+/g, ' ').trim();
}
function parseDate(s) {
var d = new Date(s);
if (isNaN(d)) d = new Date(String(s).replace(' ', 'T') + 'Z');
return d;
}
function fmtDate(s) {
var d = parseDate(s);
return isNaN(d) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}
function fmtDur(v) {
if (!v) return '';
if (v.indexOf(':') > -1) return v.replace(/^00:/, '').replace(/^0(\d:)/, '$1');
var s = parseInt(v, 10);
if (!s) return '';
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2);
}
function fromXml(xml) {
var doc = new DOMParser().parseFromString(xml, 'application/xml');
var chImg = doc.getElementsByTagName('itunes:image')[0];
var chCover = chImg ? chImg.getAttribute('href') : '';
return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) {
var img = it.getElementsByTagName('itunes:image')[0];
var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode');
return {
title: txt(it, 'title'),
link: txt(it, 'link'),
date: txt(it, 'pubDate'),
dur: fmtDur(txt(it, 'itunes:duration')),
tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '',
desc: plain(txt(it, 'description')),
cover: (img && img.getAttribute('href')) || chCover
};
});
}
function fromJson(j) {
var chCover = j.feed && j.feed.image;
return (j.items || []).map(function (it) {
return {
title: it.title,
link: it.link,
date: it.pubDate,
dur: '',
tag: '',
desc: plain(it.description),
cover: it.thumbnail || chCover
};
});
}
function buildCard(ep) {
var c = el('article', 'pc-card');
var cover = el('div', 'pc-cover');
if (safeUrl(ep.cover)) {
var img = el('img');
img.alt = '';
img.loading = 'lazy';
img.onerror = function () { cover.textContent = '🎙️'; };
img.src = ep.cover;
cover.appendChild(img);
} else {
cover.textContent = '🎙️';
}
var body = el('div', 'pc-body');
var meta = el('div', 'pc-meta');
if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag));
var ds = fmtDate(ep.date);
if (ds) meta.appendChild(el('span', 'pc-pill', ds));
if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur));
if (meta.children.length) body.appendChild(meta);
var href = safeUrl(ep.link) || safeUrl(page);
var title = el('div', 'pc-title');
title.setAttribute('role', 'heading');
title.setAttribute('aria-level', '3');
var a = el('a', '', ep.title || 'Untitled episode');
if (href) {
a.href = href;
a.target = '_blank';
a.rel = 'noopener';
}
title.appendChild(a);
body.appendChild(title);
body.appendChild(el('p', 'pc-desc', ep.desc));
c.appendChild(cover);
c.appendChild(body);
if (href) {
var o = el('div', 'mc-open');
var oa = el('a');
oa.href = href;
oa.target = '_blank';
oa.rel = 'noopener';
oa.title = 'Open episode';
oa.setAttribute('aria-label', 'Open episode');
oa.appendChild(el('span', 'mc-open-icon'));
o.appendChild(oa);
c.appendChild(o);
}
return c;
}
function layout() {
var n = cards.length;
if (!n) return;
cards.forEach(function (c, i) {
var pos = (i - active + n) % n;
c.dataset.pos = pos;
c.inert = pos !== 0;
c.setAttribute('aria-hidden', pos ? 'true' : 'false');
});
dots.forEach(function (d, i) {
d.setAttribute('aria-current', i === active ? 'true' : 'false');
});
}
function go(delta) {
active = (active + delta + cards.length) % cards.length;
layout();
}
function arrow(label, path, delta) {
var b = el('button', 'pc-btn');
b.type = 'button';
b.setAttribute('aria-label', label);
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>';
b.addEventListener('click', function () { go(delta); });
return b;
}
function render(list) {
if (!list || !list.length) return;
list = list
.filter(function (e) { return e.title; })
.sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); })
.slice(0, 3);
if (!list.length) return;
deck.innerHTML = '';
nav.innerHTML = '';
cards = [];
dots = [];
active = 0;
list.forEach(function (ep) {
var card = buildCard(ep);
cards.push(card);
deck.appendChild(card);
});
for (var k = cards.length; k < 3; k++) {
var g = el('div', 'pc-card pc-ghost');
g.dataset.pos = k;
g.setAttribute('aria-hidden', 'true');
deck.appendChild(g);
}
root.classList.add('is-loaded');
if (cards.length > 1) {
var wrap = el('div', 'pc-dots');
cards.forEach(function (_, i) {
var d = el('button', 'pc-dot');
d.type = 'button';
d.setAttribute('aria-label', 'Episode ' + (i + 1));
d.addEventListener('click', function () {
active = i;
layout();
});
dots.push(d);
wrap.appendChild(d);
});
nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1));
nav.appendChild(wrap);
nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1));
var sx = null;
deck.addEventListener('pointerdown', function (e) { sx = e.clientX; });
deck.addEventListener('pointerup', function (e) {
if (sx === null) return;
var dx = e.clientX - sx;
sx = null;
if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1);
});
}
layout();
}
fetch(feed)
.then(function (r) { if (!r.ok) throw new Error('feed'); return r.text(); })
.then(fromXml)
.catch(function () {
return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed))
.then(function (r) { return r.json(); })
.then(fromJson);
})
.then(render)
.catch(function () { /* leave empty */ });
})();