MediaWiki:Common.js
Appearance
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.
(function () {
function esc(text) {
return String(text || "").replace(/[&<>"]/g, function (m) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[m];
});
}
function renderLink(item, extraClass) {
var cls = extraClass ? ' class="' + extraClass + '"' : "";
var href = item.href ? esc(item.href) : "#";
return '<a' + cls + ' href="' + href + '">' + esc(item.label) + "</a>";
}
function buildFooter(data) {
var columns = (data.columns || []).map(function (col) {
var items = (col.items || []).map(function (item) {
return "<li>" + renderLink(item, "soji-footer-link") + "</li>";
}).join("");
return [
'<section class="soji-footer-col">',
"<h2>" + esc(col.title) + "</h2>",
"<ul>" + items + "</ul>",
"</section>"
].join("");
}).join("");
var policies = (data.policies || []).map(function (item, i) {
return (i ? '<span class="soji-footer-sep">|</span>' : "") +
renderLink(item, "soji-footer-link");
}).join("");
var social = (data.social || []).map(function (item) {
var href = item.href ? esc(item.href) : "#";
return '<a class="soji-footer-social-item" href="' + href + '" aria-label="' + esc(item.label) + '">' +
esc(item.short || item.label.charAt(0)) + "</a>";
}).join("");
var subscribe = data.subscribe
? renderLink(data.subscribe, "soji-footer-subscribe")
: "";
return [
'<section id="soji-site-footer" class="soji-site-footer">',
'<div class="soji-footer-shell">',
'<div class="soji-footer-grid">' + columns + "</div>",
'<div class="soji-footer-bottom">',
'<div class="soji-footer-meta">',
'<div class="soji-footer-policy-row">' + policies + "</div>",
'<div class="soji-footer-copy">' + esc(data.copyright || "") + "</div>",
"</div>",
'<div class="soji-footer-actions">',
subscribe,
'<div class="soji-footer-social">',
'<span class="soji-footer-social-label">Connect</span>',
'<div class="soji-footer-social-list">' + social + "</div>",
"</div>",
"</div>",
"</div>",
"</div>",
"</section>"
].join("");
}
function injectFooter(data) {
var footer = document.getElementById("footer");
if (!footer || document.getElementById("soji-site-footer")) return;
footer.insertAdjacentHTML("beforebegin", buildFooter(data));
document.body.classList.add("has-soji-footer");
}
function init() {
fetch("/index.php?title=MediaWiki:FooterData.json&action=raw")
.then(function (r) { return r.text(); })
.then(function (text) { return JSON.parse(text); })
.then(injectFooter)
.catch(function (err) { console.error("Footer load failed:", err); });
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
$(function () {
if (mw.config.get('wgNamespaceNumber') < 0) return;
var title = mw.config.get('wgTitle');
var pageName = mw.config.get('wgPageName').replace(/_/g, ' ');
// Page Main Page: không cần breadcrumb
if (pageName === 'Main Page') return;
// Lấy category trực tiếp của page hoặc của category page hiện tại
var initialCats = mw.config.get('wgCategories') || [];
if (!initialCats.length) return;
var api = new mw.Api();
var chain = [];
var seen = {};
// Đi ngược lên cây qua API
function walkUp(catName) {
if (!catName || seen[catName] || catName === 'Main Page') {
return $.Deferred().resolve();
}
seen[catName] = true;
chain.unshift(catName);
return api.get({
action: 'query',
titles: 'Category:' + catName,
prop: 'categories',
cllimit: 1,
format: 'json',
formatversion: 2
}).then(function (data) {
var pages = (data.query && data.query.pages) || [];
if (!pages.length || !pages[0].categories) return;
var parent = pages[0].categories[0].title.replace(/^Category:/, '');
return walkUp(parent);
});
}
walkUp(initialCats[0]).then(function () {
var parts = [
'<a href="' + mw.util.getUrl('Main Page') + '">Main Page</a>'
];
chain.forEach(function (c) {
parts.push('<a href="' + mw.util.getUrl(c) + '">' + c + '</a>');
});
parts.push('<strong>' + title + '</strong>');
var html = '<nav class="custom-breadcrumb">' + parts.join(' > ') + '</nav>';
$('#mw-content-text').before(html);
});
});
$(function () {
// Tự động thêm target="_blank" cho mọi link tới file .pdf
$('a[href$=".pdf"], a[href*=".pdf?"], a[href*="/file/"][href*=".pdf"]').each(function () {
$(this).attr('target', '_blank');
$(this).attr('rel', 'noopener noreferrer');
// Thêm icon PDF nhỏ
if (!$(this).find('.pdf-icon').length) {
$(this).append(' <span class="pdf-icon" title="Open PDF in new tab">📄</span>');
}
});
});
$(function () {
// Force "View" links to open in new tab
$('.doc-view a').attr({
target: '_blank',
rel: 'noopener noreferrer'
});
// Force "Download" links to trigger file download
$('.doc-download a').each(function () {
var $a = $(this);
var href = $a.attr('href');
// Extract filename from URL
var match = href.match(/\/([^\/]+\.(?:pdf|zip|doc|docx|xls|xlsx))$/i);
if (match) {
$a.attr('download', match[1]);
} else {
$a.attr('download', '');
}
});
});
/* ============================================
* Auto-collapse TOC sub-headings on page load
* Only show H2 (main Heading) by default
* ============================================ */
$(function () {
var attempts = 0;
var maxAttempts = 15;
function collapseAllTOC() {
// Find all expanded toggle buttons in TOC
var $toggles = $('.vector-toc-toggle[aria-expanded="true"]');
if ($toggles.length > 0) {
$toggles.each(function () {
// Click toggle to collapse (triggers Vue.js handler)
this.click();
});
} else if (attempts < maxAttempts) {
// TOC chưa render xong, retry
attempts++;
setTimeout(collapseAllTOC, 100);
}
}
collapseAllTOC();
});
/* ============================================
* Sync chevron arrow with collapsible state
* ============================================ */
$(function () {
$('.category-block-toggle span[class*="mw-customtoggle-cb-"]').each(function () {
var $toggle = $(this);
var match = $toggle.attr('class').match(/mw-customtoggle-(\S+)/);
if (!match) return;
var $target = $('#mw-customcollapsible-' + match[1]);
if (!$target.length) return;
function syncArrow() {
if ($target.hasClass('mw-collapsed')) {
$toggle.removeClass('is-expanded');
} else {
$toggle.addClass('is-expanded');
}
}
// Initial state
syncArrow();
// Update on click
$toggle.on('click', function () {
setTimeout(syncArrow, 50);
});
});
});
/* ============================================
* Pending Changes Badge for Reviewers
* Show notification of pending changes count
* ============================================ */
$(function () {
// Chỉ show cho user có quyền review (reviewer, sysop, admin)
var userGroups = mw.config.get('wgUserGroups') || [];
var hasReviewRights = userGroups.some(function (g) {
return ['reviewer', 'sysop', 'bureaucrat'].indexOf(g) !== -1;
});
if (!hasReviewRights) {
return;
}
// Fetch pending count from API
function fetchPendingCount() {
new mw.Api().get({
action: 'query',
list: 'oldreviewedpages',
ornamespace: '0|14',
orfilterredir: 'nonredirects',
orlimit: 500,
format: 'json'
}).done(function (data) {
var pages = (data.query && data.query.oldreviewedpages) || [];
updateBadge(pages.length);
}).fail(function (err) {
console.log('Failed to fetch pending changes:', err);
});
}
// Update or create badge in sidebar
function updateBadge(count) {
var $badge = $('#pending-reviews-badge');
if (!$badge.length) {
// Tạo badge mới — tìm sidebar và prepend
var badgeHtml =
'<div id="pending-reviews-badge" style="' +
'padding: 12px 16px;' +
'margin: 12px 8px;' +
'background: #fff3e0;' +
'border: 1px solid #f57c00;' +
'border-left: 4px solid #e65100;' +
'border-radius: 4px;' +
'cursor: pointer;' +
'transition: background 0.2s;' +
'">' +
'<a href="' + mw.util.getUrl('Special:PendingChanges') + '" style="' +
'color: #e65100;' +
'text-decoration: none;' +
'font-weight: 600;' +
'font-size: 0.95em;' +
'display: block;' +
'">' +
'⚠️ <span id="pending-count" style="' +
'display: inline-block;' +
'background: #e65100;' +
'color: white;' +
'padding: 2px 10px;' +
'border-radius: 12px;' +
'margin: 0 6px;' +
'font-weight: 700;' +
'">0</span>' +
'pending review' +
'</a>' +
'</div>';
// Try multiple sidebar selectors (Vector 2022 + legacy)
var $sidebar = $('#mw-panel .vector-main-menu-content')
.add('#mw-panel-toc-list')
.add('#mw-panel')
.add('.vector-main-menu')
.first();
if ($sidebar.length) {
$sidebar.prepend(badgeHtml);
$badge = $('#pending-reviews-badge');
} else {
// Fallback: insert at top of body
$('body').prepend(
'<div style="position:fixed;top:60px;right:20px;z-index:9999;">' +
badgeHtml + '</div>'
);
$badge = $('#pending-reviews-badge');
}
}
$('#pending-count').text(count);
if (count === 0) {
$badge.hide();
} else {
$badge.show();
}
}
// Initial fetch
fetchPendingCount();
// Refresh every 60 seconds
setInterval(fetchPendingCount, 60000);
// Also refresh on focus (when user comes back to tab)
$(window).on('focus', fetchPendingCount);
});
/* ============================================================
* Placeholder auto-hide
* Logic: section nào có user content → ẩn placeholder trong section đó
* section trống chỉ có placeholder → vẫn hiển thị (làm hint)
* ============================================================ */
( function ( $ ) {
'use strict';
function hidePlaceholdersInFilledSections() {
var $headings = $( '.mw-parser-output h1, .mw-parser-output h2' );
if ( !$headings.length ) return;
$headings.each( function ( i ) {
var $h = $( this );
var $nextH = $headings.eq( i + 1 );
var $sectionEls = $nextH.length ? $h.nextUntil( $nextH ) : $h.nextAll();
// Đọc text trong section, loại trừ text của placeholder
var contentText = '';
$sectionEls.each( function () {
var $clone = $( this ).clone();
$clone.find( '.soji-placeholder' ).remove();
contentText += $clone.text();
});
// Nếu section có user content (ngoài placeholder) → ẩn placeholder
if ( contentText.trim().length > 0 ) {
$sectionEls.find( '.soji-placeholder' ).hide();
}
});
// Xử lý riêng placeholder trong table cell:
// Nếu cell có content khác → ẩn placeholder của cell đó
$( '.soji-placeholder:visible' ).each( function () {
var $ph = $( this );
var $cell = $ph.closest( 'td, th' );
if ( $cell.length ) {
var $clone = $cell.clone();
$clone.find( '.soji-placeholder' ).remove();
if ( $clone.text().trim().length > 0 ) {
$ph.hide();
}
}
});
}
$( hidePlaceholdersInFilledSections );
})( jQuery );
/* ============================================================
* Version History Row Actions
* Click row → hover toolbar: Edit / Add Above / Add Below
* Uses MediaWiki API to insert new {{FW-Row-Detail}} block
* ============================================================ */
( function ( $, mw ) {
'use strict';
// Chỉ chạy cho user có quyền edit
if ( !mw.config.get( 'wgIsArticle' ) ) return;
if ( mw.user.isAnon() ) return;
var TEMPLATE_NAME = 'FW-Row-Detail';
// Wikitext mẫu cho row mới (placeholder ở các field)
var NEW_ROW_TEMPLATE =
'{{' + TEMPLATE_NAME + '\n' +
'| version = NEW.VERSION\n' +
'| date = ' + new Date().toISOString().split( 'T' )[ 0 ] + '\n' +
'| type = Stable\n' +
'| description = \n' +
'* \'\'\'Added\'\'\'\n' +
'** New feature\n' +
'* \'\'\'Fixed\'\'\'\n' +
'** Bug fix\n' +
'}}\n';
function init() {
var $rows = $( '.fw-version-history tr[id^="fw-v"]' );
if ( !$rows.length ) return;
$rows.each( function () {
var $row = $( this );
var rowId = $row.attr( 'id' ); // e.g., "fw-v1.2.0"
var version = rowId.replace( /^fw-v/, '' );
// Toolbar HTML
var $toolbar = $(
'<div class="fw-row-toolbar">' +
'<button class="fw-btn fw-btn-edit" title="Edit this version">✏ Edit</button>' +
'<button class="fw-btn fw-btn-add-above" title="Add new version above">⬆ Add Above</button>' +
'<button class="fw-btn fw-btn-add-below" title="Add new version below">⬇ Add Below</button>' +
'</div>'
);
$row.find( 'td:first' ).css( 'position', 'relative' ).append( $toolbar );
// Event handlers
$toolbar.find( '.fw-btn-edit' ).on( 'click', function ( e ) {
e.stopPropagation();
editRow( version );
});
$toolbar.find( '.fw-btn-add-above' ).on( 'click', function ( e ) {
e.stopPropagation();
addRow( version, 'above' );
});
$toolbar.find( '.fw-btn-add-below' ).on( 'click', function ( e ) {
e.stopPropagation();
addRow( version, 'below' );
});
});
}
/**
* Open page in edit mode, scroll to the template block of target version
*/
function editRow( version ) {
var url = mw.util.getUrl( null, { action: 'edit' } ) + '#fw-edit-v' + version;
window.location.href = url;
}
/**
* Insert new {{FW-Row-Detail}} block above or below target version
*/
function addRow( targetVersion, position ) {
if ( !confirm( 'Add new version ' + ( position === 'above' ? 'ABOVE' : 'BELOW' ) + ' v' + targetVersion + '?' ) ) {
return;
}
var api = new mw.Api();
// 1. Lấy wikitext hiện tại
api.get({
action: 'parse',
page: mw.config.get( 'wgPageName' ),
prop: 'wikitext',
formatversion: 2
}).done( function ( data ) {
var wikitext = data.parse.wikitext;
// 2. Tìm block template của target version
var escapedVersion = targetVersion.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
var blockPattern = new RegExp(
'\\{\\{\\s*' + TEMPLATE_NAME + '[\\s\\S]*?version\\s*=\\s*' + escapedVersion + '[\\s\\S]*?\\}\\}',
'i'
);
var match = wikitext.match( blockPattern );
if ( !match ) {
mw.notify( 'Cannot find template block for v' + targetVersion, { type: 'error' } );
return;
}
// 3. Insert new block above/below
var newWikitext;
if ( position === 'above' ) {
newWikitext = wikitext.replace( match[ 0 ], NEW_ROW_TEMPLATE + '\n' + match[ 0 ] );
} else {
newWikitext = wikitext.replace( match[ 0 ], match[ 0 ] + '\n' + NEW_ROW_TEMPLATE );
}
// 4. Save via API
api.postWithToken( 'csrf', {
action: 'edit',
title: mw.config.get( 'wgPageName' ),
text: newWikitext,
summary: 'Added new firmware version row ' + position + ' v' + targetVersion + ' (via row action toolbar)',
formatversion: 2
}).done( function () {
mw.notify( '✓ Row added. Reloading...', { type: 'success' } );
setTimeout( function () {
// Redirect to edit mode để user fill values
window.location.href = mw.util.getUrl( null, { action: 'edit' } );
}, 800 );
}).fail( function ( err ) {
mw.notify( 'Save failed: ' + err, { type: 'error' } );
});
}).fail( function ( err ) {
mw.notify( 'Failed to fetch page content: ' + err, { type: 'error' } );
});
}
$( init );
})( jQuery, mediaWiki );