695 lines
32 KiB
JavaScript
695 lines
32 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('Modules page initializing...');
|
||
|
||
// Reference to container and filters
|
||
const modulesContainer = document.getElementById('modules-container');
|
||
const moduleSearch = document.getElementById('module-search');
|
||
const stageFilter = document.getElementById('stage-filter');
|
||
const componentFilter = document.getElementById('component-filter');
|
||
const templateFilter = document.getElementById('template-filter');
|
||
|
||
// Data structures
|
||
let allModules = [];
|
||
let stageNames = {};
|
||
let componentNames = {};
|
||
let templateUsage = {};
|
||
let processedTemplates = [];
|
||
|
||
// Functions
|
||
function initializeData() {
|
||
console.log('Initializing data...');
|
||
|
||
// Check if data is available
|
||
if (typeof moduleData === 'undefined') {
|
||
console.error('Error: moduleData is undefined');
|
||
modulesContainer.innerHTML = '<div class="error">Error: Module data not available</div>';
|
||
return false;
|
||
}
|
||
|
||
if (typeof rawProtocolTemplates === 'undefined' || typeof templateMapper === 'undefined') {
|
||
console.error('Error: Template data or mapper not available');
|
||
modulesContainer.innerHTML = '<div class="error">Error: Template data not available</div>';
|
||
return false;
|
||
}
|
||
|
||
// Log what we're working with
|
||
console.log('Module categories:', Object.keys(moduleData));
|
||
console.log('Templates count:', rawProtocolTemplates.length);
|
||
|
||
try {
|
||
// Process templates
|
||
rawProtocolTemplates.forEach(template => {
|
||
const processedTemplate = templateMapper.convertTemplateToModules(template, moduleData);
|
||
processedTemplates.push(processedTemplate);
|
||
});
|
||
|
||
// Collect all modules into a flat array
|
||
for (const category in moduleData) {
|
||
moduleData[category].forEach(module => {
|
||
// Add category to module
|
||
module.category = category;
|
||
|
||
// Initialize template usage
|
||
module.usedInTemplates = [];
|
||
|
||
// Ensure all modules have a componentId - no uncategorized modules allowed
|
||
if (!module.componentId) {
|
||
// Try to infer from category first
|
||
if (category !== 'uncategorized') {
|
||
console.log(`Module ${module.id} missing componentId, using category: ${category}`);
|
||
module.componentId = category;
|
||
}
|
||
// If that doesn't work, infer from title or content
|
||
else {
|
||
// First, try to find clues in the title
|
||
if (module.title) {
|
||
const titleLower = module.title.toLowerCase();
|
||
// Check for key component names in the title
|
||
if (titleLower.includes('principle')) module.componentId = 'principles';
|
||
else if (titleLower.includes('process')) module.componentId = 'process';
|
||
else if (titleLower.includes('assessment')) module.componentId = 'assessment';
|
||
else if (titleLower.includes('intake')) module.componentId = 'intake';
|
||
else if (titleLower.includes('appeal')) module.componentId = 'appeal';
|
||
else if (titleLower.includes('deliberat')) module.componentId = 'deliberation';
|
||
else if (titleLower.includes('resolut')) module.componentId = 'resolution';
|
||
else if (titleLower.includes('facilit')) module.componentId = 'facilitation';
|
||
else if (titleLower.includes('particip')) module.componentId = 'participants';
|
||
else if (titleLower.includes('file') || titleLower.includes('submit')) module.componentId = 'filing';
|
||
else if (titleLower.includes('notif') || titleLower.includes('inform')) module.componentId = 'notification';
|
||
else if (titleLower.includes('delegat')) module.componentId = 'delegation';
|
||
else module.componentId = 'process'; // Default to process
|
||
}
|
||
// If no title clues, check content
|
||
else if (module.content) {
|
||
const contentLower = module.content.toLowerCase();
|
||
// Same checks in content
|
||
if (contentLower.includes('principle')) module.componentId = 'principles';
|
||
else if (contentLower.includes('process')) module.componentId = 'process';
|
||
else if (contentLower.includes('assessment')) module.componentId = 'assessment';
|
||
else if (contentLower.includes('intake')) module.componentId = 'intake';
|
||
else if (contentLower.includes('appeal')) module.componentId = 'appeal';
|
||
else if (contentLower.includes('deliberat')) module.componentId = 'deliberation';
|
||
else if (contentLower.includes('resolut')) module.componentId = 'resolution';
|
||
else if (contentLower.includes('facilit')) module.componentId = 'facilitation';
|
||
else if (contentLower.includes('particip')) module.componentId = 'participants';
|
||
else if (contentLower.includes('file') || contentLower.includes('submit')) module.componentId = 'filing';
|
||
else if (contentLower.includes('notif') || contentLower.includes('inform')) module.componentId = 'notification';
|
||
else if (contentLower.includes('delegat')) module.componentId = 'delegation';
|
||
else module.componentId = 'process'; // Default to process
|
||
}
|
||
// Last resort default
|
||
else {
|
||
module.componentId = 'process';
|
||
}
|
||
|
||
console.log(`Module ${module.id} had no componentId, assigned to: ${module.componentId}`);
|
||
}
|
||
}
|
||
|
||
// Add to all modules array
|
||
allModules.push(module);
|
||
});
|
||
}
|
||
|
||
console.log('Total modules collected:', allModules.length);
|
||
|
||
// Track which templates use each module
|
||
processedTemplates.forEach(template => {
|
||
for (const stageId in template.moduleRefs) {
|
||
if (!stageNames[stageId]) {
|
||
// Create a readable stage name
|
||
stageNames[stageId] = stageId.charAt(0).toUpperCase() + stageId.slice(1).replace(/_/g, ' ');
|
||
}
|
||
|
||
for (const componentId in template.moduleRefs[stageId]) {
|
||
if (!componentNames[componentId]) {
|
||
// Create a readable component name
|
||
componentNames[componentId] = componentId.charAt(0).toUpperCase() + componentId.slice(1).replace(/_/g, ' ');
|
||
}
|
||
|
||
for (const fieldId in template.moduleRefs[stageId][componentId]) {
|
||
const moduleId = template.moduleRefs[stageId][componentId][fieldId];
|
||
|
||
// Find the module with this id
|
||
const matchingModule = allModules.find(m => m.id === moduleId);
|
||
if (matchingModule) {
|
||
// Add template to module's usage
|
||
if (!matchingModule.usedInTemplates.includes(template.title)) {
|
||
matchingModule.usedInTemplates.push(template.title);
|
||
}
|
||
|
||
// Set stage and component for the module if not already set
|
||
if (!matchingModule.stageId) {
|
||
matchingModule.stageId = stageId;
|
||
}
|
||
|
||
// Track template usage
|
||
if (!templateUsage[template.title]) {
|
||
templateUsage[template.title] = [];
|
||
}
|
||
if (!templateUsage[template.title].includes(moduleId)) {
|
||
templateUsage[template.title].push(moduleId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Define official stages from YAML file
|
||
const officialStages = {
|
||
'intake': 'Intake',
|
||
'process': 'Process',
|
||
'assessment': 'Assessment',
|
||
'deliberation': 'Deliberation',
|
||
'resolution': 'Resolution',
|
||
'appeal': 'Appeal',
|
||
'delegation': 'Delegation'
|
||
};
|
||
|
||
// Define official components with standardized display names
|
||
const officialComponents = {
|
||
// Process stage components
|
||
'principles': 'Principles',
|
||
'community_values': 'Values',
|
||
'participants': 'Participants',
|
||
'facilitation': 'Facilitation',
|
||
'ground_rules': 'Ground Rules',
|
||
'skills': 'Skills',
|
||
|
||
// Intake stage components
|
||
'process_start': 'Process Start',
|
||
'filing': 'Filing',
|
||
'notification': 'Notification',
|
||
'rules_access': 'Rules Access',
|
||
'information_access': 'Information Access',
|
||
'participant_inclusion': 'Participant Inclusion',
|
||
|
||
// Assessment stage components
|
||
'dispute_assessment': 'Assessment',
|
||
'values_adherence': 'Values Adherence',
|
||
'jurisdiction': 'Jurisdiction',
|
||
|
||
// Deliberation stage components
|
||
'deliberation_process': 'Deliberation Process',
|
||
'additional_voices': 'Additional Voices',
|
||
'deliberation_conclusion': 'Deliberation Conclusion',
|
||
|
||
// Resolution stage components
|
||
'resolution_process': 'Resolution Process',
|
||
'resolution_failure': 'Resolution Failure',
|
||
|
||
// Appeal stage components
|
||
'appeal_criteria': 'Appeal Criteria',
|
||
'appeal_process': 'Appeal Process',
|
||
|
||
// Delegation stage components
|
||
'delegation_options': 'Delegation Options'
|
||
};
|
||
|
||
// Map all component IDs (including custom ones) to official stages
|
||
const componentToStageMap = {
|
||
// Process stage components
|
||
'principles': 'process',
|
||
'community_values': 'process',
|
||
'participants': 'process',
|
||
'facilitation': 'process',
|
||
'ground_rules': 'process',
|
||
'skills': 'process',
|
||
'values': 'process',
|
||
'agreements': 'process',
|
||
'participation_commitments': 'process',
|
||
|
||
// Intake stage components
|
||
'process_start': 'intake',
|
||
'filing': 'intake',
|
||
'notification': 'intake',
|
||
'rules_access': 'intake',
|
||
'information_access': 'intake',
|
||
'participant_inclusion': 'intake',
|
||
'reporting': 'intake',
|
||
|
||
// Assessment stage components
|
||
'dispute_assessment': 'assessment',
|
||
'values_adherence': 'assessment',
|
||
'jurisdiction': 'assessment',
|
||
'non_participation': 'assessment',
|
||
|
||
// Deliberation stage components
|
||
'deliberation_process': 'deliberation',
|
||
'additional_voices': 'deliberation',
|
||
'deliberation_conclusion': 'deliberation',
|
||
'decision_making': 'deliberation',
|
||
'discussion': 'deliberation',
|
||
|
||
// Resolution stage components
|
||
'resolution_process': 'resolution',
|
||
'resolution_failure': 'resolution',
|
||
|
||
// Appeal stage components
|
||
'appeal_criteria': 'appeal',
|
||
'appeal_process': 'appeal',
|
||
'appeal_deliberation': 'appeal',
|
||
'appeal_resolution': 'appeal',
|
||
|
||
// Delegation stage components
|
||
'delegation_options': 'delegation'
|
||
};
|
||
|
||
// Map non-standard components to official ones
|
||
const componentMapping = {
|
||
'direct_conversation': 'deliberation_process',
|
||
'conflict_awareness': 'dispute_assessment',
|
||
'accessing_help': 'process_start',
|
||
'preparation': 'principles',
|
||
'selection': 'participant_inclusion',
|
||
'criteria': 'appeal_criteria',
|
||
'agreement': 'resolution_process',
|
||
'process': 'principles',
|
||
'process_change': 'resolution_failure',
|
||
'assessment': 'dispute_assessment',
|
||
'situation': 'dispute_assessment',
|
||
'reflection': 'deliberation_conclusion',
|
||
'monitoring': 'dispute_assessment',
|
||
'participation_requirement': 'participant_inclusion'
|
||
};
|
||
|
||
allModules.forEach(module => {
|
||
// If module has no stageId, try to infer from category
|
||
if (!module.stageId && module.category) {
|
||
// Check if category matches a known stage
|
||
if (stageNames[module.category]) {
|
||
module.stageId = module.category;
|
||
} else {
|
||
// Use category as stageId if not already recognized
|
||
module.stageId = module.category;
|
||
// Create a readable stage name from category
|
||
stageNames[module.category] = module.category.charAt(0).toUpperCase() +
|
||
module.category.slice(1).replace(/_/g, ' ');
|
||
}
|
||
}
|
||
|
||
// If still no stageId, try to infer from componentId using the mapping
|
||
if (!module.stageId && module.componentId) {
|
||
const mappedStage = componentToStageMap[module.componentId];
|
||
if (mappedStage) {
|
||
module.stageId = mappedStage;
|
||
console.log(`Module ${module.id} missing stageId, inferred from component: ${mappedStage}`);
|
||
} else {
|
||
// Default stage if no mapping exists
|
||
module.stageId = 'process';
|
||
console.log(`Module ${module.id} missing stageId, defaulting to: process`);
|
||
}
|
||
}
|
||
|
||
// If STILL no stageId (somehow), assign to process
|
||
if (!module.stageId) {
|
||
module.stageId = 'process';
|
||
console.log(`Module ${module.id} had no stageId, assigned to: process`);
|
||
}
|
||
|
||
// Force module to use only official stages
|
||
if (officialStages[module.stageId]) {
|
||
// Use official stage name
|
||
module.stageName = officialStages[module.stageId];
|
||
} else {
|
||
// Map to closest official stage
|
||
const mappedStage = componentToStageMap[module.componentId];
|
||
if (mappedStage && officialStages[mappedStage]) {
|
||
module.stageId = mappedStage;
|
||
module.stageName = officialStages[mappedStage];
|
||
console.log(`Module ${module.id} had invalid stage "${module.stageId}", remapped to: ${mappedStage}`);
|
||
} else {
|
||
// Default to Process stage if can't map anywhere else
|
||
module.stageId = 'process';
|
||
module.stageName = officialStages['process'];
|
||
console.log(`Module ${module.id} had invalid stage "${module.stageId}", defaulting to: Process`);
|
||
}
|
||
}
|
||
|
||
// Handle component mapping for non-standard components
|
||
if (componentMapping[module.componentId]) {
|
||
const originalComponentId = module.componentId;
|
||
module.componentId = componentMapping[originalComponentId];
|
||
console.log(`Module ${module.id} had non-standard component "${originalComponentId}", mapped to: ${module.componentId}`);
|
||
}
|
||
|
||
// Set a readable component name using official list
|
||
if (officialComponents[module.componentId]) {
|
||
module.componentName = officialComponents[module.componentId];
|
||
} else {
|
||
// Generate a readable name for custom components
|
||
module.componentName = module.componentId.charAt(0).toUpperCase() +
|
||
module.componentId.slice(1).replace(/_/g, ' ');
|
||
|
||
// Log any component not in the official list
|
||
console.log(`Module ${module.id} uses custom component: ${module.componentId}`);
|
||
}
|
||
});
|
||
|
||
// Log the distribution of modules by stage
|
||
const stageDistribution = {};
|
||
allModules.forEach(module => {
|
||
stageDistribution[module.stageName] = (stageDistribution[module.stageName] || 0) + 1;
|
||
});
|
||
console.log('Module distribution by stage:', stageDistribution);
|
||
|
||
console.log('Data initialization complete');
|
||
console.log('Stages:', Object.keys(stageNames));
|
||
console.log('Components:', Object.keys(componentNames));
|
||
console.log('Templates:', Object.keys(templateUsage));
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Error initializing data:', error);
|
||
modulesContainer.innerHTML = `<div class="error">Error initializing data: ${error.message}</div>`;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function populateFilters() {
|
||
console.log('Populating filters...');
|
||
|
||
// Clear existing options
|
||
stageFilter.innerHTML = '<option value="">All Stages</option>';
|
||
componentFilter.innerHTML = '<option value="">All Components</option>';
|
||
templateFilter.innerHTML = '<option value="">All Templates</option>';
|
||
|
||
// Get unique stages and components
|
||
const stages = [...new Set(allModules.map(m => m.stageName))].sort();
|
||
const components = [...new Set(allModules.map(m => m.componentName))].sort();
|
||
const templates = Object.keys(templateUsage).sort();
|
||
|
||
console.log('Filter options - Stages:', stages);
|
||
console.log('Filter options - Components:', components);
|
||
console.log('Filter options - Templates:', templates);
|
||
|
||
// Add options to filters
|
||
stages.forEach(stage => {
|
||
const option = document.createElement('option');
|
||
option.value = stage;
|
||
option.textContent = stage;
|
||
stageFilter.appendChild(option);
|
||
});
|
||
|
||
components.forEach(component => {
|
||
const option = document.createElement('option');
|
||
option.value = component;
|
||
option.textContent = component;
|
||
componentFilter.appendChild(option);
|
||
});
|
||
|
||
templates.forEach(template => {
|
||
const option = document.createElement('option');
|
||
option.value = template;
|
||
option.textContent = template;
|
||
templateFilter.appendChild(option);
|
||
});
|
||
}
|
||
|
||
function renderModules() {
|
||
console.log('Rendering modules...');
|
||
|
||
// Clear container
|
||
modulesContainer.innerHTML = '';
|
||
|
||
// Get filter values
|
||
const searchText = moduleSearch.value.toLowerCase();
|
||
const stageValue = stageFilter.value;
|
||
const componentValue = componentFilter.value;
|
||
const templateValue = templateFilter.value;
|
||
|
||
console.log('Filter values:', {
|
||
search: searchText,
|
||
stage: stageValue,
|
||
component: componentValue,
|
||
template: templateValue
|
||
});
|
||
|
||
// Create a stage-based organization of modules
|
||
const modulesByStage = {};
|
||
|
||
// Filter modules based on search and filter criteria
|
||
allModules.forEach(module => {
|
||
// Check if module matches search text
|
||
const matchesSearch = searchText === '' ||
|
||
module.title.toLowerCase().includes(searchText) ||
|
||
(module.content && module.content.toLowerCase().includes(searchText));
|
||
|
||
// Check if module matches stage filter
|
||
const matchesStage = stageValue === '' || module.stageName === stageValue;
|
||
|
||
// Check if module matches component filter
|
||
const matchesComponent = componentValue === '' || module.componentName === componentValue;
|
||
|
||
// Check if module matches template filter
|
||
const matchesTemplate = templateValue === '' ||
|
||
module.usedInTemplates.includes(templateValue);
|
||
|
||
// Include module if it matches all criteria
|
||
if (matchesSearch && matchesStage && matchesComponent && matchesTemplate) {
|
||
// Initialize stage object if not exists
|
||
if (!modulesByStage[module.stageName]) {
|
||
modulesByStage[module.stageName] = [];
|
||
}
|
||
|
||
// Add module to its stage
|
||
modulesByStage[module.stageName].push(module);
|
||
}
|
||
});
|
||
|
||
// Sort stages according to the official order from the YAML file
|
||
const stageOrder = [
|
||
'Intake', 'Process', 'Assessment',
|
||
'Deliberation', 'Resolution', 'Appeal', 'Delegation'
|
||
];
|
||
|
||
// Get all stages from the data
|
||
const availableStages = Object.keys(modulesByStage);
|
||
|
||
// Sort stages according to the defined order, with any others at the end
|
||
const sortedStages = [];
|
||
|
||
// First add stages in the predefined order (if they exist in the data)
|
||
stageOrder.forEach(stage => {
|
||
if (availableStages.includes(stage)) {
|
||
sortedStages.push(stage);
|
||
}
|
||
});
|
||
|
||
// Then add any other stages not in the predefined order (sorted alphabetically)
|
||
availableStages
|
||
.filter(stage => !stageOrder.includes(stage))
|
||
.sort()
|
||
.forEach(stage => sortedStages.push(stage));
|
||
|
||
// If no modules match, show message
|
||
if (sortedStages.length === 0) {
|
||
modulesContainer.innerHTML = '<div class="no-results">No modules match your search criteria</div>';
|
||
return;
|
||
}
|
||
|
||
// Create HTML for each stage and its modules
|
||
sortedStages.forEach(stageName => {
|
||
const stageModules = modulesByStage[stageName];
|
||
|
||
// Create stage section using similar structure to builder
|
||
const stageSection = document.createElement('div');
|
||
stageSection.className = 'stage-section';
|
||
|
||
// Create stage header
|
||
const stageHeader = document.createElement('div');
|
||
stageHeader.className = 'stage-header';
|
||
|
||
const stageHeaderContent = document.createElement('div');
|
||
stageHeaderContent.className = 'stage-header-content';
|
||
stageHeaderContent.innerHTML = `
|
||
<h2>${stageName}</h2>
|
||
<div class="stage-brief">
|
||
Contains ${stageModules.length} module${stageModules.length !== 1 ? 's' : ''}
|
||
</div>
|
||
`;
|
||
|
||
// Create toggle button
|
||
const toggleBtn = document.createElement('button');
|
||
toggleBtn.className = 'toggle-btn';
|
||
toggleBtn.innerHTML = '+';
|
||
toggleBtn.setAttribute('aria-label', 'Toggle stage content');
|
||
|
||
stageHeader.appendChild(stageHeaderContent);
|
||
stageHeader.appendChild(toggleBtn);
|
||
stageSection.appendChild(stageHeader);
|
||
|
||
// Create stage body
|
||
const stageBody = document.createElement('div');
|
||
stageBody.className = 'stage-body';
|
||
stageBody.style.display = 'none';
|
||
|
||
// Group modules by component
|
||
const modulesByComponent = {};
|
||
stageModules.forEach(module => {
|
||
// Use just the component name without duplicating stage info
|
||
const cleanComponentName = module.componentName.replace(module.stageName, '').trim();
|
||
const displayComponentName = cleanComponentName || module.componentName;
|
||
|
||
if (!modulesByComponent[displayComponentName]) {
|
||
modulesByComponent[displayComponentName] = [];
|
||
}
|
||
modulesByComponent[displayComponentName].push(module);
|
||
});
|
||
|
||
// Sort components
|
||
const sortedComponents = Object.keys(modulesByComponent).sort();
|
||
|
||
// Create components container
|
||
const componentsContainer = document.createElement('div');
|
||
componentsContainer.className = 'components';
|
||
|
||
// Create component sections
|
||
sortedComponents.forEach(componentName => {
|
||
const componentModules = modulesByComponent[componentName];
|
||
|
||
// Create a component group heading with count
|
||
const componentHeading = document.createElement('h3');
|
||
componentHeading.className = 'component-group-heading';
|
||
|
||
// Create main text
|
||
const headingText = document.createTextNode(componentName);
|
||
componentHeading.appendChild(headingText);
|
||
|
||
// Add count in parentheses
|
||
const moduleCount = componentModules.length;
|
||
const countSpan = document.createElement('span');
|
||
countSpan.className = 'component-module-count';
|
||
countSpan.textContent = ` (${moduleCount} module${moduleCount !== 1 ? 's' : ''})`;
|
||
componentHeading.appendChild(countSpan);
|
||
|
||
componentsContainer.appendChild(componentHeading);
|
||
|
||
// Add modules to component section
|
||
componentModules.forEach(module => {
|
||
const moduleCard = createModuleCard(module);
|
||
componentsContainer.appendChild(moduleCard);
|
||
});
|
||
});
|
||
|
||
stageBody.appendChild(componentsContainer);
|
||
stageSection.appendChild(stageBody);
|
||
|
||
// Add toggle functionality
|
||
stageHeaderContent.addEventListener('click', function() {
|
||
if (stageBody.style.display === 'none') {
|
||
stageBody.style.display = 'block';
|
||
toggleBtn.innerHTML = '−';
|
||
} else {
|
||
stageBody.style.display = 'none';
|
||
toggleBtn.innerHTML = '+';
|
||
}
|
||
});
|
||
|
||
toggleBtn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
if (stageBody.style.display === 'none') {
|
||
stageBody.style.display = 'block';
|
||
toggleBtn.innerHTML = '−';
|
||
} else {
|
||
stageBody.style.display = 'none';
|
||
toggleBtn.innerHTML = '+';
|
||
}
|
||
});
|
||
|
||
modulesContainer.appendChild(stageSection);
|
||
});
|
||
|
||
console.log('Modules rendering complete');
|
||
}
|
||
|
||
function createModuleCard(module) {
|
||
const card = document.createElement('div');
|
||
card.className = 'component-card';
|
||
card.dataset.id = module.id;
|
||
|
||
// Format templates list
|
||
const templatesList = module.usedInTemplates.length > 0
|
||
? module.usedInTemplates.join(', ')
|
||
: 'Not used in any template';
|
||
|
||
// Create card content with structure similar to builder component
|
||
card.innerHTML = `
|
||
<div class="component-header">
|
||
<h3>${module.title}</h3>
|
||
</div>
|
||
<div class="component-body">
|
||
<div class="module-meta">
|
||
<span class="tag stage-tag">${module.stageName}</span>
|
||
<span class="tag component-tag">${module.componentName}</span>
|
||
</div>
|
||
<div class="module-content">
|
||
<textarea readonly>${module.content}</textarea>
|
||
</div>
|
||
<div class="module-templates">
|
||
<strong>Used in templates:</strong> ${templatesList}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Make the header clickable to toggle content visibility
|
||
const header = card.querySelector('.component-header');
|
||
const body = card.querySelector('.component-body');
|
||
|
||
// Add toggle button to the header (already positioned by CSS flexbox)
|
||
const toggleBtn = document.createElement('button');
|
||
toggleBtn.className = 'toggle-btn';
|
||
toggleBtn.innerHTML = '+';
|
||
toggleBtn.setAttribute('aria-label', 'Toggle module content');
|
||
header.appendChild(toggleBtn);
|
||
|
||
// Start with content hidden
|
||
body.style.display = 'none';
|
||
|
||
// Toggle functionality
|
||
function toggleContent() {
|
||
if (body.style.display === 'none') {
|
||
body.style.display = 'block';
|
||
toggleBtn.innerHTML = '−';
|
||
card.classList.add('expanded');
|
||
} else {
|
||
body.style.display = 'none';
|
||
toggleBtn.innerHTML = '+';
|
||
card.classList.remove('expanded');
|
||
}
|
||
}
|
||
|
||
header.addEventListener('click', toggleContent);
|
||
|
||
return card;
|
||
}
|
||
|
||
// Initialize page
|
||
function init() {
|
||
console.log('Initializing modules page...');
|
||
|
||
// Load data
|
||
if (!initializeData()) {
|
||
console.error('Failed to initialize data');
|
||
return;
|
||
}
|
||
|
||
// Populate filters
|
||
populateFilters();
|
||
|
||
// Render modules
|
||
renderModules();
|
||
|
||
// Add event listeners to filters
|
||
moduleSearch.addEventListener('input', renderModules);
|
||
stageFilter.addEventListener('change', renderModules);
|
||
componentFilter.addEventListener('change', renderModules);
|
||
templateFilter.addEventListener('change', renderModules);
|
||
|
||
console.log('Modules page initialized with', allModules.length, 'modules');
|
||
}
|
||
|
||
// Start initialization
|
||
init();
|
||
}); |