Initial commit
This commit is contained in:
970
static/js/builder.js
Normal file
970
static/js/builder.js
Normal file
@ -0,0 +1,970 @@
|
||||
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();
|
||||
|
||||
// Use community name if available, otherwise default
|
||||
const communityName = protocol.metadata.communityName || "Community";
|
||||
let markdown = `# ${communityName} Dispute Protocol\n\n`;
|
||||
|
||||
// Include protocol summary if available
|
||||
if (protocol.metadata.summary) {
|
||||
markdown += `${protocol.metadata.summary}\n\n`;
|
||||
markdown += '---\n\n';
|
||||
}
|
||||
|
||||
// Include template information if a template was used
|
||||
if (protocol.templateTitle) {
|
||||
markdown += `**Template Used:** ${protocol.templateTitle}\n\n`;
|
||||
markdown += '---\n\n';
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
const fieldLabel = document.querySelector(`label[for="${fieldId}"]`).textContent;
|
||||
componentContent += `#### ${fieldLabel}\n\n${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) {
|
||||
markdown += `## ${stageName}\n\n${stageContent}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Create and download the file
|
||||
downloadFile('community_dispute_protocol.md', markdown);
|
||||
});
|
||||
|
||||
// Export to PDF
|
||||
exportPdfBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
updateProtocolData();
|
||||
|
||||
// Create a styled HTML version for PDF export
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF();
|
||||
|
||||
let yPos = 20;
|
||||
|
||||
// Use community name if available, otherwise default
|
||||
const communityName = protocol.metadata.communityName || "Community";
|
||||
|
||||
// Add title
|
||||
doc.setFontSize(18);
|
||||
doc.text(`${communityName} Dispute Protocol`, 105, yPos, { align: 'center' });
|
||||
yPos += 15;
|
||||
|
||||
// Add protocol summary if available
|
||||
if (protocol.metadata.summary) {
|
||||
doc.setFontSize(12);
|
||||
const summaryLines = doc.splitTextToSize(protocol.metadata.summary, 180);
|
||||
doc.text(summaryLines, 14, yPos);
|
||||
yPos += summaryLines.length * 7 + 8;
|
||||
}
|
||||
|
||||
// Add template info if available
|
||||
if (protocol.templateTitle) {
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Template Used: ${protocol.templateTitle}`, 14, yPos);
|
||||
yPos += 10;
|
||||
}
|
||||
|
||||
// Loop through the stages
|
||||
stageHeaders.forEach(header => {
|
||||
const stageId = header.getAttribute('data-stage');
|
||||
const stageName = header.querySelector('h2').textContent;
|
||||
|
||||
let stageHasContent = false;
|
||||
let stageStartYPos = yPos;
|
||||
|
||||
// Save current position to add stage heading later if content is found
|
||||
if (yPos > 250) {
|
||||
doc.addPage();
|
||||
stageStartYPos = 20;
|
||||
yPos = 20;
|
||||
}
|
||||
|
||||
// Skip stage heading for now - we'll add it if the stage has content
|
||||
let currentYPos = stageStartYPos + 10; // Space for the heading
|
||||
|
||||
// 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) {
|
||||
let componentHasContent = false;
|
||||
let componentStartYPos = currentYPos;
|
||||
|
||||
// Check for page break
|
||||
if (currentYPos > 250) {
|
||||
doc.addPage();
|
||||
componentStartYPos = 20;
|
||||
currentYPos = 20;
|
||||
}
|
||||
|
||||
// Skip component heading for now - add if it has content
|
||||
const componentFieldStartYPos = componentStartYPos + 8;
|
||||
let fieldYPos = componentFieldStartYPos;
|
||||
|
||||
const componentName = componentCard.querySelector('h3').textContent;
|
||||
|
||||
// Loop through fields
|
||||
for (const fieldId in stageComponents[componentId]) {
|
||||
const fieldValue = stageComponents[componentId][fieldId];
|
||||
|
||||
// Skip empty fields
|
||||
if (fieldValue && fieldValue.trim()) {
|
||||
const fieldLabel = document.querySelector(`label[for="${fieldId}"]`).textContent;
|
||||
|
||||
// Add page break if needed
|
||||
if (fieldYPos > 250) {
|
||||
doc.addPage();
|
||||
fieldYPos = 20;
|
||||
}
|
||||
|
||||
// We have content - if this is the first content in the component,
|
||||
// add the component heading
|
||||
if (!componentHasContent) {
|
||||
componentHasContent = true;
|
||||
stageHasContent = true;
|
||||
|
||||
// If this is the first content in the stage, add the stage heading
|
||||
if (!stageHasContent) {
|
||||
doc.setFontSize(16);
|
||||
doc.text(stageName, 14, stageStartYPos);
|
||||
}
|
||||
|
||||
// Add component heading
|
||||
doc.setFontSize(14);
|
||||
doc.text(componentName, 14, componentStartYPos);
|
||||
}
|
||||
|
||||
// Add field heading
|
||||
doc.setFontSize(12);
|
||||
doc.text(fieldLabel, 14, fieldYPos);
|
||||
fieldYPos += 6;
|
||||
|
||||
// Split the text into lines to handle wrapping
|
||||
const textLines = doc.splitTextToSize(fieldValue, 180);
|
||||
|
||||
// Add field content
|
||||
doc.setFontSize(10);
|
||||
doc.text(textLines, 14, fieldYPos);
|
||||
fieldYPos += textLines.length * 5 + 8;
|
||||
}
|
||||
}
|
||||
|
||||
// Update current Y position if component had content
|
||||
if (componentHasContent) {
|
||||
currentYPos = fieldYPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the overall Y position if stage had content
|
||||
if (stageHasContent) {
|
||||
yPos = currentYPos;
|
||||
}
|
||||
});
|
||||
|
||||
// Save the PDF
|
||||
doc.save('community_dispute_protocol.pdf');
|
||||
});
|
||||
|
||||
// Export to JSON
|
||||
exportJsonBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
updateProtocolData();
|
||||
const jsonData = JSON.stringify(protocol, null, 2);
|
||||
downloadFile('community_dispute_protocol.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');
|
||||
});
|
Reference in New Issue
Block a user