diff --git a/html/index.html b/html/index.html index 70d793a..5c6247c 100644 --- a/html/index.html +++ b/html/index.html @@ -345,5 +345,6 @@ + diff --git a/html/index.js b/html/index.js index 9df3b30..7e35e52 100644 --- a/html/index.js +++ b/html/index.js @@ -54,6 +54,7 @@ worker.onmessage = function(evt) { tblResult.innerHTML = evt.data.content; divLogs2.innerHTML = '

Stage comments:

'; olStageComments = document.createElement('ol'); + olStageComments.id = 'olStageComments'; divLogs2.append(olStageComments); } else if (evt.data.type === 'describeCount') { @@ -218,215 +219,3 @@ if (document.getElementById('txtSeed').value === '') { let d = new Date(); document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()); } - -// Print logic - -async function printResult() { - olStageComments.childNodes.forEach(function(elLi) { elLi.classList.remove('highlight'); }); - - let printableWidth; // Printable width in CSS pixels - let paperSize = document.getElementById('selPaperSize').value; - if (paperSize === 'A4') { - printableWidth = (29.7 - 2) * 96 / 2.54; - } else if (paperSize === 'A3') { - printableWidth = (42.0 - 2) * 96 / 2.54; - } else if (paperSize === 'letter') { - printableWidth = (27.9 - 2) * 96 / 2.54; - } - printableWidth = Math.round(printableWidth); - - let wprint = window.open(''); - wprint.document.title = 'OpenTally Report'; - - // Add stylesheets - let numToLoad = 0; - let numLoaded = -1; - - function onLoadStylesheet() { - numLoaded++; - if (numLoaded == numToLoad) { - wprint.print(); - } - } - - for (let elCSSBase of document.querySelectorAll('head link')) { - numToLoad++; - let elCSS = wprint.document.createElement('link'); - elCSS.rel = elCSSBase.rel; - elCSS.type = elCSSBase.type; - if (elCSSBase.href.endsWith('?v=GITVERSION')) { - elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random()); - } else { - elCSS.href = elCSSBase.href; - } - - elCSS.onload = onLoadStylesheet; - wprint.document.head.appendChild(elCSS); - } - - // Configure printing - let elStyle = wprint.document.createElement('style'); - elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }'; - wprint.document.head.appendChild(elStyle); - - let elContainer = wprint.document.createElement('div'); - elContainer.id = 'printContainer'; - elContainer.style.width = printableWidth + 'px'; - wprint.document.body.appendChild(elContainer); - - // Copy result logs 1 - let divResultLogs1 = document.getElementById('resultLogs1'); - let divResultLogs2 = wprint.document.createElement('div'); - divResultLogs2.innerHTML = divResultLogs1.innerHTML; - elContainer.appendChild(divResultLogs2); - - // Parse table, accounting for colspan/rowspan - let elTrs1 = document.querySelector('#result').rows; - let rows = []; - for (let elTr1 of elTrs1) { - rows.push([]); - } - for (let r = 0; r < elTrs1.length; r++) { - for (let c = 0; c < elTrs1[r].cells.length; c++) { - let elTd1 = elTrs1[r].cells[c]; - rows[r].push(elTd1); - - let colspan = elTd1.getAttribute('colspan'); - if (colspan !== null) { - colspan = parseInt(colspan); - // Add ghost cells - for (let i = 1; i < colspan; i++) { - rows[r].push(null); - } - } - - let rowspan = elTd1.getAttribute('rowspan'); - // NB: Only works for rowspan in first column - if (rowspan !== null && c == 0) { - rowspan = parseInt(rowspan); - // Add ghost cells - for (let i = 1; i < rowspan; i++) { - rows[r + i].push(null); - } - } - } - } - - function copyColumn(c, elTrs2) { - let tdsAdded = []; - for (let r = 0; r < rows.length; r++) { - if (c < rows[r].length) { - let elTd1 = rows[r][c]; - if (elTd1 !== null) { - let elTd2 = wprint.document.createElement('td'); - elTd2.innerHTML = elTd1.innerHTML; - elTd2.className = elTd1.className; - if (elTd1.getAttribute('rowspan') !== null) { elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan')); } - if (elTd1.getAttribute('colspan') !== null) { elTd2.setAttribute('colspan', elTd1.getAttribute('colspan')); } - if (elTd1.getAttribute('style') !== null) { elTd2.setAttribute('style', elTd1.getAttribute('style')); } - elTrs2[r].appendChild(elTd2); - tdsAdded.push(elTd2); - } - } - } - return tdsAdded; - } - - async function copyTableColumns(startCol) { - let modelRow = document.getElementById('selReport').value === 'ballots_votes' ? rows[4] : rows[3]; - - // Add table - let elTable2 = wprint.document.createElement('table'); - elTable2.className = 'result'; - if (startCol > 1) { - elTable2.style.pageBreakBefore = 'always'; - } - elContainer.appendChild(elTable2); - - // Add rows - let elTrs2 = []; - for (let elTr1 of elTrs1) { - let elTr2 = wprint.document.createElement('tr'); - elTr2.className = elTr1.className; - elTrs2.push(elTr2); - elTable2.appendChild(elTr2); - } - - // Copy first column - copyColumn(0, elTrs2); - - // How many columns to copy? - let totalWidth = modelRow[0].clientWidth; - let endCol; - for (endCol = startCol; endCol < modelRow.length; ) { - // Check first column - if (totalWidth + modelRow[endCol].clientWidth > printableWidth) { - break; - } - - if ( - (document.getElementById('selReport').value === 'ballots_votes' && endCol + 1 < modelRow.length) || - (document.getElementById('selReport').value === 'votes_transposed' && endCol != 1 && endCol + 1 < modelRow.length) - ) { - // Check second column - if (totalWidth + modelRow[endCol].clientWidth + modelRow[endCol + 1].clientWidth > printableWidth) { - break; - } - } - - // Ok! - totalWidth += modelRow[endCol].clientWidth; - endCol++; - - if ( - (document.getElementById('selReport').value === 'ballots_votes' && endCol < modelRow.length) || - (document.getElementById('selReport').value === 'votes_transposed' && endCol != 2 && endCol + 1 < modelRow.length) - ) { - // Second column - totalWidth += modelRow[endCol].clientWidth; - endCol++; - } - } - - // Copy columns - let stages = []; - for (let c = startCol; c < endCol; c++) { - if (rows[0][c] !== null && rows[0][c].querySelector('a')) { - // Track stage headings copied - stages.push(parseInt(rows[0][c].querySelector('a').innerHTML)); - } - copyColumn(c, elTrs2); - } - - // Copy stage comments - elContainer.insertAdjacentHTML('beforeend', '

Stage comments:

'); - let olStageComments2 = wprint.document.createElement('ol'); - olStageComments2.start = stages[0]; - elContainer.append(olStageComments2); - for (let stage of stages) { - olStageComments2.insertAdjacentHTML('beforeend', olStageComments.children[stage-1].outerHTML); - } - - if (endCol < modelRow.length) { - // Start new table if columns remain - copyTableColumns(endCol); - } else { - // Copy winning candidates - elContainer.insertAdjacentHTML('beforeend', '

Count complete. The winning candidates are, in order of election:

'); - elContainer.insertAdjacentHTML('beforeend', divLogs2.lastElementChild.outerHTML); - } - } - - // Adjust results table to width - document.getElementById('resultsDiv').style.width = printableWidth + 'px'; - await new Promise(window.requestAnimationFrame); // Allow DOM to update - - // Copy table - await copyTableColumns(1); - - // Restore original view - document.getElementById('resultsDiv').style.width = 'auto'; - - // Trigger print when ready - onLoadStylesheet(); -} diff --git a/html/print.js b/html/print.js new file mode 100644 index 0000000..bba3cb9 --- /dev/null +++ b/html/print.js @@ -0,0 +1,229 @@ +/* OpenTally: Open-source election vote counting + * Copyright © 2021–2022 Lee Yingtong Li (RunasSudo) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +async function printResult() { + // Remove highlighted stage comment if any + for (let elLi of document.getElementById('olStageComments').children) { + elLi.classList.remove('highlight'); + } + + let printableWidth; // Printable width in CSS pixels + let paperSize = document.getElementById('selPaperSize').value; + if (paperSize === 'A4') { + printableWidth = (29.7 - 2) * 96 / 2.54; + } else if (paperSize === 'A3') { + printableWidth = (42.0 - 2) * 96 / 2.54; + } else if (paperSize === 'letter') { + printableWidth = (27.9 - 2) * 96 / 2.54; + } + printableWidth = Math.round(printableWidth); + + let wprint = window.open(''); + wprint.document.title = 'OpenTally Report'; + + // Add stylesheets + let numToLoad = 0; + let numLoaded = -1; + + function onLoadStylesheet() { + numLoaded++; + if (numLoaded == numToLoad) { + wprint.print(); + } + } + + for (let elCSSBase of document.querySelectorAll('head link')) { + numToLoad++; + let elCSS = wprint.document.createElement('link'); + elCSS.rel = elCSSBase.rel; + elCSS.type = elCSSBase.type; + if (elCSSBase.href.endsWith('?v=GITVERSION')) { + elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random()); + } else { + elCSS.href = elCSSBase.href; + } + + elCSS.onload = onLoadStylesheet; + wprint.document.head.appendChild(elCSS); + } + + // Configure printing + let elStyle = wprint.document.createElement('style'); + elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }'; + wprint.document.head.appendChild(elStyle); + + let elContainer = wprint.document.createElement('div'); + elContainer.id = 'printContainer'; + elContainer.style.width = printableWidth + 'px'; + wprint.document.body.appendChild(elContainer); + + // Copy result logs 1 + let divResultLogs1 = document.getElementById('resultLogs1'); + let divResultLogs2 = wprint.document.createElement('div'); + divResultLogs2.innerHTML = divResultLogs1.innerHTML; + elContainer.appendChild(divResultLogs2); + + // Parse table, accounting for colspan/rowspan + let elTrs1 = document.getElementById('result').rows; + let rows = []; + for (let elTr1 of elTrs1) { + rows.push([]); + } + for (let r = 0; r < elTrs1.length; r++) { + for (let c = 0; c < elTrs1[r].cells.length; c++) { + let elTd1 = elTrs1[r].cells[c]; + rows[r].push(elTd1); + + let colspan = elTd1.getAttribute('colspan'); + if (colspan !== null) { + colspan = parseInt(colspan); + // Add ghost cells + for (let i = 1; i < colspan; i++) { + rows[r].push(null); + } + } + + let rowspan = elTd1.getAttribute('rowspan'); + // NB: Only works for rowspan in first column + if (rowspan !== null && c == 0) { + rowspan = parseInt(rowspan); + // Add ghost cells + for (let i = 1; i < rowspan; i++) { + rows[r + i].push(null); + } + } + } + } + + function copyColumn(c, elTrs2) { + let tdsAdded = []; + for (let r = 0; r < rows.length; r++) { + if (c < rows[r].length) { + let elTd1 = rows[r][c]; + if (elTd1 !== null) { + let elTd2 = wprint.document.createElement('td'); + elTd2.innerHTML = elTd1.innerHTML; + elTd2.className = elTd1.className; + if (elTd1.getAttribute('rowspan') !== null) { elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan')); } + if (elTd1.getAttribute('colspan') !== null) { elTd2.setAttribute('colspan', elTd1.getAttribute('colspan')); } + if (elTd1.getAttribute('style') !== null) { elTd2.setAttribute('style', elTd1.getAttribute('style')); } + elTrs2[r].appendChild(elTd2); + tdsAdded.push(elTd2); + } + } + } + return tdsAdded; + } + + async function copyTableColumns(startCol) { + let modelRow = document.getElementById('selReport').value === 'ballots_votes' ? rows[4] : rows[3]; + + // Add table + let elTable2 = wprint.document.createElement('table'); + elTable2.className = 'result'; + if (startCol > 1) { + elTable2.style.pageBreakBefore = 'always'; + } + elContainer.appendChild(elTable2); + + // Add rows + let elTrs2 = []; + for (let elTr1 of elTrs1) { + let elTr2 = wprint.document.createElement('tr'); + elTr2.className = elTr1.className; + elTrs2.push(elTr2); + elTable2.appendChild(elTr2); + } + + // Copy first column + copyColumn(0, elTrs2); + + // How many columns to copy? + let totalWidth = modelRow[0].clientWidth; + let endCol; + for (endCol = startCol; endCol < modelRow.length; ) { + // Check first column + if (totalWidth + modelRow[endCol].clientWidth > printableWidth) { + break; + } + + if ( + (document.getElementById('selReport').value === 'ballots_votes' && endCol + 1 < modelRow.length) || + (document.getElementById('selReport').value === 'votes_transposed' && endCol != 1 && endCol + 1 < modelRow.length) + ) { + // Check second column + if (totalWidth + modelRow[endCol].clientWidth + modelRow[endCol + 1].clientWidth > printableWidth) { + break; + } + } + + // Ok! + totalWidth += modelRow[endCol].clientWidth; + endCol++; + + if ( + (document.getElementById('selReport').value === 'ballots_votes' && endCol < modelRow.length) || + (document.getElementById('selReport').value === 'votes_transposed' && endCol != 2 && endCol + 1 < modelRow.length) + ) { + // Second column + totalWidth += modelRow[endCol].clientWidth; + endCol++; + } + } + + // Copy columns + let stages = []; + for (let c = startCol; c < endCol; c++) { + if (rows[0][c] !== null && rows[0][c].querySelector('a')) { + // Track stage headings copied + stages.push(parseInt(rows[0][c].querySelector('a').innerHTML)); + } + copyColumn(c, elTrs2); + } + + // Copy stage comments + elContainer.insertAdjacentHTML('beforeend', '

Stage comments:

'); + let olStageComments2 = wprint.document.createElement('ol'); + olStageComments2.start = stages[0]; + elContainer.append(olStageComments2); + for (let stage of stages) { + olStageComments2.insertAdjacentHTML('beforeend', olStageComments.children[stage-1].outerHTML); + } + + if (endCol < modelRow.length) { + // Start new table if columns remain + copyTableColumns(endCol); + } else { + // Copy winning candidates + elContainer.insertAdjacentHTML('beforeend', '

Count complete. The winning candidates are, in order of election:

'); + elContainer.insertAdjacentHTML('beforeend', document.getElementById('resultLogs2').lastElementChild.outerHTML); + } + } + + // Adjust results table to width + document.getElementById('resultsDiv').style.width = printableWidth + 'px'; + await new Promise(window.requestAnimationFrame); // Allow DOM to update + + // Copy table + await copyTableColumns(1); + + // Restore original view + document.getElementById('resultsDiv').style.width = 'auto'; + + // Trigger print when ready + onLoadStylesheet(); +} diff --git a/src/cli/stv.rs b/src/cli/stv.rs index 7d9fa86..8293960 100644 --- a/src/cli/stv.rs +++ b/src/cli/stv.rs @@ -642,17 +642,19 @@ where for<'r> &'r N: ops::Neg { // HTML preamble, etc. + // TODO: Make this/URLs not hardcoded print!(r#" - OpenTally Results + OpenTally Report -
-
"#); +
+
+
"#); // Describe count println!(r#"{}
"#, stv::html::describe_count(filename, &election, &opts)); @@ -738,7 +740,7 @@ where // -------------------- // Print stage comments - println!(r#"

Stage comments:

    "#); + println!(r#"

    Stage comments:

      "#); for comment in stage_comments { println!("
    1. {}
    2. ", comment); } @@ -765,7 +767,23 @@ where } } - println!("
"); + println!(r#"
+
+ + +
+
Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.
+ + +"#); return Ok(()); }