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 {
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user