I'm trying to create a dynamic function using a for-loop in javascript, which will fire rollovers. I am using JS vs. CSS as the amount of images which will be rollovers is growing, and I figure a function is easier to maintain than x number of selectors.
This is creating a on method similar to jQuery.
var on = function(event, elem, callback, capture) {
if (typeof elem === 'function') {
capture = callback;
callback = elem;
elem = window;
}
capture = capture ? true : false;
elem = typeof elem === 'string' ? document.querySelector(elem) : elem;
if (!elem) return;
elem.addEventListener(event, callback, capture);
};
These are my rollOver and rollOut functions:
function rollOver(elem) {
document.getElementById(elem).src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.slice(length-1)}.jpg?$staticlink$`
}
function rollOut(elem) {
document.getElementById(elem).src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_NO_HOVER_${elem.slice(length-1)}.jpg?$staticlink$`
}
And this is where my for-loop lives:
document.addEventListener("DOMContentLoaded", function(event) {
var rollOverCollectionA = document.getElementById('roll-over-collection-a').querySelectorAll('img');
rollOverCollectionA = Array.prototype.slice.apply(rollOverCollectionA);
for (var i = 0; i < rollOverCollectionA.length; i++) {
on('mouseover', rollOverCollectionA[i].id, function(){
console.log( rollOverCollectionA[i].id)
rollOver(rollOverCollectionA[i].id);
});
on('mouseout', rollOverCollectionA[i].id, function(){
rollOut(rollOverCollectionA[i].id);
});
}
});
The main problems I saw were:
elem.slice(length - 1); should be elem.slice(elem.length - 1) otherwise you're subtracting 1 from undefined
elem.slice should be replaced with elem.substr(elem.lastIndexOf('-') + 1) otherwise any images over 9 will start back at 0 because you would only get the last character of the id.
When you pass a string as elem to on, it uses document.querySelector, but you pass the id without the hash symbol (#). You don't need this anyway as you already have a reference to the image element, you can just pass that.
I also tidied it up and modernized it a little bit.
The glaring problem I neglected to mention was the use of var and the for(;;) loop. Thanks to #tranktor53 for pointing that out. I always instinctively replace for(;;) loops with for...in or for...of loops where I see them, and var with let or const, I didn't even notice that it was part of the problem.
function on({ type, element = window, callback, capture = false }) {
if (typeof element === 'string') element = document.querySelector(element);
if (!element) return;
element.addEventListener(type, callback, capture);
};
function rollOver({ element, id }) {
element.src = `https://via.placeholder.com/200x100?text=${ id }+hover`;
}
function rollOut({ element, id }) {
element.src = `https://via.placeholder.com/200x100?text=${ id }+no+hover`;
}
document.addEventListener("DOMContentLoaded", _ => {
const elements = document.querySelectorAll('#roll-over-collection-a img');
for(let element of elements) {
const id = element.id.substr(element.id.lastIndexOf('-') + 1);
on({ type: 'mouseover', element, callback: _ => rollOver({ element, id }) });
on({ type: 'mouseout' , element, callback: _ => rollOut({ element, id }) });
}
});
<div id="roll-over-collection-a">
<img id="roll-over-1" src="https://via.placeholder.com/200x100?text=1+no+hover">
<img id="roll-over-2" src="https://via.placeholder.com/200x100?text=2+no+hover">
<img id="roll-over-3" src="https://via.placeholder.com/200x100?text=3+no+hover">
<img id="roll-over-4" src="https://via.placeholder.com/200x100?text=4+no+hover">
<img id="roll-over-5" src="https://via.placeholder.com/200x100?text=5+no+hover">
<img id="roll-over-6" src="https://via.placeholder.com/200x100?text=6+no+hover">
</div>
The main problem is using var in the for loop and assuming that the value of i seen in the event handler will match that of i when th call back function was created. This is incorrect, since i will have the value it reached when the for loop completed, at a later time when the handler gets executed.
In current browsers the solution is to use let instead of var and hence starting the loop as
for (let i = 0; i < rollOverCollectionA.length; i++) ...
For discussion and older solutions see JavaScript closure inside loops – simple practical example
In regards the image source string calculation
/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.slice(length-1)}.jpg?$staticlink$
please review what is needed - if you need to copy the entire value of elem as a string, don't include the call to slice. If a part of the string is required, the posted code may be correct. Slicing from a start index of elem.length-1 will copy the last letter of the element id (of course) and may also be correct.
Update: The need to capture the value of i during loop iteration can be eliminated in at least two ways:
In the mouseover and mouseout event handlers replace rollOverCollectionA[i]with this. There is no need for reverse lookup of an HTML collection based on a captured index value to determine the element an event handler is attached to.
Use event delegation with a single listener attached to on the images' DOM container. Using the same on function, and elem.id.slice(length-1) as a possible image source suffix, similar to:
document.addEventListener("DOMContentLoaded", function(event) {
var rollOverCollectionA = document.getElementById('roll-over-collection-a');
on('mouseover', rollOverCollectionA, function( event){
var elem = event.target;
if( elem && elem.id && elem.tagName === 'IMG') {
elem.src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_HOVER_${elem.id.slice(length-1)}.jpg?$staticlink$`;
}
});
on('mouseout', rollOverCollectlonA, function(event) {
var elem = event.target;
if( elem && elem.id && elem.tagName === 'IMG') {
elem.src = `/images/home-page/desktop/EYES_ON_YOU_desktop_HP_NO_HOVER_${elem.id.slice(length-1)}.jpg?$staticlink$`;
}
});
});
Related
This is the prompt:
In the event listener DOMContentLoaded set up two addEventListener methods for each of the second input elements (using the array of inputElements[] and the corresponding index) and passing the first and the second
value (use inputElements[].value) and the appropriate span element (using spanElements[]). Since you need to pass arguments, use the following construct:
inputElements[index].addEventListener('blur',function(){ fCompareInput(arguments); })
This is the Function:
function fCompareinput(value1,value2,display)
This is what I have:
inputElements[2].addEventListener('blur', function() {
fCompareInput(inputElements[2].value);
});
inputElements[2].addEventListener('blur', function() {
fCompareInput(spanElements[2].value);
});
inputElements[4].addEventListener('blur', function() {
fCompareinput(inputElements[4].value);
});
inputElements[4].addEventListener('blur', function() {
fCompareinput(spanElements[4].value);
});
I don't think I'm passing the arguments correctly.
It's simply easier to go through your inputElements collection using a loop, and then bind the event listener. Also:
You can place multiple function calls in the blur callback
Use this in the callback to refer to the DOM node that has triggered the event
Let's say you only want the element with the index of 2 and 4 to have such events bound, then we can do this:
// Define which indexes you want to bind eventistener to
var indexes = [2,4];
// Loop through the entire collection
for (let i = 0; i < inputElements.length; i++) {
if (indexes.indexOf(i) !== -1) {
inputElements[i].addEventListener('blur', function() {
fCompareinput(this.value);
fCompareinput(spanElements[i].value);
});
}
}
Supplementary:
If you want to retrieve all even numbered element (by index) in the collection, use:
if (i % 2 === 0)
If you want to retrieve all even numbered elements and excluding the first element, use:
if (i % 2 === 0 && i !== 0)
For example:
// Loop through the entire collection
for (let i = 0; i < inputElements.length; i++) {
if (i % 2 === 0) {
inputElements[i].addEventListener('blur', function() {
fCompareinput(this.value);
fCompareinput(spanElements[i].value);
});
}
}
Since you have tagged your question with jQuery, there is also a jQuery-based solution: much simpler, but of course incurs an overhead of having to load a library.
var indexes = [2,4];
$(inputElements).each(function(i) {
if (indexes.indexOf(i) !== -1) {
$(this).on('blur', function() {
fCompareinput(this.value);
fCompareinput($(spanElements).get(i).val());
});
}
});
...or, if you want to select even numbered indexes only:
$(inputElements).each(function(i) {
if (i % 2 === 0) {
$(this).on('blur', function() {
fCompareinput(this.value);
fCompareinput($(spanElements).get(i).val());
});
}
});
So, I have this little code in my js file:
window.onload = function Equal() {
var a = 'b1'
var b = 'box1'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
var a = 'b2'
var b = 'box2'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
The function itself is not important (it equals checkboxvalues set in the localstorage), but I execute it 2 times. First time with var a & b set to 'b1' & 'box1'. Then I run the script again (same script), but with var a & b set to 'b2' & 'box2'. Now, this code works, but my question is if there is a shorter way to write this? I can imagine some sort of array with a loop, but I could not get it to work for some reason. The 2 variables are pairs, and I know this might be a dumb question, but I can't find the answer anywhere.
You can use a second function which will accept the local storage key and the checkbox id like
window.onload = function Equal() {
setCheckboxState('box1', 'b1');
setCheckboxState('box2', 'b2');
}
function setCheckboxState(id, key) {
document.getElementById(id).checked = 1 == localStorage.getItem(key);
}
You might separate common logic into another function
window.onload = function Equal() {
function extractFromStorage(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
extractFromStorage('b1', 'box1');
extractFromStorage('b2', 'box2');
}
function doTheStuff(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
window.onload = function Equal() {
doTheStuff('b1', 'box1');
doTheStuff('b2', 'box2');
}
?
This is how I would do it.
There are several problems with your code.
You do not check that the element you are stetting an attribute to
exists. You do not check if the localStorage item you get is
defined.
You pollute the global name space with the function name Equal.
That function should not be named with a capital as it is not a Object generator.
There is no need to use setAttribute and removeAttribute, in
fact removeAttribute makes no sense in this case as you can not
remove the checked attribute from the element. BTW why use setAttribute here and not for window.onload?
The checked attribute is either true or false, it does not use the
string "checked"
Binding the load event via the onload attribute is not safe as you may
block 3rd party code, or worse 3rd party code may block you.
There is no error checking. DOM pages are dynamic environments, pages
have adverts and content from many places that can interfer with your
code. Always code with this in mind. Check for possible errors and deal with them in a friendly way for the end user. In this case I used an alert, not friendly for a normal user but for you the coder.
My solution.
// add an event listener rather than replace the event listener
window.addEventListener(
"load", // for the load event
function(){
// the update function that is called for each item;
var update = function(item){
// the right hand side equates to true if the localstorage
// is equal to "1". LocalStorage allways returns a string or
// undefined if the key is not defined.
item.element.checked = localStorage[item.storageName] === "1";
}
// safe element getter
var getElement = function(eId){
var e = document.getElementById(eId); // try and get the element
if(e === null){ // does it exist?
throw "Missing element:"+eId; // no then we can not continue
// the program stops here unless
// you catch the error and deal with
// it gracefully.
}
return e; //ok return the element.
}
// Item creator. This creates a new item.
// sName is the local storage name
// eId id the element ID
var item = function(sName, eId){
return {
storageName: sName, // set the loaclStorage name
element:getElement(eId); // get the element and check its safe
};
}
// make it all safe
try{
// create an array of items.
var items = [
item("b1","box1"),
item("b2","box2")
];
// for each item update the element status
items.forEach(update);
}catch(e){
alert("Could not update page?");
}
}
);
When I have the conditionals inside of an event listener inside of a function, they don't seem to work properly [I commented them in]. In fact they don't work at all, but when I delete the conditionals everything works (except it works incorrectly because I need the conditionals) I'm a lot more familiar with Java if that helps in the explanation.
var board = document.getElementById("checkersBoard");
var isRedHighlighted = false;
var isBlackHighlighted = false;
var lastClick;
board.addEventListener("click", function(e)
{
//alert(e.target.src);
if(isBlackHighlighted || isRedHighlighted)
{
if(isBlackHighlighted)
{
e.target.src= "Directory/BlackPiece.png";
lastClick.src = "Directory/BlankSpace.png";
isBlackHighlighted = false;
}
if(isRedHighlighted)
{
e.target.src= "Directory/RedPiece.png";
lastClick.src = "Directory/BlankSpace.png";
isRedHighlighted = false;
}
}
else
{
if (e.target.src == "Directory/BlackPiece.png") // why does this not work
{
e.target.src = "Directory/BlackPieceH.png"
lastClick = e.target;
isBlackHighlighted = true;
}
if (e.target.src == "Directory/RedPiece.png") // why does this not work
{
e.target.src = "Directory/RedPieceH.png"
lastClick = e.target;
isRedHighlighted = true;
}
}
});
Judging by the strings you're comparing against, such as: "Directory/BlackPiece.png", it seems that you're intending to compare the variable(s) against the value of the src attribute (e.target.getAttribute('src')), rather than the src property (e.target.src).
This would yield a comparison such as, for example:
e.target.getAttribute('src') = "Directory/BlackPiece.png";
The difference between the two is that the src attribute looks at the attribute-value found within, and retrieves the value from, the specified attribute.
Whereas the src property resolves to an absolute URL, which would give a value similar to: http://www.example.com/Directory/BlackPiece.png.
References:
Element.getAttribute.
HTMLImageElement.src.
Is there a DOM event that fires when an element's parentElement changes? If not, is there any way better than polling with a timeout?
I'm specifically interesting in knowing when the parentElement changes from null to some defined element. That is, when a DOM element is attached to the document tree somewhere.
EDIT
Given the questions in the comments, here is an example that shows how to create an element with a null parentElement:
var element = document.createElement('div');
console.assert(element.parentElement == null);
The parent is only set once it's added to the DOM:
document.body.appendChild(element);
console.assert(element.parentElement != null);
Note too that elements created using jQuery will also have a null parent when created:
console.assert($('<div></div>').get(0).parentElement == null);
Afaik there's no such "parent listener".
Yet, I found a hack that might be helpful. At least it's worth reading, since the idea is clever.
http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
He uses CSS #keyframes during the insertion and listens for the resulting animation event which tells him, that the element got inserted.
1) Such a parentElementHasChanged event doesn't exist.
2) The workaround PISquared pointed to would work but looks very strange to me.
3) In practise there is no need for such an event. A parentChange would only appear to an element if it's position in the DOM changes.To make this happen you have to run some code on the element doing this, and all that code has to use native parent.removeChild(),
parent.appendChild, parent.insertBefore() or parent.replaceChild() somewhere. The same code could run a callback afterwards so the callback would be the event.
4) You are building library code. The library could provide a single function for all DOM-insertions/removals, which wraps the four native functions and "triggers the event". That's the last and only what comes in my mind to avoid a frequently lookup for parentElement.
5) If there's a need to include the native Event API, you may create a parentChanged event with CustomEvent
element.addEventListener('parentChanged', handler); // only when Event API needed
function manipulateElementsDOMPosition(element, target, childIndex, callback, detail) {
if (!target.nodeType) {
if (arguments.length > 4) return element;
detail = callback; callback = childIndex; childIndex = target; target = null;
}
if (typeof childIndex === 'function') detail = callback, callback = childIndex;
var oldParent = element.parentElement,
newParent = target,
sameParent = oldParent === newParent,
children = newParent.children,
cl = children.length,
ix = sameParent && cl && [].indexOf.call(children, element),
validPos = typeof childIndex === 'number' && cl <= childIndex;
if (childIndex === 'replace') {
(newParent = target.parentElement).replaceChild(element, target);
if (sameParent) return element;
} else {
if (samePar) {
if (!oldParent || ix == childIndex ||
childIndex === 'first' && ix === 0 ||
childIndex === 'last' && ix === (cl - 1)) return element;
oldParent.removeChild(element);
} else if (oldParent) oldParent.removeChild(element);
if (!cl || childIndex === 'last') {
newParent.appendChild(element);
} else if (childIndex === 'first') {
newParent.insertBefore(element, children[0])
} else if (validPos) {
newParent.insertBefore(element, children[childIndex]);
} else return element;
}
console.log(element, 'parentElement has changed from: ', oldParent, 'to: ', newParent);
element.dispatchEvent(new CustomEvent('parentChanged', detail)); // only when Event API needed
if (typeof callback === 'function') callback.call(element, oldParent, newParent, detail);
return element;
}
some example usage (detail may be anything you want to pass to the event/callback). Function always return element.
// remove element
manipulateElementsDOMPosition(element /*optional:*/, callback, detail);
// prepend element in target
manipulateElementsDOMPosition(element, target, 'first' /*optional:*/, callback, detail);
// append element in target
manipulateElementsDOMPosition(element, target, 'last' /*optional:*/, callback, detail);
// add element as third child of target, do nothing when less than two children there
manipulateElementsDOMPosition(element, target, 3 /*optional:*/, callback, detail);
// replace a target-element with element
manipulateElementsDOMPosition(element, target, 'replace' /*optional:*/, callback, detail);
You can use a MutationObserver like this:
const trackedElement = document.getElementById('tracked-element');
const parent1 = document.getElementById('parent1');
const parent2 = document.getElementById('parent2');
startObserver();
function changeParent() {
if (trackedElement.parentElement == parent1) {
parent1.removeChild(trackedElement);
parent2.appendChild(trackedElement);
} else {
parent2.removeChild(trackedElement);
parent1.appendChild(trackedElement);
}
}
function startObserver() {
let parentElement = trackedElement.parentElement
new MutationObserver(function(mutations) {
if (!parentElement.contains(trackedElement)) {
trackedElement.textContent = "Parent changed";
setTimeout(() => {
trackedElement.textContent = "Parent not changed";
}, 500);
startObserver();
this.disconnect();
}
}).observe(parentElement, {
childList: true
});
}
<!doctype html>
<html lang="en">
<body>
<div id="parent1">
<p id="tracked-element">Parent not changed</p>
</div>
<div id="parent2"></div>
<button onclick="changeParent()">ChangeParent</button>
</body>
</html>
You can right click on the Parent not changed text and click inspect to make sure that it is actually changing parents
If you want to know how the .observer function works, there's VERY good documentation on it here: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
I have an array of objects called targets and I want to execute a function on each of those objects. The first method:
targets.each(function() {
if (needScrollbars($(this))) {
wrap($(this), id);
id = id + 1;
}
});
This method gives execution speed of ~125ms. The second method is:
var i=0;
while (targets[i] != undefined) {
if (needScrollbars($(this))) {
wrap($(this), id);
id = id + 1;
}
i = i+1;
}
This second method takes whopping 1385ms to execute and I get my head around that. Does anyone have any idea why a bare bones cycle runs slower than a function which I'm only guessing that's doing (just guessing) a whole lot more than a simple cycle?
Thank you.
They are totally different. The this in the first example is the current target, in the second example this is the "external" this. You should change the second example as:
var i=0;
while (targets[i] != undefined) {
var cur = $(targets[i]);
if (needScrollbars(cur)) {
wrap(cur, id);
id = id + 1;
}
i = i+1;
}
The relevant quote
More importantly, the callback is fired in the context of the current DOM element, so the keyword this refers to the element.
But I don't know why you haven't written as:
for (var i = 0; i < targets.length; i++)
{
var cur = $(targets[i]);
if (needScrollbars(cur)) {
wrap(cur, id);
id = id + 1;
}
}
And in the end the each "method" is easier to comprehend (for me).
Your second method is not functionally equivalent to the first one.
Why? Because it uses this, making it a closure on the global scope. Of course the second method is slower: it continuously shells out jQuery objects made out of global scope. Try that benchmark again with:
var i=0;
while (targets[i] !== undefined) {
var o = $(targets[i]);
if (needScrollbars(o)) {
wrap(o, id);
id++;
}
i++;
}