Creating multiple instances of a jQuery plugin - javascript

The below is the source code of a simple parallax plugin:
/*
Plugin: jQuery Parallax
Version 1.1.3
Author: Ian Lunn
Twitter: #IanLunn
Author URL: http://www.ianlunn.co.uk/
Plugin URL: http://www.ianlunn.co.uk/plugins/jquery-parallax/
Dual licensed under the MIT and GPL licenses:
http://www.opensource.org/licenses/mit-license.php
http://www.gnu.org/licenses/gpl.html
*/
(function( $ ){
var $window = $(window);
var windowHeight = $window.height();
$window.resize(function () {
windowHeight = $window.height();
});
$.fn.parallax = function(xpos, speedFactor, outerHeight) {
var $this = $(this);
var getHeight;
var firstTop;
var paddingTop = 0;
//get the starting position of each element to have parallax applied to it
$this.each(function(){
firstTop = $this.offset().top;
});
if (outerHeight) {
getHeight = function(jqo) {
return jqo.outerHeight(true);
};
} else {
getHeight = function(jqo) {
return jqo.height();
};
}
// setup defaults if arguments aren't specified
if (arguments.length < 1 || xpos === null) xpos = "50%";
if (arguments.length < 2 || speedFactor === null) speedFactor = 0.1;
if (arguments.length < 3 || outerHeight === null) outerHeight = true;
// function to be called whenever the window is scrolled or resized
function update(){
var pos = $window.scrollTop();
$this.each(function(){
var $element = $(this);
var top = $element.offset().top;
var height = getHeight($element);
// Check if totally above or totally below viewport
if (top + height < pos || top > pos + windowHeight) {
return;
}
console.log(firstTop + " " + pos);
$this.css('backgroundPosition', xpos + " " + Math.round((firstTop - pos) * speedFactor) + "px");
});
}
$window.bind('scroll', update).resize(update);
update();
};
})(jQuery);
Now suppose i call the plugin , like so , on multiple elements.
$('#intro').parallax("50%", .8);
$('#second').parallax("50%", 0.1);
$('.bg').parallax("50%", 0.4);
$('#third').parallax("50%", 0.3);
What am i really doing ? creating multiple instances of the plugin ?
A demo of the plugin itself can be seen HERE.

