Improving Mouse Wheel Algorithm Navigation for Slider - javascript

Introduction
I have a page that has full screen fixed blocks and I would like to navigate these blocks using the javascript wheel event.
If that doesn't make sense; to put it simply, I have a custom carousel that I would like to navigate using the mouse wheel one block at a time i.e one scroll roll/click moves to the previous/next item.
My current solution works, but it needs a little tweaking to make it work better.
The Issue
The problem I am having is that 1 roll/click on my mouse wheel sometimes scrolls one block, and sometimes scrolls 2 blocks.
The algorithim I am using is as follows:
For every fired wheel event, increment/decrement the global variable deltaY value by 1 (my mouse wheel tends to fire around 35-40 events each time the wheel is scrolled when in click mode, not sure if this is standard)
Whenever the deltaY value changes, determine which block (by index) should be active. I do this with the following; Math.floor(deltaY / 35) where 35 is the number of wheel events fired by 1 roll of my mouse wheel (very unsure about this as it may differ for other mice)
Update the active index and update deltaY with an integer divisible by 35
The Code
I am using Vue.js but not to worry if you are not familiar with it, all I really need is the algorithm and I can implement it into my code.
Vanilla JS
I haven't tested this code, I simply mocked it up based on my Vue.js code for those of you that are not familiar with Vue.
// Set the default values
let items = ['x', 'y', 'z'];
let active = 0;
let deltaY = 0;
/**
* Update the delta value and any other relevant
* values.
*
* #param {event} event
* #return void
*/
function updateDelta (value) {
active = Math.floor(value / 35);
deltaY = active * 35;
}
// Register the `wheel` event
window.addEventListener('wheel', (event) => {
// The navigation is only active when the page has not
// been scrolled
if (document.documentElement.scrollTop === 0) {
// If the last item is currently active then we do not need to
// listen to `down` scrolls, or, if the first item is active,
// then we do not need to listen to `up` scrolls
if (
(event.deltaY > 0 && (active - 1) === items.length)
|| (event.deltaY < 0 && deltaY === 0)
) {
return;
}
updateDelta(Math.sign(event.deltaY));
}
}, { passive: true });
My Code (Vue JS)
I have added clear comments to illustrate what Vue is doing
export default {
data() {
return {
items: ['x', 'y', 'z'],
active: 0, // Define the active index
deltaY: 0 // This is used for the scroll wheel navigation
}
},
created() {
// Register the `wheel` event
window.addEventListener('wheel', this._handleWheel, { passive: true });
},
destroyed() {
// Remove the `wheel` event
window.removeEventListener('wheel', this._handleWheel, { passive: true });
},
watch: {
// I have added watchers to both `deltaY` and `active`, however,
// this may not be necessary. These will not create an endless loop
// because the watcher is only called when a value is changed
active: function(index) {
// Whenever the `active` index changes, update the `deltaY` value
this.deltaY = index * 35;
},
deltaY: function(value) {
// Whenever the `deltaY` value changes, update the `active` index
this.active = Math.floor(value / 35);
}
},
methods: {
/**
* Handle the window wheel event.
*
* #param {event} event
* #return void
*/
_handleWheel (event) {
// The navigation is only active when the page has not
// been scrolled
if (document.documentElement.scrollTop === 0) {
// If the last item is currently active then we do not need to
// listen to `down` scrolls, or, if the first item is active,
// then we do not need to listen to `up` scrolls
if (
(event.deltaY > 0 && (this.active - 1) === this.items.length)
|| (event.deltaY < 0 && this.deltaY === 0)
) {
return;
}
this.deltaY += Math.sign(event.deltaY);
}
}
}
}

As you already pointed out yourself, the number of times a wheel event fires may vary from mice to mice and from time to time. Thus, relying on the number of events as a way to determine when to shift a block will always be unpredictable.
Instead, I suggest to try an alternative approach. For example, you could try to detect when the user stops scrolling, and just increment your index by 1. A quick Google search returned this helper function, which does just that.

Related

Javascript, scroll event listener doesn't fire until scroll is done on mobile

