Function Being Called Before Current Function Code Complete - javascript

Made a JS Fiddle implementation of the "5 hunters, 3 rabbits" problem described here: https://twitter.com/Mathgarden/status/1039247616616194048
My code is here: https://jsfiddle.net/iPrash/o037fpam/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rabbit Hunters</title>
<link rel="stylesheet" href="htmltable.css">
<script language="JavaScript">
var size = 5;
var hunters = new Array();
var arena = new Array(side, side);
function hunter(row, col) {
this.row = row;
this.col = col;
}
function hunter(row, col) {
this.row = row;
this.col = col;
}
function resetArena() {
hunters = [];
redrawArena();
}
function generate_table() {
// get the reference for the body
var body = document.getElementsByTagName("body")[0];
// creates a <table> element and a <tbody> element
var tbl = document.createElement("table");
tbl.setAttribute("class", "huntertable");
var tblBody = document.createElement("tbody");
// creating all cells
for (var i = 0; i < size; i++) {
// creates a table row
var row = document.createElement("tr");
for (var j = 0; j < size; j++) {
// Create a <td> element and a text node, make the text
// node the contents of the <td>, and put the <td> at
// the end of the table row
var cell = document.createElement("td");
cell.addEventListener("click", cellClicked);
cell.bgColor = "green";
cell.innerHTML = "O";
row.appendChild(cell);
}
// add the row to the end of the table body
tblBody.appendChild(row);
}
// put the <tbody> in the <table>
tbl.appendChild(tblBody);
// appends <table> into <body>
body.appendChild(tbl);
// sets the border attribute of tbl to 2;
tbl.setAttribute("border", "5");
}
function cellClicked() {
var cellRow = this.parentNode.rowIndex;
var cellCol = this.cellIndex;
var cHunter = new hunter(cellRow, cellCol);
if (exists(cHunter)) {
// remove the hunter
remove(cHunter);
} else {
if (hunters.length == 5) {
alert("A maximum of 5 hunters are allowed!");
return;
}
hunters.push(cHunter);
redrawArena();
}
}
function exists(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col))
return true;
}
return false;
}
function remove(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col)) {
hunters.splice(i, 1);
break;
}
}
redrawArena();
}
function redrawArena() {
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
// reset arena
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
rCell.innerHTML = "O";
rCell.bgColor = "green";
}
}
for (var hunterIndex = 0; hunterIndex < hunters.length; hunterIndex++) {
// for each hunter mark the attacked territory:
hunterRow = hunters[hunterIndex].row;
hunterCol = hunters[hunterIndex].col;
huntRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
huntCell = huntRow.getElementsByTagName("td")[hunterCol];
huntCell.innerHTML = "H";
huntCell.bgColor = "red";
// horizontal and vertical
for (var i = 0; i < size; i++) {
hRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
hCell = hRow.getElementsByTagName("td")[i];
hCell.bgColor = "red";
vRow = arenaTBody.getElementsByTagName("tr")[i];
vCell = vRow.getElementsByTagName("td")[hunterCol];
vCell.bgColor = "red";
}
// diagonals
for (var i = 1; i < size; i++) {
if (((hunterRow + i) < size) && ((hunterCol + i) < size)) {
dRow1 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell1 = dRow1.getElementsByTagName("td")[hunterCol + i];
dCell1.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol - i) >= 0)) {
dRow2 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell2 = dRow2.getElementsByTagName("td")[hunterCol - i];
dCell2.bgColor = "red";
}
if (((hunterRow + i) < size) && ((hunterCol - i) >= 0)) {
dRow3 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell3 = dRow3.getElementsByTagName("td")[hunterCol - i];
dCell3.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol + i) < size)) {
dRow4 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell4 = dRow4.getElementsByTagName("td")[hunterCol + i];
dCell4.bgColor = "red";
}
}
}
alert("Checking for win ...");
checkWin();
}
function checkWin() {
// check arena for 5 hunters and 3 rabbits...
if (hunters.length < 5)
return;
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
var rabbits = 0;
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
if (rCell.bgColor == "green") {
rabbits++;
}
}
}
if (rabbits == 3)
alert("Congrats! You did it!")
}
</script>
</head>
<body onload="generate_table()">
<h1>Rabbit Hunters</h1>
<p>
<ol>
<li>The grid below represents a forest filled with rabbits (green).</li>
<li>Hunters can attack horizontally and diagonally in all directions (like a chess queen).</li>
<li>Once placed, hunters will kill all rabbits in their lines of sight (try clicking!).</li>
<li>To remove hunters just click on them again.</li>
<li>The Reset button clears the whole forest.</li>
</ol>
<strong>Can you place 5 hunters on the grid below so that they spare 3 rabbits (three green squares should remain)?</strong>
</p>
<p>
<input type="button" value="Reset" onclick="resetArena()" />
</p>
</body>
</html>
======================
My question is: Why does the alert "Checking for win ..." show up before the last hunter is drawn (or the arena is fully redrawn). I added this debug alert because even though I have the checkWin() function being called after the redraw loop is fully complete it seems to want to start executing checkWin() first. So the last clicked hunter square turns to "H" only after the alert while I want it to be before checking for win.
Thank you!

