MediaWiki:Gadget-JourneySystem.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.
/* Journey System – ResourceLoader gadget
* Dependencies: mediawiki.util, jquery
* Last copied from MediaWiki:Common.js on 2025-06-02
*/
( function ( mw, $ ) {
'use strict';
// ── Abort if the page has no journey markup ────────────────────────────────
if ( !$( '.journey-main-container' ).length ) {
return;
}
// ── BEGIN: original common.js content (unchanged) ──────────────────────────
// ▼▼▼ Delete only the first line “(function($, mw){ …” and the very last
// line “})(jQuery, mediaWiki);” from your old file, then paste the rest
// right here. Everything else stays exactly the same.
// ……………………………………………………………………………………………………………………………
/**
* Journey System JavaScript for MediaWiki
* Enhanced with proper task counting and initialization
*/
'use strict';
console.log('%c[JourneySystem] common.js loaded','color:teal;font-weight:bold');
// Only run on pages with journey system
if (!$('.journey-main-container').length) return;
window.DuneJourneySystem = {
// Store current active journey
currentActiveJourney: null,
journeyData: {},
// Initialize the system
init: function() {
var self = this;
console.log('[JourneySystem] init() called');
// Parse journey data from the page
self.parseJourneyData();
// Set up event listeners
self.setupEventListeners();
// Check for URL hash
self.checkUrlHash();
},
setInitialAccordionState: function() {
console.log('[JourneySystem] Setting initial accordion state');
// Add collapsed class to all objectives first
$('.objective-item').addClass('collapsed');
// Remove collapsed class from first objective only
$('.objective-item:first').removeClass('collapsed');
},
// Parse journey data embedded in the page
parseJourneyData: function() {
var self = this;
// Journey data is embedded as JSON in a script tag
var dataElement = $('#journey-data-json');
if (dataElement.length) {
try {
self.journeyData = JSON.parse(dataElement.text());
} catch (e) {
console.error('Failed to parse journey data:', e);
}
}
// Alternative: Use data from HTML attributes if JSON parsing fails
if (!self.journeyData || Object.keys(self.journeyData).length === 0) {
if (window.journeyDataFromHTML) {
self.journeyData = window.journeyDataFromHTML;
}
}
},
// Set up all event listeners
setupEventListeners: function() {
var self = this;
// Journey card clicks
$(document).on('click', '.journey-card', function(e) {
e.preventDefault();
var journeyId = $(this).data('journey-id');
self.loadJourneyDetails(journeyId);
});
// Objective accordion
$(document).on('click', '.objective-header', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('[Toggle] Click detected');
var $objectiveItem = $(this).closest('.objective-item');
console.log('[Toggle] Current state:', $objectiveItem.hasClass('collapsed'));
// Just toggle this item - don't touch others!
$objectiveItem.toggleClass('collapsed');
console.log('[Toggle] New state:', $objectiveItem.hasClass('collapsed') ? 'closed' : 'open');
});
// Prevent clicks on progress from toggling accordion
$(document).on('click', '.objective-progress', function(e) {
e.stopPropagation();
});
// Task checkboxes
$(document).on('click', '.task-checkbox', function(e) {
e.stopPropagation();
$(this).toggleClass('completed');
self.updateProgress();
});
// Material checkboxes
$(document).on('click', '.material-item', function(e) {
var checkbox = $(this).find('.material-checkbox');
checkbox.toggleClass('completed');
$(this).toggleClass('checked');
self.updateMaterialProgress();
});
// Description toggle
$(document).on('click', '.description-toggle', function(e) {
e.preventDefault();
var desc = $(this).closest('.journey-description');
if (desc.hasClass('collapsed')) {
desc.removeClass('collapsed').addClass('expanded');
$(this).text('Less');
} else {
desc.addClass('collapsed').removeClass('expanded');
$(this).text('More');
}
});
// Action buttons –– use delegation so it still works after re-render
$(document).on('click', '.journey-actions .view-items', function (e) {
e.preventDefault();
e.stopPropagation();
console.log('[JourneySystem] View Items button clicked');
self.showItemPrepList(e);
return false;
});
$(document).on('click', '.journey-actions .view-guide', function (e) {
e.preventDefault();
e.stopPropagation();
console.log('[JourneySystem] View Guide button clicked');
self.showJourneyGuide(e);
return false;
});
// Popup close
$(document).on('click', '.popup-close, .journey-popup-overlay', function(e) {
if (e.target === this || $(e.target).hasClass('popup-close')) {
$('.journey-popup-overlay').removeClass('active');
setTimeout(function() {
$('.journey-popup-overlay').remove();
}, 300);
}
});
},
// Journey objective subtotals
fixTaskObjectiveIds: function() {
console.log('[JourneySystem] Fixing task objective IDs...');
$('.objective-item').each(function() {
var $objective = $(this);
var objectiveId = $objective.find('.objective-header').attr('data-objective-id');
// Set the objective ID on all tasks within this objective
$objective.find('.task-item').attr('data-objective-id', objectiveId);
console.log('Fixed objective', objectiveId, 'with', $objective.find('.task-item').length, 'tasks');
});
},
// Initialize task counts when journey loads
initializeTaskCounts: function() {
var self = this;
// Count tasks for each objective on initial load
$('.objective-item').each(function() {
var objective = $(this);
var totalTasks = objective.find('.task-item').length;
var progressEl = objective.find('.objective-progress');
// Set initial count (0 completed out of total)
progressEl.text('0/' + totalTasks);
});
},
// Load journey details via AJAX
loadJourneyDetails: function(journeyId) {
var self = this;
// Update active card styling
$('.journey-card').removeClass('active');
$('.journey-card[data-journey-id="' + journeyId + '"]').addClass('active');
// Show loading state
var panel = $('#journeyDetailsPanel');
panel.addClass('active loading');
// Make AJAX request to get journey details
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'parse',
format: 'json',
text: '{{JourneyDetails|id=' + journeyId + '}}',
contentmodel: 'wikitext'
},
success: function(response) {
if (response.parse && response.parse.text) {
var content = response.parse.text['*'];
panel.html(content);
panel.removeClass('loading');
console.log('[JourneySystem] AJAX success – journey', journeyId, 'loaded');
// Store current journey data
self.currentActiveJourney = self.findJourneyById(journeyId);
console.log('[JourneySystem] Current active journey set to:', self.currentActiveJourney);
// Give DOM time to settle before counting
setTimeout(function() {
$('[class*="objective-item"]').each(function() {
var currentClass = $(this).attr('class');
if (currentClass.includes('objective-itemcollapsed')) {
$(this).attr('class', 'objective-item collapsed');
}
});
// Initialize task counts
self.initializeTaskCounts();
self.updateProgress();
// Check for long descriptions
self.checkDescriptionLength();
// Set initial accordion state (first open, others closed)
self.setInitialAccordionState();
}, 150);
// Update URL hash using slug
// Use clean slug from data attribute
var card = $('.journey-card[data-journey-id="' + journeyId + '"]');
var slug = card.attr('data-slug');
if (slug) {
window.location.hash = slug;
console.log('[JourneySystem] Updated URL to slug:', slug);
} else if (journeyId) {
window.location.hash = 'journey-' + journeyId;
console.log('[JourneySystem] No slug found, using ID:', journeyId);
}
}
},
error: function() {
panel.removeClass('loading');
panel.html('<div class="error">Failed to load journey details</div>');
}
});
},
// Find journey by ID in data
findJourneyById: function(journeyId) {
for (var group in this.journeyData) {
var journeys = this.journeyData[group];
for (var i = 0; i < journeys.length; i++) {
if (journeys[i].id == journeyId) {
return journeys[i];
}
}
}
return null;
},
// Check and add toggle to long descriptions
checkDescriptionLength: function() {
$('.journey-description').each(function() {
var desc = $(this);
var text = desc.text();
if (text.length > 100 && !desc.find('.description-toggle').length) {
desc.addClass('collapsed');
desc.append('<button class="description-toggle">More</button>');
}
});
},
// Update progress counters with proper task counting
updateProgress: function() {
console.log('[JourneySystem] updateProgress() called');
// Small delay to ensure DOM is ready
setTimeout(function() {
// Per-objective counts (using attr instead of data for fresh elements)
$('.objective-progress').each(function() {
var $prog = $(this);
var objId = $prog.attr('data-objective-id');
// Find all tasks with this objective ID
var $tasks = $('.task-item[data-objective-id="' + objId + '"]');
var totalTasks = $tasks.length;
var completedTasks = $tasks.find('.task-checkbox.completed').length;
console.log(
'[JourneySystem] Objective', objId,
'→', completedTasks + '/' + totalTasks,
'Found tasks:', $tasks.length
);
// Update the UI
$prog.text(completedTasks + '/' + totalTasks);
$prog.toggleClass('completed', completedTasks === totalTasks && totalTasks > 0);
});
// Overall completion
var total = $('.task-item').length;
var done = $('.task-checkbox.completed').length;
var percent = total ? Math.round((done / total) * 100) : 0;
console.log('[JourneySystem] Overall →', done + '/' + total, percent + '%');
$('.completion-status').text(percent + '% COMPLETE');
// Save progress if user is logged in
if (mw.config.get('wgUserName')) {
this.saveProgress();
}
}.bind(this), 100); // 100ms delay to ensure DOM is ready
},
// UPDATED showItemPrepList with kebab-case fix
showItemPrepList: function(e) {
console.log('[JourneySystem] showItemPrepList called');
if (e) {
e.preventDefault();
e.stopPropagation();
}
var self = this;
var journeyId = null;
var journeyTitle = '';
// IMPORTANT: Remove ALL existing popups first
$('.journey-popup, .journey-popup-overlay').remove();
console.log('Removed existing popups');
// Get journey info
var $button = $(e ? e.currentTarget : '.journey-actions .view-items').first();
var $card = $button.closest('.journey-card');
// Try to find from active card (with kebab-case - this is the fix!)
var $activeCard = $('.journey-card.active');
if ($activeCard.length > 0) {
journeyId = $activeCard.data('journey-id') || $activeCard.attr('data-journey-id');
journeyTitle = $activeCard.find('.journey-title').text();
console.log('Found from active card:', journeyId, journeyTitle);
}
// From URL hash if not found
if (!journeyId && window.location.hash) {
var hash = window.location.hash.replace('#', '');
// Check if it's numeric
if (/^\d+$/.test(hash)) {
journeyId = hash;
} else {
// Find by slug
var card = $('.journey-card[data-slug="' + hash + '"]');
if (card.length > 0) {
journeyId = card.data('journey-id');
journeyTitle = card.find('.journey-name').text();
}
}
}
if (!journeyId) {
console.error('No journey ID found!');
alert('Please click on a journey card first to select it.');
return false;
}
journeyTitle = journeyTitle || 'Journey ' + journeyId;
console.log('Creating popup for journey:', journeyId);
// Create new popup with unique IDs to avoid conflicts
var timestamp = Date.now();
var overlayId = 'popup-overlay-' + timestamp;
var popupId = 'popup-' + timestamp;
var $overlay = $('<div>')
.attr('id', overlayId)
.addClass('journey-popup-overlay')
.css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0, 0, 0, 0)',
zIndex: 9999,
display: 'block'
});
var $popup = $('<div>')
.attr('id', popupId)
.addClass('journey-popup items-popup')
.css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(0.9)',
background: '#2a2a2a',
border: '2px solid #fce7c8',
padding: '20px',
zIndex: 10000,
minWidth: '500px',
maxWidth: '80%',
maxHeight: '80%',
overflowY: 'auto',
opacity: 0,
borderRadius: '5px'
})
.html(
'<div class="popup-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">' +
'<h3 class="popup-title" style="color: #fce7c8; margin: 0;">Item Prep List - ' + journeyTitle + '</h3>' +
'<button class="popup-close" style="background: none; border: none; color: #fce7c8; font-size: 28px; cursor: pointer; padding: 0;">×</button>' +
'</div>' +
'<div class="popup-content"><div class="loading" style="text-align: center; padding: 40px; color: #999;">Loading materials...</div></div>'
);
// Add to body
$('body').append($overlay);
$overlay.append($popup);
// Animate in
setTimeout(function() {
$overlay.css('background', 'rgba(0, 0, 0, 0.8)');
$popup.css({
transform: 'translate(-50%, -50%) scale(1)',
opacity: 1
});
}, 10);
// Close handlers
var closePopup = function() {
$overlay.css('background', 'rgba(0, 0, 0, 0)');
$popup.css({
transform: 'translate(-50%, -50%) scale(0.9)',
opacity: 0
});
setTimeout(function() {
$('#' + overlayId).remove();
}, 300);
};
$overlay.on('click', function(e) {
if (e.target === this) {
closePopup();
}
});
$popup.find('.popup-close').on('click', closePopup);
// Load materials
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'parse',
format: 'json',
text: '{{JourneyMaterials|id=' + journeyId + '}}',
contentmodel: 'wikitext'
},
success: function(response) {
if (response.parse && response.parse.text) {
var content = response.parse.text['*'];
$popup.find('.popup-content').html(content);
} else {
$popup.find('.popup-content').html(
'<div style="color: #ff6b6b; padding: 20px;">Unable to load materials.</div>'
);
}
},
error: function(xhr, status, error) {
$popup.find('.popup-content').html(
'<div style="color: #ff6b6b; padding: 20px;">' +
'<p><strong>Error loading materials</strong></p>' +
'<p>' + error + '</p>' +
'</div>'
);
}
});
return false;
},
// Add this helper function after showItemPrepList
getExampleMaterialsDisplay: function() {
return `
<div class="materials-container">
<div class="materials-tier level-1">
<div class="tier-header">Direct Materials Required</div>
<div class="material-item">
<span class="material-name">Duraluminum Ingot</span>
<span class="material-qty">1</span>
</div>
<div class="material-item">
<span class="material-name">Agave Seeds</span>
<span class="material-qty">10</span>
</div>
<div class="material-item">
<span class="material-name">Water</span>
<span class="material-qty">170</span>
</div>
</div>
<div class="materials-tier level-2">
<div class="tier-header">Components for Level 1 Materials</div>
<div class="material-item">
<span class="material-name">Aluminum Ingot</span>
<span class="material-qty">2</span>
</div>
<div class="material-item">
<span class="material-name">Jasmium Crystal</span>
<span class="material-qty">7</span>
</div>
<div class="material-item">
<span class="material-name">Water</span>
<span class="material-qty">1300</span>
</div>
</div>
<div class="materials-tier level-3">
<div class="tier-header">Base Resources Needed</div>
<div class="material-item">
<span class="material-name">Aluminum Ore</span>
<span class="material-qty">22</span>
</div>
<div class="material-item">
<span class="material-name">Water</span>
<span class="material-qty">800</span>
</div>
</div>
<div class="materials-summary">
<p>Note: This is example data. The database view 'vw_recipe_inputs_by_level' needs to be configured.</p>
</div>
</div>
`;
},
// NEW showJourneyGuide function (replaces showVideoGuide)
showJourneyGuide: function(e) {
console.log('[JourneySystem] showJourneyGuide called');
if (e) {
e.preventDefault();
e.stopPropagation();
}
var self = this;
var journeyId = null;
var journeyTitle = '';
// Remove existing popups
$('.journey-popup, .journey-popup-overlay').remove();
// Use the same logic to find journey ID
var $button = $(e ? e.currentTarget : '.journey-actions .view-guide').first();
// Try to find from active card (with kebab-case)
var $activeCard = $('.journey-card.active');
if ($activeCard.length > 0) {
journeyId = $activeCard.data('journey-id') || $activeCard.attr('data-journey-id');
journeyTitle = $activeCard.find('.journey-title').text();
console.log('Found from active card:', journeyId, journeyTitle);
}
// From URL hash
if (!journeyId && window.location.hash) {
var hash = window.location.hash.replace('#', '');
// Check if it's numeric
if (/^\d+$/.test(hash)) {
journeyId = hash;
} else {
// Find by slug
var card = $('.journey-card[data-slug="' + hash + '"]');
if (card.length > 0) {
journeyId = card.data('journey-id');
journeyTitle = card.find('.journey-name').text();
}
}
}
if (!journeyId) {
console.error('Could not find journey ID!');
alert('Please click on a journey card first to select it.');
return false;
}
journeyTitle = journeyTitle || 'Journey ' + journeyId;
console.log('Creating guide popup for journey:', journeyId, journeyTitle);
// Create popup (same structure as items popup)
var timestamp = Date.now();
var overlayId = 'popup-overlay-' + timestamp;
var popupId = 'popup-' + timestamp;
var $overlay = $('<div>')
.attr('id', overlayId)
.addClass('journey-popup-overlay')
.css({
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0, 0, 0, 0)',
zIndex: 9999,
display: 'block'
});
var $popup = $('<div>')
.attr('id', popupId)
.addClass('journey-popup guide-popup')
.css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) scale(0.9)',
background: '#2a2a2a',
border: '2px solid #fce7c8',
padding: '20px',
zIndex: 10000,
minWidth: '600px',
maxWidth: '90%',
maxHeight: '90%',
overflowY: 'auto',
opacity: 0,
borderRadius: '5px',
boxShadow: '0 4px 20px rgba(0,0,0,0.5)'
})
.html(
'<div class="popup-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid rgba(252,231,200,0.2);">' +
'<h3 class="popup-title" style="color: #fce7c8; margin: 0; font-size: 1.4em;">Journey Guide - ' + journeyTitle + '</h3>' +
'<button class="popup-close" style="background: none; border: none; color: #fce7c8; font-size: 28px; cursor: pointer; padding: 0; line-height: 1;">×</button>' +
'</div>' +
'<div class="popup-content"><div class="loading" style="text-align: center; padding: 40px; color: #999;">Loading guide...</div></div>'
);
$('body').append($overlay);
$overlay.append($popup);
setTimeout(function() {
$overlay.css('background', 'rgba(0, 0, 0, 0.8)');
$popup.css({
transform: 'translate(-50%, -50%) scale(1)',
opacity: 1
});
}, 10);
var closePopup = function() {
$overlay.css('background', 'rgba(0, 0, 0, 0)');
$popup.css({
transform: 'translate(-50%, -50%) scale(0.9)',
opacity: 0
});
setTimeout(function() {
$('#' + overlayId).remove();
}, 300);
};
$overlay.on('click', function(e) {
if (e.target === this) {
closePopup();
}
});
$popup.find('.popup-close').on('click', closePopup);
// Load guide
$.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'parse',
format: 'json',
text: '{{JourneyGuide|id=' + journeyId + '}}',
contentmodel: 'wikitext'
},
success: function(response) {
if (response.parse && response.parse.text) {
var content = response.parse.text['*'];
$popup.find('.popup-content').html(content);
} else {
$popup.find('.popup-content').html(
'<div style="color: #ff6b6b; padding: 20px;">Guide not available for this journey.</div>'
);
}
},
error: function(xhr, status, error) {
$popup.find('.popup-content').html(
'<div style="color: #ff6b6b; padding: 20px;">' +
'<p><strong>Error loading guide</strong></p>' +
'<p>' + error + '</p>' +
'</div>'
);
}
});
return false;
},
// Create popup structure
createPopup: function(className, title) {
var popup = $('<div class="journey-popup-overlay">' +
'<div class="journey-popup ' + className + '">' +
'<div class="popup-header">' +
'<h3 class="popup-title">' + title + '</h3>' +
'<button class="popup-close">×</button>' +
'</div>' +
'<div class="popup-content"></div>' +
'</div></div>');
return popup;
},
// Show popup with animation
showPopup: function(popup) {
$('body').append(popup);
setTimeout(function() {
popup.addClass('active');
}, 10);
},
// Update material progress in popup
updateMaterialProgress: function () {
const total = $('.material-item').length; // every row
const done = $('.material-item.checked').length; // ticked rows
// numbers
$('.materials-progress-count').text(`${done}/${total}`);
// bar width
const pct = total ? (done / total) * 100 : 0;
$('.progress-bar').css('width', pct + '%');
// glow on 100 %
$('.materials-progress-header')
.toggleClass('completed', done === total && total > 0);
},
// Check URL hash on load
checkUrlHash: function() {
const hash = window.location.hash.replace('#', '');
if (!hash) return;
console.log('[JourneySystem] Checking URL hash:', hash);
// First check if it's a numeric ID (with or without prefix)
var journeyMatch = hash.match(/^journey-(\d+)$/);
if (journeyMatch) {
this.loadJourneyDetails(journeyMatch[1]);
return;
}
// Check if it's just a number
if (/^\d+$/.test(hash)) {
this.loadJourneyDetails(hash);
return;
}
// Otherwise, treat as slug - find journey card with matching data-slug
var card = $('.journey-card[data-slug="' + hash + '"]');
if (card.length > 0) {
var journeyId = card.data('journey-id');
console.log('[JourneySystem] Found journey by slug:', hash, 'ID:', journeyId);
this.loadJourneyDetails(journeyId);
} else {
console.log('[JourneySystem] No journey found for slug:', hash);
}
},
// Placeholder for save progress functionality
saveProgress: function() {
// This would save to user preferences or a custom API
console.log('[JourneySystem] Progress save called (not implemented)');
}
};
// Initialize when DOM is ready
$(function() {
console.log('[JourneySystem] DOM ready – calling init()');
DuneJourneySystem.init();
});
/**
* Enhanced Journey Materials System
* Provides hierarchical material tracking with drill-up functionality
*/
window.EnhancedMaterialsSystem = {
// Material relationships mapping (would be populated from database)
materialRelationships: {},
// Initialize the enhanced materials system
init: function() {
console.log('[EnhancedMaterials] Initializing...');
// Set up event listeners
this.setupEventListeners();
// Initialize counts
this.updateProgressBar();
// Map material relationships (this would come from your database)
this.mapMaterialRelationships();
},
// Set up all event listeners
setupEventListeners: function() {
var self = this;
// Material checkbox changes
$(document).on('change', '.material-checkbox', function() {
var $checkbox = $(this);
var $card = $checkbox.closest('.material-card');
var itemId = $card.data('item');
var level = parseInt($card.data('level'));
// Toggle checked state
$card.toggleClass('checked', $checkbox.is(':checked'));
// Handle drill-up functionality
if ($checkbox.is(':checked') && level > 1) {
self.checkParentMaterials(itemId, level);
}
// Update progress
self.updateProgressBar();
});
// Card click to toggle checkbox
$(document).on('click', '.material-card', function () {
const $card = $(this);
const $box = $card.find('.material-checkbox');
$box.toggleClass('completed'); // visual tick
$card.toggleClass('checked'); // dim / strike-through
EnhancedMaterialsSystem.updateProgressBar();
});
// Check/Uncheck all buttons
$(document).on('click', '.check-all-btn', function() {
$('.material-checkbox').prop('checked', true).trigger('change');
});
$(document).on('click', '.uncheck-all-btn', function() {
$('.material-checkbox').prop('checked', false).trigger('change');
});
// Toggle view button
$(document).on('click', '.toggle-view-btn', function() {
var $btn = $(this);
var $container = $('.materials-enhanced-container');
var currentView = $btn.data('view');
if (currentView === 'grid') {
$container.addClass('list-view');
$btn.data('view', 'list');
$btn.html('<span class="icon">⊞</span> Grid View');
} else {
$container.removeClass('list-view');
$btn.data('view', 'grid');
$btn.html('<span class="icon">📋</span> List View');
}
});
},
// Map material relationships from the data
mapMaterialRelationships: function() {
var self = this;
// Use the actual data loaded from the database
if (window.journeyMaterialRelationships) {
// Convert the flat structure to parent->children mapping
self.materialRelationships = {};
for (var child in window.journeyMaterialRelationships) {
var parent = window.journeyMaterialRelationships[child].parent;
if (!self.materialRelationships[child]) {
self.materialRelationships[child] = [];
}
if (parent && !self.materialRelationships[child].includes(parent)) {
self.materialRelationships[child].push(parent);
}
}
}
// Mark cards that have children
this.markCardsWithChildren();
},
// Mark cards that have child materials
markCardsWithChildren: function() {
var self = this;
$('.material-card').each(function() {
var $card = $(this);
var itemId = $card.data('item');
// Check if this material is used in other recipes
var hasChildren = false;
for (var child in self.materialRelationships) {
if (self.materialRelationships[child].includes(itemId)) {
hasChildren = true;
break;
}
}
if (hasChildren) {
$card.addClass('has-children');
}
});
},
// Check parent materials when a child is checked
checkParentMaterials: function(childItemId, childLevel) {
var self = this;
// Find all materials that use this child material
var parents = this.materialRelationships[childItemId] || [];
parents.forEach(function(parentId) {
var $parentCard = $('.material-card[data-item="' + parentId + '"]');
if ($parentCard.length) {
// Check if all components for this parent are checked
if (self.areAllComponentsChecked(parentId)) {
var $parentCheckbox = $parentCard.find('.material-checkbox');
if (!$parentCheckbox.is(':checked')) {
$parentCheckbox.prop('checked', true);
$parentCard.addClass('checked has-parent-checked');
// Recursively check parents of this parent
var parentLevel = parseInt($parentCard.data('level'));
if (parentLevel > 1) {
self.checkParentMaterials(parentId, parentLevel);
}
}
}
}
});
},
// Check if all components for a material are checked
areAllComponentsChecked: function(materialId) {
var requiredComponents = this.getRequiredComponents(materialId);
for (var i = 0; i < requiredComponents.length; i++) {
var component = requiredComponents[i];
var $componentCard = $('.material-card[data-item="' + component.child + '"]');
if ($componentCard.length && !$componentCard.find('.material-checkbox').is(':checked')) {
return false;
}
}
return true;
},
// Get required components for a material (from actual database data)
getRequiredComponents: function(materialId) {
// Use the data loaded from the database
if (window.materialRequirements && window.materialRequirements[materialId]) {
return window.materialRequirements[materialId];
}
return [];
},
// Update the progress bar
updateProgressBar: function () {
const total = $('.material-card').length;
const checked = $('.material-card.checked').length;
const pct = total ? (checked / total) * 100 : 0;
$('.progress-bar').css('width', pct + '%');
$('.progress-count').text(checked);
$('.total-count').text(total);
$('.materials-progress-header').toggleClass('completed', pct === 100);
},
// Visual connection lines between related materials (optional)
drawConnections: function() {
// This could draw SVG lines between connected materials
// for a more visual representation of the hierarchy
}
};
// Patch: make span-based checkboxes work
window.EnhancedMaterialsSystem.setupEventListeners = function () {
const self = this;
// 1 ▸ Toggle by clicking the card OR the faux checkbox
$(document).on('click', '.material-card, .material-checkbox', function (e) {
// get the card and its checkbox span
const $card = $(e.target).closest('.material-card');
const $box = $card.find('.material-checkbox');
// toggle visual state
const newState = !$box.hasClass('completed');
$box.toggleClass('completed', newState);
$card.toggleClass('checked', newState);
// drill-up logic (only when turning ON)
if (newState && $card.data('level') > 1) {
self.checkParentMaterials($card.data('item'), $card.data('level'));
}
self.updateProgressBar();
});
// 2 ▸ Bulk buttons (optional – remove if you don't need them)
$(document).on('click', '.check-all-btn', () => $('.material-checkbox').addClass('completed').trigger('click'));
$(document).on('click', '.uncheck-all-btn',() => $('.material-checkbox').removeClass('completed').trigger('click'));
};
// 3 ▸ Progress bar now counts .completed spans, not :checked inputs
window.EnhancedMaterialsSystem.updateProgressBar = function () {
const total = $('.material-item').length;
const ready = $('.material-item.checked').length;
const pct = total ? (ready / total) * 100 : 0;
$('.progress-bar').css('width', pct + '%');
$('.progress-count').text(ready);
$('.total-count').text(total);
$('.materials-progress-header').toggleClass('completed', pct === 100);
};
// Hook into the existing journey system
$(document).ready(function() {
// Initialize when materials popup is opened
$(document).on('DOMNodeInserted', '.materials-enhanced-container', function() {
setTimeout(function() {
EnhancedMaterialsSystem.init();
}, 100);
});
});
/**
* Journey Card Background Image Handler
* Extracts MediaWiki file URLs and applies them as background images
*/
(function() {
'use strict';
/**
* Apply background images to journey cards
*/
function applyJourneyBackgrounds() {
// Find all journey cards with data-icon attribute
const journeyCards = document.querySelectorAll('.journey-card[data-icon]');
journeyCards.forEach(function(card) {
const iconFile = card.getAttribute('data-icon');
// Skip if no icon file specified
if (!iconFile || iconFile === '' || iconFile === 'NULL' || iconFile === 'null') {
return;
}
// Check if it ends with .png or .jpg
if (!iconFile.match(/\.(png|jpg|jpeg)$/i)) {
return;
}
// Look for the hidden MediaWiki file element
const bgSource = card.querySelector('.journey-bg-source img');
if (bgSource) {
// Get the src URL from the img element
const imageUrl = bgSource.getAttribute('src');
if (imageUrl) {
// Apply as background image
card.style.backgroundImage = 'url(' + imageUrl + ')';
card.classList.add('has-background');
// Remove the has-icon class if it exists to avoid conflicts
card.classList.remove('has-icon');
}
} else {
// Fallback: construct URL from filename
// This handles cases where MediaWiki might not have rendered the file yet
const encodedFilename = encodeURIComponent(iconFile.replace(/ /g, '_'));
const fallbackUrl = '/images/' + encodedFilename;
// Check if the file exists by creating a test image
const testImg = new Image();
testImg.onload = function() {
card.style.backgroundImage = 'url(' + fallbackUrl + ')';
card.classList.add('has-background');
card.classList.remove('has-icon');
};
testImg.onerror = function() {
// If direct path fails, try the thumb path
const thumbUrl = '/images/thumb/' + encodedFilename + '/300px-' + encodedFilename;
card.style.backgroundImage = 'url(' + thumbUrl + ')';
card.classList.add('has-background');
card.classList.remove('has-icon');
};
testImg.src = fallbackUrl;
}
});
}
/**
* Initialize when DOM is ready
*/
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyJourneyBackgrounds);
} else {
// DOM is already loaded
applyJourneyBackgrounds();
}
// Also run after a short delay to catch any lazy-loaded images
setTimeout(applyJourneyBackgrounds, 1000);
// Make function available globally for debugging
window.applyJourneyBackgrounds = applyJourneyBackgrounds;
})();
// ……………………………………………………………………………………………………………………………
}( mediaWiki, jQuery ) );