Files
builder-prototype/static/js/builder.js
2025-05-12 22:44:24 -06:00

1133 lines
40 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded - initializing builder');
// Initialize preview mode state
let previewModeActive = false;
// Preview Mode Toggle
const previewToggle = document.getElementById('preview-toggle');
if (previewToggle) {
console.log('Preview toggle found, adding event listener');
document.addEventListener('click', function(e) {
// Hacky way: find any click anywhere near the toggle and check status
if (e.target.closest('.toggle-switch') || e.target.id === 'preview-toggle') {
// Give time for the checkbox to update
setTimeout(function() {
previewModeActive = previewToggle.checked;
console.log('Preview toggle changed to:', previewModeActive);
// Add a direct style change to visually confirm toggle works
document.querySelector('.toggle-label').style.color = previewModeActive ? 'blue' : '';
togglePreviewMode(previewModeActive);
}, 50);
}
});
} else {
console.error('Preview toggle element not found!');
}
// Function to toggle preview mode
function togglePreviewMode(active) {
console.log('Toggle preview mode called with active =', active);
// Use inline styles for preview mode
// This directly styles elements without relying on classes
const styleId = 'preview-mode-style';
let styleElement = document.getElementById(styleId);
if (active) {
// Create style element if it doesn't exist
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
// Find all textareas with content to mark for display
const contentTextareas = [];
document.querySelectorAll('textarea').forEach(textarea => {
if (textarea.value && textarea.value.trim()) {
// Get the ID for later targeting
contentTextareas.push('#' + textarea.id);
// Mark parents for visibility
const field = textarea.closest('.field');
if (field) field.classList.add('has-content');
const component = textarea.closest('.component-card');
if (component) component.classList.add('has-content');
const stage = textarea.closest('.stage-section');
if (stage) stage.classList.add('has-content');
// Ensure stage is expanded
if (stage) {
const stageBody = stage.querySelector('.stage-body');
if (stageBody) stageBody.style.display = 'block';
}
}
});
// Apply direct CSS to create preview mode
styleElement.textContent = `
/* Hide module selectors and empty fields */
.module-selector { display: none !important; }
.field:not(.has-content) { display: none !important; }
/* Hide empty components and sections */
.component-card:not(.has-content) { display: none !important; }
.stage-section:not(.has-content) { display: none !important; }
/* Hide template selector */
.protocol-template-selector { display: none !important; }
.template-body { display: none !important; }
/* Expand all sections */
.stage-body { display: block !important; }
/* Make textareas read-only appearance */
textarea {
border: none !important;
background-color: transparent !important;
padding: 0 !important;
min-height: unset !important;
height: auto !important;
resize: none !important;
pointer-events: none !important;
outline: none !important;
box-shadow: none !important;
}
/* Only show filled textareas */
textarea:not(${contentTextareas.join(',')}) {
display: none !important;
}
/* Clean styling for components */
.component-header {
background-color: transparent !important;
border-bottom: none !important;
padding-bottom: 0 !important;
}
.component-short-label { display: block !important; }
.component-description { display: none !important; }
/* Improved typography for preview mode */
.component-short-label {
font-size: 1.2rem !important;
margin-bottom: 1rem !important;
color: #000 !important;
border-bottom: 1px solid #eee !important;
padding-bottom: 0.5rem !important;
font-weight: 600 !important;
}
`;
// Replace textareas with divs for better display in preview mode
updatePreviewContent();
// Make other fields read-only
document.querySelectorAll('#community-name, #protocol-summary').forEach(el => {
el.setAttribute('readonly', 'readonly');
});
} else {
// Remove preview styles
if (styleElement) {
styleElement.textContent = '';
}
// Remove preview content divs and show textareas again
document.querySelectorAll('.preview-content').forEach(div => {
const textareaId = div.dataset.forTextarea;
if (textareaId) {
const textarea = document.getElementById(textareaId);
if (textarea) {
textarea.style.display = '';
}
}
div.parentNode.removeChild(div);
});
// Make fields editable again
document.querySelectorAll('textarea, #community-name, #protocol-summary').forEach(el => {
el.removeAttribute('readonly');
});
// Remove content markers
document.querySelectorAll('.has-content').forEach(el => {
el.classList.remove('has-content');
});
// Reset any display properties that were directly set
document.querySelectorAll('.stage-body').forEach(el => {
el.style.display = '';
});
}
}
// Function to mark components and stages that have content
function markComponentsWithContent() {
// First reset all markers
document.querySelectorAll('.has-content').forEach(el => {
el.classList.remove('has-content');
});
// Mark fields with content
document.querySelectorAll('textarea').forEach(textarea => {
if (textarea.value && textarea.value.trim()) {
const field = textarea.closest('.field');
if (field) field.classList.add('has-content');
const component = textarea.closest('.component-card');
if (component) component.classList.add('has-content');
const stage = textarea.closest('.stage-section');
if (stage) stage.classList.add('has-content');
}
});
// Show all expanded sections that have content
document.querySelectorAll('.stage-section.has-content').forEach(stage => {
const stageBody = stage.querySelector('.stage-body');
if (stageBody) {
stageBody.style.display = 'block';
const toggleBtn = stage.querySelector('.toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = '-';
}
}
});
}
// Load template data
let templates = [];
// Function to fetch templates - simplified approach
function fetchTemplates() {
console.log('Fetching templates...');
// Load all templates at startup
try {
// First try loading from the standard path
fetch('/js/templates/index.js')
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch templates: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then(moduleText => {
// Add a script element to the document
const script = document.createElement('script');
script.type = 'module';
script.textContent = moduleText;
document.head.appendChild(script);
// Set a timeout to check for templates
setTimeout(() => {
// See if templates were loaded
if (window.allTemplates && Array.isArray(window.allTemplates)) {
console.log('Templates loaded successfully:', window.allTemplates.length, 'templates found');
templates = window.allTemplates;
// Populate the template selector
populateTemplateSelector(templates);
// Template selection is now handled by buttons in the template options
console.log('Template selection will be handled by buttons in the template options');
} else {
console.error('Templates not available after loading script');
}
}, 500);
})
.catch(error => {
console.error('Error fetching templates:', error);
// Try alternate loading method with ES modules import
console.log('Trying alternate loading method with ES modules...');
import('/js/templates/index.js')
.then(module => {
templates = module.default;
console.log('Templates loaded successfully using ES modules:', templates.length, 'templates found');
// Populate the template selector
populateTemplateSelector(templates);
// Template selection is now handled by buttons in the template options
console.log('Template selection will be handled by buttons in the template options');
})
.catch(importError => {
console.error('ES module import also failed:', importError);
console.error('Could not load templates through either method');
});
});
} catch (e) {
console.error('Error in template loading:', e);
}
}
// Function to populate the template selector with cards
function populateTemplateSelector(templatesList) {
if (!templatesList || templatesList.length === 0) {
console.error('Cannot populate template selector - missing templates');
return;
}
console.log('Populating template selector with', templatesList.length, 'templates');
// Find the template options container
const templateOptionsContainer = document.querySelector('.template-options');
if (!templateOptionsContainer) {
console.error('Template options container not found');
return;
}
// Clear existing template options
templateOptionsContainer.innerHTML = '';
// Create the "Create Your Own" option first
const createYourOwnOption = document.createElement('div');
createYourOwnOption.className = 'template-option';
createYourOwnOption.setAttribute('data-template-id', '');
const createYourOwnBtn = document.createElement('button');
createYourOwnBtn.className = 'template-select-btn';
createYourOwnBtn.textContent = 'Create Your Own Protocol';
createYourOwnBtn.setAttribute('type', 'button');
const createYourOwnDesc = document.createElement('p');
createYourOwnDesc.className = 'template-description';
createYourOwnDesc.textContent = 'Start with a blank protocol and build it from scratch.';
createYourOwnOption.appendChild(createYourOwnBtn);
createYourOwnOption.appendChild(createYourOwnDesc);
createYourOwnOption.addEventListener('click', function() {
console.log('Create your own option clicked');
clearAllFields();
});
templateOptionsContainer.appendChild(createYourOwnOption);
// Verify templates have required properties
let validTemplateCount = 0;
templatesList.forEach(template => {
if (!template.id || !template.title || !template.description) {
console.warn('Template missing required fields:', template);
} else {
validTemplateCount++;
}
});
console.log(`Found ${validTemplateCount} valid templates out of ${templatesList.length} total`);
// Add template options as cards
templatesList.forEach(template => {
if (!template.id || !template.title || !template.description) {
return; // Skip invalid templates
}
const templateOption = document.createElement('div');
templateOption.className = 'template-option';
templateOption.setAttribute('data-template-id', template.id);
// Make the entire card clickable
templateOption.addEventListener('click', function() {
// For debugging
console.log('Template option clicked for:', template.id);
// Find and apply the template
const selectedTemplate = templates.find(t => t.id === template.id);
if (selectedTemplate) {
applyTemplate(selectedTemplate);
// Close the template section after selection
const templateSection = document.querySelector('.protocol-template-selector');
if (templateSection) {
const templateBody = templateSection.querySelector('.template-body');
const toggleBtn = templateSection.querySelector('.toggle-btn');
if (templateBody && toggleBtn) {
templateBody.style.display = 'none';
toggleBtn.textContent = '+';
}
}
} else {
console.error('Template not found:', template.id);
}
});
const selectButton = document.createElement('button');
selectButton.className = 'template-select-btn';
selectButton.textContent = template.title;
selectButton.setAttribute('type', 'button');
const description = document.createElement('p');
description.className = 'template-description';
description.textContent = template.description;
templateOption.appendChild(selectButton);
templateOption.appendChild(description);
templateOptionsContainer.appendChild(templateOption);
console.log('Added template option:', template.title, 'with ID:', template.id);
console.log(' > Description:', template.description ? template.description.substring(0, 50) + '...' : 'MISSING');
});
// We've already set up the click handler for "Create Your Own" when creating it
}
// Function to apply a template by ID
function applyTemplateById(templateId) {
console.log('Applying template by ID:', templateId);
if (templateId) {
// Find the selected template from our loaded templates
const selectedTemplate = templates.find(t => t.id === templateId);
if (selectedTemplate) {
console.log('Found template:', selectedTemplate.title);
applyTemplate(selectedTemplate);
// Close the template section after selection
const templateSection = document.querySelector('.protocol-template-selector');
if (templateSection) {
const templateBody = templateSection.querySelector('.template-body');
const toggleBtn = templateSection.querySelector('.toggle-btn');
if (templateBody && toggleBtn) {
templateBody.style.display = 'none';
toggleBtn.textContent = '+';
}
}
} else {
console.error('Template not found:', templateId);
}
}
}
// Function to clear all fields
function clearAllFields() {
// Clear all textareas
document.querySelectorAll('textarea').forEach(textarea => {
textarea.value = '';
});
// Clear community name input
if (communityNameInput) {
communityNameInput.value = '';
}
// Clear protocol summary
if (protocolSummaryTextarea) {
protocolSummaryTextarea.value = '';
}
// Reset protocol data
protocol = {
metadata: {
communityName: "",
summary: ""
},
stages: {}
};
// Collapse all sections
stageContents.forEach(content => {
content.style.display = 'none';
const toggleBtn = content.parentElement.querySelector('.toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = '+';
}
});
// Close the template section after clearing
const templateSection = document.querySelector('.protocol-template-selector');
if (templateSection) {
const templateBody = templateSection.querySelector('.template-body');
const toggleBtn = templateSection.querySelector('.toggle-btn');
if (templateBody && toggleBtn) {
templateBody.style.display = 'none';
toggleBtn.textContent = '+';
}
}
// Update preview mode if active
if (previewModeActive) {
markComponentsWithContent();
}
console.log('All fields cleared');
}
// Function to apply a template to the form
function applyTemplate(selectedTemplate) {
if (!selectedTemplate) {
console.error('Cannot apply template: template is null or undefined');
return;
}
console.log('Applying template:', selectedTemplate.title);
console.log('Template data structure:', selectedTemplate);
// Reset protocol data while preserving metadata
protocol = {
metadata: {
communityName: communityNameInput.value || "",
summary: protocolSummaryTextarea.value || ""
},
stages: {},
templateId: selectedTemplate.id,
templateTitle: selectedTemplate.title,
templateDescription: selectedTemplate.description
};
// Always use the template description for summary when applying a template
protocolSummaryTextarea.value = selectedTemplate.description;
protocol.metadata.summary = selectedTemplate.description;
console.log('Set summary to template description:', selectedTemplate.description);
// Check if template has data.stages
if (!selectedTemplate.data || !selectedTemplate.data.stages) {
console.error('Template has invalid structure - missing data.stages:', selectedTemplate);
return;
}
console.log('Template has these stages:', Object.keys(selectedTemplate.data.stages));
// Count fields that will be populated
let fieldsPopulated = 0;
// Apply the template content directly to the form
for (const stageId in selectedTemplate.data.stages) {
console.log(`Processing stage ${stageId}...`);
if (!protocol.stages[stageId]) {
protocol.stages[stageId] = {};
}
for (const componentId in selectedTemplate.data.stages[stageId]) {
console.log(` Processing component ${componentId}...`);
if (!protocol.stages[stageId][componentId]) {
protocol.stages[stageId][componentId] = {};
}
for (const fieldId in selectedTemplate.data.stages[stageId][componentId]) {
const content = selectedTemplate.data.stages[stageId][componentId][fieldId];
const textarea = document.getElementById(fieldId);
console.log(` Processing field ${fieldId}:`);
console.log(` Content exists: ${Boolean(content)}`);
console.log(` Field element exists: ${Boolean(textarea)}`);
if (textarea && content) {
// Apply the content to the textarea
textarea.value = content;
// Height adjustment is now handled by the global script
// Store in protocol data
protocol.stages[stageId][componentId][fieldId] = content;
fieldsPopulated++;
console.log(` ✓ Populated field ${fieldId} with content (${content.length} chars)`);
} else {
if (!textarea) {
console.warn(` ✗ Textarea element not found for field ID: ${fieldId}`);
}
if (!content) {
console.warn(` ✗ No content for field ID: ${fieldId}`);
}
}
}
}
}
console.log(`Template application complete: ${fieldsPopulated} fields populated`);
// Expand all sections to show the populated content
stageContents.forEach(content => {
content.style.display = 'block';
const toggleBtn = content.parentElement.querySelector('.toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = '-';
}
});
// Update preview mode if active
if (previewModeActive) {
markComponentsWithContent();
}
// Dispatch custom event for textarea auto-resize
document.dispatchEvent(new CustomEvent('templateApplied'));
}
// No helper functions needed anymore as we've simplified the approach
// Start loading templates
fetchTemplates();
// Protocol data structure
let protocol = {
metadata: {
communityName: "",
summary: ""
},
stages: {}
};
// DOM elements
const stageHeaders = document.querySelectorAll('.stage-header');
console.log('Found stage headers:', stageHeaders.length);
const stageContents = document.querySelectorAll('.stage-body');
console.log('Found stage bodies:', stageContents.length);
const moduleSelects = document.querySelectorAll('.module-select');
console.log('Found module selects:', moduleSelects.length);
const communityNameInput = document.getElementById('community-name');
const protocolSummaryTextarea = document.getElementById('protocol-summary');
const exportBtn = document.getElementById('export-btn');
const exportMdBtn = document.getElementById('export-md');
const exportPdfBtn = document.getElementById('export-pdf');
const exportJsonBtn = document.getElementById('export-json');
const importJsonInput = document.getElementById('import-json');
const importBtn = document.getElementById('import-btn');
// This function is no longer needed with the new template UI
// Keeping an empty function to avoid errors if it's called elsewhere
function initializeTemplateSelector(templatesList) {
console.log('initializeTemplateSelector is deprecated, using new UI instead');
}
// Hide module selectors since we're using templates directly
document.querySelectorAll('.module-selector').forEach(selector => {
selector.style.display = 'none';
});
// Update protocol data from form inputs
function updateProtocolData() {
// Update metadata
protocol.metadata = {
communityName: communityNameInput.value || "",
summary: protocolSummaryTextarea.value || ""
};
// Reset the stages data
protocol.stages = {};
// Get all textareas and their values
const textareas = document.querySelectorAll('textarea');
textareas.forEach(textarea => {
const fieldId = textarea.id;
const fieldValue = textarea.value;
// Skip empty fields
if (!fieldValue || !fieldValue.trim()) return;
// Find the component and stage for this field
const componentCard = textarea.closest('.component-card');
if (!componentCard) return;
const componentId = componentCard.id.replace('component-', '');
const stageSection = textarea.closest('.stage-section');
if (!stageSection) return;
const stageId = stageSection.id.replace('stage-', '');
// Initialize stage and component if they don't exist
if (!protocol.stages[stageId]) {
protocol.stages[stageId] = {};
}
if (!protocol.stages[stageId][componentId]) {
protocol.stages[stageId][componentId] = {};
}
// Set the field value
protocol.stages[stageId][componentId][fieldId] = fieldValue;
});
// If a template is selected, preserve the template information
const selectedTemplateId = protocolTemplateSelect ? protocolTemplateSelect.value : '';
if (selectedTemplateId) {
const selectedTemplate = templates.find(t => t.id === selectedTemplateId);
if (selectedTemplate) {
protocol.templateId = selectedTemplateId;
protocol.templateTitle = selectedTemplate.title;
protocol.templateDescription = selectedTemplate.description;
}
}
// Remove empty components and stages
for (const stageId in protocol.stages) {
// Check if stage has any non-empty components
const stageComponents = protocol.stages[stageId];
let stageHasContent = false;
for (const componentId in stageComponents) {
// Check if component has any fields
const component = stageComponents[componentId];
const fieldCount = Object.keys(component).length;
if (fieldCount > 0) {
stageHasContent = true;
} else {
// Remove empty component
delete stageComponents[componentId];
}
}
// If stage has no content, remove it
if (!stageHasContent) {
delete protocol.stages[stageId];
}
}
}
// Export to Markdown
exportMdBtn.addEventListener('click', function(e) {
e.preventDefault();
updateProtocolData();
try {
// Use community name if available, otherwise default
const communityName = protocol.metadata.communityName || "Community";
let markdown = `# ${communityName}\n# CommunityDispute\n\n`;
// Include protocol summary if available
if (protocol.metadata.summary) {
markdown += `${protocol.metadata.summary}\n\n`;
markdown += '---\n\n';
}
// Template information removed as requested
// Loop through the stages in order
stageHeaders.forEach(header => {
const stageId = header.getAttribute('data-stage');
const stageName = header.querySelector('h2').textContent;
let stageContent = '';
let hasContent = false;
// Get components for this stage
const stageComponents = protocol.stages[stageId];
if (stageComponents) {
// Loop through components
for (const componentId in stageComponents) {
const componentCard = document.getElementById(`component-${componentId}`);
if (componentCard) {
const componentName = componentCard.querySelector('h3').textContent;
let componentContent = '';
let componentHasContent = false;
// Loop through fields
for (const fieldId in stageComponents[componentId]) {
const fieldValue = stageComponents[componentId][fieldId];
// Skip empty fields
if (fieldValue && fieldValue.trim()) {
// Skip the field label, just add the content
componentContent += `${fieldValue}\n\n`;
componentHasContent = true;
hasContent = true;
}
}
// Only add component if it has content
if (componentHasContent) {
stageContent += `### ${componentName}\n\n${componentContent}`;
}
}
}
}
// Only add stage if it has content
if (hasContent) {
// Get the stage section ID (e.g., "Intake")
const stageSection = header.closest('.stage-section');
const stageSectionId = stageSection ? stageSection.id.replace('stage-', '') : '';
const stageSectionTitle = stageSectionId ? stageSectionId.charAt(0).toUpperCase() + stageSectionId.slice(1) : '';
// Use just the section title to avoid duplication
if (stageSectionTitle) {
markdown += `## ${stageSectionTitle}\n\n${stageContent}`;
} else {
markdown += `## ${stageName}\n\n${stageContent}`;
}
}
});
// Create and download the file
downloadFile('communityDispute.md', markdown);
} catch (error) {
console.error('Error generating Markdown:', error);
alert('Failed to generate Markdown. Please try again.');
}
});
// Export to PDF
exportPdfBtn.addEventListener('click', function(e) {
e.preventDefault();
updateProtocolData();
try {
// Check if jspdf is properly loaded
if (typeof window.jspdf === 'undefined') {
console.error('jsPDF library not loaded properly');
alert('PDF export is currently unavailable. The required library failed to load.');
return;
}
// Create a styled HTML version for PDF export
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Set consistent text width parameters
const margins = {
left: 14,
right: 14,
pageWidth: 210, // A4 width in mm (portrait)
width: 182 // We'll use this as the consistent text width - should be pageWidth - left - right
};
let yPos = 20;
// Use community name if available, otherwise default
const communityName = protocol.metadata.communityName || "Community";
// Add title with line break
doc.setFontSize(18);
doc.text(`${communityName}\nCommunityDispute`, 105, yPos, { align: 'center' });
yPos += 25;
// Page width indicator removed
// Add protocol summary if available
if (protocol.metadata.summary) {
doc.setFontSize(12);
const summaryLines = doc.splitTextToSize(protocol.metadata.summary, margins.width);
doc.text(summaryLines, margins.left, yPos);
yPos += summaryLines.length * 7 + 8;
}
// Template information removed as requested
// Set up direct rendering approach to avoid duplication
const sectionsToProcess = [];
// Find all unique stage sections with content
document.querySelectorAll('.stage-section').forEach(section => {
const sectionId = section.id.replace('stage-', '');
const sectionTitle = sectionId.charAt(0).toUpperCase() + sectionId.slice(1);
const componentsWithContent = [];
// Look for components with content in this section
section.querySelectorAll('.component-card').forEach(componentCard => {
const componentId = componentCard.id.replace('component-', '');
// Check if this component has content in the protocol data
const stageId = section.id.replace('stage-', '');
if (protocol.stages[stageId] &&
protocol.stages[stageId][componentId] &&
Object.keys(protocol.stages[stageId][componentId]).length > 0) {
componentsWithContent.push({
id: componentId,
element: componentCard,
name: componentCard.querySelector('h3').textContent
});
}
});
// Only include sections that have components with content
if (componentsWithContent.length > 0) {
sectionsToProcess.push({
id: sectionId,
title: sectionTitle,
components: componentsWithContent
});
}
});
// Process each section directly to avoid duplication
sectionsToProcess.forEach(section => {
// Add page break if needed
if (yPos > 250) {
doc.addPage();
yPos = 20;
}
// Add section header only once (e.g., "Intake")
doc.setFontSize(18);
doc.text(section.title, margins.left, yPos);
yPos += 15;
// Process components in this section
section.components.forEach(component => {
// Add page break if needed
if (yPos > 250) {
doc.addPage();
yPos = 20;
}
// Add component heading
doc.setFontSize(14);
doc.text(component.name, margins.left, yPos);
yPos += 8;
// Get fields for this component
const stageId = section.id;
const componentId = component.id;
if (protocol.stages[stageId] && protocol.stages[stageId][componentId]) {
// Loop through fields
for (const fieldId in protocol.stages[stageId][componentId]) {
const fieldValue = protocol.stages[stageId][componentId][fieldId];
// Skip empty fields
if (fieldValue && fieldValue.trim()) {
// Add page break if needed
if (yPos > 250) {
doc.addPage();
yPos = 20;
}
// Format field content with larger font size (12)
doc.setFontSize(12);
// Process markdown to plaintext for PDF
let processedContent = fieldValue;
try {
// Strip HTML tags from rendered markdown to get clean text
const temp = document.createElement('div');
temp.innerHTML = marked.parse(fieldValue);
// Convert links to format: text (url)
const links = temp.querySelectorAll('a');
links.forEach(link => {
const linkText = link.textContent;
const href = link.getAttribute('href');
// Only modify if href is different from text
if (href && href !== linkText) {
const replacement = `${linkText} (${href})`;
link.textContent = replacement;
}
});
// Bold text handling
const boldElements = temp.querySelectorAll('strong, b');
boldElements.forEach(el => {
el.textContent = `*${el.textContent}*`; // Surround with asterisks
});
// Get plain text content
processedContent = temp.textContent;
} catch (error) {
console.error('Error processing Markdown for PDF:', error);
}
// Use consistent width for all text
const textLines = doc.splitTextToSize(processedContent, margins.width);
doc.text(textLines, margins.left, yPos);
yPos += textLines.length * 6 + 8; // Slightly increase spacing for larger font
}
}
}
});
});
// Save the PDF
doc.save('communityDispute.pdf');
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again or use another export format.');
}
});
// Export to JSON
exportJsonBtn.addEventListener('click', function(e) {
e.preventDefault();
updateProtocolData();
const jsonData = JSON.stringify(protocol, null, 2);
downloadFile('communityDispute.json', jsonData);
});
// Import from JSON
importBtn.addEventListener('click', function(e) {
e.preventDefault(); // Prevent the default anchor behavior
importJsonInput.click();
});
importJsonInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const importedProtocol = JSON.parse(e.target.result);
protocol = importedProtocol;
// Populate metadata fields if present
if (protocol.metadata) {
if (protocol.metadata.communityName) {
communityNameInput.value = protocol.metadata.communityName;
}
if (protocol.metadata.summary) {
protocolSummaryTextarea.value = protocol.metadata.summary;
}
}
// Populate the component fields with the imported data
for (const stageId in protocol.stages) {
for (const componentId in protocol.stages[stageId]) {
for (const fieldId in protocol.stages[stageId][componentId]) {
const textarea = document.getElementById(fieldId);
if (textarea) {
textarea.value = protocol.stages[stageId][componentId][fieldId];
}
}
}
}
// If the imported protocol has template information, highlight the template
if (protocol.templateId) {
// Highlight the selected template option
const templateOptions = document.querySelectorAll('.template-option');
templateOptions.forEach(option => {
const templateId = option.getAttribute('data-template-id');
if (templateId === protocol.templateId) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
// Expand all sections
stageContents.forEach(content => {
content.style.display = 'block';
const toggleBtn = content.parentElement.querySelector('.toggle-btn');
if (toggleBtn) {
toggleBtn.textContent = '-';
}
});
} else {
// If no template, remove any highlights
document.querySelectorAll('.template-option').forEach(option => {
option.classList.remove('selected');
});
}
// Update preview mode if active
if (previewModeActive) {
markComponentsWithContent();
}
alert('Protocol imported successfully!');
} catch (error) {
alert('Failed to import protocol. Invalid JSON format.');
console.error(error);
}
};
reader.readAsText(file);
}
});
// Helper function to download a file
function downloadFile(filename, content) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
// Save data when inputs change
document.querySelectorAll('textarea').forEach(textarea => {
textarea.addEventListener('input', function() {
updateProtocolData();
// Update preview mode if active
if (previewModeActive) {
markComponentsWithContent();
}
});
});
// Function to update preview content with Markdown support
function updatePreviewContent() {
// First, clear any existing preview divs
document.querySelectorAll('.preview-content').forEach(div => {
div.parentNode.removeChild(div);
});
// Show all textareas again
document.querySelectorAll('textarea').forEach(textarea => {
textarea.style.display = '';
});
// Then create new preview divs for all textareas with content
document.querySelectorAll('textarea').forEach(textarea => {
if (textarea.value && textarea.value.trim()) {
// Create a div to replace the textarea for preview
const previewDiv = document.createElement('div');
previewDiv.className = 'preview-content';
// Parse Markdown and set as HTML content
try {
previewDiv.innerHTML = marked.parse(textarea.value);
} catch (error) {
console.error('Error parsing Markdown:', error);
previewDiv.textContent = textarea.value;
}
previewDiv.dataset.forTextarea = textarea.id;
// Hide the textarea and insert the div
textarea.style.display = 'none';
textarea.parentNode.insertBefore(previewDiv, textarea.nextSibling);
}
});
}
// Update preview content when preview mode is toggled
document.getElementById('preview-toggle').addEventListener('change', function() {
if (this.checked) {
// Wait for the preview mode styles to apply, then update content
setTimeout(updatePreviewContent, 100);
}
});
// Update preview content when module content changes
document.querySelectorAll('.module-select').forEach(select => {
select.addEventListener('change', function() {
setTimeout(() => {
if (document.getElementById('preview-toggle').checked) {
updatePreviewContent();
}
}, 100);
});
});
// Update preview content on window resize
window.addEventListener('resize', function() {
if (document.getElementById('preview-toggle').checked) {
updatePreviewContent();
}
});
console.log('Builder initialization complete');
});