MediaWiki:Gadget-CargoHelperCore.js
From Angelina Jordan Wiki
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Gadget-CargoHelperCore.js
Heavy logic: dialog creation, Cargo queries, validation,
insertion, form logic.
Only loaded on demand by Gadget-CargoHelperLoader.js.
*/
( function () {
'use strict';
mw.loader.using([
'oojs-ui-core',
'oojs-ui-windows',
'oojs-ui-widgets',
'mediawiki.util',
'mediawiki.api'
]).then(function () {
// ------------------------------------------------------------
// EXPORT ENTRYPOINT
// ------------------------------------------------------------
window.CargoHelper = window.CargoHelper || {};
window.CargoHelper.openDialog = function () {
// ----------------- Helpers -----------------
function luaCompatibleSanitize(title) {
title = String(title || '');
title = title.replace(/&[#\w]+;/g, '_');
title = title.replace(/[^\w_.-]/g, '_'); // keep alnum, underscore, hyphen, dot
return title;
}
function logError(tag, err) {
if (window.console && console.error) console.error(tag, err);
}
// fetch performances for this page (to list existing perfs)
function fetchPerformancesForPage() {
// Decode page name like decode() does
var page = mw.config.get('wgPageName') || '';
page = page.replace(/_/g, ' '); // convert underscores to spaces
page = $('<textarea/>').html(page).text(); // decode HTML entities just in case
var api = new mw.Api();
return api.get({
action: 'cargoquery',
tables: 'Performances',
fields: 'perfID,event,date,contextstr',
where: 'song="' + page.replace(/"/g, '\\"') + '"',
order_by: 'perfID',
limit: 5000
}).then(function (data) {
if (!data.cargoquery) return [];
return data.cargoquery.map(r => r.title || {}).map(t => ({
perfID: t.perfID || '',
event: t.event || '',
date: t.date || '',
contextstr: t.contextstr || ''
}));
}).catch(function (err) { logError('fetchPerformancesForPage failed:', err); return []; });
}
// fetch ALL distinct contextstr (complete strings) from Performances table
function fetchAllContexts() {
var api = new mw.Api();
// Cargo doesn't have DISTINCT keyword; grouping produces distinct values.
return api.get({
action: 'cargoquery',
tables: 'Performances',
fields: 'contextstr',
group_by: 'contextstr',
order_by: 'contextstr',
limit: 5000
}).then(function (data) {
if (!data.cargoquery) return [];
var arr = data.cargoquery.map(function (r) { return (r.title && r.title.contextstr) ? r.title.contextstr : ''; })
.filter(function (s) { return !!s; });
// preserve query order, already unique via group_by
return arr;
}).catch(function (err) { logError('fetchAllContexts failed:', err); return []; });
}
// compute next local numeric id
function computeNextLocalId(perfs, page) {
var base = luaCompatibleSanitize(page) + '_';
var max = 0;
perfs.forEach(function (p) {
var id = p.perfID || '';
if (id.indexOf(base) !== 0) return;
var tail = id.slice(base.length);
var m = tail.match(/^(\d+)/);
if (!m) return;
var n = parseInt(m[1], 10);
if (!isNaN(n) && n > max) max = n;
});
return String(max + 1);
}
// Build performance template invocation (literal wikitext)
function buildPerformanceTemplate(params) {
var lines = ['{{Performance'];
if (params.song) lines.push('|song=' + params.song);
if (params.event) lines.push('|event=' + params.event);
if (params.context) lines.push('|context=' + params.context);
if (params.date) lines.push('|date=' + params.date);
if (params.type) lines.push('|type=' + params.type);
if (params.pos) lines.push('|pos=' + params.pos);
if (params.withField) lines.push('|with=' + params.withField);
if (params.comment) lines.push('|comment=' + params.comment);
if (params.id) lines.push('|id=' + params.id);
lines.push('}}');
return lines.join('\n');
}
// Build video template invocation (literal wikitext)
function buildVideoTemplate(params) {
var lines = ['{{Video'];
if (params.pid) lines.push('|pid=' + params.pid);
if (params.url) lines.push('|url=' + params.url);
if (params.duration) lines.push('|duration=' + params.duration);
if (params.quality) lines.push('|quality=' + params.quality);
if (params.comment) lines.push('|comment=' + params.comment);
lines.push('}}');
return lines.join('\n');
}
// ----------------- Validation -----------------
function validIsoLeadingDate(s) {
// Accepts "YYYY", "YYYY-MM", "YYYY-MM-DD" at the start, optionally followed by space + freeform.
// Examples accepted: "2020", "2020-05", "2020-05-12", "2020-05-12 extra notes"
if (!s) return false;
var m = s.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:\s.*)?$/);
if (!m) return false;
// If month exists, must be 01-12; if day exists, must be 01-31 (basic check).
if (m[2]) {
var mm = parseInt(m[2], 10);
if (mm < 1 || mm > 12) return false;
}
if (m[3]) {
var dd = parseInt(m[3], 10);
if (dd < 1 || dd > 31) return false;
}
return true;
}
function validUrl(s) {
if (!s) return false;
// Accept absolute http(s) or protocol-relative //.
return /^(https?:\/\/|\/\/).+/i.test(s.trim());
}
// ----------------- OOUI dialog -----------------
function openDialog(existingPerfs, allContexts) {
// ------------------- Widgets -------------------
var modeSelect = new OO.ui.DropdownInputWidget({
options: [
{ data: 'existing', label: 'Use existing performance' },
{ data: 'new', label: 'Create new performance' }
],
value: existingPerfs.length ? 'existing' : 'new'
});
var existingPerfDropdown = null;
if (existingPerfs.length) {
existingPerfDropdown = new OO.ui.DropdownInputWidget({
options: existingPerfs.map(function(p) {
var lastUnd = (p.perfID || '').lastIndexOf('_');
var localId = (lastUnd >= 0) ? p.perfID.slice(lastUnd + 1) : p.perfID;
return { data: p.perfID, label: localId + ' — ' + (p.event || '') + ' (' + (p.date || '') + ')' };
}),
placeholder: 'Choose existing performance'
});
}
var eventInput = new OO.ui.TextInputWidget();
var dateInput = new OO.ui.TextInputWidget();
var contextCombo = new OO.ui.ComboBoxInputWidget({
options: allContexts.map(c => ({ data: c, label: c })),
placeholder: 'Type or pick a full context string'
});
var originalContextOptions = allContexts.map(c => ({ data: c, label: c }));
// Dynamic filter for context combo
contextCombo.on('input', function(value) {
var valLower = value.toLowerCase();
contextCombo.clearOptions();
originalContextOptions.forEach(function(opt) {
if (opt.data.toLowerCase().includes(valLower)) {
contextCombo.addOption({ data: opt.data, label: opt.label });
}
});
});
var typeInput = new OO.ui.DropdownInputWidget({
options: [
{ data: '', label: '—' },
{ data: 'recording', label: 'recording' },
{ data: 'track', label: 'track' },
{ data: 'music-video', label: 'music-video' },
{ data: 'live', label: 'live' },
{ data: 'rehearsal', label: 'rehearsal' },
{ data: 'soundcheck', label: 'soundcheck' },
{ data: 'sing-along', label: 'sing-along' },
{ data: 'lip-sync', label: 'lip-sync' }
]
});
var posInput = new OO.ui.TextInputWidget();
var withInput = new OO.ui.TextInputWidget({ multiline: true });
var commentPInput = new OO.ui.TextInputWidget({ multiline: true });
var urlInput = new OO.ui.TextInputWidget();
var hr = new OO.ui.LabelWidget( { label: $( '<hr>' ).attr('style', 'color: gray' ) }); // horizontal line separator
var durationInput = new OO.ui.DropdownInputWidget({
options: [
{ data: '', label: '—' },
{ data: 'full', label: 'full' },
{ data: 'short', label: 'short' },
{ data: 'fragment', label: 'fragment' }
]
});
var qualityInput = new OO.ui.DropdownInputWidget({
options: [
{ data: '', label: '—' },
{ data: 'best', label: 'best' },
{ data: 'good', label: 'good' },
{ data: 'acceptable', label: 'acceptable' },
{ data: 'poor', label: 'poor' }
]
});
var commentVInput = new OO.ui.TextInputWidget({ multiline: true });
// ------------------- Layout -------------------
var form = new OO.ui.FieldsetLayout({ label: 'Add Video / Performance' });
form.addItems([ new OO.ui.FieldLayout(modeSelect, { label: 'Mode' }) ]);
if (existingPerfDropdown) form.addItems([ new OO.ui.FieldLayout(existingPerfDropdown, { label: 'Existing performance', align: 'top' }) ]);
form.addItems([
new OO.ui.FieldLayout(eventInput, { label: 'Event' }),
new OO.ui.FieldLayout(dateInput, { label: 'Date' }),
new OO.ui.FieldLayout(contextCombo, { label: 'Context (complete string, # separated)', align: 'top' }),
new OO.ui.FieldLayout(typeInput, { label: 'Type' }),
new OO.ui.FieldLayout(posInput, { label: 'Position' }),
new OO.ui.FieldLayout(withInput, { label: 'With (partners)' }),
new OO.ui.FieldLayout(commentPInput, { label: 'Performance comment' }),
hr,
new OO.ui.FieldLayout(urlInput, { label: 'Video URL' }),
new OO.ui.FieldLayout(durationInput, { label: 'Duration' }),
new OO.ui.FieldLayout(qualityInput, { label: 'Quality' }),
new OO.ui.FieldLayout(commentVInput, { label: 'Video comment' })
]);
var submitButton = new OO.ui.ButtonWidget({ label: 'Insert', flags: ['primary'] });
var cancelButton = new OO.ui.ButtonWidget({ label: 'Cancel' });
form.$element.append(submitButton.$element).append(cancelButton.$element);
var panel = new OO.ui.PanelLayout({ expanded: true, padded: true, content: [ form ] });
// ------------------- Dialog -------------------
function AddVideoDialog(config) {
AddVideoDialog.super.call(this, config);
}
OO.inheritClass(AddVideoDialog, OO.ui.ProcessDialog);
AddVideoDialog.static.name = 'AddVideoDialog';
AddVideoDialog.static.title = 'Add Video / Performance';
AddVideoDialog.prototype.initialize = function () {
AddVideoDialog.super.prototype.initialize.call(this);
this.$body.append(panel.$element);
this.$element.css({ width: '900px', 'max-width': '95%', left: '50%', transform: 'translateX(-50%)' });
};
var windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
var dlg = new AddVideoDialog();
windowManager.addWindows([ dlg ]);
windowManager.openWindow(dlg);
// ------------------- Mode logic -------------------
// Enable/disable fields based on mode
function updateFields() {
var isExisting = modeSelect.getValue() === 'existing';
if (existingPerfDropdown) {
existingPerfDropdown.setDisabled(!isExisting);
}
if (isExisting) {
// Existing mode → grey fields, populate values
eventInput.setDisabled(true);
dateInput.setDisabled(true);
contextCombo.setDisabled(true);
typeInput.setDisabled(true);
posInput.setDisabled(true);
withInput.setDisabled(true);
commentPInput.setDisabled(true);
var selected = existingPerfDropdown ? existingPerfDropdown.getValue() : null;
var perf = existingPerfs.find(p => p.perfID === selected) || {};
eventInput.setValue(perf.event || '');
dateInput.setValue(perf.date || '');
contextCombo.setValue(perf.contextstr || '');
typeInput.setValue(perf.type || '');
posInput.setValue(perf.pos || '');
withInput.setValue(perf.with || '');
commentPInput.setValue(perf.comment || '');
} else {
// New mode → clear and enable fields
eventInput.setDisabled(false);
dateInput.setDisabled(false);
contextCombo.setDisabled(false);
typeInput.setDisabled(false);
posInput.setDisabled(false);
withInput.setDisabled(false);
commentPInput.setDisabled(false);
// Clear only performance-related fields
eventInput.setValue('');
dateInput.setValue('');
contextCombo.setValue('');
typeInput.setValue('');
posInput.setValue('');
withInput.setValue('');
commentPInput.setValue('');
}
}
modeSelect.on('change', updateFields);
if (existingPerfDropdown) existingPerfDropdown.on('change', updateFields);
updateFields();
// ------------------- Helper functions -------------------
function buildPerformanceTemplate(params) {
var fields = [];
if (params.song) fields.push("song=" + params.song);
if (params.event) fields.push("event=" + params.event);
if (params.context) fields.push("context=" + params.context);
if (params.date) fields.push("date=" + params.date);
if (params.type) fields.push("type=" + params.type);
if (params.pos) fields.push("pos=" + params.pos);
if (params.withField) fields.push("with=" + params.withField);
if (params.comment) fields.push("comment=" + params.comment);
if (params.id) fields.push("id=" + params.id);
return "{{Performance|" + fields.join('|') + "}}";
}
function buildVideoTemplate(params) {
var fields = [];
if (params.pid) fields.push("pid=" + params.pid);
if (params.url) fields.push("url=" + params.url);
if (params.duration) fields.push("duration=" + params.duration);
if (params.quality) fields.push("quality=" + params.quality);
if (params.comment) fields.push("comment=" + params.comment);
return "{{Video|" + fields.join('|') + "}}";
}
function insertAtCursor(textarea, newText) {
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var original = textarea.value;
textarea.value = original.slice(0, start) + newText + original.slice(end);
textarea.selectionStart = textarea.selectionEnd = start + newText.length;
textarea.scrollTop = textarea.scrollHeight;
}
function formatForInsertion(perfBlock, videoBlock, textarea) {
var start = textarea.selectionStart;
var textBefore = textarea.value.slice(0, start);
var atLineStart = textBefore.endsWith('\n') || textBefore === '';
if (atLineStart) {
if (perfBlock) {
// only add bullets if perfBlock exists
return "* " + perfBlock + "\n*: " + videoBlock;
} else {
// only video block, no bullet for perf
return videoBlock;
}
} else {
// no bullets if not at line start
return (perfBlock || '') + videoBlock;
}
}
// ------------------- Submit (with improved validation) -------------------
submitButton.on('click', function () {
try {
// Clear previous inline error styles
function clearInvalid(widget) {
if (widget && widget.$input) {
widget.$input.css('outline', '').css('box-shadow', '');
} else if (widget && widget.$element) {
widget.$element.css('outline', '').css('box-shadow', '');
}
}
[ eventInput, dateInput, contextCombo, urlInput, posInput, withInput, commentPInput, commentVInput ].forEach(clearInvalid);
// helper to mark invalid and attach handler to clear when edited
function markInvalid(widget) {
if (!widget) return;
if (widget.$input) {
widget.$input.css('outline', '2px solid #d33').css('box-shadow', '0 0 0 2px rgba(211,51,51,0.08)');
widget.$input.one('input', function () { widget.$input.css('outline', '').css('box-shadow', ''); });
} else if (widget.$element) {
widget.$element.css('outline', '2px solid #d33').css('box-shadow', '0 0 0 2px rgba(211,51,51,0.08)');
widget.$element.one('input', function () { widget.$element.css('outline', '').css('box-shadow', ''); });
}
}
// Validation helpers (reuse existing validators)
function isValidIsoLeadingDate(s) {
if (!s) return false;
var m = s.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:\s.*)?$/);
if (!m) return false;
if (m[2]) {
var mm = parseInt(m[2], 10);
if (mm < 1 || mm > 12) return false;
}
if (m[3]) {
var dd = parseInt(m[3], 10);
if (dd < 1 || dd > 31) return false;
}
return true;
}
function isValidUrl(s) {
if (!s) return false;
return /^(https?:\/\/|\/\/).+/i.test(s.trim());
}
// gather values
var page = mw.config.get('wgPageName');
var mode = modeSelect.getValue();
var pidLocal;
// determine pid
if (mode === 'existing') {
if (!existingPerfDropdown) { alert('No existing performance to choose.'); return; }
var fullPerfID = existingPerfDropdown.getValue();
if (!fullPerfID) { alert('Please select an existing performance.'); return; }
var lastUnd = fullPerfID.lastIndexOf('_');
pidLocal = (lastUnd >= 0) ? fullPerfID.slice(lastUnd + 1) : fullPerfID;
} else {
pidLocal = computeNextLocalId(existingPerfs || [], page);
}
// run field validation and collect errors
var errors = [];
// URL is always required
var urlVal = urlInput.getValue() || '';
if (!isValidUrl(urlVal)) {
errors.push('Video URL is required and must start with "http://", "https://" or "//".');
markInvalid(urlInput);
}
// If new performance, date is required and must be valid ISO-leading
var perfDate = dateInput.getValue() || '';
if (mode === 'new') {
if (!perfDate) {
errors.push('Date is required when creating a new performance.');
markInvalid(dateInput);
} else if (!isValidIsoLeadingDate(perfDate)) {
errors.push('Date must start with an ISO date (YYYY, YYYY-MM or YYYY-MM-DD).');
markInvalid(dateInput);
}
}
// optionally check that context is present when creating new perf
var ctxVal = contextCombo.getValue() || '';
if (mode === 'new' && !ctxVal) {
errors.push('Context is required when creating a new performance.');
markInvalid(contextCombo);
}
// If errors, show OOUI dialog and focus first invalid
if (errors.length) {
// Message dialog
function ValidationDialog(config) { ValidationDialog.super.call(this, config); }
OO.inheritClass(ValidationDialog, OO.ui.MessageDialog);
ValidationDialog.static.name = 'ValidationDialog';
ValidationDialog.static.title = 'Validation error';
ValidationDialog.static.actions = [
{ action: 'close', label: 'Close', flags: ['primary'] }
];
var wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
var dlgVal = new ValidationDialog();
wm.addWindows([dlgVal]);
wm.openWindow(dlgVal, {
message: errors.join('\n')
});
// focus first invalid input after the dialog opens
var toFocus = null;
if (!isValidUrl(urlVal)) toFocus = urlInput;
else if (mode === 'new' && (!perfDate || !isValidIsoLeadingDate(perfDate))) toFocus = dateInput;
else if (mode === 'new' && !ctxVal) toFocus = contextCombo;
if (toFocus) {
// small timeout to wait until dialog is visible
setTimeout(function () {
if (toFocus.focus) toFocus.focus();
else if (toFocus.$input) toFocus.$input.focus();
}, 50);
}
return;
}
// Build single-line templates
var perfBlock = '';
if (mode === 'new') {
// single-line performance template
var perfFields = [];
if (page) perfFields.push('song=' + page);
if (eventInput.getValue()) perfFields.push('event=' + eventInput.getValue());
if (contextCombo.getValue()) perfFields.push('context=' + contextCombo.getValue());
if (dateInput.getValue()) perfFields.push('date=' + dateInput.getValue());
if (typeInput.getValue()) perfFields.push('type=' + typeInput.getValue());
if (posInput.getValue()) perfFields.push('pos=' + posInput.getValue());
if (withInput.getValue()) perfFields.push('with=' + withInput.getValue());
if (commentPInput.getValue()) perfFields.push('comment=' + commentPInput.getValue());
if (pidLocal) perfFields.push('id=' + pidLocal);
perfBlock = '{{Performance|' + perfFields.join('|') + '}}';
}
var videoFields = [];
if (pidLocal) videoFields.push('pid=' + pidLocal);
if (urlInput.getValue()) videoFields.push('url=' + urlInput.getValue());
if (durationInput.getValue()) videoFields.push('duration=' + durationInput.getValue());
if (qualityInput.getValue()) videoFields.push('quality=' + qualityInput.getValue());
if (commentVInput.getValue()) videoFields.push('comment=' + commentVInput.getValue());
var videoBlock = '{{Video|' + videoFields.join('|') + '}}';
// Compose combined text according to beginning-of-line rules
var combinedForPreload = (mode === 'new' ? '* ' + perfBlock + '\n*: ' : '') + videoBlock;
// Insert at cursor if editing, otherwise redirect with single preload param
var $textarea = $('#wpTextbox1');
if ($textarea.length) {
var insertText;
// test if cursor at line start
var ta = $textarea[0];
var start = ta.selectionStart;
var textBefore = ta.value.slice(0, start);
var atLineStart = textBefore.endsWith('\n') || textBefore === '';
if (atLineStart) {
insertText = (mode === 'new' ? '* ' + perfBlock + '\n*: ' : '') + videoBlock;
} else {
insertText = (mode === 'new' ? perfBlock : '') + videoBlock;
}
insertAtCursor(ta, insertText);
windowManager.closeWindow(dlg);
} else {
var url = mw.util.getUrl(page, {
action: 'edit',
section: 'new',
preload: 'Template:CargoAddVideoPreload',
'preloadparams[]': [ combinedForPreload ]
});
window.location.href = url;
}
} catch (err) {
logError('Submit handler failed:', err);
alert('Error while preparing insertion (see console).');
}
});
// ------------------- Cancel -------------------
cancelButton.on('click', function() {
windowManager.closeWindow(dlg);
});
}
// ----------------- Wiring / entry -----------------
// Keep a cached copy of perfs for id computation
var existingPerfsArray = [];
function onAddClick() {
// fetch both page-specific performances (for existing selection) and all contexts (global)
var p1 = fetchPerformancesForPage();
var p2 = fetchAllContexts();
Promise.all([p1, p2]).then(function (res) {
existingPerfsArray = res[0] || [];
var allContexts = res[1] || [];
openDialog(existingPerfsArray, allContexts);
}).catch(function (err) {
logError('Failed to fetch data for dialog:', err);
alert('Failed to load data (see console).');
});
}
//---------------------------------------------------------
// 1) Run initial Cargo loads + open the dialog
//---------------------------------------------------------
Promise.all([
fetchPerformancesForPage(),
fetchAllContexts()
]).then(function (res) {
var existingPerfsArray = res[0] || [];
var allContexts = res[1] || [];
openDialog(existingPerfsArray, allContexts);
}).catch(function (err) {
console.error('CargoHelperCore: failed to load data: ', err);
alert('Failed to load data (see console).');
});
}; // end openDialog
}); // mw.loader.using
}());