Basic ballot input prototype
This commit is contained in:
parent
4205d9b262
commit
b6f3f56280
60
html/blt/index.html
Normal file
60
html/blt/index.html
Normal 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
178
html/blt/index.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
@ -58,7 +58,7 @@ body {
|
|||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
td {
|
#result td {
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
min-height: 1em;
|
min-height: 1em;
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ td.count sup {
|
|||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
tr:first-child td {
|
#result tr:first-child td {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
td.excluded {
|
td.excluded {
|
||||||
@ -88,6 +88,35 @@ tr.info td {
|
|||||||
-webkit-print-color-adjust: exact;
|
-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 */
|
/* Print stylesheet */
|
||||||
|
|
||||||
#printWarning {
|
#printWarning {
|
||||||
|
Reference in New Issue
Block a user