diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3267d7b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.adxstudio/workspace.json diff --git a/bin/PDF export ADC.adc b/bin/PDF export ADC.adc deleted file mode 100644 index c3302e1..0000000 Binary files a/bin/PDF export ADC.adc and /dev/null differ diff --git a/bin/PDFExport.adc b/bin/PDFExport.adc new file mode 100644 index 0000000..62b8256 Binary files /dev/null and b/bin/PDFExport.adc differ diff --git a/changelog.md b/changelog.md index cd0e9c1..c63732a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,52 @@ Version 1.0.0 -- Initial version \ No newline at end of file +- Initial version +Version 1.0.1 +- Testing CSS and default.html changes to accomodate responses and vizualize them +Version 1.0.2 +- included "showallresponses" option in config to show all responses or only selected in export +Version 1.0.3 +- included Table/Loop support +Version 1.0.4 +- included "pdf_filename" option in config to allow user to name PDF naming +Version 1.1.4 +- changed PDFExport.js and PDFExport.min.js to accomodate user input for pdf filenames +Version 1.1.5 +- style changes for loops/grids, checkbox format made universal across question types +Version 1.2.0 +- included table/loop support for open/numeric/date loop items +Version 1.2.1 +- included fix for open/numeric loops being accidentally combined in the export +Version 1.2.2 +- included fix for table borders not rendering correctly +Version 1.2.3 +- changed "show_all_responses" and "show_answered" values (string from boolean) and names for better clarity +Version 1.3.0 +- incorporated "show_answered" logic only exporting questions with valid answers if enabled (non-loop) +Version 1.3.1 +- changed ADC syntax to CurrentADC.PropValue from CurrentADC.Var +Version 1.3.2 +- changed syntax in PDFEXPORT.js to target embedPage instead of CopyPage for PDF background +Version 1.3.3 +- changed default naming convention for pdf_filename property +Version 1.4.0 +- changed default.html script to acommodate loops if "show_answered" property is enabled +Version 1.4.1 +- changed css properties for export to be cleaner and more legible +Version 1.5.0 +- changed default.html export so table and question stylings resemble example template +Version 1.6.0 +- included intro section and css styling to more closely resemble example template +Version 1.6.1 +- changed issue in CSS styling for intro section +Version 1.6.2 +- included parameters for report title, intro and outro text, changed default.html variables to accomodate these values +Version 1.6.3 +- included parameters location of page numbers +Version 1.7.0 +- changed PDFExport.js to include pageNumberPosition object instead of hard coding X and Y coordinates for page numbers +- included parameters for copyright text and position +Version 1.7.1 +- fixed issue where showCopyright wasnt initialized correctly +Version 1.8.0 +- included syntax in PDFExport.js to accomodate copyright properties \ No newline at end of file diff --git a/config.xml b/config.xml index 9147287..d536ef3 100644 --- a/config.xml +++ b/config.xml @@ -5,13 +5,13 @@ version="2.2.0" askiaCompat="5.5.2"> - PDF export ADC + PDFExport a732d86d-7e2d-4944-a060-988189d715a6 1.0.0 - 2026-03-10 + 2026-03-16 - + @@ -56,12 +56,44 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -75,5 +107,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/PDFExport.qex b/example/PDFExport.qex index e33ed88..239a917 100644 Binary files a/example/PDFExport.qex and b/example/PDFExport.qex differ diff --git a/resources/dynamic/default.html b/resources/dynamic/default.html index 7fb9539..a8b15ad 100644 --- a/resources/dynamic/default.html +++ b/resources/dynamic/default.html @@ -1,40 +1,438 @@
- !! Dim arrQuestions = Survey.Questions Dim i Dim j + Dim k + Dim tempK + Dim loopIndex + Dim tempLoopIndex + Dim groupEndK + Dim questionTitle + Dim questionClass + Dim baseLoopTitle + Dim baseChildTitle + Dim currentLoopTitle + Dim currentChildTitle + Dim rowLabel Dim str = "" Dim filter = CurrentADC.PropValue("question_filter") + Dim showAllResponses = {%= (CurrentADC.PropValue("show_all_responses") = "1") %} + Dim showAnsweredOnly = {%= (CurrentADC.PropValue("show_answered") = "0") %} + Dim isAnswered + Dim shouldRenderQuestion + Dim inputVal + Dim tempInputVal + Dim rowIsAnswered + Dim loopHasAnsweredRows + + + Dim reportTitle = "{%= CurrentADC.PropValue("report_title") %}" + Dim introText1 = "{%= CurrentADC.PropValue("intro_line1") %}" + Dim introText2 =" {%= CurrentADC.PropValue("intro_line2") %}" + Dim closingText1 ="{%= CurrentADC.PropValue("closing_line1") %}" + Dim closingText2 = "{%= CurrentADC.PropValue("closing_line2") %}" + If (filter <> "") Then arrQuestions = arrQuestions.FilterByTag(filter) EndIf - For i = 1 To arrQuestions.Count + ' REPORT INTRO + str = str + "
" + str = str + "
" + reportTitle + "
" + str = str + "
" + + If introText1 <> "" Then + str = str + "

