MediaWiki:Common.js: Difference between revisions
From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Instrument form: set page name via form action URL instead of hidden field |
Instrument form: fill nickname from make/model/serial when empty, for page name formula |
||
| Line 304: | Line 304: | ||
* Instrument form — validation + page name computation | * Instrument form — validation + page name computation | ||
* Requires at least a nickname or one of make/model/serial. | * Requires at least a nickname or one of make/model/serial. | ||
* | * When nickname is empty, fills it with make/model/serial so the | ||
* | * page name formula <Nickname[Instrument]> resolves correctly. | ||
*/ | */ | ||
(function() { | (function() { | ||
| Line 335: | Line 335: | ||
} | } | ||
// | // If no nickname, fill it from make/model/serial for page naming | ||
if (!hasNickname) { | |||
if (hasNickname) { | var parts = [makeVal, modelVal, serialVal].filter(function(s) { | ||
return s !== ''; | return s !== ''; | ||
}).join(' '); | }); | ||
nickname.value = parts.join(' '); | |||
} | } | ||
}); | }); | ||
})(); | })(); | ||
Revision as of 19:30, 30 March 2026
/**
* Blue Railroad Submission - Date to Block Height Converter
* Adds a datepicker that converts dates to Ethereum block heights
* Also shows time ago for news templates
*/
(function() {
'use strict';
// Only run on the Blue Railroad Submission form
if (!document.querySelector('input[name="Blue Railroad Submission[block_height]"]')) {
return;
}
// Reference point: known block and timestamp
// Post-merge average block time is ~12.12 seconds
const AVG_BLOCK_TIME = 12.12;
// Get current block from footer (format: "24,328,442")
function getCurrentBlockFromFooter() {
const footerLink = document.querySelector('a[href*="etherscan.io/block/"]');
if (footerLink) {
const match = footerLink.href.match(/block\/(\d+)/);
if (match) return parseInt(match[1]);
}
return null;
}
// Calculate block height from date
function dateToBlockHeight(targetDate, refBlock, refTimestamp) {
const targetTimestamp = targetDate.getTime() / 1000;
const secondsDiff = refTimestamp - targetTimestamp;
const blocksDiff = Math.round(secondsDiff / AVG_BLOCK_TIME);
return refBlock - blocksDiff;
}
// Add the datepicker UI
function addDatePicker() {
const blockInput = document.querySelector('input[name="Blue Railroad Submission[block_height]"]');
if (!blockInput) return;
const container = document.createElement('div');
container.style.marginTop = '8px';
container.innerHTML = `
<label style="display: block; margin-bottom: 4px; font-size: 0.9em;">
Or pick a date/time:
</label>
<input type="datetime-local" id="br-datepicker" style="padding: 4px; margin-right: 8px;">
<button type="button" id="br-convert-btn" style="padding: 4px 12px; cursor: pointer;">
Convert to Block
</button>
<span id="br-status" style="margin-left: 8px; font-size: 0.9em; color: #666;"></span>
`;
blockInput.parentNode.appendChild(container);
const datePicker = document.getElementById('br-datepicker');
const convertBtn = document.getElementById('br-convert-btn');
const status = document.getElementById('br-status');
// Set default to now
const now = new Date();
datePicker.value = now.toISOString().slice(0, 16);
convertBtn.addEventListener('click', function() {
const selectedDate = new Date(datePicker.value);
if (isNaN(selectedDate.getTime())) {
status.textContent = 'Invalid date';
status.style.color = 'red';
return;
}
const currentBlock = getCurrentBlockFromFooter();
if (!currentBlock) {
status.textContent = 'Could not find reference block';
status.style.color = 'red';
return;
}
const currentTimestamp = Date.now() / 1000;
const estimatedBlock = dateToBlockHeight(selectedDate, currentBlock, currentTimestamp);
if (estimatedBlock > currentBlock) {
status.textContent = 'Date is in the future!';
status.style.color = 'orange';
} else {
status.textContent = '~estimated';
status.style.color = 'green';
}
blockInput.value = estimatedBlock;
});
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addDatePicker);
} else {
addDatePicker();
}
})();
/**
* TimeAgo — shows "X blocks ago" for transcluded templates.
*
* Usage in wikitext:
* <span class="timeago" data-lastmod-page="Template:NewsShorts"></span>
*
* Fetches the last revision timestamp from the API and renders
* a block-based relative time string using Ethereum block heights.
*/
(function() {
'use strict';
// Post-merge average block time is ~12.12 seconds
var AVG_BLOCK_TIME = 12.12;
var spans = document.querySelectorAll('.timeago[data-lastmod-page]');
if (!spans.length) return;
// Format number with commas (e.g., 1234567 -> "1,234,567")
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// Collect unique page titles
var titles = [];
spans.forEach(function(span) {
var t = span.getAttribute('data-lastmod-page');
if (t && titles.indexOf(t) === -1) titles.push(t);
});
// Batch query the API (up to 50 titles per request)
var api = mw.config.get('wgScriptPath') + '/api.php';
var url = api + '?action=query&prop=revisions&rvprop=timestamp&format=json&titles=' +
encodeURIComponent(titles.join('|'));
fetch(url).then(function(r) { return r.json(); }).then(function(data) {
var pages = data.query && data.query.pages || {};
var timestamps = {};
Object.keys(pages).forEach(function(id) {
var page = pages[id];
if (page.revisions && page.revisions[0]) {
timestamps[page.title] = new Date(page.revisions[0].timestamp);
}
});
spans.forEach(function(span) {
var title = span.getAttribute('data-lastmod-page');
var ts = timestamps[title];
if (!ts) {
span.textContent = '';
return;
}
span.textContent = formatBlocksAgo(ts);
span.title = ts.toLocaleString();
});
});
function formatBlocksAgo(date) {
var secondsAgo = Math.floor((Date.now() - date.getTime()) / 1000);
var blocksAgo = Math.round(secondsAgo / AVG_BLOCK_TIME);
if (blocksAgo < 1) {
return 'this block';
} else if (blocksAgo === 1) {
return '1 block ago';
} else {
return formatNumber(blocksAgo) + ' blocks ago';
}
}
})();
/**
* HLS Video Player - Initializes HLS.js for IPFS-hosted videos
*
* Usage in wikitext (via Template:HLSVideo):
* <div class="hls-video-player" data-cid="QmXet6..."></div>
*
* The gadget loads hls.js and initializes players for any element
* with the hls-video-player class and a data-cid attribute.
*
* Supports both HLS streams (CID/master.m3u8) and raw video files.
* Tries HLS first; if the manifest 404s, falls back to direct playback.
*/
(function() {
'use strict';
var IPFS_GATEWAY = 'https://ipfs.delivery-kid.cryptograss.live/ipfs';
var HLS_JS_URL = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
var hlsLoadPromise = null;
// CIDv1 (bafy...) is Base32 lowercase, but MediaWiki capitalizes page titles.
// CIDv0 (Qm...) is Base58 case-sensitive — must not be lowercased.
function normalizeCid(cid) {
if (cid && cid.substring(0, 4) === 'Bafy') {
return cid.toLowerCase();
}
return cid;
}
function loadHls() {
if (typeof Hls !== 'undefined') {
return Promise.resolve();
}
if (hlsLoadPromise) {
return hlsLoadPromise;
}
hlsLoadPromise = new Promise(function(resolve) {
var script = document.createElement('script');
script.src = HLS_JS_URL;
script.onload = resolve;
document.head.appendChild(script);
});
return hlsLoadPromise;
}
function initPlayers() {
var containers = document.querySelectorAll('.hls-video-player[data-cid]:not([data-initialized])');
if (!containers.length) return;
// Mark ALL containers immediately to prevent race conditions
containers.forEach(function(c) {
c.setAttribute('data-initialized', 'true');
});
// Then load hls.js and initialize
loadHls().then(function() {
containers.forEach(initPlayer);
});
}
function createVideoElement(width, maxWidth) {
var video = document.createElement('video');
video.controls = true;
video.playsInline = true;
video.style.width = width;
video.style.maxWidth = maxWidth;
video.style.backgroundColor = '#000';
video.style.display = 'block';
return video;
}
function fallbackToDirectVideo(container, cid, width, maxWidth) {
// CID is a raw video file, not an HLS stream — play directly
var directUrl = IPFS_GATEWAY + '/' + normalizeCid(cid);
container.innerHTML = '';
var video = createVideoElement(width, maxWidth);
video.src = directUrl;
video.addEventListener('error', function() {
container.innerHTML = '<p style="color: red; padding: 1em;">Error loading video. The IPFS content may not be available.</p>';
});
container.appendChild(video);
}
function initPlayer(container) {
var cid = container.getAttribute('data-cid');
var normalCid = normalizeCid(cid);
var width = container.getAttribute('data-width') || '100%';
var maxWidth = container.getAttribute('data-max-width') || '800px';
var hlsSrc = IPFS_GATEWAY + '/' + normalCid + '/master.m3u8';
var video = createVideoElement(width, maxWidth);
container.appendChild(video);
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS — try HLS first, fall back on error
video.src = hlsSrc;
video.addEventListener('error', function() {
fallbackToDirectVideo(container, cid, width, maxWidth);
});
} else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(hlsSrc);
hls.attachMedia(video);
hls.on(Hls.Events.ERROR, function(event, data) {
if (data.fatal) {
// HLS failed (likely no master.m3u8) — try direct video
hls.destroy();
fallbackToDirectVideo(container, cid, width, maxWidth);
}
});
} else {
// No HLS support — try direct video
fallbackToDirectVideo(container, cid, width, maxWidth);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPlayers);
} else {
initPlayers();
}
if (typeof mw !== 'undefined' && mw.hook) {
mw.hook('wikipage.content').add(function($content) {
initPlayers();
});
}
})();
/**
* Instrument form — validation + page name computation
* Requires at least a nickname or one of make/model/serial.
* When nickname is empty, fills it with make/model/serial so the
* page name formula <Nickname[Instrument]> resolves correctly.
*/
(function() {
'use strict';
var form = document.querySelector('form#pfForm');
if (!form) return;
var nickname = document.querySelector('input[name="Instrument[nickname]"]');
if (!nickname) return; // not the instrument form
form.addEventListener('submit', function(e) {
var make = document.querySelector('input[name="Instrument[make]"]');
var model = document.querySelector('input[name="Instrument[model]"]');
var serial = document.querySelector('input[name="Instrument[serial]"]');
var nickVal = nickname.value.trim();
var makeVal = make ? make.value.trim() : '';
var modelVal = model ? model.value.trim() : '';
var serialVal = serial ? serial.value.trim() : '';
var hasNickname = nickVal !== '';
var hasMaker = makeVal !== '' || modelVal !== '' || serialVal !== '';
if (!hasNickname && !hasMaker) {
e.preventDefault();
alert('Please provide either a nickname or at least one of make, model, or serial number.');
return;
}
// If no nickname, fill it from make/model/serial for page naming
if (!hasNickname) {
var parts = [makeVal, modelVal, serialVal].filter(function(s) {
return s !== '';
});
nickname.value = parts.join(' ');
}
});
})();