About problem
Your function checkWin isn't really invoked before any other code in function redrawArena. Described problem is caused by usage of code, that is blocking main UI thread of browser, which is used for updating rendered document and executing its JS code (except workers). So if you dynamically change properties (like CSS styles) of some elements in document, browser might not repaint (or reflow) document immediately, this will usually happen after all JS functions already present in call stack are popped out from it (i.e. they return value). Let us call some part of function code as "blocking code", if it will prevent this function from returning its value for longer time (long enough to make website noticeable unresponsive to user). Most common sources of blocking code are synchronous XMLHttpRequests, native JS dialogs, long running loops (e.g. some heavy computation), etc.
Here is a simple example demonstrating how will longer running loop (after first color change) block repainting of document, so you will never see "busy status" (red color):
#status {
display: inline-block;
width: 10px;
height: 10px;
background-color: gray;
}
<!DOCTYPE html>
<html>
<head>
<script>
function start(){
setStatus('red');
compute();
}
function compute(){
var a = [];
for (var i = 0; i < 10; i+=0.000001){
a.push(Math.sin(i) + Math.cos(i));
}
setStatus('green');
}
function setStatus(color){
document.getElementById('status').style.backgroundColor = color;
}
</script>
</head>
<body>
<div>Status: <div id="status"></div></div>
<button onclick="start()">Compute</button>
<button onclick="setStatus(null)">Reset</button>
</body>
</html>
Alert modal
Obviously, such a thread blocking behavior in your code can be caused only by alert() calls. But here arises question - Which browser(s) are you using for testing your code? According to alert spec:
Show message to the user.
Optionally, pause while waiting for the user to acknowledge the message.
and then pause spec:
If necessary, update the rendering or user interface of any Document or browsing context to reflect the current state.
Wait until the condition goal is met. While a user agent has a paused task, the corresponding event loop must not run further tasks, and any script in the currently running task must block. User agents should remain responsive to user input while paused, however, albeit in a reduced capacity since the event loop will not be doing anything.
alert should not block rendering (UI may be refreshed during pause), and theoretically it doesn't need to block any subsequent JS code while waiting for user (as spec says that pause is optional).
However, browsers are not always following specification (and in the case of pausing, experiments are allowed and encouraged), so I decided to test your code on several browsers I had available on current machine (Win7):
|-------------|---------------------|-----------------------|
| | | |
| browser | blocks UI refresh | executes subsequent |
| | during alert | code during alert |
| | | |
|-------------|---------------------|-----------------------|
| Chrome 69.0 | yes | no* |
|-------------|---------------------|-----------------------|
| Opera 55.0 | yes | no |
|-------------|---------------------|-----------------------|
| FF Dev 63.0 | no | no |
|-------------|---------------------|-----------------------|
| FF 62.0 | no | no |
|-------------|---------------------|-----------------------|
| FF 60.2 | no | no |
|-------------|---------------------|-----------------------|
| FF 52.9 | no | no |
|-------------|---------------------|-----------------------|
| IE 11 | no | no |
|-------------|---------------------|-----------------------|
As you can see from above table, I encountered described problem only in Chrome and Opera (same rendering engine, both in currently latest stable version). None of tested browsers will run any subsequent JS code during alert, however Chrome appears to push callbacks of input events into call stack, if these events are triggered before displaying first alert (*). E.g. if you manage to click on your table cell more than once fast enough, function cellClicked will be invoked more times (so if you confirm first alert, Chrome will update UI state and display another alert). Any other browser from table doesn't seem to have this behavior though.
Possible solutions
The most straightforward solution to avoid potential unwanted block of pending UI update is giving browser time to perform updates before blocking code will be executed. This can be achieved by moving alert and subsequent code into asynchronous callback, e.g. by using setTimeout function. In following snippet, I have moved calling of alert "Checking for win ..." and checkWin() in function redrawArena into callback of setTimeout function added there. This will allow browsers refresh your table before any alert is displayed, and thus bypassing unwanted UI blocking effect in affected browsers. You can also play with delay parameter of setTimeout to find minimal value that will allow to trigger UI refresh in all targeted browsers.
.huntertable tr {
cursor: pointer;
}
.huntertable td {
font-size: 40px;
text-align: center;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rabbit Hunters</title>
<script>
var size = 5;
var hunters = new Array();
function hunter(row, col) {
this.row = row;
this.col = col;
}
function resetArena() {
hunters = [];
redrawArena();
}
function generate_table() {
// get the reference for the body
var body = document.getElementsByTagName("body")[0];
// creates a <table> element and a <tbody> element
var tbl = document.createElement("table");
tbl.setAttribute("class", "huntertable");
var tblBody = document.createElement("tbody");
// creating all cells
for (var i = 0; i < size; i++) {
// creates a table row
var row = document.createElement("tr");
for (var j = 0; j < size; j++) {
// Create a <td> element and a text node, make the text
// node the contents of the <td>, and put the <td> at
// the end of the table row
var cell = document.createElement("td");
cell.addEventListener("click", cellClicked);
cell.bgColor = "green";
cell.innerHTML = "O";
row.appendChild(cell);
}
// add the row to the end of the table body
tblBody.appendChild(row);
}
// put the <tbody> in the <table>
tbl.appendChild(tblBody);
// appends <table> into <body>
body.appendChild(tbl);
// sets the border attribute of tbl to 2;
tbl.setAttribute("border", "5");
}
function cellClicked() {
var cellRow = this.parentNode.rowIndex;
var cellCol = this.cellIndex;
var cHunter = new hunter(cellRow, cellCol);
if (exists(cHunter)) {
// remove the hunter
remove(cHunter);
} else {
if (hunters.length == 5) {
alert("A maximum of 5 hunters are allowed!");
return;
}
hunters.push(cHunter);
redrawArena();
}
}
function exists(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col))
return true;
}
return false;
}
function remove(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col)) {
hunters.splice(i, 1);
break;
}
}
redrawArena();
}
function redrawArena() {
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
// reset arena
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
rCell.innerHTML = "O";
rCell.bgColor = "green";
}
}
for (var hunterIndex = 0; hunterIndex < hunters.length; hunterIndex++) {
// for each hunter mark the attacked territory:
hunterRow = hunters[hunterIndex].row;
hunterCol = hunters[hunterIndex].col;
huntRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
huntCell = huntRow.getElementsByTagName("td")[hunterCol];
huntCell.innerHTML = "H";
huntCell.bgColor = "red";
// horizontal and vertical
for (var i = 0; i < size; i++) {
hRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
hCell = hRow.getElementsByTagName("td")[i];
hCell.bgColor = "red";
vRow = arenaTBody.getElementsByTagName("tr")[i];
vCell = vRow.getElementsByTagName("td")[hunterCol];
vCell.bgColor = "red";
}
// diagonals
for (var i = 1; i < size; i++) {
if (((hunterRow + i) < size) && ((hunterCol + i) < size)) {
dRow1 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell1 = dRow1.getElementsByTagName("td")[hunterCol + i];
dCell1.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol - i) >= 0)) {
dRow2 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell2 = dRow2.getElementsByTagName("td")[hunterCol - i];
dCell2.bgColor = "red";
}
if (((hunterRow + i) < size) && ((hunterCol - i) >= 0)) {
dRow3 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell3 = dRow3.getElementsByTagName("td")[hunterCol - i];
dCell3.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol + i) < size)) {
dRow4 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell4 = dRow4.getElementsByTagName("td")[hunterCol + i];
dCell4.bgColor = "red";
}
}
}
setTimeout(function() {
alert("Checking for win ...");
checkWin();
},20);
}
function checkWin() {
// check arena for 5 hunters and 3 rabbits...
if (hunters.length < 5)
return;
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
var rabbits = 0;
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
if (rCell.bgColor == "green") {
rabbits++;
}
}
}
if (rabbits == 3)
alert("Congrats! You did it!")
}
</script>
</head>
<body onload="generate_table()">
<h1>Rabbit Hunters</h1>
<p>
<ol>
<li>The grid below represents a forest filled with rabbits (green).</li>
<li>Hunters can attack horizontally and diagonally in all directions (like a chess queen).</li>
<li>Once placed, hunters will kill all rabbits in their lines of sight (try clicking!).</li>
<li>To remove hunters just click on them again.</li>
<li>The Reset button clears the whole forest.</li>
</ol>
<strong>Can you place 5 hunters on the grid below so that they spare 3 rabbits (three green squares should remain)?</strong>
</p>
<p>
<input type="button" value="Reset" onclick="resetArena()" />
</p>
</body>
</html>
However, in most cases (like yours), it is better to display such a kind of output directly into document. This way, your message will be displayed alongside other changes in DOM on next UI refresh. With the help of HTML, CSS and JS you can create your personal method for displaying messages (including custom modals, infobars, etc.). In below snippet, I have created very simple example, how can your app output messages to user. Basically, I have added additional div for displaying messages, created two functions for showing/removing messages in this div (showMessage/removeMessage) and replaced alerts with showMessage calls (and removed pointless alert "Checking for win ...").
.huntertable tr {
cursor: pointer;
}
.huntertable td {
font-size: 40px;
text-align: center;
}
#infobar {
color: red;
font-weight: bold;
margin-bottom: 10px;
visibility: hidden;
min-height: 20px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rabbit Hunters</title>
<script>
var size = 5;
var hunters = new Array();
function hunter(row, col) {
this.row = row;
this.col = col;
}
function showMessage(msgText) {
var infobar = document.getElementById('infobar');
infobar.innerHTML = msgText;
infobar.style.visibility = 'visible';
}
function removeMessage() {
var infobar = document.getElementById('infobar');
infobar.innerHTML = '';
infobar.style.visibility = null;
}
function resetArena() {
removeMessage();
hunters = [];
redrawArena();
}
function generate_table() {
// get the reference for the body
var body = document.getElementsByTagName("body")[0];
// creates a <table> element and a <tbody> element
var tbl = document.createElement("table");
tbl.setAttribute("class", "huntertable");
var tblBody = document.createElement("tbody");
// creating all cells
for (var i = 0; i < size; i++) {
// creates a table row
var row = document.createElement("tr");
for (var j = 0; j < size; j++) {
// Create a <td> element and a text node, make the text
// node the contents of the <td>, and put the <td> at
// the end of the table row
var cell = document.createElement("td");
cell.addEventListener("click", cellClicked);
cell.bgColor = "green";
cell.innerHTML = "O";
row.appendChild(cell);
}
// add the row to the end of the table body
tblBody.appendChild(row);
}
// put the <tbody> in the <table>
tbl.appendChild(tblBody);
// appends <table> into <body>
body.appendChild(tbl);
// sets the border attribute of tbl to 2;
tbl.setAttribute("border", "5");
}
function cellClicked() {
removeMessage();
var cellRow = this.parentNode.rowIndex;
var cellCol = this.cellIndex;
var cHunter = new hunter(cellRow, cellCol);
if (exists(cHunter)) {
// remove the hunter
remove(cHunter);
} else {
if (hunters.length == 5) {
showMessage("A maximum of 5 hunters are allowed!");
return;
}
hunters.push(cHunter);
redrawArena();
}
}
function exists(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col))
return true;
}
return false;
}
function remove(hunter) {
for (var i = 0; i < hunters.length; i++) {
if ((hunters[i].row == hunter.row) && (hunters[i].col == hunter.col)) {
hunters.splice(i, 1);
break;
}
}
redrawArena();
}
function redrawArena() {
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
// reset arena
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
rCell.innerHTML = "O";
rCell.bgColor = "green";
}
}
for (var hunterIndex = 0; hunterIndex < hunters.length; hunterIndex++) {
// for each hunter mark the attacked territory:
hunterRow = hunters[hunterIndex].row;
hunterCol = hunters[hunterIndex].col;
huntRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
huntCell = huntRow.getElementsByTagName("td")[hunterCol];
huntCell.innerHTML = "H";
huntCell.bgColor = "red";
// horizontal and vertical
for (var i = 0; i < size; i++) {
hRow = arenaTBody.getElementsByTagName("tr")[hunterRow];
hCell = hRow.getElementsByTagName("td")[i];
hCell.bgColor = "red";
vRow = arenaTBody.getElementsByTagName("tr")[i];
vCell = vRow.getElementsByTagName("td")[hunterCol];
vCell.bgColor = "red";
}
// diagonals
for (var i = 1; i < size; i++) {
if (((hunterRow + i) < size) && ((hunterCol + i) < size)) {
dRow1 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell1 = dRow1.getElementsByTagName("td")[hunterCol + i];
dCell1.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol - i) >= 0)) {
dRow2 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell2 = dRow2.getElementsByTagName("td")[hunterCol - i];
dCell2.bgColor = "red";
}
if (((hunterRow + i) < size) && ((hunterCol - i) >= 0)) {
dRow3 = arenaTBody.getElementsByTagName("tr")[hunterRow + i];
dCell3 = dRow3.getElementsByTagName("td")[hunterCol - i];
dCell3.bgColor = "red";
}
if (((hunterRow - i) >= 0) && ((hunterCol + i) < size)) {
dRow4 = arenaTBody.getElementsByTagName("tr")[hunterRow - i];
dCell4 = dRow4.getElementsByTagName("td")[hunterCol + i];
dCell4.bgColor = "red";
}
}
}
checkWin();
}
function checkWin() {
// check arena for 5 hunters and 3 rabbits...
if (hunters.length < 5)
return;
var arenaTable = document.getElementsByTagName("tbl")[0];
var arenaTBody = document.getElementsByTagName("tbody")[0];
var rabbits = 0;
for (var rowi = 0; rowi < size; rowi++) {
for (var coli = 0; coli < size; coli++) {
rRow = arenaTBody.getElementsByTagName("tr")[rowi];
rCell = rRow.getElementsByTagName("td")[coli];
if (rCell.bgColor == "green") {
rabbits++;
}
}
}
if (rabbits == 3)
showMessage("Congrats! You did it!");
}
</script>
</head>
<body onload="generate_table()">
<h1>Rabbit Hunters</h1>
<p>
<ol>
<li>The grid below represents a forest filled with rabbits (green).</li>
<li>Hunters can attack horizontally and diagonally in all directions (like a chess queen).</li>
<li>Once placed, hunters will kill all rabbits in their lines of sight (try clicking!).</li>
<li>To remove hunters just click on them again.</li>
<li>The Reset button clears the whole forest.</li>
</ol>
<strong>Can you place 5 hunters on the grid below so that they spare 3 rabbits (three green squares should remain)?</strong>
</p>
<p>
<input type="button" value="Reset" onclick="resetArena()" />
</p>
<div id="infobar"></div>
</body>
</html>
TL;DR
In general, you should avoid using JS alerts neither for displaying usual textual output from your app, nor for debugging it. Alerts may prevent UI refresh in some browsers until they are confirmed by user, and also they may be blocked by browser, making them unreliable for displaying important information. If you need to display messages to user, you should create one or more methods for displaying them dynamically in document (you can actually find many different solutions over the web). For debugging, you should use Dev Tools panel integrated in every modern browser and methods from Console object. If you still insist on using alerts in your app, and you are experiencing unwanted blocking of page UI refresh or unrelated code because of them, you will probably need to put them into asynchronous callbacks.