I am using an event listener for scroll that will change a button from position:fixed to position:relative on the page depending where the user is on the page. I have a pretty straight forward setup, I am using react, when the react component mounts, I add a listener with a throttler for the scroll event. This works great on desktop, however on mobile, I notice the event to change the positioning on the element is not firing until the scroll is done.
Here is the setup I have -
On the component mount, add listener :
componentDidMount() {
window.addEventListener('scroll', this.scrollThrottler);
}
Which calls the throttling function :
let scrollTimeout;
scrollThrottler() {
// pass through throttle designated at 15 fps
if ( !scrollTimeout ) {
scrollTimeout = setTimeout(() => {
scrollTimeout = null;
this.handleScroll();
}, 66);
}
}
And then the function it calls just checks if a certain div at the right point on the DOM to change the class on it like so :
handleScroll() {
const width = this.state.windowWidth ? this.state.windowWidth : window.innerWidth;
const stickyBoundary = this.refs.approved.getBoundingClientRect().top + this.refs.approved.offsetHeight + this.refs.stickyBar.offsetHeight - window.innerHeight;
if ( (width <= 991) && (stickyBoundary > 0)) {
this.refs.rootNode.className = 'row primary-product-block sticky-add-btn';
} else {
this.refs.rootNode.className = 'row primary-product-block';
}
}
So this works great on desktop, however on mobile it looks like it is not snapping into position fixed or relative until after the scroll event is done. Is there anyway to smooth this out? Thanks!
add touchmove event to your code
componentDidMount() {
window.addEventListener('scroll', this.scrollThrottler);
window.addEventListener('touchmove', this.scrollThrottler);
}

HTML Canvas: Controlling keydown registry when key is held down

I'm looking for a way to control how many keydown events are registered in a given period of time when a key is held down. I have two animation functions, collapse() and expand() which collapse and expand a box when the down key is pressed. I've got it rigged so that the second animation is kicked off after the first. However, I have a timer, hovering(t) set within the first animation that is reset with every keypress so that the second animation doesn't begin until the key is released and the timer expires.
function collapse(){
if(h > 1 && arrayCount < myArray.length){
reqAnimFrame(collapse);
h -= 10;
clear();
draw();
} else {
arrayCount += 1;
h = 0;
clearHoverTimer();
hovering(250);
}
}
function expand(){
if(h < 100){
reqAnimFrame(expand);
h += 10;
clear();
draw();
} else {
h = 100;
clear();
draw();
}
}
Here's where my problem is: the first animation function also cycles through an array of strings via the arrayCount variable. When collapse() fires, the arrayCount increments by one. Unfortunately, when the key is held down, it fires off the collapse function in quick succession and the array is cycled through way too quickly.
Is it possible to restrict the key event timing so that say half the keys are registered?
I tried setting a variable heldDown to false, which would allow the keyEvent to register. The keyEvent would call collapse and start heldDownTimer. Once heldDownTimer expires, heldDown would be reset to false and the cycle would start over.
Set flags indicating the current state of your collapsing & expanding animation.
var doCollapsing indicates if the collapsing code should animate.
var doExpanding indicates if the expanding code should animate.
In your keydown handler, you can ignore 'extra' keydown by only setting the flags when they indicate the animation loop is idle.
// listen for keydown and doCollapsing only if the animation is currently idle
if(!doCollapsing && !doExpanding){
doCollapsing=true; // start collapsing
doExpanding=false; // don't expand until a keyup fires
}
This will cause collapse+expand to execute once instead of being triggered with every keydown.
// variables indicating the state of the animation
var doCollapsing=false;
var doExpanding=false;
// listen for keydown events
document.addEventListener("keydown",handleKeydown,false);
// listen for keyup events
document.addEventListener("keyup",handleKeyup,false);
// start the animation loop
requestAnimationFrame(animate);
// handle keydown events
function handleKeydown(e){
// listen for keydown
// doCollapsing only if the animation is idle
if(!doCollapsing && !doExpanding){
doCollapsing=true;
doExpanding=false;
}
}
// handle keyup events
function handleKeyup(e){
doExpanding=true;
}
// the animation loop
function animate(time){
requestAnimationFrame(animate);
if(doCollapsing){
// Do collapsing until done
// When done: doCollapsing=false;
}else if(doExpanding && hoveringTimerHasExpired){
// Do expanding until done
// When done: doExpanding=false;
}
}

