tags:

views:

41

answers:

1

I have designed a jQuery Slider Control that takes a 'pool' of points, and allows them to be distributed between multiple sliders. It works pretty well, except that I am having some problems with very small overflow increments.

Basically, it is possible to make adjustments in large quantities based on mouse movement, and so it lets someone 'spend' more in a slider than intended. I am at a loss as to how to deal with this. Posted below is my entire code.

To test it, build a simple HTML page with this code and try sliding the first two sliders all the way to 500, then try sliding the third. It won't slide (intended behavior).

Then, slide the first or second slider back a little bit (subtracting), and slide the third forward. You are able to occasionally go over the intended 'spendable' bounds.

Sample will require jQuery UI, latest version from google CDN.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"&gt;&lt;/script&gt;
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.js"&gt;&lt;/script&gt;

Javascript

<style type="text/css">
    #eq > div {
        width:300px;
    }
    #eq > div .slider {
        width: 200px; float: left;
    }
    #eq > div > span {
        float: right;
    }
</style>

<script type="text/javascript">
    $(document).ready(function () {
        var spendable = 1000;
        var spent = 0;

        function spend(quantity) {
            spent += quantity;
            $('#attemptedToSpend').text(spent);
        }

        function change(previous, current) {
            var adjustment = (current - previous);
            $('#change').text(adjustment);
            return adjustment;
        }

        function calculateSpent() {
            var totalled = 0;

            $("#eq .slider").each(function () {
                totalled += parseInt($(this).parent('div:eq(0)').find('.spent').text());
            });

            $('#spent').text(totalled);
        }

        $("#eq .slider").each(function () {
            var current = 0;
            var previous = 0;
            var adjustment = 0;

            $(this).slider({
                range: "min",
                value: 0,
                min: 0,
                max: 500,
                step: 1,
                animate: true,
                orientation: "horizontal",
                start: function (event, ui) {
                },
                stop: function (event, ui) {
                },
                slide: function (event, ui) {
                    // set the current value to whatever is selected.
                    current = ui.value;

                    // determine the adjustment being made relative to the last
                    // adjustment, instead of just the slider's value.
                    adjustment = change(previous, current);

                    if (spent >= spendable) {
                        if (adjustment > 0)
                            return false;
                    }

                    // spend the points, if we are able to.
                    spend(adjustment);

                    // set the previous value
                    previous = current;

                    $(this).parent('div:eq(0)').find('.spent').text(current);

                    calculateSpent();

                }
            });
        });
    });
</script>

Html

<p class="ui-state-default ui-corner-all" style="padding: 4px; margin-top: 4em;">
    <span style="float: left; margin: -2px 5px 0 0;"></span>Distribution
</p>
    <strong>Total Points Spendable: </strong><div id="spendable">1000</div>
    <strong>Total Points Spent (Attempted): </strong><div id="attemptedToSpend">0</div>
    <strong>Total Points Spent: </strong><div id="spent">0</div>
    <strong>Change: </strong><div id="change">0</div>
    <div id="status"></div>

<div id="eq">
    <div style="margin: 15px;" id="test1">Test1</div>
    <br />
    <div class="slider"></div><span class="spent">0</span>

    <div style="margin: 15px;" id="test2">Test2</div>
    <br />
    <div class="slider"></div><span class="spent">0</span>

    <div style="margin: 15px;" id="test3">Test3</div>
    <br />
    <div class="slider"></div><span class="spent">0</span>

    <div style="margin: 15px;" id="test4">Test4</div>
    <br />
    <div class="slider"></div><span class="spent">0</span>

    <div style="margin: 15px;" id="test5">Test5</div>
    <br />
    <div class="slider"></div><span class="spent">0</span>
</div>
+1  A: 

Hi!

I tried keeping your script intact, but ended up largely rewriting it. It should be solid now. Good news: I have only changed the JS, everything else is intact, though I don't update all your monitoring fields any more.

DEMO

$(
  function()
  {
    var
        maxValueSlider = 500,
        maxValueTotal = 1000,
        $sliders = $("#eq .slider"),
        valueSliders = [],
        $displaySpentTotal = $('#spent');

    function arraySum(arr)
    {
      var sum = 0, i;
      for(i in arr) sum += arr[i];
      return sum;
    }

    $sliders
      .each(
        function(i, slider)
        {
          var
            $slider = $(slider),
            $spent = $slider.next('.spent');
          valueSliders[i] = 0;
          $slider
            .slider(
              {
                range: 'min',
                value: 0,
                min: 0,
                max: maxValueSlider,
                step: 1,
                animate: true,
                orientation: "horizontal",
                slide:
                  function(event, ui)
                  {
                    var
                      sumRemainder = arraySum(valueSliders) - valueSliders[i],
                      adjustedValue = Math.min(maxValueTotal - sumRemainder, ui.value);
                    valueSliders[i] = adjustedValue;
                    // display the current total
                    $displaySpentTotal.text(sumRemainder + adjustedValue);
                    // display the current value
                    $spent.text(adjustedValue);
                    // set slider to adjusted value
                    $slider.slider('value', adjustedValue);

                    // stop sliding (return false) if value actually required adjustment
                    return adjustedValue == ui.value;
                  }
              }
            );
        }
      );
  }
);
Thomas
Thank you so much for your help, this will be invaluable in helping me understand it better. I'm trying to figure out what you did that fixed the problem.
Stacey
@Stacey: I couldn't tell you, to be honest. I've gone through a number of theories as to what exactly was causing the problems, and for a while, I tried fixing those exact issues by altering your implementation. Then at some point, I decided just to rewrite it from scratch, to see how I would have done it, and it turned out I didn't run into the same problem. The best candidate I can remember was that `ui.value` in the slide event and the method `$slider.slider('value')` called from the same event would have just that discrepancy, i.e. `$slider.slider('value')` would always be off by 1-3.
Thomas
But then again, I couldn't find any point in your script where that would have skewed the results. The two main changes in my approach are (1.) that I store my values in an array, as opposed to parsing them from the mark-up, and (2.) that I actually adjust the value of the offending slider. I use the mark-up really just for display purposes, and not to read in the values.
Thomas
I think it boils down to you just knew what you were doing better than me. I am still just a rookie developer, and even more infantile with Javascript. (I couldn't even use javascript without jQuery.)
Stacey