" + introText1 + "

" + EndIf + + If introText2 <> "" Then + str = str + "

" + introText2 + "

" + EndIf + + str = str + "
" + str = str + "
" + + i = 1 + While i <= arrQuestions.Count + + questionTitle = arrQuestions[i].ShortCaption + If questionTitle = "" Then + questionTitle = arrQuestions[i].LongCaption + End If + + questionClass = "askia-question-label" + If arrQuestions[i].Type = "chapter" Then + questionClass = "askia-chapter" + End If + + ' LOOP RENDERING + If arrQuestions[i].Type = "loop" Then + + If i < arrQuestions.Count And (arrQuestions[i + 1].Type = "single" Or arrQuestions[i + 1].Type = "multiple" Or arrQuestions[i + 1].Type = "open" Or arrQuestions[i + 1].Type = "numeric" Or arrQuestions[i + 1].Type = "date" Or arrQuestions[i + 1].Type = "datetime") Then + + baseLoopTitle = questionTitle + + baseChildTitle = arrQuestions[i + 1].ShortCaption + If baseChildTitle = "" Then + baseChildTitle = arrQuestions[i + 1].LongCaption + End If + + ' First pass: detect whether this loop group contains any answered rows + loopHasAnsweredRows = False + tempLoopIndex = 1 + tempK = i + + While tempK < arrQuestions.Count And arrQuestions[tempK].Type = "loop" + + currentLoopTitle = arrQuestions[tempK].ShortCaption + If currentLoopTitle = "" Then + currentLoopTitle = arrQuestions[tempK].LongCaption + End If + + currentChildTitle = arrQuestions[tempK + 1].ShortCaption + If currentChildTitle = "" Then + currentChildTitle = arrQuestions[tempK + 1].LongCaption + End If + + If currentLoopTitle <> baseLoopTitle Or currentChildTitle <> baseChildTitle Then + Exit While + End If + + rowIsAnswered = False + + If arrQuestions[tempK + 1].Type = "single" Or arrQuestions[tempK + 1].Type = "multiple" Then + For j = 1 To arrQuestions[tempK + 1].Responses.Count + If arrQuestions[tempK + 1].Responses[j].IsSelected Then + rowIsAnswered = True + Exit For + EndIf + Next + + ElseIf arrQuestions[tempK + 1].Type = "open" Or arrQuestions[tempK + 1].Type = "numeric" Or arrQuestions[tempK + 1].Type = "date" Or arrQuestions[tempK + 1].Type = "datetime" Then + tempInputVal = arrQuestions[tempK + 1].InputValue() + If tempInputVal <> "" Then + If tempInputVal.Trim() <> "" Then + rowIsAnswered = True + EndIf + EndIf + EndIf + + If rowIsAnswered = True Then + loopHasAnsweredRows = True + EndIf + + tempLoopIndex = tempLoopIndex + 1 + tempK = tempK + 2 + + EndWhile + + groupEndK = tempK + shouldRenderQuestion = True + + If showAnsweredOnly = True Then + If loopHasAnsweredRows = False Then + shouldRenderQuestion = False + EndIf + EndIf + + If shouldRenderQuestion = True Then + + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + + rowLabel = arrQuestions[k].Responses[loopIndex].Caption + If rowLabel = "" Then + rowLabel = "Item " + loopIndex + End If + + str = str + "" + str = str + "" + str = str + "" + + EndIf + + loopIndex = loopIndex + 1 + k = k + 2 + + EndWhile + + str = str + "

" + questionTitle + "

" + str = str + "
Your answer:
" + + ' CLOSED LOOP CHILD: render as grid + If arrQuestions[i + 1].Type = "single" Or arrQuestions[i + 1].Type = "multiple" Then + + str = str + "" + str = str + "" + str = str + "" + + For j = 1 To arrQuestions[i + 1].Responses.Count + str = str + "" + Next + + str = str + "" + str = str + "" + + loopIndex = 1 + k = i + + While k < arrQuestions.Count And arrQuestions[k].Type = "loop" + + currentLoopTitle = arrQuestions[k].ShortCaption + If currentLoopTitle = "" Then + currentLoopTitle = arrQuestions[k].LongCaption + End If + + currentChildTitle = arrQuestions[k + 1].ShortCaption + If currentChildTitle = "" Then + currentChildTitle = arrQuestions[k + 1].LongCaption + End If + + If currentLoopTitle <> baseLoopTitle Or currentChildTitle <> baseChildTitle Then + Exit While + End If + + rowIsAnswered = False + For j = 1 To arrQuestions[k + 1].Responses.Count + If arrQuestions[k + 1].Responses[j].IsSelected Then + rowIsAnswered = True + Exit For + EndIf + Next + + shouldRenderQuestion = True + If showAnsweredOnly = True Then + If rowIsAnswered = False Then + shouldRenderQuestion = False + EndIf + EndIf + + If shouldRenderQuestion = True Then + + str = str + "" + + rowLabel = arrQuestions[k].Responses[loopIndex].Caption + If rowLabel = "" Then + rowLabel = "Item " + loopIndex + End If + + str = str + "" + + For j = 1 To arrQuestions[k + 1].Responses.Count + str = str + "" + Next + + str = str + "" + + EndIf + + loopIndex = loopIndex + 1 + k = k + 2 + + EndWhile + + str = str + "
Item" + arrQuestions[i + 1].Responses[j].Caption + "
" + rowLabel + "" + + If arrQuestions[k + 1].Responses[j].IsSelected Then + str = str + "" + ElseIf showAllResponses Then + str = str + "" + Else + str = str + " " + EndIf + + str = str + "
" - str = str + "" - str = str + "" - str = str + "" - str = str + "" + ' OPEN / NUMERIC / DATE / DATETIME LOOP CHILD: render as 2-column table + ElseIf arrQuestions[i + 1].Type = "open" Or arrQuestions[i + 1].Type = "numeric" Or arrQuestions[i + 1].Type = "date" Or arrQuestions[i + 1].Type = "datetime" Then - For j = 1 To arrQuestions[i].Responses.Count + str = str + "