How do I detect the Keyboard show/hide event occurence on Android browser

I am in a fix. I am not able to identify a way to capture the keyboard show/hide status on a mobile device browser.
Problem :
I have a popup on a form in which a Text Field is present. When the user taps on the text field the keyboard shows up pushing the popup on the form and eventually making the text field invisible.
Is there a way to identify the key board show/hide status???
No, there is no way to reliably know when a keyboard is showing. The one level of control you do have is you can set your app to pan or resize when the keyboard shows up. If you set it to resize, it will recalculate your layout and shrink things so if fits the remaining screen. If you choose pan, it will keep the same size and just slide up the entire app.
you can find out keyboard show/hide inside your application,Try following code inside oncreate method,and pass your parent layout to view.
final View activityRootView = rellayLoginParent;
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
Rect r = new Rect();
// r will be populated with the coordinates of your view that area still visible.
activityRootView.getWindowVisibleDisplayFrame(r);
int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
//MyLog.w("height difference is", "" + heightDiff);
if (heightDiff > 100)
{ // if more than 100 pixels, its probably a keyboard...
if(lytAppHeader.getVisibility() == View.VISIBLE)
{
lytAppHeader.setVisibility(View.GONE);
}
}
else
{
if(lytAppHeader.getVisibility() == View.GONE)
{
lytAppHeader.setVisibility(View.VISIBLE);
}
}
}
});
It seems there is no reliable way to do this in the browser. The closest I have come is to listen for focus events and then temporarily listen for resize events. If a resize occurs in the next < 1 second, it's very likely that the keyboard is up.
Apologies for the jQuery...
onDocumentReady = function() {
var $document = $(document);
var $window = $(window);
var initialHeight = window.outerHeight;
var currentHeight = initialHeight;
// Listen to all future text inputs
// If it's a focus, listen for a resize.
$document.on("focus.keyboard", "input[type='text'],textarea", function(event) {
// If there is a resize immediately after, we assume the keyboard is in.
$window.on("resize.keyboard", function() {
$window.off("resize.keyboard");
currentHeight = window.outerHeight;
if (currentHeight < initialHeight) {
window.isKeyboardIn = true;
}
});
// Only listen for half a second.
setTimeout($window.off.bind($window, "resize.keyboard"), 500);
});
// On blur, check whether the screen has returned to normal
$document.on("blur.keyboard", "input[type="text"],textarea", function() {
if (window.isKeyboardIn) {
setTimeout(function() {
currentHeight = window.outerHeight;
if (currentHeight === initialHeight) {
window.isKeyboardIn = false;
}, 500);
}
});
};

How to detect a double-click-drag in Javascript/jQuery

I'd like to detect in a web page when the user selects some text by dragging. However, there's one scenario in Windows which I'm calling a "double-click-drag" (sorry if there's already a better name I don't know) and I can't figure out how to detect it. It goes like this:
press mouse button
quickly release mouse button
quickly press mouse button again
drag with the button held down
This causes the dragging to select whole Words. It's quite a useful technique from the user perspective.
What I'm trying to do is tell the difference between a double-click-drag and a click followed by a separate drag. So when I get to step 2 I will get a click event but I don't want to treat it as a click yet; I want to see if they're about to immediately do step 3.
Presumably Windows detects this on the basis of the timing and how much the mouse has moved between step 2 and 3, but I don't know the parameters it uses so I can't replicate the windows logic. note that even if the mouse doesn't move at all between step 2 and 3, I still get a mousemove event.
I realise that I should be designing interfaces that are touch-friendly and device-neutral, and I have every intention of supporting other devices, but this is an enterprise application aimed at users on windows PCs so I want to optimize this case if I can.
We've done something similar. Our final solution was to create a click handler that suppressed the default response, and then set a global variable to the current date/time. We then set another function to fire in some 200ms or so that would handle the "click" event. That was our base function.
We then modified it to look at the global variable to determine when the last click occured. If it's been less than 200ms (modify based on your needs) we set a flag that would cause the click handler to fizzle and called a double click handler.
You could extend that approach by having your click and double click handlers manually fire the drag functionality.
I don't have access to the aforementioned code right now, but here is an example of that framework being used to track keyboard clicks to determine if a scanner or user has finished typing in a field:
var lastKeyPress = loadTime.getTime();
// This function fires on each keypress while the cursor is in the field. It checks the field value for preceding and trailing asterisks, which
// denote use of a scanner. If these are found it cleans the input and clicks the add button. This function also watches for rapid entry of keyup events, which
// also would denote a scanner, possibly one that does not use asterisks as control characters.
function checkForScanKeypress() {
var iVal = document.getElementById('field_id').value;
var currentTime = new Date()
var temp = currentTime.getTime();
if (temp - lastKeyPress < 80) {
scanCountCheck = scanCountCheck + 1;
} else {
scanCountCheck = 0;
}
lastKeyPress = currentTime.getTime();
}
// The script above tracks how many successive times two keyup events have occurred within 80 milliseconds of one another. The count is reset
// if any keypress occurs more than 80 milliseconds after the last (preventing false positives from manual entry). The script below runs
// every 200 milliseconds and looks to see if more than 3 keystrokes have occurred in such rapid succession. If so, it is assumed that a scanner
// was used for this entry. It then waits until at least 200 milliseconds after the last event and then triggers the next function.
// The 200ms buffer after the last keyup event insures the function is not called before the scanner completes part number entry.
function checkForScan() {
var currentTime = new Date();
var temp = currentTime.getTime();
if (temp - lastKeyPress > 200 && scanCountCheck > 3) {
FiredWhenUserStopsTyping();
scanCountCheck = 0;
}
setTimeout(checkForScan, 200);
}
Here is some code that I just wrote up based upon the above ideas. It's not tested and doesn't contain the actual drag events, but should give you a good starting point:
var lastClick = loadTime.getTime();
function fireOnClickEvent(event) {
event.preventDefault;
var currentTime = new Date()
var temp = currentTime.getTime();
if (temp - lastClick < 80) {
clearTimeout(tf);
doubleClickHandler();
} else {
tf = setTimeout(singleClickHandler, 100);
}
lastClick = currentTime.getTime();
}
function singleClickHandler() {
// Begin normal drag function
}
function doubleClickHandler() {
// Begin alternate drag function
}
A single double-click-drag action involves the following events in sequence:
mousedown -> mouseup -> click -> mousedown -> mousemove
With that in mind, I came up with this simple solution:
let maybeDoubleClickDragging = false;
let maybeDoubleClickDraggingTimeout;
const element = document.querySelector('#container');
element.addEventListener("click", function (e) {
maybeDoubleClickDragging = true;
element.removeEventListener("mousemove", handleMousemove);
});
element.addEventListener("mousedown", (e) => {
element.addEventListener("mousemove", handleMousemove);
if (maybeDoubleClickDragging) {
clearTimeout(maybeDoubleClickDraggingTimeout);
return;
}
});
element.addEventListener("mouseup", (event) => {
maybeDoubleClickDraggingTimeout = setTimeout(() => {
maybeDoubleClickDragging = false;
}, 200);
});
function handleMousemove(e) {
if(maybeDoubleClickDragging) {
element.textContent = 'you are double-click-dragging'
}
}
#container {
width: 300px;
height: 300px;
background: yellow;
}
<div id="container"></div>

