First off, let me apologize if my question isn't worded correctly - I'm not a professional coder so my terminology might be weird. I hope my code isn't too embarrassing :(
I have a fade() method that fades an image in and out with a mouse rollover. I would like to use a wrapper object (I think this is the correct term), to hold the image element and a few required properties, but I don't know how to accomplish this. fade() is called from the HTML, and is designed to be dropped into a page without much additional setup (so that I can easily add new fading images to any HTML), just like this:
<div id="obj" onmouseover="fade('obj', 1);" onmouseout="fade('obj', 0);">
The fade(obj, flag) method starts a SetInterval that fades the image in, and when the pointer is moved away, the interval is cleared and a new SetInterval is created to fade the image out. In order to save the opacity state, I've added a few properties to the object: obj.opacity, obj.upTimer, and obj.dnTimer.
Everything works okay, but I don't like the idea of adding properties to HTML elements, because it might lead to a future situation where some other method overwrites those properties. Ideally, I think there should be a wrapper object involved, but I don't know how to accomplish this cleanly without adding code to create the object when the page loads. If anyone has any suggestions, I would greatly appreciate it!
Here's my fader method:
var DELTA = 0.05;
function fade(id, flag) {
var element = document.getElementById(id);
var setCmd = "newOpacity('" + id + "', " + flag + ")";
if (!element.upTimer) {
element.upTimer = "";
element.dnTimer = "";
}
if (flag) {
clearInterval(element.dnTimer);
element.upTimer = window.setInterval(setCmd, 10);
} else {
clearInterval(element.upTimer);
element.dnTimer = window.setInterval(setCmd, 10);
}
}
function newOpacity(id, flag) {
var element = document.getElementById(id);
if (!element.opacity) {
element.opacity = 0;
element.modifier = DELTA;
}
if (flag) {
clearInterval(element.dnTimer)
element.opacity += element.modifier;
element.modifier += DELTA; // element.modifier increases to speed up fade
if (element.opacity > 100) {
element.opacity = 100;
element.modifier = DELTA;
return;
}
element.opacity = Math.ceil(element.opacity);
} else {
clearInterval(element.upTimer)
element.opacity -= element.modifier;
element.modifier += DELTA; // element.modifier increases to speed up fade
if (element.opacity < 0) {
element.opacity = 0;
element.modifier = DELTA;
return;
}
element.opacity =
Math.floor(element.opacity);
}
setStyle(id);
}
function setStyle(id) {
var opacity = document.getElementById(id).opacity;
with (document.getElementById(id)) {
style.opacity = (opacity / 100);
style.MozOpacity = (opacity / 100);
style.KhtmlOpacity = (opacity / 100);
style.filter = "alpha(opacity=" + opacity + ")";
}
}
You are right, adding the handlers in your HTML is not good. You also loose the possible to have several handlers for event attached to one object.
Unfortunately Microsoft goes its own way regarding attaching event handlers. But you should be able to write a small wrapper function to take care of that.
For the details, I suggest you read quirksmode.org - Advanced event registration models.
An example for W3C compatible browsers (which IE is not): Instead of adding your event handler in the HTML, get a reference to the element and call addEventListener:
var obj = document.getElementById('obj');
obj.addEventListener('mouseover', function(event) {
fade(event.currentTarget, 1);
}, false);
obj.addEventListener('mouseout', function(event) {
fade(event.currentTarget, 0);
}, false);
As you can see I'm passing directly a reference to the object, so in you fade method you already have a reference to the object.
You could wrap this in a function that accepts an ID (or reference) and every time you want to attach an event handler to a certain element, you can just pass the ID (or reference) to this function.
If you want to make your code reusable, I suggest to put everything into an object, like this:
var Fader = (function() {
var DELTA = 0.05;
function newOpacity() {}
function setStyle() {}
return {
fade: function(...) {...},
init: function(element) {
var that = this;
element.addEventListener('mouseover', function(event) {
that.fade(event.currentTarget, 1);
}, false);
element.addEventListener('mouseout', function(event) {
that.fade(event.currentTarget, 0);
}, false);
}
};
}())
Using an object to hold your functions reduces pollution of the global namespace.
Then you could call it with:
Fader.init(document.getElementById('obj'));
Explanation of the above code:
We have an immediate function (function(){...}()) which means, the function gets defined and executed (()) in one go. This function returns an object (return {...};, {..} is the object literal notation) which has the properties init and fade. Both properties hold functions that have access to all the variables defined inside the immediate function (they are closures). That means they can access newOpacity and setStyle which are not accessible from the outside. The returned object is assigned to the Fader variable.
This doesn't directly answer your question but you could use the jQuery library. It's simple, all you have to do is add a script tag at the top:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js">
Then your div would look like:
<div id="obj" onmouseover="$('#obj').fadeIn()" onmouseout="$('#obj').fadeOut()">
jQuery will handle all the browser dependencies for you so you don't have to worry about things like differences between firefox and mozilla etc...
If you want to keep your HTML clean, you should consider using JQuery to set up the events.
Your HTML will look like this:-
<div id="obj">
Your JavaScript will look "something" like this:-
$(document).ready(function() {
$("#obj").mouseover(function() {
Page.fade(this, 1);
}).mouseout(function(){
Page.fade(this, 0);
});
});
var Page = new function () {
// private-scoped variable
var DELTA = 0.05;
// public-scoped function
this.fade = function(divObj, flag) {
...
};
// private-scoped function
var newOpacity = function (divObj, flag) {
...
};
// private-scoped function
var setStyle = function (divObj) {
...
};
};
I introduced some scoping concept in your Javascript to ensure you are not going to have function overriding problems.
Related
I have created the following click count factory for adding the click count to the event information already present in a regular click event. This function creates a clickCountObj to track the number of clicks, as well as a new function for catching a click event on the given element parameter and reporting it back to the listener parameter along with the click count.
Originally, I wanted to do this as a class, rather than a factory... Way back when I was working in Java, I would have done it with a façade class, so that's what I was thinking. But I've concluded that it is not possible in Javascript, because the same function you'd use to create the object would be the one called in response to the click, and I can't see a way around this.
The purpose of this question is simply to improve my understanding and using of JavaScript. Please let me know if I am wrong in my conclusion stated above, or if there are any other alternatives to doing this a better way?
function clickCount(element, listener) {
let clickCountObj = {};
clickCountObj.clickCount = 0;
clickCountObj.clickDelay = 500;
clickCountObj.element = element;
clickCountObj.lastClickTime = 0;
let clickCountListener = function (e) {
// alert("last click time: " + clickCountObj.clickDelay);
if ((e.timeStamp - clickCountObj.clickDelay) < clickCountObj.lastClickTime) {
clickCountObj.clickCount = clickCountObj.clickCount + 1;
// alert("click count up: " + clickCountObj.clickCount);
}
else {
clickCountObj.clickCount = 1;
}
clickCountObj.lastClickTime = e.timeStamp;
listener.call(element, clickCountObj.clickCount, e);
};
if (!element) throw "No element to listener to";
element.addEventListener("click", clickCountListener);
return clickCountListener;
}
For sure you can also use a class:
class ClickCounter {
constructor(element, onClick, delay = 500) {
this.element = element;
this.onClick = onClick;
this.counter = 0;
this.delay = delay;
this.lastClicked = 0;
element.addEventListener("click", () => this.click(), false);
}
click() {
if(Date.now() < this.lastClicked + this.delay)
return;
this.lastClicked = Date.now();
this.onClick.call(this.element, this.counter++);
}
}
new ClickCounter(document.body, count => {
alert(count);
});
[is] doing this a better way?
No, not really. Using a class is not really useful here as you don't want to expose properties and you also don't need inheritance. A factory seems to be a good approach here.
Small sidenote: Instead of
return clickCountListener;
it would make more sense to
return clickCountObj;
as it would expose the settings and the count which might be useful.
warning: unserious content below
Way back when I was working in Java ...
... you took over that senseless naming scheme (clickCountObj.clickCount). I guess you won't loose any necessary information with just settings.count ...
I have read countless of answers of this issue and I came up with the following, but it doesn't work either.
function fitToParent(objsParent, tagName) {
var parent, imgs, imgsCant, a, loadImg;
//Select images
parent = document.getElementById(objsParent);
imgs = parent.getElementsByTagName(tagName);
imgsCant = imgs.length;
function scaleImgs(a) {
"use strict";
var w, h, ratioI, wP, hP, ratioP, imgsParent;
//Get image dimensions
w = imgs[a].naturalWidth;
h = imgs[a].naturalHeight;
ratioI = w / h;
//Get parent dimensions
imgsParent = imgs[a].parentNode;
wP = imgsParent.clientWidth;
hP = imgsParent.clientHeight;
ratioP = wP / hP;
//I left this as a test, all this returns 0 and false, and they shouldn't be
console.log(w);
console.log(h);
console.log(ratioI);
console.log(imgs[a].complete);
if (ratioP > ratioI) {
imgs[a].style.width = "100%";
} else {
imgs[a].style.height = "100%";
}
}
//Loop through images and resize them
var imgCache = [];
for (a = 0; a < imgsCant; a += 1) {
imgCache[a] = new Image();
imgCache[a].onload = function () {
scaleImgs(a);
//Another test, this returns empty, for some reason the function fires before aplying a src to imgCache
console.log(imgCache[a].src);
}(a);
imgCache[a].src = imgs[a].getAttribute('src');
}
}
fitToParent("noticias", "img");
To summarise, the problem is the event onload triggers before the images are loaded (or that is how I understand it).
Another things to add:
I don't know at first the dimensions of the parent nor the child,
because they varied depending of their position on the page.
I don't want to use jQuery.
I tried with another function, changing the onload event to
window, and it worked, but it takes a lot of time to resize because
it waits for everything to load, making the page appear slower,
that's how I came to the conclusion the problem has something to do
with the onload event.
EDIT:
I made a fiddle, easier to look at the problem this way
https://jsfiddle.net/whn5cycf/
for some reason the function fires before aplying a src to imgCache
Well, the reason is that you are calling the function immedeatly:
imgCache[a].onload = function () {
}(a);
// ^^^ calls the function
You call the function and assign undefined (the return value of that function) to .onload.
If you want to use an IIFE to capture the current value of a, you have to make it return a function and accept a parameter to which the current value of a is assigned to:
imgCache[a].onload = function (a) {
return function() {
scaleImgs(a);
};
}(a);
Have a look again at JavaScript closure inside loops – simple practical example .
I am trying to create the fadeIn() function using Javascript. I am having trouble, when I click the fadeIn button, it does not perform a fadeIn animation, instead I have to click it several times to fadeIn. Would anyone know how I can fix this issue?
jsFiddle
// Created a jQuery like reference
function $(selector) {
if (!(this instanceof $)) return new $(selector); // if new object is not defined, return new object
this.selector = selector; // setting selector attribute
this.node = document.querySelector(this.selector); // finds single element from the DOM
};
var fInFrom = 0, fOutFrom = 10;
$.prototype.fadeIn = function() {
var target = this.node,
newSetting = fInFrom / 10;
// Set Default styles for opacity
target.style.display = 'block';
target.style.opacity = newSetting;
// fadeInFrom will increment by 1
fInFrom++;
var loopTimer = setTimeout('this.fadeIn', 50);
if (fInFrom === 10) {
target.style.opacity = 1;
clearTimeout(loopTimer);
fInFrom = 0;
return false;
}
return this;
}
$('#fadeIn').node.addEventListener('click', function() {
$('#box').fadeIn();
});
This line is your problem:
setTimeout('this.fadeIn', 50)
That will set a timeout to evaluate the expression this.fadeIn in the global scope in approximately 50 milliseconds from the current time. There's two problems with that:
It's in the global scope; this is window, not an instance of $, so this.fadeIn is undefined.
Even if it were resolved correctly, you're only evaluating this.fadeIn; you're not calling it. You would need to use this.fadeIn() for it to do anything. (If you do that with the current code, this will reveal your first problem.)
To solve this, pass not a string but a function that does what you want it to do. You might naïvely do this:
setTimeout(function() {
this.fadeIn();
}, 50);
Unfortunately, while we now have lexical scoping for variables, this in JavaScript is dynamic; we have to work around that. Since we do have lexical scoping for variables, we can utilize that: [try it]
var me = this; // store the current value of this in a variable
var loopTimer = setTimeout(function() {
me.fadeIn();
}, 50);
After that's solved, you might want to look into:
Not using global variables to hold the fade state. Even after that fix, running two fade animations at once on different elements won't work as expected. (Try it.)
Only setting the timeout if you need to; right now, you always set it and then clear it if you don't need it. You might want to only set it if you need it in the first place.
Background: I am trying to edit a zen cart horizontal pop out menu to make the popout open inline within the menu. The problem I am having is that I am struggling to get my head around the javascript/jquery that came with it.
Without posting the whole thing the structure of the code is something like this:
(declare some vars)
//some functions like this:
function funcname(obj) {
//do something
}
//then one big master function like this:
function bigfunc(arg1, arg2, arg3, arg4, arg5) {
//declare some vars based on this
this.varname1=varname1;
this.varname2=varname2;
//declare some functions inside the big function
this.innerfunc1= function() {
//do stuff
}
this.innerfunc2= function() {
//do stuff
}
}//end of big function
//then goes on to declare init function
function initfunc(){
//this creates new bigfunc(arg1 arg2 arg3...) for each main menu item
}
//finally calls init function with
window.onload = initfunc();
Now on to my confusion -
1) firstly for clarification, am I correct in thinking based on all the this's floating about in bigfunc() and the fact that it is called with new bigfunc() that this is creating an object?
2)My current problem is with one of the functions inside bigfunc() which looks like this:
this.slideChildMenu = function() {
var divref = this.children[0].div;
var ulref = this.children[0].ul;
var maxwidth = this.children[0].width;
var nextWidth;
if (this.isMouseOnMe || this.isMouseOnChild()) {
nextWidth = divref.offsetWidth + slideSpeed_out;
if (nextWidth >= maxwidth) {
this.finishOpeningChild(divref, ulref, maxwidth);
} else {
ulref.style.left = nextWidth - maxwidth + "px";
divref.style.width = nextWidth + "px";
setTimeout("slideChildMenu('" + this.getId() + "')", slideTimeout_out);
}
}
Now my plan is to alter this to use jquery show to open the element so I tried this:
this.slideChildMenu = function() {
var divref = this.children[0].div;
var ulref = this.children[0].ul;
if (this.isMouseOnMe || this.isMouseOnChild()) {
$(divref).show(function(){
this.finishOpeningChild(divref, ulref);
});
}
}
But I am getting this-> TypeError: this.finishOpeningChild is not a function
Now, there is a lot of other stuff going on in this js so I wouldnt dream of asking someone on here to do my work for me, but I am hoping that if someone can explain to me why this function is not a function I may be able to work the rest out.
NOTE: I thought this was to do with the scope of "this" but the value of this appears to be exactly the same in both versions of the code.
I know this is a long one but your help is greatly appreciated.
The value of this in a function is called the "context" in which the function runs. In general, whenever you pass a callback function as an argument (as you do with $(divref).show(function() {...})), the function can run the callback in whatever context it wants. In this case, the jQuery show function chooses to run its callback in the context of the element being animated.
However, you want access to the value of this at the time the anonymous callback function is defined, rather than when it is run. The solution here is to store the outer value of this in a variable (traditionally called self) which is included in the scope of the newly-defined function:
this.slideChildMenu = function() {
//...
var self = this;
$(divref).show(function(){
self.finishOpeningChild(divref, ulref);
});
}
I am thinking that the jQuery selector has changed the scope of this.
In your example $(this); would refer to object being animated per jQuery api docs:
If supplied, the callback is fired once the animation is complete. This can be useful for stringing different animations together in sequence. The callback is not sent any arguments, but this is set to the DOM element being animated. If multiple elements are animated, it is important to note that the callback is executed once per matched element, not once for the animation as a whole.
If the object in question is instantiated you can call it with dot notation without using this like bigFunc.finishOpeningChild(divref, ulref);
You're probably a little confused about scope, it's not always easy keeping track, but doing something more like this:
var site = {
init: function(elm) {
self=site;
self.master.funcname2(self.varname1, elm); //call function in master
},
funcname: function(obj) {
//do something
},
varname1: 'some string',
varname2: 3+4,
master: function() {
this.varname3 = sin(30);
this.funcname2 = function(stuff, element) {
site.funcname(element); //call function in 'site'
var sinus = site.master.varname3; //get variable
}
}
}
window.onload = function() {
var elm = document.getElementById('elementID');
site.init(elm); //call init function
}
usually makes it a little easier to keep track.
There are many topics related to my question and i have been through most of them, but i haven't got it right. The closest post to my question is the following:
How to call functions that are nested inside a JQuery Plugin?
Below is the jquery plugin i am using. On resize, the element sizes are recalculated. I am now trying to call the function resizeBind() from outside of the jquery plugin and it gives me error
I tried the following combinations to call the function
$.fn.splitter().resizeBind()
$.fn.splitter.resizeBind()
Any ideas, where i am getting wrong?
;(function($){
$.fn.splitter = function(args){
//Other functions ......
$(window).bind("resize", function(){
resizeBind();
});
function resizeBind(){
var top = splitter.offset().top;
var wh = $(window).height();
var ww = $(window).width();
var sh = 0; // scrollbar height
if (ww <0 && !jQuery.browser.msie )
sh = 17;
var footer = parseInt($("#footer").css("height")) || 26;
splitter.css("height", wh-top-footer-sh+"px");
$("#tabsRight").css("height", splitter.height()-30+"px");
$(".contentTabs").css("height", splitter.height()-70+"px");
}
return this.each(function() {
});
};
})(jQuery);
I had the same problem. Those answers on related posts didn't work for my case either. I solved it in a round about way using events.
The example below demonstrates calling a function that multiplies three internal data values by a given multiplier, and returns the result. To call the function, you trigger an event. The handler in turn triggers another event that contains the result. You need to set up a listener for the result event.
Here's the plugin - mostly standard jQuery plugin architecture created by an online wizard:
(function($){
$.foo = function(el, options){
// To avoid scope issues, use 'base' instead of 'this'
var base = this;
// Access to jQuery and DOM versions of element
base.$el = $(el);
base.el = el;
// Add a reverse reference to the DOM object
base.$el.data("foo", base);
base.init = function(){
base.options = $.extend({},$.foo.defaultOptions, options);
// create private data and copy in the options hash
base.private_obj = {};
base.private_obj.value1 = (base.options.opt1);
base.private_obj.value2 = (base.options.opt2);
base.private_obj.value3 = (base.options.opt3);
// make a little element to dump the results into
var ui_element = $('<p>').attr("id","my_paragraph").html(base.private_obj.value1 +" "+ base.private_obj.value2+" " +base.private_obj.value3);
base.$el.append(ui_element);
// this is the handler for the 'get_multiplied_data_please' event.
base.$el.bind('get_multiplied_data_please', function(e,mult) {
bar = {};
bar.v1 = base.private_obj.value1 *mult;
bar.v2 = base.private_obj.value2 *mult;
bar.v3 = base.private_obj.value3 *mult;
base.$el.trigger("here_is_the_multiplied_data", bar);
});
};
base.init();
}
$.foo.defaultOptions = {
opt1: 150,
opt2: 30,
opt3: 100
};
$.fn.foo = function(options){
return this.each(function(){
(new $.foo(this, options));
});
};
})(jQuery);
So, you can attach the object to an element as usual when the document is ready. And at the same time set up a handler for the result event.
$(document).ready(function(){
$('body').foo();
$('body').live('here_is_the_multiplied_data', function(e, data){
console.log("val1:" +data.v1);
console.log("val2:" +data.v2);
console.log("val3:" +data.v3);
$("#my_paragraph").html(data.v1 +" "+ data.v2+" " +data.v3);
});
})
All that's left is to trigger the event and pass it a multiplier value
You could type this into the console - or trigger it from a button that picks out the multiplier from another UI element
$('body').trigger('get_multiplied_data_please', 7);
Disclaimer ;) - I'm quite new to jQuery - sorry if this is using a hammer to crack a nut.
resizeBind function is defined as private so you cannot access it from outside of it's scope. If you want to use it in other scopes you need to define it like that
$.fn.resizeBind = function() { ... }
Then you would call it like that $(selector').resizeBind()
You have defined the resizeBind function in a scope that is different from the global scope. If you dont'use another javascript framework or anything else that uses the $ function (to prevent conflict) you can delete the
(function($){
...
})(jQuery);
statement and in this way the function will be callable everywhere without errors
I didn't test it:
this.resizeBind = function() { .... }