MediaWiki:Common.js: Difference between revisions
No edit summary |
Sprite preview: no upscale for NPC, cap at 256x256, remove redundant image-rendering (via update-page on MediaWiki MCP Server) |
||
| Line 179: | Line 179: | ||
fetchSprite(spriteName, function(spriteData) { | fetchSprite(spriteName, function(spriteData) { | ||
if (spriteData) { | if (spriteData) { | ||
var scaledWidth = | var scaledWidth = spriteData.width || 32; | ||
var scaledHeight = | var scaledHeight = spriteData.height || 32; | ||
preview.innerHTML = '<img src="' + spriteData.url + '" alt="' + spriteName + '" class="pixel-sprite" style=" | |||
// Cap at 256x256 | |||
var maxSize = 256; | |||
if (scaledWidth > maxSize || scaledHeight > maxSize) { | |||
var scale = maxSize / Math.max(scaledWidth, scaledHeight); | |||
scaledWidth = Math.floor(scaledWidth * scale); | |||
scaledHeight = Math.floor(scaledHeight * scale); | |||
} | |||
preview.innerHTML = '<img src="' + spriteData.url + '" alt="' + spriteName + '" class="pixel-sprite" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">'; | |||
} else { | } else { | ||
preview.innerHTML = '<span style="color: #c33;">Sprite not found: ' + spriteName + '.png</span>'; | preview.innerHTML = '<span style="color: #c33;">Sprite not found: ' + spriteName + '.png</span>'; | ||
| Line 250: | Line 259: | ||
var scaledWidth = (spriteData.width || 32) * 2; | var scaledWidth = (spriteData.width || 32) * 2; | ||
var scaledHeight = (spriteData.height || 32) * 2; | var scaledHeight = (spriteData.height || 32) * 2; | ||
previewDiv.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style=" | previewDiv.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">'; | ||
} else { | } else { | ||
previewDiv.innerHTML = '<span style="color: var(--text-muted, #888); font-size: 12px;">No sprite</span>'; | previewDiv.innerHTML = '<span style="color: var(--text-muted, #888); font-size: 12px;">No sprite</span>'; | ||
| Line 470: | Line 479: | ||
function initNPCForm() { | function initNPCForm() { | ||
// Try immediately | // Try immediately | ||
var | var spriteFound = initSpritePreview(); | ||
var shopFound = initShopItemPreview(); | |||
// If not found, wait for page to settle and try again | // If not found, wait for page to settle and try again | ||
if (! | if (!spriteFound || !shopFound) { | ||
onPageSettled(function() { | onPageSettled(function() { | ||
initSpritePreview(); | |||
initShopItemPreview(); | initShopItemPreview(); | ||
}); | }); | ||
| Line 484: | Line 495: | ||
clearTimeout(observerTimeout); | clearTimeout(observerTimeout); | ||
observerTimeout = setTimeout(function() { | observerTimeout = setTimeout(function() { | ||
initSpritePreview(); | |||
initShopItemPreview(); | initShopItemPreview(); | ||
}, 300); | }, 300); | ||
| Line 579: | Line 591: | ||
var scaledWidth = (spriteData.width || 32) * 2; | var scaledWidth = (spriteData.width || 32) * 2; | ||
var scaledHeight = (spriteData.height || 32) * 2; | var scaledHeight = (spriteData.height || 32) * 2; | ||
previewContainer.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style=" | previewContainer.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">'; | ||
} else { | } else { | ||
previewContainer.innerHTML = '<span style="color: var(--text-muted, #888);">No sprite found for "' + itemName + '"</span>'; | previewContainer.innerHTML = '<span style="color: var(--text-muted, #888);">No sprite found for "' + itemName + '"</span>'; | ||
Latest revision as of 22:08, 31 January 2026
/**
* Apogea Wiki Common JavaScript
*/
(function() {
'use strict';
/**
* Wait for page to "settle" - fires callback after DOM stops changing
* Useful for waiting for async widgets (OOUI, etc.) to finish loading
*/
function onPageSettled(callback, options) {
options = options || {};
var debounceMs = options.debounce || 300;
var maxWaitMs = options.maxWait || 30000;
var startTime = Date.now();
var timeout;
var settled = false;
function done() {
if (settled) return;
settled = true;
observer.disconnect();
callback();
}
var observer = new MutationObserver(function() {
clearTimeout(timeout);
// If we've waited too long, just fire anyway
if (Date.now() - startTime > maxWaitMs) {
done();
return;
}
timeout = setTimeout(done, debounceMs);
});
observer.observe(document.body, { childList: true, subtree: true });
// Start the timer - if nothing changes, fire after debounce period
timeout = setTimeout(done, debounceMs);
}
/**
* Watch an input for value changes (handles programmatic changes from OOUI)
* Calls callback when value changes, with debouncing
*/
function watchInputValue(input, callback, debounceMs) {
debounceMs = debounceMs || 300;
var lastValue = input.value;
var timeout;
function checkValue() {
if (input.value !== lastValue) {
lastValue = input.value;
clearTimeout(timeout);
timeout = setTimeout(callback, debounceMs);
}
}
// Poll for value changes (catches OOUI programmatic updates)
setInterval(checkValue, 150);
// Also listen to native events for immediate response to user typing
input.addEventListener('input', function() {
lastValue = input.value; // Sync to avoid double-firing
clearTimeout(timeout);
timeout = setTimeout(callback, debounceMs);
});
}
/**
* Fetch and display a sprite image by name
* Sprite files should be named "{Name}.png" matching the item/entity name
*/
function fetchSprite(spriteName, callback) {
if (!spriteName) {
callback(null);
return;
}
var fileName = spriteName.replace(/_/g, ' ') + '.png';
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'query',
titles: 'File:' + fileName,
prop: 'imageinfo',
iiprop: 'url|size',
format: 'json'
},
dataType: 'json',
success: function(data) {
var pages = data.query.pages;
for (var id in pages) {
if (pages[id].imageinfo && pages[id].imageinfo[0]) {
callback({
url: pages[id].imageinfo[0].url,
width: pages[id].imageinfo[0].width,
height: pages[id].imageinfo[0].height
});
return;
}
}
callback(null);
},
error: function() {
callback(null);
}
});
}
/**
* Sprite preview for Page Forms
*/
function initSpritePreview() {
var inputs = document.querySelectorAll('input');
var found = false;
inputs.forEach(function(input) {
var name = input.getAttribute('name') || '';
// Match sprite fields
if (name.indexOf('sprite]') === -1) return;
// Skip template fields (Page Forms uses [num] as placeholder)
if (name.indexOf('[num]') !== -1) return;
found = true;
console.log('[Apogea] Found sprite field:', name);
// Skip if already has preview
if (input.dataset.hasPreview) return;
input.dataset.hasPreview = 'true';
// Extract variant index from field name (e.g., ItemEntry[1b][sprite] -> 1b)
var indexMatch = name.match(/\[(\d+[a-z]?)\]/);
var variantIndex = indexMatch ? indexMatch[1] : null;
// Create preview container
var preview = document.createElement('div');
preview.className = 'sprite-preview';
preview.style.cssText = 'margin-top: 5px; min-height: 48px;';
input.parentNode.appendChild(preview);
// Find corresponding name field with same variant index
var nameInput = null;
if (variantIndex) {
nameInput = document.querySelector('input[name*="[' + variantIndex + '][name]"]');
} else {
// Fallback for non-indexed fields
var container = input.closest('tr') || input.closest('fieldset') || input.parentNode.parentNode;
if (container) {
var allInputs = container.parentNode.querySelectorAll('input');
allInputs.forEach(function(inp) {
var n = inp.getAttribute('name') || '';
if (n.indexOf('[name]') !== -1) {
nameInput = inp;
}
});
}
}
function updatePreview() {
var spriteName = input.value.trim();
// Fall back to name field if sprite is empty
if (!spriteName && nameInput) {
spriteName = nameInput.value.trim();
}
if (!spriteName) {
preview.innerHTML = '<span style="color: #888;">No preview</span>';
return;
}
fetchSprite(spriteName, function(spriteData) {
if (spriteData) {
var scaledWidth = spriteData.width || 32;
var scaledHeight = spriteData.height || 32;
// Cap at 256x256
var maxSize = 256;
if (scaledWidth > maxSize || scaledHeight > maxSize) {
var scale = maxSize / Math.max(scaledWidth, scaledHeight);
scaledWidth = Math.floor(scaledWidth * scale);
scaledHeight = Math.floor(scaledHeight * scale);
}
preview.innerHTML = '<img src="' + spriteData.url + '" alt="' + spriteName + '" class="pixel-sprite" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">';
} else {
preview.innerHTML = '<span style="color: #c33;">Sprite not found: ' + spriteName + '.png</span>';
}
});
}
// Update on input change (debounced)
var timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(updatePreview, 500);
});
// Also update when name field changes
if (nameInput) {
nameInput.addEventListener('input', function() {
if (!input.value.trim()) {
clearTimeout(timeout);
timeout = setTimeout(updatePreview, 500);
}
});
}
// Initial preview
updatePreview();
});
return found;
}
/**
* Shop item preview for NPC forms
*/
function initShopItemPreview() {
var containers = document.querySelectorAll('.shop-entry-container');
var found = false;
containers.forEach(function(container) {
var previewDiv = container.querySelector('.shop-item-preview');
if (!previewDiv) return;
// Find the item input field within this container
var itemInput = container.querySelector('input[name*="[item]"]');
if (!itemInput) return;
// Skip template containers
if (itemInput.getAttribute('name').indexOf('[num]') !== -1) return;
// Skip if already initialized
if (previewDiv.dataset.initialized) return;
previewDiv.dataset.initialized = 'true';
found = true;
console.log('[Apogea] Initializing shop item preview for:', itemInput.getAttribute('name'));
function updatePreview() {
var itemName = itemInput.value.trim();
if (!itemName) {
previewDiv.innerHTML = '';
return;
}
// Sprite name matches item name
fetchSprite(itemName, function(spriteData) {
if (spriteData) {
var scaledWidth = (spriteData.width || 32) * 2;
var scaledHeight = (spriteData.height || 32) * 2;
previewDiv.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">';
} else {
previewDiv.innerHTML = '<span style="color: var(--text-muted, #888); font-size: 12px;">No sprite</span>';
}
});
}
// Watch for value changes (handles OOUI combobox)
watchInputValue(itemInput, updatePreview, 300);
// Initial preview
updatePreview();
});
return found;
}
/**
* Live infobox preview for item forms
*/
function initInfoboxPreview() {
var containers = document.querySelectorAll('.item-form-container');
console.log('[Apogea] initInfoboxPreview called, found containers:', containers.length);
var found = false;
containers.forEach(function(container, idx) {
var previewDiv = container.querySelector('.infobox-preview');
console.log('[Apogea] Container', idx, '- has previewDiv:', !!previewDiv);
if (!previewDiv) return;
// Find variant index from any field in this container
var anyField = container.querySelector('input[name*="["], select[name*="["]');
console.log('[Apogea] Container', idx, '- anyField:', anyField ? anyField.getAttribute('name') : 'none');
// Skip template containers (Page Forms uses [num] as placeholder)
if (anyField && anyField.getAttribute('name').indexOf('[num]') !== -1) {
console.log('[Apogea] Container', idx, '- skipping template container');
return;
}
var variantIndex = null;
if (anyField) {
var indexMatch = anyField.getAttribute('name').match(/\[(\d+[a-z]?)\]/);
variantIndex = indexMatch ? indexMatch[1] : null;
console.log('[Apogea] Container', idx, '- indexMatch:', indexMatch, '- variantIndex:', variantIndex);
}
// Check if already initialized for THIS variant (handles cloned elements from template)
var initializedVariant = previewDiv.dataset.initializedVariant;
console.log('[Apogea] Container', idx, '- initializedVariant:', initializedVariant, '- current variant:', variantIndex);
if (initializedVariant === variantIndex) {
console.log('[Apogea] Container', idx, '- already initialized for this variant, skipping');
return;
}
// Mark as initialized with the variant index
previewDiv.dataset.initializedVariant = variantIndex;
found = true;
console.log('[Apogea] Initializing infobox preview for variant:', variantIndex || 'default');
// Get all form fields in this container
var fields = container.querySelectorAll('input, select, textarea');
console.log('[Apogea] Container', idx, '- field count:', fields.length);
function getFieldValue(fieldName) {
var selector;
if (variantIndex) {
// Scoped to this variant's index
selector = '[name*="[' + variantIndex + '][' + fieldName + ']"]';
} else {
selector = '[name*="[' + fieldName + ']"]';
}
var field = container.querySelector(selector);
return field ? field.value.trim() : '';
}
function updateInfoboxPreview() {
console.log('[Apogea] updateInfoboxPreview called for variant:', variantIndex);
var name = getFieldValue('name') || mw.config.get('wgTitle') || 'Item';
var sprite = getFieldValue('sprite') || name;
var rarity = getFieldValue('rarity') || 'common';
var description = getFieldValue('description');
var damage = getFieldValue('damage');
var armor = getFieldValue('armor');
var defense = getFieldValue('defense');
var health = getFieldValue('health');
var mana = getFieldValue('mana');
var rng = getFieldValue('rng');
var attackSpeed = getFieldValue('attack_speed');
var moveSpeed = getFieldValue('move_speed');
var ability = getFieldValue('ability');
var magic = getFieldValue('magic');
var healthRegen = getFieldValue('health_regen');
var manaRegen = getFieldValue('mana_regen');
var weight = getFieldValue('weight');
var size = getFieldValue('size');
var slots = getFieldValue('container');
var type = getFieldValue('type');
var slot = getFieldValue('slot');
console.log('[Apogea] Variant', variantIndex, '- name:', name, 'sprite:', sprite);
// Build wikitext for preview with all params
var params = [
'preview=true',
'name=' + name,
'sprite=' + sprite,
'rarity=' + rarity,
'float=none'
];
if (description) params.push('description=' + description);
if (damage) params.push('damage=' + damage);
if (armor) params.push('armor=' + armor);
if (defense) params.push('defense=' + defense);
if (health) params.push('health=' + health);
if (mana) params.push('mana=' + mana);
if (rng) params.push('rng=' + rng);
if (attackSpeed) params.push('attack_speed=' + attackSpeed);
if (moveSpeed) params.push('move_speed=' + moveSpeed);
if (ability) params.push('ability=' + ability);
if (magic) params.push('magic=' + magic);
if (healthRegen) params.push('health_regen=' + healthRegen);
if (manaRegen) params.push('mana_regen=' + manaRegen);
if (weight) params.push('weight=' + weight);
if (size) params.push('size=' + size);
if (slots) params.push('container=' + slots);
if (type) params.push('type=' + type);
if (slot) params.push('slot=' + slot);
var wikitext = '{{Infobox Item|' + params.join('|') + '}}';
// Use API to parse the wikitext
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'parse',
text: wikitext,
contentmodel: 'wikitext',
prop: 'text',
format: 'json',
disablelimitreport: true
},
dataType: 'json',
success: function(data) {
if (data.parse && data.parse.text) {
previewDiv.innerHTML = data.parse.text['*'];
} else {
previewDiv.innerHTML = '<em style="color: #888;">Preview unavailable</em>';
}
},
error: function() {
previewDiv.innerHTML = '<em style="color: #888;">Preview unavailable</em>';
}
});
}
// Debounced update
var timeout;
console.log('[Apogea] Attaching listeners to', fields.length, 'fields for variant', variantIndex);
fields.forEach(function(field) {
field.addEventListener('input', function() {
console.log('[Apogea] Input event on field:', field.getAttribute('name'), 'for variant:', variantIndex);
clearTimeout(timeout);
timeout = setTimeout(updateInfoboxPreview, 800);
});
field.addEventListener('change', function() {
console.log('[Apogea] Change event on field:', field.getAttribute('name'), 'for variant:', variantIndex);
clearTimeout(timeout);
timeout = setTimeout(updateInfoboxPreview, 800);
});
});
// Initial preview immediately
updateInfoboxPreview();
});
return found;
}
/**
* Initialize Item Form features (only on FormEdit/Item pages)
*/
function initItemForm() {
// Try immediately
var spriteFound = initSpritePreview();
var infoboxFound = initInfoboxPreview();
// If not found, wait for page to settle and try again
if (!spriteFound || !infoboxFound) {
onPageSettled(function() {
initSpritePreview();
initInfoboxPreview();
});
}
// Watch for dynamically added fields (e.g., "Add another" in Page Forms)
// Debounced to allow Page Forms to finish rendering new elements
var observerTimeout;
var observer = new MutationObserver(function(mutations) {
clearTimeout(observerTimeout);
observerTimeout = setTimeout(function() {
initSpritePreview();
initInfoboxPreview();
}, 300);
});
observer.observe(document.body, { childList: true, subtree: true });
}
/**
* Initialize NPC Form features
*/
function initNPCForm() {
// Try immediately
var spriteFound = initSpritePreview();
var shopFound = initShopItemPreview();
// If not found, wait for page to settle and try again
if (!spriteFound || !shopFound) {
onPageSettled(function() {
initSpritePreview();
initShopItemPreview();
});
}
// Watch for dynamically added shop entries
var observerTimeout;
var observer = new MutationObserver(function(mutations) {
clearTimeout(observerTimeout);
observerTimeout = setTimeout(function() {
initSpritePreview();
initShopItemPreview();
}, 300);
});
observer.observe(document.body, { childList: true, subtree: true });
}
/**
* Edit Item page enhancements
* Shows sprite preview and changes button text based on page existence
*/
function initEditItemPage() {
console.log('[Apogea] initEditItemPage called');
// Find the text input - it's inside .pfPageNameWithoutNamespace with name="page_name"
var textInput = document.querySelector('.pfPageNameWithoutNamespace input[name="page_name"]');
// Find the submit button and its label
var submitButton = document.querySelector('.pfCreateOrEditButton button[type="submit"]');
var buttonLabel = document.querySelector('.pfCreateOrEditButton .oo-ui-labelElement-label');
console.log('[Apogea] Text input found:', !!textInput);
console.log('[Apogea] Submit button found:', !!submitButton);
if (!textInput || !submitButton) {
console.log('[Apogea] Required elements not found yet');
return false;
}
console.log('[Apogea] Edit Item page enhancements initializing');
// Find the fieldset to insert after
var fieldset = textInput.closest('fieldset');
// Create status container
var statusContainer = document.createElement('div');
statusContainer.className = 'edit-item-status';
statusContainer.style.cssText = 'margin: 10px 0; font-size: 14px;';
// Create preview container
var previewContainer = document.createElement('div');
previewContainer.className = 'edit-item-preview';
previewContainer.style.cssText = 'margin: 15px 0; min-height: 64px;';
// Insert after the fieldset
if (fieldset && fieldset.parentNode) {
fieldset.parentNode.insertBefore(statusContainer, fieldset.nextSibling);
fieldset.parentNode.insertBefore(previewContainer, statusContainer.nextSibling);
}
var originalButtonText = buttonLabel ? buttonLabel.textContent : 'Edit Item';
function updatePreview() {
var itemName = textInput.value.trim();
console.log('[Apogea] updatePreview called, itemName:', itemName);
if (!itemName) {
previewContainer.innerHTML = '';
statusContainer.innerHTML = '';
if (buttonLabel) {
buttonLabel.textContent = originalButtonText;
}
return;
}
// Check both page existence and sprite existence in parallel
var pageExists = null;
var spriteData = null;
var checksComplete = 0;
function onCheckComplete() {
checksComplete++;
if (checksComplete < 2) return;
console.log('[Apogea] Both checks complete - pageExists:', pageExists, 'spriteData:', spriteData);
// Update button text
var buttonText = pageExists ? 'Edit Item' : 'Add Item';
if (buttonLabel) {
buttonLabel.textContent = buttonText;
}
// Update status
var statusHtml = '';
if (pageExists) {
statusHtml = '<span style="color: var(--color-success, #7a9a5a);">✓ Page exists</span>';
} else {
statusHtml = '<span style="color: var(--gold, #d4a84b);">⚠ New item (page does not exist)</span>';
}
statusContainer.innerHTML = statusHtml;
// Update sprite preview
if (spriteData && spriteData.url) {
var scaledWidth = (spriteData.width || 32) * 2;
var scaledHeight = (spriteData.height || 32) * 2;
previewContainer.innerHTML = '<img src="' + spriteData.url + '" alt="' + itemName + '" style="width: ' + scaledWidth + 'px; height: ' + scaledHeight + 'px;">';
} else {
previewContainer.innerHTML = '<span style="color: var(--text-muted, #888);">No sprite found for "' + itemName + '"</span>';
}
}
// Check if page exists
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'query',
titles: itemName,
format: 'json'
},
dataType: 'json',
success: function(data) {
pageExists = false;
if (data.query && data.query.pages) {
for (var id in data.query.pages) {
if (id !== '-1' && !data.query.pages[id].missing) {
pageExists = true;
break;
}
}
}
onCheckComplete();
},
error: function() {
pageExists = false;
onCheckComplete();
}
});
// Sprite name matches item name
fetchSprite(itemName, function(data) {
spriteData = data;
onCheckComplete();
});
}
// Watch for value changes (handles OOUI autocomplete selection)
watchInputValue(textInput, updatePreview, 300);
// Initial check if there's already a value
if (textInput.value.trim()) {
updatePreview();
}
return true;
}
// Main initialization
function init() {
console.log('[Apogea] Common.js loaded');
var pageName = mw.config.get('wgPageName');
var canonicalSpecialPage = mw.config.get('wgCanonicalSpecialPageName');
var action = mw.config.get('wgAction');
console.log('[Apogea] pageName:', pageName, 'canonicalSpecialPage:', canonicalSpecialPage, 'action:', action);
onPageSettled(function() {
if (pageName == 'Apogea_Wiki:Edit_Item') {
initEditItemPage();
} else if (document.getElementsByClassName('item-form-fields')) {
console.log('[Apogea] Item Edit form detected');
initItemForm();
} else if (pageName == 'Apogea_Wiki:Edit_NPC') {
console.log('[Apogea] NPC form detected');
initNPCForm();
}
}, { debounce: 500 });
}
// Wait for DOM and jQuery
$(document).ready(init);
})();