First of all, I'm not so sure about how to ask this question. I have an array of elements:
var buttons = publishedWork.getElementsByTagName('button');
and what I what is that each button changes its content from ▶ to ◼ and viceversa by clicking. The thing is that I don't know how many buttons will be in total, I intend to do it with a for:
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function() {
if (currentButton === buttons[i]) {
currentButton.textContent = '▶';
currentButton = null;
} else {
currentButton = buttons[i];
currentButton.textContent = '◼';
}
}
}
But what this code does is that, no matter what button I click, it always changes the content of the first button, from which I get that is the expression buttons[i] the one that is stored in currentButton, and not the reference to the button itself.
So my question is:
is this a case to resolve by closures (a topic that I'm just beginning to grasp) or is there another solution?
Unless I'm mistaken, it looks like the common "defining an index-dependent function in a loop" issue. When the onclick function is invoked, it accesses the i variable. But when that happens, i has been all the way through the loop, and is 0. So all the click-handlers just see i == 0
To solve it, you can create a function that, in turn, creates the click-handler:
var currentButton;
function createClickHandler(index) {
var button = buttons[index];
return function() {
if (currentButton === button) {
currentButton.textContent = '▶';
currentButton = null;
} else {
currentButton = button;
currentButton.textContent = '◼';
}
};
}
for (var i = buttons.length; i;) {
buttons[--i].onclick = createClickHandler(i);
}
Edit: Or use Diode's suggestion :)
I was focused on the "index in a closure" problem, but Diode's answer is much cleaner, and the better way to handle it
Use this or event.currentTarget inside click handler
....
buttons[--i].onclick = function(event) {
// both `this` and `event.currentTarget` gives the clicked button here
}
....
.
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function() {
if (currentButton === this) {
...
currentButton = null;
} else {
currentButton = this;
...
}
}
}
and in your else condition you have to reset the current button first.
var currentButton;
for (var i = buttons.length; i;) {
buttons[--i].onclick = function(event) {
if (currentButton === this) {
currentButton.textContent = '▶';
currentButton = null;
} else {
if(currentButton){
currentButton.textContent = '▶';
}
currentButton = this;
currentButton.textContent = '◼';
}
}
}
Related
Here's the code i've written, where when i write a word into e search field, it appends it in the element "word" by displaying it letter by letter. But, the problem is, that i don't know how to write the code that when i write another word in the search field, it deletes the word that appear to element "Word", then writes the new one i've written.
let text = document.getElementById("txt");
let elem = document.getElementsByClassName("target")[0];
let word = elem.querySelector(".word");
let btn = document.getElementsByClassName("btn")[0];
let error = document.querySelector('#error');
i = 0;
word.style.color = "#ffe100";
btn.addEventListener("click", function init() {
if (text.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
let save = word.textContent += text.value.charAt(i);
i++;
}
if (i < text.value.length) {
window.setTimeout(init, 100);
}
});
I've try many of alternatives, but there's no result.
I will iteratively change and/or improve your code in this answer, and will try to comment on each change in the code.
Refactor
First off, I'll have an easier time explaining the different approaches after refactoring your code:
// Prefer `const` over `let` when applicable
const input = document.querySelector("input"); // Previously: text
const output = document.querySelector("output"); // Previously: word
const button = document.querySelector("button"); // Previously: btn
const error = document.getElementById("error");
// You forgot `let`. Always declare your variables!
let i = 0;
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
reveal();
}
});
// Moved code from listener to here
function reveal() {
// Removed `save`; was unused
output.textContent += input.value[i];
++i;
// Moved here because it only runs if `input.value !== ""` anyways
if (i < input.value.length) {
setTimeout(reveal, 100);
}
}
/* Prefer to use CSS for static styles! */
.word {
color: #ffe100;
}
<!-- I assume your HTML to have looked similar to this: -->
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Now let's take a look at your refactored code from above:
There is no resetting: Revealing can only continue (text can only be added).
The value of input is referenced directly: When its value changes...
Then revealing may stop prematurely.
Then the further revealed text may not represent the entered text upon button-press.
Allow reusability
The issue of point 1 can be solved by (re-)setting i and output.textContent in the listener. To solve point 2, we need to use some buffer for the text:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
let i = 0;
let text = ""; // The buffer
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// (Re-)setting
i = 0;
text = input.value;
output.textContent = "";
reveal();
}
});
function reveal() {
output.textContent += text[i];
++i;
if (i < text.length) {
setTimeout(reveal, 100);
}
}
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
With these two small changes, your code now successfully deletes the revealed text in place for new text!
Adding states
But the deletion doesn't happen letter-by-letter. That would require some way to keep track of whether we are deleting or revealing.
Let's use a state-machine that –upon prompting– deletes the already revealed text (if any) and then reveals the new text:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
let i = 0;
let text = "";
let state = "nothing"; // The state
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// Remember previous state (for below)
const previousState = state;
// Issue deletion and update text-to-reveal
state = "delete";
text = input.value;
if (previousState === "nothing") {
// Start state-machine
nextCharacter();
}
}
});
// Rename function
function nextCharacter() {
if (state === "nothing") return;
if (state === "delete") {
output.textContent = output.textContent.slice(0, i);
// Finished deleting?
if (i === 0) {
const requiresRevealing = i < text.length;
state = requiresRevealing ? "reveal" : "nothing";
} else {
--i;
}
} else if (state === "reveal") {
output.textContent += text[i];
++i
// Finished revealing?
if (i === text.length) {
state = "nothing";
}
}
// Requires continuing?
if (state !== "nothing") {
setTimeout(nextCharacter, 100);
}
}
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Code quality refactoring
The code now works, but(!) the logic is scattered everywhere, and you need to know what variables to update for the revealing to work correctly. Instead, we could make use of classes:
const input = document.querySelector("input");
const output = document.querySelector("output");
const button = document.querySelector("button");
const error = document.getElementById("error");
// Move related variables and functions into class
class Revealer {
output;
index = 0;
text = "";
state = "nothing";
constructor(output) {
this.output = output;
}
// Moved from listener
reveal(text) {
const previousState = this.state;
this.state = "delete";
this.text = text;
if (previousState === "nothing") {
this.next();
}
}
// Previously nextCharacter()
next() {
if (this.state === "nothing") return;
if (this.state === "delete") {
this.deleteCharacter();
} else if (this.state === "reveal") {
this.revealCharacter();
}
if (this.state !== "nothing") {
setTimeout(() => this.next(), 100);
}
}
// Use more specific functions for readability
deleteCharacter() {
this.output.textContent = this.output.textContent.slice(0, this.index);
if (this.index === 0) {
const requiresRevealing = this.index < this.text.length;
this.state = requiresRevealing ? "reveal" : "nothing";
} else {
--this.index;
}
}
revealCharacter() {
this.output.textContent += this.text[this.index];
++this.index;
if (this.index === this.text.length) {
this.state = "nothing";
}
}
}
const revealer = new Revealer(output);
button.addEventListener("click", () => {
if (input.value == "") {
error.style.opacity = '1.0';
} else {
error.style.opacity = '0.0';
// Use simple call
revealer.reveal(input.value);
}
});
.word {
color: #ffe100;
}
<input><button>Submit</button>
<div>
<output></output>
</div>
<p id="error">Please submit (actual) text.</p>
Ideas for practicing
While the above code works, there are still ways to improve it or implement it differently:
Only delete until the remaining text is the same as the leading substring of the text-to-add.
You can use promises (with async/await) instead of using setTimeout() directly.
You can implement the revealing as functionality of a custom element.
...
Try implementing (one of) these as practice!
I just started learning JavaScript, and I am trying to get two different EventListeners to do the opposite function. Right now, I have it so that you can click on a mark and then a word, letter, or space and the mark appears/disappears there. I want to be able to click a word, letter, or space then click on a mark to add/remove it. Sentence Punctuation Marks I've tried switching the .querySelector and .querySelectorAll, but that doesn't work. Any help would be much appreciated.
var classlist = el.querySelectorAll(".letter, .space");
for (var i = 0; i < classlist.length; i++) {
classlist[i].addEventListener("click", function(event) {
var element = this.parentNode;
console.log(this);
if (
tooltype === "longVowel" ||
tooltype === "shortVowel" ||
this.classList.contains("space") === true
) {
element = this;
console.log(element);
}
var add = true;
var checklist = element.querySelectorAll("." + tooltype);
if (checklist.length > 0) {
add = false;
}
socket0s6.emit("action", {
slide: demo6,
action: "markingWords",
element: element.id,
class: tooltype,
add: add,
});
});
}
var classlist = el.querySelector(".toolbar").querySelectorAll("li");
for (var i = 0; i < classlist.length; i++) {
classlist[i].addEventListener("click", function(event) {
tooltype = this.className;
});
}
document.getElementById("slideContainer").appendChild(el);
/*}
//var oReq = new XMLHttpRequest();
//oReq.addEventListener("load", reqListener);
//oReq.open("GET", "slides/lesson0/slide6.html");
//oReq.send();*/
/*************** Server action ******************/
//socket0s6.on('action', function(details) {
var socket0s6 = {
emit: function(a, details) {
// //console.log(details[1]);
if (details.slide === demo6 && details.action === "markingWords") {
//document.getElementById(details.element).classList.toggle("active");
if (details.add) {
var el = document.createElement("span");
el.className = details.class;
document.getElementById(details.element).appendChild(el);
} else {
//where we will put code to find and remove existing marks
var markList = document
.getElementById(details.element)
.querySelectorAll("." + details.class);
console.log(markList);
for (var i = 0; i < markList.length; i++) {
markList[i].parentNode.removeChild(markList[i]);
}
}
}
},
};
I'm tring to code a tic tac toe game by using buttons, and in my plan I want to store a button.innertext to be a variable so that I can use it to make the judgement of game result more convinient. Here's the code, for simplicity, I only put one button here
const b1 = document.getElementById("b1");
const b = [b1];
let b1T = b1.innerText;
let xo = true;
show();
function show() {
for(let i = 0; i < 9; i++) {
b[i].addEventListener("click",go)
}
}
//decide x or o is displayed
function go(event) {
if(xo) {
event.target.innerText = "X";
event.target.disabled = true;
xo = false;
} else {
event.target.innerText = "O";
event.target.disabled = true;
xo = true;
}
}
function decide() {
if(b1T === "X"){
//do something
}
}
When I tried to write the part in function decide, I noticed that b1T didn't change it's value to "X" or "O", by console.log(b1T) I only got empty like I setted in HTML file <button id = "b1"><button>, but when I typed console.log(b1.innerText) it did show the innerText of it. Am I settig the biT variable in the wrong place or something else?
Get the text from your button inside the decide function instead. Your button is changing it's inner text when you press it, so you need to get the latest text (not the oldest one).
function decide() {
const b1 = document.getElementById("b1");
if(b1.innerText === "X"){
//do something
}
}
This is happening because the code on line number 2, i.e.
let b1T = b1.innerText
is run only during page load. You need to set that again to either "X" or "O" when handling the click events.
There is code error inside show(). ReferenceError: Can't find variable: b
Once above issue is fixed and b[i] is replaced with b1, its working fine.
Please check below code snippet.
const b1 = document.getElementById("b1");
const btn = document.getElementById("btn");
let b1T = b1.innerText;
let xo = true;
show();
function show() {
for(let i = 0; i < 9; i++) {
b1.addEventListener("click",go)
}
}
//decide x or o is displayed
function go(event) {
if(xo) {
event.target.innerText = "X";
event.target.disabled = true;
xo = false;
} else {
event.target.innerText = "O";
event.target.disabled = true;
xo = true;
}
}
function decide() {
if(b1T === "X"){
//do something
}
}
<body>
<button id="b1">Click</button>
</body>
Let me know if this is what expected.
My solution is to create a dc variable to record how many buttons are pressed, and put the function decide() into function go(), and declare b1T inside the function decide() so that everytime I click the button it will update all the variables that store button.innertext.
Here's the code:
const b1 = document.getElementById("b1");
const btn = document.getElementById("btn");
let b1T = b1.innerText;
let dc = 0;
let xo = true;
show();
function show() {
for(let i = 0; i < 9; i++) {
b1.addEventListener("click",go)
}
}
//decide x or o is displayed
function go(event) {
if(xo) {
event.target.innerText = "X";
event.target.disabled = true;
dc += 1;
xo = false;
} else {
event.target.innerText = "O";
event.target.disabled = true;
dc+= 1;
xo = true;
}
decide();
}
function decide() {
let b1T = b1.innerText;
if(dc >= 5) {
if(b1T === "X"){
//do something
}
}
}
Thank all you guys' efforts
I am comparing two attributes of the same object to work out which one is larger, if one is larger then it sets another attribute to True.. else it sets it to false.
Here is my function:
country.prototype.cure = function(){
for (var i = 0; i<this.diseases.length; i++)
{
console.log(this.health);
console.log(this.diseases[i].cureLevel);
if (this.heatlh >= this.diseases[i].cureLevel)
{
this.diseases[i].cured = true;
createDiseaseTable();
}
else
{
this.diseases[i].cured = false;
}
}
}
NOTE: this.health = 39000000 and this.diseases[i].cureLevel = 2500000
The problem is that whenever I run the function, despite this.health being larger it will always miss the if and go straight to the else...
Try this:
country.prototype.cure = function(){
for (var i = 0; i<this.diseases.length; i++)
{
var a=parseInt(this.health);
var b=parseInt(this.diseases[i].cureLevel);
if (a >= b)
{
this.diseases[i].cured = true;
createDiseaseTable();
}
else
{
this.diseases[i].cured = false;
}
}
}
I'm dealing with a ball-of-mudd project that uses frames & iframes to create a "customizable" interface (like people did in 2002).
The application runs from within a hta and kind of emulates a real WPF style app. I need to capture keys so I can selectively change/refresh some of the subframes.
What I'm trying to do is, if there was a sub-sub frame called desktop and that had some frames in it how would I capture an event, safely, across all frames; and refresh a frames subframes?
Any help appreciated; I accept no responsibility for nausia caused by repeating the last paragraph too many times. :)
Answering to get the formatting
arrFrames[i].document.onkeypress = function(){
var evtobj = window.event ? event : e;
evtobj.cancelBubble = true;
if (evtobj.stopPropagation){ evtobj.stopPropagation();}
top.console.log(evtobj.type+' - '+(evtobj.which?evtobj.which:evtobj.keyCode));
};
I don't know anything about HTA, but the question is marked as javascript / jquery / iframe, so i'll guess it isn't a problem...
You can use an object in window.top to manage your events in a centralized place.
In your main window, you use something like:
var getTopObject = function() {
return window.top.topObject;
}
var TopClass = function () {
this.processClick = function (frame) {
//do something...
alert('click in ' + frame.document.location.toString());
var msj = frame.document.getElementById("msj");
msj.innerHTML = "change!";
};
}
window.top.topObject = new TopClass();
And then, on every iframe, you put:
window.onclick = function () { getTopObject().processClick(window); };
That way you get notified of the click event.
Also note that inside the 'processClick' function in the example you can access the iframe document.
Of course, you can do this a lot more complex, but that's the basic idea. You will have to deal with different events in your case.
Hope this helps, and sorry for my english!
Working; digs through the frames in a loop using a function calling itself; I limited it to 8 rather as I know thats the deepest it will get. You can always change that yourself.
var XXX_util_keyboard = function()
{
//"private" variables:
var objTopWindow = top.window.frames,
arrFrames = [],
MaxDepth = 8;
//"private" methods:
var AddToArray = function(obj){
if(typeof obj.document != "undefined") {
arrFrames.push(obj);
return true;
}
return false;
};
var DeleteFromArray = function(obj){
if(typeof obj != "undefined") {
arrFrames.splice(arrFrames.indexOf(obj), 1);
return true;
}
return false;
};
var FrameLoop = function(objFrames){
if(MaxDepth > 0){
if(objFrames !== null)
{
for(var k = 0; k < objFrames.frames.length; k++)
{
var tmp = objFrames.frames[k];
AddToArray( tmp );
FrameLoop( tmp );
}
this.MaxDepth--;
}
}
};
var AttachEvent = function(key, fn) {
for(var i = 0; i < arrFrames.length; i++){
arrFrames[i].document.onkeypress = function(e) {
var evt = e || window.event,
charCode;
if(evt === null){ evt = this.parentWindow.event; /*IE doesnt capture scope correctly*/ }
charCode = evt.keyCode || evt.which;
alert(charCode);
evt.cancelBubble = true;
if (evt.stopPropagation){ evt.stopPropagation();}
};
}
};
return {
init: function()
{
AddToArray(this.getTopWindow()[0]);
FrameLoop(this.getTopWindow()[0]);
},
getFrames: function()
{
if(arrFrames.length < 1){ FrameLoop(objTopWindow[0]); }
return arrFrames;
},
getTopWindow: function()
{
return objTopWindow === undefined ? window.frames : objTopWindow.window.frames;
},
attachEvent: function()
{
if(arrFrames.length < 1){ FrameLoop(objTopWindow[0]); }
AttachEvent();
}
};
}();