This code adds a class (a border in this case) to an single element (a square) when the user clicks on that element. This part of the code is working fine.
I would then like to be able to remove however many borders were added (1-3 in this example), with the click of a single button, using a for loop.
I was able to do remove the borders by just repeating item1.classList.remove('.bigBorder'); (for example with item1) but that certainly does not scale well.
const item = document.querySelector('.item');
const item1 = document.querySelector('.item1');
const item2 = document.querySelector('.item2');
const item3 = document.querySelector('.item3');
const clearBordersButton = document.querySelector('.clearBorders');
const bigBorder = document.querySelector('.bigBorder');
item1.addEventListener('click', function() {
item1.classList.add('bigBorder');
});
item2.addEventListener('click', function() {
item2.classList.add('bigBorder');
});
item3.addEventListener('click', function() {
item3.classList.add('bigBorder');
});
clearBordersButton.addEventListener('click', clearBorders);
function clearBorders() {
for (let i = 0; i < item.length; i++) {
item[i].classList.remove('bigBorder');
}
};
* {
box-sizing: border-box;
}
.container {
width: 960px;
margin: 20px auto;
border: 1px solid black;
}
.boxes {
display: flex;
justify-content: space-around;
border: 1px solid black;
}
.item1,
.item2,
.item3 {
border: 2px solid blue;
margin: 20px 0;
width: 100px;
height: 100px;
}
.bigBorder {
border: 10px solid black;
}
<div class="container">
<div class="boxes">
<div class="item item1">item</div>
<div class="item item2">item</div>
<div class="item item3">item</div>
</div>
<button class="clearBorders">clear borders</button>
</div>
Instead of attaching a listener to each item you can use event delegation - attach one listener to the parent component (.boxes) and have that listen to events as they "bubble up" the DOM from its children.
If you also select all the item elements with querySelectorAll it's a simple process to iterate over them and remove the class.
// Cache the elements
const boxes = document.querySelector('.boxes');
const items = document.querySelectorAll('.item');
const clear = document.querySelector('.clear');
// Add listeners to the container, and the button
boxes.addEventListener('click', handleClick, false);
clear.addEventListener('click', clearBorders, false);
// Because we're using event delegation
// check that the child element that was clicked
// on was has a `.item` class, and then add the new class
function handleClick(e) {
if (e.target.matches('.item')) {
e.target.classList.add('bigBorder');
}
}
function clearBorders() {
items.forEach(item => {
item.classList.remove('bigBorder');
});
};
.boxes{display:flex;justify-content:space-around;width:250px}
.item{display:flex;justify-content:center;align-items:center;background-color:#efefef;border:10px solid white;margin:20px 0;width:50px;height:50px}
.item:hover{cursor:pointer;background-color:#cdcdcd;}
.bigBorder{border:10px solid #000}
<div class="boxes">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
</div>
<button class="clear">clear borders</button>
It's important to note that querySelector returns the first matching element where as querySelectorAll returns a node list of matching elements.
We can use the node list to assign the event lister and remove the class.
//Selects multimple itmes
const items = document.querySelectorAll('.item');
//selects first matching itme
const clearBordersButton = document.querySelector('.clearBorders');
const bigBorder = document.querySelector('.bigBorder');
//Iterate the items
items.forEach(function(element){
//add an event listern
element.addEventListener("click", function(){
//"this" is the item clicked
this.classList.add("bigBorder");
})
})
clearBordersButton.addEventListener('click', clearBorders);
function clearBorders() {
//find the elements
document.querySelectorAll(".item.bigBorder").forEach(function(element){
//remove the class
element.classList.remove("bigBorder");
})
};
* {
box-sizing: border-box;
}
.container {
width: 960px;
margin: 20px auto;
border: 1px solid black;
}
.boxes {
display: flex;
justify-content: space-around;
border: 1px solid black;
}
.item1,
.item2,
.item3 {
border: 2px solid blue;
margin: 20px 0;
width: 100px;
height: 100px;
}
.bigBorder {
border: 10px solid black;
}
<div>
<div class="container">
<div class="boxes">
<div class="item item1">item</div>
<div class="item item2">item</div>
<div class="item item3">item</div>
</div>
<button class="clearBorders">clear borders</button>
</div>
</div>
Related
I use a filtering function in my webpage. When a button is clicked, it hides the associated elements and fades itself. Once it's clicked again, it brings those elements back and restores its color.
What I want to do is to add two additional buttons: "Show all" and "Hide all". I want them to show/hide all elements and also fade/restore all the button colors upon click.
First I made an onclick event handler for these two buttons, but it didn't work properly. I think I have to combine everything in the same onload event, and that's where I got stuck. Could you please help me to modify my js to achieve my goal?
Snippet:
for (let button of document.querySelectorAll(".filterbutton")) {
button.addEventListener("click", filter);
}
let filters = new Set;
function toggleDisplay(selector, display) {
let elems = document.querySelectorAll(selector);
for (let elem of elems) {
elem.style.display = display;
}
}
function filter() {
let filterSelector = this.dataset.filter;
let show = filters.delete(filterSelector);
this.style.color = show ? "" : "rgb(200,200,200)";
if (!show) {
filters.add(filterSelector); // toggle this filter
} else {
toggleDisplay(filterSelector, "");
}
if (filters.size) {
toggleDisplay([...filters].join(","), "none");
}
}
.filterbutton,
.showallbutton,
.hideallbutton {
border: 1px solid;
display: inline-block;
background: lightblue;
padding: 5px;
cursor: pointer
}
.group a {
display: block;
}
<div class="filter">
<div class="filterbutton" data-filter=".filter01">Filter 01</div>
<div class="filterbutton" data-filter=".filter02">Filter 02</div>
<div class="filterbutton" data-filter=".filter03">Filter 03</div>
</div>
<div class="group">
<a class="filter01 filter02 filter03">This element has filter01, filter02 and filter03</a>
<a class="filter01 filter02">This element has filter01 and filter02</a>
<a class="filter01 filter03">This element has filter01 and filter03</a>
<a class="filter02 filter03">This element has filter02 and filter03</a>
<a class="filter01">This element has filter01 only</a>
<a class="filter02">This element has filter02 only</a>
<a class="filter03">This element has filter03 only</a>
</div>
<div class="show-hide">
<div class="showallbutton">Show all</div>
<div class="hideallbutton">Hide all</div>
</div>
I always delegate
Here I delegate from document; if you can find a closer container, use that instead
I also removed the dot from the data-filter=". <<<
Lastly I made the color change a class.
the code to detect the show/hide buttons can be simpler if you give both a common class
Here is the solution based on a PEN you showed me
window.addEventListener("DOMContentLoaded", function() {
const filtered = document.querySelectorAll(".group a");
const buttons = document.querySelectorAll(".filter-button");
const toggleContent = () => {
let filterSelector = [...buttons] // the buttons
.filter(btn => btn.matches(".inactive")) // that are grey
.map(btn => btn.dataset.filter); // get their data-filter
console.log(filterSelector)
filtered.forEach(anc => { // Hide the links whose classes are are in the filterSelector
const hide = filterSelector.length > 0 &&
filterSelector.some(filter => [...anc.classList].includes(filter))
anc.style.display = hide ? "none" : "inline-block";
});
}
const filter = e => {
const tgt = e.target; // what was clicked?
if (tgt.matches(".filter-button")) { // a button?
tgt.classList.toggle("inactive"); // toggle it
toggleContent(); // toggle the links
} else if (tgt.closest(".toggleall")) { // or if (!tgt.matches("toggleButton")) return if you give the buttons a class
const show = tgt.matches(".toggleall-show"); // show all?
const hide = tgt.matches(".toggleall-hide"); // hide all?
if (!show && !hide) return; // something else was clicked
buttons.forEach(btn => btn.classList[show ? "remove" : "add"]("inactive")); // if show, remove all inactive if not, addd all inacctive
toggleContent(); // toggle the links
}
};
document.addEventListener("click", filter);
});
#charset "utf-8";
.container {
width: calc(100% - 56px);
padding: 18px;
padding-bottom: 2px;
margin: 10px;
float: left;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 0 1px rgb(0, 0, 0);
}
.toggleall {
margin-bottom: 16px;
display: block;
font-family: Arial, Helvetica, sans-serif;
color: rgb(70, 70, 70);
}
.toggleall-show,
.toggleall-hide {
width: 70px;
padding: 5px;
margin: 2px;
font-family: Arial, Helvetica, sans-serif;
color: rgb(70, 70, 70);
font-weight: bold;
font-size: 12px;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 5px;
box-shadow: inset 0 0 0 1px rgb(200, 200, 200);
transition: all 0.5s ease;
}
.toggleall-show:hover,
.toggleall-hide:hover {
text-decoration: underline;
transition: all 0.1s ease;
}
.filter {
margin-bottom: 16px;
display: block;
font-family: Arial, Helvetica, sans-serif;
color: rgb(70, 70, 70);
}
.filter-title,
.filter-button {
padding: 5px;
margin: 2px;
font-family: Arial, Helvetica, sans-serif;
color: rgb(70, 70, 70);
font-weight: bold;
font-size: 12px;
text-align: center;
display: inline-block;
border-radius: 5px;
}
.filter-button {
cursor: pointer;
box-shadow: inset 0 0 0 1px rgb(200, 200, 200);
}
.filter-button:hover {
text-decoration: underline;
}
.inactive {
color: rgb(200, 200, 200);
}
.group {
width: calc(100% - 60px);
padding: 20px;
padding-bottom: 40px;
margin: 10px;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-auto-rows: 200px;
grid-gap: 20px;
grid-auto-flow: dense;
counter-reset: div;
float: left;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 0 1px rgb(0, 0, 0);
}
.group a {
height: 200px;
display: inline-flex;
position: relative;
overflow: hidden;
background: rgb(255, 255, 255);
transition: all 0.5s ease, height 0s;
}
<div class="platform-controller">
<div class="filter">
<div class="filter-title">Filter List 1:</div>
<div class="filter-button" data-filter="filter0a">Item A</div>
<div class="filter-button" data-filter="filter0b">Item B</div>
<div class="filter-button" data-filter="filter0c">Item C</div>
</div>
<div class="filter">
<div class="filter-title">Filter List 2:</div>
<div class="filter-button" data-filter="filter01">Item 1</div>
<div class="filter-button" data-filter="filter02">Item 2</div>
<div class="filter-button" data-filter="filter03">Item 3</div>
</div>
<div class="toggleall">
<div class="toggleall-show">Show all</div>
<div class="toggleall-hide">Hide all</div>
</div>
</div>
<div class="group">
<a class="filter0a filter01" href="www.google.com">Image 1</a>
<a class="filter0b filter02" href="www.google.com">Image 2</a>
<a class="filter0c filter03" href="www.google.com">Image 3</a>
</div>
What I've done is wrapped the adding of event listeners into a function, and made sure that function is only called after the document is loaded. This ensures that the listeners make it to the button regardless of when the script is loaded.
I've also added a function, toggleAll that accepts a state boolean`. If true, all filter-ables will be displayed, and if false, all hidden. Along with this, I changed toggleDisplay's second argument to be a bool instead of a string. I did this because the word "toggle" suggests an on and off state as opposed to any number of possible strings.
Lastly, I added a .toggled class to be applied to the buttons. This just removes the color value from being a magic string.
let filterButtons = null;
const allPossibleFilters = [];
// Cannot be const since we need to set it again in toggleAll
let filters = new Set();
/**
* Initialize the page
*
* Adds the event listeners to the buttons
*/
function init() {
// Store for later use
filterButtons = document.querySelectorAll(".filterbutton");
filterButtons.forEach((btn) => {
// Add event listeners
btn.addEventListener("click", filter);
// Scrape possible filters
allPossibleFilters.push(btn.dataset.filter);
});
document.querySelector('.showallbutton').addEventListener("click", toggleAll.bind(null, true));
document.querySelector('.hideallbutton').addEventListener("click", toggleAll.bind(null, false));
}
function toggleDisplay(selector, displayState) {
const elems = document.querySelectorAll(selector);
const display = displayState ? "" : "none"
for (const elem of elems) {
elem.style.display = display;
}
}
function filter() {
const filterSelector = this.dataset.filter;
const show = filters.delete(filterSelector);
if (show) {
toggleDisplay(filterSelector, true);
this.classList.remove("toggled");
} else {
filters.add(filterSelector);
this.classList.add("toggled");
}
if (filters.size) {
toggleDisplay([...filters].join(","), false);
}
}
/**
* Toggle the view state of all filter-ables
*
* #param {bool} state Whether or not all filter-ables should be shown
*/
function toggleAll(state) {
if (state) {
filters.clear();
filterButtons.forEach((btn) => {
btn.classList.remove("toggled");
});
toggleDisplay(allPossibleFilters.join(','), state);
} else {
filters = new Set(allPossibleFilters);
filterButtons.forEach((btn) => {
btn.classList.add("toggled");
});
toggleDisplay([...filters].join(','), state);
}
}
/**
* Call the init function once the page has finished loading
*/
if (document.readyState != "loading") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
.filterbutton,
.showallbutton,
.hideallbutton {
border: 1px solid;
display: inline-block;
background: lightblue;
padding: 5px;
cursor: pointer
}
.group a {
display: block;
}
.filterbutton.toggled {
color: rgb(200, 200, 200);
}
<div class="filter">
<div class="filterbutton" data-filter=".filter01">Filter 01</div>
<div class="filterbutton" data-filter=".filter02">Filter 02</div>
<div class="filterbutton" data-filter=".filter03">Filter 03</div>
</div>
<div class="group">
<a class="filter01 filter02 filter03">This element has filter01, filter02 and filter03</a>
<a class="filter01 filter02">This element has filter01 and filter02</a>
<a class="filter01 filter03">This element has filter01 and filter03</a>
<a class="filter02 filter03">This element has filter02 and filter03</a>
<a class="filter01">This element has filter01 only</a>
<a class="filter02">This element has filter02 only</a>
<a class="filter03">This element has filter03 only</a>
</div>
<div class="show-hide">
<div class="showallbutton">Show all</div>
<div class="hideallbutton">Hide all</div>
</div>
I'm trying to build a very basic chess board (with no embedded rules) as a learning challenge. Basically my board is made of 64 divs, where each div has a class .square. I have two functions: one for adding the .active class to the clicked square, and another function for moving the piece with the .active class to the new square. I've tried to put the two different eventListeners to call the different functions in an IF ELSE statement, but my condition doesn't work because the querySelector doesn't check in real time if there's any div with the class .active at that given moment.
My code looks like this:
let squares = document.querySelectorAll(`.square`);
let activeSquares = document.querySelectorAll(`.active`);
// This condition needs to check if there's any square with the class .active in REAL TIME
if (activeSquares.length > 0) {
squares.forEach(square => {
square.addEventListener(`click`, movePiece);
function movePiece() {
let pieceToMove = document.querySelector(`.active`).textContent;
square.textContent = pieceToMove;
}
});
} else {
squares.forEach(square => {
square.addEventListener(`click`, selectPiece);
function selectPiece() {
square.className = `active square`;
}
});
}
How do I make it check for the class in real time? Or is my approach completely wrong?
Please be merciful, I've only been learning this stuff for a couple months, if I'm missing some basic knowledge please point it out so I can look it up.
Thanks!
In the event handler (a function that is called when a registered event is triggered) we use the Event Object property Event.target which always points to the tag that the user interacted with. Read the following articles for more details:
Events
Event Delegation
Details are commented in example
// Reference <table>
const board = document.querySelector('.board');
// This function goes beyond the scope of the question
const buildTable = table => {
const file = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'g'];
const rank = [8, 7, 6, 5, 4, 3, 2, 1];
let matrix = [];
for (let r = 0; r < 8; r++) {
let row = table.insertRow();
matrix.push([]);
for (let c = 0; c < 8; c++) {
let col = row.insertCell();
col.dataset.pos=`${file[c]+rank[r]}`;
matrix[r].push(`${file[c]+rank[r]}`);
}
}
return matrix;
};
const matrix = buildTable(board);
//console.log(matrix);
// Bind the 'click' event to <table>
board.onclick = activeSQ;
function activeSQ(e) {
// Reference the tag user clicked
const clk = e.target;
/*
If the user clicked a <td>...
...remove .active from the <td> that was .active previously...
...then add .active to the <td> the user clicked...
...log the algebraic notation of the clicked square
*/
if (clk.matches('td')) {
const prev = this.querySelector('td.active');
if (prev) {
prev.classList.remove('active');
}
clk.classList.add('active');
console.log(clk.dataset.pos);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
font: 1vw/1 'Segoe UI';
}
html,
body {
width: 100%;
height: 100%;
}
body {
overflow: hidden;
}
table {
table-layout: fixed;
border-collapse: collapse;
width: 40%;
margin: 2.5% auto;
border: 0.5px solid lightgrey;
}
td {
width: 12.5%;
height: 5rem;
border: 0.5px solid lightgrey;
}
tr:nth-of-type(odd) td:nth-of-type(even) {
background: black;
}
tr:nth-of-type(even) td:nth-of-type(odd) {
background: black;
}
.pos {
display: block;
}
.white,
.black {
display: inline-flex;
justify-content: center;
align-items: center;
width: 90%;
height: 90%;
}
td.active {
outline: 3px inset gold;
}
.as-console-row::after { width: 0; font-size: 0; }
.as-console-row-code { width: 100%; word-break: break-word; }
.as-console-wrapper { max-height: 25% !important; }
<table class='board'></table>
Its simple code will look like this, you can develop it :
const squares = document.getElementsByClassName('square')
Array.from(squares).forEach(ele => {
ele.addEventListener('click', function () {
if(this.classList.contains('active')) {
this.classList.remove("active")
} else {
currentActive = document.getElementsByClassName('active')[0]
if (currentActive) {
let thisHtml = this.innerHTML
this.innerHTML = currentActive.innerHTML
currentActive.innerHTML = thisHtml
currentActive.classList.remove("active")
} else {
this.classList.add("active")
}
}
})
})
#container {
height: 169px;
width: 169px;
}
.square {
height: 40px;
width: 40px;
background-color: #ccc;
float:left;
left: 0;
border: solid 1px;
}
.active {
background-color: red;
}
<div id="container">
<div class='square'>1</div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
<div class='square'></div>
</div>
I have an array of elements. I want when I hover on any element from this array to add a class to said element. How can I do that?
Loop through each item in the array and an event listener for mouseover which adds the class.
const array = document.querySelectorAll('div');
const classToAdd = 'red';
array.forEach(e => e.addEventListener('mouseover', () => {
e.classList.add(classToAdd);
}))
div {
height: 100px;
width: 100px;
border: 1px solid;
}
.red {
background-color: red;
}
<div></div>
<div></div>
<div></div>
<div></div>
If you have multiple elements with different ids or class you can try this.
let elements = ['#hud-menu', '#hud-intro', '.hud-shop', '.hud-settings'];
document.querySelectorAll(elements).forEach(element => {
element.addEventListener('mouseover', () => {
element.classList.add('blue');
});
});
div {
height: 100px;
width: 100px;
border: 2px solid;
border-radius: 10px;
text-align: center;
}
.blue {
background-color: blue;
}
<div id="hud-menu">Hud Menu</div><br>
<div id="hud-intro">Hud Intro</div><br>
<div class="hud-shop">Hud Shop</div><br>
<div class="hud-settings">Hud Settings</div>
And just in case if you want to remove the class when you stop hovering over an element you can use mouseout like the example below.
let elements = ['#hud-menu', '#hud-intro', '.hud-shop', '.hud-settings'];
// Add class on mouseover
document.querySelectorAll(elements).forEach(element => {
element.addEventListener('mouseover', () => {
element.classList.add('blue');
});
});
// Remove class on mouseout
document.querySelectorAll(elements).forEach(element => {
element.addEventListener('mouseout', () => {
element.classList.remove('blue');
});
});
div {
height: 100px;
width: 100px;
border: 2px solid;
border-radius: 10px;
text-align: center;
}
.blue {
background-color: blue;
}
<div id="hud-menu">Hud Menu</div><br>
<div id="hud-intro">Hud Intro</div><br>
<div class="hud-shop">Hud Shop</div><br>
<div class="hud-settings">Hud Settings</div>
I would like to get an html element without its children, to set an event addEventListener("click) on it, so that the function will only be executed when it is clicked, not on its children. I can only use Javascript. Is this possible?
const divs = document.querySelectorAll("div");
const body = document.querySelector("body");
const myFunction = function() {
this.classList.add("clicked")
}
divs.forEach(function(element) {
element.addEventListener("click", myFunction)
});
.grandparent {
padding: 20px;
border: 5px solid black;
}
.parent {
padding: 20px;
border: 5px solid blue;
}
.child {
padding: 20px;
height: 100px;
width: 100px;
border: 5px solid yellow;
}
.clicked {
background-color: red;
}
<div data-time="3000" class="grandparent">
<div data-time="2000" class="parent">
<div data-time="1000" class="child"></div>
</div>
</div>
this function adds a class to each div, now I would like clicking outside of div to remove that class, however the body variable contains including its children.
If you want to ignore all and any child clicks, then check if the currentTarget is different than the target of the event.
target is the element the event originated from (the deepest child that received the event)
currentTarget is the element on which the event handler is attached
const divs = document.querySelectorAll("div");
const body = document.querySelector("body");
const myFunction = function(event) {
if (event.target === event.currentTarget) {
this.classList.add("clicked")
}
}
divs.forEach(function(element) {
element.addEventListener("click", myFunction)
});
.grandparent {
padding: 20px;
border: 5px solid black;
}
.parent {
padding: 20px;
border: 5px solid blue;
}
.child {
padding: 20px;
height: 100px;
width: 100px;
border: 5px solid yellow;
}
.clicked {
background-color: red;
}
<div data-time="3000" class="grandparent">
<div data-time="2000" class="parent">
<div data-time="1000" class="child"></div>
</div>
</div>
I am having problem of traversing through each HTML element one by one.There are two buttons #up and #down.On click of #up the id #myID should move to the next element upwards and vice versa for #down.The problem is I am able to move through the siblings but not through the child elements.
For example if I click on #down the id #myID should have moved to p tag which is the child of that div on next click to span which is child of p then on next click to div.But in my code it is directly jumping to div ignoring the children.
JSFIDDLE
Here is the code:
$("#up").click(function() {
$("#startHere").find("#myID").next().attr('id', 'myID');
$('#startHere').find("#myID").removeAttr('id');
});
$("#down").click(function() {
$("#startHere").find("#myID").prev().attr('id', 'myID');
$('#startHere').find("#myID").next().removeAttr('id');
})
#myID {
border: 2px solid yellow;
}
#startHere {
height: 100%;
width: 50%;
}
div {
height: 100px;
width: 100px;
border: 2px solid;
margin: 10px;
}
p {
height: 50px;
width: 50px;
border: 2px solid blue;
margin: 10px;
}
h1 {
height: 40px;
width: 40px;
border: 2px solid red;
margin: 10px;
}
span {
display: block;
height: 25px;
width: 25px;
border: 2px solid green;
margin: 10px;
}
button {
height: 25px;
width: 100px;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="up">GO DOWN</button>
<button id="down">GO UP</button>
<div id="startHere">
<div id="myID">
<p><span></span></p>
</div>
<div><span></span></div>
<div>
<h1></h1>
</div>
<p></p>
<h1></h1>
<p><span></span></p>
</div>
I think you can just find all the elements first, jQuery returns them in DOM order, which is what you want. No need to search for the next/prev element on-the-fly.
var allElements = $("#startHere").find('*');
var currentIndex = allElements.index('#myID');
function move(delta) {
// Find the new index
var index = currentIndex + delta;
// Clamp to 0…lengh of list
// Here we could also make it wrap instead
index = Math.max(Math.min(index, allElements.length - 1), 0);
// Remove the ID from the old element
allElements.eq(currentIndex).removeAttr('id');
// Add the ID to the new element
allElements.eq(index).attr('id', 'myID');
// Update the index
currentIndex = index;
}
$("#up").click(function() {
move(1);
});
$("#down").click(function() {
move(-1);
})
var allElements = $("#startHere").find('*');
var currentIndex = allElements.index('#myID');
function move(delta) {
// Find the new index
var index = currentIndex + delta;
// Clamp to 0…lengh of list
// Here we could also make it wrap instead
index = Math.max(Math.min(index, allElements.length - 1), 0);
// Remove the ID from the old element
allElements.eq(currentIndex).removeAttr('id');
// Add the ID to the new element
allElements.eq(index).attr('id', 'myID');
// Update the index
currentIndex = index;
}
$("#up").click(function() {
move(-1);
});
$("#down").click(function() {
move(1);
})
#myID {
border: 2px solid yellow;
}
#startHere {
height: 100%;
width: 50%;
}
div {
height: 100px;
width: 100px;
border: 2px solid;
margin: 10px;
}
p {
height: 50px;
width: 50px;
border: 2px solid blue;
margin: 10px;
}
h1 {
height: 40px;
width: 40px;
border: 2px solid red;
margin: 10px;
}
span {
display: block;
height: 25px;
width: 25px;
border: 2px solid green;
margin: 10px;
}
button {
height: 25px;
width: 100px;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="down">GO DOWN</button>
<button id="up">GO UP</button>
<div id="startHere">
<div id="myID">
<p><span></span></p>
</div>
<div><span></span></div>
<div>
<h1></h1>
</div>
<p></p>
<h1></h1>
<p><span></span></p>
</div>
If you do need the elements on-the-fly (because they might have changed), you can still use the same tactic (and simply build up the allElements list in the move function and get the index using allElements.index('#myID')) but it might be more performant to update the list only when you know it changed (after an Ajax request, after modification on event handlers, etc.).
Edit:
The code for searching the next/prev element on-the-fly is a bit more work because it has to recurse when traversing up but makes it possible to have a different set of rules for up vs. down movement.
var boundary = $("#startHere");
function findNext(node, anchor) {
if(!anchor && node.children(':first-child').length) {
return node.children(':first-child');
}
if(node.next().length) {
return node.next();
}
if(!boundary.find(node.parent()).length) {
// Out of boundary. Stick to the last node
return anchor||node;
}
return findNext(node.parent(), anchor||node);
}
function findPrev(node, anchor) {
if(!anchor && node.children(':last-child').length) {
return node.children(':last-child');
}
if(node.prev().length) {
return node.prev();
}
if(!boundary.find(node.parent()).length) {
// Out of boundary. Stick to the last node
return anchor||node;
}
return findPrev(node.parent(), anchor||node);
}
function move(finder) {
// Find the current item
var current = boundary.find('#myID');
// Find the next item
var next = finder(current);
// Remove the ID from the old element
current.removeAttr('id');
// Add the ID to the new element
next.attr('id', 'myID');
}
$("#up").click(function() {
move(findPrev);
});
$("#down").click(function() {
move(findNext);
})
var boundary = $("#startHere");
function findNext(node, anchor) {
if(!anchor && node.children(':first-child').length) {
return node.children(':first-child');
}
if(node.next().length) {
return node.next();
}
if(!boundary.find(node.parent()).length) {
// Out of boundary. Stick to the last node
return anchor||node;
}
return findNext(node.parent(), anchor||node);
}
function findPrev(node, anchor) {
if(!anchor && node.children(':last-child').length) {
return node.children(':last-child');
}
if(node.prev().length) {
return node.prev();
}
if(!boundary.find(node.parent()).length) {
// Out of boundary. Stick to the last node
return anchor||node;
}
return findPrev(node.parent(), anchor||node);
}
function move(finder) {
// Find the current item
var current = boundary.find('#myID');
// Find the next item
var next = finder(current);
// Remove the ID from the old element
current.removeAttr('id');
// Add the ID to the new element
next.attr('id', 'myID');
}
$("#up").click(function() {
move(findPrev);
});
$("#down").click(function() {
move(findNext);
})
#myID {
border: 2px solid yellow;
}
#startHere {
height: 100%;
width: 50%;
}
div {
height: 100px;
width: 100px;
border: 2px solid;
margin: 10px;
}
p {
height: 50px;
width: 50px;
border: 2px solid blue;
margin: 10px;
}
h1 {
height: 40px;
width: 40px;
border: 2px solid red;
margin: 10px;
}
span {
display: block;
height: 25px;
width: 25px;
border: 2px solid green;
margin: 10px;
}
button {
height: 25px;
width: 100px;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="down">GO DOWN</button>
<button id="up">GO UP</button>
<div id="startHere">
<div id="myID">
<p><span></span></p>
</div>
<div><span></span></div>
<div>
<h1></h1>
</div>
<p></p>
<h1></h1>
<p><span></span></p>
</div>
This is really bad UI. To select some nodes in some states, you first have to navigate “UP” and then “DOWN” again. But it seems to do what you ask for.