最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Detecting number input spinner click - Stack Overflow

programmeradmin4浏览0评论

I've got a simple number input with a min="1" and max="12" value set, this is used as an hour selector. I'd like it to cycle through the hours, so when you get to 12 and press the "up" arrow, it goes back to 1 and vice-versa as well.

Right now I have this mostly working:

var inputTimer = null;

function cycle(element) {
  if (element.attributes.max && element.attributes.min) {
    var prevVal = element.value;
    inputTimer = setTimeout(function() {
      if (prevVal === element.attributes.max.value) {
        element.value = element.attributes.min.value;
      } else if (prevVal === element.attributes.min.value) {
        element.value = element.attributes.max.value;
      }
    }, 50);
  }
}

$("input[type='number']")
  .on("mousedown", function(e) {
    //this event happens before the `input` event!
    cycle(this);
  }).on('keydown', function(e) {
    //up & down arrow keys
    //this event happens before the `input` event!
    if (e.keyCode === 38 || e.keyCode === 40) {
      cycle(this);
    }
  }).on('input', function(e) {
    //this event happens whenever the value changes
    clearTimeout(inputTimer);
  });
<script src=".1.1/jquery.min.js"></script>
<input type="number" min="1" max="12" value="12" />

I've got a simple number input with a min="1" and max="12" value set, this is used as an hour selector. I'd like it to cycle through the hours, so when you get to 12 and press the "up" arrow, it goes back to 1 and vice-versa as well.

Right now I have this mostly working:

var inputTimer = null;

function cycle(element) {
  if (element.attributes.max && element.attributes.min) {
    var prevVal = element.value;
    inputTimer = setTimeout(function() {
      if (prevVal === element.attributes.max.value) {
        element.value = element.attributes.min.value;
      } else if (prevVal === element.attributes.min.value) {
        element.value = element.attributes.max.value;
      }
    }, 50);
  }
}

$("input[type='number']")
  .on("mousedown", function(e) {
    //this event happens before the `input` event!
    cycle(this);
  }).on('keydown', function(e) {
    //up & down arrow keys
    //this event happens before the `input` event!
    if (e.keyCode === 38 || e.keyCode === 40) {
      cycle(this);
    }
  }).on('input', function(e) {
    //this event happens whenever the value changes
    clearTimeout(inputTimer);
  });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="number" min="1" max="12" value="12" />

Working DEMO

The issue I have is that I can't find a way to detect if the arrow spinners in the input have been clicked, or just the input as a whole has been clicked. Right now it has an issue where it changes the value when you click anywhere in the field when the value is currently at 1 or 12

Is there a way to detect if the click event occurs on the spinners/arrows within the text field?

Share Improve this question edited Dec 3, 2015 at 17:23 Tomáš Zato 53.1k63 gold badges307 silver badges821 bronze badges asked Dec 3, 2015 at 17:14 Chris BarrChris Barr 34k28 gold badges102 silver badges152 bronze badges 5
  • onchange event is working fine jsfiddle.net/29bea3y0 – Ramanlfc Commented Dec 3, 2015 at 17:27
  • @Ramanlfc no it isn't. I can't cycle around back to the min/max values like in my demo above. – Chris Barr Commented Dec 3, 2015 at 17:53
  • Perhaps the answer here, on how to get mouse co-ordinates could be used in combination with what you already have: stackoverflow.com/questions/7790725/… – Martin Eyles Commented Aug 20, 2020 at 1:46
  • I have also looked at using this.style.cursor, but it always returns "", and $(this).css('cursor'), but it always returns "text", whether or not you are over the up/down arrows. – Martin Eyles Commented Aug 20, 2020 at 16:22
  • Posted an alternate answer here: stackoverflow.com/a/76673785/3622569 – Ben in CA Commented Jul 12, 2023 at 19:15
Add a comment  | 

9 Answers 9

Reset to default 2

You have to handle the input event, like this:

