9

I have an HTML5 'range' control to which I want to add a plus (+) and minus (-) buttons on either sides.

The fiddle works fine, except that the value increase (or decrease) only once on 'click and hold'. While I want is it should increase (or decrease) continuously.

Fiddle

HTML,

<input type='button' id='minus'/>
<div class='range-container'>
    <input id='range' type='range' min='0' max='100' step='1'/>
</div>
<input type='button' id='plus'/>

JavaScript,

$('#plus').click(function() {
    $('#range').val(parseInt($('#range').val()) + 1);
});

$('#minus').click(function() {
    $('#range').val(parseInt($('#range').val()) - 1);
});

HTML5 'number' control have this experience natively.

Looked through SO, couldn't find this question anywhere. Closest I got is, this, which again does only one click.

Community
  • 1
  • 1
anoopelias
  • 9,240
  • 7
  • 26
  • 39
  • Use setInterval to run a function continuously while the mouse is down. Increment in that function. clear the interval on mouseup and start it on mousedown. – Sergiu Paraschiv Jan 24 '15 at 16:25
  • @SergiuParaschiv Thanks, will try. Though, I was hoping for a more 'native'/jQuery solution, since it is supported on number control. – anoopelias Jan 24 '15 at 16:44
  • @SergiuParaschiv Actually it's not as simple as that. There are many strings attached. Primarily detecting a `click` vs. a `mousedown` and doing the right thing when there is no `mouseup` event because the mouse has moved outside of the button. Secondly there would be finding a delays and an easing that works and making the whole thing work independently of the number range. – Tomalak Jan 24 '15 at 16:51
  • Obviously. That's why I did not post that as an _answer_. It's a starting point. – Sergiu Paraschiv Jan 24 '15 at 16:53

5 Answers5

7

You can use requestAnimationFrame to constantly check if any button is still pressed. If still pressed, you can increment or decrement your value.

  • Create a 'number' variable that starts at zero.
  • If the Add button is pressed, set an 'isDown' variable to 1.
  • If the Subtract button is pressed, set the 'isDown' variable to -1.
  • If any button is released, set the 'isDown' variable to 0;
  • Start a requestAnimationFrame loop that constantly checks if 'isDown' is not zero. If not zero, requestAnimationFrame changes the 'number' variable by the isDown value.

Here's example code and a Demo:

var $result=$('#result');
var number=0;
var isDown=0;
var delay=250;
var nextTime=0;

requestAnimationFrame(watcher);

$("button").mousedown(function(e){handleMouseDown(e);});
$("button").mouseup(function(e){handleMouseUp(e);});
$("button").mouseout(function(e){handleMouseUp(e);});


function handleMouseDown(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  // Put your mousedown stuff here
  isDown=(e.target.id=='Add')?1:-1;
}

function handleMouseUp(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  // Put your mouseup stuff here
  isDown=0;
}

