So I have a simple layout of,
<a name="#A1"></a>
<div></div>
<a name="#A2"></a>
<div></div>
<a name="#A3"></a>
<div></div>
And Im using the script below to jump to the next anchor point on mouse scroll event. I have tested it and it works on Firefox and IE but on Chrome only scrolling down works, when you scroll up nothing happens.
(function() {
var delay = false;
$(document).on('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
if(delay) return;
delay = true;
setTimeout(function(){delay = false},200)
var wd = event.originalEvent.wheelDelta || -event.originalEvent.detail;
var a= document.getElementsByTagName('a');
if(wd < 0) {
for(var i = 0 ; i < a.length ; i++) {
var t = a[i].getClientRects()[0].top;
if(t >= 40) break;
}
}
else {
for(var i = a.length-1 ; i >= 0 ; i--) {
var t = a[i].getClientRects()[0].top;
if(t < -20) break;
}
}
$('html,body').animate({
scrollTop: a[i].offsetTop
});
});
})();
The error that im getting is Uncaught TypeError: Cannot read property 'top' of undefined
From the specification
If the element on which it was invoked does not have an associated
layout box return an empty sequence and stop this algorithm.
getClientRects() function may return empty list. So element at [0] index may not exist (undefined). Thus you should check length of the list before accessing element in it.
Changed "getElementsByTagName" to "getElementsByClassName"
(function() {
var delay = false;
$(document).on('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
if(delay) return;
delay = true;
setTimeout(function(){delay = false},200)
var wd = event.originalEvent.wheelDelta || -event.originalEvent.detail;
var a = document.getElementsByClassName("full-height");
if(wd < 0) {
for(var i = 0 ; i < a.length ; i++) {
var t = a[i].getClientRects()[0].top;
if(t >= 40) break;
}
}
else {
for(var i = a.length-1 ; i >= 0 ; i--) {
var t = a[i].getClientRects()[0].top;
if(t < -20) break;
}
}
$('html,body').animate({
scrollTop: a[i].offsetTop
});
});
})();
This doesn't look right if you're trying to get the computed top of an element.
var t = a[i].getClientRects()[0].top;
This should use offsetTop.
var t = a[i].getClientRects()[0].offsetTop;
Related
I'm creating a game where the user wanders around a cemetery and collects stories from different graves. It's a classic top-down game. I'm building a script where if the user walks into a grave their movement stops, but I'm having trouble setting up collisions. I am using jQuery. Here is what I have so far:
var position = -1;
var $char = $('#char');
var keyCode = null;
var fired = false;
var $stones = $('.stones div');
var collision = null;
document.onkeydown = function(e) {
keyCode = e.which || e.keyCode;
if (!fired) {
position = -1;
fired = true;
switch (keyCode) {
case 38: position = 0; break; //up
case 40: position = 1; break; //down
case 37: position = 2; break; //left
case 39: position = 3; break; //right
}
walking();
stepping = setInterval(walking,125);
}
};
document.onkeyup = function(e) {
//standing
clearInterval(stepping);
stepping = 0;
fired = false;
};
function walking() {
$stones.each(function() { //check all the stones...
collision = collision($(this), $char, position); ...for collisions
if (collision) { //if any, then break loop
return false;
}
});
if (!collision) { //check if there was a collision
//if no collision, keep walking x direction
}
function collision($el, $charEl, position) {
var $el = $el[0].getBoundingClientRect();
var $charEl = $charEl[0].getBoundingClientRect();
var elBottom = parseInt($el.bottom);
var elRight = parseInt($el.right);
var elLeft = parseInt($el.left);
var elTop = parseInt($el.top);
var charBottom = parseInt($charEl.bottom);
var charRight = parseInt($charEl.right);
var charLeft = parseInt($charEl.left);
var charTop = parseInt($charEl.top);
//this is where I'm stuck
}
}
I've tried various different codes, but nothing seems to work. I keep having an issue where if I'm going forward and then I bump into a headstone and I turn around, I'm stuck. Here's an example code of what I mean:
if (position == 0 &&
!(elTop > charBottom ||
elBottom < charTop ||
elRight < charLeft + 1 ||
elLeft > charRight - 1)
) {
return true;
}
if (position == 1 &&
!(elTop > charBottom ||
elBottom < charTop ||
elRight < charLeft + 1 ||
elLeft > charRight - 1)
) {
return true;
}
return false;
I have looked this question and this question and this question and so far I'm not having any luck. Can somebody help me with the logic or supply an example code of what I need to do?
Thank you.
Your game is looking good man!
I recently wrote some collision detection and had the exact same problem. The issue is that once your coordinates are true of the case of the collision then they will always be true on any other movement.
You need to store the previous position your character was in and revert back to it OR perform the check before you change your characters coordinates.
I managed to find the following solution, thanks to stwitz' about idea, as well as this script: https://magently.com/blog/detecting-a-jquery-collision-part-iv/
var position = -1;
var $char = $('#char');
var keyCode = null;
var fired = false;
var stepSize = 32;
var $stones = $('.stones div');
//new
var cancelTop = cancelRight = cancelLeft = cancelBottom = false;
var charEl = $char[0].getBoundingClientRect();
var charLeft = parseInt(charEl.left);
var charRight = parseInt(charEl.right);
var charTop = parseInt(charEl.top);
var charBottom = parseInt(charEl.bottom);
function walking() {
if (position == 0 && !cancelTop) {
//if moving up & is safe to move up
} else if (position == 1 && !cancelBottom) {
//if moving down & is safe to move down
} else if (position == 2 && !cancelLeft) {
//if moving left and is safe to move left
} else if (position == 3 && !cancelRight) {
//if moving right and is safe to move right
}
cancelTop = cancelRight = cancelLeft = cancelBottom = false; //mark all as safe until we check
$stones.each(function() {
collision($(this));
});
}
document.onkeydown = function(e) {
keyCode = e.which || e.keyCode;
if (!fired) {
position = -1;
fired = true;
switch (keyCode) {
case 38: position = 0; break; //up
case 40: position = 1; break; //down
case 37: position = 2; break; //left
case 39: position = 3; break; //right
}
walking();
stepping = setInterval(walking,125);
}
};
document.onkeyup = function(e) {
//standing
clearInterval(stepping);
stepping = 0;
fired = false;
};
function collision($el) {
var el = $el[0].getBoundingClientRect();
var elBottom = parseInt(el.bottom);
var elRight = parseInt(el.right);
var elLeft = parseInt(el.left);
var elTop = parseInt(el.top);
if (
(elRight == charLeft) &&
(elBottom - stepSize >= charBottom && charBottom >= elTop + stepSize)
) {
cancelLeft = true;
return true;
}
if (
(elLeft == charRight) &&
(elBottom - stepSize >= charBottom && charBottom >= elTop + stepSize)
) {
cancelRight = true;
return true;
}
if (
(elTop + stepSize > charBottom) &&
(elTop <= charBottom) &&
(elLeft < charRight) &&
(elRight > charLeft)
)
{
cancelBottom = true;
return true;
}
if (
(elBottom - stepSize < charTop) &&
(elBottom >= charTop) &&
(elLeft < charRight) &&
(elRight > charLeft)
)
{
cancelTop = true;
return true;
}
return false;
}
Trying to get away from jQuery crutches and refactoring the bellow functions as plain JS.
//slide toggle navigation
$('#nav-toggle').click(function () {
$('.nav-list').slideToggle();
});
//toggle hamburger menu
$('#nav-toggle').click(function () {
this.classList.toggle('active');
});
I got the last one working as:
const toggle = document.getElementById('nav-toggle');
const navList = document.getElementsByClassName('nav-list');
toggle.addEventListener('click', function(e){
this.classList.toggle('active'); //toggle hamburger menu
e.preventDefault();
}, false);
I hit a blocker when I tried to pass both functionalities to the same click event:
//Add a click event listener
toggle.addEventListener('click', function(e){
//show hide div
if (navList.style.display == 'none') {
navList.style.display === 'block';
} else {
navList.style.display === 'none';
}
this.classList.toggle('active');
e.preventDefault();
}, false);
I have .nav-list as display none in my CSS.
Do I need separate event listeners for each event?
I tried that, but the error is still the same = TypeError: navList.style is undefined
Also, what would be the refactor for this nasty one bellow as plain JS?
$('nav ul li a:not(:only-child)').click(function (e) {
$(this).siblings('.nav-dropdown').toggle();
// Close one dropdown when selecting another
$('.nav-dropdown').not($(this).siblings()).hide();
e.stopPropagation();
});
You're out of jQuery, but you're still thinking in jQuery. navList is a NodeList object now, you need to iterate:
//Add a click event listener
toggle.addEventListener('click', function(e){
//show hide div
for( var i = 0; i < navList.length; ++i ){
if (navList[i].style.display == 'none') {
navList[i].style.display === 'block';
} else {
navList[i].style.display === 'none';
}
}
this.classList.toggle('active');
e.preventDefault();
}, false);
Edit:
This monstrosity should do the other fuction's job:
(function(){
var navDropdown = document.getElementsByClassName('nav-dropdown');
var nav = document.getElementsByTagName('nav');
for( var i = 0; i < nav.length; ++i ){
var ul = nav[i].getElementsByTagName('ul');
for( var j = 0; j < ul.length; ++j ){
var li = ul[j].getElementsByTagName('li');
for( var m = 0; m < li.length; ++m ){
var a = li[m].getElementsByTagName('a');
if( a.length > 0 ){
for( var k = 0; k < a.length; ++k ){
a[k].addEventListener('click', (function(a){
return function(e){
for( var n = 0; n < a.length; ++n ){
a[n].classList.toggle('nav-dropdown');
}
for( var o = 0; o < navDropdown.length; ++o ){
var found = false;
for( var n = 0; n < a.length; ++n ){
if( navDropdown[o] === a[n] ){
found = true;
break;
}
}
if( !found ){
navDropdown[o].classList.add('hide');
}
}
e.preventDefault();
};
})(a)
);
}
}
}
}
}
})();
It might very possibly be one of the worst things I've ever written, but it should work...
Obviously I can't check if it does without the actual page, but at least it runs.
you can iterate over a node list by using Array.from which will treat it as an array.
Array.from(navList).forEach(function(navItem){
navItem.style.display = navItem.style.display === 'none' ? 'block' : 'none';
})
You have the equal signs in your syntax crossed, should be:
if (navList.style.display === 'none') {
navList.style.display = 'block';
} else {
navList.style.display = 'none';
}
Quick cheat sheet:
= : assign
== : equals (also crosses javascript "type borders", i.e. 0 == "0" is true)
=== : strict equals (0 === "0" is false, 0 === 0 is true)
I am trying to implement this code on my site:
Since it is a WordPress site made with page builder, I had to add all the anchor tags with jquery like this:
$('<a name="#A1"></a>').insertBefore('#header');
$('<a name="#A2">Tag #2.</a>').insertBefore('#services');
$('<a name="#A3">Tag #3.</a>').insertBefore('#portfolio');
$('<a name="#A4">Tag #4.</a>').insertBefore('#clients');
The code works, but when I try to scroll, nothing happens on the page.
I used this code that you can also see in my codepen
JS:
(function() {
var delay = false;
$(document).on('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
if(delay) return;
delay = true;
setTimeout(function(){delay = false},200)
var wd = event.originalEvent.wheelDelta || -event.originalEvent.detail;
var a= document.getElementsByTagName('a');
if(wd < 0) {
for(var i = 0 ; i < a.length ; i++) {
var t = a[i].getClientRects()[0].top;
if(t >= 40) break;
}
}
else {
for(var i = a.length-1 ; i >= 0 ; i--) {
var t = a[i].getClientRects()[0].top;
if(t < -20) break;
}
}
$('html,body').animate({
scrollTop: a[i].offsetTop
});
});
})();
How can I make the jquery Work?
I inspected the site and see this error duplicating everytime I try to scroll.
Here is the site am trying to test.
It is throwing error on this line of code -
var t = a[i].getClientRects()[0].top;
because a[i] for
<a href="#" class="zn-res-trigger zn-menuBurger zn-menuBurger--3--s zn-
menuBurger--anim1" id="zn-res-trigger">
<span></span>
<span></span>
<span></span>
</a>
don't have getClientRects() method.
So replace your code with
for(var i = 0 ; i < a.length ; i++) {
if(!a[i].className.includes('zn-res-trigger')) //add this check{
var t = a[i].getClientRects()[0].top;
...........
}
}
Also check with the help of debugger, may be there are other unused anchor tags, so you have to include check for them also
You have some a element that does not have top property which cause the undefined error.
try the bellow code in which I do check the a[i].getClientRects().length in order to get the top value :
(function() {
var delay = false;
$(document).on('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
if(delay) return;
delay = true;
setTimeout(function(){delay = false},200)
var wd = event.originalEvent.wheelDelta || -event.originalEvent.detail;
var a= document.getElementsByTagName('a');
var scroll = false;
if(wd < 0) {
for(var i = 0 ; i < a.length ; i++) {
if(a[i].getClientRects().length>0) {
scroll = true;
var t = a[i].getClientRects()[0].top;
if(t >= 40) break;
}
}
}
else {
for(var i = a.length-1 ; i >= 0 ; i--) {
if(a[i].getClientRects().length>0) {
scroll = true;
var t = a[i].getClientRects()[0].top;
if(t < -20) break;
}
}
}
if(scroll){
$('html,body').animate({
scrollTop: a[i].offsetTop
});
}
});
})();
I have 5 anchors on my html page. Is there any way that the page scrolls automatically to the next anchor (#) by a single Mouse-wheel scroll? Is there a way that it happens regardless of the anchor's name? just to the next anchor.
This works in Chrome, IE, Firefox, Opera, and Safari:
(function() {
var delay = false;
$(document).on('mousewheel DOMMouseScroll', function(event) {
event.preventDefault();
if(delay) return;
delay = true;
setTimeout(function(){delay = false},200)
var wd = event.originalEvent.wheelDelta || -event.originalEvent.detail;
var a= document.getElementsByTagName('a');
if(wd < 0) {
for(var i = 0 ; i < a.length ; i++) {
var t = a[i].getClientRects()[0].top;
if(t >= 40) break;
}
}
else {
for(var i = a.length-1 ; i >= 0 ; i--) {
var t = a[i].getClientRects()[0].top;
if(t < -20) break;
}
}
if(i >= 0 && i < a.length) {
$('html,body').animate({
scrollTop: a[i].offsetTop
});
}
});
})();
Fiddle at http://jsfiddle.net/t6LLybx8/728/
How it works
To monitor the mouse wheel in most browsers, use $(document).on('mousewheel'). Firefox is the oddball, and it requires $(document).on('DOMMouseScroll').
To get the direction of the mouse wheel (up or down), use event.originalEvent.wheelDelta. Again, Firefox is the oddball, and you have to use -event.originalEvent.detail.
If the direction is a negative number, you're scrolling down the page. In that case, loop through each tag beginning with the first, until its first getClientRects() top is >= 40. (I used 40, in case the browser adds a default margin at the top of the viewport.)
If the direction is a positive number, you're scrolling up the page. In that case, loop through each tag beginning with the last, until its first getClientRects() top is < -20. (I used -20 to ensure we move up the page.)
The delay variable prevents the mouse wheel from scrolling too quickly. The entire function is wrapped in a closure, so delay remains a private variable.
let's say you have array of IDs.then you can do something like...
var ancherList = ["id1","id2","id3"];
var currentPosition = null;
var mousewheelevent = 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll';
$(document).on(mousewheelevent,function(e){
var scrollToAncher function (id,speed){
spd = speed ? "slow" //deafult value for the animation speed
var ancherTag = $("a[name='"+ id +"']");
$('html,body').animate({scrollTop: ancherTag.offset().top},spd);
}
e.preventDefault();
var delta = e.originalEvent.deltaY ? -(e.originalEvent.deltaY) : e.originalEvent.wheelDelta ? e.originalEvent.wheelDelta : -(e.originalEvent.detail);
if (delta > 0){
console.log("up")
//check your current position and target id
switch(currentPosition){
case null :
case ancherList[0] :
scrollToAncher(ancherList[1]);
currentPosition = ancherList[1];
break;
case ancherList[1] :
currentPosition = ancherList[2];
scrollToAncher(ancherList[2]);
break;
case ancherList[2] :
currentPosition = ancherList[0];
scrollToAncher(ancherList[0]);
break;
}
} else {
console.log("down")
//do the same for mouse wheel down
}
});
code ain't tested.sorry if there was syntax error
I was hoping to get a little help with my code. This works in moz/webkit but not in ie. I don't quite understand why :(
$(window).trigger('hashchange');
// Add .selected class to nav on page scroll
var $sections = $('section');
var $navs = $('nav > ul > li');
var topsArray = $sections.map(function() {
return $(this).position().top - 50;
}).get();
var len = topsArray.length;
var currentIndex = 0;
var getCurrent = function( top ) {
for( var i = 0; i < len; i++ ) {
if( top > topsArray[i] && topsArray[i+1] && top < topsArray[i+1] ) {
return i;
}
}
};
$(document).scroll(function(e) {
var scrollTop = $(this).scrollTop();
var secondSection = topsArray[1];
if(scrollTop <= 200) { // moved past the header
$navs.eq(0).removeClass("selected")
} else if(scrollTop >= 205 && scrollTop <= secondSection ) { // between header and 2nd section
$navs.eq(0).addClass("selected")
}
var checkIndex = getCurrent( scrollTop );
if( checkIndex !== currentIndex ) {
currentIndex = checkIndex;
$navs.eq( currentIndex ).addClass("selected").siblings(".selected").removeClass("selected");
}
});
IE is not very forgiving of javascript errors. Try adding some missing semicolons:
if(scrollTop <= 200) { // moved past the header
$navs.eq(0).removeClass("selected"); //missing semicolon
} else if(scrollTop >= 205 && scrollTop <= secondSection ) { // between header and 2nd section
$navs.eq(0).addClass("selected"); //missing semicolon
}
If it is IE only, then IE is having trouble parsing the js. Try running your js through a debugger like JsLint if you run into issues.