<?php
/* 'js-pref-voter' - Preferential ballot filler UI in Javascript
 * and accompanying PHP functions
 *
 * http://iki.fi/elonen/code//js-pref-voter/
 *
 * Copyright (c) 2004,2005 Jarno Elonen <elonen@iki.fi>
 * The MIT License:
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

class Js_Pref_Voter
{
  var $options,      // Array( JS idx => name of cand, party, "all others" or "-" )
      $n_options,    // count($options)
      $real_cands,   // Like parameter $candidates of constructor but without any parties
      $n_real_cands; // count($real_cands)

  // UI strings
  var $shared_pos_str, $up_symbol, $down_symbol, $del_symbol;

  var $js_to_candid, // Array( JS array idx => cand id )
      $party_cands;  // Array( party's JS array id (or False) => Array( cand's JS array idx ))

  // The constructor.
  // Parameter $candidates has the following format:
  //
  //   array( [party_name => array( cand id => cand name, ... ) | cand id cand name ], ... )
  //
  // I.e. the array can contain both candidates that are in some party and those who are not.
  // Nested parties are not allowed. Example:
  //
  //    $ice_creams = Array(
  //      "Fruits" => Array(
  //          1 => "banana",
  //          2 => "pineapple",
  //          3 => "orange",
  //          4 => "mango"),
  //      "Berries" => Array(
  //          5 => "strawberry",
  //          6 => "blueberry",
  //          7 => "blackberry",
  //          8 => "lingonberry"),
  //      "Classics" => Array(
  //          9 => "vanilla",
  //          10 => "chocolate chips",
  //          11 => "toffe"),
  //      "Specials" => Array(
  //          12 => "tofu based",
  //          13 => "liqorice",
  //          14 => "green tea",
  //          15 => "garlic"),
  //      16 => "Grandma's home made"
  //    );
  function Js_Pref_Voter( $candidates,
        $up_sym = '<img border="0" src="gfx/list-up.gif" alt="/\" title="Move up">',
        $down_sym = '<img border="0" src="gfx/list-down.gif" alt="\/"  title="Move down">',
        $del_sym = '<img border="0" src="gfx/list-del.gif" alt="X">',
        $shared_pos_str = "...",
        $all_others_str = "(all others)" )
  {
    // Build some arrays from candidate list
    $this->real_cands = Array();
    $this->options = Array( 0=>'-' );
    $this->js_to_candid = Array();
    $this->party_cands = Array();
    $i = 1;
    foreach( $candidates as $party => $cands )
    {
      if ( is_array( $cands ))
      {
        $this->options[] = '(' . $party . ')';
        $cur_party = $i;
        $this->party_cands[$cur_party] = Array();
        $i++;
        foreach( $cands as $candid => $c )
        {
          $this->party_cands[$cur_party][] = $i;
          $this->js_to_candid[$i] = $candid;
          $this->options[] = "  " . $c;
          $this->real_cands[$candid] = $c;
          $i++;
        }
      }
      else if ( is_string( $cands ))
      {
        $candid = $party;
        $this->options[] = $cands;
        $this->party_cands[false][] = $i;
        $this->js_to_candid[$i] = $candid;
        $this->real_cands[$candid] = $cands;
        $i++;
      }
    }

    $this->options[] = $all_others_str;
    $this->n_options = count($this->options);
    $this->n_real_cands = count($this->real_cands);

    // Save appearance preferences
    $this->shared_pos_str = $shared_pos_str;
    $this->up_symbol = $up_sym;
    $this->down_symbol = $down_sym;
    $this->del_symbol = $del_sym;
  }

  // Returns an array of form Array( <pos> => Array(<cand-id>, <cand-id>...), ... )
  // or False if $vote is not a valid vote string as generated by the Javascript.
  // You may wish to pass the result to Expand_Parties
  function Parse_Vote_String($vote)
  {
    $res = Array();
    $counts = array_fill(1, $this->n_options, 0);
    foreach( explode("_", $vote) as $str )
    {
      $pair = explode("-", $str);
      if ( count($pair) == 2 )
      {
        $pos = (int)$pair[0];
        $cand = (int)$pair[1];

        // Check that both position and candidate
        // are within range
        if ( $pos < 1 || $pos > $this->n_options ||
            $cand < 1 || $cand > $this->n_options )
          return False;

        if ( !isset($res[$pos]))
          $res[$pos] = Array();
        $res[$pos][] = $cand;

        // Check that each candidate exist at most once
        if ( ++$counts[$cand] > 1)
          return false;
      }
    }
    if ( count($res) > $this->n_options)
      return false;
    return $res;
  }

/*
  function Make_Matrix()
  {
    $names = array();
    $mtx = array();
    $candid_to_row = array();
    foreach( $this->real_cands as $id=>$name )
    {
      $row = array();
      foreach( $this->real_cands as $id2=>$name2 )
        $row[] = 0;
      $mtx[] = $row;
      $names[] = $name;
      $candid_to_row[$id] = count($mtx)-1;
    }
  }
*/

  // "Expands" array from Parse_Vote_String() by
  // replacing all parties with their members and then
  // "all others" by non-mentioned candidates.
  function Expand_Parties( $vote_array )
  {
    // Build a list of explicitly mentioned individuals
    $mentioned_explicitly = array();
    foreach( $vote_array as $slot )
      foreach( $slot as $i )
        if ( isset( $this->js_to_candid[$i] )) // is a real candidate?
          $mentioned_explicitly[$i] = True;

    // Expand parties
    foreach( $vote_array as $slot_i => $slot )
      foreach( $slot as $i_idx => $i )
        if ( isset($this->party_cands[$i])) // is a party?
        {
          foreach( $this->party_cands[$i] as $c )
            if ( !isset($mentioned_explicitly[$c]))
            {
              $vote_array[$slot_i][] = $c;
              $mentioned_explicitly[$c] = True;
            }
          unset($vote_array[$slot_i][$i_idx]);
        }

    // Expand "all others"
    $ao_idx = count($this->options)-1;
    foreach( $vote_array as $slot_i => $slot )
      foreach( $slot as $i_idx => $i )
        if ( $i == $ao_idx )
        {
          unset($vote_array[$slot_i][$i_idx]);
          foreach( $this->js_to_candid as $c => $db_candid )
            if ( !isset($mentioned_explicitly[$c]))
            {
              $vote_array[$slot_i][] = $c;
              $mentioned_explicitly[$c] = True;
            }
          break;
        }

    // Clean up empty positions, if any
    foreach( $vote_array as $i => $slot )
      if ( count($slot) == 0 )
        unset( $vote_array[$i] );

    // Sanity checks (debug / defensive programming)
    $mentioned = array();
    foreach( $vote_array as $slot_i => $slot )
      foreach( $slot as $i_idx => $i )
      {
        assert( !isset( $mentioned[$i] )); // must not be a duplicate
        assert( isset( $this->js_to_candid[$i] )); // must be persons
        $mentioned[$i] = True;
      }
    return $vote_array;
  }



  // Prints out a Javascript code that should be positioned in the <head> section
  // of the page. Parameter $pre_fill may contain an array as returned by Parse_Vote_Strings().
  // If $pre_fill is not empty, the ballot split is pre-filled accordingly.
  function Print_Ballot_Filler_Head( $pre_fill = Array() )
  {
?>
    <script language="JavaScript">
       /* Javascript portion of 'js-pref-voter'
        * http://iki.fi/elonen/code//js-pref-voter/
        *
        * Copyright (c) 2004,2005 Jarno Elonen <elonen@iki.fi>
        * The MIT License:
        *
        * Permission is hereby granted, free of charge, to any person
        * obtaining a copy of this software and associated documentation
        * files (the "Software"), to deal in the Software without
        * restriction, including without limitation the rights to use,
        * copy, modify, merge, publish, distribute, sublicense, and/or
        * sell copies of the Software, and to permit persons to whom the
        * Software is furnished to do so, subject to the following
        * conditions:
        *
        * The above copyright notice and this permission notice shall be
        * included in all copies or substantial portions of the Software.
        *
        * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
        * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
        * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
        * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
        * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
        * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
        * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
        */

        var n_options = <?php print $this->n_options; ?>;
        var others_idx = n_options-1;

        var n_places = <?php print $this->n_real_cands; ?>;
        var last_place = n_places-1;

        var bubble_enabled = true;
        var successive_swap_cycles = 0;

        var old_val_on_focus = -1;

        var votes = new Array(n_places);
        for ( i=0; i<n_places; i++ )
            votes[i] = 0;

        var shared_flags = new Array(n_places);
        for ( i=0; i<n_places; i++ )
            shared_flags[i] = 0;

        var cands = new Array(<?php foreach ( $this->options as $o ) printf('"%s", ', addslashes($o)); ?> false);


        // Try to find an object with given name
        function Find_Obj( name )
        {
            doc = document;
            if ( doc[name] )
                return doc[name];
            if ( doc.all && doc.all[name] )
                return doc.all[name];
            if ( doc.getElementById && doc.getElementById(name))
                return doc.getElementById(name);
            for (var i=0; i<doc.forms.length; i++ )
                if ( doc.forms[i][name] )
                    return doc.forms[i][name];
            for (var i=0; i<doc.anchors.length; i++ )
                if ( doc.anchors[i].name == name )
                    return doc.anchors[u];
            return null;
        }

        // Returns location [x,y] of given object (reference)
        function Get_Loc( obj )
        {
            var x=0, y=0;
            if (obj.offsetParent)
            {
                while (obj.offsetParent)
                {
                    x += obj.offsetLeft
                    y += obj.offsetTop
                    obj = obj.offsetParent;
                }
            }
            else if (obj.x || obj.y)
            {
                x += obj.x;
                y += obj.y;
            }
            return [x,y];
        }

        // Returns size [x,y] of given object (reference)
        function Get_Size( obj )
        {
            var w=24, h=24;
            if (obj.offsetWidth)
            {
                w = obj.offsetWidth;
                h = obj.offsetHeight;
            }
            return [w,h];
        }

        // Returns viewport Y-bounds [top,bottom]
        function Get_Viewport_Y_Bounds()
        {
            var top=-1, bottom=-1;

            if (window.innerHeight)
            {
                top = window.pageYOffset;
                bottom = top + window.innerHeight;
            }
            else if (document.documentElement && document.documentElement.scrollTop)
            {
                top = document.documentElement.scrollTop;
                bottom = document.documentElement.scrollBottom;
            }
            else if (document.body)
            {
                top = document.body.scrollTop;
                bottom = document.body.scrollBottom;
            }
            return [top, bottom];
        }

        // Returns [left, top, right, bottom] for object with given name or null if none was found
        function Get_Bounds( name )
        {
            var obj = Find_Obj( name );
            if( !obj )
                return null;
            if( obj.style )
                obj = obj.style;

            var x = parseInt( obj.left ) ? parseInt( obj.left ) : 0;
            var y = parseInt( obj.top ) ? parseInt( obj.top ) : 0;

            if ( x == 0 && y == 0 )
            {
                o = obj;
                if ( o.offsetParent )
                {
                    for( var x = 0, y = 0; o.offsetParent; o = o.offsetParent )
                    {
                        x += o.offsetLeft;
                        y += oLink.offsetTop;
                    }
                }
                else if ( o.x || o.y )
                {
                    x = o.x;
                    y = o.y;
                }
            }

            if ( obj.clip && typeof( obj.clip.bottom ) == 'number' )
                return [theDiv.clip.left, theDiv.clip.top, theDiv.clip.right, theDiv.clip.bottom];
            if ( typeof( obj.pixelWidth ) != 'undefined' )
                return [x, y, x+obj.pixelWidth, y+obj.pixelHeight];
            if ( typeof( obj.width ) != 'undefined' && typeof( obj.height ) != 'undefined' )
                return [x, y, x+parseInt(obj.width), y+obj.parseInt(height)];
            return [x,y,x+16,y+16];
        }

        // Move layer to given location and set it visible/invisible
        function Move_Layer( id, x, y)
        {
            var obj = Find_Obj( id );
            if( !obj )
            {
                ShowMessage("No such object: " + id);
                return;
            }
            if( obj.style )
                obj = obj.style;
            var pix = document.childNodes ? 'px' : 0;
            obj.left = x + pix;
            obj.top = y + pix;
        }

        function Set_Layer_Vis( id, visible )
        {
            var obj = Find_Obj( id );
            if( !obj )
            {
                ShowMessage("No such object: " + id);
                return;
            }
            if( obj.style )
                obj = obj.style;
            obj.visibility = visible ? 'visible' : 'hidden';
        }

        function TrimStr( txt )
        {
            while(txt.length > 0 && txt.charAt(txt.length-1) == ' ' || txt.charCodeAt(txt.length-1) == 0xA0)
                txt = txt.substring(0, txt.length-1);
            while(txt.length > 0 && txt.charAt(0) == ' ' || txt.charCodeAt(0) == 0xA0)
                txt = txt.substring(1);
            return txt;
        }

        function VoteText(i)
        {
            return TrimStr(cands[VoteVal(i)]);
        }

        function VoteVal(i)
        {
            return votes[i];
        }

        var coded_change_guard = 0;
        function SetVote(i, sel)
        {
            coded_change_guard++;
            votes[i] = sel;
            document.ballotform.elements["place_" + i].value = cands[sel];
            coded_change_guard--;
        }

        function PlaceDivided(i)
        {
            return shared_flags[i];
        }

        function SetPlaceDivided(i, divided)
        {
            shared_flags[i] = divided;
            new_val = "" + (i+1) + ".";
            if ( divided )
                new_val = "<?php print $this->shared_pos_str; ?>";
            document.ballotform.elements["shared_" + i].value = new_val;
        }

        function SwapPlaces(a,b)
        {
            var tmp = VoteVal(a);
            SetVote(a, VoteVal(b));
            SetVote(b, tmp);
        }

        function LastNonEmptyIdx()
        {
            for ( i=last_place; i>=0; i-- )
                if ( VoteVal(i) > 0 )
                    return i;
            return -1;
        }

        function ShowMessage( txt )
        {
            window.status = txt;
        }

        function ShowMessageOf( idx )
        {
            if ( VoteVal(idx) == 0 ) // dont' show anything for '-'
                return;
            if ( idx == 0 )
                ShowMessage( "'" + VoteText(idx) + "' is you first choise."  );
            else if ( LastNonEmptyIdx() == idx )
                ShowMessage( "'" + VoteText(idx) + "' is your LAST choise." );
            else if ( VoteVal(idx-1) != 0 && VoteVal(idx+1) != 0)
                ShowMessage( "You prefer '" + VoteText(idx) + "' over '" + VoteText(idx+1) +
                             "' but '" + VoteText(idx-1) + "' over '" + VoteText(idx) + "'." );
            else if ( VoteVal(idx+1) != 0 )
                ShowMessage( "You prefer '" + VoteText(idx) + "' over '" + VoteText(idx+1) + "'." );
            else if ( VoteVal(idx-1) != 0 )
                ShowMessage( "You prefer '" + VoteText(idx-1) + "' over '" + VoteText(idx) + "'." );
        }

        function Remove_Dupes( idx )
        {
            var sel = VoteVal(idx);
            if ( sel > 0 )
                for ( i=0; i<n_places; i++ )
                    if ( i != idx && VoteVal(i) == sel )
                    {
                        SetVote(i, 0);
                        oldplace = i;
                        newplace = idx;
                        if ( i < idx )
                            newplace -= 1; // compensate for the empty place
                        else
                            oldplace -= 1; // compensate for the added element
                        if ( newplace != oldplace )
                            ShowMessage( "'" + VoteText(idx) + "' moved from place " + (oldplace+1) +
                                         " to place " + (newplace+1) + "." );
                    }
        }

        function Add_Others()
        {
            var has_others = false;
            for ( i=0; i<n_places; i++ )
                if ( VoteVal(i) == others_idx )
                {
                    has_others = true;
                    break;
                }
            if ( !has_others && LastNonEmptyIdx() < last_place )
                SetVote( LastNonEmptyIdx()+1, others_idx );
        }

        function ScrollDownFrom( idx )
        {
            coded_change_guard++;
            var old_bubble_e = bubble_enabled;
            start = LastNonEmptyIdx() + 1;
            if (start>last_place)
                start = last_place;
            for ( i=start; i>idx; i-- )
            {
                SetVote(i, VoteVal(i-1));
                SetPlaceDivided(i, PlaceDivided(i-1));
            }
            bubble_enabled = old_bubble_e;
            coded_change_guard--;
        }

        function Bubble()
        {
            var did_switch = false;
            var iterations = successive_swap_cycles;
            if ( iterations < 1 )
                iterations = 1;

            while( bubble_enabled && iterations-- > 0 )
            {
                var last_was_empty = false;
                did_switch = false;
                for ( var i=0; i<n_places && bubble_enabled; i++ )
                {
                    var sel = VoteVal(i);
                    var cur_is_empty = ( sel == 0 );
                    if ( last_was_empty && !cur_is_empty )
                    {
                        SetPlaceDivided(i-1, PlaceDivided(i));
                        SetPlaceDivided(i, 0);
                        SwapPlaces( i, i-1 );
                        did_switch = true;
                    }
                    last_was_empty = cur_is_empty;
                }
                if ( did_switch )
                    successive_swap_cycles++;
                else
                {
                    successive_swap_cycles = 0;
                    break;
                }
            }

            if ( !did_switch )
                bubble_enabled = false;
        }

        function Bubble_Timer()
        {
            Bubble();
            if ( bubble_enabled )
                window.setTimeout("Bubble_Timer()", 200);
        }

        function Start_Bubble()
        {
            bubble_enabled = true;
            Bubble_Timer();
        }

        function Remove_Handler( i )
        {
            ShowMessage( "Removed '" + VoteText(i) + "'." );
            SetVote(i, 0);
            Start_Bubble();
        }
        function Up_Handler( i )
        {
            SwapPlaces(i, i-1);
            ShowMessageOf(i-1);
            Start_Bubble();
        }
        function Down_Handler( i )
        {
            SwapPlaces(i, i+1);
            ShowMessageOf(i+1);
            Start_Bubble();
        }

        function Clear_All()
        {
            for ( i=0; i<n_places; i++ )
            {
                SetVote(i, 0);
                SetPlaceDivided(i, 0);
            }
            ShowMessage( "Vote sheet cleared." );
        }

        function Toggle_Shared( idx )
        {
            ShowMessage("Toggle " + idx);
            if ( !PlaceDivided(idx))
            {
                SetPlaceDivided(idx, 1);
                ShowMessage( "Place " + (idx+1) + " is now divided with place " + idx + "." );
            }
            else
            {
                SetPlaceDivided(idx, 0);
                ShowMessage( "Place " + (idx+1) + " is now distinct (non-divided) from place " + idx + "." );
            }
        }

        var cand_selector_at = -1;
        function Place_Click_Handler( i )
        {
            if ( cand_selector_at == i )
            {
                cand_selector_at = -1;
                Hide_Cand_Selector();
            }
            else
            {
                cand_selector_at = i;
                var butt = Find_Obj( "place_" + i );
                var loc = Get_Loc( butt );

                var butt_sz = Get_Size(butt);

                // Try to keep the popup inside the browser frame
                var sz = Get_Size(Find_Obj("candsel"));
                var y_bounds = Get_Viewport_Y_Bounds();
                if ( y_bounds[1] > 0 && loc[1]+sz[1] > y_bounds[1] )
                    loc[1] -= (sz[1] + butt_sz[1]);

                Move_Layer( "candspopup", loc[0], loc[1] + butt_sz[1] );
                Set_Layer_Vis( "candspopup", true );
                Find_Obj("candsel").focus()

                // Restore scrolling offset in case the
                // browser tried to be smart and decided
                // to scroll the viewport    print_r($this->options);

                var new_y_bounds = Get_Viewport_Y_Bounds();
                if ( new_y_bounds[0] != y_bounds[0] )
                    scrollTo(0, y_bounds[0]);
            }
        }

        function Hide_Cand_Selector()
        {
            Move_Layer('candspopup', -500,0);
            Set_Layer_Vis( 'candspopup', false );
            idx = cand_selector_at;
            Remove_Dupes(idx);
            Start_Bubble();
        }

        function Cand_Sel_Handler()
        {
            idx = cand_selector_at;
            window.setTimeout("Hide_Cand_Selector(); cand_selector_at = -1;", 100);

            sel = Find_Obj("candsel").value;

            bubble_enabled = false;
            if ( VoteVal(idx) > 0 && sel > 0 &&
                 (VoteVal(idx) != others_idx || LastNonEmptyIdx() < last_place))
                    ScrollDownFrom(idx);
            SetVote(idx, sel);

            // Add 'all others' before the selection if
            // there is empty space before it
            if ( idx >= 2 && VoteVal(idx-1) == 0 && VoteVal(idx-2) == 0 )
            {
                var has_others = false;
                for ( var i=0; i<n_places; i++ )
                    if ( VoteVal(i) == others_idx )
                    {
                        has_others = true;
                        break;
                    }
                if ( !has_others )
                    SetVote(idx-1, others_idx);
            }

            ShowMessageOf( idx );
            Remove_Dupes( idx );
            Add_Others();
            Start_Bubble();
        }

        function Set_Votes_Field()
        {
            // Clean up the ballot form first
            bubble_enabled = true;
            while ( bubble_enabled )
                Bubble();

            var res = "";
            for ( var i=0; i<n_places; i++ )
                if ( VoteVal(i) > 0 )
                {
                    var p = i;
                    while ( PlaceDivided(p))
                        p -= 1;
                    res = res + "" + (p+1) + "-" + VoteVal(i) + "_";
                }
            document.ballotform.elements["vote"].value = res;
        }

        // This should be called upon loading the page
        function Prefill_Form()
        {
<?php
            foreach( $pre_fill as $pos => $cands )
            {
                $first = true;
                foreach ( $cands as $ci )
                {
                    print "                SetVote($pos, " . $ci . ");\n";
                    if ( !$first )
                        print "                SetPlaceDivided($pos, 1);\n";
                    $pos++;
                    $first = false;
                }
            }
?>
        }

    </script>
<?php
}

  function Print_Ballot_Filler_Table()
  {
?>
    <div style="width: 100%;" onMouseDown="Hide_Cand_Selector();">
      <input type="hidden" name="vote" value="">
      <table>
<?php
    for ( $i=0; $i<$this->n_real_cands; $i++ )
    {
        print "<tr>\n";
        print '<td><input type="button" name="shared_'.$i.'" value="' . ($i+1) . '." ';
        if ( $i > 0 )
              printf( 'onClick="Toggle_Shared(%d);"', $i );
        else
            print "disabled";
        print ' title="Toggle divided place"></td>' . "\n";

        print '<td><input type="button" style="width: 200px; position: relative;" name="place_'.$i.'" onClick="Place_Click_Handler('.$i.')" value="' .
            $this->options[0] . '" title="Click to insert a candidate at position ' . ($i+1) . '">';
        print "</td>\n";

        print '<td><a href="#" target="_self" onClick="Remove_Handler('.$i.'); return false;" title="Remove">'.$this->del_symbol.'</a></td>' . "\n";

        if ( $i < $this->n_real_cands-1 )
            print '<td><a href="#" target="_self" onClick="Down_Handler('.$i.'); return false;">'.$this->down_symbol.'</a></td>';
        else
            print '<td>&nbsp;</td>';
        print "\n";

        if ( $i > 0 )
            print '<td><a href="#" target="_self" onClick="Up_Handler('.$i.'); return false;">'.$this->up_symbol.'</a></td>';
        else
            print '<td>&nbsp;</td>';
        print "\n";

        print "</tr>\n";
    }
?>
      </table>
    </div>

      <div id="candspopup" style="background: white; visibility: hidden; display: block; position: absolute; left: 16px; top: 16px; z-index: 200; border: black thin solid;">

          <select size="10" name="candsel" name="candsel"  onChange="Cand_Sel_Handler();" onBlur="Hide_Cand_Selector();">
<?php
        foreach( $this->options as $k => $v )
             print '<option value="' . $k . '">' . str_replace(" ", "&nbsp;", htmlentities($v)) . "</option>\n";
?>
          </select>
      </div>
<?php
  }
}
?>
