I browsed thru various similar questions, but I found nowhere real help on my problem. There are some Jquery examples, but none of them weren't applicable, since I want to do it by vanilla JavaScript only.
The Problem
I have rating widget which I want to build like every time I click on "p" element, eventListener adds class "good" on all p elements before and on the clicked one, and remove class good on all that are coming after the clicked one.
My Thoughts
I tried to select all the "p" elements and iterate over them by using a for loop to add class before and on clicked element, and to leave it unchanged or remove after, but somewhere I am doing it wrong, and it adds class only on clicked element not on all previous ones.
My Code
function rating () {
var paragraph = document.getElementsByTagName('p');
for (var i = 0; i < paragraph.length; i++) {
paragraph[i].addEventListener('click', function(e) {
e.target.className='good';
})
}
}
rating();
Result
Expected result is that every p element before the clicked one, including clicked one should be having class good after click and all the ones that are after the clicked one, should have no classes.
Actual Result: Only clicked element is having class good.
Using Array#forEach , Element#nextElementSibling and Element#previousElementSibling
General logic behind this is to loop through all the previous and next sibling elements. For each element, add or remove the class .good until there are no more siblings to handle.
const ps = document.querySelectorAll('p');
ps.forEach(p => {
p.addEventListener("click", function() {
let next = this.nextElementSibling;
let prev = this;
while(prev !== null){
prev.classList.add("good");
prev = prev.previousElementSibling;
}
while(next !== null){
next.classList.remove("good");
next = next.nextElementSibling;
}
})
})
p.good {
background-color: red;
}
p.good::after {
content: ".good"
}
p {
background-color: lightgrey;
}
<div id='rating'>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
</div>
Alternative:
Using Array#slice to get the previous and next group of p's.
const ps = document.querySelectorAll('p');
ps.forEach((p,i,list) => {
p.addEventListener("click", function() {
const arr = [...list];
const previous = arr.slice(0,i+1);
const next = arr.slice(i+1);
previous.forEach(pre=>{
pre.classList.add("good");
})
next.forEach(nex=>{
nex.classList.remove("good");
});
})
})
p.good {
background-color: red;
}
p.good::after {
content: ".good"
}
p {
background-color: lightgrey;
}
<div id='rating'>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
</div>
function setup() {
var paragraph = document.querySelectorAll("p");
for (p of paragraph) {
p.onclick = (event) => {
let index = Array.from(paragraph).indexOf(event.target);
[].forEach.call(paragraph, function(el) {
el.classList.remove("good");
});
for (let i = 0; i <= index; i++) {
paragraph[i].classList.add("good");
}
}
}
}
//Example case
document.body.innerHTML = `
<div id='rating'>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
</div>`;
setup();
The key ingredient is to use Node.compareDocumentPosition() to find out if an element precedes or follows another element:
var paragraphs;
function handleParagraphClick(e) {
this.classList.add('good');
paragraphs.forEach((paragraph) => {
if (paragraph === this) {
return;
}
const bitmask = this.compareDocumentPosition(paragraph);
if (bitmask & Node.DOCUMENT_POSITION_FOLLOWING) {
paragraph.classList.remove('good');
}
if (bitmask & Node.DOCUMENT_POSITION_PRECEDING) {
paragraph.classList.add('good');
}
});
}
function setup() {
paragraphs = [...document.getElementsByTagName('p')];
paragraphs.forEach((paragraph) => {
paragraph.addEventListener('click', handleParagraphClick);
});
}
setup();
#rating { display: flex; }
p { font-size: 32px; cursor: default; }
p:hover { background-color: #f0f0f0; }
.good { color: orange; }
<div id='rating'>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
<p>*</p>
</div>`
Related
I have a bunch of divs with some text in them.
I am trying to create a simple function that targets divs with a specific text and gives them a class. I also want to give the divs with other text a common class.
I have managed to target the divs with the specific texts, but I cant figure out how to target the divs with other texts.
This is the HTML
<h1>The divs</h1>
<div>High</div>
<div>Low</div>
<div>Medium</div>
<div>Hit 'em high</div>
<div>Medium rare</div>
<div>A funky name</div>
and this is the function
const divs = document.querySelectorAll('div');
function classChanger () {
for (let i = 0; i < divs.length; i++) {
if (divs[i].textContent === 'High') {
divs[i].classList.add('high');
} if (divs[i].textContent === 'Medium') {
divs[i].classList.add('medium');
} if (divs[i].textContent === 'Low') {
divs[i].classList.add('low');
} if (divs[i].textContent === 'anything') {
divs[i].classList.add('none');
}
}
};
classChanger();
I have tried to use several more if statemets with !== 'High' and so on, but they just overwrite the previous code.
So I am just wondering, how do I target the last three divs and give them the class none?
I have tried googling but I dont think im googling the right question, thats why I am asking here.
You can add High, Medium, and Low to an array. As you loop over the elements check to see if array includes the text (and set the class to the lowercase version of the text), otherwise add a none class.
const divs = document.querySelectorAll('div');
const range = ['High', 'Medium', 'Low'];
function classChanger() {
for (let i = 0; i < divs.length; i++) {
const text = divs[i].textContent;
if (range.includes(text)) {
divs[i].classList.add(text.toLowerCase());
} else {
divs[i].classList.add('none');
}
}
}
classChanger();
.high { color: red; }
.medium { color: orange; }
.low { color: yellow; }
.none { color: darkgray; }
<div>High</div>
<div>Low</div>
<div>Medium</div>
<div>Hit 'em high</div>
<div>Medium rare</div>
<div>A funky name</div>
You Can use an else if statement like this:
const divs = document.querySelectorAll("div");
function classChanger() {
for (let i = 0; i < divs.length; i++) {
if (divs[i].textContent === "High") {
divs[i].classList.add("high");
} else if (divs[i].textContent === "Medium") {
divs[i].classList.add("medium");
} else if (divs[i].textContent === "Low") {
divs[i].classList.add("low");
} else {
divs[i].classList.add("none");
}
}
}
classChanger();
If you want to target elements based on content, you first need to create a function for such purpose like:
function queryElementsWithFilter(selector, filterCallback) {
if (typeof selector !== 'string') throw new TypeError('param 1 must be a string')
if (typeof filterCallback !== 'function') throw new TypeError('param 2 must be a function')
const nodes = [...document.querySelectorAll(selector)]
return nodes.filter(filterCallback)
}
Now you can get the elements you want and do anything with them.
const textTokens = ['High', 'Medium', 'Low']
textTokens.forEach(textToken => {
const divCollection = queryElementsWithFilter(
'div',
node => node.textContent === textToken
)
divCollection.forEach(div => div.classList.add(textToken.toLowerCase()))
})
const unmatchedNodes = queryElementsWithFilter(
'div',
node => !textTokens.includes(node.textContent)
)
unmatchedNodes.forEach(div => div.classList.add('none'))
I would like to find out how can I go (on click) through every element with class children. If children element has additional class selected I would like to set counter to += 1.
After All I have plan to print message on same click: You have either 0,1,2,3 elements selected with class .SELECTED
Can you help me on that?
const children = document.querySelectorAll('.children');
const selected = document.getElementsByClassName('selected');
const button = document.getElementById('bttn');
let counter = 0;
for (let i = 0; i < children.length; i++) {
children[i].addEventListener('click', () => {
if (children[i].classList.contains('selected')) {
children[i].classList.remove('selected')
} else {
children[i].classList.add('selected')
}
button.addEventListener('click', () => {
for (u = 0; u < children[i]; u++) {
if (children[i].classList == 'selected') {
counter++;
}
}
console.log(counter);
})
})
}
#div {
display: inline-flex;
flex-direction: column;
}
.children {
cursor: pointer;
}
.selected {
background-color: royalblue;
}
<div id="div">
<span class='children selected'>children1</span>
<span class='children'>children2</span>
<span class='children'>children3</span>
</div>
<p><input id="bttn" type='button' value='klik'></p>
You can make your loops a little easier to read using a forEach loop and referencing objects with event.target. Rather than testing to see if an element contains a class just to add/remove it, use classList.toggle().
e.target.classList.toggle('selected');
To get the total number of children with class selected you can simply do this one liner:
counter = document.querySelectorAll('.children.selected').length
const children = document.querySelectorAll('.children');
const selected = document.getElementsByClassName('selected');
const button = document.getElementById('bttn');
let counter = 0;
children.forEach(el => {
el.addEventListener('click', (e) => {
e.target.classList.toggle('selected');
})
})
button.addEventListener('click', () => {
counter = document.querySelectorAll('.children.selected').length;
console.log(counter);
})
#div {
display: inline-flex;
flex-direction: column;
}
.children {
cursor: pointer;
}
.selected {
background-color: royalblue;
}
<div id="div">
<span class='children selected'>children1</span>
<span class='children'>children2</span>
<span class='children'>children3</span>
</div>
<p><input id="bttn" type='button' value='klik'></p>
I think the following more or less does what you are hoping to accomplish(?). The total reported should vary between 0 and max number of .children elements - the counter therefore needs to be reset when the button is clicked and the count re-calculated.
/*
initial variables for counter,
click event and class names
*/
let counter=0;
let pcn='children';
let cn='selected';
let evt='click';
/*
shorthand utility methods
*/
const d=document;
const qa=(n,e)=>n.querySelectorAll(e);
const q=(n,e)=>n.querySelector(e);
/*
Find all nodes with class `children`
and iterate through collection.
Add a `click` handler to each and
toggle the active class `selected`
when clicked.
Increase the counter accordingly.
*/
let col=qa(d, '.' + pcn );
col.forEach( n=>{
n.addEventListener(evt,e=>{
n.classList.toggle( cn );
counter += n.classList.contains( cn ) ? 1 : 0;
});
});
/*
Assign a `click` evnt handler to each button ( assumed more than one )
- reset the counter when clicked and restart the counter.
*/
qa(d,'input[type="button"]').forEach(n=>{
n.addEventListener(evt,e=>{
counter=0;
col.forEach( c=>{
if( c.classList.contains( cn ) )counter++;
});
console.info( counter );
});
});
div {
display: inline-flex;
flex-direction: column;
}
.children {
cursor: pointer;
}
.selected {
background-color: royalblue;
}
<div>
<span class='children selected'>children1</span>
<span class='children'>children2</span>
<span class='children'>children3</span>
</div>
<input type='button' value='klik' />
I have been looking at how to use classes to create objects in Javascript and wanted to create a class with a function inside that I could use once a div is clicked...However, every time I click the div I get TypeError
I have google search answers and see results with a button in html solution, but I want to do this using javascript only.
I have tried using
document.getElementById("myDiv").onclick = function(event) {Bucket.selectAndFill(event)};
but it says that selectAndFill is not a function in the console.
let buckets = [];
class Bucket {
constructor(Content, SelectStatus) {
this.content = Content;
this.selectStatus = SelectStatus;
}
selectAndFill() {
for (let bucket of buckets) {
this.content = "i changed it";
}
}
}
for (let i = 0; i < 4; i++) {
buckets[i] = new Bucket("empty", false);
console.log(buckets[i]);
}
//this line was my attempt but it says that selectAndFill is not a function in the console
document.getElementById("myDiv").onclick = function() {
Bucket.selectAndFill()
};
#myDiv {
position: relative;
width: 100px;
height: 100px;
background: red;
}
<div id="myDiv">
</div>
You can call the function from Class modifying the call this way:
const bucketFill = new Bucket;
document.getElementById("myDiv").onclick = bucketFill.selectAndFill;
Just add static keyword next to the method.
let buckets = [];
class Bucket {
constructor(Content, SelectStatus) {
this.content = Content;
this.selectStatus = SelectStatus;
}
static selectAndFill() {
for (let bucket of buckets) {
this.content = "i changed it";
}
console.log(buckets);
}
}
for (let i = 0; i < 4; i++) {
buckets[i] = new Bucket("empty", false);
console.log(buckets[i]);
}
document.getElementById("myDiv").onclick = function() {
Bucket.selectAndFill();
};
#myDiv {
position: relative;
width: 100px;
height: 100px;
background: red;
}
<div id="myDiv">
</div>
I am trying to compare two values of different div classes using the data attribute. I have multiple div classes like these two, all with different data-vale:
<div class="card" data-value=38>
<img class ="front-face" src= "PNG/QH.png"/>
</div>
<div class="card" data-value=39>
<img class ="front-face" src= "PNG/KH.png"/>
</div>
Every div class will also receive a unique order using cards.style.order
In a JS function the user will select two cards (firstCard,secondCard) and the function should check if the value of firstCard is one greater than the value of the card with order 1 more than secondCard.
Here is the code I have been trying to use:
const cards = document.querySelectorAll('.card');
if(firstCard.dataset.value === cards[cards.indexOf(secondCard)-1].dataset.value)
cards is an array housing all of my div classes. One of the errors I received told my that indexOf is not a usable function here. I think this may be because the div classes are not actually part of an array and I need a different method to find the 'value of' something rather than the 'index of'.
For example, if a row of cards goes 2D,7S,9D,8H,3D, the user can click 3D for firstCard and 7S for secondCard. The function will verify that the value of 3D is one greater than 2D and will swap the positions of 3D and 7S.
I made my little snippet with auto-generating squares that can be moved as you suggested. You might check it out. If something is not okay, you might point it out, maybe I forgot something.
let cardsArray = [{
"value": 7,
"color": "red"
}, {
"value": 6,
"color": "yellow"
}, {
"value": 5,
"color": "green"
}, {
"value": 4,
"color": "red"
}, {
"value": 3,
"color": "yellow"
}, {
"value": 2,
"color": "green"
}, {
"value": 1,
"color": "green"
}]
// Function to automatically generate some squares
function drawSquares() {
let squaresParent = $("#squaresParent");
squaresParent.empty();
let lCALen = cardsArray.length;
for (let i = 0; i < lCALen; ++i) { //make it efficient for checking length
// Creating a text object for display purposes only
let newText = $('<p>').append(cardsArray[i].value);
// Creating a new card
let newCard = $('<div>').attr({
'data-value': cardsArray[i].value,
'style': 'background-color:' + cardsArray[i].color,
'index': i,
'class': 'card'
}).append(newText);
squaresParent.append(newCard);
}
}
drawSquares();
assignEvents();
let firstCard = null;
let secondCard = null;
function compareAndMove(fC, sC) {
//console.log("Switching...");
let firstCardIndex = fC.attr("index");
let secondCardIndex = sC.attr("index");
let beforeSecondCardIndex = 0;
if (secondCardIndex > 0)
beforeSecondCardIndex = secondCardIndex - 1;
let firstCard = cardsArray[firstCardIndex];
let secondCard = cardsArray[secondCardIndex];
let beforeSecond = cardsArray[beforeSecondCardIndex];
if (firstCard.value - 1 === beforeSecond.value) {
let temp = firstCard;
cardsArray[firstCardIndex] = secondCard;
cardsArray[secondCardIndex] = temp;
drawSquares();
assignEvents();
} else {
fC.removeClass("selected");
sC.removeClass("selected");
}
//console.log("Exiting...");
}
function assignEvents() {
$(".card").click(function() {
//console.log("Card clicked");
if (!firstCard) {
firstCard = $(this);
firstCard.addClass("selected");
return;
}
if (!secondCard) {
secondCard = $(this);
secondCard.addClass("selected");
}
compareAndMove(firstCard, secondCard);
firstCard = null;
secondCard = null;
});
}
.card {
width: 50px;
height: 50px;
border-radius: 10px;
border: 1px solid black;
display: inline-block;
text-align: center;
margin: 5px;
}
.selected {
border: 3px solid black;
}
body {
background-color: #ABCDEF;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<body>
<div id="squaresParent"></div>
</body>
</html>
I think you are looking for something like this..
var currentCard = $(".cards").first();
var currentCardInd = $(currentCard).attr("index");
$(".cards").on("click", function(event){
if(currentCard && parseInt(currentCardInd) < parseInt($(this).attr("index"))){
currentCard.attr("index", $(this).attr("index"));
$(this).attr("index", currentCardInd.toString());
$(this).swapWith(currentCard);
currentCard = $(".cards[index='"+$(this).attr("index")+"']").next();
currentCardInd = $(currentCard).attr("index");
}
});
jQuery.fn.swapWith = function(_with) {
return this.each(function() {
var copyWith = $(_with).clone(true);
var copyOrig = $(this).clone(true);
$(_with).replaceWith(copyOrig);
$(this).replaceWith(copyWith);
});
};
.cards{
padding:10px;
background-color:lightblue;
display:inline-block;
cursor:pointer;
border-radius:5px;
}
#card-7S{
background-color:lightgreen;
}
#card-9D{
background-color:lightyellow;
}
#card-8H{
background-color:lightgray;
}
#card-3D{
background-color:lightpink;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="main">
<div id="card-2d" class="cards" index="2">2D</div>
<div id="card-7S" class="cards" index="4">7S</div>
<div id="card-9D" class="cards" index="7">9D</div>
<div id="card-8H" class="cards" index="10">8H</div>
<div id="card-3D" class="cards" index="15">3D</div>
</div>
You can also check here..
https://jsfiddle.net/nimittshah/7z3y8mb5/
Array.from is the canonical way to get an array of elements
<div class="card" data-value="38">
<img class="front-face" src="PNG/QH.png"/>
</div>
<div class="card" data-value="39">
<img class="front-face" src="PNG/KH.png"/>
</div>
<script>
const [first, second] = Array.from(document.querySelectorAll('.card'))
if (first.dataset.value === second.dataset.value) {
/* do your thing... */
}
</script>
The problem with the code is that querySelectorAll() returns a NodeList which behave as an array but it is not really an array.
The same problem happens with getElementsByClassName() this one return an HTMLCollection which behave as an array but does not include all the prototypes functionalities since it is not really an array.
So you have several options to get the index, after getting the index you can get the element that is before it.
Use call or apply to trick the Array methods
`Array.prototype.indexOf.call(cards, second)`
Iterate manually
`let selectedIndex = 0;
for(let i =0; i < cards.length; i++){
if(cards[i] === second){
selectedIndex = i;
}
};
console.log(selectedIndex)`
Create an array using cards NodeList
const cards = document.querySelectorAll('.card');
const cardsArray = Array.from(cards)
if(firstCard.dataset.value ===
cardsArray[cardsArray.indexOf(secondCard)-1].dataset.value)
Use any library to select the element that returns an array, as jQuery
I've created this little example on JSFiddle (http://jsfiddle.net/4UvUv/198/) which allows you to click 3 button and when you click a button, it pushes a value to an array called 'selected'. So lets say I click the 'Cats' button, it will push the value 'cats' to the selected array.
But what I don't know is how to remove 'Cats' from the array if the 'Cats' button was clicked again. Would someone know how to do this using my example? Or if theres a better way?
How I push to the Selected array:
var selected = []
$("#cats").click(function(e) {
console.log("Cats");
var value = 'cats';
selected.push(value);
})
You can try something like this:
$("#cats").click(function(e) {
console.log("Cats");
var value = 'cats';
var index = selected.indexOf(value);
if (index === -1) {
selected.push(value);
} else {
selected.splice(index, 1);
}
});
It can be optimized I think
A much simpler way of achieving this is to only toggle a class on the button elements when you click them. You can then only generate the array when the #results button is clicked. This way you don't need to worry about maintaining the array when items are added/removed. Try this:
$(".button").click(function(e) {
$(this).toggleClass('selected');
});
$("#result").click(function(e) {
var selected = $('.button.selected').map(function() {
return this.id;
}).get();
console.log(selected);
})
.selected {
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button class="button" id="cats">Cats</button>
<button class="button" id="dogs">Dogs</button>
<button class="button" id="rabbits">Rabbits</button>
<br />
<button id="result">Result</button>
$("#dogs").click(function(e) {
var index = selected.indexOf("Dogs");
if(index == -1){
console.log("Dogs");
var value = 'Dogs';
selected.push(value);
}else{
selected.splice(index,1);
}
})
try something like this:
var selected = [];
var i = 0;
$("#cats").click(function(e) {
if(i == 0){
console.log("Cats");
var value = 'cats';
selected.push(value);
i++} else {
var check = selected.indexOf('cats');
if(check !== -1){
selected.splice(check, 1);
}
i--;
}
});
Check this solution. you can use indexOf function to know whether the item already exists in the array or not.
var selected = []
$('.buttons').click(function(e) {
var value = $(this).text();
addOrRemove(value);
});
$("#result").click(function(e) {
console.clear();
console.log("results: ", selected);
});
function addOrRemove(item) {
console.clear();
console.log(item);
var index = selected.indexOf(item);
if (index === -1) {
selected.push(item);
} else {
selected.splice(index, 1);
}
}
button {
font-size: 16px;
padding: 10px;
min-width: 100px;
margin: 5px;
background-color: #87CEEB;
border: 1px solid #E6E6E6;
color: white;
}
button:hover {
background-color: #67BCDE;
}
button:focus {
outline: none;
background-color: #3AB2E2;
}
div button {
margin-top: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.2.3/jquery.min.js"></script>
<button id="cats" class="buttons">Cats</button>
<button id="dogs" class="buttons">Dogs</button>
<button id="rabbits" class="buttons">Rabbits</button>
<div>
<button id="result">Result</button>
</div>