MediaWiki:Common.js: Difference between revisions
Appearance
No edit summary Tag: Reverted |
No edit summary |
||
| (32 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | |||
* MediaWiki:Common.js | |||
* Once Human Guide | |||
*/ | |||
mw.loader.using(['mediawiki.util']).then(function () { | mw.loader.using(['mediawiki.util']).then(function () { | ||
$(function () { | $(function () { | ||
/* ================= TAB SYSTEM | /* ============================================================ | ||
* TAB SYSTEM | |||
* Wires up .mw-tab-buttons groups so each .mw-tab-btn shows its | |||
* matching .mw-tab-content sibling. | |||
* ============================================================ */ | |||
function initTabs() { | |||
document.querySelectorAll('.mw-tab-buttons').forEach(group => { | |||
const buttons = group.querySelectorAll('.mw-tab-btn'); | |||
buttons.forEach(btn => { | |||
btn.addEventListener('click', function () { | |||
const tabId = this.getAttribute('data-tab'); | |||
// Deactivate buttons in this group only | |||
buttons.forEach(b => b.classList.remove('active')); | |||
this.classList.add('active'); | |||
// Hide only the direct-child tab panels | |||
const container = group.parentElement; | |||
container.querySelectorAll(':scope > .mw-tab-content').forEach(c => { | |||
c.classList.remove('active'); | |||
}); | |||
// Show the target panel and init any dropdowns inside it | |||
const target = container.querySelector('#' + tabId); | |||
if (target) { | |||
target.classList.add('active'); | |||
initDropdowns(target); | |||
} | |||
}); | }); | ||
}); | }); | ||
}); | }); | ||
// | // Auto-click the first tab in each group on load | ||
document.querySelectorAll('.mw-tab-buttons').forEach(group => { | |||
const first = group.querySelector('.mw-tab-btn'); | |||
} | if (first) first.click(); | ||
} | }); | ||
} | |||
/* ============================================================ | |||
* JSON-DRIVEN DROPDOWN UI | |||
* Renders .mw-dropdown-ui[data-options] containers as a | |||
* label button, sorted list, and detail output panel. | |||
* Supports both the new "lines" array format and the old | |||
* semicolon-separated "content" string format. | |||
* ============================================================ */ | |||
function initDropdowns(scope) { | |||
scope.querySelectorAll('.mw-dropdown-ui[data-options]').forEach(container => { | |||
if (container.dataset.rendered === 'true') return; | |||
let data; | |||
try { | |||
data = JSON.parse(container.dataset.options); | |||
} catch (e) { | |||
console.error('Bad dropdown JSON:', e); | |||
return; | |||
} | |||
container.innerHTML = ''; | |||
const btn = document.createElement('div'); | |||
btn.className = 'mod-dropdown-btn'; | |||
btn.textContent = container.dataset.label || 'Select One'; | |||
const list = document.createElement('div'); | |||
list.className = 'mod-dropdown-list'; | |||
const output = document.createElement('div'); | |||
output.className = 'mw-dropdown-output'; | |||
output.innerHTML = '<div class="mw-ui-placeholder">Select something to see details</div>'; | |||
Object.values(data) | |||
.sort((a, b) => a.label.localeCompare(b.label)) | |||
.forEach(item => { | |||
const option = document.createElement('div'); | |||
option.className = 'mod-dropdown-item'; | |||
option.textContent = item.label; | |||
option.onclick = () => { | |||
btn.textContent = item.label; | |||
list.style.display = 'none'; | |||
let lines = []; | |||
if (item.lines && Array.isArray(item.lines)) { | |||
lines = item.lines; | |||
} else if (item.content) { | |||
lines = item.content.split(';').map(s => s.trim()).filter(Boolean); | |||
} | |||
output.innerHTML = | |||
`<div class="mw-ui-title">${item.label}</div>` + | |||
lines.map(line => `<div class="mw-ui-line">${line}</div>`).join(''); | |||
}; | |||
list.appendChild(option); | |||
}); | |||
// Toggle list open/closed | |||
btn.onclick = (e) => { | |||
e.stopPropagation(); | |||
list.style.display = list.style.display === 'block' ? 'none' : 'block'; | |||
}; | |||
// Close when clicking outside | |||
document.addEventListener('click', () => { | |||
list.style.display = 'none'; | |||
}); | |||
container.appendChild(btn); | |||
container.appendChild(list); | |||
container.appendChild(output); | |||
container.dataset.rendered = 'true'; | |||
}); | |||
} | |||
/* ============================================================ | |||
* EXCLUSIVE COLLAPSIBLE DROPDOWNS (Loot Crate pages) | |||
* When one item's image is opened, all others in the group | |||
* are forced to collapse so only one is visible at a time. | |||
* ============================================================ */ | |||
function initExclusiveCollapsibles() { | |||
const outerKey = 'lootcrate'; | |||
const outerCollapsibleId = 'mw-customcollapsible-' + outerKey; | |||
let suppress = false; | |||
// Listen on document and check the click target. Use NATIVE click() | |||
// to retrigger MediaWiki's handler — jQuery .trigger() doesn't always | |||
// reach handlers that MW attached with native addEventListener. | |||
document.addEventListener('click', function (e) { | |||
if (suppress) return; | |||
// Find an ancestor with a mw-customtoggle-* class | |||
let toggle = e.target; | |||
while (toggle && toggle !== document) { | |||
if (toggle.className && /mw-customtoggle-(\S+)/.test(toggle.className)) break; | |||
toggle = toggle.parentElement; | |||
} | |||
if (!toggle || toggle === document) return; | |||
// Must be inside the outer loot-crate dropdown | |||
const | const outerCol = document.getElementById(outerCollapsibleId); | ||
if (!outerCol || !outerCol.contains(toggle)) return; | |||
const match = toggle.className.match(/mw-customtoggle-(\S+)/); | |||
if (!match) return; | |||
const clickedKey = match[1]; | |||
const pickedName = toggle.textContent.trim(); | |||
setTimeout(function () { | |||
suppress = true; | |||
// Close any other items that are currently open | |||
const inners = outerCol.querySelectorAll('[class*="mw-customtoggle-"]'); | |||
inners.forEach(function (t) { | |||
const m = t.className.match(/mw-customtoggle-(\S+)/); | |||
if (!m || m[1] === clickedKey) return; | |||
const img = document.getElementById('mw-customcollapsible-' + m[1]); | |||
if (img && !img.classList.contains('mw-collapsed')) { | |||
t.click(); | |||
} | |||
}); | |||
// Update outer dropdown label | |||
const outerToggle = document.querySelector('.mw-customtoggle-' + outerKey); | |||
if (outerToggle) { | |||
const labelSpan = outerToggle.querySelector('span'); | |||
if (labelSpan) labelSpan.textContent = pickedName; | |||
} | |||
// Close the outer dropdown | |||
if (outerCol && !outerCol.classList.contains('mw-collapsed') && outerToggle) { | |||
outerToggle.click(); | |||
} | |||
suppress = false; | |||
}, 50); | |||
}, true); | |||
} | |||
/* ============================================================ | |||
* DISCORD TAB OVERRIDE | |||
* Repurposes the page's "Discussion" tab as a Discord invite. | |||
* ============================================================ */ | |||
function initDiscordTab() { | |||
const discussionTab = document.querySelector('#ca-talk a'); | |||
if (discussionTab) { | |||
discussionTab.href = 'https://discord.com/invite/FZtkXeGeUA'; | |||
discussionTab.target = '_blank'; | |||
discussionTab.textContent = 'Discord'; | |||
} | } | ||
} | |||
/* ============================================================ | |||
* BOOT | |||
* ============================================================ */ | |||
initTabs(); | |||
initExclusiveCollapsibles(); | |||
initDiscordTab(); | |||
}); | }); | ||
}); | }); | ||
Latest revision as of 13:38, 27 May 2026
/**
* MediaWiki:Common.js
* Once Human Guide
*/
mw.loader.using(['mediawiki.util']).then(function () {
$(function () {
/* ============================================================
* TAB SYSTEM
* Wires up .mw-tab-buttons groups so each .mw-tab-btn shows its
* matching .mw-tab-content sibling.
* ============================================================ */
function initTabs() {
document.querySelectorAll('.mw-tab-buttons').forEach(group => {
const buttons = group.querySelectorAll('.mw-tab-btn');
buttons.forEach(btn => {
btn.addEventListener('click', function () {
const tabId = this.getAttribute('data-tab');
// Deactivate buttons in this group only
buttons.forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Hide only the direct-child tab panels
const container = group.parentElement;
container.querySelectorAll(':scope > .mw-tab-content').forEach(c => {
c.classList.remove('active');
});
// Show the target panel and init any dropdowns inside it
const target = container.querySelector('#' + tabId);
if (target) {
target.classList.add('active');
initDropdowns(target);
}
});
});
});
// Auto-click the first tab in each group on load
document.querySelectorAll('.mw-tab-buttons').forEach(group => {
const first = group.querySelector('.mw-tab-btn');
if (first) first.click();
});
}
/* ============================================================
* JSON-DRIVEN DROPDOWN UI
* Renders .mw-dropdown-ui[data-options] containers as a
* label button, sorted list, and detail output panel.
* Supports both the new "lines" array format and the old
* semicolon-separated "content" string format.
* ============================================================ */
function initDropdowns(scope) {
scope.querySelectorAll('.mw-dropdown-ui[data-options]').forEach(container => {
if (container.dataset.rendered === 'true') return;
let data;
try {
data = JSON.parse(container.dataset.options);
} catch (e) {
console.error('Bad dropdown JSON:', e);
return;
}
container.innerHTML = '';
const btn = document.createElement('div');
btn.className = 'mod-dropdown-btn';
btn.textContent = container.dataset.label || 'Select One';
const list = document.createElement('div');
list.className = 'mod-dropdown-list';
const output = document.createElement('div');
output.className = 'mw-dropdown-output';
output.innerHTML = '<div class="mw-ui-placeholder">Select something to see details</div>';
Object.values(data)
.sort((a, b) => a.label.localeCompare(b.label))
.forEach(item => {
const option = document.createElement('div');
option.className = 'mod-dropdown-item';
option.textContent = item.label;
option.onclick = () => {
btn.textContent = item.label;
list.style.display = 'none';
let lines = [];
if (item.lines && Array.isArray(item.lines)) {
lines = item.lines;
} else if (item.content) {
lines = item.content.split(';').map(s => s.trim()).filter(Boolean);
}
output.innerHTML =
`<div class="mw-ui-title">${item.label}</div>` +
lines.map(line => `<div class="mw-ui-line">${line}</div>`).join('');
};
list.appendChild(option);
});
// Toggle list open/closed
btn.onclick = (e) => {
e.stopPropagation();
list.style.display = list.style.display === 'block' ? 'none' : 'block';
};
// Close when clicking outside
document.addEventListener('click', () => {
list.style.display = 'none';
});
container.appendChild(btn);
container.appendChild(list);
container.appendChild(output);
container.dataset.rendered = 'true';
});
}
/* ============================================================
* EXCLUSIVE COLLAPSIBLE DROPDOWNS (Loot Crate pages)
* When one item's image is opened, all others in the group
* are forced to collapse so only one is visible at a time.
* ============================================================ */
function initExclusiveCollapsibles() {
const outerKey = 'lootcrate';
const outerCollapsibleId = 'mw-customcollapsible-' + outerKey;
let suppress = false;
// Listen on document and check the click target. Use NATIVE click()
// to retrigger MediaWiki's handler — jQuery .trigger() doesn't always
// reach handlers that MW attached with native addEventListener.
document.addEventListener('click', function (e) {
if (suppress) return;
// Find an ancestor with a mw-customtoggle-* class
let toggle = e.target;
while (toggle && toggle !== document) {
if (toggle.className && /mw-customtoggle-(\S+)/.test(toggle.className)) break;
toggle = toggle.parentElement;
}
if (!toggle || toggle === document) return;
// Must be inside the outer loot-crate dropdown
const outerCol = document.getElementById(outerCollapsibleId);
if (!outerCol || !outerCol.contains(toggle)) return;
const match = toggle.className.match(/mw-customtoggle-(\S+)/);
if (!match) return;
const clickedKey = match[1];
const pickedName = toggle.textContent.trim();
setTimeout(function () {
suppress = true;
// Close any other items that are currently open
const inners = outerCol.querySelectorAll('[class*="mw-customtoggle-"]');
inners.forEach(function (t) {
const m = t.className.match(/mw-customtoggle-(\S+)/);
if (!m || m[1] === clickedKey) return;
const img = document.getElementById('mw-customcollapsible-' + m[1]);
if (img && !img.classList.contains('mw-collapsed')) {
t.click();
}
});
// Update outer dropdown label
const outerToggle = document.querySelector('.mw-customtoggle-' + outerKey);
if (outerToggle) {
const labelSpan = outerToggle.querySelector('span');
if (labelSpan) labelSpan.textContent = pickedName;
}
// Close the outer dropdown
if (outerCol && !outerCol.classList.contains('mw-collapsed') && outerToggle) {
outerToggle.click();
}
suppress = false;
}, 50);
}, true);
}
/* ============================================================
* DISCORD TAB OVERRIDE
* Repurposes the page's "Discussion" tab as a Discord invite.
* ============================================================ */
function initDiscordTab() {
const discussionTab = document.querySelector('#ca-talk a');
if (discussionTab) {
discussionTab.href = 'https://discord.com/invite/FZtkXeGeUA';
discussionTab.target = '_blank';
discussionTab.textContent = 'Discord';
}
}
/* ============================================================
* BOOT
* ============================================================ */
initTabs();
initExclusiveCollapsibles();
initDiscordTab();
});
});