991 lines
35 KiB
JavaScript
991 lines
35 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; }
|
|
|
|
/* 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: none !important; }
|
|
|
|
/* Improved typography for preview mode */
|
|
.component-header h3 {
|
|
font-size: 1.4rem !important;
|
|
margin-bottom: 1rem !important;
|
|
color: #000 !important;
|
|
border-bottom: 1px solid #eee !important;
|
|
padding-bottom: 0.5rem !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 module data
|
|
let allModules = {};
|
|
try {
|
|
// Check if moduleData is defined (from modules.js)
|
|
if (typeof moduleData !== 'undefined') {
|
|
allModules = moduleData;
|
|
console.log('Module data loaded from modules.js');
|
|
}
|
|
} catch (e) {
|
|
console.error('Error loading module data:', e);
|
|
allModules = {};
|
|
}
|
|
|
|
// Load and process template data
|
|
let templates = [];
|
|
try {
|
|
// Check if raw templates are defined (from templates.js)
|
|
if (typeof rawProtocolTemplates !== 'undefined' && typeof templateMapper !== 'undefined') {
|
|
console.log('Raw protocol templates loaded from templates.js');
|
|
|
|
// Process each template to use module references
|
|
rawProtocolTemplates.forEach(rawTemplate => {
|
|
const processedTemplate = templateMapper.convertTemplateToModules(rawTemplate, allModules);
|
|
templates.push(processedTemplate);
|
|
});
|
|
|
|
console.log('Processed templates to use module references:', templates);
|
|
}
|
|
} catch (e) {
|
|
console.error('Error processing protocol templates:', e);
|
|
templates = [];
|
|
}
|
|
|
|
// 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 protocolTemplateSelect = document.getElementById('protocol-template');
|
|
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');
|
|
|
|
// Populate protocol template select
|
|
if (protocolTemplateSelect && templates.length > 0) {
|
|
templates.forEach(template => {
|
|
const option = document.createElement('option');
|
|
option.value = template.id;
|
|
option.textContent = template.title;
|
|
protocolTemplateSelect.appendChild(option);
|
|
});
|
|
|
|
// Add template selection event handler
|
|
protocolTemplateSelect.addEventListener('change', function() {
|
|
const selectedTemplateId = this.value;
|
|
|
|
if (selectedTemplateId) {
|
|
// Find the selected template
|
|
const selectedTemplate = templates.find(t => t.id === selectedTemplateId);
|
|
|
|
if (selectedTemplate) {
|
|
console.log('Applying template:', selectedTemplate);
|
|
|
|
console.log('Selected template:', selectedTemplate.title);
|
|
|
|
// Reset protocol data while preserving metadata
|
|
protocol = {
|
|
metadata: {
|
|
communityName: communityNameInput.value || "",
|
|
summary: protocolSummaryTextarea.value || ""
|
|
},
|
|
stages: {},
|
|
templateId: selectedTemplate.id,
|
|
templateTitle: selectedTemplate.title,
|
|
templateDescription: selectedTemplate.description
|
|
};
|
|
|
|
// If summary is empty, use template description
|
|
if (!protocol.metadata.summary) {
|
|
protocolSummaryTextarea.value = selectedTemplate.description;
|
|
protocol.metadata.summary = selectedTemplate.description;
|
|
}
|
|
|
|
// Apply the template module references to the form
|
|
for (const stageId in selectedTemplate.moduleRefs) {
|
|
if (!protocol.stages[stageId]) {
|
|
protocol.stages[stageId] = {};
|
|
}
|
|
|
|
for (const componentId in selectedTemplate.moduleRefs[stageId]) {
|
|
if (!protocol.stages[stageId][componentId]) {
|
|
protocol.stages[stageId][componentId] = {};
|
|
}
|
|
|
|
for (const fieldId in selectedTemplate.moduleRefs[stageId][componentId]) {
|
|
const moduleId = selectedTemplate.moduleRefs[stageId][componentId][fieldId];
|
|
const textarea = document.getElementById(fieldId);
|
|
|
|
if (textarea) {
|
|
// Find the module with this ID
|
|
let moduleContent = '';
|
|
|
|
for (const category in allModules) {
|
|
const foundModule = allModules[category].find(m => m.id === moduleId);
|
|
if (foundModule) {
|
|
moduleContent = foundModule.content;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Apply the module content to the textarea
|
|
textarea.value = moduleContent;
|
|
|
|
// Store in protocol data
|
|
protocol.stages[stageId][componentId][fieldId] = moduleContent;
|
|
|
|
// If there's a template selector for this field, update it
|
|
const moduleSelector = document.querySelector(`select.module-select[data-field-id="${fieldId}"][data-component-id="${componentId}"]`);
|
|
if (moduleSelector) {
|
|
moduleSelector.value = moduleId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
} else {
|
|
// Clear all fields if "Create Your Own" is selected
|
|
|
|
document.querySelectorAll('textarea').forEach(textarea => {
|
|
textarea.value = '';
|
|
});
|
|
|
|
// Reset protocol data
|
|
protocol = { stages: {} };
|
|
|
|
// Collapse all sections
|
|
stageContents.forEach(content => {
|
|
content.style.display = 'none';
|
|
const toggleBtn = content.parentElement.querySelector('.toggle-btn');
|
|
if (toggleBtn) {
|
|
toggleBtn.textContent = '+';
|
|
}
|
|
});
|
|
|
|
// Update preview mode if active
|
|
if (previewModeActive) {
|
|
markComponentsWithContent();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize all module selects
|
|
populateModuleSelects();
|
|
|
|
// Module selection handlers
|
|
moduleSelects.forEach(select => {
|
|
select.addEventListener('change', function() {
|
|
const fieldId = this.getAttribute('data-field-id');
|
|
const componentId = this.getAttribute('data-component-id');
|
|
const targetTextarea = document.getElementById(fieldId);
|
|
|
|
if (targetTextarea && this.value) {
|
|
// Find the selected module
|
|
for (const category in allModules) {
|
|
const selectedModule = allModules[category].find(m => m.id === this.value);
|
|
if (selectedModule) {
|
|
targetTextarea.value = selectedModule.content;
|
|
|
|
// Update protocol data
|
|
updateProtocolData();
|
|
|
|
// Update preview mode if active
|
|
if (previewModeActive) {
|
|
markComponentsWithContent();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Function to populate module select dropdowns
|
|
function populateModuleSelects() {
|
|
console.log('Populating module selects...');
|
|
console.log('Available module categories:', Object.keys(allModules));
|
|
|
|
// Debugging: Log all modules to check componentId and fieldId
|
|
console.log('All module mapping:');
|
|
for (const category in allModules) {
|
|
console.log(`Category: ${category}`);
|
|
allModules[category].forEach(module => {
|
|
console.log(` Module: ${module.id}, Component: ${module.componentId}, Field: ${module.fieldId}`);
|
|
});
|
|
}
|
|
|
|
moduleSelects.forEach(select => {
|
|
const fieldId = select.getAttribute('data-field-id');
|
|
const componentId = select.getAttribute('data-component-id');
|
|
|
|
console.log(`Processing module select for fieldId: ${fieldId}, componentId: ${componentId}`);
|
|
|
|
// Clear existing options except the first one
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
// Find modules that match this field and component
|
|
let hasOptions = false;
|
|
|
|
// Always show select first - we'll hide it later if no options found
|
|
select.closest('.module-selector').style.display = 'flex';
|
|
|
|
// Check for case matching issues and missing references
|
|
for (const category in allModules) {
|
|
let exactMatches = allModules[category].filter(m =>
|
|
m.fieldId === fieldId && m.componentId === componentId
|
|
);
|
|
|
|
let caseInsensitiveMatches = allModules[category].filter(m =>
|
|
m.fieldId.toLowerCase() === fieldId.toLowerCase() &&
|
|
m.componentId.toLowerCase() === componentId.toLowerCase() &&
|
|
!exactMatches.includes(m)
|
|
);
|
|
|
|
if (caseInsensitiveMatches.length > 0) {
|
|
console.warn(`Found ${caseInsensitiveMatches.length} case-insensitive matches for ${componentId}/${fieldId}. Consider fixing these module references.`);
|
|
|
|
// Add case-insensitive matches to the collection
|
|
caseInsensitiveMatches.forEach(module => {
|
|
// Create a copy with corrected references
|
|
const correctedModule = {
|
|
...module,
|
|
componentId: componentId,
|
|
fieldId: fieldId
|
|
};
|
|
|
|
// Add to the exact matches
|
|
exactMatches.push(correctedModule);
|
|
});
|
|
}
|
|
|
|
if (exactMatches.length > 0) {
|
|
console.log(`Found ${exactMatches.length} modules in category ${category} for ${componentId}/${fieldId}`);
|
|
hasOptions = true;
|
|
|
|
// Don't use option groups - add options directly to the select
|
|
// This avoids showing category labels which can be confusing
|
|
exactMatches.forEach(module => {
|
|
const option = document.createElement('option');
|
|
option.value = module.id;
|
|
|
|
// Use the module title directly from the definition
|
|
// This relies on proper module titles being defined in modules.js
|
|
option.textContent = module.title;
|
|
|
|
// Add directly to select instead of to a group
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
// If no modules found, hide the selector
|
|
if (!hasOptions) {
|
|
console.log(`No modules found for ${componentId}/${fieldId}, hiding selector`);
|
|
select.closest('.module-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);
|
|
|
|
// Use consistent width for all text
|
|
const textLines = doc.splitTextToSize(fieldValue, 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() {
|
|
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, select that template
|
|
if (protocol.templateId && protocolTemplateSelect) {
|
|
protocolTemplateSelect.value = protocol.templateId;
|
|
|
|
// Update template description
|
|
if (protocol.templateDescription && templateDescription) {
|
|
templateDescription.textContent = protocol.templateDescription;
|
|
templateDescription.style.display = 'block';
|
|
}
|
|
|
|
// 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, reset the template selector
|
|
if (protocolTemplateSelect) {
|
|
protocolTemplateSelect.value = '';
|
|
}
|
|
if (templateDescription) {
|
|
templateDescription.textContent = '';
|
|
templateDescription.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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';
|
|
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');
|
|
}); |