MediaWiki:Gadget-ResourcePage.js
From Dune Awakening DB
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.
/**
* ResourcePage Gadget - Complete Fixed Version
* Handles recipe popups and search functionality for resource pages
*
* @requires jquery
* @requires mediawiki.api
* @requires mediawiki.util
*/
(function($, mw) {
'use strict';
// Only run on pages with resource page elements
if (!$('.crafting-section, .recipe-table').length) {
return;
}
console.log('[ResourcePage] Gadget loading...');
// Cache for storing recipe data
var recipeCache = {
craftedWith: null,
craftedFrom: null
};
// Initialize module
function init() {
console.log('[ResourcePage] Initializing...');
// Add required styles
injectRequiredStyles();
// Add table search functionality
initTableSearch();
// Add popup handlers - FIXED VERSION
initPopupHandlers();
// Format tables
formatTables();
// Add animation styles
addAnimationStyles();
}
// Inject required CSS
function injectRequiredStyles() {
if ($('#resourcePageGadgetStyles').length === 0) {
var styles = `<style id="resourcePageGadgetStyles">
/* Span button styles */
.dune-action-button {
display: inline-block !important;
cursor: pointer !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
/* Loading spinner */
.loading-spinner {
font-size: 48px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Sort indicators */
.recipe-popup-table th {
cursor: pointer;
user-select: none;
}
.recipe-popup-table th.sort-asc::after {
content: " ↑";
color: #fce7c8;
}
.recipe-popup-table th.sort-desc::after {
content: " ↓";
color: #fce7c8;
}
/* Popup note */
.popup-note {
background: rgba(252,231,200,.1);
border: 1px solid rgba(252,231,200,.3);
padding: 10px;
margin-bottom: 15px;
color: #E3BB7A;
text-align: center;
}
/* Recipe count */
.recipe-count {
position: absolute;
right: 60px;
top: 50%;
transform: translateY(-50%);
color: #E3BB7A;
font-size: 14px;
}
/* Popup stats */
.popup-stats {
color: #E3BB7A;
font-size: 13px;
margin-left: 15px;
}
/* Popup controls */
.popup-controls {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 15px;
}
.popup-search-container {
flex: 1;
}
</style>`;
$('head').append(styles);
}
}
// Format tables to ensure proper styling
function formatTables() {
$('.recipe-table').each(function() {
var $table = $(this);
// Ensure proper header styling
$table.find('thead tr').addClass('tr-dark');
// Add wrapper if missing
if (!$table.parent().hasClass('dune-table-wrapper')) {
$table.wrap('<div class="dune-table-wrapper"></div>');
}
});
}
// Initialize table search functionality
function initTableSearch() {
// Create search inputs if they don't exist
$('.crafting-section').each(function() {
var $section = $(this);
var $table = $section.find('.recipe-table');
if ($table.length && !$section.find('.recipe-search-container').length) {
var searchHtml = '<div class="recipe-search-container">' +
'<input type="text" class="recipe-search-input" placeholder="Search recipes...">' +
'</div>';
$section.find('.dune-card-description').after(searchHtml);
}
});
// Bind search functionality
$(document).on('input', '.recipe-search-input', function() {
var $input = $(this);
var $table = $input.closest('.crafting-section').find('.recipe-table');
filterTable($table, $input.val());
});
}
// Filter table rows based on search input
function filterTable($table, searchTerm) {
var term = searchTerm.toLowerCase();
$table.find('tbody tr').each(function() {
var $row = $(this);
var text = $row.text().toLowerCase();
if (text.indexOf(term) > -1) {
$row.show();
} else {
$row.hide();
}
});
}
// FIXED: Initialize popup handlers with event delegation
function initPopupHandlers() {
console.log('[ResourcePage] Setting up popup handlers...');
// View All Crafted With - using event delegation
$(document).on('click', '.view-all-crafted-with', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('[ResourcePage] View All Crafted With clicked');
var $element = $(this);
var resourceName = $element.data('resource') || $element.attr('data-resource');
if (!resourceName) {
console.error('[ResourcePage] No resource name found!');
return false;
}
showRecipePopup('craftedWith', resourceName);
return false;
});
// View All Crafted From - using event delegation
$(document).on('click', '.view-all-crafted-from', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('[ResourcePage] View All Crafted From clicked');
var $element = $(this);
var resourceName = $element.data('resource') || $element.attr('data-resource');
if (!resourceName) {
console.error('[ResourcePage] No resource name found!');
return false;
}
showRecipePopup('craftedFrom', resourceName);
return false;
});
// Close popup - overlay click
$(document).on('click', '.recipe-popup-overlay', function(e) {
if (e.target === this) {
console.log('[ResourcePage] Overlay clicked - closing popup');
closeRecipePopup();
}
});
// Close popup - close button
$(document).on('click', '.popup-close', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('[ResourcePage] Close button clicked');
closeRecipePopup();
return false;
});
// Escape key to close popup
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && $('.recipe-popup-overlay').length > 0) {
console.log('[ResourcePage] Escape key pressed - closing popup');
closeRecipePopup();
}
});
// Prevent text selection on span buttons
$(document).on('selectstart', '.dune-action-button', function(e) {
e.preventDefault();
return false;
});
// Add hover logging for debugging
$(document).on('mouseenter', '.dune-action-button', function() {
console.log('[ResourcePage] Hovering over button:', $(this).text());
});
}
// Show recipe popup
function showRecipePopup(type, resourceName) {
console.log('[ResourcePage] Showing popup for:', type, resourceName);
var title = type === 'craftedWith'
? 'All Recipes Using ' + resourceName
: 'All Recipes Creating ' + resourceName;
// Show loading state
showLoadingPopup();
// First, check for full data in hidden elements
var dataSelector = '.' + type.replace('craftedWith', 'crafted-with').replace('craftedFrom', 'crafted-from') + '-full-data';
var $fullDataElement = $(dataSelector);
console.log('[ResourcePage] Looking for data in:', dataSelector);
if ($fullDataElement.length && $fullDataElement.text().trim()) {
try {
// Parse JSON data from hidden element
var jsonText = $fullDataElement.text().trim();
console.log('[ResourcePage] Found JSON data, parsing...');
var jsonData = JSON.parse(jsonText);
var rows = jsonData.results || jsonData || [];
console.log('[ResourcePage] Parsed', rows.length, 'rows');
// Transform to expected format
var formattedRows = rows.map(function(row) {
return {
item: formatItemName(row.OutputItem || row.output_item || ''),
resources: formatResourceList(row.Resources || row.resources || ''),
stations: formatStations(row.Stations || row.station || row.stations || ''),
water: formatWater(row.WaterML || row.water_ml),
craftTime: formatCraftTime(row.CraftTime || row.craft_time),
schematic: formatSchematic(row.Schematic || row.schematic)
};
});
displayPopup(title, formattedRows, false);
return;
} catch (e) {
console.error('[ResourcePage] Failed to parse full recipe data:', e);
}
}
// Fallback to API call
console.log('[ResourcePage] No cached data found, making API call...');
fetchRecipeDataViaAPI(type, resourceName, title);
}
// Format item name (handle wiki links)
function formatItemName(name) {
if (!name) return 'Unknown';
// Handle wiki links if present
if (name.indexOf('[[') > -1) {
return name.replace(/\[\[([^\|]+)\|?([^\]]*)\]\]/g, function(match, link, display) {
return '<a href="/wiki/' + link.replace(/ /g, '_') + '">' + (display || link) + '</a>';
});
}
return name;
}
// Format resource list for display
function formatResourceList(resources) {
if (!resources) return 'None';
// If it's already HTML formatted, return as is
if (resources.indexOf('<br') > -1) {
return resources;
}
// Otherwise, split by semicolons or newlines and format
return resources.split(/[;\n]/).map(function(r) {
return r.trim();
}).filter(Boolean).join('<br>');
}
// Format stations
function formatStations(stations) {
if (!stations) return 'Unknown';
// Handle multiple stations
return stations.split(/[;,]/).map(function(s) {
return s.trim();
}).filter(Boolean).join('<br>');
}
// Format water amount
function formatWater(water) {
if (!water || water === '0' || water === 'N/A') return 'N/A';
return parseInt(water).toLocaleString() + ' mL';
}
// Format craft time
function formatCraftTime(time) {
if (!time || time === '0' || time === 'N/A') return 'Instant';
var seconds = parseInt(time);
if (isNaN(seconds)) return time;
if (seconds < 60) return seconds + 's';
if (seconds < 3600) {
var mins = Math.floor(seconds / 60);
var secs = seconds % 60;
return mins + 'm ' + (secs > 0 ? secs + 's' : '');
}
var hours = Math.floor(seconds / 3600);
var mins = Math.floor((seconds % 3600) / 60);
return hours + 'h ' + (mins > 0 ? mins + 'm' : '');
}
// Format schematic
function formatSchematic(schematic) {
if (!schematic || schematic === 'N/A' || schematic === 'None') return 'None';
return formatItemName(schematic);
}
// Fetch recipe data via API
function fetchRecipeDataViaAPI(type, resourceName, title) {
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'parse',
format: 'json',
page: mw.config.get('wgPageName'),
prop: 'text',
disablelimitreport: true,
disableeditsection: true,
disabletoc: true
},
success: function(response) {
if (response.parse && response.parse.text) {
var html = response.parse.text['*'];
var $html = $('<div>').html(html);
// Try to find full data first
var dataSelector = '.' + type.replace('craftedWith', 'crafted-with').replace('craftedFrom', 'crafted-from') + '-full-data';
var $dataElement = $html.find(dataSelector);
if ($dataElement.length && $dataElement.text().trim()) {
try {
var jsonData = JSON.parse($dataElement.text().trim());
var rows = (jsonData.results || jsonData || []).map(function(row) {
return {
item: formatItemName(row.OutputItem || row.output_item || ''),
resources: formatResourceList(row.Resources || row.resources || ''),
stations: formatStations(row.Stations || row.station || row.stations || ''),
water: formatWater(row.WaterML || row.water_ml),
craftTime: formatCraftTime(row.CraftTime || row.craft_time),
schematic: formatSchematic(row.Schematic || row.schematic)
};
});
console.log('[ResourcePage] Found full data via API:', rows.length, 'rows');
displayPopup(title, rows, false);
return;
} catch (e) {
console.error('[ResourcePage] Failed to parse API JSON data:', e);
}
}
// Fallback to parsing table
var tableId = type === 'craftedWith' ? '#craftedWithTable' : '#craftedFromTable';
var $table = $html.find(tableId);
var rows = [];
$table.find('tbody tr').each(function() {
var $cells = $(this).find('td');
if ($cells.length >= 3) {
rows.push({
item: $cells.eq(0).html(),
resources: $cells.eq(1).html(),
stations: $cells.eq(2).html(),
water: 'N/A',
craftTime: 'N/A',
schematic: 'N/A'
});
}
});
console.log('[ResourcePage] Found', rows.length, 'rows via API table parsing');
displayPopup(title, rows, rows.length < 10); // Mark as limited if less than 10
}
},
error: function(xhr, status, error) {
console.error('[ResourcePage] API call failed:', error);
var rows = getVisibleRows(type);
displayPopup(title, rows, true);
}
});
}
// Get visible rows as fallback
function getVisibleRows(type) {
var $table = $(type === 'craftedWith' ? '#craftedWithTable' : '#craftedFromTable');
var rows = [];
$table.find('tbody tr').each(function() {
var $row = $(this);
var $cells = $row.find('td');
if ($cells.length >= 3) {
rows.push({
item: $cells.eq(0).html(),
resources: $cells.eq(1).html(),
stations: $cells.eq(2).html(),
water: 'N/A',
craftTime: 'N/A',
schematic: 'N/A'
});
}
});
console.log('[ResourcePage] Using visible rows:', rows.length);
return rows;
}
// Show loading popup
function showLoadingPopup() {
var loadingHtml = '<div class="recipe-popup-overlay active">' +
'<div class="recipe-popup">' +
'<div class="popup-content" style="text-align: center; padding: 60px;">' +
'<div style="font-size: 24px; color: #fce7c8; margin-bottom: 20px;">Loading recipes...</div>' +
'<div class="loading-spinner">⟳</div>' +
'</div>' +
'</div>' +
'</div>';
$('body').append(loadingHtml);
}
// Display popup with data
function displayPopup(title, rows, isLimited) {
// Remove any existing popup
$('.recipe-popup-overlay').remove();
// Build table rows HTML
var tableRowsHtml = '';
rows.forEach(function(row, index) {
tableRowsHtml += '<tr data-index="' + index + '">' +
'<td>' + row.item + '</td>' +
'<td>' + row.resources + '</td>' +
'<td>' + row.stations + '</td>' +
'<td>' + row.water + '</td>' +
'<td>' + row.craftTime + '</td>' +
'<td>' + row.schematic + '</td>' +
'</tr>';
});
// Note if data is limited
var limitedNote = isLimited ?
'<div class="popup-note">Note: This may not show all recipes. Reload the page to see full data.</div>' :
'';
// Build popup HTML
var popupHtml = '<div class="recipe-popup-overlay">' +
'<div class="recipe-popup">' +
'<div class="popup-header">' +
'<h3 class="popup-title">' + title + '</h3>' +
'<span class="recipe-count">' + rows.length + ' recipe' + (rows.length !== 1 ? 's' : '') + '</span>' +
'<button class="popup-close">×</button>' +
'</div>' +
'<div class="popup-content">' +
limitedNote +
'<div class="popup-controls">' +
'<div class="popup-search-container">' +
'<input type="text" class="popup-search-input" placeholder="Search recipes..." id="popupSearchInput">' +
'</div>' +
'<div class="popup-stats">' +
'<span class="visible-count">' + rows.length + '</span> of <span class="total-count">' + rows.length + '</span> shown' +
'</div>' +
'</div>' +
'<div class="popup-table-wrapper">' +
'<table class="infobox-dune-standard-table recipe-popup-table" id="popupRecipeTable">' +
'<thead>' +
'<tr class="tr-dark">' +
'<th data-sort="item" class="sortable">Item Created</th>' +
'<th data-sort="resources" class="sortable">Resources Needed</th>' +
'<th data-sort="stations" class="sortable">Stations</th>' +
'<th data-sort="water" class="sortable">Water (mL)</th>' +
'<th data-sort="time" class="sortable">Craft Time</th>' +
'<th data-sort="schematic" class="sortable">Schematic</th>' +
'</tr>' +
'</thead>' +
'<tbody>' + tableRowsHtml + '</tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
// Add to page
$('body').append(popupHtml);
// Activate popup
setTimeout(function() {
$('.recipe-popup-overlay').addClass('active');
}, 10);
// Initialize popup functionality
initPopupFunctionality();
}
// Initialize popup functionality (search and sort)
function initPopupFunctionality() {
var $searchInput = $('#popupSearchInput');
var $visibleCount = $('.visible-count');
var $totalCount = $('.total-count');
var totalRows = $('#popupRecipeTable tbody tr').length;
// Search functionality with counting
$searchInput.on('input', function() {
var searchTerm = $(this).val().toLowerCase();
var visibleRows = 0;
$('#popupRecipeTable tbody tr').each(function() {
var $row = $(this);
var text = $row.text().toLowerCase();
if (searchTerm === '' || text.indexOf(searchTerm) > -1) {
$row.show();
visibleRows++;
} else {
$row.hide();
}
});
$visibleCount.text(visibleRows);
});
// Sort functionality
$('#popupRecipeTable thead th.sortable').on('click', function() {
var $th = $(this);
var sortKey = $th.data('sort');
var $tbody = $('#popupRecipeTable tbody');
var rows = $tbody.find('tr').toArray();
// Determine sort direction
var ascending = !$th.hasClass('sort-desc');
// Remove sort classes from all headers
$('#popupRecipeTable thead th').removeClass('sort-asc sort-desc');
// Add appropriate class to clicked header
$th.addClass(ascending ? 'sort-asc' : 'sort-desc');
// Sort rows
rows.sort(function(a, b) {
var aText = $(a).find('td').eq($th.index()).text().trim();
var bText = $(b).find('td').eq($th.index()).text().trim();
// Special handling for numeric columns
if (sortKey === 'water') {
var aNum = parseInt(aText.replace(/[^\d]/g, '')) || 0;
var bNum = parseInt(bText.replace(/[^\d]/g, '')) || 0;
return ascending ? aNum - bNum : bNum - aNum;
}
if (sortKey === 'time') {
var aSeconds = parseTimeToSeconds(aText);
var bSeconds = parseTimeToSeconds(bText);
return ascending ? aSeconds - bSeconds : bSeconds - aSeconds;
}
// Sort as text
return ascending
? aText.localeCompare(bText)
: bText.localeCompare(aText);
});
// Re-append sorted rows
$tbody.empty();
rows.forEach(function(row) {
$tbody.append(row);
});
});
// Focus search input
$searchInput.focus();
}
// Parse time string to seconds for sorting
function parseTimeToSeconds(timeStr) {
if (!timeStr || timeStr === 'Instant' || timeStr === 'N/A') return 0;
var seconds = 0;
var hourMatch = timeStr.match(/(\d+)h/);
var minMatch = timeStr.match(/(\d+)m/);
var secMatch = timeStr.match(/(\d+)s/);
if (hourMatch) seconds += parseInt(hourMatch[1]) * 3600;
if (minMatch) seconds += parseInt(minMatch[1]) * 60;
if (secMatch) seconds += parseInt(secMatch[1]);
return seconds;
}
// Close recipe popup
function closeRecipePopup() {
$('.recipe-popup-overlay').removeClass('active');
setTimeout(function() {
$('.recipe-popup-overlay').remove();
}, 300);
}
// Add required CSS for animations
function addAnimationStyles() {
if ($('#resourcePageAnimations').length === 0) {
var styles = `
<style id="resourcePageAnimations">
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.recipe-popup-table th.sort-asc::after {
content: ' ↑';
color: #fce7c8;
}
.recipe-popup-table th.sort-desc::after {
content: ' ↓';
color: #fce7c8;
}
.recipe-popup-table th.sortable:hover {
background: rgba(252,231,200,.1) !important;
cursor: pointer;
}
/* Gallery styles */
.gallery-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.gallery-item {
flex: 0 0 120px;
text-align: center;
}
.gallery-item img {
max-width: 100%;
height: auto;
border: 2px solid rgba(252,231,200,.3);
transition: all 0.3s ease;
}
.gallery-item:hover img {
border-color: #fce7c8;
transform: scale(1.05);
box-shadow: 0 0 15px rgba(252,231,200,.5);
}
.gallery-item .caption {
font-size: 12px;
color: #E3BB7A;
margin-top: 4px;
}
</style>
`;
$('head').append(styles);
}
}
// Initialize when DOM is ready
$(document).ready(function() {
init();
});
})(jQuery, mediaWiki);