Related

Javascript Pagination Issue

I'm trying to make a simple Javascript pagination function, but I'm having this issue where instead of iterating through the array, it keeps adding new list items to the innerhtml.
I have tried creating an element and appending it to the DOM.
I have tried using if/else statements to display the list items I
want.
<body>
<div class='result'></div>
<button class="add">+</button>
<script src='pretty.js'></script>
</body>
let dogs = [
'goldendoodle',
'poodle',
'afghan hound',
'golden retriever',
'labrador',
'chihuahua',
'pitbull',
'german shepherd',
'greyhound',
'bull terrier'
]
let high = 1;
let low = 0;
let result = document.querySelector('.result');
let add = document.querySelector('.add');
function Pagination(low,high) {
for(var i = 0 ; i < dogs.length;i++) {
let answer = document.createElement('div');
answer.classList.add('dogs-dom');
answer.innerHTML = dogs[i];
result.appendChild(answer);
if(i >= low && i < high) {
answer.style.display ='block';
}
if(i < low || i > high) {
answer.style.display ='none';
}
}
}
Pagination(low,high);
add.addEventListener('click', () => {
low += 2;
high += 2;
Pagination(low,high);
});
When I click the button, I want the next two array items to appear and replace the last two shown.
To use the approach you've outlined above you'll need to clear the innerHtml of the result element before appending new children. At the top of your Pagination function try result.innerHtml = '';.
That said if you are using a hide/show approach to paginate the list it would be more efficient to create the dom elements only once and modify the style.display property of each instead of clearing out the result and re-creating all of the answer divs on every click.
Your Pagination function only adds elements to the dom each time it is called.
You can either remove the existing elements every time Pagination is called, and render only those that should be displayed, e.g.:
function Pagination(low,high) {
result.innerHTML = ''; // remove all children of result
// only render the children which should be visible
for(var i = low ; i < high;i++) {
let answer = document.createElement('div');
answer.classList.add('dogs-dom');
answer.innerHTML = dogs[i];
result.appendChild(answer);
}
}
Or you can use display: block; / display: none. (Will not scale very well with large lists)
function Pagination(low,high) {
// only append all dogs once
if(result.childElementCount === 0) {
for(var i = 0; i < dogs.length;i++) {
let answer = document.createElement('div');
answer.classList.add('dogs-dom');
answer.style.display ='none';
answer.innerHTML = dogs[i];
result.appendChild(answer);
}
}
// toggle display: none / block for each element
for(var i = 0; i < dogs.length;i++) {
if(i >= low && i < high)
answer.style.display ='block';
else
answer.style.display ='none';
}
}
As a bonus, heres a reusable pagination class example:
function Pagination(container, items) {
this.container = container;
this.result = container.querySelector('.result');
this.prevBtn = container.querySelector('.prev');
this.nextBtn = container.querySelector('.next');
this.items = items;
this.offset = 0;
this.limit = 5;
this.updateDom();
this.prevBtn.onclick = this.prevPage.bind(this);
this.nextBtn.onclick = this.nextPage.bind(this);
}
Pagination.prototype.nextPage = function() {
if((this.offset + this.limit) < this.items.length)
this.offset += this.limit;
this.updateDom();
};
Pagination.prototype.prevPage = function() {
if(this.offset >= this.limit)
this.offset -= this.limit;
this.updateDom();
};
Pagination.prototype.updateDom = function() {
this.result.innerHTML = '';
let stop = Math.min(this.offset + this.limit, this.items.length);
for(let i = this.offset; i < stop; i++) {
let el = document.createElement("div");
el.appendChild(document.createTextNode(this.items[i]));
this.result.appendChild(el);
}
let hasPrev = this.offset > 0;
if(hasPrev)
this.prevBtn.classList.remove('hide');
else
this.prevBtn.classList.add('hide');
let hasNext = (this.offset + this.limit) < this.items.length;
if(hasNext)
this.nextBtn.classList.remove('hide');
else
this.nextBtn.classList.add('hide');
};
let items = [];
for (let i = 1; i <= 50; i++)
items.push(`Item ${i}`);
let pagination = new Pagination(document.querySelector(".paginate"), items);
// You can also programatically switch to the next / prev page:
// pagination.nextPage();
// pagination.prevPage();
.hide { visibility: hidden; }
<div class="paginate">
<div class="result"></div>
<button class="prev">PREV</button>
<button class="next">NEXT</button>
</div>
Maybe this is along the lines of what you want to do?
It tracks only a globalIndex (which would be like like your 'low' variable).
The showNextTwoItems function:
- Notes the indexes where we should start and end
- Clears the container div
- Enters a while loop that appends items and increments the current index
- Updates the globalIndex when enough items have been added
let dogs = [ 'goldendoodle', 'poodle', 'afghan hound', 'golden retriever', 'labrador', 'chihuahua', 'pitbull', 'german shepherd', 'greyhound', 'bull terrier' ],
containerDiv = document.querySelector('.result'),
addBtn = document.querySelector('.add'),
globalIndex = 0; // Tracks where we left off (starts at zero)
const NUMBER_TO_SHOW = 2;
addBtn.addEventListener("click", showNextTwoItems); // Calls function on click
function showNextTwoItems(){
let numberToShow = NUMBER_TO_SHOW, // In case we ever want to change numberToShow
currentIndex = globalIndex, // Gets local copy of globalIndex (always < dogs.length)
// Lets us stop looping when we've shown enough or reach the end of the array
stopBeforeIndex = Math.min(currentIndex + numberToShow, dogs.length);
containerDiv.innerHTML = ""; // Clears div
while(currentIndex < stopBeforeIndex){
// Creates and appends a text node with the next dog
const newItem = document.createTextNode(dogs[currentIndex]);
containerDiv.appendChild(newItem);
// Creates and appends a line break
const lineBreak = document.createElement("BR");
containerDiv.appendChild(lineBreak);
// Moves on to the next index
currentIndex++;
}
// Updates global index (making sure it is not too big for the array)
globalIndex = currentIndex < dogs.length ? currentIndex : 0;
}
<button class="add">+</button>
<div class='result'></div>