Detect whether the intent of a user is to tap or to scroll up/down the page on tactile device

If a user taps (touchstart) outside a popup-div, I want to hide the popup.
But if the user's intent is to scroll/swipe (touchmove), I don't want to hide the popup.
How could the code look like to detect and respond to those two actions (with or without jQuery)?
Here is a basic example of how you could do this:
http://jsfiddle.net/4CrES/2/
The logic behind it involves detecting the initial touch time and saving it to a var
touchTime = new Date();
In the touchend handler subtract this time from the current time to get the difference:
var diff = new Date() - touchTime;
Use an if statement to decide whether the touch duration was short enough to consider it a tap, or long enough to consider it a drag.
if (diff < 100){
//It's a tap
}
else {
//Not a quick tap
}
You could write a more robust implementation by doing a similar difference of the initial touch y position to the final touch y position in the handlers. Another option is to compare the scrollTop of the scrolling area to see if it has been scrolled.
Since click events do not bubble up the DOM on mobile Safari while touch events and custom events do, I recently wrote some code to detect a quick-tap.
It's a quick-tap when
The touch event starts and ends without any movement along the screen
No scrolling occurrs
It all happens in less than 200ms.
If the touch is determined to be a 'quickTap', the TouchManager causes the touched element in the DOM to emit a custom "quickTap" event which then bubbles up the DOM to any other elements that happen to be listening for it. This code defines and creates the touch manager and it will be ready to go immediately
Drawbacks:
Uses jQuery
Only designed with one finger in mind.
also borrowed some code from modernizr. (You can omit that bit if you already include Modernizr.)
Maybe this is overkill, but it's part of a larger codebase I'm working on.
/**
* Click events do not bubble up the DOM on mobile Safari unless the click happens on a link or form input, but other events do bubble up.
* The quick-tap detects the touch-screen equivalent of a click and triggers a custom event on the target of the tap which will bubble up the DOM.
* A touch is considered a click if there is a touch and release without any movement along the screen or any scrolling.
*/
var qt = (function ($) {
/**
* Modernizr 3.0.0pre (Custom Build) | MIT
* Modernizr's touchevent test
*/
var touchSupport = (function() {
var bool,
prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
bool = true;
} else {
var query = ['#media (',prefixes.join('touch-enabled),('),'heartz',')','{#modernizr{top:9px;position:absolute}}'].join('');
testStyles(query, function( node ) {
bool = node.offsetTop === 9;
});
}
return bool;
}()),
MobileTapEvent = 'tapEvent';
if(touchSupport) {
/* Create a new qt (constructor)*/
var startTime = null,
startTouch = null,
isActive = false,
scrolled = false;
/* Constructor */
function qt() {
var _qt = this,
context = $(document);
context.on("touchstart", function (evt) {
startTime = evt.timeStamp;
startTouch = evt.originalEvent.touches.item(0);
isActive = true;
scrolled = false;
})
context.on("touchend", function (evt) {
window.ct = evt.originalEvent['changedTouches'];
// Get the distance between the initial touch and the point where the touch stopped.
var duration = evt.timeStamp - startTime,
movement = _qt.getMovement(startTouch, evt.originalEvent['changedTouches'].item(0)),
isTap = !scrolled && movement < 5 && duration < 200;
if (isTap) {
$(evt.target).trigger('quickTap', evt);
}
})
context.on('scroll mousemove touchmove', function (evt) {
if ((evt.type === "scroll" || evt.type === 'mousemove' || evt.type === 'touchmove') && isActive && !scrolled) {
scrolled = true;
}
});
}
/* Calculate the movement during the touch event(s)*/
qt.prototype.getMovement = function (s, e) {
if(!s || !e) return 0;
var dx = e.screenX - s.screenX,
dy = e.screenY - s.screenY;
return Math.sqrt((dx * dx) + (dy * dy));
};
return new qt();
}
}(jQuery));
To use the code you would add it to your page then just listen for the quickTap event.
<script type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="quick-tap.js"></script>
<script type="text/javascript">
$(document).on('quickTap', function(evt, originalEvent) {
console.log('tap event detected on: ', evt.target.nodeName, 'tag');
});
</script>
evt is the quickTap event.
evt.target is the tapped DOM element (not the jQuery object).
originalEvent is the touchend event where qt determines whether it was a tap or not.
You can hide the popup-div on touchend event.
In touchstart event you remember window.scrollY.
In touchend event, if scrollY positions differ the user has scrolled.

Categories