I am trying to recreate the WoW talent calculator as seen here - https://classicdb.ch/?talent#h
Project files - https://codepen.io/jjchrisdiehl/pen/gNQLgR
This is a project to help better understand Javascript, so please avoid any jQuery workarounds for this - thanks.
If you look at the HTML code, you'll see I have the HTML set up as div's with the class 'item' and then I have another div nested inside of the 'item' div with the class 'points'.
<div class="item two nature_wispsplode button" data-max="5">
<div class="points"></div>
</div>
<div class="item three fire_fireball button" data-max="3">
<div class="points"></div>
</div>
The idea is to append a Javascript event listener called logMouseButton to every div with the class 'item'. This will listen for a click and log whether it was a left or right mouse click.
/* Get item div element for addEventListener*/
let itemButton = document.getElementsByClassName("item");
/* Apply logMouseButton to every itemButton */
for (var i = 0; i < itemButton.length; i++) {
itemButton[i].addEventListener("mouseup", logMouseButton, false);
}
Now the logMouseButton code was hacked from the Moz page on MouseEvents .button. My thoughts are to use a switch to manage adding or subtracting points to each item's individual counters.
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
/* Set Counter */
var counter = 0;
/* Add or subtract points based on button clicked */
function logMouseButton(e) {
/* Set the max number of clicks in points counter based off of data-max attribute */
var maxPoints = this.getAttribute("data-max");
if (typeof e === "object") {
switch (e.button) {
case 0:
if (counter == 0 || counter < maxPoints) {
counter = counter + 1;
}
document.querySelector(".item .points").innerHTML = counter;
// alert(counter);
break;
case 1:
log.textContent = "Middle button clicked.";
break;
case 2:
if (counter > 0) {
counter = counter - 1;
}
document.querySelector(".item .points").innerHTML = counter;
break;
default:
log.textContent = `Unknown button code: ${btnCode}`;
}
}
}
Left click increments, right click decrements. As you can see in my codepen project, the right/left clicks work, but only for one item.
My question is - how would I apply this to each item individually so that they manage their own counter independently of the other items?
Thanks!
Note - I managed to get the counters working with some direction from klugjo. I ended up logging the HTML value of 'points', incrementing the value, then adding the new value back to the innerHTML: https://codepen.io/jjchrisdiehl/pen/JgXzKe
If you have any insights as to why this isnt the best way to do it, or why another way is better, let me know!
You need to access the div that corresponds to the element that was clicked.
Using document.querySelector(".item .points") will always select the first element in the DOM.
You can use e.target to access the element that was clicked, and since what you need is the only child of that element, you can replace
document.querySelector(".item .points").innerHTML = counter;
with
e.target.children[0].innerHTML = counter;
Then you will run into another issue, which is that your counter is global and common to all the buttons.
So you will have to use a hashmap (JS Object) instead of a single integer for counter
var counter = {};
An approach is to select the .item elements and create an array of .item length (number of .item elements in the page) to store the counter for each one individually.
Here's a demo, it contains some helpful comments :
/** selecting the elements with ".item" class and declaring an array to store each element counter separately **/
const items = document.querySelectorAll('.item'),
countArr = new Array(items.length);
/** loop through the elements and add "mouseup" listener (to ensure catching both left and right clicks) for each element **/
/**
* el: the current element from the ".item" elements collection
* i: the index of that elemnt in the collection
**/
items.forEach((el, i) => {
countArr[i] = 0; /** initialize each array element with 0 so we can count later (new Array puts "undefined" as the array elements values) **/
/** add the "mouseup" listener **/
el.addEventListener('mouseup', e => {
let txtCount = el.querySelector('.count'); /** selecting the "span" that contains the counter from the current elemnt (don't forget that we're looping in ".item" elements collection) **/
if(e.which === 1) countArr[i]++; /** left click **/
else if(e.which === 3) countArr[i]--; /** right click **/
txtCount.textContent = countArr[i]; /** update the "span" wih the calculated counter as left click adds 1 and right click removes 1 from the counter of each element **/
});
});
/** basic styling for the demo **/
.item {
display: inline-block;
margin: 15px 0;
padding: 8px;
border: 2px solid teal;
user-select: none;
}
.item .count {
display: inline-block;
background-color: #181818;
color: #fff;
font-size: 1.2rem;
font-weight: bold;
padding: 8px;
border-radius: 4px;
}
<div class="item">counter = <span class="count">0</span></div>
<div class="item">counter = <span class="count">0</span></div>
<div class="item">counter = <span class="count">0</span></div>
Related
I am trying to create somewhat of a tabSelect function in a card game where the player will be able to select 1 of the 5 cards in their hand. I made a highlight class and am adding it to a div containing the card image so the outline changes from blue to light blue. So in the loop the class should be added to the first div, then the second div while removing it from the first div, then the third div while removing it from the second div....etc.
this is what i have tried so far but its not working as intended :
selectCard() {
const $playerOneHand = $('.pOne')
for (i = 0; i < $playerOneHand.length; i++) {
if ($($playerOneHand[i - 1]).hasClass('highlight') == true) {
$($playerOneHand[i - 1]).removeClass('highlight')
}
if ($($playerOneHand[i]).hasClass('highlight') == false) {
$($playerOneHand[i]).addClass('highlight')
return
}
}
}
Get the index of the card that's currently highlighted. Increment that index to get the one to highlight in its place.
function selectCard() {
var index = $(".pOne.highlight").index(".pOne");
index = (index + 1) % $(".pOne").length; // increment and wrap around
$(".pOne").removeClass("highlight");
$(".pOne").eq(index).addClass("highlight");
}
$("#next").click(selectCard);
.pOne {
height: 30px;
width: 30px;
background-color: yellow;
}
.pOne.highlight {
background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="pOne highlight">1</div>
<div class="pOne">2</div>
<div class="pOne">3</div>
<div class="pOne">4</div>
<div class="pOne">5</div>
<button id="next">Next Card</button>
I want to replace a specific div element with a different one, when it has reached 3 clicks on it. That is the only task, I am trying to accomplish with the code.
I have tried looking at some code that does this but all of them replace it with get go, they don't give you a number amount to specify when to replace it with.
Example: <div id="1"></div> has been clicked on 3 times by a user. Once it exceeds that amount replace it with <div id="3"></div>
Changing the id attribute is not a good idea, instead you can use data- attribute like the following way:
var count = 0; // Declare a variable as counter
$('#1').click(function(){
count++; // Increment the couter by 1 in each click
if(count == 3) // Check the counter
$(this).data('id', '3'); // Set the data attribute
console.log($(this).data('id'));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="1" data-id="1">Click</div>
You could write a JavaScript function that keeps track how often you clicked on a specific DOM element (i. e. the div element with id="1"). As soon as the element was clicked three times, it will be replaced by another DOM element which can be created in JavaScript as well.
var clicks = 0;
function trackClick(el) {
clicks++;
if(clicks === 3) {
var newEl = document.createElement('div');
newEl.textContent = 'Div3';
newEl.id = '3';
el.parentNode.replaceChild(newEl, el);
}
}
<div id="1" onclick="trackClick(this)">Div1</div>
In case you should use a library like jQuery or have another HTML structure, please specify your question to improve this code snippet so that it fits for your purpose.
The main idea is to start listening click events on the first div and count them.
The below code shows this concept. Firstly we put first div into variable to be able to create event listeners on it and also create count variable with initial value: 0. Then pre-make the second div, which will replace the first one later.
And the last part is also obvious: put event listener on a div1 which will increment count and check if it is equal 3 each time click happens.
const div1 = document.querySelector('#id-1');
let count = 0;
// pre-made second div for future replacement
const divToReplace = document.createElement('div');
divToReplace.id = 'id-2';
divToReplace.innerText = 'div 2';
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
div1.parentNode.replaceChild(divToReplace, div1);
}
});
<div id="id-1"> div 1 </div>
Note that this approach is easy to understand, but the code itself is not the best, especially if you will need to reuse that logic. The below example is a bit more complicated - we create a function which takes 2 arguments: one for element to track and another - the element to replace with. Such approach will allow us to reuse functionality if needed.
function replaceAfter3Clicks(elem, newElem) {
let count = 0;
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
elem.parentNode.replaceChild(newElem, elem);
}
});
}
const div1 = document.querySelector('#id-1');
// pre-made second div for future replacement
const div2 = document.createElement('div');
div2.id = 'id-2';
div2.innerText = 'div 2';
replaceAfter3Clicks(div1, div2);
<div id="id-1"> div 1 </div>
If you know, how to use JQuery, just put a click event handler on your div 1. On that handler, increment a click counter to 3. If it reaches 3, replace the div with JQuery again.
If there are multiple divs to replace, use an array of counters instead of a single one, or modify a user-specific data attribute via JQuery.
Using native JavaScript, rather than relying upon library (for all the benefits that might offer), the following approach is possible:
// A named function to handle the 'click' event on the relevant elements;
// the EventObject is passed in, automatically, from EventTarget.addEventListener():
const replaceOn = (event) => {
// caching the element that was clicked (because I'm using an Arrow function
// syntax we can't use 'this' to get the clicked element):
let el = event.target,
// creating a new <div> element:
newNode = document.createElement('div'),
// retrieving the current number of clicks set on the element, after this
// number becomes zero we replace the element. Here we use parseInt() to
// convert the string representation of the number into a base-10 number:
current = parseInt(el.dataset.replaceOn, 10);
// here we update the current number with the decremented number (we use the
// '--' operator to reduce the number by one) and then we update the
// data-replace-on attribute value with the new number:
el.dataset.replaceOn = --current;
// here we discover if that number is now zero:
if (current === 0) {
// if so, we write some content to the created <div> element:
newNode.textContent = "Original element has been replaced.";
// and here we use Element.replaceWith() to replace the current
// 'el' element with the new newNode element:
el.replaceWith(newNode);
}
};
// here we use the [data-replace-on] attribute-selector to search
// through the document for all elements with that attribute, and
// use NodeList.forEach() to iterate over that NodeList:
document.querySelectorAll('[data-replace-on]').forEach(
// using an Arrow function we pass a reference to the current
// Node of the NodeList to the function, and here we use
// EventTarget.addEventListener() to bind the replaceOn function
// (note the deliberate lack of parentheses) to handle the
// 'click' event:
(element) => element.addEventListener('click', replaceOn)
);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
div {
border: 1px solid #000;
display: inline-block;
padding: 0.5em;
border-radius: 1em;
}
div[data-replace-on] {
cursor: pointer;
}
div[data-replace-on]::before {
content: attr(data-replace-on);
}
<div data-replace-on="3"></div>
<div data-replace-on="13"></div>
<div data-replace-on="1"></div>
<div data-replace-on="21"></div>
<div data-replace-on="1"></div>
<div data-replace-on="6"></div>
<div data-replace-on="4"></div>
References:
CSS:
Attribute-selectors ([attribute=attribute-value]).
JavaScript:
Arrow function syntax.
ChildNode.replaceWith().
document.querySelectorAll().
EventTarget.addEventListener().
NodeList.prototype.forEach().
I am struggle to manage correctly the element with the text node "None".
The main issue of this code is the initial element with the text node "None" is not being removed after I add new Items. The expected behavior is load the page with this "None" element. When user add at least 1 item, then removed. If the list is empty, then appears "None" again.
What is the best way to fix it?
UPDATE:
Now the snippet of code is running properly. Just for better clarification: if you run this code, it works fine at first. But when I clean all the items, then "None" still appears above the other items.
let input = document.querySelector("#userInput"),
button = document.querySelector("#buttonInput"),
ul = document.querySelector("ul"),
allLi = document.querySelectorAll("ul li");
function inputLength() {
return input.value.length;
}
function insertMessageIfListEmpty() {
if (typeof ul.children[0] === "undefined") {
var li = document.createElement("li");
li.appendChild(document.createTextNode("None"));
ul.appendChild(li);
}
}
function createListElement() {
let li = document.createElement("li");
li.appendChild(document.createTextNode(input.value));
ul.appendChild(li);
input.value = "";
createDeleteButtonIcon(li);
if (allLi[0].innerHTML === "None") {
allLi[0].remove();
}
}
function createDeleteButtonIcon(item) {
let i = document.createElement("i"),
span = document.createElement("span");
i.innerHTML = "×";
i.classList.add("iconX");
span.appendChild(i);
item.appendChild(span);
addEventDeleteParent(item);
}
function deleteNodeOnClick(e) {
if (e.target.tagName === "I") {
e.target.parentNode.parentNode.remove();
}
insertMessageIfListEmpty();
}
function addListAfterClick() {
if(inputLength() > 0) {
createListElement();
}
}
function addListAfterKeyDown(event) {
if(inputLength() > 0 && event.which === 13) { //13 charCode: ENTER
createListElement();
}
}
function addEventDeleteParent(elem) {
elem.addEventListener("click", deleteNodeOnClick);
}
input.addEventListener("keydown", addListAfterKeyDown);
button.addEventListener("click", addListAfterClick);
.iconX {
font: normal .7em arial;
margin-left: 10px;
padding: 0 12px;
border-radius: 4px;
background-color: #ff0030;
color: #fff;
cursor: pointer;
}
<h1>TODO LIST - IMPORTANT TASKS</h1>
<input id="userInput" type="text" placeholder="Insert Item">
<button id="buttonInput">Add</button>
<ul>
<li>None</li>
</ul>
Why having these much functions when the functionality can be done with only two :
addItem(itemText, addDelBtn): a function that adds an item to the to-do list. It accepts two arguments :
itemText: is the text to be shown in the item that is going to be added. Mainly this argument is used on page load in order to add the item with the text None and when the to-do list becomes empty once again.
addDelBtn: a flag that is used to see if a delete button should be added or not (in the item). Mainly, this arguments equals true when a normal item is added (after pressing the add item button) and it equals false when adding the item with None text.
deleteItem(e): a function that deletes an item from the to-do list.
e: the click event that is used to get the parent of the delete button that was pressed in order to delete it from the to-do list.
The main functionality :
an item is added based on the input's text if that text isn't empty (after trimming the text).
the items that are added after writing in the input and pressing the add item button have a button that is used to delete that item from the to-do list.
if the to-do list is/becomes empty after deleting items or page load, an item with None text is automatically added. This item doesn't have a delete button and it'll be delete when a new item is added after pressing the add item button.
if the to-do list is empty and a new item is going to be added, the None item will automatically be deleted and that new item becomes the first item in the to-do list.
items are added at any time with the same logic described above.
Another point, as we have to dynamically create elements (the delete buttons and the lis), I created a function that create an element based on its tag name (li, button...).
So, here's a demo, it contains a wealth of helpful comments that may assist you while reading the code.
/**
* #const input the "input" on which an item is created based on its value.
* #const button the button that adds a new item.
* #const ul the to-do list.
* #const createElement(tagName, options) a function that create an element and return it.
#param tagName the element's tag name.
#param options an object that holds the attributes and/or the events for that element.
* #const deleteItem(e) a function that deletes an element from the to-do list.
#param e the event (mainly the click event).
* #const addItem(itemText, addDelBtn) a function that adds a new item to the to-do list.
#param itemText the text for that item (if not specified the input's value is used)
#param addDelBtn a boolean flag that is used to see wether a delete button should be added to the new item or not.
**/
const input = document.getElementById("userInput"),
button = document.getElementById("buttonInput"),
ul = document.getElementById("toDoList"),
createElement = (tagName, options) => {
/** creates a new element based on the tagName parameter **/
const el = document.createElement(tagName);
/** where in the options object (the second parameter) we should search (it may contao "attributes" and/or "events") **/
let field = "attributes";
/** if we have attributes to be added to the new element **/
if (options["attributes"])
for (let i in options[field])
/** apply the attributes **/
options[field].hasOwnProperty(i) && (el[i] = options[field][i]);
/** if we have events to be attached to the new element also the "field" value becomes "events" if so **/
if (options["events"] && (field = "events"))
for (let i in options[field])
/** attach the events **/
options[field].hasOwnProperty(i) &&
el.addEventListener(i, options[field][i]);
/** return the newly created element **/
return el;
},
deleteItem = e => {
/** remove the item where the delete button was pressed based on the "target" attribute of the "event" (e) object **/
ul.removeChild(e.target.parentNode);
/** if the to-do list becomes empty add an item with the text "None" and without a delete button (see the arguments passed "None" and false) **/
!ul.firstChild && addItem("None", false);
},
addItem = (itemText, addDelBtn) => {
/** create new "li" using createElement function **/
const li = createElement("li", {
attributes: {
textContent: input.value || itemText
}
});
/** if the first item in the to-do list is the one that has the "None" text delete it **/
ul.firstChild && ul.firstChild.textContent.toUpperCase() === "NONE" && ul.removeChild(ul.firstChild);
/** if the input has a value remove that value (the text in it) **/
input.value && (input.value = "");
/** if the "addDelBtn" is true then a button that deletes an item is added to the created "li" **/
addDelBtn &&
li.appendChild(
createElement("button", {
attributes: {
type: "button",
innerHTML: "×", /** × is the entity for X sign**/
className: "iconX"
},
events: {
click: deleteItem
}
})
);
/** add that "li" to the to-do list **/
ul.appendChild(li);
};
/** attach click event to the add button **/
button.addEventListener("click", () => input.value.trim().length && addItem.call(null, false, true));
/** on page load add a "None" item **/
!ul.firstChild && addItem("None", false);
.iconX {
width: 20px;
height: 20px;
margin: 0 4px;
border: 0;
border-radius: 20px;
font-weight: bold;
line-height: 20px;
color: #f00;
background-color: #c0c0c0;
cursor: pointer;
transition: all .4s 0s ease;
}
.iconX:hover {
color: #fff;
background-color: #f00;
}
<h1>TODO LIST - IMPORTANT TASKS</h1>
<input id="userInput" type="text" placeholder="Insert Item">
<button id="buttonInput">Add</button>
<!-- the "ul" tag has an "ID" of "toDoList" to simply select it in the "JavaScipt" part -->
<!-- also it is initially empty and "JavaScript" will add an item with "None" text when the page loads -->
<ul id="toDoList"></ul>
for future questions please limit the shared code to the relevant portions only.
I hope the following is what you are after:
/**
* [someFunction description]
* #param ul [the target ul]
* #param li [the new li element]
* #return ul
*/
function populateUl(ul, li) {
//check if li is only holding the 'none' li
let ulContent = ul.getElementsByTagName("li")
if (ulContent.length == 1 && ulContent[0].textcontent) {
ul.innerHTML = "";
}
ul.appendChild(li);
return ul;
}
This is an assignment for school. One part of the assignment is to double click on a div that has been created inside of a class. When you double click on it, it should get deleted.
The html has a button that creates the div as an instance of the class. That worked fine. Then I put a double click event listener on the div's class and .remove() on a class method (see code).
But when double clicked, it deletes the div and all the divs after it. I believe this is happening because all the divs have the same class name.
I could assign ids to each div when they're created and delete that specific div. I'm just wondering if there is another way to grab the div that's clicked.
I also tried using 'this' to refer to the div clicked. However, I need 'this' to connect to the method 'this.removeOne' so I used an arrow function.
Basics of the code below. All the details here: https://jsfiddle.net/ChristyCakes/8w0htt1s/
Bottom Line: I want to double click on a div and remove only that one div. Is giving it an id the only way?
This question is very similar to other ones on stack, but I'm having trouble applying the answers to this particular situation - or I need clearer explanation.
// create a class called Die
class Die {
constructor(value) {
// set value property
this.value = value;
// create a div for each new object, assign class
this.div = $('<div></div>').attr("class", "div");
$(this.div).append(this.value);
$('.container').append(this.div);
// when Roll All Dice button is clicked, run the rollAll method (below)
$('#all').click(() => this.rollAll());
// when a div is clicked, run rollOne function below
$('.div').click(() => this.rollOne());
// when any div is double clicked, run removeOne function below
$('.div').dblclick(() => this.removeOne());
}
// roll all dice again; get random number, replace value in div
rollAll() {
this.value = Math.floor(Math.random() * 6 + 1);
$(this.div).html(this.value);
}
// roll the clicked dice again
rollOne() {
this.value = Math.floor(Math.random() * 6 + 1);
$(this.div).html(this.value);
}
//remove die that was clicked from display and from sumable dice
removeOne() {
$(this.div).remove();
}
}
// when Add All Dice button is clicked, add value (text) in each div, display sum as alert
$('#sum').click(function() {
let sum = 0;
$('.div').each(function() {
sum += parseFloat($(this).text()); //parseFloat turns strings into sumable numbers
})
alert(sum);
})
// Roll 1 Die button generates random number, creates an instance of Die class
$('#btn').click(function roll() {
let val = Math.floor(Math.random() * 6 + 1);
let die = new Die(val);
})
.div {
border: 1rem solid black;
height: 20rem;
width: 20%;
float: left;
display: flex;
align-items: center;
justify-content: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1>Dicey Business</h1>
<div class="container">
<div>
<button id="btn">Roll 1 Die</button>
<button id="all">Roll All Dice</button>
<button id="sum">Add All Dice</button>
</div>
</div>
You should be able to pass the element that is double-clicked to removeOne by getting it from the event object that is given to all event handlers:
$('.div').dblclick((event) => this.removeOne(event.target));
Then in removeOne you can do:
removeOne(elem) {
$(elem).remove();
}
Currently, each instantiation is attaching a remove-this-div-on-click listener to all .divs:
$('.div').click(() => this.removeOne());
So, when any .div is clicked, every listener triggers, and all are removed. Instead, in the constructor, only attach the listener to the newly created .div:
$(this.div).dblclick(() => this.removeOne());
// create a class called Die
class Die {
constructor(value) {
// set value property
this.value = value;
// create a div for each new object, assign class
this.div = $('<div></div>').attr("class", "div");
$(this.div).append(this.value);
$('.container').append(this.div);
// when Roll All Dice button is clicked, run the rollAll method (below)
$('#all').click(() => this.rollAll());
// when a div is clicked, run rollOne function below
$(this.div).click(() => this.rollOne());
// when any div is double clicked, run removeOne function below
$(this.div).dblclick(() => this.removeOne());
}
// roll all dice again; get random number, replace value in div
rollAll() {
this.value = Math.floor(Math.random() * 6 + 1);
$(this.div).html(this.value);
}
// roll the clicked dice again
rollOne() {
this.value = Math.floor(Math.random() * 6 + 1);
$(this.div).html(this.value);
}
//remove die that was clicked from display and from sumable dice
removeOne() {
$(this.div).remove();
}
}
// when Add All Dice button is clicked, add value (text) in each div, display sum as alert
$('#sum').click(function() {
let sum = 0;
$('.div').each(function() {
sum += parseFloat($(this).text()); //parseFloat turns strings into sumable numbers
})
alert(sum);
})
// Roll 1 Die button generates random number, creates an instance of Die class
$('#btn').click(function roll() {
let val = Math.floor(Math.random() * 6 + 1);
let die = new Die(val);
})
.div {
border: 1rem solid black;
height: 20rem;
width: 20%;
float: left;
display: flex;
align-items: center;
justify-content: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1>Dicey Business</h1>
<div class="container">
<div>
<button id="btn">Roll 1 Die</button>
<button id="all">Roll All Dice</button>
<button id="sum">Add All Dice</button>
</div>
</div>
You also might consider naming the dice divs a bit more informatively - a class name of div sounds like a very easy source of confusion. (Maybe .die instead, for example?)
Please take a look at this basic example:
http://tympanus.net/Blueprints/ResponsiveFullWidthGrid/
Now imagine that by clicking a cat box, I would need (especially on small to medium screens) to add a 100%-width text box (say a description of the clicked cat) below the clicked cat's row. That text box should push down the rest of the rows.
I am full css/js/frontend developer but I never faced a problem like this. It's also the first time I'm going to use a flexbox layout. With a fixed layout would be quite trivial, but in this case I cannot figure out a good way of doing it. One of the things to solve for example is: where should I put the box (relative to the clicked box?), and should I change position via javascript based on current items-per-row or maybe there a smarter css way?
Any idea is appreciated.
That was an interesting challenge :)
The only way to know where to place the expanded area (I called it infoBox), is to identify the 1st node of the next line, and then insert it before it. If there is no node on the last line, we can append it to the end of the ul.
I've also added a window.resize event handler that will close the infoBox, so it won't break the responsive layout, and a close button.
Working example - fiddle.
HTML was copy paste from the codrop article.
JS
var rfgrid = document.querySelector('.cbp-rfgrid');
var items = Array.from(document.querySelectorAll('.cbp-rfgrid > li'));
/** Create infoBox **/
var infoBox = document.createElement('div');
infoBox.classList.add('infoBox');
infoBox.innerHTML = '<div class="close">X</div><div class="content"></div>';
infoBoxClose = infoBox.querySelector('.close');
infoBoxContent = infoBox.querySelector('.content');
/** add close button functionality **/
infoBoxClose.addEventListener('click', function() {
rfgrid.removeChild(infoBox);
});
/** remove infoBox on resize to maintain layout flow **/
window.addEventListener('resize', function() {
rfgrid.removeChild(infoBox);
});
items.forEach(function (item) {
item.addEventListener('click', function (event) {
event.preventDefault();
var insertReference = findReference(this); // get refence to next line 1st node
infoBoxContent.innerHTML = items.indexOf(this); // example of changing infoBox content
if(insertReference) {
rfgrid.insertBefore(infoBox, insertReference); // insert infoBox before the reference
} else {
rfgrid.appendChild(infoBox); // insert infoBox as last child
};
});
});
/** find reference to 1st item of next line or null if last line **/
function findReference(currentNode) {
var originalTop = currentNode.offsetTop; // get the clicked item offsetTop
do {
currentNode = currentNode.nextSibling; // get next sibling
} while (currentNode !== null && (currentNode.nodeType !== 1 || currentNode.offsetTop === originalTop)); // keep iterating until null (last line) or a node with a different offsetTop (next line)
return currentNode;
}
CSS (in addition to the original)
.infoBox {
position: relative;
width: 100%;
height: 200px;
padding: 20px 0 0 0;
clear: both;
background: paleturquoise;
}
.infoBox > .close {
position: absolute;
top: 5px;
right: 5px;
cursor: pointer;
}