You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@steve.apache.org by hu...@apache.org on 2015/03/16 12:43:22 UTC

svn commit: r1666970 - in /steve/trunk/www: cgi-bin/cast-vote.pl htdocs/images/ htdocs/images/ballot_bg.png htdocs/images/dragleft.png htdocs/images/dragright.png htdocs/images/target.png htdocs/steve_interactive.css htdocs/steve_interactive.js

Author: humbedooh
Date: Mon Mar 16 11:43:22 2015
New Revision: 1666970

URL: http://svn.apache.org/r1666970
Log:
Add interactive mode for STV type elections.
Here goes nuthin'

Added:
    steve/trunk/www/htdocs/images/
    steve/trunk/www/htdocs/images/ballot_bg.png   (with props)
    steve/trunk/www/htdocs/images/dragleft.png   (with props)
    steve/trunk/www/htdocs/images/dragright.png   (with props)
    steve/trunk/www/htdocs/images/target.png   (with props)
    steve/trunk/www/htdocs/steve_interactive.css
    steve/trunk/www/htdocs/steve_interactive.js
Modified:
    steve/trunk/www/cgi-bin/cast-vote.pl

Modified: steve/trunk/www/cgi-bin/cast-vote.pl
URL: http://svn.apache.org/viewvc/steve/trunk/www/cgi-bin/cast-vote.pl?rev=1666970&r1=1666969&r2=1666970&view=diff
==============================================================================
--- steve/trunk/www/cgi-bin/cast-vote.pl (original)
+++ steve/trunk/www/cgi-bin/cast-vote.pl Mon Mar 16 11:43:22 2015
@@ -51,6 +51,8 @@ my $issue_name = "$group-$issue";
 
 my $q = CGI->new;
 
+my $interactive = $q->param(interactive)
+
 if ($ENV{REQUEST_METHOD} eq "GET" or $ENV{REQUEST_METHOD} eq "HEAD") {
 
     open my $fh, "$VOTE_ISSUEDIR/$group/$issue/issue"
@@ -99,6 +101,9 @@ EOT
     if ($type eq "yna") {
         $output .= yna_form($voter, $issue_name, $issue_content, $trailer);
     }
+    elsif ($type =~ /^stv([1-9])$/ && $interactive) {
+        $output .= stv_form_interactive($1, $voter, $issue_name, $issue_content, $trailer);
+    }
     elsif ($type =~ /^stv([1-9])$/) {
         $output .= stv_form($1, $voter, $issue_name, $issue_content, $trailer);
     }
@@ -384,6 +389,88 @@ $other_issues
 </html>
 EoSTV
 }
