From f964821effb0f743fb7c10d52ce28bbd43e0f5d1 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 19 Apr 2022 00:06:37 +0200 Subject: [PATCH] fix(slider): support mouse & touch all the time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As proposed in #1988, I did a rework of the touch event handling of the slider component. The new touch event code does not prevent mouse input (fixes [slider] cannot move slider using mouse on iPad #1988) tracks Touch identifier to avoid confusion by other fingers on the touch screen handles touchcancel events in a sensible way does no dynamic binding and unbinding to all touch events on the page. In combination with the second point, this enables multitouch sliding of multiple sliders on the same page (I know, nobody asked for this, but it works. ;) ) – Unfortunately, multitouch sliding of the first and second thumb of the same slider is not easy to implement due global state within each slider module. only binds to touchstart events on the .thumb element to avoid accidential sliding while scrolling the page on a mobile device (obsoletes [slider] Add option to disable touch-/mouse-sliding on the track #1987) The last point can be considered a disadvantage/regression compared to the old code, if you consider touch-sliding anywhere on the slider as a feature. However, in my experience, it's really annoying to change a slider position (which has immediate real-world effects in my home automation software) when scrolling the page and accidentially hitting a slider. Using the mouse, one can still slide the slider anywhere on its track. --- src/definitions/modules/slider.js | 117 +++++++++++++++++------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/src/definitions/modules/slider.js b/src/definitions/modules/slider.js index 77c8cbef9b..ffe4e3bfbf 100644 --- a/src/definitions/modules/slider.js +++ b/src/definitions/modules/slider.js @@ -70,6 +70,7 @@ $.fn.slider = function(parameters) { $module = $(this), $currThumb, + touchIdentifier, $thumb, $secondThumb, $track, @@ -86,7 +87,6 @@ $.fn.slider = function(parameters) { secondPos, offset, precision, - isTouch, gapRatio = 1, previousValue, @@ -104,7 +104,6 @@ $.fn.slider = function(parameters) { currentRange += 1; documentEventID = currentRange; - isTouch = module.setup.testOutTouch(); module.setup.layout(); module.setup.labels(); @@ -175,14 +174,6 @@ $.fn.slider = function(parameters) { } } }, - testOutTouch: function() { - try { - document.createEvent('TouchEvent'); - return true; - } catch (e) { - return false; - } - }, customLabel: function() { var $children = $labels.find('.label'), @@ -236,9 +227,6 @@ $.fn.slider = function(parameters) { module.bind.globalKeyboardEvents(); module.bind.keyboardEvents(); module.bind.mouseEvents(); - if(module.is.touch()) { - module.bind.touchEvents(); - } if (settings.autoAdjustLabels) { module.bind.windowEvents(); } @@ -251,7 +239,7 @@ $.fn.slider = function(parameters) { $(document).on('keydown' + eventNamespace + documentEventID, module.event.activateFocus); }, mouseEvents: function() { - module.verbose('Binding mouse events'); + module.verbose('Binding mouse and touch events'); $module.find('.track, .thumb, .inner').on('mousedown' + eventNamespace, function(event) { event.stopImmediatePropagation(); event.preventDefault(); @@ -264,27 +252,20 @@ $.fn.slider = function(parameters) { $module.on('mouseleave' + eventNamespace, function(event) { isHover = false; }); - }, - touchEvents: function() { - module.verbose('Binding touch events'); - $module.find('.track, .thumb, .inner').on('touchstart' + eventNamespace, function(event) { - event.stopImmediatePropagation(); - event.preventDefault(); - module.event.down(event); - }); - $module.on('touchstart' + eventNamespace, module.event.down); + // All touch events are invoked on the element where the touch *started*. Thus, we can bind them all + // on the thumb(s) and don't need to worry about interference with other components, i.e. no dynamic binding + // and unbinding required. + $module.find('.thumb') + .on('touchstart' + eventNamespace, module.event.touchDown) + .on('touchmove' + eventNamespace, module.event.move) + .on('touchend' + eventNamespace, module.event.up) + .on('touchcancel' + eventNamespace, module.event.touchCancel); }, slidingEvents: function() { // these don't need the identifier because we only ever want one of them to be registered with document module.verbose('Binding page wide events while handle is being draged'); - if(module.is.touch()) { - $(document).on('touchmove' + eventNamespace, module.event.move); - $(document).on('touchend' + eventNamespace, module.event.up); - } - else { - $(document).on('mousemove' + eventNamespace, module.event.move); - $(document).on('mouseup' + eventNamespace, module.event.up); - } + $(document).on('mousemove' + eventNamespace, module.event.move); + $(document).on('mouseup' + eventNamespace, module.event.up); }, windowEvents: function() { $window.on('resize' + eventNamespace, module.event.resize); @@ -294,24 +275,22 @@ $.fn.slider = function(parameters) { unbind: { events: function() { $module.find('.track, .thumb, .inner').off('mousedown' + eventNamespace); - $module.find('.track, .thumb, .inner').off('touchstart' + eventNamespace); $module.off('mousedown' + eventNamespace); $module.off('mouseenter' + eventNamespace); $module.off('mouseleave' + eventNamespace); - $module.off('touchstart' + eventNamespace); + $module.find('.thumb') + .off('touchstart' + eventNamespace) + .off('touchmove' + eventNamespace) + .off('touchend' + eventNamespace) + .off('touchcancel' + eventNamespace); $module.off('keydown' + eventNamespace); $module.off('focusout' + eventNamespace); $(document).off('keydown' + eventNamespace + documentEventID, module.event.activateFocus); $window.off('resize' + eventNamespace); }, slidingEvents: function() { - if(module.is.touch()) { - $(document).off('touchmove' + eventNamespace); - $(document).off('touchend' + eventNamespace); - } else { - $(document).off('mousemove' + eventNamespace); - $(document).off('mouseup' + eventNamespace); - } + $(document).off('mousemove' + eventNamespace); + $(document).off('mouseup' + eventNamespace); }, }, @@ -341,10 +320,31 @@ $.fn.slider = function(parameters) { module.bind.slidingEvents(); } }, + touchDown: function(event) { + event.preventDefault(); // disable mouse emulation and touch-scrolling + event.stopImmediatePropagation(); + if(touchIdentifier !== undefined) { + // ignore multiple touches on the same slider -- + // we cannot handle changing both thumbs at once due to shared state + return; + } + $currThumb = $(event.target); + var touchEvent = event.touches ? event : event.originalEvent; + touchIdentifier = touchEvent.targetTouches[0].identifier; + if(previousValue === undefined) { + previousValue = module.get.currentThumbValue(); + } + }, move: function(event) { - event.preventDefault(); + if(event.type == 'mousemove') { + event.preventDefault(); // prevent text selection etc. + } + if(module.is.disabled()) { + // touch events are always bound, so we need to prevent touch-sliding on disabled sliders here + return; + } var value = module.determine.valueFromEvent(event); - if($currThumb === undefined) { + if(event.type == 'mousemove' && $currThumb === undefined) { var eventPos = module.determine.eventPos(event), newPos = module.determine.pos(eventPos) @@ -381,13 +381,26 @@ $.fn.slider = function(parameters) { }, up: function(event) { event.preventDefault(); + if(module.is.disabled()) { + // touch events are always bound, so we need to prevent touch-sliding on disabled sliders here + return; + } var value = module.determine.valueFromEvent(event); module.set.value(value); module.unbind.slidingEvents(); + touchIdentifier = undefined; if (previousValue !== undefined) { previousValue = undefined; } }, + touchCancel: function(event) { + event.preventDefault(); + touchIdentifier = undefined; + if (previousValue !== undefined) { + module.update.value(previousValue); + previousValue = undefined; + } + }, keydown: function(event, first) { if(settings.preventCrossover && module.is.range() && module.thumbVal === module.secondThumbVal) { $currThumb = undefined; @@ -500,9 +513,6 @@ $.fn.slider = function(parameters) { }, smooth: function() { return settings.smooth || $module.hasClass(settings.className.smooth); - }, - touch: function() { - return isTouch; } }, @@ -766,12 +776,19 @@ $.fn.slider = function(parameters) { return value; }, eventPos: function(event) { - if(module.is.touch()) { + if(event.type === "touchmove" || event.type === "touchend") { + var + touchEvent = event.touches ? event : event.originalEvent, + touch = touchEvent.changedTouches[0]; // fall back to first touch if correct touch not found + for(var i=0; i < touchEvent.touches.length; i++) { + if(touchEvent.touches[i].identifier === touchIdentifier) { + touch = touchEvent.touches[i]; + break; + } + } var - touchEvent = event.changedTouches ? event : event.originalEvent, - touches = touchEvent.changedTouches[0] ? touchEvent.changedTouches : touchEvent.touches, - touchY = touches[0].pageY, - touchX = touches[0].pageX + touchY = touch.pageY, + touchX = touch.pageX ; return module.is.vertical() ? touchY : touchX; }