I'm writing a simple text-base game in my free time. It's called TARDIS flight (Doctor Who!) and I'm working on the main menu.
So I'm using a function, addMainMenuListeners, to add all the event listeners with addEventListener, after I set the innerHTML of the main menu.
Everything works fine, until the point where I go back to the main menu from one of the submenus. Then, I found that the buttons don't work anymore.I'm calling addMainMenuListeners after I set the innerHTML, but even though I do it, and I do it in the console, and I check, there is no event.
Code:
In my main javascript file:
function addMainMenuListeners()
{
if($("start")) $("start").addEventListener("click", startGame);
if($("instructions")) $("instructions").addEventListener("click", instructions);
if($("settings")) $("settings").addEventListener("click", settings);
if($("back")) $("back").addEventListener("click", resetMainMenu);
if($("volume")) $("volume").addEventListener("change", function(){saveAndChangeVolume($("volume").value);});
}
function instructions()
{
$("mainmenu").innerHTML = "<h1>Instructions</h1><p>Fly your TARDIS through the time vortex using the console. Make sure that you use the correct materialization codes. Try to keep the time-space coordinates close to the values of the ones given. AND DON'T CREATE PARADOXES, WHATEVER YOU DO!</p><button id='back'>Back</button>";
addMainMenuListeners();
return true;
}
function settings()
{
$("mainmenu").innerHTML = "<h1>Volume</h1><span>1</span><input type='range' id='volume' min='1' max='100' /><span>100</span><br><button id='back'>Back</button>";
addMainMenuListeners();
loadVolume();
return true;
}
function resetMainMenu()
{
$("mainmenu").innerHTML = "<h1>TARDIS Flight</h1><button id='start'>Start!</button><button id='instructions'>Instructions</button><button id='settings'>Settings</button>";
addMainMenuListeners();
return true;
}
And in my HTML file:
<div id="mainmenu">
<h1>TARDIS Flight</h1>
<button id="start">Start!</button>
<button id="instructions">Instructions</button>
<button id="settings">Settings</button>
</div>
If you need any clarification, just ask.
EDIT: Evidentally, nobody got what I meant. I was readding the event listeners after doing the innerHTML, as you can see from the code. I simply cannot see the event being added, the function is firing but not adding the event.
Also, I am using a custom $ function, just a return document.getElementById(id) sort of function.
Check to see if your $() uses any caching. If it caches old references to elements then when innerHTML is set the $("id") will return a reference to an invalid reference.
[edit] The references are more likely valid even though they are no longer visible in the HTML DOM. So modifying the detached elements works but it doesn't do any good since they are detached from the DOM.
Sukima psychic ability's catched the main problem: your custom $ function (to replace document.getElementById) used a caching mechanism.
After some testing (out of personal curiosity) it turned out that as long as you have some reference to an element, the element is still accessible, even after elm.parentNode.removeChild or a full elm.parentNode.innerHTML rewrite (at least, in FF).
In other words: the events WERE added, every time, just to the wrong/old/previous elements instead of the new ones. Otherwise there would also have been errors that the elements didn't exist and thus didn't have an addEventListener method..
See this crude test-fiddle for proof: http://jsfiddle.net/G28Lu/
I toyed around with a $ function (as you haven't posted yours yet) and gave it an 'refresh'-flag to go with the optional cache mechanism:
var $=function(/*cache_enabled*/c){ // getElementById with optional caching & refresh
return c ?( c={}, function(s,r){return (!r && c[s]) || (c[s]=document.getElementById(s));}
):( function(s){return document.getElementById(s);} );
}(true); // or nothing or 0 etc to disable the cache
Note: dynamically created functions are slow (for some reason) so it has 2 functions so the browser can run them optimized (and the other should be cleared by the garbage collector).
To request a fresh just add a flag that evaluates to true as second argument, like: $('elm_id', 1)
Then I modified your addMainMenuListeners function a little to first test for the existence of a fresh getElementById and then add the eventListener via the freshly updated cached reference (so, essentially I changed nothing in the flow of your routine).
formatted to emphasize what changed and how it works
function addMainMenuListeners(){
$( 'start' , 1) && $( 'start' ).addEventListener('click', startGame);
$('instructions', 1) && $('instructions').addEventListener('click', instructions);
$( 'settings' , 1) && $( 'settings' ).addEventListener('click', settings);
$( 'back' , 1) && $( 'back' ).addEventListener('click', resetMainMenu);
$( 'volume' , 1) && $( 'volume' ).addEventListener('change', function(){
saveAndChangeVolume($('volume').value);
});
}
Finally, putting above 2 functions and the rest of your functions/html into this fiddle rendered a working result: http://jsfiddle.net/xHUGu/ !
Note: I had to substitute a dummy function startGame otherwise there would be a fatal error. The missing volume-functions were not critical.
I would like to point out that this is not really the way to go with your interface, there would be a lot of work you could save both yourself and the browser. You might want to look into div's (containing your subsections of html) and toggle them so there is only one visible. Like tabs. (Hint for google-query).
Credit still goes to Sukima ('the force is strong in this one'), but I felt it was a good idea to share the correct explanation to your problem (with proof) and not to waste the work that was done anyway (out of curiosity).
Hope this helps!
Disabling caching on the $ function worked. It was referencing to a destroyed HTML element, and that's why it didn't work. Also, setTimeouts helped for reliability in the case that innerHTML didn't execute in time.
Related
I'm trying to have .child_box class to open and close slow but seems Local Storage is not respecting it. Either it wont open or it wont close. Without Local Storage, it works fine. Stumped.
The js:
$("document").ready(function () {
$(".manualclose").click(function () {
$(".child_box").toggle();
});
ls = localStorage.getItem('on')
if(ls) {
$(".child_box").show("slow")
}
$(".open_child").click(function () {
localStorage.setItem('on',true)
toggled = $(".child_box").toggle();
if(toggled.is(":hidden")) {
localStorage.clear();
}
});
$(".manualclose").click(function() {
localStorage.clear();
$(".child_box").hide("slow")
});
});
The button:
<div class="open_child" title="', $txt['sub_boards2'], '">
<i class="fas fa-plus-circle"></i>
</div>
localStorage isn't conflicting with toggle(). The problem is down to the way the browser schedules a reflow whilst executing JavaScript.
In this event handler
$(".open_child").click(function(){
localStorage.setItem('on',true)
toggled = $(".child_box").toggle(500);
if(toggled.is(":hidden")){
localStorage.clear();
}
});
your code toggles the .child-box element. It immediately goes to see if that element is now hidden.
The browser is running the animation that is caused by .toggle() and carries on executing the JavaScript. It checks whether the element is hidden, which it isn't because the animation hasn't completed yet, and so doesn't clear the localStorage. Only later when the animation completes would the element appear as 'hidden'.
You need to do things in a different order:
$(".open_child").click(function(){
let hidden =$(".child_box").is(":hidden");
if (hidden) {
$(".child_box").show(500);
localStorage.setItem('on',true)
} else {
$(".child_box").hide(500);
localStorage.removeItem('on');
}
});
This version checks the hidden status first, then shows or hides the element as required, and updates localStorage to match.
There is an alternative approach: use the complete function available to the jQuery .toggle() method to update localStorage. You'd still need to check to see what .toggle() has just done, so you don't gain much.
FWIW, I never use .toggle() precisely because I don't know what action it's performing.
A couple of other thoughts:
You're not declaring the variable you use, so they're being placed in the global context. This is a bad idea. Declare them in the functions they're used in with let.
localStorage stores strings, not other data type. JavaScript has coerced the data for you so you've got away with it, but good practice suggests that you should be more rigorous.
Using localStorage.clear() precludes the use of localStorage for any other purpose. Use localStorage.removeItem() instead.
I don't think it's localStorage. what i noticed:
a) you don't declare the variable ls.
b) why are you using a div as a button?
c) you don't use parse and stringify to get and set values in the localStorage.
d) you put a title in a DIV container
f) the title you set looks like PHP. it is missing the < ?php echo $text ... ;? > Tags
As I learn by "doing" I find myself venturing into uncharted territory with the document.getElementById and addEventListener functions. I am using these functions for a small image (button) that will have — based on various steps the user takes — states of a) disabled, b) enabled, c) mouseover, d) mouseout, e) swap image, and f) various cursor styles.
Yesterday I spent a couple of hours reading through Stack Overflow and MDN Web Docs to understand how all the pieces work together. I then built the code and it worked perfectly. That's the good news. The bad news is this morning I just happened to click a tab on a tabbar (that exists on the same page) and when I went back to the original tab all of the event listeners had dropped and I do not understand why. By choosing "document" as my main target for the getElementById, I thought I was safe in that the event listeners and global variable would be active as long I was within the "document." Could someone help me understand what I'm missing? Here are the various steps I've taken:
I first declare the global variable
window.elem_populate = 0;
Then in the window.onload I apply a value to the variable. BTW, I use window.onload because the ID for the target was not being created in time and was throwing an "undefined" error.
elem_populate = document.getElementById("toolbar_populate");
Then, depending on input from the user I use this approach:
elem_populate.addEventListener('mouseover', hover_populate_on);
elem_populate.addEventListener('mouseout', hover_populate_off);
Here are a couple of examples of the functions being called:
hover_populate_off = function hover_populate_off() {
elem_populate.style.opacity = "1.0";
elem_populate.style.cursor = "not-allowed";
}
hover_populate_on = function hover_populate_on() {
elem_populate.style.opacity = "0.8";
elem_populate.style.cursor = "pointer";
}
The actual target ("toolbar_populate") is positioned on Tab 1 of a two-tab tabbar. If I don't move off the tab, everything works magically. But when I move to Tab 2 and then go back to Tab 1 all the references are lost and the functionality stops. Because everything is on the same document I'm not understanding why I'm losing the reference. Any help or direction would be appreciated. Thanks in advance ...
So I got a little codepen. Everything works so far except a little thing. I got a <h1> and an <input>. When I type something in the text input, its value should get passed to the <h1> in realtime.
I tried to do that with a keyup function:
$('input[name=titleInput]').keyup(function(){
$('#title').text(this.value);
});
Something happens, but not what I want.When I type something in the text input, then delete it (with backspace) and re-enter something, only the first character gets passed to the title.Try it out on my codepen. Maybe it's just a stupid mistake of mine, but to me this behaviour is pretty weird.Thanks for your help in advance!EDIT:I am using text-fill-color, which may causes the problem.EDIT 2:A friend of mine tested it. It worked for her. She's using Chrome and the same version as me (58.0.3029.110 (official build) (64-Bit)).
Chrome does not update the content correctly. Such kind of bugs can always happen if you use vendor prefixed css properties, so you should avoid those.
You could hide the container before update, and then show it again with a timeout. This will trigger an update, but would also result in flickering.
$('input[name=titleInput]').keyup(function(){
$('.clipped').hide()
$('#title').text(this.value);
setTimeout(function() {
$('.clipped').show();
})
});
EDIT An alternative might be to use background-clip on the text and provide the inverted image yourself, but I right now don't have time to test that.
EDIT2 Based on the test of #TobiasGlaus the following code does solve the problem without flickering:
$('input[name=titleInput]').keyup(function(){
$('.clipped').hide().show(0)
$('#title').text(this.value);
});
This seems to be different to $('.clipped').hide().show() most likely it starts an animation with duration 0 and uses requestAnimationFrame which also triggers a redraw. To not relay on this jQuery behaviour, the code should be written as:
$('input[name=titleInput]').keyup(function(){
if( window.requestAnimationFrame ) {
$('.clipped').hide();
}
$('#title').text(this.value);
if( window.requestAnimationFrame ) {
window.requestAnimationFrame(function() {
$('.clipped').show();
})
}
});
i'd use the following lines:
$('input[name=titleInput]').bind('keypress paste', function() {
setTimeout(function() {
var value = $('input[name=titleInput]').val();
$('#title').text(value);
}, 0)
});
This will listen to the paste / keypress events, and will update the value on change.
As soon as I try to inject raw html in the document's body, a saved instance of an element retrieved with .querySelector(); abruptly resets it's .clientHeight and .clientWidth properties.
The following page shows the problem
<head>
<script>
window.addEventListener('load', pageInit);
function pageInit() {
// saving instance for later use
var element = document.querySelector('#element')
alert(element.clientHeight); // returns 40
document.querySelector('body').innerHTML += '<p>anything</p>';
alert(document.querySelector('#element').clientHeight); // returns 40
alert(element.clientHeight); // returns 0
}
</script>
<style>
#element {
height: 40px;
}
</style>
</head>
<body>
<div id="element"></div>
</body>
Why exactly the instance properties of the element variable gets reset?
As pointed in this question addEventListener gone after appending innerHTML eventListeners gets detached but this still doesn't explain why that element node still exist and why it's properties were zeroed out.
It would seem that when you reset the innerHTML of the body element, you're causing the entire page to be re-rendered. Thus, the div#element that is shown on the page is not the same element that the JavaScript variable element points to; rather, the one you see is a new element that was created when the page was re-rendered. However, the div#element that element points to still exists; it hasn't yet been destroyed by the browser's garbage collector.
To prove this, try replacing the last alert (the one that alerts a 0) with console.log(element), and then attempting to click on the element in the console window. You'll see <div id="element"></div>, but when you click on it, nothing happens, since the div is not on the page.
Instead, what you want to do is the following:
const newEl = document.createElement('p');
newEl.innerHTML = 'anything';
document.querySelector('body').appendChild(newEl);
instead of setting the body.innerHTML property.
FYI you should probably switch the alerts for console.logs, since alerts annoy the hell out of people and the console is the way to test things out in development.
You're running into a problem where the browser has reflowed the doc and dumped its saved properties. Also known as Layout Thrashing. Accessing certain DOM properties (and/or calling certain DOM methods) "will trigger the browser to synchronously calculate the style and layout". That quote is from Paul Irish's Comprehensive list of what forces layout/reflow. Though innerHTML isn't included there, pretty sure that's the culprit.
In your sample, the first alert works for obvious reasons. The second works because you're asking the browser to go and find the element and get the property again. The third fails because you're relying on a stored value (that's no longer stored).
The simplest way around it is to use requestAnimationFrame when using methods/properties that will force a reflow.
window.addEventListener('load', pageInit);
function pageInit() {
// saving instance for later use
var element = document.querySelector('#element');
console.log("before:", element.clientHeight); // returns 40
requestAnimationFrame(function() {
document.querySelector('body').innerHTML += '<p>anything</p>';
});
console.log("WITH rAF after:", element.clientHeight); // returns ~0~ 40!
// out in the wild again
document.querySelector('body').innerHTML += '<p>anything</p>';
// oh no!
console.warn("W/O rAF after:", element.clientHeight); // returns 0
}
#element {
height: 40px;
}
<div id="element"></div>
The title of the question expresses what I think is the ultimate question behind my particular case.
My case:
Inside a click handler, I want to make an image visible (a 'loading' animation) right before a busy function starts. Then I want to make it invisible again after the function has completed.
Instead of what I expected I realize that the image never becomes visible. I guess that this is due to the browser waiting for the handler to end, before it can do any redrawing (I am sure there are good performance reasons for that).
The code (also in this fiddle: http://jsfiddle.net/JLmh4/2/)
html:
<img id="kitty" src="http://placekitten.com/50/50" style="display:none">
<div>click to see the cat </div>
js:
$(document).ready(function(){
$('#enlace').click(function(){
var kitty = $('#kitty');
kitty.css('display','block');
// see: http://unixpapa.com/js/sleep.html
function sleepStupidly(usec)
{
var endtime= new Date().getTime() + usec;
while (new Date().getTime() < endtime)
;
}
// simulates bussy proccess, calling some function...
sleepStupidly(4000);
// when this triggers the img style do refresh!
// but not before
alert('now you do see it');
kitty.css('display','none');
});
});
I have added the alert call right after the sleepStupidly function to show that in that moment of rest, the browser does redraw, but not before. I innocently expected it to redraw right after setting the 'display' to 'block';
For the record, I have also tried appending html tags, or swapping css classes, instead of the image showing and hiding in this code. Same result.
After all my research I think that what I would need is the ability to force the browser to redraw and stop every other thing until then.
Is it possible? Is it possible in a crossbrowser way? Some plugin I wasn't able to find maybe...?
I thought that maybe something like 'jquery css callback' (as in this question: In JQuery, Is it possible to get callback function after setting new css rule?) would do the trick ... but that doesn't exist.
I have also tried to separte the showing, function call and hiding in different handlers for the same event ... but nothing. Also adding a setTimeout to delay the execution of the function (as recommended here: Force DOM refresh in JavaScript).
Thanks and I hope it also helps others.
javier
EDIT (after setting my preferred answer):
Just to further explain why I selected the window.setTimeout strategy.
In my real use case I have realized that in order to give the browser time enough to redraw the page, I had to give it about 1000 milliseconds (much more than the 50 for the fiddle example). This I believe is due to a deeper DOM tree (in fact, unnecessarily deep).
The setTimeout let approach lets you do that.
Use JQuery show and hide callbacks (or other way to display something like fadeIn/fadeOut).
http://jsfiddle.net/JLmh4/3/
$(document).ready(function () {
$('#enlace').click(function () {
var kitty = $('#kitty');
// see: http://unixpapa.com/js/sleep.html
function sleepStupidly(usec) {
var endtime = new Date().getTime() + usec;
while (new Date().getTime() < endtime);
}
kitty.show(function () {
// simulates bussy proccess, calling some function...
sleepStupidly(4000);
// when this triggers the img style do refresh!
// but not before
alert('now you do see it');
kitty.hide();
});
});
});
Use window.setTimeout() with some short unnoticeable delay to run slow function:
$(document).ready(function() {
$('#enlace').click(function() {
showImage();
window.setTimeout(function() {
sleepStupidly(4000);
alert('now you do see it');
hideImage();
}, 50);
});
});
Live demo
To force redraw, you can use offsetHeight or getComputedStyle().
var foo = window.getComputedStyle(el, null);
or
var bar = el.offsetHeight;
"el" being a DOM element
I do not know if this works in your case (as I have not tested it), but when manipulating CSS with JavaScript/jQuery it is sometimes necessary to force redrawing of a specific element to make changes take effect.
This is done by simply requesting a CSS property.
In your case, I would try putting a kitty.position().left; before the function call prior to messing with setTimeout.
What worked for me is setting the following:
$(element).css('display','none');
After that you can do whatever you want, and eventually you want to do:
$(element).css('display','block');