Image will not draw in webpage from Javascript

The images i'm trying to draw will not work. When I run functions in javascript, I get this error:
Uncaught TypeError: context.drawImage is not a function
at Image.drawing.onload (test.js:74)
test.js:82 Uncaught TypeError: context.drawImage is not a function
at Image.drawing.onload (test.js:82)
Here is my code:
HTML:
<html>
<head>
<title>Maze</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js">
</script>
<script type="text/javascript" src="js/test.js"></script>
<style type="text/css">
#maze {
border-collapse: collapse;
}
#maze td {
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<table id="maze">
<tbody></tbody>
</table>
<canvas>
<script>
var disp = newMaze(20,20);
for (var i = 0; i < disp.length; i++) {
$('#maze > tbody').append("<tr>");
for (var j = 0; j < disp[i].length; j++) {
var selector = i+"-"+j;
$('#maze > tbody').append("<td id='"+selector+"'> </td>");
if (disp[i][j][0] == 0) { $('#'+selector).css('border-top', '2px
solid black'); }
if (disp[i][j][1] == 0) { $('#'+selector).css('border-right', '2px
solid black'); }
if (disp[i][j][2] == 0) { $('#'+selector).css('border-bottom', '2px
solid black'); }
if (disp[i][j][3] == 0) { $('#'+selector).css('border-left', '2px
solid black'); }
}
$('#maze > tbody').append("</tr>");
}
</script>
</canvas>
</body>
</html>
JS:
function newMaze(x, y) {
// Establish variables and starting grid
var totalCells = x*y;
var cells = new Array();
var unvis = new Array();
for (var i = 0; i < y; i++) {
cells[i] = new Array();
unvis[i] = new Array();
for (var j = 0; j < x; j++) {
cells[i][j] = [0,0,0,0];
unvis[i][j] = true;
}
}
// Set a random position to start from
var currentCell = [Math.floor(Math.random()*y), Math.floor(Math.random()*x)];
var path = [currentCell];
unvis[currentCell[0]][currentCell[1]] = false;
var visited = 1;
// Loop through all available cell positions
while (visited < totalCells) {
// Determine neighboring cells
var pot = [[currentCell[0]-1, currentCell[1], 0, 2],
[currentCell[0], currentCell[1]+1, 1, 3],
[currentCell[0]+1, currentCell[1], 2, 0],
[currentCell[0], currentCell[1]-1, 3, 1]];
var neighbors = new Array();
// Determine if each neighboring cell is in game grid, and whether it has already been checked
for (var l = 0; l < 4; l++) {
if (pot[l][0] > -1 && pot[l][0] < y && pot[l][1] > -1 && pot[l][1] < x && unvis[pot[l][0]][pot[l][1]]) { neighbors.push(pot[l]); }
}
// If at least one active neighboring cell has been found
if (neighbors.length) {
// Choose one of the neighbors at random
next = neighbors[Math.floor(Math.random()*neighbors.length)];
// Remove the wall between the current cell and the chosen neighboring cell
cells[currentCell[0]][currentCell[1]][next[2]] = 1;
cells[next[0]][next[1]][next[3]] = 1;
// Mark the neighbor as visited, and set it as the current cell
unvis[next[0]][next[1]] = false;
visited++;
currentCell = [next[0], next[1]];
path.push(currentCell);
}
// Otherwise go back up a step and keep going
else {
currentCell = path.pop();
}
}
debugger
drawUser(currentCell[0],currentCell[1]);
var first = currentCell[0] + 10;
var second = currentCell[1] + 10;
if(first >= 20){
first = currentCell[0] - 10;
}
if(second >= 20){
second = currentCell[1] - 10;
}
drawJewel(first,second);
return cells;
}
function drawUser(x,y){
var context = document.getElementById("maze");
drawing = new Image();
drawing.src = "img/userpic.png";
drawing.onload = function() {
context.drawImage(drawing,x,y);
};
}
function drawJewel(x,y){
var context = document.getElementById("maze");
drawing = new Image();
drawing.src = "img/diamondpic.png";
drawing.onload = function() {
context.drawImage(drawing,x,y);
};
}
The canvas tags around the script tags in the HTML was one of my attempts at a fix, as well as the debugger in the JS code.
I'm new to jQuery but it needs to be used for generating the maze.
All i'm trying to do is display the images, diamondpic.png and userpic.png, both images 20x20 px drawn in paint, in the maze located in two separate, random cells. I'm not familiar with "canvas" or "context" and I think it is the issue.
Any help appreciated.
A couple of problems here. First, the #maze element is a table, not a canvas. If you want to use a canvas to draw your images, you need to draw on the canvas element, not a table.
Second, you need to define context as a 2d context of your canvas element.
Replace
var context = document.getElementById("maze");
with
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");

Set a custom % of 1's into a 2D blank array, where the 1's are randomly shuffled?

I've been a long time lurker on Stack Overflow but I couldn't seem to find a suitable existing solution...
I'm learning JS and HTML, and I've been playing around with 2D arrays to make game board. So far I made a custom # of rows/columns for a game board with all white tiles (represented as 0 for now).
My goal is to use an input field for a % of black tiles (represented as 1) to fill up the board (2D Array), but the black tiles have to be randomly distributed/shuffled among it.
Here's what I've got so far..
https://jsfiddle.net/5pvm4mmy/6/
function generateArray() {
var myNode = document.getElementById("table");
while (myNode.firstChild) {
myNode.removeChild(myNode.firstChild);
}
rows = $("#rows-field").val();
cols = $("#cols-field").val();
concentration = $("#concentration-field").val()
source = $("#source-field").val();
target = $("#target-field").val();
var table = document.getElementById("table");
for (var i = 0; i < rows; i++) {
var tr = document.createElement('tr');
for (var j = 0; j < cols; j++) {
var td = document.createElement('td');
if (i%2 == j%2) {
td.className = "white";
} else {
td.className = "black";
}
tr.appendChild(td);
}
table.appendChild(tr);
}
document.body.appendChild(table);
}
Thanks in advance for any help or advice.
If you need a random selection of a predefined set of values, you can use a stack. Think of it as a deck of cards and you pick a random card each time from the number of card left in the deck. In this case you have only 2 values but you may want to set the number of black and white. For this you can use a pseudo stack.
var black = 29; // 29 blacks the rest white
var white = (rows * cols) - black;
function getRandSquare(){
var select = Math.floor(Math.random() * (black + white));
if(select > black){
white -= 1;
return "white";
}
black -= 1;
return "black";
}
If you have many options like a deck of cards you use an array.
Example of a random stack.
// create a deck
var cards = [];
for(var c = 0; c < 52; c++){
cards[c] = c;
}
function shuffle(cards){
var shuf = []; // to hold the shuffled deck
while(cards.length > 0){ // pick a random item, take it from the stack and
// put on the new stack until there are no items
// left
shuf.push(cards.splice(Math.floor(Math.random() * cards.length),1));
}
return shuf; // return shuffled deck
}
cards = shuffle(cards); // get shuffled deck.
Which will work for anything where you need to pick randomly from a predefined set. It only takes one pass and the set is as random as the random number generator
To show psuedo stack working ... Always has 60 black
var cont;
function draw(){
var rows = 15;
var cols = 15;
var black = 60; // 29 blacks the rest white
var white = (rows * cols) - black;
function getRandSquare(){
var select = Math.floor(Math.random() * (black + white));
if(select > black-1){
white -= 1;
return "white";
}
black -= 1;
return "black";
}
var bCount = 0;
cont = document.createElement("div");
for(var y = 0; y < rows; y++){
for(var x = 0; x < cols; x++){
var s = document.createElement("span");
s.className = getRandSquare();
if(s.className === "black"){
s.textContent = bCount;
bCount += 1;
}
s.style.top = ((y+2) * 20) + "px";
s.style.left = (x * 20) + "px";
s.style.width = "20px";
s.style.height = "20px";
cont.appendChild(s);
}
}
document.body.appendChild(cont);
}
document.body.onclick = function(){
document.body.removeChild(cont);
cont = null;
draw();
}
draw();
span {
position:absolute;
border : 1px solid;
font-size : small;
text-align : center;
}
.black {
background : black;
border-color :white;
color : white;
}
.white {
background : white;
border-color :black;
}
<h3>Click to randomise</h3>
Never mind. I got it done, thanks!
https://jsfiddle.net/5pvm4mmy/8/
function generateArray() {
var myNode = document.getElementById("table");
while (myNode.firstChild) {
myNode.removeChild(myNode.firstChild);
}
rows = $("#rows-field").val();
cols = $("#cols-field").val();
concentration = $("#concentration-field").val();
source = $("#source-field").val();
target = $("#target-field").val();
var table = document.getElementById("table");
for (var i = 0; i < rows; i++) {
var tr = document.createElement('tr');
for (var j = 0; j < cols; j++) {
var td = document.createElement('td');
if (concentration < Math.floor((Math.random() * 100) + 1)) {
td.className = "white";
} else {
td.className = "black";
}
tr.appendChild(td);
}
table.appendChild(tr);
}
document.body.appendChild(table);
}

Changing status of <td> on click with javascript

I am making a conways game of life in javascript and having trouble getting my onclick implementation to work. It is supposed to change the life status of the cell when the td is clicked, but instead I am getting an error at my console that says : TypeError: World.tds is undefined.
TLDR: Can't figure out why onclick won't work. World.tds[] is undefined for some reason.
Onclick implementation:
if (table !== null) {
for (var i = 0; i < 20; i++) {
for (var j = 0; j < 20; j++)
World.tds[i][j].onclick = function() {
if (World.tds[i][j].cells.alive) {
World.tds[i][j].cells.alive = false;
} else {
World.tds[i][j].cells.alive = true;
}
};
}
}
Constructor and tds[] filling
var World = function() {
this.h = maxH;
this.w = maxW;
this.tds = [];
};
//generate the world in which the cells move
World.prototype.init = function() {
var row, cell;
for (var r = 0; r < this.h; r++) {
this.tds[r] = [];
row = document.createElement('tr');
for (var c = 0; c < this.w; c++) {
cell = document.createElement('td');
this.tds[r][c] = cell;
row.appendChild(cell);
}
table.appendChild(row);
}
};
Problem Statement - When you trigger the click handler, by that time values of i and j have updated to 20 each and World.tds[20][20] is undefined.
Update your code inside for loop to
(function(i,j) {
World.tds[i][j].onclick = function() {
if (World.tds[i][j].cells.alive) {
World.tds[i][j].cells.alive = false;
} else {
World.tds[i][j].cells.alive = true;
}
};
})(i,j);

JavaScript table overwriting html file

I have an assignment to write JavaScript which will create a table(x columns, y rows...) and I managed to do that. I have a problem with writing to the html page.
My code:
document.write('<table border = \"3\">');
while(i <= y)
{
document.write('<tr>');
j = 0;
while(j <= x)
{
if (i == 0)
{
if (j == 0)
{
document.write(' <th></th> ');
}
else
{
document.write('<th>');
document.write("x = " + j);
document.write('</th>');
}
}
else
{
if (j == 0)
{
document.write('<th>');
document.write("y = " + i);
document.write('</th>');
}
else
{
document.write('<td>');
document.write(operation(i,j));
document.write('</td>');
}
}
j++;
}
document.write('</tr>');
i++;
}
document.write('</table>');
}
I guess it is overwriting the page because of document.write? How can I change this?
Please don't use document.write. See Why is document.write discouraged.
I think your use of document.write means it overwrite's the entire page. This is working as intended if you call the function after the page has loaded. All calls to document.write after the document is loaded creates a new document. We prefer DOM manipulation since it a safe way to directly edit the DOM that doesn't involve string manipulation.
Writing HTML as text should be left to the browser which parses a HTML file and loads it.
// feature detection for cross browser compliance.
var txt = function(node, text) {
if (node.innerText !== undefined) {
node.innerText = text;
} else if (node.textContent !== undefined) {
node.textContent = text;
}
}
function createTable(rows, columns) {
var table = document.createElement("table");
table.border = '3';
var tbody = document.createElement("tbody");
var thead = document.createElement("thead");
for (var i = 0; i < rows; i++) {
var tr = document.createElement("tr");
for (var j = 0; j < columns; j++) {
var cell;
if (i === 0) {
cell = document.createElement("th");
if (j !== 0) {
txt(cell, "x = " + j);
}
}
else {
if (j == 0) {
cell = document.createElement("th");
txt(cell, "y = " + i);
}
else {
cell = document.createElement("td");
txt(cell, operation(i, j));
}
}
tr.appendChild(cell);
}
if (i === 0) {
thead.appendChild(tr);
} else {
tbody.appendChild(tr);
}
}
table.appendChild(thead);
table.appendChild(tbody);
return table;
}
Using DOM manipulation you could do it like above. There may be a few optimisations I missed. Live example (only tested chrome 10)
At least it works as intended.
Let me know if you want to see an implementation in say jQuery or some other DOM manipulation library. The code will be a lot nicer and more robust in terms of cross browser compliance.
I see a number of problems here. First and foremost, I don't believe your code is properly balanced. I believe as a first step it should look more like:
<script language="javascript">
document.write('<table border = \"3\">');
while(i <= y) {
document.write('<tr>');
j = 0;
while(j <= x) {
if (i == 0) {
if (j == 0) {
document.write(' <th></th> ');
} else {
document.write('<th>');
document.write("x = " + j);
document.write('</th>');
}
} else {
if (j == 0) {
document.write('<th>');
document.write("y = " + i);
document.write('</th>');
} else {
document.write('<td>');
document.write(operation(i,j));
document.write('</td>');
}
}
j++;
}
document.write('</tr>');
i++;
}
document.write('</table>');
</script>
Secondarily it looks to me like you're never really ever instantiating i, and while you do it with j, you're not doing it early enough.
At the line while(i <= y) the interpreter is basically seeing:
while (undefined <= undefined) {
At some level, I'm honestly not sure that document.writes is really the best solution for you, I would probably recommend using document.createElement instead, but that's perhaps more of a personal preference.
Attempting to step a level further out yet sticking with your methodology, I might look toward something like:
function generateTable (width, height) {
document.write("<table border=\"3\">");
var i = 1;
while (i <= height) {
document.write("<tr>");
var j = 1;
while (j <= width) {
if (i == 0) {
// header row
document.write("<th>"+ j +" x "+ i +"</th>");
} else {
document.write("<td>"+ j +" x "+ i +"</td>");
}
j++;
}
document.write("</tr>");
i++;
}
document.write("</table>");
}
generateTable(5, 10);

Categories

Resources