" + arrQuestions[i].shortcaption + "

" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" - str = str + "" - str = str + "" - str = str + "" + loopIndex = 1 + k = i - Next + While k < arrQuestions.Count And arrQuestions[k].Type = "loop" - str = str + "
ItemValue
" - Next - return str + currentLoopTitle = arrQuestions[k].ShortCaption + If currentLoopTitle = "" Then + currentLoopTitle = arrQuestions[k].LongCaption + End If + + currentChildTitle = arrQuestions[k + 1].ShortCaption + If currentChildTitle = "" Then + currentChildTitle = arrQuestions[k + 1].LongCaption + End If + + If currentLoopTitle <> baseLoopTitle Or currentChildTitle <> baseChildTitle Then + Exit While + End If + + rowIsAnswered = False + inputVal = arrQuestions[k + 1].InputValue() + If inputVal <> "" Then + If inputVal.Trim() <> "" Then + rowIsAnswered = True + EndIf + EndIf + + shouldRenderQuestion = True + If showAnsweredOnly = True Then + If rowIsAnswered = False Then + shouldRenderQuestion = False + EndIf + EndIf + + If shouldRenderQuestion = True Then + + str = str + "
" + rowLabel + "" + arrQuestions[k + 1].InputValue() + "
" + + EndIf + + str = str + "" + str = str + "" + str = str + "" + + i = k + + Else + + i = groupEndK + + EndIf + + Else + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "

" + questionTitle + "

" + + i = i + 1 + EndIf + + Else + + isAnswered = False + shouldRenderQuestion = True + + If arrQuestions[i].Type = "single" Or arrQuestions[i].Type = "multiple" Then + For j = 1 To arrQuestions[i].Responses.Count + If arrQuestions[i].Responses[j].IsSelected Then + isAnswered = True + Exit For + EndIf + Next + + ElseIf arrQuestions[i].Type = "open" Or arrQuestions[i].Type = "numeric" Or arrQuestions[i].Type = "date" Or arrQuestions[i].Type = "datetime" Then + inputVal = arrQuestions[i].InputValue() + If inputVal <> "" Then + If inputVal.Trim() <> "" Then + isAnswered = True + EndIf + EndIf + + Else + isAnswered = True + EndIf + + If showAnsweredOnly = True And isAnswered = False Then + shouldRenderQuestion = False + Else + shouldRenderQuestion = True + EndIf + + If shouldRenderQuestion = True Then + + str = str + "" + str = str + "" + str = str + "" + str = str + "" + str = str + "" + + If arrQuestions[i].Type = "single" Or arrQuestions[i].Type = "multiple" Or arrQuestions[i].Type = "open" Or arrQuestions[i].Type = "numeric" Or arrQuestions[i].Type = "date" Or arrQuestions[i].Type = "datetime" Then + str = str + "" + str = str + "" + str = str + "" + EndIf + + str = str + "

" + questionTitle + "

" + str = str + "
Your answer:
" + EndIf + + ' Closed questions + If arrQuestions[i].Type = "single" Or arrQuestions[i].Type = "multiple" Then + + For j = 1 To arrQuestions[i].Responses.Count + + If showAllResponses Or arrQuestions[i].Responses[j].IsSelected Then + + str = str + "
" + + If arrQuestions[i].Responses[j].IsSelected Then + str = str + "" + Else + str = str + "" + EndIf + + str = str + "" + str = str + "
" + + EndIf + + Next + + ' Open / numeric / date / datetime questions + ElseIf arrQuestions[i].Type = "open" Or arrQuestions[i].Type = "numeric" Or arrQuestions[i].Type = "date" Or arrQuestions[i].Type = "datetime" Then + + str = str + "
" + arrQuestions[i].InputValue() + "
" + + EndIf + + If arrQuestions[i].Type = "single" Or arrQuestions[i].Type = "multiple" Or arrQuestions[i].Type = "open" Or arrQuestions[i].Type = "numeric" Or arrQuestions[i].Type = "date" Or arrQuestions[i].Type = "datetime" Then + str = str + "
" + + EndIf + + i = i + 1 + + EndIf + + EndWhile + + ' REPORT CLOSING + If closingText1 <> "" Or closingText2 <> "" Then + str = str + "
" + + If closingText1 <> "" Then + str = str + "

