MediaWiki:Common.js: Difference between revisions
Empty search help; Biography wording |
Add Internet Archive click-to-play embeds for interview watchlist |
||
| (21 intermediate revisions by the same user not shown) | |||
| Line 9: | Line 9: | ||
* Hide retired “(merged duplicate)” roster pages from find/search suggestions and results. | * Hide retired “(merged duplicate)” roster pages from find/search suggestions and results. | ||
* Fill soldier Teammates rails from live Category:Team … membership (no roster-module rebuild). | * Fill soldier Teammates rails from live Category:Team … membership (no roster-module rebuild). | ||
* Inject site hub links into the Minerva hamburger (Sidebar is not used by Minerva 1.43). | |||
*/ | */ | ||
function rbIsMergedDuplicateTitle(title) { | function rbIsMergedDuplicateTitle(title) { | ||
| Line 15: | Line 16: | ||
$(function () { | $(function () { | ||
(function injectMinervaSiteMenu() { | |||
if (mw.config.get('skin') !== 'minerva') { | |||
return; | |||
} | |||
var $drawer = $('#mw-mf-page-left'); | |||
var $after = $('#p-navigation'); | |||
if (!$drawer.length || !$after.length || $drawer.data('rbSiteMenu')) { | |||
return; | |||
} | |||
$drawer.data('rbSiteMenu', 1); | |||
var links = [ | |||
{ title: 'Contents', label: 'Contents', icon: 'listBullet' }, | |||
{ title: 'Start here', label: 'Start here', icon: 'lightbulb' }, | |||
{ title: 'Introduction', label: 'Introduction', icon: 'book' }, | |||
{ title: 'The Stories', label: 'Stories', icon: 'newspaper' }, | |||
{ title: 'Category:Classes', label: 'Classes', icon: 'userGroup' }, | |||
{ title: 'Photos', label: 'Photos', icon: 'imageGallery' }, | |||
{ title: 'Training materials', label: 'Training materials', icon: 'articles' }, | |||
{ title: 'Contribute', label: 'Contribute', icon: 'edit' }, | |||
{ title: 'Contact Us', label: 'Contact Us', icon: 'speechBubbles' }, | |||
{ title: 'About', label: 'About', icon: 'infoFilled' } | |||
]; | |||
var $ul = $('<ul id="p-ritchie" class="toggle-list__list"></ul>'); | |||
links.forEach(function (item) { | |||
var name = 'rb-' + String(item.title).toLowerCase().replace(/[^a-z0-9]+/g, '-'); | |||
var $a = $('<a class="toggle-list-item__anchor"></a>') | |||
.addClass('menu__item--' + name) | |||
.attr('href', mw.util.getUrl(item.title)); | |||
$a.append($('<span class="minerva-icon"></span>').addClass('minerva-icon--' + item.icon)); | |||
$a.append($('<span class="toggle-list-item__label"></span>').text(item.label)); | |||
$ul.append($('<li class="toggle-list-item"></li>').append($a)); | |||
}); | |||
$after.after($ul); | |||
}()); | |||
(function buildHomeFind() { | (function buildHomeFind() { | ||
var $slot = $('.mw-parser-output .home-find-slot').first(); | var $slot = $('.mw-parser-output .home-find-slot').first(); | ||
| Line 510: | Line 546: | ||
}); | }); | ||
var $cards = $('.mw-parser-output .yt-interview | function interviewCardRef($el) { | ||
var yt = String($el.attr('data-youtube-id') || ''); | |||
if (yt) { | |||
return { key: 'yt:' + yt, href: '#yt-' + yt }; | |||
} | |||
var sc = String($el.attr('data-soundcloud-track') || ''); | |||
if (sc) { | |||
return { key: 'sc:' + sc, href: '#sc-' + sc }; | |||
} | |||
return null; | |||
} | |||
var $cards = $('.mw-parser-output .yt-interview').filter(function () { | |||
return interviewCardRef($(this)) !== null; | |||
}); | |||
$cards.each(function (idx) { | $cards.each(function (idx) { | ||
var $card = $(this); | var $card = $(this); | ||
| Line 522: | Line 572: | ||
} | } | ||
var mine = splitSoldiers($card.attr('data-soldiers')); | var mine = splitSoldiers($card.attr('data-soldiers')); | ||
var selfRef = interviewCardRef($card); | |||
var picks = []; | var picks = []; | ||
var seen = {}; | var seen = {}; | ||
function addCard($other, why) { | function addCard($other, why) { | ||
var | var ref = interviewCardRef($other); | ||
if (! | if (!ref || !selfRef || seen[ref.key] || ref.key === selfRef.key) { | ||
return; | return; | ||
} | } | ||
seen[ | seen[ref.key] = 1; | ||
picks.push({ | picks.push({ | ||
href: ref.href, | |||
heading: String($other.attr('data-heading') || $other.find('h3, h2').first().text() || 'Interview'), | heading: String($other.attr('data-heading') || $other.find('h3, h2').first().text() || 'Interview'), | ||
short: String($other.attr('data-short') || ''), | short: String($other.attr('data-short') || ''), | ||
| Line 557: | Line 608: | ||
picks.slice(0, 3).forEach(function (item) { | picks.slice(0, 3).forEach(function (item) { | ||
var label = item.short ? (item.heading + ' — ' + item.short) : item.heading; | var label = item.short ? (item.heading + ' — ' + item.short) : item.heading; | ||
var $a = $('<a></a>').attr('href', | var $a = $('<a></a>').attr('href', item.href).text(label); | ||
var $li = $('<li></li>').append($a); | var $li = $('<li></li>').append($a); | ||
if (item.why === 'same soldier') { | if (item.why === 'same soldier') { | ||
| Line 570: | Line 621: | ||
}()); | }()); | ||
(function | (function buildSoundCloudEmbeds() { | ||
// | var SC_TRACK = /^[0-9]{6,12}$/; | ||
function playSoundCloud($slot, trackId, title) { | |||
var src = 'https://w.soundcloud.com/player/?url=' + | |||
encodeURIComponent('https://api.soundcloud.com/tracks/' + trackId) + | |||
'&color=%23ff5500&auto_play=true&hide_related=true&show_comments=false' + | |||
'&show_user=true&show_reposts=false&show_teaser=false&visual=true'; | |||
var $iframe = $('<iframe class="sc-embed-frame" scrolling="no" frameborder="no" allow="autoplay"></iframe>') | |||
.attr({ | |||
src: src, | |||
title: title || 'SoundCloud audio', | |||
loading: 'lazy' | |||
}); | |||
$slot.empty().append($iframe).addClass('sc-embed-playing'); | |||
} | } | ||
function | $('.mw-parser-output .sc-embed[data-soundcloud-track]').each(function () { | ||
var $ | var $slot = $(this); | ||
if ($ | if ($slot.data('rbScEmbed')) { | ||
return $ | return; | ||
} | |||
var trackId = String($slot.attr('data-soundcloud-track') || ''); | |||
if (!SC_TRACK.test(trackId)) { | |||
return; | |||
} | } | ||
$slot.data('rbScEmbed', 1); | |||
var title = String($slot.attr('data-title') || 'SoundCloud audio'); | |||
var $fallback = $slot.find('.sc-embed-fallback').first(); | |||
var $btn = $('<button type="button" class="sc-embed-play"></button>') | |||
$ | .attr('aria-label', 'Play ' + title) | ||
var $ | .append($('<span class="sc-embed-play-icon" aria-hidden="true"></span>')) | ||
.append($('<span class="sc-embed-play-text"></span>').text('Play audio')); | |||
var $poster = $('<div class="sc-embed-poster"></div>').append($btn); | |||
if ($fallback.length) { | |||
if ($ | $fallback.before($poster); | ||
$fallback.hide(); | |||
} else { | } else { | ||
$slot.prepend($poster); | |||
} | } | ||
$poster.on('click', function (ev) { | |||
ev.preventDefault(); | |||
playSoundCloud($slot, trackId, title); | |||
}); | |||
}); | |||
}()); | |||
(function buildAviaryEmbeds() { | |||
var AV_MEDIA = /^[0-9]{4,12}$/; | |||
function playAviary($slot, mediaId, title) { | |||
var src = 'https://milken.aviaryplatform.com/embed/media/' + | |||
encodeURIComponent(mediaId) + | |||
'?embed=true&media_player=true'; | |||
var $iframe = $('<iframe class="aviary-embed-frame" allowfullscreen allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>') | |||
.attr({ | |||
src: src, | |||
title: title || 'Aviary media', | |||
loading: 'lazy' | |||
}); | |||
$slot.empty().append($iframe).addClass('aviary-embed-playing'); | |||
} | } | ||
$('.mw-parser-output .aviary-embed[data-aviary-media]').each(function () { | |||
var $slot = $(this); | |||
if ($slot.data('rbAviaryEmbed')) { | |||
$ | return; | ||
var $ | } | ||
if ($ | var mediaId = String($slot.attr('data-aviary-media') || ''); | ||
$ | if (!AV_MEDIA.test(mediaId)) { | ||
return; | |||
} | |||
$slot.data('rbAviaryEmbed', 1); | |||
var title = String($slot.attr('data-title') || 'Aviary media'); | |||
var playLabel = String($slot.attr('data-play-label') || 'Play video'); | |||
var $fallback = $slot.find('.aviary-embed-fallback').first(); | |||
var $btn = $('<button type="button" class="aviary-embed-play"></button>') | |||
.attr('aria-label', playLabel + ' — ' + title) | |||
.append($('<span class="aviary-embed-play-icon" aria-hidden="true"></span>')) | |||
.append($('<span class="aviary-embed-play-text"></span>').text(playLabel)); | |||
var $poster = $('<div class="aviary-embed-poster"></div>').append($btn); | |||
if ($fallback.length) { | |||
$fallback.before($poster); | |||
$fallback.hide(); | |||
} else { | } else { | ||
$slot.prepend($poster); | |||
} | } | ||
$poster.on('click', function (ev) { | |||
ev.preventDefault(); | |||
playAviary($slot, mediaId, title); | |||
}); | |||
}); | |||
}()); | |||
(function buildInternetArchiveEmbeds() { | |||
// archive.org item identifiers (e.g. RobertA.Potash13january2012YiddishBookCenter) | |||
var IA_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{5,120}$/; | |||
function playInternetArchive($slot, itemId, title) { | |||
var src = 'https://archive.org/embed/' + encodeURIComponent(itemId); | |||
var $iframe = $('<iframe class="ia-embed-frame" allowfullscreen allow="autoplay; encrypted-media; fullscreen; picture-in-picture"></iframe>') | |||
.attr({ | |||
src: src, | |||
title: title || 'Internet Archive media', | |||
loading: 'lazy' | |||
}); | |||
$slot.empty().append($iframe).addClass('ia-embed-playing'); | |||
} | } | ||
$('.mw-parser-output .ia-embed[data-archive-id]').each(function () { | |||
var $slot = $(this); | |||
if ($slot.data('rbIaEmbed')) { | |||
return; | |||
var $ | |||
if ($ | |||
} | } | ||
var itemId = String($slot.attr('data-archive-id') || ''); | |||
if (!IA_ID.test(itemId)) { | |||
return; | |||
} | } | ||
$slot.data('rbIaEmbed', 1); | |||
var title = String($slot.attr('data-title') || 'Internet Archive media'); | |||
var playLabel = String($slot.attr('data-play-label') || 'Play interview'); | |||
var $fallback = $slot.find('.ia-embed-fallback').first(); | |||
var $btn = $('<button type="button" class="ia-embed-play"></button>') | |||
.attr('aria-label', playLabel + ' — ' + title) | |||
.append($('<span class="ia-embed-play-icon" aria-hidden="true"></span>')) | |||
var $ | .append($('<span class="ia-embed-play-text"></span>').text(playLabel)); | ||
if ($ | var $poster = $('<div class="ia-embed-poster"></div>').append($btn); | ||
$ | if ($fallback.length) { | ||
$fallback.before($poster); | |||
$fallback.hide(); | |||
} else { | } else { | ||
$slot.prepend($poster); | |||
} | } | ||
} | $poster.on('click', function (ev) { | ||
ev.preventDefault(); | |||
playInternetArchive($slot, itemId, title); | |||
}); | |||
}); | |||
}()); | }()); | ||
(function | (function placeSoldierAwardsSection() { | ||
// Ribbon racks belong in the main column after Biography (like Wikipedia's | |||
// Awards section). Do not use a wiki == heading here — that stole the TOC. | |||
var $out = $('.mw-parser-output').first(); | var $out = $('.mw-parser-output').first(); | ||
var $awards = $out.find('.soldier-awards-section').first(); | |||
if (!$out.length) { | if (!$out.length) { | ||
return; | return; | ||
} | } | ||
function sectionAfter($h) { | |||
var $nodes = $h.nextUntil('h2'); | |||
if ($nodes.length) { | |||
return $nodes.last(); | |||
} | |||
return $h; | |||
$ | |||
} | } | ||
if ($awards.length && !$awards.data('rbAwardsPlaced')) { | |||
$awards.data('rbAwardsPlaced', 1); | |||
var $bio = $out.find('h2').filter(function () { | |||
var t = $(this).find('.mw-headline').text() || $(this).text(); | |||
return /^(Biography|Life)\b/i.test($.trim(t)); | |||
}).first(); | |||
if ($bio.length) { | |||
sectionAfter($bio).after($awards); | |||
} else { | |||
var $before = $out.find('h2').filter(function () { | |||
var t = $(this).find('.mw-headline').text() || $(this).text(); | |||
return /^(Gallery|Research notes|Sources|Technical record)\b/i.test($.trim(t)); | |||
}).first(); | |||
if ($before.length) { | |||
$before.before($awards); | |||
} else { | |||
$out.append($awards); | |||
} | |||
} | |||
} | } | ||
var | |||
if ( | // Class/team rail rides with awards (after Biography). | ||
var $related = $out.find('.soldier-related').first(); | |||
if ($related.length && !$related.data('rbRelatedPlaced')) { | |||
$related.data('rbRelatedPlaced', 1); | |||
var $awardsNow = $out.find('.soldier-awards-section').first(); | |||
if ($awardsNow.length) { | |||
$awardsNow.after($related); | |||
} else { | |||
var $bio2 = $out.find('h2').filter(function () { | |||
var t = $(this).find('.mw-headline').text() || $(this).text(); | |||
return /^(Biography|Life)\b/i.test($.trim(t)); | |||
}).first(); | |||
if ($bio2.length) { | |||
sectionAfter($bio2).after($related); | |||
} | |||
} | |||
} | } | ||
if ( | |||
// Story cue belongs under the lead (above Biography). | |||
var $storyCue = $out.find('.soldier-story-cue').first(); | |||
if ($storyCue.length && !$storyCue.data('rbStoryCuePlaced')) { | |||
$storyCue.data('rbStoryCuePlaced', 1); | |||
var $leadCue = $out.find('.soldier-lead, .page-lead').first(); | |||
if ($leadCue.length) { | |||
$leadCue.after($storyCue); | |||
} | |||
} | } | ||
// Interview / film cue sits with the story cue under the lead. | |||
var $mediaCue = $out.find('.soldier-media-cue').first(); | |||
if ($mediaCue.length && !$mediaCue.data('rbMediaCuePlaced')) { | |||
$mediaCue.data('rbMediaCuePlaced', 1); | |||
var $afterStory = $out.find('.soldier-story-cue').first(); | |||
if ($afterStory.length) { | |||
$afterStory.after($mediaCue); | |||
} else { | |||
var $leadMedia = $out.find('.soldier-lead, .page-lead').first(); | |||
if ($leadMedia.length) { | |||
$leadMedia.after($mediaCue); | |||
} | |||
} | |||
} | } | ||
if ( | |||
// Contents box belongs after the lead on soldier/profile pages only. | |||
// Other pages (e.g. Site changelog) keep MediaWiki's default TOC placement. | |||
var $toc = $out.find('#toc, .toc, .mw-table-of-contents').first(); | |||
if ($toc.length && !$toc.data('rbTocPlaced')) { | |||
$toc.data('rbTocPlaced', 1); | |||
var $lead = $out.find('.soldier-lead, .page-lead, .home-lead').first(); | |||
if ($lead.length) { | |||
$lead.after($toc); | |||
} else { | |||
var $ibox = $out.find('table.soldier-infobox, table.infobox').first(); | |||
if ($ibox.length) { | |||
$ibox.after($toc); | |||
} | |||
} | |||
} | } | ||
}()); | |||
(function moveSoldierFooterCues() { | |||
var $out = $('.mw-parser-output').first(); | |||
if (!$out.length) { | |||
return; | |||
} | } | ||
var | var $notes = $out.find('.research-notes-disclaimer').first(); | ||
if ( | var $sources = $out.find('.source-confidence-cue').first(); | ||
if (!$notes.length && !$sources.length) { | |||
return | return; | ||
} | } | ||
var $footer = $out.find('.soldier-page-footer-notes').first(); | |||
if (!$footer.length) { | |||
$footer = $('<div class="soldier-page-footer-notes"></div>'); | |||
$out.append($footer); | |||
} | } | ||
if ($notes.length) { | |||
if ( | $footer.append($notes); | ||
} | } | ||
if ($sources.length) { | |||
if ( | $footer.append($sources); | ||
} | } | ||
var | }()); | ||
var NAME_PARTICLES = { | |||
von: 1, van: 1, de: 1, der: 1, den: 1, da: 1, di: 1, du: 1, | |||
la: 1, le: 1, del: 1, della: 1, ter: 1, ten: 1, zu: 1, zum: 1, bin: 1 | |||
}; | |||
var NAME_SUFFIX = { | |||
jr: 'Jr', 'jr.': 'Jr.', sr: 'Sr', 'sr.': 'Sr.', | |||
esq: 'Esq', 'esq.': 'Esq.', ii: 'II', iii: 'III', iv: 'IV' | |||
}; | |||
function lettersOnly(s) { | |||
return String(s || '').replace(/[^A-Za-z]/g, ''); | |||
} | |||
function isAllCaps(s) { | |||
var letters = lettersOnly(s); | |||
return letters.length >= 1 && letters === letters.toUpperCase(); | |||
} | |||
function isAllLower(s) { | |||
var letters = lettersOnly(s); | |||
return letters.length >= 2 && letters === letters.toLowerCase(); | |||
} | |||
function titleCaseWord(s) { | |||
s = String(s || '').toLowerCase(); | |||
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; | |||
} | |||
function prettyCapsWord(word) { | |||
var trail = ''; | |||
while (word && /[.,]$/.test(word)) { | |||
trail = word.slice(-1) + trail; | |||
word = word.slice(0, -1); | |||
} | } | ||
var up = lettersOnly(word).toUpperCase(); | |||
return | if (up.length >= 5 && up.slice(0, 2) === 'MC') { | ||
return 'Mc' + titleCaseWord(up.slice(2)) + trail; | |||
} | } | ||
if ( | if (up.length >= 7 && up.slice(0, 3) === 'MAC') { | ||
return | return 'Mac' + titleCaseWord(up.slice(3)) + trail; | ||
} | } | ||
return | return titleCaseWord(up) + trail; | ||
} | } | ||
function | function prettyNameToken(word, leading) { | ||
word = $.trim(word); | |||
if (! | if (!word) { | ||
return | return word; | ||
} | } | ||
if (/\d/.test(word)) { | |||
return word; | |||
} | } | ||
if (word.indexOf('-') !== -1 && word.charAt(0) !== '-' && word.slice(-1) !== '-') { | |||
return $.map(word.split('-'), function (part, i) { | |||
return prettyNameToken(part, leading && i === 0); | |||
}).join('-'); | |||
} | } | ||
var | var apos = word.indexOf("'") !== -1 ? "'" : (word.indexOf('\u2019') !== -1 ? '\u2019' : ''); | ||
var | if (apos) { | ||
if ( | var bits = word.split(apos); | ||
return prettyNameToken(bits[0], leading) + apos + prettyNameToken(bits.slice(1).join(apos), false); | |||
} | |||
if (/^(?:[A-Za-z]\.)+$/.test(word)) { | |||
return (isAllCaps(word) || isAllLower(word)) ? word.toUpperCase() : word; | |||
} | |||
var low = word.toLowerCase(); | |||
if (NAME_SUFFIX[low]) { | |||
return NAME_SUFFIX[low]; | |||
} | |||
var bare = lettersOnly(word); | |||
if (bare.length === 1) { | |||
return bare.toUpperCase() + word.replace(/[A-Za-z]/, ''); | |||
} | } | ||
var lowBare = bare.toLowerCase(); | |||
if (NAME_SUFFIX[lowBare] && /^[A-Za-z]+\.?$/.test(word)) { | |||
var canon = NAME_SUFFIX[lowBare]; | |||
if (word.slice(-1) === '.' && canon.slice(-1) !== '.') { | |||
var | return canon + '.'; | ||
} | } | ||
}); | return canon; | ||
} | |||
if (NAME_PARTICLES[lowBare] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) { | |||
return leading ? titleCaseWord(lowBare) : lowBare; | |||
} | |||
if (isAllCaps(word) && bare.length >= 2 && bare.length <= 3 && !/[AEIOUY]/i.test(bare)) { | |||
return word; | |||
} | |||
if (isAllCaps(word) || isAllLower(word)) { | |||
return prettyCapsWord(word); | |||
} | |||
return word; | |||
} | } | ||
if (mw.config.get('wgNamespaceNumber') === 14) { | function prettyNamePhrase(text, capLead) { | ||
applyPrettySoldierLabels($('.mw-category-group li a')); | text = $.trim(text || '').replace(/\s*;\s*/g, ' '); | ||
} | if (!text) { | ||
if (mw.config.get('wgCanonicalSpecialPageName') === 'Search') { | return ''; | ||
applyPrettySoldierLabels($('.mw-search-result-heading a')); | } | ||
return $.map(text.split(/\s*,\s*/), function (clause, clauseI) { | |||
clause = $.trim(clause); | |||
if (!clause) { | |||
return null; | |||
} | |||
var words = clause.split(/\s+/); | |||
return $.map(words, function (word, i) { | |||
var particle = lettersOnly(word).toLowerCase(); | |||
var leading = (clauseI === 0 && i === 0 && capLead) || words.length === 1; | |||
if (NAME_PARTICLES[particle] && /^[A-Za-z]+$/.test(word) && (isAllCaps(word) || isAllLower(word))) { | |||
return leading ? titleCaseWord(particle) : word.toLowerCase(); | |||
} | |||
return prettyNameToken(word, i === 0); | |||
}).join(' '); | |||
}).join(', '); | |||
} | |||
function prettySoldierLabel(text) { | |||
var m = $.trim(text || '').match(/^(.*) \(([^()]+)\)\s*$/); | |||
if (!m || m[1].indexOf('(') !== -1) { | |||
return prettyNamePhrase(text, true); | |||
} | |||
return prettyNamePhrase(m[1], true) + ' (' + m[2] + ')'; | |||
} | |||
function maybePrettySoldierTitle(label) { | |||
var text = $.trim(label || ''); | |||
if (!text || text.indexOf(',') === -1) { | |||
return text; | |||
} | |||
var sur = text.split(',')[0]; | |||
var letters = lettersOnly(sur); | |||
if (!(letters.length >= 2 && letters === letters.toUpperCase())) { | |||
return text; | |||
} | |||
return prettySoldierLabel(text); | |||
} | |||
function applyPrettySoldierLabels($links) { | |||
$links.each(function () { | |||
var $a = $(this); | |||
var label = $a.text(); | |||
var pretty = maybePrettySoldierTitle(label); | |||
if (pretty && pretty !== label) { | |||
$a.text(pretty); | |||
if ($a.attr('title') === label || !$a.attr('title')) { | |||
$a.attr('title', pretty); | |||
} | |||
} | |||
}); | |||
} | |||
if (mw.config.get('wgNamespaceNumber') === 14) { | |||
applyPrettySoldierLabels($('.mw-category-group li a')); | |||
} | |||
if (mw.config.get('wgCanonicalSpecialPageName') === 'Search') { | |||
applyPrettySoldierLabels($('.mw-search-result-heading a')); | |||
} | } | ||
// Search box suggestions use the real page title; pretty-case ALL-CAPS surnames as they appear. | // Search box suggestions use the real page title; pretty-case ALL-CAPS surnames as they appear. | ||
| Line 883: | Line 1,074: | ||
function teamHref(name) { | function teamHref(name) { | ||
return | return mw.util.getUrl('Team ' + name); | ||
} | } | ||
function teamLink(name, label) { | function teamLink(name, label) { | ||
| Line 995: | Line 1,186: | ||
} | } | ||
var negative = /fail(?:ed|ure)?|flunk|wash(?:ed)?\s*out|kick(?:ed)?\s*out|dismiss|expel|unsatisfactor|poor\s+(?:performance|work|grades)|awol|court[-\s]?martial|disciplin|discharged\s+for\s+cause|drop(?:ped)?\s+(?:out|from)|relieved|eliminat|unfit|misconduct|desert|excess\s+absence|absence\s*\(|\billness\b|sick\s+call|medical\s+board|psychiatr|neurot|homosexual|suicid|alcohol(?:ism)?|drunk|intoxicat|venereal|\bVD\b|syphili|gonorrh|mental\s+(?:case|illness)|nervous\s+breakdown|not\s+graduat|did\s+not\s+graduate|physically\s+unfit|unfit\s+for|non[-\s]?graduate|certificate\s+listed\s+as\s+ng/i; | var negative = /fail(?:ed|ure)?|flunk|wash(?:ed)?\s*out|kick(?:ed)?\s*out|dismiss|expel|unsatisfactor|poor\s+(?:performance|work|grades)|awol|court[-\s]?martial|disciplin|discharged\s+for\s+cause|drop(?:ped)?\s+(?:out|from)|relieved|eliminat|unfit|misconduct|desert|excess\s+absence|absence\s*\(|\billness\b|sick\s+call|medical\s+board|psychiatr|neurot|homosexual|suicid|alcohol(?:ism)?|drunk|intoxicat|venereal|\bVD\b|syphili|gonorrh|mental\s+(?:case|illness)|nervous\s+breakdown|not\s+graduat|did\s+not\s+graduate|physically\s+unfit|unfit\s+for|non[-\s]?graduate|certificate\s+listed\s+as\s+ng|(?:^|\b)NG\b(?:\s+Class|\s+OB|\s*$|\s*[,;:/]|\s+\d)|\bnongrad/i; | ||
var $ul = $heading.nextAll('ul').first(); | var $ul = $heading.nextAll('ul').first(); | ||
if (!$ul.length) { | if (!$ul.length) { | ||
| Line 1,238: | Line 1,429: | ||
{ key: 'death_year', label: 'Death year', hint: 'Four digits, e.g. 2001' }, | { key: 'death_year', label: 'Death year', hint: 'Four digits, e.g. 2001' }, | ||
{ key: 'nativity', label: 'Nativity (birthplace)' }, | { key: 'nativity', label: 'Nativity (birthplace)' }, | ||
{ key: 'religion', label: 'Religion | { key: 'religion', label: 'Religion' } | ||
] | ] | ||
}, | }, | ||
| Line 1,264: | Line 1,451: | ||
collapsed: true, | collapsed: true, | ||
fields: [ | fields: [ | ||
{ key: 'other_class', label: 'Other class' }, | { | ||
key: 'other_class', | |||
label: 'Other class', | |||
hint: 'Roster code, e.g. SC8, DC36, MITU4 (shown expanded on the page)' | |||
}, | |||
{ key: 'division', label: 'Unit / division' }, | { key: 'division', label: 'Unit / division' }, | ||
{ key: 'loc', label: 'LOC | { key: 'loc', label: 'LOC' } | ||
] | ] | ||
} | } | ||
| Line 1,274: | Line 1,463: | ||
var $box = $('<div>', { 'class': 'soldier-facts-box' }); | var $box = $('<div>', { 'class': 'soldier-facts-box' }); | ||
var $intro = $('<p>' | var $intro = $('<p>', { 'class': 'soldier-facts-intro' }); | ||
$intro.append( | $intro.append( | ||
$('<strong>', { 'class': 'soldier-facts-label' }).text('Editing this page'), | |||
$('<span>', { 'class': 'soldier-facts-help' }).text(' — '), | |||
$('<button>', { type: 'button', 'class': 'soldier-facts-toggle', text: 'Edit soldier facts' }), | $('<button>', { type: 'button', 'class': 'soldier-facts-toggle', text: 'Edit soldier facts' }), | ||
$('<span>', { 'class': 'soldier-facts-help' }).text(' (infobox fields), '), | |||
$('<a>', { href: editUrl, text: 'Edit biography' }), | $('<a>', { href: editUrl, text: 'Edit biography' }), | ||
$('<span>', { 'class': 'soldier-facts-help' }).text(' (visual editor for Biography and notes), or '), | |||
$('<a>', { href: sourceUrl, text: 'Edit source' }), | $('<a>', { href: sourceUrl, text: 'Edit source' }), | ||
$('<span>', { 'class': 'soldier-facts-help' }).text(' (full page markup).') | |||
); | ); | ||
$box.append($intro); | $box.append($intro); | ||
| Line 1,292: | Line 1,480: | ||
$draftBar.append( | $draftBar.append( | ||
$draftBtn, | $draftBtn, | ||
$('<span>', { 'class': 'soldier-summary-help' }).text( | |||
' — optional 5–10 sentence opening from Personal details and the Biography section. Review before saving.' | |||
) | |||
); | ); | ||
$box.append($draftBar); | $box.append($draftBar); | ||
| Line 2,275: | Line 2,465: | ||
$panel.append($banner); | $panel.append($banner); | ||
} else { | } else { | ||
var engineBit = job.ocr_engine ? (' · Engine: ' + job.ocr_engine) : ''; | |||
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | $panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | ||
(job.doc_type_label || 'Document') + | (job.doc_type_label || 'Document') + | ||
' · OCR quality ' + (job.ocr_quality || 0) + '%' + | ' · OCR quality ' + (job.ocr_quality || 0) + '%' + | ||
engineBit + | |||
(job.error ? (' · Error: ' + job.error) : '') | (job.error ? (' · Error: ' + job.error) : '') | ||
)); | )); | ||
if (job.ocr_engine_notes && job.ocr_engine_notes.length) { | |||
$panel.append($('<p>', { 'class': 'ocr-fact-meta' }).text( | |||
job.ocr_engine_notes.slice(0, 3).join(' · ') | |||
)); | |||
} | |||
} | } | ||
| Line 2,462: | Line 2,659: | ||
if (isMultiPersonJob(job) && !job.bound_title) { | if (isMultiPersonJob(job) && !job.bound_title) { | ||
$forceMulti.hide(); | $forceMulti.hide(); | ||
} | |||
var $forceVision = $('<label>', { 'class': 'ocr-force-vision' }).append( | |||
$('<input>', { type: 'checkbox', checked: mwApiBool(job.force_vision) }), | |||
' Handwriting / hard scan (print OCR first; paid vision only if weak)' | |||
); | |||
if (!cfg.llmVision) { | |||
$forceVision.append(document.createTextNode(' — needs RITCHIE_LLM_API_KEY on OCR service')); | |||
} | } | ||
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | ||
| Line 2,476: | Line 2,680: | ||
$applyNotes, $('<br>'), | $applyNotes, $('<br>'), | ||
$forceMulti, $('<br>'), | $forceMulti, $('<br>'), | ||
$forceVision, $('<br>'), | |||
$forceId | $forceId | ||
); | ); | ||
var $save = $('<button>', { type: 'button', text: 'Save draft' }); | var $save = $('<button>', { type: 'button', text: 'Save draft' }); | ||
var $rebuild = $('<button>', { type: 'button', text: 'Rebuild | var $rebuild = $('<button>', { type: 'button', text: 'Rebuild facts / re-OCR if handwriting checked' }); | ||
$advBody.append($('<div>', { 'class': 'ocr-advanced-actions' }).append($save, $rebuild)); | $advBody.append($('<div>', { 'class': 'ocr-advanced-actions' }).append($save, $rebuild)); | ||
$adv.append($advBody); | $adv.append($advBody); | ||
| Line 2,567: | Line 2,769: | ||
force_identity: $forceId.find('input').prop('checked'), | force_identity: $forceId.find('input').prop('checked'), | ||
force_multi: $forceMulti.find('input').prop('checked'), | force_multi: $forceMulti.find('input').prop('checked'), | ||
force_vision: $forceVision.find('input').prop('checked'), | |||
apply_notes: $applyNotes.find('input').prop('checked') | apply_notes: $applyNotes.find('input').prop('checked') | ||
}; | }; | ||
| Line 2,769: | Line 2,972: | ||
bound_title: boundTitle || '', | bound_title: boundTitle || '', | ||
force_multi: opts.forceMulti ? true : false, | force_multi: opts.forceMulti ? true : false, | ||
force_vision: opts.forceVision ? true : false, | |||
file: file, | file: file, | ||
format: 'json' | format: 'json' | ||
| Line 2,815: | Line 3,019: | ||
var $section = $('<div>', { 'class': 'soldier-contrib-section soldier-contrib-ocr' }); | var $section = $('<div>', { 'class': 'soldier-contrib-section soldier-contrib-ocr' }); | ||
$section.append($('<p>', { 'class': 'soldier-contrib-heading' }).append( | $section.append($('<p>', { 'class': 'soldier-contrib-heading' }).append( | ||
$('<strong>').text('OCR a document') | $('<strong>').text('OCR a document to enrich this profile') | ||
)); | )); | ||
$section.append($('<p>', { 'class': 'soldier-contrib-help' }).text( | $section.append($('<p>', { 'class': 'soldier-contrib-help' }).text( | ||
'Scan | 'Scan a form, letter, certificate, clipping, or roster. Review proposes infobox facts and a short biography draft — nothing is written until Apply. Team rosters can update many soldiers at once.' | ||
)); | )); | ||
var $file = $('<input>', { | var $file = $('<input>', { | ||
| Line 2,828: | Line 3,032: | ||
$('<input>', { type: 'checkbox' }), | $('<input>', { type: 'checkbox' }), | ||
' This is a multi-person roster (not only this soldier)' | ' This is a multi-person roster (not only this soldier)' | ||
); | |||
var $visionMode = $('<label>', { 'class': 'ocr-force-multi-upload' }).append( | |||
$('<input>', { type: 'checkbox' }), | |||
' Handwriting / hard scan (free print OCR first; vision only if needed)' | |||
); | ); | ||
var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' }); | var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' }); | ||
var $host = $('<div>', { 'class': 'ocr-review-host' }); | var $host = $('<div>', { 'class': 'ocr-review-host' }); | ||
$section.append($file, $rosterMode, $btn, $host); | $section.append($file, $rosterMode, $visionMode, $btn, $host); | ||
$btn.on('click', function () { | $btn.on('click', function () { | ||
startUpload($file, pageTitle, $host, cfg, { | startUpload($file, pageTitle, $host, cfg, { | ||
forceMulti: $rosterMode.find('input').prop('checked') | forceMulti: $rosterMode.find('input').prop('checked'), | ||
forceVision: $visionMode.find('input').prop('checked') | |||
}); | }); | ||
}); | }); | ||
| Line 2,857: | Line 3,066: | ||
$up.append($('<h3>').text('Upload a document')); | $up.append($('<h3>').text('Upload a document')); | ||
$up.append($('<p>').text( | $up.append($('<p>').text( | ||
'For | 'Enrich soldier profiles from scans: propose infobox facts, a short biography draft, and Sources. For multi-person IPW rosters, review is a table (usually Team membership). Rank, role, and ASN are matching cues until you approve.' | ||
)); | )); | ||
var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | var $file2 = $('<input>', { type: 'file', accept: '.jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.pdf' }); | ||
var $vision2 = $('<label>', { 'class': 'ocr-force-multi-upload' }).append( | |||
$('<input>', { type: 'checkbox' }), | |||
' Handwriting / hard scan (vision only if print OCR is weak)' | |||
); | |||
var $btn2 = $('<button>', { type: 'button', text: 'Upload & OCR' }); | var $btn2 = $('<button>', { type: 'button', text: 'Upload & OCR' }); | ||
var $host2 = $('<div>', { 'class': 'ocr-review-host' }); | var $host2 = $('<div>', { 'class': 'ocr-review-host' }); | ||
$up.append($file2, ' ', $btn2, $host2); | $up.append($file2, $vision2, ' ', $btn2, $host2); | ||
$btn2.on('click', function () { | $btn2.on('click', function () { | ||
startUpload($file2, '', $host2, cfg); | startUpload($file2, '', $host2, cfg, { | ||
forceVision: $vision2.find('input').prop('checked') | |||
}); | |||
}); | }); | ||
$slot.append($up); | $slot.append($up); | ||
| Line 3,107: | Line 3,322: | ||
var $block = $(this); | var $block = $(this); | ||
var cat = String($block.attr('data-team-cat') || ''); | var cat = String($block.attr('data-team-cat') || ''); | ||
if (!/^Category:Team /.test(cat)) { | if (!/^Category:Team /.test(cat)) { | ||
return; | return; | ||
} | } | ||
var $links = $block.find('.soldier-related-links-live').first(); | var $links = $block.find('.soldier-related-links-live').first(); | ||
if (!$links.length) { | if (!$links.length) { | ||
return; | return; | ||
} | } | ||
$links.text('Loading teammates…'); | $links.text('Loading teammates…'); | ||
fetchCategoryMembers(cat).done(function (titles) { | fetchCategoryMembers(cat).done(function (titles) { | ||
var others = titles.filter(function (t) { | var others = titles.filter(function (t) { | ||
return t.replace(/_/g, ' ') !== current; | return t.replace(/_/g, ' ') !== current; | ||
}); | }); | ||
others.sort(function (a, b) { | others.sort(function (a, b) { | ||
return titleToCaption(a).localeCompare(titleToCaption(b)); | return titleToCaption(a).localeCompare(titleToCaption(b)); | ||
}); | }); | ||
$links.empty(); | $links.empty(); | ||
if (!others.length) { | if (!others.length) { | ||
$links.append(document.createTextNode('No other teammates listed yet · ')); | $links.append(document.createTextNode('No other teammates listed yet · ')); | ||
$links.append(wikiLink(cat, 'full roster')); | $links.append(wikiLink(cat, 'full roster')); | ||
return; | return; | ||
} | } | ||
var max = 24; | var max = 24; | ||
others.slice(0, max).forEach(function (title, i) { | others.slice(0, max).forEach(function (title, i) { | ||
if (i > 0) { | if (i > 0) { | ||
$links.append(document.createTextNode(' · ')); | $links.append(document.createTextNode(' · ')); | ||
} | } | ||
$links.append(wikiLink(title, titleToCaption(title))); | $links.append(wikiLink(title, titleToCaption(title))); | ||
}); | }); | ||
$links.append(document.createTextNode(' · ')); | $links.append(document.createTextNode(' · ')); | ||
$links.append(wikiLink(cat, others.length > max ? 'full roster (' + others.length + ')' : 'full roster')); | $links.append(wikiLink(cat, others.length > max ? 'full roster (' + others.length + ')' : 'full roster')); | ||
}).fail(function () { | }).fail(function () { | ||
$links.empty().append(wikiLink(cat, 'Browse team roster')); | $links.empty().append(wikiLink(cat, 'Browse team roster')); | ||
}); | }); | ||
}); | }); | ||
}); | }); | ||
}()); | }()); | ||
$(function rbCampManualUpload() { | |||
var $slot = $('.mw-parser-output .camp-manual-upload-slot').first(); | |||
if (!$slot.length || $slot.data('rbCampManualUpload')) { | |||
return; | |||
} | |||
$slot.data('rbCampManualUpload', 1); | |||
var LIBRARY = 'Training materials/Library'; | |||
var PLACEHOLDER = "''No manuals listed yet. Upload a PDF above and it will appear here.''"; | |||
var DOC_EXT = /\.(pdf|docx?|txt|rtf|odt|tiff?|jpe?g|png|gif|webp)$/i; | |||
var pageTitle = mw.config.get('wgPageName'); | |||
var loggedIn = !!mw.config.get('wgUserName'); | |||
var uploadUrl = mw.util.getUrl('Special:Upload'); | |||
$slot.empty(); | |||
var $box = $('<div>', { 'class': 'camp-manual-upload-box soldier-upload-body' }); | |||
$box.append($('<p>', { 'class': 'soldier-contrib-help' }).append( | |||
document.createTextNode('Upload a manual or related Camp Ritchie document (PDF preferred). Also '), | |||
$('<a>', { href: uploadUrl, text: 'Special:Upload' }), | |||
document.createTextNode('.') | |||
)); | |||
if (!loggedIn) { | |||
$box.append($('<p>', { 'class': 'soldier-upload-login' }).html( | |||
'<a href="' + mw.util.getUrl('Special:UserLogin', { returnto: pageTitle }) + '">Log in</a> to upload files.' | |||
)); | |||
$slot.append($box); | |||
return; | |||
} | |||
var $form = $('<form>', { 'class': 'soldier-upload-form camp-manual-upload-form' }); | |||
var $file = $('<input>', { | |||
type: 'file', | |||
name: 'campManualFile', | |||
'class': 'soldier-upload-file', | |||
accept: '.pdf,.doc,.docx,.txt,.rtf,.odt,.tif,.tiff,.jpg,.jpeg,.png' | |||
}); | |||
var $label = $('<label>').append( | |||
document.createTextNode('Display title (optional)'), | |||
$('<input>', { | |||
type: 'text', | |||
name: 'campManualTitle', | |||
'class': 'camp-manual-title', | |||
placeholder: 'e.g. Interrogation manual, 1943', | |||
maxlength: 200 | |||
}) | |||
); | |||
var $note = $('<label>').append( | |||
document.createTextNode('Short note / source (optional)'), | |||
$('<input>', { | |||
type: 'text', | |||
name: 'campManualNote', | |||
'class': 'camp-manual-note', | |||
placeholder: 'e.g. NARA / museum scan', | |||
maxlength: 300 | |||
}) | |||
); | |||
var $status = $('<p>', { 'class': 'soldier-upload-status', text: '' }); | |||
var $progress = $('<div>', { 'class': 'camp-manual-progress', hidden: true }); | |||
var $progressFill = $('<div>', { 'class': 'camp-manual-progress-fill' }); | |||
var $progressBar = $('<div>', { | |||
'class': 'camp-manual-progress-bar', | |||
role: 'progressbar', | |||
'aria-valuemin': 0, | |||
'aria-valuemax': 100, | |||
'aria-valuenow': 0 | |||
}).append($progressFill); | |||
var $progressLabel = $('<p>', { 'class': 'camp-manual-progress-label', text: '' }); | |||
$progress.append($progressBar, $progressLabel); | |||
var $submit = $('<button>', { type: 'submit', text: 'Upload to library' }); | |||
$form.append($file, $label, $note, $submit, $progress, $status); | |||
$box.append($form); | |||
$slot.append($box); | |||
function setStatus(msg, isError) { | |||
$status.text(msg).css('color', isError ? '#ba0000' : '#202122'); | |||
} | |||
function formatBytes(n) { | |||
if (n >= 1024 * 1024) { | |||
return (n / (1024 * 1024)).toFixed(1) + ' MB'; | |||
} | |||
if (n >= 1024) { | |||
return Math.round(n / 1024) + ' KB'; | |||
} | |||
return n + ' B'; | |||
} | |||
function setProgress(loaded, total) { | |||
$progress.prop('hidden', false); | |||
if (!total) { | |||
$progress.addClass('is-indeterminate'); | |||
$progressBar.attr('aria-valuenow', 0); | |||
$progressFill.css('width', '30%'); | |||
$progressLabel.text('Uploading…'); | |||
return; | |||
} | |||
$progress.removeClass('is-indeterminate'); | |||
var pct = Math.max(0, Math.min(100, Math.round((loaded / total) * 100))); | |||
$progressBar.attr('aria-valuenow', pct); | |||
$progressFill.css('width', pct + '%'); | |||
$progressLabel.text(pct + '% · ' + formatBytes(loaded) + ' of ' + formatBytes(total)); | |||
} | |||
function resetProgress() { | |||
$progress.prop('hidden', true).removeClass('is-indeterminate'); | |||
$progressFill.css('width', '0%'); | |||
$progressBar.attr('aria-valuenow', 0); | |||
$progressLabel.text(''); | |||
} | |||
function escapePipe(s) { | |||
return String(s || '').replace(/\|/g, '–').replace(/\[\[/g, '').replace(/\]\]/g, ''); | |||
} | |||
function buildBullet(filename, displayTitle, note) { | |||
var label = escapePipe(displayTitle || filename.replace(/_/g, ' ')); | |||
var line = '* [[Media:' + filename + '|' + label + ']]'; | |||
if (note) { | |||
line += ' — ' + escapePipe(note); | |||
} | |||
return line; | |||
} | |||
function appendToLibrary(api, filename, displayTitle, note) { | |||
var bullet = buildBullet(filename, displayTitle, note); | |||
return api.get({ | |||
action: 'query', | |||
prop: 'revisions|info', | |||
titles: LIBRARY, | |||
rvprop: 'content|timestamp', | |||
rvslots: 'main', | |||
curtimestamp: 1, | |||
formatversion: 2, | |||
maxage: 0, | |||
smaxage: 0 | |||
}).then(function (data) { | |||
var page = data.query && data.query.pages && data.query.pages[0]; | |||
var rev = page && page.revisions && page.revisions[0]; | |||
var text = (rev && rev.slots && rev.slots.main && rev.slots.main.content) || ''; | |||
var basets = rev && rev.timestamp; | |||
var startts = data.curtimestamp; | |||
var next; | |||
var trimmed = $.trim(text); | |||
var already = trimmed.indexOf('[[Media:' + filename + '|') !== -1 || | |||
trimmed.indexOf('[[File:' + filename + '|') !== -1; | |||
if (already) { | |||
return purgeLibraryPages(api).then(function () { | |||
return { edit: { result: 'Success', nochange: true } }; | |||
}); | |||
} | |||
if (!trimmed || trimmed.indexOf(PLACEHOLDER) !== -1) { | |||
next = bullet + '\n'; | |||
} else { | |||
next = trimmed + '\n' + bullet + '\n'; | |||
} | |||
var params = { | |||
action: 'edit', | |||
title: LIBRARY, | |||
text: next, | |||
summary: 'Add training material: ' + filename, | |||
format: 'json' | |||
}; | |||
if (basets) { | |||
params.basetimestamp = basets; | |||
} | |||
if (startts) { | |||
params.starttimestamp = startts; | |||
} | |||
return api.postWithToken('csrf', params).then(function (editData) { | |||
if (editData.edit && editData.edit.result === 'Success') { | |||
return purgeLibraryPages(api).then(function () { | |||
return editData; | |||
}); | |||
} | |||
if (editData.error && editData.error.code === 'editconflict') { | |||
return appendToLibrary(api, filename, displayTitle, note); | |||
} | |||
throw new Error((editData.error && editData.error.info) || 'Could not update library list'); | |||
}); | |||
}); | |||
} | |||
function failMessage(code, data) { | |||
if (data && data.error && data.error.info) { | |||
return data.error.info; | |||
} | |||
if (data && data.exception && data.exception.message) { | |||
return data.exception.message; | |||
} | |||
if (code && typeof code === 'object' && code.message) { | |||
return code.message; | |||
} | |||
if (typeof code === 'string' && code && code !== 'http' && code !== 'error' && code !== 'timeout') { | |||
return code; | |||
} | |||
if (data && data.xhr && data.xhr.status) { | |||
return 'Upload failed (HTTP ' + data.xhr.status + '). Try again, or use Special:Upload.'; | |||
} | |||
return 'Upload failed (connection dropped or timed out). Try again, or use Special:Upload.'; | |||
} | |||
function purgeLibraryPages(api) { | |||
return api.postWithToken('csrf', { | |||
action: 'purge', | |||
titles: 'Training materials|' + LIBRARY, | |||
forcelinkupdate: 1, | |||
format: 'json' | |||
}); | |||
} | |||
function readLibraryText(api) { | |||
return api.get({ | |||
action: 'query', | |||
prop: 'revisions|info', | |||
titles: LIBRARY, | |||
rvprop: 'content|timestamp', | |||
rvslots: 'main', | |||
curtimestamp: 1, | |||
formatversion: 2, | |||
maxage: 0, | |||
smaxage: 0 | |||
}).then(function (data) { | |||
var page = data.query && data.query.pages && data.query.pages[0]; | |||
var rev = page && page.revisions && page.revisions[0]; | |||
return { | |||
text: (rev && rev.slots && rev.slots.main && rev.slots.main.content) || '', | |||
basetimestamp: rev && rev.timestamp, | |||
starttimestamp: data.curtimestamp | |||
}; | |||
}); | |||
} | |||
function writeLibraryText(api, next, summary, basets, startts) { | |||
var params = { | |||
action: 'edit', | |||
title: LIBRARY, | |||
text: next, | |||
summary: summary, | |||
format: 'json' | |||
}; | |||
if (basets) { | |||
params.basetimestamp = basets; | |||
} | |||
if (startts) { | |||
params.starttimestamp = startts; | |||
} | |||
return api.postWithToken('csrf', params).then(function (editData) { | |||
if (editData.edit && editData.edit.result === 'Success') { | |||
return purgeLibraryPages(api).then(function () { | |||
return editData; | |||
}); | |||
} | |||
if (editData.error && editData.error.code === 'editconflict') { | |||
throw new Error('editconflict'); | |||
} | |||
throw new Error((editData.error && editData.error.info) || 'Could not update library list'); | |||
}); | |||
} | |||
function removeFromLibrary(api, filename) { | |||
return readLibraryText(api).then(function (cur) { | |||
var lines = String(cur.text || '').split(/\r?\n/); | |||
var nextLines = lines.filter(function (line) { | |||
return line.indexOf('[[Media:' + filename + '|') === -1 && | |||
line.indexOf('[[File:' + filename + '|') === -1 && | |||
line.indexOf(filename) === -1; | |||
}); | |||
var next = $.trim(nextLines.join('\n')); | |||
if (!next) { | |||
next = PLACEHOLDER; | |||
} else { | |||
next = next + '\n'; | |||
} | |||
return writeLibraryText( | |||
api, | |||
next, | |||
'Remove from Training materials library: ' + filename, | |||
cur.basetimestamp, | |||
cur.starttimestamp | |||
).then(null, function (err) { | |||
if (String(err && err.message || err) === 'editconflict') { | |||
return removeFromLibrary(api, filename); | |||
} | |||
throw err; | |||
}); | |||
}); | |||
} | |||
function deleteLibraryFile(api, filename) { | |||
return api.postWithToken('csrf', { | |||
action: 'delete', | |||
title: 'File:' + filename, | |||
reason: 'Removed from Training materials library', | |||
format: 'json' | |||
}).then(function (data) { | |||
if (data.error) { | |||
throw new Error(data.error.info || 'Could not delete file'); | |||
} | |||
return removeFromLibrary(api, filename); | |||
}); | |||
} | |||
function filenameFromHref(href, titleAttr) { | |||
if (titleAttr && /\.[a-z0-9]{2,5}$/i.test(titleAttr)) { | |||
return String(titleAttr).replace(/ /g, '_'); | |||
} | |||
if (!href) { | |||
return ''; | |||
} | |||
try { | |||
href = decodeURIComponent(href); | |||
} catch (e) { /* ignore */ } | |||
var m = href.match(/\/wiki\/(?:File|Media):([^?#]+)/i) || | |||
href.match(/[?&]title=(?:File|Media):([^&#]+)/i) || | |||
href.match(/\/images\/(?:thumb\/)?[^/]+\/[^/]+\/([^/?#]+)$/i); | |||
if (!m) { | |||
return ''; | |||
} | |||
return m[1].replace(/_/g, ' ').trim().replace(/ /g, '_'); | |||
} | |||
function enhanceLibraryManage() { | |||
if (!mw.config.get('wgUserName')) { | |||
return; | |||
} | |||
var $heading = $('.mw-parser-output .mw-heading, .mw-parser-output h2').filter(function () { | |||
return /^Library\b/i.test($(this).text().replace(/\s*\[.*$/, '').trim()); | |||
}).first(); | |||
if (!$heading.length) { | |||
return; | |||
} | |||
var $list = $heading.nextAll('ul').first(); | |||
if (!$list.length) { | |||
$list = $heading.parent().nextAll('ul').first(); | |||
} | |||
if (!$list.length || $list.data('rbCampManualManage')) { | |||
return; | |||
} | |||
$list.data('rbCampManualManage', 1); | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
var canDeleteFile = groups.indexOf('sysop') !== -1; | |||
var api = new mw.Api(); | |||
$list.children('li').each(function () { | |||
var $li = $(this); | |||
if ($li.find('.camp-manual-lib-actions').length) { | |||
return; | |||
} | |||
var $a = $li.find('a.internal, a[href*="/images/"], a[href*="File:"], a[href*="Media:"]').first(); | |||
if (!$a.length) { | |||
$a = $li.find('a[href]').first(); | |||
} | |||
var filename = filenameFromHref($a.attr('href'), $a.attr('title')); | |||
if (!filename) { | |||
return; | |||
} | |||
var $actions = $('<span>', { 'class': 'camp-manual-lib-actions' }); | |||
var $remove = $('<button>', { | |||
type: 'button', | |||
'class': 'camp-manual-lib-remove', | |||
text: 'Remove' | |||
}).attr('title', 'Remove from this list (file stays on the wiki)'); | |||
$actions.append($remove); | |||
if (canDeleteFile) { | |||
$actions.append($('<button>', { | |||
type: 'button', | |||
'class': 'camp-manual-lib-delete', | |||
text: 'Delete file' | |||
}).attr('title', 'Delete the file from the wiki (administrators only)')); | |||
} | |||
$li.append($actions); | |||
$remove.on('click', function () { | |||
if (!window.confirm('Remove “' + filename.replace(/_/g, ' ') + '” from the Library list?\n(The file will remain on the wiki.)')) { | |||
return; | |||
} | |||
$actions.find('button').prop('disabled', true); | |||
removeFromLibrary(api, filename).done(function () { | |||
window.location.reload(); | |||
}).fail(function (code, data) { | |||
window.alert(failMessage(code, data)); | |||
$actions.find('button').prop('disabled', false); | |||
}); | |||
}); | |||
$actions.on('click', '.camp-manual-lib-delete', function () { | |||
if (!window.confirm('Permanently delete file “' + filename.replace(/_/g, ' ') + '” from the wiki?')) { | |||
return; | |||
} | |||
$actions.find('button').prop('disabled', true); | |||
deleteLibraryFile(api, filename).done(function () { | |||
window.location.reload(); | |||
}).fail(function (code, data) { | |||
window.alert(failMessage(code, data)); | |||
$actions.find('button').prop('disabled', false); | |||
}); | |||
}); | |||
}); | |||
} | |||
enhanceLibraryManage(); | |||
$form.on('submit', function (evt) { | |||
evt.preventDefault(); | |||
var file = $file[0].files[0]; | |||
if (!file) { | |||
setStatus('Choose a file first.', true); | |||
return; | |||
} | |||
if (!DOC_EXT.test(file.name)) { | |||
setStatus('File type not allowed (PDF, Office docs, images, TIFF).', true); | |||
return; | |||
} | |||
var maxBytes = 120 * 1024 * 1024; | |||
if (file.size > maxBytes) { | |||
setStatus('File is too large (max about 120 MB). Split the PDF or compress it, then try again.', true); | |||
return; | |||
} | |||
var displayTitle = $.trim($form.find('.camp-manual-title').val() || ''); | |||
var note = $.trim($form.find('.camp-manual-note').val() || ''); | |||
$submit.prop('disabled', true); | |||
resetProgress(); | |||
setProgress(0, file.size); | |||
setStatus('Uploading…'); | |||
var api = new mw.Api({ ajax: { timeout: 0 } }); | |||
var fileText = 'Camp Ritchie / MITC training material.'; | |||
var uploadedName = null; | |||
var uploadParams = { | |||
filename: file.name, | |||
comment: 'Uploaded to Training materials library', | |||
text: fileText, | |||
ignorewarnings: true | |||
}; | |||
function doUpload() { | |||
/* Multipart + XHR progress (api.upload alone often has no progress events). */ | |||
return api.postWithToken('csrf', $.extend({ | |||
action: 'upload', | |||
file: file, | |||
format: 'json' | |||
}, uploadParams), { | |||
contentType: 'multipart/form-data', | |||
timeout: 0, | |||
xhr: function () { | |||
var xhr = $.ajaxSettings.xhr(); | |||
if (xhr.upload) { | |||
xhr.upload.addEventListener('progress', function (ev) { | |||
if (ev.lengthComputable) { | |||
setProgress(ev.loaded, ev.total); | |||
} | |||
}); | |||
} | |||
return xhr; | |||
} | |||
}); | |||
} | |||
doUpload().then(function (data) { | |||
uploadedName = data.upload && data.upload.filename; | |||
if (!uploadedName) { | |||
throw new Error((data.error && data.error.info) || 'Upload failed'); | |||
} | |||
setProgress(file.size, file.size); | |||
$progress.addClass('is-indeterminate'); | |||
$progressLabel.text('Finishing…'); | |||
setStatus('Adding to Library list…'); | |||
return appendToLibrary(api, uploadedName, displayTitle, note); | |||
}).done(function () { | |||
$progress.removeClass('is-indeterminate'); | |||
$progressLabel.text('100% · done'); | |||
setStatus('Done. Reloading…'); | |||
window.location.reload(); | |||
}).fail(function (code, data) { | |||
var err = failMessage(code, data); | |||
if (uploadedName) { | |||
err = 'File uploaded as “' + uploadedName + '”, but the Library list was not updated: ' + err + | |||
' Refresh this page; if it is missing from the list, say so and we can add it.'; | |||
} | |||
setStatus(String(err), true); | |||
resetProgress(); | |||
$submit.prop('disabled', false); | |||
}); | |||
}); | |||
}); | |||
(function rbBooksFilters() { | (function rbBooksFilters() { | ||
| Line 3,182: | Line 3,885: | ||
}()); | }()); | ||
/* Related-media wrap removed: story/interview cues already sit under the lead | |||
* with their own labels (Full story, etc.). Wrapping them added a stray | |||
* Related media heading near the top of the article. */ | |||