+
+sub stv_form_interactive {
+    my ($num, $voter, $issue_name, $issue_content, $trailer) = @_;
+    my $other_issues = other_issues($issue_name, $voter);
+    my @chars;
+    my @names;
+    while ($issue_content =~ m/\[([a-z])\]\s+(.+)/g) {
+        push @chars, "'$1'";
+        push @names, "\"$2\"";
+    }
+    my $str_candidates = join(", ", @names);
+    my $str_chars = join(", ", @chars);
+    my $str_statements = ""; # TODO!
+    my $num_candidates = scalar(@names);
+    return <<EoSTV;
+ <!DOCTYPE HTML>
+<html>
+    <head>
+        <link rel="stylesheet" href="/steve_interactive.css">
+        <script type="text/javascript">
+
+        // STV Data
+        var seats = $num; // Number of seats on the board
+        
+        // Nominees
+        var candidates = [ $str_names ];
+        var chars = [ $str_chars ];
+        
+        // Statements
+        var statements = { $str_statements };
+        
+        </script>
+        <script src="/steve_interactive.js" type="text/javascript"></script>
+
+        <title>Cast your vote &lt;$voter&gt; on $issue_name:</title>
+    </head>
+    <body onload="shuffleCandidates(); drawCandidates()">
+        <h2><h1>Cast your vote &lt;$voter&gt; on $issue_name:</h1></h2>
+        
+        <p>
+            This is an interactive ballot for $issue_name, with $num_candidates
+            nominated people and $num board seats available. The red line
+            denotes the cutaway, should all your choices be voted in. All the
+            nominees are placed in random order on the canidate list. If the
+            canidate has prepared a statement, you can view it by clicking on
+            the statement link to the right of the candidate's name.
+        </p>
+        
+        <p>
+            <b>How to vote:</b><br/> Drag a candidate from the candidate list to the
+            ballot box to place them in the vote. You can rearrange your votes as you
+            see fit, by dragging candidates up/down on the ballot box list. You may
+            place as many canidates in the ballot box as you see fit. If you want to
+            remove a single candidate from your ballot box, simply drag the canidate
+            back to the list of remaining candidates to the left.
+        </p>
+        
+        <div id="candidates">
+            Not seeing the canidate list? Please enable JavaScript!
+        </div>
+        
+        <div id="ballotbox" ondragover="event.preventDefault();"
+         ondragenter="event.preventDefault();" ondragend="event.preventDefault();"
+         ondrop="dropCandidate(event)">
+            <font color='red'><h3>Drag candidates over here to vote for them</h3></font>
+            <ol id="ballot">
+                <img src="/images/target.png" style="margin-left: 100px;"/>
+            </ol>
+            <div id="stv">
+                <form method="POST">
+                    <b>Your STV order:</b>
+                    <input type="text" id="vote" name="vote" style="width: 160px; font-family:
+                    monospace;"/> <input type="submit" value="Cast votes"/> &nbsp;
+                    <input type="button" value="Reset" onclick="reset()"/>
+                </form>
+            </div>
+        </div>
+        $other_issues
+    </body>
+</html>
+EoSTV
+}
 
 sub select_form {
     my ($num, $voter, $issue_name, $issue_content, $trailer) = @_;

Added: steve/trunk/www/htdocs/images/ballot_bg.png
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/images/ballot_bg.png?rev=1666970&view=auto
==============================================================================
Binary file - no diff available.

Propchange: steve/trunk/www/htdocs/images/ballot_bg.png
------------------------------------------------------------------------------
    svn:mime-type = image/png

Added: steve/trunk/www/htdocs/images/dragleft.png
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/images/dragleft.png?rev=1666970&view=auto
==============================================================================
Binary file - no diff available.

Propchange: steve/trunk/www/htdocs/images/dragleft.png
------------------------------------------------------------------------------
    svn:mime-type = image/png

Added: steve/trunk/www/htdocs/images/dragright.png
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/images/dragright.png?rev=1666970&view=auto
==============================================================================
Binary file - no diff available.

Propchange: steve/trunk/www/htdocs/images/dragright.png
------------------------------------------------------------------------------
    svn:mime-type = image/png

Added: steve/trunk/www/htdocs/images/target.png
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/images/target.png?rev=1666970&view=auto
==============================================================================
Binary file - no diff available.

Propchange: steve/trunk/www/htdocs/images/target.png
------------------------------------------------------------------------------
    svn:mime-type = image/png

Added: steve/trunk/www/htdocs/steve_interactive.css
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/steve_interactive.css?rev=1666970&view=auto
==============================================================================
--- steve/trunk/www/htdocs/steve_interactive.css (added)
+++ steve/trunk/www/htdocs/steve_interactive.css Mon Mar 16 11:43:22 2015
@@ -0,0 +1,253 @@
+/*
+#####
+# 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.
+#####
+*/
+body {
+  /*color: #333333;*/
+  font-family: 'Helvetica', arial;
+}
+.wrap {
+  padding: 40px;
+  text-align: center;
+}
+hr {
+  clear: both;
+  margin-top: 40px;
+  margin-bottom: 40px;
+  border: 0;
+  border-top: 1px solid #aaaaaa;
+}
+h1 {
+  font-size: 30px;
+  margin-bottom: 40px;
+}
+p {
+  margin-bottom: 20px;
+}
+.btn {
+  background: #428bca;
+  border: #357ebd solid 1px;
+  border-radius: 3px;
+  color: #fff;
+  display: inline-block;
+  font-size: 14px;
+  padding: 8px 15px;
+  text-decoration: none;
+  text-align: center;
+  min-width: 60px;
+  position: relative;
+  transition: color .1s ease;
+  /* top: 40em;*/
+}
+.btn:hover {
+  background: #357ebd;
+}
+.btn.btn-big {
+  font-size: 18px;
+  padding: 15px 20px;
+  min-width: 100px;
+}
+.btn-close {
+  color: #aaaaaa;
+  font-size: 30px;
+  text-decoration: none;
+  position: absolute;
+  right: 5px;
+  top: 0;
+}
+.btn-close:hover {
+  color: #919191;
+}
+.modal:before {
+  content: "";
+  display: none;
+  background: rgba(0, 0, 0, 0.6);
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 10;
+}
+.modal:target:before {
+  display: block;
+}
+.modal:target .modal-dialog {
+  -webkit-transform: translate(0, 0);
+  -ms-transform: translate(0, 0);
+  transform: translate(0, 0);
+  top: 20%;
+}
+.modal-dialog {
+  background: #fefefe;
+  border: #333333 solid 1px;
+  border-radius: 5px;
+  margin-left: -200px;
+  position: fixed;
+  left: 50%;
+  top: -100%;
+  z-index: 11;
+  width: 640px;
+  word-wrap: break-word;
+  -webkit-transform: translate(0, -500%);
+  -ms-transform: translate(0, -500%);
+  transform: translate(0, -500%);
+  -webkit-transition: -webkit-transform 0.3s ease-out;
+  -moz-transition: -moz-transform 0.3s ease-out;
+  -o-transition: -o-transform 0.3s ease-out;
+  transition: transform 0.3s ease-out;
+}
+.modal-body {
+  padding: 20px;
+  word-wrap: break-word;
+}
+pre {
+  word-wrap: break-word;
+  white-space: pre-wrap;
+}
+.modal-header,
+.modal-footer {
+  padding: 10px 20px;
+}
+.modal-header {
+  border-bottom: #eeeeee solid 1px;
+}
+.modal-header h2 {
+  font-size: 20px;
+}
+.modal-footer {
+  border-top: #eeeeee solid 1px;
+  text-align: right;
+}
+/*ADDED TO STOP SCROLLING TO TOP*/
+#close {
+  display: none;
+}
+
+.cls {
+    width: 100%;
+    height: 2px;
+    float: left;
+}
+#stv {
+    width: 100%;
+    margin-top: 60px;
+    float: left;
+}
+#ballotbox {
+    width: 460px;
+    min-height: 100px;
+    padding-top: 30px;
+    padding-bottom: 10px;
+    background-image: url(/images/ballot_bg.png), linear-gradient(to bottom, #cedce7 0%,#596a72 100%);
+    background-repeat: repeat;
+    border: 4px groove #333;
+    float: left;
+}
+#candidates {
+    width: 580px;
+    float: left;
+    min-height: 400px;
+    background-repeat: no-repeat;
+}
+
+#ballot {
+  margin: 0 0 0 0;
+}
+.ballotbox {
+    border: 1px dotted #666;
+    padding: 4px;
+    max-width: 300px;
+    min-height: 22px;
+    cursor: move;
+    cursor: grab;
+    font-family: monospace;
+    font-size: 16px;
+    background: linear-gradient(to bottom, #f8f8f8 0%,#dddddd 100%);
+    -khtml-user-drag: element;
+}
+
+.fillerbox {
+    padding: 4px;
+    max-width: 300px;
+    min-height: 26px;
+    font-family: monospace;
+    font-size: 16px;
+    margin: 0 0 0 0;
+    -khtml-user-drag: element;
+    margin-left: 40px;
+}
+
+
+.ballotbox_clist {
+    border: 1px dotted #666;
+    padding: 4px;
+    max-width: 330px;
+    min-height: 22px;
+    cursor: move;
+    cursor: grab;
+    font-family: monospace;
+    font-size: 16px;
+    background: linear-gradient(to bottom, #f8f8f8 0%,#dddddd 100%);
+    -khtml-user-drag: element;
+}
+
+.statement_marker {
+    border: 1px dotted #666;
+    padding: 4px;
+    max-width: 100px;
+    min-height: 12px;
+    font-family: monospace;
+    font-size: 12px;
+    color: #FFF;
+    background: linear-gradient(to bottom, #3b679e 0%,#2b88d9 50%,#207cca 51%,#7db9e8 100%);
+    -khtml-user-drag: element;
+    float: right;
+    right: 0px;
+    top: 0px;
+    position: static;
+}
+
+.statement_marker a {
+    color: #fff;
+}
+
+.ballotSelected {
+    border: 1px dotted #666;
+    padding: 4px;
+    max-width: 300px;
+    min-height: 22px;
+    cursor: move;
+    cursor: grab;
+    font-family: monospace;
+    font-size: 16px;
+    background: linear-gradient(to bottom, #f4f0ad 0%,#eae477 100%);
+}
+.ballotbox:hover {
+    background: linear-gradient(to bottom, #b0d4e3 0%,#88bacf 100%);
+}
+p {
+    max-width: 1100px;
+}
+body, html {
+    width: 1200px;
+    margin: 0 auto;
+}
+
+* {
+    -webkit-user-select: none; 
+}
\ No newline at end of file

Added: steve/trunk/www/htdocs/steve_interactive.js
URL: http://svn.apache.org/viewvc/steve/trunk/www/htdocs/steve_interactive.js?rev=1666970&view=auto
==============================================================================
--- steve/trunk/www/htdocs/steve_interactive.js (added)
+++ steve/trunk/www/htdocs/steve_interactive.js Mon Mar 16 11:43:22 2015
@@ -0,0 +1,463 @@
+/* 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 ballotNames = []
+var ballotChars = []
+var chars = chars? chars : ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']  // Corresponding STV letters, in same order as nominees
+var fading = false
+// Cut away unused chars
+while (chars.length > candidates.length) chars.splice(-1,1)
+
+// Make copies for reset
+var candidates_copy = []
+var chars_copy = []
+
+var failover = null;
+
+for (i in candidates) candidates_copy.push(candidates[i])
+for (i in chars) chars_copy.push(chars[i])
+
+
+// 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 reset() {
+    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)
+}
+
+
+// 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)
+        outer.setAttribute("draggable", "true")
+        outer.setAttribute("ondragstart", "dragVote(event)")
+        outer.appendChild(inner)
+        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")
+            statement.setAttribute("title", "Click to read " + name + "'s statement")
+            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">×</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)
+        }/* else {
+            var statement = document.createElement('div')
+            statement.setAttribute("class", "statement_marker")
+            statement.style = "background: linear-gradient(to bottom, #e2e2e2 0%,#dbdbdb 50%,#d1d1d1 51%,#fefefe 100%) !important;"
+            statement.style.color = "#666";
+            statement.innerHTML = "<i>No statement</i>"
+
+            outer.appendChild(statement)
+        }*/
+        box.appendChild(outer)
+        
+    }
+}
+
+// Did we drop a vote on top of another?
+function dropCandidate(ev) {
+    
+    ev.preventDefault();
+    source = ev.dataTransfer.getData("Text");
+    dest = ev.target.getAttribute("data")
+    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")
+        //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")
+        
+    } else {
+        dest = null
+        source = null
+    }
+}
+
+function showLines(ev) {
+    
+    source = ev.dataTransfer.getData("Text");
+    source = source ? source : failover;
+    ev.preventDefault();
+    if (ev.target) {
+        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")
+        
+        // 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.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(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.transform = "scaleY(0)"
+            outer.style.minHeight = "0px"
+        }
+        
+        // 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"
+    
+    // Set the current STV order
+    document.getElementById('cast').style.width = (chars_copy.length * 8)+ "px"
+    document.getElementById('cast').value = ballotChars.join("")
+}
+
+
+// Fade in/out maneuvres
+function fadeOut(x) {
+    if (source) {
+        if (!x) {
+            x = 1
+        }
+        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");
+        }
+    }   
+}
+
+function fadeIn(x, y) {
+    if (source) {
+        x += 0.1
+        if (x >= 1) {
+            x = 1
+        }
+        document.getElementById(source).style.opacity = String(x)
+        document.getElementById(source).style.height = (x*22) + "px"
+        document.getElementById(source).style.fontSize = (x*16) + "px"
+        document.getElementById(source).style.transform = "scaleY(" + x + ")"
+        
+        
+        document.getElementById(source).setAttribute("class", "ballotSelected")
+        if (x < 1) {
+            fading = true
+            window.setTimeout(function() { fadeIn(x, y)}, 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();
+            
+            
+        }
+    }   
+}