${ap_escapeHTML(ap_data.schoolName)}
${ap_escapeHTML(ap_data.academicYear)}
Course Description
${ap_escapeHTML(ap_data.courseDescription).replace(/\n/g, '
')}
Course Materials
Primary Textbook:
${ap_escapeHTML(ap_data.primaryTextbook)}
Other Materials:
${ap_data.otherMaterials.map(m => `- ${ap_escapeHTML(m)}
`).join('')}
Grading Policy
| Category |
Weight |
${ap_data.gradingPolicy.map(item => `
| ${ap_escapeHTML(item.category)} |
${ap_escapeHTML(item.weight)} |
`).join('')}
Curricular Requirements
| Code |
Requirement Description |
Teacher Evidence |
${ap_data.curricularRequirements.map(item => `
| ${ap_escapeHTML(item.code)} |
${ap_escapeHTML(item.req).replace(/\n/g, ' ')} |
${ap_escapeHTML(item.evidence).replace(/\n/g, ' ')} |
`).join('')}
`;
}
/**
* Reads all values from the config tab and updates the ap_data object
*/
function ap_updateDataFromConfig() {
if (!ap_inputCourseName) return;
ap_data.courseName = ap_inputCourseName.value;
ap_data.teacherName = ap_inputTeacherName.value;
ap_data.schoolName = ap_inputSchoolName.value;
ap_data.academicYear = ap_inputAcademicYear.value;
ap_data.courseDescription = ap_inputDescription.value;
ap_data.primaryTextbook = ap_inputTextbook.value;
ap_data.otherMaterials = [];
ap_materialsContainer.querySelectorAll('.ap-input-material').forEach(input => {
if (input.value) ap_data.otherMaterials.push(input.value);
});
ap_data.gradingPolicy = [];
ap_gradingContainer.querySelectorAll('.flex').forEach(entry => {
const id = parseInt(entry.getAttribute('data-id'), 10);
const category = entry.querySelector('.ap-input-grading-cat').value;
const weight = entry.querySelector('.ap-input-grading-weight').value;
if (category || weight) {
ap_data.gradingPolicy.push({ id, category, weight });
}
});
ap_data.curricularRequirements = [];
ap_reqsContainer.querySelectorAll('.border').forEach(entry => {
const id = parseInt(entry.getAttribute('data-id'), 10);
const code = entry.querySelector('.ap-input-req-code').value;
const req = entry.querySelector('.ap-input-req-desc').value;
const evidence = entry.querySelector('.ap-input-req-evidence').value;
if (code || req || evidence) {
ap_data.curricularRequirements.push({ id, code, req, evidence });
}
});
}
/**
* Generates and downloads a multi-page PDF of the dashboard
*/
async function ap_downloadPDF() {
// Check for required libraries
if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
console.error("AP Tool Error: jsPDF or html2canvas library not loaded.");
alert("Error: PDF libraries failed to load. Please check console.");
return;
}
// 1. Render the full-size poster into the clone
ap_renderDashboard(ap_pdfRenderClone);
const { jsPDF } = window.jspdf;
try {
// 2. Render canvas from the clone
const canvas = await html2canvas(ap_pdfRenderClone, {
scale: 2, // High resolution
useCORS: true,
windowWidth: ap_pdfRenderClone.scrollWidth,
windowHeight: ap_pdfRenderClone.scrollHeight
});
const imgData = canvas.toDataURL('image/png');
const imgWidth = canvas.width;
const imgHeight = canvas.height;
// Use 'px' for units to match canvas
const pdf = new jsPDF({ orientation: 'p', unit: 'px', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
// Scale image height to fit pdf width
const ratio = imgWidth / imgHeight;
const scaledImgHeight = pdfWidth / ratio;
let heightLeft = scaledImgHeight;
let position = 0; // y-position of the image on the page
// 3. Add the first page
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, scaledImgHeight);
heightLeft -= pdfHeight;
// 4. Add subsequent pages if needed
while (heightLeft > 0) {
position -= pdfHeight; // Move the image's y-position up
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, scaledImgHeight);
heightLeft -= pdfHeight;
}
const safeName = (ap_data.courseName || 'syllabus').replace(/[^a-z0-9]/gi, '_').toLowerCase();
pdf.save(`${safeName}_syllabus.pdf`);
} catch (error) {
console.error("AP Tool Error: PDF generation failed.", error);
alert("An error occurred while generating the PDF. Please try again.");
}
}
/**
* Utility to escape HTML for display
*/
function ap_escapeHTML(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>"']/g, function(m) {
return {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[m];
});
}
// --- EVENT LISTENERS ---
// Tab link clicks
ap_tabLinks.forEach((link, index) => {
link.addEventListener('click', () => ap_switchTab(index));
});
// Next/Prev button clicks
if (ap_prevButton) {
ap_prevButton.addEventListener('click', () => {
if (ap_currentTab > 0) ap_switchTab(ap_currentTab - 1);
});
}
if (ap_nextButton) {
ap_nextButton.addEventListener('click', () => {
if (ap_currentTab < ap_tabLinks.length - 1) ap_switchTab(ap_currentTab + 1);
});
}
// PDF download
if (ap_downloadPdfButton) {
ap_downloadPdfButton.addEventListener('click', ap_downloadPDF);
}
// --- Config Tab "Add" Buttons ---
if (ap_addMaterialButton) {
ap_addMaterialButton.addEventListener('click', () => ap_addMaterialInput('', ap_materialCounter++));
}
if (ap_addGradingButton) {
ap_addGradingButton.addEventListener('click', () => ap_addGradingInput());
}
if (ap_addReqButton) {
ap_addReqButton.addEventListener('click', () => ap_addReqInput());
}
// --- Config Tab "Remove" Buttons (Event Delegation) ---
if (ap_configTab) {
ap_configTab.addEventListener('click', (e) => {
if (e.target.classList.contains('ap-remove-material')) {
e.target.closest('.flex').remove();
}
if (e.target.classList.contains('ap-remove-grading')) {
e.target.closest('.flex').remove();
}
if (e.target.classList.contains('ap-remove-req')) {
e.target.closest('.border').remove();
}
});
// Auto-update dashboard on config changes
ap_configTab.addEventListener('change', () => {
ap_updateDataFromConfig();
if (ap_currentTab === 0) {
ap_renderDashboard(ap_dashboardOutput);
}
});
}
// --- INITIALIZATION ---
ap_initSampleData();
ap_renderConfig();
ap_renderDashboard(ap_dashboardOutput);
// Set initial tab state
ap_tabPanes.forEach((pane, index) => {
pane.classList.toggle('hidden', index !== 0);
pane.classList.toggle('ap-active', index === 0);
});
});