Setting click event to next tab in container with arrow keys - javascript

I have a filter container with three tabs. The currently selected tab has class ag-tab-selected. When the container loads, the first tab is always selected and has the ag-tab-selected class. I need to be able to navigate between the tabs with the left and right arrow keys.
As the user hits the right arrow key, the ag-tab-selected class should be applied to the next tab, and if the left arrow key is hit, the ag-tab-selected class should be applied to the previous tab. The current tab should also have .click() applied to it so that when the user does hit the arrow key, the view is changed based on which tab is selected. I'm currently able to loop through the available tabs and have a trigger applied to the current one, but having trouble iterating to the previous or next with the arrow keys:
if(e.key === 'ArrowRight') {
for(let i = 0; i < tabTriggers.length; i++){
//on arrow, trigger currently focused ag-tab with click
if(tabTriggers[0].classList.contains('ag-tab-selected') === true){
tabTriggers[i].click();
}
}
}
Link to current fiddle: Link

I finished the task based on the code you provided, i also extended it with some logic so that you'd have a working example. It's available here http://jsfiddle.net/631zjmcq/
So I defined a click handler for each tab, so that you gain some interactivity (i wanted to see the solution in action)
The basis of the code you provided was working well, it just had the problem that once you've found an element, you didn't stop the loop and it ran until it reached the last element in the list. This means it clicked every element until it reached the last one, so I put a break statement into that for loop.
To go backwards, I modified the for loop, so that this time it would loop from the end of the list backwards.
const selectedTabClass = 'ag-tab-selected';
document.querySelectorAll('.ag-tab').forEach(tab => {
tab.addEventListener('click', e => {
if (tab.classList.contains('ag-tab-selected')) return
document.querySelector(`.${selectedTabClass}`)
.classList.remove(selectedTabClass)
tab.classList.add(selectedTabClass)
})
})
document.addEventListener('keydown', e => {
let tabTriggers = document.querySelectorAll('.ag-tab');
if (e.key === 'ArrowRight') {
for (let i = 0; i < tabTriggers.length; i++) {
if (tabTriggers[i].classList.contains('ag-tab-selected') === true) {
tabTriggers[i+1].click();
break;
}
}
}
if (e.key === 'ArrowLeft') {
for (let i = tabTriggers.length-1; i > 0; i--) {
if (tabTriggers[i].classList.contains('ag-tab-selected') === true) {
tabTriggers[i-1].click();
break;
}
}
}
});

Related

Prevent window scroll on navigating dropdown with arrow keys

I'm trying to prevent page scrolling on my custom dropdown search using StimulusJS. Much like in gmail, where you can type something in a search box and use arrow keys to navigate. I go to that point where I can navigate the dropdown, but at the same time default behaviour for arrow keys is problematic here.
// results are divs that get focused
const results = [node1, node2, node3]
if (this.listCounter <= -1) { this.listCounter = -1 }
if (this.listCounter >= results.length) { this.listCounter = results.length - 1 }
switch (event.key) {
case 'ArrowDown':
this.listCounter++
break
case 'ArrowUp':
this.listCounter--
// when we reach the top we focus back on input element
if (this.listCounter <= 0) {
this.userInputTarget.focus()
}
break
default:
break
}
if (results[this.listCounter]) {
results[this.listCounter].focus()
}
So this works well, but the problem is that pressing arrow keys up/down also invokes scroll on the page. So I tried disabling it, but only when the key is pressed. I don't want to disable this behaviour for the whole page, only when certain elements are focused. Below console.log() gets fired, but it doesn't stop the window from being scrolled.
connect() {
window.addEventListener('keyup', this.preventKeyboardEvents.bind(this), false)
}
preventKeyboardEvents(e) {
const key = e.key
const results = window.allMultisearchActiveElements
const activeElement = results.includes(document.activeElement) || document.activeElement === this.userInputTarget
if (activeElement && (key === "ArrowDown" || key === "ArrowUp" || key === "Enter")) {
e.preventDefault()
console.log('fired')
}
}
Which part of it am I getting wrong? Is it even possible to .preventDefault() only for certain events?
Comes out it was rather simple.
window.addEventListener('keydown', this.preventKeyboardEvents.bind(this), false)
Since when pressing a key keydown event is fired first, I assume it fires a default behaviour of scrolling in the browser window. So using keyup was a problem here, because it was fired after the window has received the event from keydown.