No, you are not creating multiple instances of the plugin.
What you are doing is that you are calling this function multiple times:
$.fn.parallax = function(xpos, speedFactor, outerHeight) {
This is perfectly fine to do.

What you really are looking at is a jQuery extension method. This method merges the contents of an object onto the jQuery prototype to provide new jQuery instance methods.
Whenever you see the fn property, you are looking at an alias to the prototype property of jQuery.
Lets examine some lines in the parallax script you are embedding:
$.fn.parallax = function(xpos, speedFactor, outerHeight) {
This line is the start of a new jQuery prototype extension method that takes three arguments
Here is a more simple example that extends jQuery with a new method
$(function () {
// declare the new method greenify
$.fn.greenify = function() {
// The element that this method is used on will have the color green by using jQuery .css();
this.css( "color", "green" );
};
// Then to use your brand new jQuery extension method simply do this
$( "a" ).greenify();
$('.myElem').greenify();
$('#someElemId').greenify();
});
What is happening is that we are using the same method and applying it to different elements in the dom.
I hope this made it clearer what is really going on and how extension methods work.

Related

How to fix jQuery Image Sequence on Scroll

I try to implement a javascript function that animate an image sequence while scrolling.
I try to create the animation with the script from this link: https://www.jqueryscript.net/animation/jQuery-Plugin-To-Create-Image-Sequence-Animation-On-Scroll-Sequencer.html
My problem is that this script is already three years old and does not work with jQuery 3.2.1. But I have to use this much newer jQuery version.
The code of the script looks like this:
/**
* jQuery-Sequencer
* https://github.com/skruf/jQuery-sequencer
*
* Created by Thomas Låver
* http://www.laaver.com
*
* Version: 2.0.0
* Requires: jQuery 1.6+
*
*/
(function($) {
$.fn.sequencer = function(options, cb) {
var self = this,
paths = [],
load = 0,
sectionHeight,
windowHeight,
currentScroll,
percentageScroll,
index;
if(options.path.substr(-1) === "/") {
options.path = options.path.substr(0, options.path.length - 1)
}
for (var i = 0; i <= options.count; i++) {
paths.push(options.path + "/" + i + "." + options.ext);
}
$("<div class='jquery-sequencer-preload'></div>").appendTo("body").css("display", "none");
$(paths).each(function() {
$("<img>").attr("src", this).load(function() {
$(this).appendTo("div.jquery-sequencer-preload");
load++;
if (load === paths.length) {
cb();
}
});
});
$(window).scroll(function() {
sectionHeight = $(self).height();
windowHeight = $(this).height();
currentScroll = $(this).scrollTop();
percentageScroll = 100 * currentScroll / (sectionHeight - windowHeight);
index = Math.round(percentageScroll / 100 * options.count);
if(index < options.count) {
$("img.sequencer").attr("src", paths[index]);
}
});
return this;
};
}(jQuery));
So I already changed the line 37 from
$("<img>").attr("src", this).load(function() {
to
$("<img>").attr("src", this).on("load", function() {
With this change all my images are loaded but I still get the error message: Uncaught TypeError: cb is not a function
What do I have to change as well, so the script is working again?
Thanks for any tip.
cb() is a Callback function will be called once the preloader is done fetching images.
From the example you have linked to, the callback is:
function() {
$("div#preload").hide();
}
which quite simply hides the preloader message.
In context, the plugin is initialised as:
$("div#images").sequencer({
count: 128,
path: "./images",
ext: "jpg"
}, function() {
$("div#preload").hide();
});
As you have not supplied how you are calling this function, I suspect you are missing this function callback.

How to add class to only one element that matches condition using jquery?

I'm trying to check if element crossed bottom edge of viewport. If it did, I want to add class start to this element. The problem is that when condition is satisfied class adds to all h2 elements.
Here is my code:
$.fn.checkAnimation = function() {
var context = this;
function isElementInViewport(elem) {
var $elem = context;
// Get the scroll position of the page.
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
// Get the position of the element on the page.
var elemTop = Math.round( $elem.offset().top );
var elemBottom = elemTop + $elem.height();
return (elemTop < viewportBottom);
}
// Check if it's time to start the animation.
function checkAnimation() {
console.log(isElementInViewport($elem));
var $elem = context;
// If the animation has already been started
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
// Start the animation
context.addClass('start');
}
}
checkAnimation();
return this;
};
$(window).on('scroll scrollstart touchmove orientationchange resize', function(){
$('h2').checkAnimation();
});
You'll need to change your checkAnimation jQuery plugin to loop through all elements in the jQuery object and process them individually or call your function like this
$('h2').each(function(){
$(this).checkAnimation();
}
Here is what I mean by processing the elements individually inside the plugin:
$.fn.checkAnimation = function() {
function isElementInViewport($elem) {
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
var elemTop = Math.round( $elem.offset().top );
var elemBottom = elemTop + $elem.height();
return (elemTop < viewportBottom);
}
function checkAnimation() {
var $elem = $(this);
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
$elem.addClass('start');
}
}
return this.each(checkAnimation);
};
If you use this version of the plugin you can call it like this:
$('h2').checkAnimation();
It will add the class only to the element that matches the condition not to all the element in the jQuery object you've called the function on.
Should be $elem.addClass('start'); instead and remove the var $elem = context; statement like :
function checkAnimation() {
console.log(isElementInViewport($elem));
// If the animation has already been started
if ($elem.hasClass('start')) return;
if (isElementInViewport($elem)) {
// Start the animation
$elem.addClass('start');
}
}
Hope this helps.
this inside a jQuery plugin is the jQuery object that contains the whole collection of elements represented by the previous selector/filter.
In order to treat each element in the collection as an individual instance you need to loop through the initial this.
Very basic pattern:
$.fn.pluginName = function(options){
// return original collection as jQuery to allow chaining
// loop over collection to access individual elements
return this.each(function(i, elem){
// do something with each element instance
$(elem).doSomething(); // elem === this also
});
}

get object position as specified in CSS to modify in jQuery

First question, so please let me know if I could/should be asking this more succinctly/clearly...
I'm working on an experimental script that vibrates objects with a given class with increasing intensity over time. I'm working with jRumble as a base. My script needs to get the initial CSS position of objects to be vibrated and then add the modifying var. I set this up like so at first:
$this.animate({
'left': parseFloat($this.css( "left" )) + rx + 'px',
which works great in Chrome, but Safari and iOS return different values. This explained the problem: jquery $('selector').css('top') returns different values for IE, firefox and Chrome, so I switched to using .offset:
offset = $(".offreg").offset();
...
'left': offset.left + rx + 'px',
but this is problematic because the script iterates-- .offset returns the current position each time instead of the initial one. I tried placing the var outside of the iterating section, but this (obviously) doesn't work because it is no longer associated with the "this" of the rumble script. The var is then set at the value of the first element with class .offreg.
The page itself is here: http://sallymaier.github.io/off-register/
I'm going to revert to the last version that worked in Chrome. My github is public, so you can see my mess-making there I think.
Full script as it works in Chrome below:
var offRegister = function() {
if (!($ = window.jQuery)) { // see if jQuery is already called, if not, calling script
script = document.createElement( 'script' );
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';
script.onload=runRumbler;
document.body.appendChild(script);
}
else {
runRumbler();
}
function runRumbler() {
$(document).ready(function(){
var count = 0;
$(function addone(){
count = count+1;
(function($) {
$.fn.jrumble = function(options){ // jRumble by Jack Rugile. http://jackrugile.com/jrumble/
/*========================================================*/
/* Options
/*========================================================*/
var defaults = {
x: count/5,
y: count/5,
rotation: 0,
speed: 200,
opacity: false,
opacityMin: .5
},
opt = $.extend(defaults, options);
return this.each(function(){
/*========================================================*/
/* Variables
/*========================================================*/
var $this = $(this),
x = opt.x*2,
y = opt.y*2,
rot = opt.rotation*2,
speed = (opt.speed === 0) ? 1 : opt.speed,
opac = opt.opacity,
opacm = opt.opacityMin,
inline,
interval;
/*========================================================*/
/* Rumble Function
/*========================================================*/
var rumbler = function(){
var rx = Math.floor(Math.random() * (x+1)) -x/2,
ry = Math.floor(Math.random() * (y+1)) -y/2,
rrot = Math.floor(Math.random() * (rot+1)) -rot/2,
ropac = opac ? Math.random() + opacm : 1;
/*========================================================*/
/* Ensure Movement From Original Position
/*========================================================*/
rx = (rx === 0 && x !== 0) ? ((Math.random() < .5) ? 1 : -1) : rx;
ry = (ry === 0 && y !== 0) ? ((Math.random() < .5) ? 1 : -1) : ry;
/*========================================================*/
/* Check Inline
/*========================================================*/
if($this.css('display') === 'inline'){
inline = true;
$this.css('display', 'inline-block');
}
/*========================================================*/
/* Rumble Element
/*========================================================*/
$this.animate({
'left': parseFloat($this.css( "left" )) + rx + 'px', // move from declared position
'top': parseFloat($this.css( "top" )) + ry+'px',
'-ms-filter':'progid:DXImageTransform.Microsoft.Alpha(Opacity='+ropac*100+')',
'filter':'alpha(opacity='+ropac*100+')',
'-moz-opacity':ropac,
'-khtml-opacity':ropac,
'opacity':ropac,
'-webkit-transform':'rotate('+rrot+'deg)',
'-moz-transform':'rotate('+rrot+'deg)',
'-ms-transform':'rotate('+rrot+'deg)',
'-o-transform':'rotate('+rrot+'deg)',
'transform':'rotate('+rrot+'deg)'
});
}; /* close rumble function */
/*========================================================*/
/* Rumble CSS Reset
/*========================================================*/
var reset = {
'left':0,
'top':0,
'-ms-filter':'progid:DXImageTransform.Microsoft.Alpha(Opacity=100)',
'filter':'alpha(opacity=100)',
'-moz-opacity':1,
'-khtml-opacity':1,
'opacity':1,
'-webkit-transform':'rotate(0deg)',
'-moz-transform':'rotate(0deg)',
'-ms-transform':'rotate(0deg)',
'-o-transform':'rotate(0deg)',
'transform':'rotate(0deg)'
};
/*========================================================*/
/* Rumble Start/Stop Trigger
/*========================================================*/
$this.bind({
'startRumble': function(e){
e.stopPropagation();
clearInterval(interval);
interval = setInterval(rumbler, speed)
},
'stopRumble': function(e){
e.stopPropagation();
clearInterval(interval);
if(inline){
$this.css('display', 'inline');
}
$this.css(reset);
}
});
});// End return this.each
};// End $.fn.jrumble
})(jQuery);
/*===============================================================*/
/* Specify selector to vibrate below.
/* For bookmarklet, 'div' will vibrate all elements,
/* in plugin this can be specifically targeted to a class or id.
/*===============================================================*/
$('.offreg').jrumble();
$('.offreg').trigger('startRumble');
setTimeout(addone, 1000); // how many seconds to wait before adding to count, increasing vibration
}); // end addone()
});
};
};
offRegister();
I've been playing around with this source code the last couple minutes, and it seems to me the problem stems from the use of position: absolute on the rumbling elements themselves. In the jRumble docs http://jackrugile.com/jrumble/#documentation it says that 'For rumble elements that are position fixed/absolute, they should instead be wrapped in an element that is fixed/absolute'.
So if you set your container element to position: absolute and then position all the interior divs relative to the container, you'll no longer need to alter the jRumble source code and redefine the plugin every iteration to change the default position or compute the offset of each element - all you'll have to do is increment the x and y values in the jRumble call itself.
The code below is UNTESTED, but I think you want something closer to this (again, this all hinges on the .offreg elements being position: relative):
$(document).ready(function(){
var count = 0;
function addone(){
count++;
$('.offreg').jrumble({
x: count,
y: count,
rotation: 1
});
$('.offreg').trigger('startRumble');
setTimeout(addone, 1000);
}
});
I like Sean's answer of sidestepping the need for adding code to override $.fn.jrumble, which creates a brittle maintenance point and makes $.fn.jrumble unusable outside this code block (since it depends on the variable "count").
For the immediate issue of the variable scope, if you still need to solve that problem, I'd suggest moving the redefinition of $.fn.jrumble out of addone(), and then scoping a variable, "offset", outside of said redefinition. Then refer to that variable in your call to $this.animate(). Code is below, but again, Sean's answer should make this unnecessary.
var offRegister = function() {
if (!($ = window.jQuery)) { // see if jQuery is already called, if not, calling script
script = document.createElement( 'script' );
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js';
script.onload=runRumbler;
document.body.appendChild(script);
}
else {
runRumbler();
}
};
function runRumbler() {
$(document).ready(function(){
var count = 0;
var offset;
function addone() {
...
$('.offreg').jrumble();
$('.offreg').trigger('startRumble');
offset = $('.offreg').offset();
setTimeout(addone, 1000); // how many seconds to wait before adding to
}
(function($){
$.fn.jrumble = function(options){
...
$this.animate({
'left': parseFloat(offset) + rx + 'px',
...
});
...
};
)(jQuery);
...
}
offRegister();
Another suggestion: make the whole thing an anonymous function. Instead of:
var offRegister = function() {
...
}
offRegister();
Try:
(function(){
...
})();
That way you don't stick a variable "offRegister" in the global namespace. In this case, doing so isn't a problem, but in general code that you want to reuse elsewhere is cleaner if it doesn't introduce new globals.

tips.hide() function. Potential bug in infoVis/JIT?

I have a force directed graph with "Tips" enabled. I don't want to show tips for those nodes which are hidden i.e. for whom "alpha" is zero. In onShow call back function I am trying to use tips.hide() but it is not hiding the tip. Here is my code.
Tips: {
enable: true,
type: 'Native',
onShow: function(tip, node) {
if( node.getData('alpha') == 0 ) this.fd.tips.hide(false);
else tip.innerHTML = node.name;
}
}
When I drilled down into infovis library jit.js I found something which looks like a bug. Below is the hide function which basically sets style.display to 'none'.
hide: function(triggerCallback) {
this.tip.style.display = 'none';
triggerCallback && this.config.onHide();
}
Now look at the code below.
onMouseMove: function(e, win, opt) {
if(this.dom && this.isLabel(e, win)) {
this.setTooltipPosition($.event.getPos(e, win));
}
if(!this.dom) {
var node = opt.getNode();
if(!node) {
this.hide(true);
return;
}
if(this.config.force || !this.node || this.node.id != node.id) {
this.node = node;
this.config.onShow(this.tip, node, opt.getContains());
}
this.setTooltipPosition($.event.getPos(e, win));
}
},
setTooltipPosition: function(pos) {
var tip = this.tip,
style = tip.style,
cont = this.config;
style.display = ''; //This looks like a problem
//get window dimensions
var win = {
'height': document.body.clientHeight,
'width': document.body.clientWidth
};
//get tooltip dimensions
var obj = {
'width': tip.offsetWidth,
'height': tip.offsetHeight
};
//set tooltip position
var x = cont.offsetX, y = cont.offsetY;
style.top = ((pos.y + y + obj.height > win.height)?
(pos.y - obj.height - y) : pos.y + y) + 'px';
style.left = ((pos.x + obj.width + x > win.width)?
(pos.x - obj.width - x) : pos.x + x) + 'px';
}
As you can see the onShow function is called from the onMouseMove function. And after onShow, setTooltipPosition function is called which sets the style.display back to ' ' (see my comment in code). Because of this the tip is not being hidden even after calling hide() from onShow function.
When I commented out that line in setTooltipPosition, it worked i.e. tool tips were hidden for nodes which were hidden.
Is this a bug in infovis or am I doing something wrong. I would like to know how is hide function supposed to be used if its not a bug.
Also, does anyone know of any other better way to hide a tool tip?
I had a similar problem. The solution was to use the .css('visibility', 'visible') instead of .hide() - because the element was hidden to start with using css styling.

Floating widget/banner using the prototype JS

I created a script using the originals from jQuery: http://terkel.jp/demo/jquery-floating-widget-plugin.html
But when typing to prototype JS it is not working properly.
It strictly doesn't count correctly the upper and lower height.
Any thoughts? I attached my code and link to view the active script.
Online problem: http://1.delnik20.cz/
Prototype:
document.observe('dom:loaded', function() {
Element.addMethods({
floatingWidget: function(el, classes) {
var $this = $(el),
$parent = $this.getOffsetParent(),
$window = $(window),
top = $this.positionedOffset().top - parseFloat($this.getStyle('marginTop').replace(/auto/, 0)),
bottom = $parent.positionedOffset().top + $parent.getHeight() - $this.getHeight();
if ($parent.getHeight() > $this.getHeight()) {
Event.observe($window, 'scroll', function (e) {
var y = document.viewport.getScrollOffsets().top;
if (y > top) {
$this.addClassName(classes.floatingClass);
if (y > bottom) {
$this.removeClassName(classes.floatingClass).addClassName(classes.pinnedBottomClass);
} else {
$this.removeClassName(classes.pinnedBottomClass);
}
} else {
$this.removeClassName(classes.floatingClass);
}
});
}
return el;
}
});
$('floating-widget').floatingWidget({
floatingClass: 'floating',
pinnedBottomClass: 'pinned-bottom'
});
});
Prototypejs equivalent to jQuery's offset() is cumulativeOffset(), the equivalent to outerHeight() is measure('margin-box-height') (which may be optimized using the Element.Layout object). Thus your code should look like:
floatingWidget: function(el, classes) {
var $this = $(el),
$parent = $this.getOffsetParent(),
$window = $(window),
top = $this.cumulativeOffset().top - $this.measure('margin-top'),
bottom = $parent.cumulativeOffset().top + $parent.getHeight() - $this.measure('margin-box-height');
if ($parent.getHeight() > $this.measure('margin-box-height')) {
...

Categories

Resources