function watcher(time){
  requestAnimationFrame(watcher);
  if(time<nextTime){return;}
  nextTime=time+delay;
  if(isDown!==0){
    number+=isDown;
    $result.text(number);
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button id=Add>Add</button>
<button id=Subtract>Subtract</button>
<span id='result'>0</span>
markE
  • 102,905
  • 11
  • 164
  • 176
  • When I click the mouse button and move the mouse out before releasing it, your example code keeps counting. – Tomalak Jan 24 '15 at 16:46
  • I've been struggling with that as well. Isn't there an event that I can listen to? I mean, the *browser* surely knows that the button has been released and animates it accordingly. Using `mouseout` is a dirty hack that does not align with how the UI works. – Tomalak Jan 24 '15 at 16:53
  • 2
    Nice use of `requestAnimationFrame()` though, I had been trying with `setInterval()`. – Tomalak Jan 24 '15 at 16:55
  • Yep, I started using `requestAnimationFrame` a while back because it feeds in a timestamp. While that timestamp is not really needed here, it can be useful in other situations where the delay between actions must be variable. – markE Jan 24 '15 at 16:59
  • Thank you. Accepting this answer since `requestAnimationFrame` seems cleaner approach. – anoopelias Jan 24 '15 at 17:30
  • 1
    @anoopelias Check whether it is supported in all your target browsers: http://caniuse.com/#feat=requestanimationframe. – Tomalak Jan 24 '15 at 17:41
  • 2
    @Tomalak, it's fairly well supported in modern browsers. If you are working with older browsers then check this link which contains a polyfill that drops back to using setInterval if requestAnimationFrame is not available: http://creativejs.com/resources/requestanimationframe/ – markE Jan 24 '15 at 17:45
  • For the accepted answer code snippet, how to stop when `$result.text()` gets to zero? The following achieves the desired effect but only stops decreasing the amount at the end of the function, I think it could probably be done somewhere sooner: https://jsfiddle.net/rwone/ta2vajff/ – user1063287 Feb 13 '16 at 14:32
  • It's not working properly, as if you tap a button and miss timing - nothing will happen. – Marek Dec 21 '16 at 13:27
  • If you want this to one decimal place... https://jsfiddle.net/85cnLg3b/ –  Feb 12 '18 at 21:22
3

Edit, Updated

Try

var range = $("#range")
, fx = function(elem, prop) {
  return elem
    .animate({
      value: range.prop(prop)
    }, {
      duration: 3000,
      easing: "linear",
      step: function(now) {
             elem.val(now + prop === ("max","+"||"min","-") + elem.prop("step"))
      }
    })
};

$('#plus').mousedown(function(e) {
  fx(range, "max")
});

$('#minus').mousedown(function minus(e) {
  fx(range, "min")
});

$(document).mouseup(function(e) {
  range.stop(true, false)
});

jsfiddle http://jsfiddle.net/bnesu3h9/3/

var range = $("#range")
, fx = function(elem, prop) {
  return elem
    .animate({
      value: range.prop(prop)
    }, {
      duration: 3000,
      easing: "linear",
      step: function(now) {
        elem.val(now + prop === ("max","+"||"min","-") + elem.prop("step"))
      }
    })
};

$('#plus').mousedown(function(e) {
  fx(range, "max")
});

$('#minus').mousedown(function minus(e) {
  fx(range, "min")
});

$(document).mouseup(function(e) {
  range.stop(true, false)
});
#plus {
  width: 15px;
  height: 15px;
  background-color: red;
  float: left;
}
#minus {
  width: 15px;
  height: 15px;
  background-color: blue;
  float: left;
}
.range-container {
  float: left;
  overflow: auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<input type='button' id='minus' />
<div class='range-container'>
  <input id='range' type='range' min='0' max='100' step='1' />
</div>
<input type='button' id='plus' />
guest271314
  • 1
  • 15
  • 104
  • 177
  • 1
    That does not work very well. Try moving the mouse pointer out of the button before releasing it. Also don't use global variables. – Tomalak Jan 24 '15 at 17:00
  • That's actually pretty good. I'd argue that's a lot better than my solution. I'll try and remember to use `animate` for this kind of stuff in the future. Now just honor the `step` attribute and make it ID-agnostic and you're all set. ;) – Tomalak Jan 24 '15 at 19:00
  • Nice :) Yeah, I know it exists, I just need to remember to think of it. – Tomalak Jan 24 '15 at 19:16
  • 1
    I've updated your fiddle. http://jsfiddle.net/bnesu3h9/4/. I'm gonna delete my answer in favor of this approach. – Tomalak Jan 24 '15 at 20:09
  • Interesting solution, however it will not work for my particular case, because I would need more than a CSS change in a step. (That detail was left out in the question for simplicity. See `$imageCropper.cropit('zoom', .75);` under http://scottcheng.github.io/cropit/ ). – anoopelias Jan 25 '15 at 05:40
  • @anoopelias Not tried `cropit` . Should be possible to various stuff inside `step` function . Utilized `step=1` as it was present at `html` , appeared to be desired increment / decrement value ? Not certain how `$imageCropper.cropit('zoom', .75);` implemented , incorporated into "detail was left out in the question" ? Would solution be implemented at `class=icon icon-image large-image` , `class=icon icon-image small-image` ? Thanks – guest271314 Jan 25 '15 at 14:32
  • @guest271314 I am using cropit, but the problem is I am not sure what it will do on calling `cropit`/`zoom`. I just use it as a js library. – anoopelias Jan 27 '15 at 05:52
2

This answer should help.

The click event includes mouseup and mousedown. You'll want to handle mousedown alone at first, and continuously check to see if the mouse is still down. You can stop checking on document mouseup.

Community
  • 1
  • 1
chris
  • 1,831
  • 18
  • 33
  • what's this have to do with polling? polling is related to ajax and http calls to a server. – Sergiu Paraschiv Jan 24 '15 at 16:29
  • The basic idea is just checking over and over. [Polling doesn't always mean AJAX](http://www.cs.umd.edu/class/sum2003/cmsc311/Notes/IO/polling.html). Or [here for a jQuery example that isn't related to AJAX](http://benalman.com/code/projects/jquery-dotimeout/examples/delay-poll/). – chris Jan 24 '15 at 16:33
  • yes, I know that. you linked to an article about http polling though. I think that will create more confusion, not help a beginner. – Sergiu Paraschiv Jan 24 '15 at 16:36
  • And _you_ should make sure you read the whole story before accusing people. Did you read the one where I say "yes, I know that" and "that's not the point"? – Sergiu Paraschiv Jan 24 '15 at 17:02
1

The very basic approach to this is to start looping at certain interval while one of buttons is pressed, doing value changes at each tick. Start when button is clicked, stop when it's released. Here's simplistic code for concept demonstration purpose only:

// Store the reference
var range = $('#range');

// These functions will change the value
function increment () {
    range.val(parseInt(range.val()) + 1)
}

function decrement () {
    range.val(parseInt(range.val()) - 1)
}

// Attaches polling function to element
function poll (el, interval, fn) {
    var isHeldDown = false;

    return el
    .on("mousedown", function() {
        isHeldDown = true;

        (function loop (range) {
            if (!isHeldDown) return; // Stop if it was released
            fn();
            setTimeout(loop, interval); // Run the function again       
        })();
    })
    .on("mouseup mouseleave", function () {
        isHeldDown = false; // Stop polling on leave or key release
    });
}

poll($("#plus"), 40, increment);
poll($("#minus"), 40, decrement);

JSFiddle.

In production grade version you'd want to apply timing function instead of constant interval; think about all possible events that should start and stop the polling so it won't stuck forever when user moves pointer away or something; use requestAnimationFrame to control timing function more precisely.

0

this is a question that is asked a lot. I have answered on a duplicate thread with a working snippet that exactly does what you are looking for.

https://stackoverflow.com/a/70957862/13795525

function imposeMinMax(el) {
    if (el.value != '') {
        if (parseInt(el.value) < parseInt(el.min)) {
            el.value = el.min;
        }
        if (parseInt(el.value) > parseInt(el.max)) {
            el.value = el.max;
        }
    }
}


var index = 0;
var interval;
var timeout;
var stopFlag=false;

function clearAll(){
   clearTimeout(timeout);
   clearInterval(interval);
}


function modIn(el) {
   var inId = el.id;
   if (inId.charAt(0) == 'p') {
      var targetId = inId.charAt(2);      
      var maxValue = Number(document.getElementById(targetId).max);
      var actValue = Number(document.getElementById(targetId).value);
      index = actValue;
      if(actValue < maxValue){
         stopFlag=false;
         document.getElementById(targetId).value++;
      }else{
      stopFlag=true;
      }
      timeout = setTimeout(function(){
      interval = setInterval(function(){        
         if(index+1 >= maxValue){
            index=0;
            stopFlag=true;
         }  
         if(stopFlag==false){
            document.getElementById(targetId).value++;
         } 
         index++;
      }, 100);
      }, 500);      
   imposeMinMax(document.getElementById(targetId));
   }
   if (inId.charAt(0) == 'm') {
      var targetId = inId.charAt(2);
      var minValue = Number(document.getElementById(targetId).min);
      var actValue = Number(document.getElementById(targetId).value);
      index = actValue;
      if(actValue > minValue){
         stopFlag=false;
         document.getElementById(targetId).value--;
      }else{
         stopFlag=true;
      }
      timeout = setTimeout(function(){
         interval = setInterval(function(){        
            if(index-1 <= minValue){
               index=0;
               stopFlag=true;
            }  
            if(stopFlag==false){
               document.getElementById(targetId).value--;
            } 
            index--;
         }, 100);
         }, 500);      
      imposeMinMax(document.getElementById(targetId));
   }
}
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Button example</title>
  <meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>

  <button type='button' class='btn btn-danger btn-sm ' id='mbA' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>-</button>
  <input type='number' id='A'  onchange='imposeMinMax(this)' value='200' max='350' min='150' step='1' style='width: 50px;'>                 
  <button type='button' class='btn btn-danger btn-sm ' id='pbA' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>+</button>&nbsp;

  <button type='button' class='btn btn-danger btn-sm signBut' id='mbB' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>-</button>
  <input type='number'  id='B'  onchange='imposeMinMax(this)' value='250' max='450' min='150' step='1' style='width: 50px;'>                 
  <button type='button' class='btn btn-danger btn-sm ' id='pbB' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>+</button>&ensp;

  <button type='button' class='btn btn-danger btn-sm signBut' id='mbC' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>-</button>
  <input type='number'  id='C'  onchange='imposeMinMax(this)' value='3' max='10' min='1' step='1' style='width: 50px;'>                 
  <button type='button' class='btn btn-danger btn-sm ' id='pbC' onmousedown='modIn(this)' onmouseup='clearAll()' onmouseleave='clearAll()'>+</button>

</body>



</html>
Pietro
  • 127
  • 6
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/31004468) – Mario Petrovic Feb 07 '22 at 09:04
  • 1
    I have added the snippet in the answer as well. – Pietro Feb 07 '22 at 10:43