" + closingText1 + "

" + EndIf + + If closingText2 <> "" Then + str = str + "

" + closingText2 + "

" + EndIf + + str = str + "
" + EndIf + + Return str !!
Export to PDF
-
- + \ No newline at end of file diff --git a/resources/dynamic/init.js b/resources/dynamic/init.js index ab5dcb8..862a45f 100644 --- a/resources/dynamic/init.js +++ b/resources/dynamic/init.js @@ -1,13 +1,18 @@ var PDFExport = new PDFExport({ id: {%= CurrentADC.InstanceId %}, - pdfBefore: '{%= CurrentADC.Var("pages_before") %}', - pdfAfter: '{%= CurrentADC.Var("pages_after") %}', - pdfBack: '{%= CurrentADC.Var("page_background") %}', + pdfBefore: '{%= CurrentADC.PropValue("pages_before") %}', + pdfAfter: '{%= CurrentADC.PropValue("pages_after") %}', + pdfBack: '{%= CurrentADC.PropValue("page_background") %}', marginTop: '{%= CurrentADC.PropValue("margin_top")%}', marginRight: '{%= CurrentADC.PropValue("margin_right")%}', marginBottom: '{%= CurrentADC.PropValue("margin_bottom")%}', marginLeft: '{%= CurrentADC.PropValue("margin_left")%}', - displayPageNumber: {%= CurrentADC.PropValue("display_page_number") %} + displayPageNumber: {%= CurrentADC.PropValue("display_page_number") %}, + pdfFilename: "{%= CurrentADC.PropValue("pdf_filename") %}", + pageNumberPosition: '{%= CurrentADC.PropValue("page_number_position") %}', + showCopyright: '{%= CurrentADC.PropValue("show_copyright") %}', + copyrightText: "{%= CurrentADC.PropValue("copyright_line") %}", + copyrightPosition: '{%= CurrentADC.PropValue("copyright_position") %}' }); PDFExport.init(); diff --git a/resources/dynamic/styles.css b/resources/dynamic/styles.css index 57dc306..23a756b 100644 --- a/resources/dynamic/styles.css +++ b/resources/dynamic/styles.css @@ -1,19 +1,282 @@ .pdf_questions_wrapper { - width:0; - height:0; - overflow:hidden; + width: 0; + height: 0; + overflow: hidden; } .pdf_questions { - width:800px; - min-width:800px; + width: 800px; + min-width: 800px; + font-family: Arial, Helvetica, sans-serif; + color: #1f1f1f; + background: #ffffff; } -.pdf_question { - font-size:14px; - border-bottom:1px solid #ccc; - padding-bottom:10px; +/* GENERAL */ +table { + border-collapse: collapse; + width: 100%; } +.pdf_export { + display: inline-block; + margin-top: 24px; + padding: 12px 22px; + background-color: #e84f3d; + color: #ffffff; + font-size: 14px; + font-weight: 600; + text-align: center; + border-radius: 4px; + border: none; + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.1s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.15); +} + +.pdf_export:hover { + background-color: #cf402f; +} + +.pdf_export:active { + transform: translateY(1px); + box-shadow: 0 1px 2px rgba(0,0,0,0.2); +} + +/* QUESTION BLOCK */ +.pdf-table { + margin: 0 0 38px 0; + border: none; + border-radius: 0; + overflow: visible; + background: transparent; +} + +.pdf-table td, +.pdf-table th { + border: none; + padding: 0; +} + +/* QUESTION TITLE BAR */ +.askia-question-label, +.askia-chapter { + position: relative; + padding-left: 24px !important; +} + +.askia-question-label::before, +.askia-chapter::before { + content: ""; + position: absolute; + left: 0; + top: 0; + width: 8px; + height: 48px; + background: #e84f3d; +} + +.askia-question-label h1, +.askia-chapter h1 { + margin: 0; + height: 48px; + line-height: 48px; + padding: 0 16px; + background: #263d50; + color: #ffffff; + font-size: 18px; + font-weight: 700; + border: none; + margin-bottom: 12px; +} + +/* CHAPTERS */ +.askia-chapter { + margin-top: 28px; +} + +.askia-chapter h1 { + background: #263d50; +} + +/* BODY / ANSWER AREA */ +.askia-question-body { + padding: 22px 0 0 0; + +} + +.askia-answer-intro { + margin: 0 0 12px 0; + font-size: 16px; + font-weight: 400; + color: #1f1f1f; +} + +.askia-response { + padding: 6px 0; + font-size: 15px; + line-height: 1.5; + border: none; +} + +.askia-response:last-child { + border-bottom: none; +} + +.askia-response-label { + display: inline-block; + vertical-align: middle; + line-height: 1.45; + color: #1f1f1f; +} + +/* CHECKBOX STYLE */ +.askia-response input[type="checkbox"] { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.askia-checkbox { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #e84f3d; + border-radius: 2px; + margin-right: 10px; + vertical-align: middle; + position: relative; + box-sizing: border-box; + top: -1px; +} + +.askia-checkbox.checked::after { + content: ""; + position: absolute; + left: 4px; + top: 0px; + width: 4px; + height: 8px; + border: solid #e84f3d; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* OPEN / NUMERIC / DATE ANSWERS */ +.askia-open-answer { + margin-top: 2px; + font-size: 15px; + line-height: 1.5; + color: #1f1f1f; +} + +/* GRID QUESTIONS / LOOP TABLES */ +.askia-grid { + width: 100%; + border-collapse: collapse; + margin-top: 10px; + table-layout: fixed; +} + +.askia-grid thead th { + background: #263d50; + color: #ffffff; + font-weight: 700; + text-align: center; + padding: 10px 8px; + border: 1px solid #d9e0e6; + font-size: 14px; +} + +.askia-grid-corner { + text-align: left !important; + width: 30%; +} + +.askia-grid tbody td { + border: 1px solid #d9e0e6; + padding: 10px 8px; + font-size: 14px; + vertical-align: middle; + background: #ffffff; +} + +.askia-grid-row-header { + font-weight: 700; + text-align: left; + background: #f6f8fa !important; + width: 30%; +} + +.askia-grid-cell { + text-align: center; +} + +.askia-grid-value { + display: inline-block; + color: #1f1f1f; + background: transparent; + border-radius: 0; + padding: 0; +} + +/* OPTIONAL REPORT SECTIONS */ +.report-intro-text { + font-size: 16px; + line-height: 1.65; + margin-bottom: 18px; + color: #1f1f1f; +} + +.page-break { + page-break-after: always; + break-after: page; +} + +.pdf-report-intro { + margin: 0 0 36px 0; +} + +.pdf-report-title { + background: #e84f3d; + color: #ffffff; + text-align: center; + font-size: 18px; + font-weight: 700; + text-transform: uppercase; + padding: 10px 18px; + margin-bottom: 0; +} + +.pdf-report-intro-body { + + padding: 28px 28px 22px 28px; + background: #ffffff; +} + +.pdf-report-intro-text { + margin: 0 0 18px 0; + font-size: 16px; + line-height: 1.65; + color: #1f1f1f; +} + +.pdf-report-intro-text:last-child { + margin-bottom: 0; +} + +.pdf-report-closing { + margin-top: 34px; +} + +.pdf-report-closing-text { + margin: 0 0 14px 0; + font-size: 16px; + line-height: 1.65; + color: #1f1f1f; +} -.page-break { page-break-after: always; break-after: page; } \ No newline at end of file +.pdf-report-closing-text:last-child { + margin-bottom: 0; +} \ No newline at end of file diff --git a/resources/static/PDFExport.js b/resources/static/PDFExport.js index 5a99a18..8885b38 100644 --- a/resources/static/PDFExport.js +++ b/resources/static/PDFExport.js @@ -1,30 +1,29 @@ + var PDFExport = (function () { async function getPdfData(input) { if (!input) return null; if (typeof input === 'string') { - // Fetch from URL const res = await fetch(input); if (!res.ok) throw new Error('Failed to fetch PDF: ' + input); return new Uint8Array(await res.arrayBuffer()); } else { - // File upload return new Uint8Array(await input.arrayBuffer()); } } function getPagesFromHtml(element) { - // Automatically paginate content based on height, respecting manual .page-break divs - // Accept optional width/height for content area const DEFAULT_PAGE_HEIGHT_PX = 1123; const DEFAULT_PAGE_WIDTH_PX = 794; + let pageWidthPx = arguments[1] || DEFAULT_PAGE_WIDTH_PX; let pageHeightPx = arguments[2] || DEFAULT_PAGE_HEIGHT_PX; + const pages = []; let current = document.createElement('div'); current.style.width = pageWidthPx + 'px'; current.style.boxSizing = 'border-box'; - // Create a hidden container for measuring + const measureContainer = document.createElement('div'); measureContainer.style.position = 'fixed'; measureContainer.style.left = '-99999px'; @@ -47,167 +46,286 @@ var PDFExport = (function () { flushPage(); continue; } + const clone = node.cloneNode(true); current.appendChild(clone); - // Measure height + measureContainer.innerHTML = ''; measureContainer.appendChild(current.cloneNode(true)); + if (measureContainer.scrollHeight > pageHeightPx) { - // Remove last node, flush page, start new page with this node current.removeChild(clone); flushPage(); current.appendChild(clone); } } + flushPage(); document.body.removeChild(measureContainer); return pages; } - async function renderHtmlToCanvas(pageDiv, width, height) { - // Set fixed size for consistent rendering pageDiv.style.width = width + 'px'; pageDiv.style.height = height + 'px'; - // Hide and append to DOM pageDiv.style.position = 'fixed'; pageDiv.style.left = '-99999px'; pageDiv.style.top = '0'; pageDiv.style.zIndex = '-1'; document.body.appendChild(pageDiv); + let res; try { - res = await html2canvas(pageDiv, { width, height, backgroundColor: null, scale: 2 }); + res = await html2canvas(pageDiv, { + width, + height, + backgroundColor: null, + scale: 2 + }); } catch (e) { console.error('Error rendering HTML to canvas:', e); throw e; } finally { - // Remove from DOM after rendering if (pageDiv.parentNode) pageDiv.parentNode.removeChild(pageDiv); } + return res; } + function estimateTextWidth(text, fontSize) { + return text.length * (fontSize * 0.52); + } + + function getPageNumberCoordinates(pageWidth, pageHeight, margins, position, textWidth, fontSize) { + const m = margins; + const safeBottomY = Math.max(12, m.bottom / 2); + const safeTopY = pageHeight - Math.max(fontSize + 4, m.top / 2); + const safeLeftX = m.left; + const safeCenterX = (pageWidth - textWidth) / 2; + const safeRightX = pageWidth - m.right - textWidth; - async function generatePdf({ introPdf, outroPdf, templatePdf, htmlElement, margins, displayPageNumber }) { + switch ((position || 'bottom-right').toLowerCase()) { + case 'bottom-left': + return { x: safeLeftX, y: safeBottomY }; + + case 'bottom-center': + return { x: safeCenterX, y: safeBottomY }; + + case 'bottom-right': + return { x: safeRightX, y: safeBottomY }; + + case 'top-left': + return { x: safeLeftX, y: safeTopY }; + + case 'top-center': + return { x: safeCenterX, y: safeTopY }; + + case 'top-right': + return { x: safeRightX, y: safeTopY }; + + default: + return { x: safeRightX, y: safeBottomY }; + } + } + + async function generatePdf({ + introPdf, + outroPdf, + templatePdf, + htmlElement, + margins, + displayPageNumber, + pageNumberPosition, + showCopyright, + copyrightText, + copyrightPosition, + pdfFilename + }) { const PDFLib = window.PDFLib; - const pageWidth = 595.28; // A4 width in pt - const pageHeight = 841.89; // A4 height in pt - // Default margins in pt (1 inch = 72pt) + const pageWidth = 595.28; + const pageHeight = 841.89; + const defaultMargins = { top: 40, right: 40, bottom: 40, left: 40 }; const m = margins ? Object.assign({}, defaultMargins, margins) : defaultMargins; - // 1. Prepare intro, outro, template PDFs let introDoc, outroDoc, templateDoc; if (introPdf) introDoc = await PDFLib.PDFDocument.load(await getPdfData(introPdf)); if (outroPdf) outroDoc = await PDFLib.PDFDocument.load(await getPdfData(outroPdf)); if (templatePdf) templateDoc = await PDFLib.PDFDocument.load(await getPdfData(templatePdf)); - // 2. Prepare content pages from HTML - // Calculate available content area in px (A4 at 96dpi) const PX_PER_PT = 96 / 72; const contentWidthPx = Math.round((pageWidth - m.left - m.right) * PX_PER_PT); const contentHeightPx = Math.round((pageHeight - m.top - m.bottom) * PX_PER_PT); + const contentPages = getPagesFromHtml(htmlElement, contentWidthPx, contentHeightPx); const canvases = []; + for (const pageDiv of contentPages) { const canvas = await renderHtmlToCanvas(pageDiv, contentWidthPx, contentHeightPx); canvases.push(canvas); } - // 3. Create new PDF and merge intro const pdfDoc = await PDFLib.PDFDocument.create(); + if (introDoc) { const introPages = await pdfDoc.copyPages(introDoc, introDoc.getPageIndices()); - introPages.forEach(p => pdfDoc.addPage(p)); + introPages.forEach(function (p) { + pdfDoc.addPage(p); + }); } - // 4. Add content pages with template background and page numbers for (let i = 0; i < canvases.length; ++i) { let page; + if (templateDoc) { - const [tpl] = await pdfDoc.copyPages(templateDoc, [0]); + const tpl = await pdfDoc.embedPage(templateDoc.getPage(0)); page = pdfDoc.addPage([pageWidth, pageHeight]); - page.drawPage(tpl); + page.drawPage(tpl, { + x: 0, + y: 0, + width: pageWidth, + height: pageHeight + }); } else { page = pdfDoc.addPage([pageWidth, pageHeight]); } - // Draw HTML as image inside margins + const imgData = canvases[i].toDataURL('image/png'); const png = await pdfDoc.embedPng(imgData); const imgWidth = contentWidthPx / PX_PER_PT; const imgHeight = contentHeightPx / PX_PER_PT; + page.drawImage(png, { x: m.left, y: pageHeight - m.top - imgHeight, width: imgWidth, - height: imgHeight, + height: imgHeight }); - // Add page number inside bottom margin if (displayPageNumber) { - page.drawText(`Page ${i + 1} of ${canvases.length}`, { - x: pageWidth - m.right - 120, - y: m.bottom / 2, - size: 12, - color: PDFLib.rgb(0.2, 0.2, 0.2), + const pageNumberText = `Page ${i + 1} of ${canvases.length}`; + const fontSize = 12; + const textWidth = estimateTextWidth(pageNumberText, fontSize); + + const coords = getPageNumberCoordinates( + pageWidth, + pageHeight, + m, + pageNumberPosition, + textWidth, + fontSize + ); + + page.drawText(pageNumberText, { + x: coords.x, + y: coords.y, + size: fontSize, + color: PDFLib.rgb(0.2, 0.2, 0.2) + }); + } + + + if (showCopyright && copyrightText) { + + const fontSize = 10; + + const text = copyrightText; + + const textWidth = estimateTextWidth(text, fontSize); + + const coords = getPageNumberCoordinates( + pageWidth, + pageHeight, + m, + copyrightPosition, + textWidth, + fontSize + ); + + page.drawText(text, { + x: coords.x, + y: coords.y, + size: fontSize, + color: PDFLib.rgb(0.4, 0.4, 0.4) // slightly lighter than page number }); } } - // 5. Merge outro if (outroDoc) { const outroPages = await pdfDoc.copyPages(outroDoc, outroDoc.getPageIndices()); - outroPages.forEach(p => pdfDoc.addPage(p)); + outroPages.forEach(function (p) { + pdfDoc.addPage(p); + }); } - // 6. Download const pdfBytes = await pdfDoc.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; - a.download = 'generated.pdf'; + a.download = pdfFilename || 'export.pdf'; a.click(); + URL.revokeObjectURL(url); } function PDFExport(options) { - this.options = options; + this.options = Object.assign({ + pdfFilename: 'export.pdf', + pageNumberPosition: 'bottom-right', + showCopyright: false, + copyrightText: '', + copyrightPosition: 'bottom-left' + + }, options || {}); - // Init open question transcriptions PDFExport.prototype.init = function () { const that = this; const container = document.getElementById("adc_" + this.options.id); const exportBtn = container.querySelector(".pdf_export"); + const introPdf = this.options.pdfBefore; const outroPdf = this.options.pdfAfter; const templatePdf = this.options.pdfBack; const displayPageNumber = this.options.displayPageNumber; + const pageNumberPosition = this.options.pageNumberPosition || 'bottom-right'; const margins = { top: parseFloat(this.options.marginTop) || 0, right: parseFloat(this.options.marginRight) || 0, bottom: parseFloat(this.options.marginBottom) || 0, - left: parseFloat(this.options.marginLeft) || 0, + left: parseFloat(this.options.marginLeft) || 0 }; exportBtn.addEventListener('click', async function () { - try { const htmlElement = container.querySelector(".pdf_questions"); + if (!htmlElement) { - alert('HTML element not found for selector: ' + selector); + alert('HTML element not found for selector: .pdf_questions'); return; } - await generatePdf({ introPdf, outroPdf, templatePdf, htmlElement, margins, displayPageNumber }); + + await generatePdf({ + introPdf, + outroPdf, + templatePdf, + htmlElement, + margins, + displayPageNumber, + pageNumberPosition, + showCopyright: that.options.showCopyright, + copyrightText: that.options.copyrightText, + copyrightPosition: that.options.copyrightPosition, + pdfFilename: that.options.pdfFilename + }); } catch (e) { alert('Error generating PDF: ' + e.message); } - }); - } + }; } return PDFExport; -})(); +})(); \ No newline at end of file diff --git a/resources/static/PDFExport.min.js b/resources/static/PDFExport.min.js index 0e804a6..5547bd9 100644 --- a/resources/static/PDFExport.min.js +++ b/resources/static/PDFExport.min.js @@ -1 +1 @@ -var PDFExport=function(){async function D(t){if(!t)return null;if("string"!=typeof t)return new Uint8Array(await t.arrayBuffer());var e=await fetch(t);if(e.ok)return new Uint8Array(await e.arrayBuffer());throw new Error("Failed to fetch PDF: "+t)}async function d({introPdf:t,outroPdf:e,templatePdf:o,htmlElement:a,margins:i,displayPageNumber:n}){var r=window.PDFLib,d=595.28,l=841.89,s={top:40,right:40,bottom:40,left:40},c=i?Object.assign({},s,i):s;let p,h,f;t&&(p=await r.PDFDocument.load(await D(t))),e&&(h=await r.PDFDocument.load(await D(e))),o&&(f=await r.PDFDocument.load(await D(o)));var g=96/72,m=Math.round((d-c.left-c.right)*g),u=Math.round((l-c.top-c.bottom)*g),y=[];for(const E of function(t,e,o){let a=e||794;var i=o||1123;const n=[];let r=document.createElement("div");r.style.width=a+"px",r.style.boxSizing="border-box";var d,l=document.createElement("div");function s(){r.childNodes.length&&(n.push(r),(r=document.createElement("div")).style.width=a+"px",r.style.boxSizing="border-box")}l.style.position="fixed",l.style.left="-99999px",l.style.top="0",l.style.width=a+"px",l.style.visibility="hidden",document.body.appendChild(l);for(const c of t.childNodes)1===c.nodeType&&c.classList.contains("page-break")?s():(d=c.cloneNode(!0),r.appendChild(d),l.innerHTML="",l.appendChild(r.cloneNode(!0)),l.scrollHeight>i&&(r.removeChild(d),s(),r.appendChild(d)));return s(),document.body.removeChild(l),n}(a,m,u)){var w=await async function(t,e,o){t.style.width=e+"px",t.style.height=o+"px",t.style.position="fixed",t.style.left="-99999px",t.style.top="0",t.style.zIndex="-1",document.body.appendChild(t);let a;try{a=await html2canvas(t,{width:e,height:o,backgroundColor:null,scale:2})}catch(t){throw console.error("Error rendering HTML to canvas:",t),t}finally{t.parentNode&&t.parentNode.removeChild(t)}return a}(E,m,u);y.push(w)}const P=await r.PDFDocument.create();p&&(await P.copyPages(p,p.getPageIndices())).forEach(t=>P.addPage(t));for(let e=0;eP.addPage(t));i=await P.save(),s=new Blob([i],{type:"application/pdf"}),t=URL.createObjectURL(s),e=document.createElement("a");e.href=t,e.download="generated.pdf",e.click(),URL.revokeObjectURL(t)}function e(t){this.options=t,e.prototype.init=function(){const e=document.getElementById("adc_"+this.options.id);var t=e.querySelector(".pdf_export");const o=this.options.pdfBefore,a=this.options.pdfAfter,i=this.options.pdfBack,n=this.options.displayPageNumber,r={top:parseFloat(this.options.marginTop)||0,right:parseFloat(this.options.marginRight)||0,bottom:parseFloat(this.options.marginBottom)||0,left:parseFloat(this.options.marginLeft)||0};t.addEventListener("click",async function(){try{var t=e.querySelector(".pdf_questions");t?await d({introPdf:o,outroPdf:a,templatePdf:i,htmlElement:t,margins:r,displayPageNumber:n}):alert("HTML element not found for selector: "+selector)}catch(t){alert("Error generating PDF: "+t.message)}})}}return e}(); \ No newline at end of file +var PDFExport=function(){async function t(t){if(!t)return null;if("string"!=typeof t)return new Uint8Array(await t.arrayBuffer());{let e=await fetch(t);if(!e.ok)throw Error("Failed to fetch PDF: "+t);return new Uint8Array(await e.arrayBuffer())}}async function e(t,e,o){t.style.width=e+"px",t.style.height=o+"px",t.style.position="fixed",t.style.left="-99999px",t.style.top="0",t.style.zIndex="-1",document.body.appendChild(t);let i;try{i=await html2canvas(t,{width:e,height:o,backgroundColor:null,scale:2})}catch(n){throw console.error("Error rendering HTML to canvas:",n),n}finally{t.parentNode&&t.parentNode.removeChild(t)}return i}function o(t,e){return t.length*(.52*e)}function i(t,e,o,i,n,a){let r=o,l=Math.max(12,r.bottom/2),s=e-Math.max(a+4,r.top/2),p=r.left,d=(t-n)/2,c=t-r.right-n;switch((i||"bottom-right").toLowerCase()){case"bottom-left":return{x:p,y:l};case"bottom-center":return{x:d,y:l};case"bottom-right":default:return{x:c,y:l};case"top-left":return{x:p,y:s};case"top-center":return{x:d,y:s};case"top-right":return{x:c,y:s}}}async function n({introPdf:n,outroPdf:a,templatePdf:r,htmlElement:l,margins:s,displayPageNumber:p,pageNumberPosition:d,showCopyright:c,copyrightText:h,copyrightPosition:f,pdfFilename:g}){let y=window.PDFLib,u={top:40,right:40,bottom:40,left:40},m=s?Object.assign({},u,s):u,w,x,b;n&&(w=await y.PDFDocument.load(await t(n))),a&&(x=await y.PDFDocument.load(await t(a))),r&&(b=await y.PDFDocument.load(await t(r)));let P=96/72,$=Math.round((595.28-m.left-m.right)*P),_=Math.round((841.89-m.top-m.bottom)*P),C=function t(e){let o=arguments[1]||794,i=arguments[2]||1123,n=[],a=document.createElement("div");a.style.width=o+"px",a.style.boxSizing="border-box";let r=document.createElement("div");function l(){a.childNodes.length&&(n.push(a),(a=document.createElement("div")).style.width=o+"px",a.style.boxSizing="border-box")}for(let s of(r.style.position="fixed",r.style.left="-99999px",r.style.top="0",r.style.width=o+"px",r.style.visibility="hidden",document.body.appendChild(r),e.childNodes)){if(1===s.nodeType&&s.classList.contains("page-break")){l();continue}let p=s.cloneNode(!0);a.appendChild(p),r.innerHTML="",r.appendChild(a.cloneNode(!0)),r.scrollHeight>i&&(a.removeChild(p),l(),a.appendChild(p))}return l(),document.body.removeChild(r),n}(l,$,_),D=[];for(let v of C){let F=await e(v,$,_);D.push(F)}let E=await y.PDFDocument.create();if(w){let L=await E.copyPages(w,w.getPageIndices());L.forEach(function(t){E.addPage(t)})}for(let T=0;T