$('[type=number]').on('input',function(){
  this.value %= 12 ;
  if( this.value < 1 )
    this.value -= -12 ;
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type=number>

I searched a lot and it seems there is no way to natively detect that. That makes this one a very important question because I think this should be added to new versions of HTML.

There are many possible workarouds. They all fail on the problem the it's impossible to know, in which direction is value going. I decided to use mouse position information to detect, whether is user increasing or decreasing a value. It works, but does not properly handle the situation, when user holds the button.

var inputTimer = null;

function cycle(event) {
  var value = this.value;
  // Value deep within bonds -> no action
  if(value>this.min && value<this.max) {
    return;  
  }
  // Check coordinate of the mouse
  var x,y;
  //This is the current screen rectangle of input
  var rect = this.getBoundingClientRect();
  var width = rect.right - rect.left;
  var height = rect.bottom-rect.top;
  //Recalculate mouse offsets to relative offsets
  x = event.clientX - rect.left;
  y = event.clientY - rect.top;
  // Now let's say that we expect the click only in the last 80%
  // of the input
  if(x/width<0.8) {
    console.log("Not click on arrows.", x, width);  
    return;
  }
  // Check "clicked button" by checking how high on input was clicked
  var percentHeight = y/height;
  // Top arrow was clicked
  if(percentHeight<0.5 && value==this.max) {
      this.value = this.min; 
      event.preventDefault();
      return false;
  }
  // Bottom arrow was clicked
  if(percentHeight>0.5 && value==this.min) {
      this.value = this.max; 
      event.preventDefault();
      return false;
  }
}
var input = document.getElementById("number");
input.addEventListener("mousedown", cycle);
<input id="number" type="number" min="1" max="12" value="12" />

For your use case, you can just use the normal input event:

const input = document.querySelector('input');

input.addEventListener('input', (e) => {
  if (e.target.value > 12) {
    e.target.value = 0
  }
  if (e.target.value < 0) {
    e.target.value = 12
  }
})
<input type="number" value="0" />

This not only cycles the value, but also prevents any manual input outside of the 0-12 range.

But if you really want to detect whether it is by manual input or clicking on the spinner, you can use event.constructor. For manual input (and pasting), event is an instance of InputEvent but when you cycle using the arrows (either by click or keyboard) then the event is an instance of Event:

const input = document.querySelector('input');

input.addEventListener('input', (e) => {
  if(e.constructor === InputEvent) {
    //This part won't run when the user uses the spinner
    e.target.value = Math.min(12, Math.max(0, e.target.value));
  }
  
  if (e.target.value > 12) {
    e.target.value = 0
  }
  if (e.target.value < 0) {
    e.target.value = 12
  }
})
<input type="number" value="0" />

This code makes sure that when the user enters something larger than 12, it will be reset to 12 (unlike the previous example that set it to 0)

So it works great for both use cases: manual input and clicking on the arrows

A method you could try is by using the Attributes of the element to track what the previous value is. This isn't, of course, actually tracking which button got hit but it's the closest I've been able to get.

JS:

$(document).ready(function() {
    function Init(){
        var test = document.getElementById('test');
        test.setAttribute('prev', 0);
    }   
    
    Init()

    $('#test').on('input', function() {
        var test = document.getElementById('test')
        var d = test.value - test.getAttribute('prev');
        console.log(d);
        test.setAttribute('prev', test.value);
    });
});

HTML:

<input type="number" id="test">

Then all you would have is logic that says if d(irection) is positive, they clicked up. If negative, they clicked down. If it's 0 then they didn't click a button.

$(document).ready(function() {
  function Init() {
    var test = document.getElementById('test');
    test.setAttribute('prev', 0);
  }

  Init()

  $('#test').on('input', function() {
    var test = document.getElementById('test')
    var d = test.value - test.getAttribute('prev');
    console.log(d);
    test.setAttribute('prev', test.value);
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<input type="number" id="test">

I think this is what you really want.

<input type="time" value="01:00" step="600"/>

There is currently no native way to capture the arrow input events separate from the input box events. Everything using number input seems to be kinda hacky for this purpose.

Next best option is something like http://jdewit.github.io/bootstrap-timepicker/

This doesn't work for your specific situation where you have a maximum and want it to wrap, but it might be helpful for others who want to process the field value based on changes via arrows, such as for setting .toFixed(2) to a currency value like I needed:

document.getElementById('el').setAttribute('data-last',document.getElementById('el').value);
document.getElementById('el').addEventListener('keyup', function(){
    this.setAttribute('data-last',this.value);
});
document.getElementById('el').addEventListener('click', function(){
    if(this.value>this.getAttribute('data-last')) console.log('up clicked');
    if(this.value<this.getAttribute('data-last')) console.log('down clicked');
});

This is my code written in JQuery , this one can implement auto-increment ( + & - ) long-press spin buttons .

$.fn.spinInput = function (options) {
    var settings = $.extend({
        maximum: 1000,
        minimum: 0,
        value: 1,
        onChange: null
    }, options);

    return this.each(function (index, item) {
        var min = $(item).find('>*:first-child').first();
        var max = $(item).find('>*:last-child').first();
        var v_span = $(item).find('>*:nth-child(2)').find('span');
        var v_input = $(item).find('>*:nth-child(2)').find('input');
        var value = settings.value;
        $(v_input).val(value);
        $(v_span).text(value);
        async function increment() {
            value = Number.parseInt($(v_input).val());
            if ((value - 1) > settings.maximum) return;
            value++;
            $(v_input).val(value);
            $(v_span).text(value);
            if (settings.onChange) settings.onChange(value);
        }
        async function desincrement() {
            value = Number.parseInt($(v_input).val());
            if ((value - 1) < settings.minimum) return;
            value--
            $(v_input).val(value);
            $(v_span).text(value);
            if (settings.onChange) settings.onChange(value);
        }
        var pressTimer;

        function actionHandler(btn, fct, time = 100, ...args) {
            function longHandler() {
                pressTimer = window.setTimeout(function () {
                    fct(...args);
                    clearTimeout(pressTimer);
                    longHandler()
                }, time);
            }
            $(btn).mouseup(function () {
                clearTimeout(pressTimer);
            }).mousedown(function () {
                longHandler();
            });

            $(btn).click(function () {
                fct(...args);
            });
        }

        actionHandler(min, desincrement, 100);
        actionHandler(max, increment, 100)
    })
}




$('body').ready(function () {
    $('.spin-input').spinInput({ value: 1, minimum: 1 });
});
:root {
    --primary-dark-color: #F3283C;
    --primary-light-color: #FF6978;
    --success-dark-color: #32A071;
    --sucess-light-color: #06E775;
    --alert-light-color: #a42a23;
    --alert-dark-color: #7a1f1a;
    --secondary-dark-color: #666666;
    --secondary-light-color: #A6A6A6;
    --gold-dark-color: #FFA500;
    --gold-light-color: #FFBD00;
    --default-dark-color: #1E2C31;
    --default-light-color: #E5E5E5;
}

.fx-row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

.fx-colum {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
}

.fx-colum.nowrap,
.fx-row.nowrap {
    flex-wrap: nowrap;
}

.fx-row.fx-fill>*,
.fx-colum.fx-fill>* {
    flex-grow: 1;
}

.spin-input {
    border: 1px solid var(--secondary-light-color);
}

.spin-input>div:first-child {
    cursor: pointer;
    border-right: 1px solid var(--secondary-light-color);
}

.spin-input>div:first-child:active {
    transform: translate3d(1px, 0px, 1px)
}

.spin-input>div:last-child {
    flex: none;
    border-left: 1px solid var(--secondary-light-color);
    cursor: pointer;
}

.spin-input>div:last-child:active {
    transform: translate3d(1px, 0px, 1px)
}

.icon {
    font-weight: bold;
    text-align: center;
    vertical-align: middle;
    padding: 12px;
    font-size: 28px;
}

.icon.primary,
.icon.primary .ci {
    color: var(--primary-dark-color);
}

.icon.reactive:hover .ci {
    color: var(--primary-light-color);
}

.hidden {
    display: none;
}
<script src="https://releases.jquery.com/git/jquery-3.x-git.min.js"></script>
<div class="spin-input nowrap fx-row fx-fill" >
                        <div class="icon reactive">
                            <span class="ci ci-minus">-</span>
                        </div>
                        <div class="icon">
                            <span>0</span>
                            <input type="text" class="hidden" value="0">
                        </div>
                        <div class="icon reactive">
                            <span class="ci ci-plus">+</span>
                        </div>
                    </div>

There is my jQuery plugin , I hope that can help you .

So I am not sure there is anyway to determine what is being clicked, be it field input or little arrows, but I was able to get it working like this.

Fiddle: http://jsfiddle.net/nusjua9s/4/

JS:

(function($) {

    var methods = {
        cycle: function() {
            if (this.attributes.max && this.attributes.min) {
                var val = this.value;
                var min = parseInt(this.attributes.min.value, 10);
                var max = parseInt(this.attributes.max.value, 10);
                if (val === this.attributes.max.value) {
                    this.value = min + 1;
                } else if (val === this.attributes.min.value) {
                    this.value = max - 1;
                } else if (!(val > min && val < max)) {
                    // Handle values being typed in that are out of range
                    this.value = $(this).attr('data-default');
                }
            }
        }
    };

    $.fn.circularRange = function() {
        return this.each(function() {
            if (this.attributes.max && this.attributes.min) {
                var $this = $(this);
                var defaultVal = this.value;
                var min = parseInt(this.attributes.min.value, 10);
                var max = parseInt(this.attributes.max.value, 10);
                $this.attr('min', min - 1);
                $this.attr('max', max + 1);
                $this.attr('data-default', defaultVal);
                $this.on("change", methods.cycle);
            }
        });
    };

})(jQuery);

$("input[type='number']").circularRange();

HTML:

<input type="number" min="1" max="12" value="12" />

So I am not sure why I keep thinking about this and it still doesn't solve what you are seeing with the flash of out of range numbers which I don't see. But now its not confusing to setup the html ranges at least. You can set the range you want without thinking and just initialize the type="number" fields.

Try with $('input[type="number"]').change(function() {}); ? No result ?

发布评论

评论列表(0)

  1. 暂无评论