Basic ballot input prototype

This commit is contained in:
RunasSudo 2021-01-25 19:47:18 +11:00
parent 4205d9b262
commit b6f3f56280
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 269 additions and 2 deletions

60
html/blt/index.html Normal file
View File

@ -0,0 +1,60 @@
<!--
pyRCV2: Preferential vote counting
Copyright © 2020–2021 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 <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>pyRCV2 Ballot Input Tool</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="../main.css?v=GITVERSION">
</head>
<body>
<div class="menudiv">
<input type="file" id="inpFile" style="display: none;" onchange="changeInpFile()">
<button onclick="clickOpenJSON()">Open JSON</button>
<button onclick="clickSaveJSON()">Save JSON</button>
<!--<button>Import BLT</button>
<button>Export BLT</button>-->
<!--GITREV-->
<a href="https://yingtongli.me/blog/2020/12/24/pyrcv2.html">Information and instructions</a>
</div>
<div id="bltMain">
<select id="selBallots" size="20" onchange="changeBallot()">
<option value="new" selected>-- New Ballot --</option>
</select>
<div>
<div>
<button onclick="clickEditCandidates()">Edit candidates</button>
Ballot value: <input id="txtBallotValue" type="number" value="1" min="1" style="width: 3em;">
</div>
<table id="tblBallot"></table>
<button onclick="clickSave()">Save and advance to next ballot</button>
</div>
</div>
<div id="divEditCandidates" style="display: none;">
<div>Enter the candidates' names, one per line:</div>
<div><textarea id="txtCandidates"></textarea></div>
<div><button onclick="clickSaveCandidates()">Save candidates</button></div>
<div>Warning: Adding, removing or reordering candidates once ballots have been input may result in unexpected behaviour.</div>
</div>
<script src="index.js?v=GITVERSION"></script>
</body>
</html>

178
html/blt/index.js Normal file
View File

@ -0,0 +1,178 @@
/*
pyRCV2: Preferential vote counting
Copyright © 20202021 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 <https://www.gnu.org/licenses/>.
*/
let candidates = ['Candidate 1', 'Candidate 2', 'Candidate 3'];
let ballots = [];
let inpFile = document.getElementById('inpFile');
let txtBallotValue = document.getElementById('txtBallotValue');
let tblBallot = document.getElementById('tblBallot');
let selBallots = document.getElementById('selBallots');
selBallots.value = 'new';
function initBallot() {
// Init ballot table
tblBallot.innerHTML = '';
for (let candidate of candidates) {
let elTr = document.createElement('tr');
let elTd = document.createElement('td');
let elInput = document.createElement('input');
elInput.type = 'number';
elInput.min = '1';
elInput.style.width = '3em';
elTd.appendChild(elInput);
elTr.appendChild(elTd);
// Add listener
elInput.addEventListener('keyup', function(evt) {
if (evt.key === 'Enter') {
evt.preventDefault();
clickSave();
}
});
elTd = document.createElement('td');
elTd.innerText = candidate;
elTr.appendChild(elTd);
tblBallot.appendChild(elTr);
}
}
function clickSave() {
// Save ballot
let ballot = {'value': txtBallotValue.value, 'preferences': []};
for (let elInput of tblBallot.querySelectorAll('input')) {
ballot['preferences'].push(elInput.value);
}
if (selBallots.value === 'new') {
// New ballot
ballots.push(ballot);
// Add new ballot <option>
let elOption = document.createElement('option');
elOption.innerText = 'Ballot ' + ballots.length;
selBallots.insertBefore(elOption, selBallots.selectedOptions[0]);
} else if (selBallots.selectedIndex >= 0) {
// Editing existing ballot
ballots[selBallots.selectedIndex] = ballot;
// Advance to next ballot
selBallots.selectedIndex += 1;
}
// Update ballot entry
changeBallot();
}
function changeBallot() {
if (selBallots.value === 'new') {
// Clear input
for (let elInput of tblBallot.querySelectorAll('input')) {
elInput.value = '';
}
txtBallotValue.value = '1';
} else if (selBallots.selectedIndex >= 0) {
// Update input
let ballot = ballots[selBallots.selectedIndex];
let i = 0;
for (let elInput of tblBallot.querySelectorAll('input')) {
elInput.value = ballot['preferences'][i];
i += 1;
}
txtBallotValue.value = ballot['value'];
}
// Move cursor to first input box
tblBallot.querySelector('input').focus();
tblBallot.querySelector('input').select();
}
function clickOpenJSON() {
inpFile.value = '';
inpFile.click();
}
async function changeInpFile() {
if (inpFile.value !== '') {
// Open JSON file
let text = await inpFile.files[0].text();
let obj = JSON.parse(text);
// Load data
candidates = obj['candidates'];
ballots = obj['ballots'];
// Update ballot entry
initBallot();
// Update ballot list
selBallots.innerHTML = '<option value="new" selected>-- New Ballot --</option>';
selBallots.value = 'new';
for (let i = 0; i < ballots.length; i++) {
let elOption = document.createElement('option');
elOption.innerText = 'Ballot ' + (i + 1);
selBallots.insertBefore(elOption, selBallots.selectedOptions[0]);
}
}
inpFile.value = '';
}
function clickSaveJSON() {
let result = {
'candidates': candidates,
'ballots': ballots,
};
let url = URL.createObjectURL(new File([JSON.stringify(result)], 'ballots.json', {type: 'application/json'}));
let elA = document.createElement('a');
elA.href = url;
elA.download = '';
elA.click();
}
function clickEditCandidates() {
document.getElementById('bltMain').style.display = 'none';
document.getElementById('divEditCandidates').style.display = 'block';
document.getElementById('txtCandidates').value = candidates.join('\n');
}
function clickSaveCandidates() {
candidates = [];
for (let candidate of document.getElementById('txtCandidates').value.split('\n')) {
if (candidate.trim() !== '') {
candidates.push(candidate.trim());
}
}
// Update ballot entry
initBallot();
document.getElementById('bltMain').style.display = 'flex';
document.getElementById('divEditCandidates').style.display = 'none';
}
// Init tasks
initBallot();
changeBallot();

View File

@ -58,7 +58,7 @@ body {
table {
border-collapse: collapse;
}
td {
#result td {
padding: 0px 8px;
min-height: 1em;
}
@ -69,7 +69,7 @@ td.count sup {
font-size: 0.6rem;
top: 0;
}
tr:first-child td {
#result tr:first-child td {
vertical-align: bottom;
}
td.excluded {
@ -88,6 +88,35 @@ tr.info td {
-webkit-print-color-adjust: exact;
}
/* BLT input tool */
#selBallots {
min-width: 10em;
margin-right: 1em;
}
#bltMain {
display: flex;
}
#tblBallot {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
#tblBallot input {
margin-right: 0.5ex;
}
#divEditCandidates div {
margin-bottom: 0.5em;
}
#txtCandidates {
min-width: 20em;
min-height: 10em;
}
/* Print stylesheet */
#printWarning {