Attach the event on click to his father with pure javascript

Newby to pure JS.
I'm creating a menu that has to work with mobile.
I'm trying to create with pure .js, instead of using jQuery so, that's an experiment and it has been challenging.
Here's my code:
JS:
(function() {
var menu = document.querySelector('.mobile-menu');
var subMenu = {
downToggle: document.getElementsByClassName('sub-menu'),
downToggleTitle: document.getElementsByClassName('sub-menu-title'),
subMenuItems: document.getElementsByClassName('sub-menu-item-mobile'),
searchBar: document.getElementById('mobile-search'),
onclickimg: document.querySelectorAll('.sub-menu-arrow'),
};
function listen() {
for(var i=0; i<subMenu.downToggleTitle.length; i++) {
subMenu.downToggleTitle.item(i).addEventListener('click', function(e) {
// if there is a menu that's already open and it's not the element that's been clicked, close it before opening the selected menu
for(var i=0; i<subMenu.downToggleTitle.length; i++) {
if (subMenu.downToggleTitle.item(i).classList.contains('expanded') && subMenu.downToggleTitle.item(i) !== e.target) {
subMenu.downToggleTitle.item(i).classList.toggle('expanded');
}
}
// inside each sub-menu is a third-level-sub-menu. So inside each sub-menu we
// check if it's already open, then close it
for(var i=0; i<subMenu.subMenuItems.length; i++) {
// console.log("test test")
if(subMenu.subMenuItems.item(i).classList.contains('expanded') && subMenu.subMenuItems.item(i) !== e.target) {
subMenu.subMenuItems.item(i).classList.toggle('expanded');
}
}
this.classList.toggle('expanded');
});
}
for(var i=0; i<subMenu.subMenuItems.length; i++) {
subMenu.subMenuItems.item(i).addEventListener('click', function(e) {
for(var i=0; i<subMenu.subMenuItems.length; i++) {
if(subMenu.subMenuItems[i].classList.contains('expanded') && subMenu.subMenuItems[i] !== e.target) {
subMenu.subMenuItems[i].classList.toggle('expanded');
console.log("hello Aug 20");
}
}
this.classList.toggle('expanded');
});
}
} listen();
}());
The behavior that I want to change is the following:
In the first version, if the client press the .sub-menu-title class (the variable downToggleTitle), which is a li item, the very element will toggle the class expanded. Now I want something a little bit different.
I added the class sub-menu-arrow, which is the variable onclickimg to an img at the very end of my list element, so if the client will click on the arrow, all the class element sub-menu-title ( var = downToggleTitle ) will toggle the class expanded.
This is not happening because for some reason if I change the code in this way:
subMenu.onclickimg.item(i).addEventListener('click', function(e) {
for(var i=0; i<subMenu.downToggleTitle.length; i++) {
if (subMenu.downToggleTitle.item(i).classList.contains('expanded') && subMenu.downToggleTitle.item(i) !== e.target) {
subMenu.downToggleTitle.item(i).classList.toggle('expanded');
}
}
the class expanded will be toggled to the sub-menu-arrow elements (like I said, some images with animations).
Any suggestion on how to target the parent element in this case?
Also, is it possible to exclude the anchor element with the class mobile-toplevel-link from the click event?
The <a> element is the other children of the sub-menu-title class
This is really just a comment. You can greatly simplify your code using the iterator functionality of modern NodeLists. I don't see the point of the subMenu object, it just makes references longer.
Also, I've replaced getElementsByClassName as it produces a live NodeList, whereas querySelectorAll returns a static list. Not much difference here, but can be significant in other cases.
The following is a simple refactoring, it should work exactly as your current code is supposed to. Note that for arrow functions, this is adopted from the enclosing execution context.
(function() {
let menu = document.querySelector('.mobile-menu');
let downToggle = document.querySelectorAll('.sub-menu'),
downToggleTitle = document.querySelectorAll('.sub-menu-title'),
subMenuItems = document.querySelectorAll('.sub-menu-item-mobile'),
searchBar = document.getElementById('mobile-search'),
onclickimg = document.querySelectorAll('.sub-menu-arrow');
function listen() {
downToggleTitle.forEach(dtTitle => {
dtTitle.addEventListener('click', function(e) {
// If there is a menu that's already open
// and it's not the element that's been clicked,
// close it before opening the selected menu
downToggleTitle.forEach(node => {
if (node.classList.contains('expanded') && node !== this) {
node.classList.toggle('expanded');
}
});
// inside each sub-menu is a third-level-sub-menu. So inside each sub-menu
// If it's already open, close it
subMenuItems.forEach(item => {
// console.log("test test")
if (item.classList.contains('expanded') && item !== this) {
item.classList.toggle('expanded');
}
});
this.classList.toggle('expanded');
});
});
subMenuItems.forEach(item => {
item.addEventListener('click', function(e) {
subMenuItems.forEach(item => {
if (items.classList.contains('expanded') && item !== this) {
item.classList.toggle('expanded');
console.log("hello Aug 20");
}
});
this.classList.toggle('expanded');
});
});
}
listen();
}());
If you want to get the parent element of the clicked target, then you can exploit your current eventListener and use the e.target.parentNode to get it. This will return you an element, which you can add/remove CSS classes from and do pretty much everything you like. Keep in mind, you can use the .parentNode multiple times and for example, if you wanted to get the "grandparent" of an element (2 levels up) you could write e.target.parentNode.parentNode and so on.

Prevent actions before setTimeout() is over

Im curently making a memory game and if two card matches it blocks them but if not they should flip back. The problem is that you can click on other cards even if there are two cards already flipped and if you click more cards the game goes crazy and stops working
Any idea?
here's the code
https://codepen.io/stivennpe/pen/KRxvxR?editors=1010
restart ();
bindcards();
// to restart the game and shuffle the cards
function restart() {
$('.restart').on('click', function () {
cards = shuffle($('.card'));
$(".card").each(function() {
$( this ).removeClass( "open match show" );
});
$('.deck').html(cards);
bindcards();
});
}
//to open/show the card
function bindcards(){
$('.card').click(function () {
$(this).addClass('open show');
let openCards = $('.open');
let list = jQuery.makeArray(openCards);
if (list.length === 2 && list[0].innerHTML ===
list[1].innerHTML){
$(openCards).addClass('match');
}
if (list.length === 2) {
setTimeout(hola, 1000)
function hola() {$(openCards).removeClass('open show');
}
}
});
}
thanks
Seems like for a simple solution you could set a global variable, something like blockClicks
If blockClicks is true, do nothing when a user clicks. Reset its value to false after timeout
There is a race condition between applying the .open class to the div element, refreshing the page, querying that element, and the user speeding their way through your game. Instead of adding the .open class to the div, hoping the page refreshes quick enough, then immediately querying against with to find out how many open cards you have, keep a local variable count. Below is the slight modification to your code
function bindcards() {
let numOfOpenCards = 0;
$(".card").click(function(e) {
++numOfOpenCards;
if(numOfOpenCards > 2)
return;
$(this).addClass("open show");
let openCards = $(".open");
let list = jQuery.makeArray(openCards);
if (numOfOpenCards >= 2 && list && list.length >= 2 && list[0].innerHTML === list[1].innerHTML) {
$(openCards).addClass("match");
}
if (numOfOpenCards >= 2) {
setTimeout(hola, 1000);
function hola() {
numOfOpenCards = 0;
let openCards = $(".open");
let list = jQuery.makeArray(openCards);
if(list) {
for(let i = 0; i < list.length; ++i)
$(list[i]).removeClass("open show");
}
}
}
});
}
If two cards are already open, the most recent card check. The previous card check should still complete and refresh as needed.
https://codepen.io/anon/pen/NzPBrB?editors=1010
Additional edit
If you do not want to use numOfOpenCards, you can use the following. Find how many are already open. If there are more than 2 already, just quit. If there are less than already flipped, add the class then query the DOM again. Anywhere numOfCards is used you can replace with list.length;
let openCards = $(".open");
let list = jQuery.makeArray(openCards);
if(list && list.length > 2)
return;
$(this).addClass("open show");
openCards = $(".open");
list = jQuery.makeArray(openCards);
if (list.length >= 2) {
setTimeout(hola, 1000);
function hola() {
$(list).removeClass('open show');
}
}

JS full page slide navigate with keypress

I'm building a sort of presentation using IntersectionObserver API. And I want to be able to go up/down the slides using the keyboard arrow keys.
So far I have managed to make it work on click event. It's easier because I can just ask the interSectionObserver to go to the next sibling on click. But on key press I find the implementation a bit more tricky.
I have a reduced test case on CodePen https://codepen.io/umbriel/pen/ppqLXX
function ioHandler(entries) {
for (let entry of entries) {
entry.target.style.opacity = entry.intersectionRatio.toFixed(2);
entry.target.addEventListener('click', function(e) {
if (this.nextElementSibling !== null) {
this.nextElementSibling.scrollIntoView({
'behavior': 'smooth'
});
}
},true);
if (entry.intersectionRatio > .5) {
entry.target.classList.add('active')
} else {
entry.target.classList.remove('active')
}
}
}
Thanks!
Use the onkeydown event listener
Keycodes:
left = 37
right = 39
up = 38
down = 40

How to determine if element is last or first child of parent in javascript/jquery?

I have the following code, emulating a click on the left or right key down events. This is used as part of a gallery slideshow:
$(document).keydown(function (e) {
if(e.keyCode == 37) { // left
$(".thumb-selected").prev().trigger('click');
}
else if(e.keyCode == 39) { // right
$("thumb-selected").next().trigger('click');
}
});
Essentially it picks the next or previous sibling (depending on the key pressed) and call the click event that will in turn display the appropriate image in the gallery. These images are all stored in a unordered list.
Where I am stumped is that when the first or last image is selected and the left or right button is clicked (respectively), I want it to get the next picture at the opposite end of the list of images. For example, if the first image is selected and the left arrow is pressed; given that there is no previous image (or li element), it will get the last image in the list. This way the keys never lose functionality.
Is there a function in jquery that will either check if the present element is the first or last child of its parent, or return its index relative to its parent so I can compare its index to the size() (child count) of his parent element?
You can use the is()[docs] method to test:
if( $(".thumb-selected").is( ':first-child' ) ) {
// whatever
} else if( $(".thumb-selected").is( ':last-child' ) ) {
// whatever
}
You can do this in native JavaScript like so:
var thumbSelected = document.querySelector('.thumb-selected');
var parent = thumbSelected.parentNode;
if (thumbSelected == parent.firstElementChild) {
// thumbSelected is the first child
} else if (thumbSelected == parent.lastElementChild) {
// thumbSelected is the last child
}
You could use index() which returns the index of the element or you could use prev() and next() to check if there is one more element.
if(e.keyCode == 37) { // left
if($(".thumb-selected").prev().length > 0)){
$(".thumb-selected").prev().trigger('click');
}else{
//no more elements
}
}
else if(e.keyCode == 39) { // right
if($(".thumb-selected").next().length > 0)){
$(".thumb-selected").next().trigger('click');
}else{
//no more elements
}
}
EDIT - i updated the code because it makes more sense to use next() and prev() instead of nextAll() and prevAll()
var length = $('#images_container li').length;
if(e.keyCode == 37) { // left
if($('.thumb-selected').index() > 0)
$(".thumb-selected").prev().trigger('click');
else
$('.thumb-container').eq(length-1).trigger('click');
}
else if(e.keyCode == 39) { // right
if($('.thumb-selected').index() < length-1)
$("thumb-selected").next().trigger('click');
else
$('.thumb-container').eq(0).trigger('click');
}
.thumb-container is the parent element of the all the thumbs. At least what I got from your code.
HTML
<ul class="thumb-container">
<li>thumb</li>
<li>thumb</li>
.
.
.
</ul>

Categories

Resources