I am teaching myself JS. I am able to move a piece to a new location but for the 2nd piece the chess piece disappears. It seems that the addEventListener is going into a loop and I am not understanding why. Just need to understand what concept am I missing here:
My code below:
chess.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chess board</title>
<style>
.cell {
height: 30px;
width: 30px;
border: 1.5px solid grey;
border-style: inset;
}
.greencell {
background-color: #AFE1AF;
}
.darkgreencell {
background-color: #097969;
}
.redcell {
background-color: red;
}
</style>
</head>
<body>
<script src="js/chess.js" defer></script>
</body>
</html>
js/chess.js
function movep(cbp,cbt,i,j) {
//Creating rules for pawn
console.log('P');
//console.log(cbp);
//refresh_colors(cbp,cbt);
if ((i>=0) & (i<=7)) {
if(cbp[i+1][j].length<2) {
//Based on player 1, if the location below the pawn is open then cbt masks that as 1
cbt[i+1][j]=1;
}
}
potential_moves(cbp,cbt,[i,j]);
update_board(cbp,cbt);
}
var possiblelocs=function(event,i,j,cbp,cbt) {
//based on string value of cbp (the chess piece of interest)
//I have to create rules for possible ways it can go
if (cbp[i][j].includes('P') ) {movep(cbp,cbt,i,j);}
//else if (cbp[i][j].includes('K')) {console.log('K');}
else if (cbp[i][j].includes('N')) {movep(cbp,cbt,i,j);}//using the function for pawn here for debugging purposes
//else if (cbp[i][j].includes('Q')) {console.log('Q');}
else if (cbp[i][j].includes('R')) {movep(cbp,cbt,i,j);}//using the function for pawn here for debugging purposes
//else if (cbp[i][j].includes('B')) {console.log('B');}
//console.log(cbp);
}
function update_board(cbp,cbt) {
//fills the board with all the proper pieces
var elem = document.getElementsByTagName('td');
//console.log(cbp);
for(var i=0;i<8;i++) {
for(var j=0;j<8;j++) {
elem[8*i+j].innerHTML=cbp[i][j];
if (elem[8*i+j].innerHTML.length > 1) {
//create a clickable EventListener if there is a string value >1 (i.e. not-empty)
elem[8*i+j].addEventListener( "click",possiblelocs.bind(event,'str',i,j,cbp,cbt) );
}
}
}
}
var movelocs=function(event,i,j,cbp,cbt,pc) {
//replace old location of string in cbp to the new one
cbp[i][j]=cbp[pc[0]][pc[1]];
cbp[pc[0]][pc[1]]='';
update_board(cbp,cbt);
}
function potential_moves(cbp,cbt,pc) {
//updates the board with possible location a specific piece can move (based on cbt)
//and makes the red cells clickable
var elem = document.getElementsByTagName('td');
for(var i=0;i<8;i++) {
for(var j=0;j<8;j++) {
if (cbt[i][j]==1) {
elem[8*i+j].setAttribute('class', 'cell redcell');
//once click move the board to the new location
elem[8*i+j].addEventListener( "click",movelocs.bind(event,'str',i,j,cbp,cbt,pc) );
}
}
}
}
I have tried to root cause it but I am unable to root cause even more.
Here is the behavior i see:
When the board starts up:
After I click 'R11' I see a red square show up down below to see where it can go as show below:
Once that is completed I see R11 pieces moves down 1 and I also click on N11 to see my available options (I know the rules are not right for those pieces). I then see the following image:
At this point I click on the red square below N11 and I see that N11 has completely gone.
My best understanding is that movelocs goes into a loop and deletes the piece I just moved. I am not sure why it does that.
If someone has any advice on how to debug this that would helpful as well.
I just went fast through your code and I see potential problem.
In update_board you add event listener to all fields. And in the event handler function possiblelocs you call movep in this function you call update_board again. This set another event listener.
So each time you click on field you basically double the number of listeners.
Related
I am trying to write a tutorial for my students, in the form of a webpage with hidden "spoilers" that the student can unhide, presumably after thinking about the answer. So, long story short, the behavior I am looking for is:
in the beginning, the text appears with a lot of hidden words;
when a piece of text is clicked, it appears, and stays uncovered afterwards;
this should work with minimal overhead (not forcing me to install a complex framework) and on all my students' machines, even if the browser is outdated, even if jquery is not installed.
I searched for off the shelf solutions, but all those I checked were either too complicated or not doing exactly what I wanted. So I decided to do my own.
What I have so far is this:
<HTML>
<STYLE>
span.spoil {background-color: black;}
span.spoiled {background-color: white;}
</STYLE>
<HEAD>
<TITLE>SPOIL</TITLE>
<META http-equiv="Content-Type" content="text/html;charset=UTF-8">
<!--LINK rel="Stylesheet" type="text/css" href=".css"-->
</HEAD>
<BODY>
This is a text with <span class="spoil" onclick="showspoil(this)">spoil data</span>.
<br>
<span class="spoil" onclick="showspoil(this)">Unspoil me.</span>
<br>
<span class="spoil" onclick="showspoil(this)">And me.</span>
<script>
function showspoil(e) {
e.className="spoiled";
}
// var classname = document.getElementsByClassName("spoil");
// for (var i = 0; i < classname.length; i++) {
// classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
// }
</script>
</BODY>
</HTML>
It does the job, except that I find it annoying to have to write explicitly the "onclick..." for each element. So I tried adding an event listener to each member of the class, by imitating similar resources found on the web: unfortunately, this part (the commented code above) does not work. In particular, I do not see which parameter I should pass to the function to transmit "the element itself".
Can anyone help? If I may play it lazy, I am more looking for an answer to this specific query than for pointers to a series of courses I should take: I admit it, I have not been doing html for a loooooong time, and I am sure I would need a lot of readings to be efficient again: simply, I do not have the time for the moment, and I do not really need it: I just need to solve this issue to set up a working solution.
Problem here is you are calling the method and assigning what it returns to be bound as the event listener
classname[i].addEventListener('click', showspoil(WHATEXACTLY?), false);
You can either use a closure or call the element directly.
classname[i].addEventListener('click', function () { showspoil(this); }, false);
or
classname[i].addEventListener('click', showspoil, false);
If you call it directly, you would need to change the function to
function showspoil(e) {
this.className="spoiled";
}
Another option would be to not bind click on every element, just use event delegation.
function showspoil(e) {
e.className="spoiled";
}
document.addEventListener("click", function (e) { //list for clcik on body
var clicked = e.target; //get what was clicked on
if (e.target.classList.contains("spoil")) { //see if it is an element with the class
e.target.classList.add("spoiled"); //if it is, add new class
}
});
.spoil { color: red }
.spoiled { color: green }
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
function unspoil() {
this.className = "spoiled"; // "this" is the clicked object
}
window.onload = function() {
var spoilers = document.querySelectorAll(".spoil"); // get all with class spoil
for (var i = 0; i < spoilers.length; i++) {
spoilers[i].onclick = unspoil;
}
}
span.spoil {
background-color: black;
}
span.spoiled {
background-color: white;
}
This is a text with <span class="spoil">spoil data</span>.
<br>
<span class="spoil">Unspoil me.</span>
<br>
<span class="spoil">And me.</span>
An additional approach could be to add the click-listener to the document and evaluate the event target:
document.addEventListener("click", function(e){
if (e.target.className == "spoil"){
e.target.className = "spoiled";
}
});
That way
you only need one event listener in the whole page
you can also append other elements dynamically with that class without the need for a new event handler
This should work, because the event's target is always the actual element being clicked. If you have sub-elements in your "spoil" items, you may need to traverse up the parent chain. But anyway I think this is the least resource-wasting way.
var spoilers = document.getElementsByClassName('spoil');
for(i=0;i<spoilers.length;i++){
spoilers[i].addEventListener('click',function(){
this.className = "spoiled";
});
}
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;
}
So I have a javascript function that inserts a span on clicking a button. But the problem is when I insert it, it overlaps the other elements. How can I move the other elements down when the button is clicked to make room for the inserted element? And then move them back up when the element is removed? Here is my code:
$(".top-button").on("click", function() {
if (this.nextElementSibling) {
$(this.nextElementSibling).slideToggle(600);
};
});
$(".bottom-button").on("click", function() {
$(this.nextElementSibling).slideToggle(600);
});
And a working demo here http://codepen.io/andrewcockerham/pen/xjgkL/
Basically, when I click on the yellow and green 'buttons' on Entry 1, the MP and IP boxes toggle, but they overlap the other elements (When Entry 1 is collapsed, {click on it}). How can I make the other elements move out to make room when the MP and IP appear, then return to their normal place when the MP or IP disappear?
I've tried appendChild(), insertAfter(), insertBefore(), all without success.
Please forgive the ugly demo and ugly code - its a WIP! Thanks!
So I figured out how to do it, so I'll answer my own question.
Basically I just inserted a blank or empty div 'behind' the inserted span, thus moving the DOM down or up. Here's the JS code:
$(".top-button").on("click", function() {
if (this.nextElementSibling) {
if ($(this.nextElementSibling).css('display') == "none") {
// insert blank element to move the DOM down
$("<div class='top-blank'><span></span></div>").insertBefore($(this).parent());
} else {
// remove blank element when collapse dropdown
$(this).parent().siblings('.top-blank').slideUp(600);
}
$(this.nextElementSibling).slideToggle(600);
};
});
$(".bottom-button").on("click", function() {
if ($(this.nextElementSibling).css('display') == "none") {
$("<div class='blank'><span></span></div>").insertAfter($(this).parent());
} else {
// remove inserted stuff
$('.blank').slideUp(500);
}
$(this.nextElementSibling).slideToggle(600);
});
and the CSS:
.blank {
position: relative;
min-height: 60px;
border-left: 2px solid white;
left: -50px;
}
.top-blank {
position: relative;
min-height: 60px;
}
updated Codepen working example: http://codepen.io/andrewcockerham/pen/xjgkL/
Not sure if this is the best or most proper way, but it works in my case. Interested to hear if there are other better solutions for this.
I am developing a page that will create many <div>s and appending them to a container <div>. I need to know which one of them is being clicked on. At first, I figured putting an eventListener on each one of them would be fine. However, I just read an article about using Smart Event Handlers here: Best Practices for Speeding Up Your Web Site. And it talks about using only 1 eventListener and figuring out which one of the elements the event originates from.
EDIT
I removed the previous example, here is a small example that is much closer to my target functionality and illustrates why it would be preferable to have one listener that dispatches what was clicked on. The main thing is that the <div> is what knows which index in an array needs to be grabbed for data. However, the data gets presented in <div>s that are inside the <div> that knows the array index and the event doesn't hook with him for some reason.
When you run it, you see that the log only lists the contact whenever the green "middle" <div> gets clicked but not the red "information" <div>s. How can I get the red information <div>s to trigger the listener as well without adding a zillion listeners?
JSFiddle
<!DOCTYPE html>
<html>
<head>
<title>Bubbling</title>
<meta charset="UTF-8">
<style>
div{
margin: 10px;
padding: 10px;
width: 200px;
}
#big{
background: #ccffcc;
}
.contactDiv{
background: #99ff99;
}
.nameDiv, .phoneDiv{
background: #ff9999;
}
#log{
background: #ccccff;
}
</style>
</head>
<body>
<div id="log">I log stuff</div>
<button id="adder">Add Some Div</button>
<!--Highest div that encloses multiple other div-->
<div id="big"></div>
<script>
var log = document.getElementById("log"),
adderButton = document.getElementById("adder"),
bigDiv = document.getElementById("big"),
numDivsToMake = 100,
i, contactDiv, nameDiv, phoneDiv,
contacts = [];
for (i = 0; i < numDivsToMake; i++) {
contacts.push({
name: "Bob-" + i,
phone: "555-1234"
});
}
// Make more divs whenever we click the super button
adderButton.addEventListener("click", function() {
// Don't make more
adderButton.setAttribute("disabled");
// Make the divs with data
for (i = 0; i < numDivsToMake; i++) {
// Make name and number divs
contactDiv = document.createElement("div");
nameDiv = document.createElement("div");
phoneDiv = document.createElement("div");
// Add classes
contactDiv.className = "contactDiv";
nameDiv.className = "nameDiv";
phoneDiv.className = "phoneDiv";
// Set their values
nameDiv.innerHTML = contacts[i].name;
phoneDiv.innerHTML = contacts[i].phone;
// Set the container to know how to get back to the data in the array
contactDiv.setAttribute("data-contactId", i);
// Add them to the dom
bigDiv.appendChild(contactDiv);
contactDiv.appendChild(nameDiv);
contactDiv.appendChild(phoneDiv);
}
});
// Make smart handler
bigDiv.addEventListener("click", function(e) {
// Get whether the element has the attribute we want
var att = e.target.getAttribute("data-contactId");
// Say if it does or not
if (att) {
log.innerHTML = "You clicked: " +
contacts[att].name + " " +
contacts[att].phone;
}
else {
console.log("No attribute");
}
});
</script>
</body>
</html>
I think I understand what you're doing. The event delegation you've set up in the fiddle seems to work well. The data attribute you want to select could easily be selected inside your click handler by querying the DOM. Instead of looking at the source of the click if you know you always want the data, just do another document.getElementById to retrieve your data. I think you're trying to collapse two steps into one in a way that won't work with your design.
I have some icons that the user is picking with his mouse.
I have this series of icons where I can select and set their border. I am limiting the number of chosen icons to 5. The first selected would become a yellow border one. The next 4 would be black border.
On document.ready, I do:
$('img.selectable').click(function(){
image_click(this); });
For the CSS:
.selectable {
border: 3px solid #ebe6b3;
float:left;
margin:1px;
}
For the HTML:
<img class="selectable" src="img/first_icon.png">
I have this function:
function image_click(e)
{
if($(e).data("clicked")=="yes")
{
images_selected--;
$(e).data("clicked","no").css('border','3px solid ' + NEUTRAL_COLOR);
if(images_selected==1)
{
$('img.selectable').not( e ).filter(function() {
return $( this ).data("clicked")=="yes";
}).css('border','3px solid ' + YELLOW_COLOR);
}
}
else
{
if (images_selected<5)
{
images_selected++;
if(images_selected==1)
{
$(e).data("clicked","yes").css('border','3px solid ' YELLOW_COLOR);
}
else
{
$(e).data("clicked","yes").css('border','3px solid ' + BLACK_COLOR);
}
}
}
};
There has to be a first icon, which will be yellow all the time. I was thinking of doing it with an order array, which stores the order of the objects.
The thing is I didn't seem to be able to call an object from the array and still preserve it's css function.
I was thinking of something like:
var x=[];
inside image_click(e){..
at some point I store the object:
$(e).data("order",clicked_img);
x[clicked_img]=e;
and when I pop it out:
alert(x[clicked_img].data("order"));
...}
BUT....
Seems like I have no acces to the data anymore. Like when the object left the jQuery realm it has lost it's civil rights. I don't know how to access it's data variables.
Help please!
Thanks!
You saved the DOM element, not the jQuery object. It should be
x[clicked_img]=$(e);
not
x[clicked_img]=e;
Like when the object left the jQuery realm it has lost it's civil
rights.
Simple solution: put it back in the jQuery realm. You have a couple options:
x[clicked_img] = $(e);
// ...
alert(x[clicked_img].data("order"));
or:
x[clicked_img] = e;
// ...
alert($(x[clicked_img]).data("order"));
or:
x[clicked_img] = e; // assuming e is the DOM element, not the event
// ...
alert(x[clicked_img].dataset.order);
The latter is not recommended for now as I'm unsure of the cross-browser implications, but in any case, that's how you do it in "regular" JavaScript.
I'd suggest the first method, in which case you should also be assigning $(e) to a variable at the beginning of image_click so it doesn't rebuild the jQuery object every time.