pysteve/www/htdocs/js/steve_stv.js (548 lines of code) (raw):

/* WARNING: This script contains Voodoo! */ /* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var candidates = [] var statements = [] var seconds_txt = [] var ballotNames = [] var ballotChars = [] var chars; var fading = false var seats = 0; var maxnum = 9999 // Make copies for reset var candidates_copy = [] var chars_copy = [] var failover = null; var tb = "" // Set transfer data during drag'n'drop function dragVote(ev) { ev.dataTransfer.setData("Text", ev.target.getAttribute("data")); failover = ev.target.getAttribute("data") if (ballotNames.indexOf(failover) == -1) { document.getElementById('candidates').style.backgroundImage = "url(/images/dragright.png)" document.getElementById('candidates').style.backgroundRepeat = "no-repeat" } else { document.getElementById('candidates').style.backgroundImage = "url(/images/dragleft.png)" document.getElementById('candidates').style.backgroundRepeat = "no-repeat" } } var source, dest function cancel(ev) { ev.preventDefault() } function resetList() { candidates = [] chars = [] for (i in candidates_copy) candidates.push(candidates_copy[i]) for (i in chars_copy) chars.push(chars_copy[i]) ballotNames = [] ballotChars = [] shuffleCandidates(); drawCandidates() fading = false document.getElementById('ballot').innerHTML = '<img src="/images/target.png" style="margin-left: 100px;" ondrop="event.preventDefault(); dropCandidate(event);"/>'; drawList(); } // Did we drop a vote on top of another? function dropVote(ev, parent) { //ev.preventDefault(); if (parent || fading) return; // Get who we dragged and who we dropped it on source = ev.dataTransfer.getData("Text"); dest = parent ? ev.target.parentNode.getAttribute("data") : ev.target.getAttribute("data") if (dest == "UPPER") { dest = ballotNames[0]} if (dest == "LOWER") { dest = ballotNames[ballotNames.length -1] } if (candidates.indexOf(dest) != -1) { alert("Back to school!") } // If we didn't drag this onto ourselves, let's initiate the fade-out and swap if (source != dest) { fadeOut(1, "ballot"); } } function dropComplete(z) { if (fading) { return; } // Get array indices var sid = ballotNames.indexOf(source); var did = ballotNames.indexOf(dest) // Splice! if (sid >= 0 && did >= 0) { ballotNames.splice(did, 0, ballotNames.splice(sid, 1)[0]) ballotChars.splice(did, 0, ballotChars.splice(sid, 1)[0]) } else { //alert(source + ":" + dest) } //ev.preventDefault(); // Redraw and carry on drawList() fadeIn(0, z, Math.random()) } // A little shuffle, so we don't all get the same order at first function shuffleCandidates() { for (var i = 0; i < candidates.length; i++) { // Pick some numbers var sid = parseInt(Math.random()*candidates.length-0.01); var did = parseInt(Math.random()*candidates.length-0.01); // Splice! if (sid >= 0 && did >= 0) { candidates.splice(did, 0, candidates.splice(sid, 1)[0]) chars.splice(did, 0, chars.splice(sid, 1)[0]) } } } function drawCandidates() { var box = document.getElementById('candidates') box.innerHTML = "<h3>Candidates:</h3>" for (i in candidates) { var name = candidates[i] var char = chars[i] // Add element and set drag'n'drop + data var outer = document.createElement('div') var inner = document.createElement('span') inner.style.fontFamily = "monospace" inner.innerHTML = char + ": " + name; inner.setAttribute("ondrop", "dropCandidate(event, true)") outer.setAttribute("class", "ballotbox_clist") outer.setAttribute("id", name) outer.setAttribute("data", name) inner.setAttribute("data", name) inner.setAttribute("draggable", "false") outer.setAttribute("draggable", "true") outer.setAttribute("ondragstart", "dragVote(event)") outer.appendChild(inner) // if it's a super long name, like a data blob, don't tooltip it if (name.length > 32) outer.setAttribute("title", "Drag to move the candidate to the ballot box") else outer.setAttribute("title", "Drag to move " + name + " to the ballot box") outer.setAttribute("ondrop", "dropCandidate(event, false)") outer.setAttribute("ondragover", "event.preventDefault();") outer.setAttribute("ondragend", "event.preventDefault();") outer.setAttribute("ondragenter", "event.preventDefault();") // Does the candidate have a statement? if so, put it on there if (statements[char]) { var statement = document.createElement('div') statement.setAttribute("class", "statement_marker") // if it's a super long name, like a data blob, don't tooltip it if (name.length > 32) statement.setAttribute("title", "Click to read the candidate's statement") else statement.setAttribute("title", "Click to read " + name + "'s statement") statement.setAttribute("onclick", "location.hash='#statement_"+char+"';") statement.innerHTML = "<a href='#statement_"+char+"'>Statement</a>" outer.appendChild(statement) var popup = document.createElement("div") popup.setAttribute("class", "modal") popup.setAttribute("id", "statement_" + char) popup.setAttribute("aria-hidden", "true") var popupd = document.createElement("div") popupd.setAttribute("class", "modal-dialog") popup.appendChild(popupd) var popuph = document.createElement("div") popuph.setAttribute("class", "modal-header") popuph.innerHTML = '<h2>Statement from ' + name + '</h2><a href="#close" class="btn-close" aria-hidden="true">&#xd7;</a>' var popupb = document.createElement("div") popupb.setAttribute("class", "modal-body") popupb.innerHTML = '<pre>' + (statements[char] ? statements[char] : "This candidate has not prepared a statement") +'</pre>' var popupf = document.createElement("div") popupf.setAttribute("class", "modal-footer") popupf.innerHTML = '<a href="#close" class="btn">Close statement</a>' popupd.appendChild(popuph) popupd.appendChild(popupb) popupd.appendChild(popupf) document.getElementsByTagName('body')[0].appendChild(popup) } // Does the candidate have a nomination and/or seconds? if so, put it on there if (seconds_txt[char]) { var seconds = document.createElement('div') seconds.setAttribute("class", "statement_marker") seconds.style.float = 'right' seconds.style.marginRight = '4px' seconds.setAttribute("title", "Click to read " + name + "'s nomination and/or seconds") seconds.innerHTML = "<a href='#seconds_"+char+"'>2nds</a>" outer.appendChild(seconds) var popup = document.createElement("div") popup.setAttribute("class", "modal") popup.setAttribute("id", "seconds_" + char) popup.setAttribute("aria-hidden", "true") var popupd = document.createElement("div") popupd.setAttribute("class", "modal-dialog") popup.appendChild(popupd) var popuph = document.createElement("div") popuph.setAttribute("class", "modal-header") popuph.innerHTML = '<h2>Nomination/Seconds for ' + name + '</h2><a href="#close" class="btn-close" aria-hidden="true">&#xd7;</a>' var popupb = document.createElement("div") popupb.setAttribute("class", "modal-body") popupb.innerHTML = '<pre>' + (seconds_txt[char] ? seconds_txt[char] : "This candidate does not have a nomination statement") +'</pre>' var popupf = document.createElement("div") popupf.setAttribute("class", "modal-footer") popupf.innerHTML = '<a href="#close" class="btn">Close window</a>' popupd.appendChild(popuph) popupd.appendChild(popupb) popupd.appendChild(popupf) document.getElementsByTagName('body')[0].appendChild(popup) } box.appendChild(outer) } document.getElementById('ballotbox').style.height = box.offsetHeight + "px" } // Did we drop a vote on top of another? function dropCandidate(ev) { if (ev.preventDefault) ev.preventDefault(); if (ballotNames.length >= maxnum) { return; // MNTV lockout } source = ev.dataTransfer ? ev.dataTransfer.getData("Text") : tb; source = source ? source : tb dest = ev.target.getAttribute("data") dest = dest ? dest : "LOWER" if (dest == source) { // touch fixes dest = "LOWER" } var z = 0; if (dest == "UPPER") { dest = ballotNames[0]; z = 0} if (dest == "LOWER") { dest = ballotNames[ballotNames.length -1]; z = 1;} if (dest && candidates.indexOf(dest) != -1) { return; } if (ballotNames.indexOf(source) == -1 && candidates.indexOf(source) != -1) { var x = ballotNames.indexOf(dest) x += z if (ballotNames.indexOf(dest) != -1) { ballotNames.splice(x,0,source); ballotChars.splice(x,0,chars[candidates.indexOf(source)]); } else { ballotNames.push(source) ballotChars.push(chars[candidates.indexOf(source)]) } chars.splice(candidates.indexOf(source), 1) candidates.splice(candidates.indexOf(source), 1) fadeIn(0, "ballot", Math.random()) //ev.preventDefault() drawCandidates(); drawList(); } } // Did we drop a vote on top of another? function dropBack(ev) { ev.preventDefault(); source = ev.dataTransfer.getData("Text"); dest = ev.target.getAttribute("data") if (dest == "UPPER") { dest = ballotNames[0]} if (dest == "LOWER") { dest = ballotNames[ballotNames.length -1] } if ((!dest || candidates.indexOf(dest) != -1) && ballotNames.indexOf(source) != -1) { candidates.push(source) chars.push(ballotChars[ballotNames.indexOf(source)]) ballotChars.splice(ballotNames.indexOf(source), 1) ballotNames.splice(ballotNames.indexOf(source), 1) drawList(); drawCandidates(); fadeIn(0, "candidates", Math.random()) } else { dest = null source = null } } function showLines(ev) { source = ev.dataTransfer.getData("Text"); source = source ? source : failover; ev.preventDefault(); if (ev.target && ev.target.getAttribute) { var above = false dest = ev.target.getAttribute("data") var odest = dest; var override = false if (dest == "UPPER") { dest = ballotNames[0]; override = true; above = true;} if (dest == "LOWER") { dest = ballotNames[ballotNames.length-1]; override = true; above= false; } for (i=0;i< document.getElementById('ballot').childNodes.length;i++) { var el = document.getElementById('ballot').childNodes[i] el.style.borderTop = "" el.style.borderBottom = "" } document.getElementById('UPPER').style.borderTop = "none" document.getElementById('LOWER').style.borderBottom = "none" document.getElementById('UPPER').style.borderBottom = "none" document.getElementById('LOWER').style.borderTop = "none" if (ballotNames.indexOf(dest) != -1 && dest != source) { a = ballotNames.indexOf(source); b = ballotNames.indexOf(dest); override = false if (a != -1 && !override) { if (a > b) { above = true; } else { above = false; } } else { b--; if (b == -1) { above = false; } if (b == ballotNames.length-1) { above = false; } } if (((a == -1 || above == true) && odest != "UPPER") || odest == "LOWER") { document.getElementById(odest).style.borderTop = "16px solid #0AF"; } else { document.getElementById(odest).style.borderBottom = "16px solid #0AF"; } } } } function insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } function insertBefore(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode); } function drawList() { // Remove drag helper document.getElementById('candidates').style.background = ""; // Fetch ballot master and clear it var ballot = document.getElementById('ballot') ballot.innerHTML = "" var s = 0; // For each nominee, do... for (i in ballotNames) { s++; var el = ballotNames[i]; var outer = document.createElement('li'); // Set style outer.setAttribute("class", "ballotbox") var no = document.createElement('div'); no.setAttribute("class", "ballotNumber") no.innerHTML = (s) // Above/below cutaway line? If so, draw it if (s == seats) { outer.style.borderBottom = "1px solid #A00" } if (s == seats+1) { outer.style.borderTop = "1px solid #A00" } // 'grey out' people below cutaway line if (s > seats) { outer.style.opacity = "0.75" } // Add element and set drag'n'drop + data var inner = document.createElement('span') inner.style.left = "35px" inner.style.maxWidth = "300px" inner.style.maxHeight = "60px" inner.style.overflow = "hidden" inner.innerHTML = ballotChars[i] + ": " + el; inner.setAttribute("ondrop", "dropVote(event, true)") outer.setAttribute("id", el) outer.setAttribute("data", el) inner.setAttribute("data", el) outer.setAttribute("draggable", "true") outer.setAttribute("ondragstart", "dragVote(event)") outer.setAttribute("ondragenter", "showLines(event)") outer.appendChild(no) outer.appendChild(inner) outer.setAttribute("title", "Drag to move " + el + " up or down on the list") outer.setAttribute("ondrop", "dropVote(event, false)") if (el == source) { outer.style.opacity = "0" } // Add to box ballot.appendChild(outer) } // Drop upper and lower filler boxes, so people can drag to the top/bottom of the list as well if (!document.getElementById('UPPER')) { var d = document.createElement('div'); d.setAttribute("class", "fillerbox") d.setAttribute("data", "UPPER"); d.setAttribute("id", "UPPER"); d.setAttribute("ondragenter", "showLines(event)") d.setAttribute("ondrop", "dropVote(event, false)") insertBefore(d, ballot); var d = document.createElement('div'); d.setAttribute("class", "fillerbox") d.setAttribute("id", "LOWER") d.setAttribute("data", "LOWER"); d.setAttribute("ondrop", "dropVote(event, false)") d.setAttribute("ondragenter", "showLines(event)") insertAfter(d, ballot); } // Clear any bad lines document.getElementById('UPPER').style.borderTop = "none" document.getElementById('LOWER').style.borderBottom = "none" document.getElementById('UPPER').style.borderBottom = "none" document.getElementById('LOWER').style.borderTop = "none" } // Fade in/out maneuvres function fadeOut(x) { if (source) { if (!x) { x = 1 } if (fading) { return; } x -= 0.1 document.getElementById(source).setAttribute("class", "ballotSelected") document.getElementById(source).style.opacity = String(x) if (x > 0) { window.setTimeout(function() { fadeOut(x)}, 20) } else { dropComplete("candidates"); } } } var gz = 0; function fadeIn(x, y, z) { if (source) { if (x == 0) { gz = z; } if (z != gz) { return; } x += 0.1 if (x >= 1) { x = 1 } document.getElementById(source).style.opacity = String(x) document.getElementById(source).setAttribute("class", "ballotSelected") if (x < 1) { fading = true window.setTimeout(function() { fadeIn(x, y, z)}, 25) } else { window.setTimeout(function() {fading = false }, 250) if (y == "ballot") { document.getElementById(source).setAttribute("class", "ballotbox") } else { document.getElementById(source).setAttribute("class", "ballotbox_clist") } source = null drawList(); } } } var step = -1 function loadIssue(election, issue, uid, callback) { var messages = ["Herding cats...", "Shaving yaks...", "Shooing some cows away...", "Fetching election data...", "Loading issues..."] if (!election || !uid) { var l = document.location.search.substr(1).split("/"); election = l[0]; issue = l.length > 1 ? l[l.length-2] : ""; uid = l.length > 2 ? l[l.length-1] : ""; } if (step == -1) { getJSON("/steve/voter/view/" + election + "/" + issue + "?uid=" + uid, [election, issue, uid], callback) } var obj = document.getElementById('preloader'); step++; if (!election_data && obj) { if (step % 2 == 1) obj.innerHTML = messages[parseInt(Math.random()*messages.length-0.01)] } else if (obj && (step % 2 == 1)) { obj.innerHTML = "Ready..!" } if (step % 2 == 1) { obj.style.transform = "translate(0,0)" } else if (obj) { obj.style.transform = "translate(0,-500%)" } if (!election_data|| (step % 2 == 0) ) { window.setTimeout(loadElection, 750, election, uid, callback); } } function makeLetter(num) { var x = "A".charCodeAt(0) while (num > 25) { x += 1 num -= 25 } var letter = String.fromCharCode(x) + String.fromCharCode("A".charCodeAt(0) + num) return letter } function displayIssueSTV(code, response, state) { initTouch() chars = [] // Corresponding STV letters, in same order as nominees for (var z = 0; z <= 100; z++) { chars.push(makeLetter(z)) } election_data = response if (code != 200) { document.getElementById('preloaderWrapper').innerHTML = "<h1>Could not load issue:</h1><h2>" + response.message + "</h2>"; } else { candidates = [] seconds_txt = [] statements = {} var m = response.issue.type.match(/(\d+)/); if (m) { seats = parseInt(m[1]) if (response.issue.type.match(/mntv/) || response.issue.type.match(/fic/)) { maxnum = seats } } for (c in response.issue.candidates) { var candidate = response.issue.candidates[c]; candidates.push(candidate.name); statements[chars[c]] = candidate.statement; seconds_txt[chars[c]] = candidate.seconds_txt; // don't use .seconds, that's for arrays! } document.getElementById('cnum').innerHTML = candidates.length document.getElementById('snum').innerHTML = seats while (chars.length > candidates.length) chars.splice(-1,1) for (i in candidates) candidates_copy.push(candidates[i]) for (i in chars) chars_copy.push(chars[i]) var obj = document.getElementById('preloaderWrapper') obj.innerHTML = "" obj.setAttribute("style", "min-width: 100%; min-height: 400px;") obj.setAttribute("id", "ballotWrapper") var c = document.createElement('div') c.setAttribute("id", "candidates") c.setAttribute("ondragover", "event.preventDefault();") c.setAttribute("ondragenter", "event.preventDefault();") c.setAttribute("ondragend", "event.preventDefault();") c.setAttribute("ondrop", "dropBack(event);") obj.appendChild(c) var b = document.createElement('div') b.setAttribute("id", "ballotbox") b.setAttribute("ondragover", "event.preventDefault();") b.setAttribute("ondragenter", "event.preventDefault();") b.setAttribute("ondragend", "event.preventDefault();") b.setAttribute("ondrop", "dropCandidate(event);") b.innerHTML = "<font color='red'><h3>Drag candidates over here:</h3</font>" var l = document.createElement('ol') l.setAttribute("id", "ballot") b.appendChild(l) obj.appendChild(b) l.innerHTML = "<img src='/images/target.png'/>" var stvdiv = document.createElement('div') stvdiv.setAttribute("id", "stv") b.appendChild(stvdiv) var vote = document.createElement('input') vote.setAttribute("type", "button") vote.setAttribute("class", "btn-green") vote.setAttribute("value", "Cast votes") vote.setAttribute("onclick", "castVotes();") var reset = document.createElement('input') reset.setAttribute("type", "button") reset.setAttribute("class", "btn-red") reset.setAttribute("value", "Reset") reset.setAttribute("onclick", "resetList();") stvdiv.appendChild(vote) stvdiv.appendChild(reset) stvdiv.appendChild(document.createElement('br')) stvdiv.appendChild(document.createElement('br')) shuffleCandidates(); drawCandidates(); document.getElementById('title').innerHTML = response.issue.title document.title = response.issue.title + " - Apache STeVe" } } function castVotes(args) { var l = document.location.search.substr(1).split("/"); election = l[0]; issue = l.length > 1 ? l[l.length-2] : ""; uid = l.length > 2 ? l[l.length-1] : ""; var v = ballotChars.join(" ") if (v == "") { v = "-" } postREST("/steve/voter/vote/" + election + "/" + issue, { uid: uid, vote: v }, undefined, castVotesCallback, null) } function castVotesCallback(code, response, state) { if (code != 200) { alert(response.message) } else { document.getElementById('votebox').innerHTML = "<h2>Your vote has been registered!</h2><p style='text-align:center;'><big>Should you reconsider, you can always reload this page and vote again.<br/><br/><a href=\"javascript:void(location.href='election.html'+document.location.search);\">Back to election front page</a></big></p>" } } function touchHandler(event) { var touch = event.changedTouches[0]; for (var i in event.changedTouches) { if (event.changedTouches[i].target && event.changedTouches[i].target.getAttribute ) { touch = event.changedTouches[i] break } } if (!touch.target || !touch.target.getAttribute || !touch.target.getAttribute("data")) { return } if (event.type == 'touchstart') { dragVote(touch) } if (event.type == 'touchend' || event.type == 'touchcancel') { tb = touch.target.getAttribute('data') dropCandidate(touch) } event.preventDefault(); } function initTouch() { document.addEventListener("touchstart", touchHandler, true); document.addEventListener("touchmove", touchHandler, true); document.addEventListener("touchend", touchHandler, true); document.addEventListener("touchcancel", touchHandler, true); }