I have developed a jQuery plugin which does the job of showing and hiding content with a two sequenced animations. I am happy with the behaviour of the plugin when used on a single element, however when there are two elements on the page with the same class name used in the plugin call I get four callbacks being returned. It would seem that the plugin is not quite right and I would appreciate any hints or help that could get me closer to finishing this plugin.
I am keen to improve the quality of my code and would also appreciate any general feedback.
A working example of the plugin can be found here: http://jsfiddle.net/nijk/sVu7h/
The plugin code is as follows:
(function($){
$.fn.showHide = function(method, duration, options, callback){
//console.log(method, duration, options, callback);
var animating = false;
var defaults = {
$elem: this,
easing: "swing"
}
var _sh = this;
var init = function(){
_sh.method = method;
if("show" !== _sh.method && "hide" !== _sh.method){
_sh.method = "show";
}
if( duration < 0 || (typeof(duration) == "string" && ("slow" !== duration && "normal" !== duration && "fast" !== duration) ) ){
duration = "normal";
}
console.log( duration, typeof(duration) );
if(typeof(options) == "function"){
callback = options;
options = {};
}
_sh.config = $.extend({}, defaults, options);
if(!animating){
//return _sh.each(function(index){
//console.log("found element number: " + (index + 1));
eval(_sh.method)();
//});
}
}
var show = function(){
animating = true;
_sh.config.$elem.wrap('<div class="show-hide"/>').parent().hide();
_sh.config.$elem.css({"opacity":0, "display":"block"});
console.log("element height:", _sh.config.$elem.parent().outerHeight());
_sh.config.$elem.parent().slideDown(duration, _sh.config.easing, function(){
_sh.config.$elem.animate({"opacity": 1}, duration, _sh.config.easing, function(){
console.log("show final cleanup called");
_sh.config.$elem.addClass("visible").unwrap();
$.isFunction(callback) && callback();
animating = false;
});
});
};
var hide = function(){
animating = true;
_sh.config.$elem.wrap('<div class="show-hide"/>');
_sh.config.$elem.animate({"opacity":0}, duration, _sh.config.easing, function(){
_sh.config.$elem.slideUp(duration, _sh.config.easing, function(){
console.log("hide final cleanup called");
_sh.config.$elem.removeClass("visible").hide().unwrap();
$.isFunction(callback) && callback();
animating = false;
});
});
}
init();
return this;
}
})(jQuery);
#david.mchonechase: Thank you very much for your explanation and the code example.
I have made some tweeks on the callbacks so that the correct context for 'this' is returned. Any suggestions to improve to code would be greatly appreciated.
Working code updated here: http://jsfiddle.net/nijk/sVu7h/ and as follows:
(function($){
$.fn.showHide = function(method, duration, options, callback){
var animating = false;
var defaults = { easing: "swing" };
var _sh = this;
_sh.method = show;
if("hide" === method){
_sh.method = hide;
}
if( duration < 0 || (typeof(duration) == "string" && ("slow" !== duration && "normal" !== duration && "fast" !== duration) ) ){
duration = "normal";
}
if(typeof(options) == "function"){
callback = options;
options = {};
}
_sh.config = $.extend({}, defaults, options);
function show(elem){
animating = true;
elem.wrap('<div class="show-hide"/>').parent().hide();
elem.css({"opacity":0, "display":"block"});
elem.parent().slideDown(duration, _sh.config.easing, function(){
elem.animate({"opacity": 1}, duration, _sh.config.easing, function(){
elem.addClass("visible").unwrap();
$.isFunction(callback) && callback.call(this);
animating = false;
});
});
};
function hide(elem){
animating = true;
elem.wrap('<div class="show-hide"/>');
elem.animate({"opacity":0}, duration, _sh.config.easing, function(){
elem.slideUp(duration, _sh.config.easing, function(){
elem.removeClass("visible").hide().unwrap();
$.isFunction(callback) && callback.call(this);
animating = false;
});
});
};
if(!animating){
// loop through each element returned by jQuery selector
return this.each(function(){
_sh.method($(this));
});
}
}
})(jQuery);
The problem is the mixture of global variables and the parent() call. The _sh.$elem variable contains two elements (one per jQuery selector result). The _sh.config.$elem.parent().slideDown call in the show function is called twice. Once complete, it then runs _sh.config.$elem.animate once per "showMe" element. So, the parent().slideDown is called twice, which then calls _sh.config.$elem.animate twice.
I usually try avoid global variables within jQuery plug-ins for functions like your show and hide, but the critical part is elements. (The animating global variable makes sense, though.)
I think something like this would work:
(function($){
$.fn.showHide = function(method, duration, options, callback){
//console.log(method, duration, options, callback);
var animating = false;
var defaults = {
//$elem: this,
easing: "swing"
}
var _sh = this;
var init = function(){
var methodFn = show; // reference actual function instead of string, since eval is evil (usually)
if("hide" === method){
methodFn = hide;
}
if( duration < 0 || (typeof(duration) == "string" && ("slow" !== duration && "normal" !== duration && "fast" !== duration) ) ){
duration = "normal";
}
console.log( duration, typeof(duration) );
if(typeof(options) == "function"){
callback = options;
options = {};
}
_sh.config = $.extend({}, defaults, options);
if(!animating){
// loop through each element returned by jQuery selector
_sh.each(function(){
methodFn($(this)); // pass the single element to the show or hide functions
});
}
}
var show = function(elem){
animating = true;
elem.wrap('<div class="show-hide"/>').parent().hide();
elem.css({"opacity":0, "display":"block"});
console.log("element height:", elem.parent().outerHeight());
elem.parent().slideDown(duration, _sh.config.easing, function(){
elem.animate({"opacity": 1}, duration, _sh.config.easing, function(){
console.log("show final cleanup called");
elem.addClass("visible").unwrap();
$.isFunction(callback) && callback();
animating = false;
});
});
};
var hide = function(elem){
animating = true;
elem.wrap('<div class="show-hide"/>');
elem.animate({"opacity":0}, duration, _sh.config.easing, function(){
elem.slideUp(duration, _sh.config.easing, function(){
console.log("hide final cleanup called");
elem.removeClass("visible").hide().unwrap();
$.isFunction(callback) && callback();
animating = false;
});
});
}
init();
return this;
}
})(jQuery);
Related
I have been searching for all available solutions
This is the best so far i could find but it doesn't work as it should be
It works for first few images then stops. Then i refresh page, works for few more images and then stops again.
So basically what i want to achieve is, give like a 100 images to a function, it starts downloading them 1 by 1
So browser caches those images and those images are not downloaded on other pages and instantly displayed
I want this images to be cached on mobile as well
Here my javascript code that i call. Actually i have more than 100 images but didn't put all here
I accept both jquery and raw javascript solutions doesn't matter
(function() {
'use strict';
var preLoader = function(images, options) {
this.options = {
pipeline: true,
auto: true,
/* onProgress: function(){}, */
/* onError: function(){}, */
onComplete: function() {}
};
options && typeof options == 'object' && this.setOptions(options);
this.addQueue(images);
this.queue.length && this.options.auto && this.processQueue();
};
preLoader.prototype.setOptions = function(options) {
// shallow copy
var o = this.options,
key;
for (key in options) options.hasOwnProperty(key) && (o[key] = options[key]);
return this;
};
preLoader.prototype.addQueue = function(images) {
// stores a local array, dereferenced from original
this.queue = images.slice();
return this;
};
preLoader.prototype.reset = function() {
// reset the arrays
this.completed = [];
this.errors = [];
return this;
};
preLoader.prototype.load = function(src, index) {
console.log("downloading image " + src);
var image = new Image(),
self = this,
o = this.options;
// set some event handlers
image.onerror = image.onabort = function() {
this.onerror = this.onabort = this.onload = null;
self.errors.push(src);
o.onError && o.onError.call(self, src);
checkProgress.call(self, src);
o.pipeline && self.loadNext(index);
};
image.onload = function() {
this.onerror = this.onabort = this.onload = null;
// store progress. this === image
self.completed.push(src); // this.src may differ
checkProgress.call(self, src, this);
o.pipeline && self.loadNext(index);
};
// actually load
image.src = src;
return this;
};
preLoader.prototype.loadNext = function(index) {
// when pipeline loading is enabled, calls next item
index++;
this.queue[index] && this.load(this.queue[index], index);
return this;
};
preLoader.prototype.processQueue = function() {
// runs through all queued items.
var i = 0,
queue = this.queue,
len = queue.length;
// process all queue items
this.reset();
if (!this.options.pipeline)
for (; i < len; ++i) this.load(queue[i], i);
else this.load(queue[0], 0);
return this;
};
function checkProgress(src, image) {
// intermediate checker for queue remaining. not exported.
// called on preLoader instance as scope
var args = [],
o = this.options;
// call onProgress
o.onProgress && src && o.onProgress.call(this, src, image, this.completed.length);
if (this.completed.length + this.errors.length === this.queue.length) {
args.push(this.completed);
this.errors.length && args.push(this.errors);
o.onComplete.apply(this, args);
}
return this;
}
if (typeof define === 'function' && define.amd) {
// we have an AMD loader.
define(function() {
return preLoader;
});
} else {
this.preLoader = preLoader;
}
}).call(this);
// Usage:
$(window).load(function() {
new preLoader([
'//static.pokemonpets.com/images/attack_animations/absorb1.png',
'//static.pokemonpets.com/images/attack_animations/bleeding1.png',
'//static.pokemonpets.com/images/attack_animations/bug_attack1.png',
'//static.pokemonpets.com/images/attack_animations/bug_attack2.png',
'//static.pokemonpets.com/images/attack_animations/bug_boost1.png',
'//static.pokemonpets.com/images/attack_animations/burned1.png',
'//static.pokemonpets.com/images/attack_animations/change_weather_cloud.png',
'//static.pokemonpets.com/images/attack_animations/confused1.png',
'//static.pokemonpets.com/images/attack_animations/copy_all_enemy_moves.png',
'//static.pokemonpets.com/images/attack_animations/copy_last_move_enemy.png',
'//static.pokemonpets.com/images/attack_animations/cringed1.png',
'//static.pokemonpets.com/images/attack_animations/critical1.png',
'//static.pokemonpets.com/images/attack_animations/cure_all_status_problems.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack1.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack2.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack3.png',
'//static.pokemonpets.com/images/attack_animations/dark_boost1.png',
'//static.pokemonpets.com/images/attack_animations/double_effect.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack1.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack2.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack3.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack4.png'
]);
});
Something like this?
I do not have time to make it pretty.
const pref = "https://static.pokemonpets.com/images/attack_animations/";
const defa = "https://imgplaceholder.com/20x20/000/fff/fa-image";
const images=['absorb1','bleeding1','bug_attack1','bug_attack2','bug_boost1','burned1','change_weather_cloud','confused1','copy_all_enemy_moves','copy_last_move_enemy','cringed1','critical1','cure_all_status_problems','dark_attack1','dark_attack2','dark_attack3','dark_boost1','double_effect','dragon_attack1','dragon_attack2','dragon_attack3','dragon_attack4'];
let cnt = 0;
function loadIt() {
if (cnt >= images.length) return;
$("#imagecontainer > img").eq(cnt).attr("src",pref+images[cnt]+".png"); // preload next
cnt++;
}
const $cont = $("#container");
const $icont = $("#imagecontainer");
// setting up test images
$.each(images,function(_,im) {
$cont.append('<img src="'+defa+'" id="'+im+'"/>'); // actual images
$icont.append('<img src="'+defa+'" data-id="'+im+'"/>'); // preload images
});
$("#imagecontainer > img").on("load",function() {
if (this.src.indexOf("imgplaceholder") ==-1) { // not for the default image
$("#"+$(this).attr("data-id")).attr("src",this.src); // copy preloaded
loadIt(); // run for next entry
}
})
loadIt(); // run
#imagecontainer img { height:20px }
#container img { height:100px }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container"></div>
<hr/>
<div id="imagecontainer"></div>
I want to ask if there is a way to stop the auto looping. My goal is to have pause as default state and when user hover over my loop (images) it starts looping.
I managed to reverse the hover effect(mousenter starting the loop, mouseleave pause the loop), which was no problem. But i cant figure how to pause loop when my page loads.
HTML:
<div data-looper="go" class="looper">
<div class="looper-inner">
<div class="item">
<img src="http://lorempixel.com/320/240/sports" alt="">
</div>
<div class="item">
<img src="http://lorempixel.com/320/240/animals" alt="">
</div>
<div class="item">
<img src="http://lorempixel.com/320/240/food" alt="">
</div>
</div>
</div>
looper.js
;(function($, window, document, undefined) {
"use strict";
// css transition support detection
var cssTransitionSupport = (function(){
// body element
var body = document.body || document.documentElement,
// transition events with names
transEndEvents = {
'transition' : 'transitionend',
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition' : 'transitionend',
'MsTransition' : 'MSTransitionEnd',
'OTransition' : 'oTransitionEnd otransitionend'
}, name;
// check for each transition type
for (name in transEndEvents){
// if transition type is supported
if (body.style[name] !== undefined) {
// return transition end event name
return transEndEvents[name];
}
}
// css transitions are not supported
return false;
})();
// class definition
var Looper = function(element, options) {
this.$element = $(element);
this.options = options;
this.looping = false;
// self-reference
var self = this;
// setup keyboard accessibility
this.$element.attr('tabindex', 0)
// use keydown, keypress not reliable in IE
.keydown(function(e){
// handle key
switch (e.which) {
// left arrow
case 37:
// go to previous item
self.prev();
break;
// right arrow
case 39:
// go to next item
self.next();
break;
// give control back to browser for other keys
default: return;
}
// prevent browser default action
e.preventDefault();
})
// ARIA
.find('.item').attr('aria-hidden', true);
// setup pause on hover
this.options.pause === 'hover' && this.$element
.on('mouseenter', $.proxy(this.loop, this))
.on('mouseleave', $.proxy(this.pause, this));
// trigger init event
this.$element.trigger('init');
};
// class prototype definition
Looper.prototype = {
/**
* Start auto-loop of items
* #param e
* #return Looper
*/
loop: function(e) {
if (!e) this.paused = false;
// check for interval
if (this.interval) {
// remove interval
clearInterval(this.interval);
this.interval = null;
}
// setup new loop interval
if (this.options.interval && !this.paused)
this.interval = setInterval($.proxy(this.next, this), this.options.interval);
// return reference to self for chaining
return this;
},
/**
* Pause auto-loop of items
* #param e
* #return Looper
*/
pause: function(e) {
if (!e) this.paused = true;
if (this.$element.find('.next, .prev').length && cssTransitionSupport) {
this.$element.trigger(cssTransitionSupport);
this.loop();
}
// remove interval
clearInterval(this.interval);
this.interval = null;
// return reference to self for chaining
return this;
},
/**
* Show next item
* #return Looper
*/
next: function() {
// return if looping
if (this.looping) return this;
// go to next item
return this.go('next');
},
/**
* Show previous item
* #return Looper
*/
prev: function() {
// return if looping
if (this.looping) return this;
// go to previous item
return this.go('prev');
},
/**
* Show item at specified position
* #param pos
* #return Looper
*/
to: function(pos) {
// return if looping
if (this.looping) return this;
// zero-base the position
--pos;
var // all items
$items = this.$element.find('.item'),
// active item
$active = $items.filter('.active'),
// active position
activePos = $items.index($active);
// return if position is out of range
if (pos > ($items.length - 1) || pos < 0) return this;
// if position is already active
if (activePos == pos)
// restart loop
return this.pause().loop();
// show item at position
return this.go($($items[pos]));
},
/**
* Show item
* #param to
* #return Looper
*/
go: function(to) {
// return if looping
if (this.looping) return this;
// all items
var $items = this.$element.find('.item');
// if no items, do nothing
if (!$items.length) return this;
// active item
var $active = $items.filter('.active'),
// active position
activePos = $items.index($active),
// next item to show
$next = typeof to == 'string' ? $active[to]() : to,
// next position
nextPos = $items.index($next),
// is there an auto-loop?
isLooping = this.interval,
// direction of next item
direction = typeof to == 'string'
? to
: ((activePos == -1 && nextPos == -1) || nextPos > activePos
? 'next'
: 'prev'),
// fallback if next item not found
fallback = direction == 'next' ? 'first' : 'last',
// self-reference
that = this,
// finish
complete = function(active, next, direction) {
// if not looping, already complete
if (!this.looping) return;
// set looping status to false
this.looping = false;
// update item classes
active.removeClass('active go ' + direction)
// update ARIA state
.attr('aria-hidden', true);
next.removeClass('go ' + direction).addClass('active')
// update ARIA state
.removeAttr('aria-hidden');
// custom event
var e = $.Event('shown', {
// related target is new active item
relatedTarget: next[0],
// related index is the index of the new active item
relatedIndex: $items.index(next)
});
// trigger shown event
this.$element.trigger(e);
};
// ensure next element
$next = $next && $next.length ? $next : $items[fallback]();
// return if next element is already active
if ($next.hasClass('active')) return this;
// custom event
var e = $.Event('show', {
// related target is next item to show
relatedTarget: $next[0],
relatedIndex: $items.index($next[0])
});
// trigger show event
this.$element.trigger(e);
// return if the event was canceled
if (e.isDefaultPrevented()) return this;
// set looping status to true
this.looping = true;
// if auto-looping, pause loop
if (isLooping) this.pause();
// if using a slide or cross-fade
if (this.$element.hasClass('slide') || this.$element.hasClass('xfade')) {
// if css transition support
if (cssTransitionSupport) {
// add direction class to active and next item
$next.addClass(direction);
$active.addClass('go ' + direction);
// force re-flow on next item
$next[0].offsetWidth;
// add go class to next item
$next.addClass('go');
// finish after transition
this.$element.one(cssTransitionSupport, function() {
// weird CSS transition when element is initially hidden
// may cause this event to fire twice with invalid $active
// element on first run
if (!$active.length) return;
complete.call(that, $active, $next, direction);
});
// ensure finish
setTimeout(function() {
complete.call(that, $active, $next, direction);
}, this.options.speed);
// no css transition support, use jQuery animation fallback
} else {
// setup animations
var active = {},
activeStyle,
next = {},
nextStyle;
// save original inline styles
activeStyle = $active.attr('style');
nextStyle = $next.attr('style');
// cross-fade
if (this.$element.hasClass('xfade')) {
active['opacity'] = 0;
next['opacity'] = 1;
$next.css('opacity', 0); // IE8
}
// slide
if (this.$element.hasClass('slide')) {
if (this.$element.hasClass('up')) {
active['top'] = direction == 'next' ? '-100%' : '100%';
next['top'] = 0;
} else if (this.$element.hasClass('down')) {
active['top'] = direction == 'next' ? '100%' : '-100%';
next['top'] = 0;
} else if (this.$element.hasClass('right')) {
active['left'] = direction == 'next' ? '100%' : '-100%';
next['left'] = 0;
} else {
active['left'] = direction == 'next' ? '-100%' : '100%';
next['left'] = 0;
}
}
$next.addClass(direction);
// do animations
$active.animate(active, this.options.speed);
$next.animate(next, this.options.speed, function(){
// finish
complete.call(that, $active, $next, direction);
// reset inline styles
$active.attr('style', activeStyle || '');
$next.attr('style', nextStyle || '');
});
}
// no animation
} else {
// finish
complete.call(that, $active, $next, direction);
}
// if auto-looping
if ((isLooping || (!isLooping && this.options.interval))
// and, no argument
&& (!to
// or, argument not equal to pause option
|| (typeof to == 'string' && to !== this.options.pause)
// or, argument is valid element object and pause option not equal to "to"
|| (to.length && this.options.pause !== 'to')))
// start/resume loop
this.loop();
// return reference to self for chaining
return this;
}
};
// plugin definition
$.fn.looper = function(option) {
// looper arguments
var looperArgs = arguments;
// for each matched element
return this.each(function() {
var $this = $(this),
looper = $this.data('looperjs'),
options = $.extend({}, $.fn.looper.defaults, $.isPlainObject(option) ? option : {}),
action = typeof option == 'string' ? option : option.looper,
args = option.args || (looperArgs.length > 1 && Array.prototype.slice.call(looperArgs, 1));
// ensure looper plugin
if (!looper) $this.data('looperjs', (looper = new Looper(this, options)));
// go to position if number
if (typeof option == 'number') looper.to(option);
// execute method if string
else if (action) {
// with arguments
if (args) looper[action].apply(looper, args.length ? args : ('' + args).split(','));
// without arguments
else looper[action]();
}
// if there's an interval, start the auto-loop
else if (options.interval) looper.loop()
// otherwise, go to first item in the loop
else looper.go();
});
};
// default options
$.fn.looper.defaults = {
// auto-loop interval
interval: 2000,
// when to pause auto-loop
pause: 'hover',
// css transition speed (must match css definition)
speed: 500
};
// constructor
$.fn.looper.Constructor = Looper;
// data api
$(function() {
// delegate click event on elements with data-looper attribute
$('body').on('click.looper', '[data-looper]', function(e) {
var $this = $(this);
// ignore the go method
if ($this.data('looper') == 'go') return;
// get element target via data (or href) attribute
var href, $target = $($this.data('target')
|| (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')), //strip for ie7
// setup options
options = $.extend({}, $target.data(), $this.data());
// start looper plugin
$target.looper(options);
// prevent default event
e.preventDefault();
});
// auto-init plugin
$('[data-looper="go"]').each(function(){
var $this = $(this);
$this.looper($this.data());
});
});
})(jQuery, window, document);
I did not find anything useful in plugin documentation.
Link to looper.js website
I'm struggling to understand why the transitions don't behave as expected. It's supposed to apply the "from", then add the "transition" to the "el", then it's supposed to run "to" and finally onTransitionEnd it's supposed to run "callback" (prepended to which is a bit of code which clears the transition properties).
In Webkit browsers, it transitions slideDown correctly, but slideUp is instant. Reverse is true in Firefox.
Erg?
JSFiddle: http://jsfiddle.net/enhTd/
var $ = function(query) {
var a = [],
n = document.querySelectorAll(query),
l = n.length;
for( var i = 0; i<l; i++){
a.push(n[i]);
}
if(l>1) {return a;} else {return a[0];}
},
$id = function(query) { return document.getElementById(query);},
getSupportedPropertyName = function(properties) {
for (var i = 0; i < properties.length; i++) {
if (typeof document.body.style[properties[i]] != "undefined") {
return properties[i];
}
}
return null;
},
vendorTransitions = ["transition", "msTransition", "webkitTransition", "MozTransition", "OTransition"],
prefixedTransitionProperty = getSupportedPropertyName(vendorTransitions),
transition = function(opts){
opts.from && opts.from();
if(prefixedTransitionProperty){
var c = opts.callback || function() {},
el = opts.el,
cb = function(event){
var ev = event, callback = c;
ev.target.removeEventListener(prefixedTransitionProperty+"End", cb);
ev.target.style[prefixedTransitionProperty] = "none";
if(callback) {
callback(ev);
}
};
el.style[prefixedTransitionProperty] = opts.transition || "";
el.addEventListener(prefixedTransitionProperty+"End", cb);
}
opts.to && opts.to();
},
slideDown = function(el, t){
var style = el.style,
h, oh = el.offsetHeight,
t = t || 1000;
//Grab real height
style.height = "";
h = el.offsetHeight;
transition({
"el": el,
transition: "height "+t+"ms ease",
from: function() {
style.height = oh+"px";
},
to: function(){
style.overflow = "hidden";
style.height = h+"px";
},
callback: function(event){
event.target.style.height = "";
}
});
},
slideUp = function(el, t){
var style = el.style,
h = el.offsetHeight,
t = t || 1000;
transition({
"el": el,
transition: "height "+t+"ms ease",
from: function() {
style.height = h+"px";
},
to: function(){
style.overflow = "hidden";
style.height = "0";
}
});
},
slideToggle = function(el, t){
var t = t || 1000;
if(el.style.height=="0px"){
slideDown(el, t);
} else {
slideUp(el, t);
}
};
slideUp($id("intro"));
$("a[href='#intro']").forEach(function(el){
el.addEventListener("click", function(ev) {
ev.preventDefault();
if(ev.target.classList.contains("hide")){
slideUp($(ev.target.hash));
} else {
slideDown($(ev.target.hash));
}
});
});
$("li h3").forEach(function(el){
el.addEventListener("click", function(ev) {
ev.preventDefault();
slideToggle(ev.target.parentNode);
});
});
In some browsers, you cannot just set the initial state, set the final state and expect a CSS transition to work.
Instead, the initial state must be set and then it must be rendered before you can set the final state and expect the transition to run. The simplest way to cause it to be rendered is to return back to the event loop and then do any further processing (like setting the final state) after a setTimeout() call.
try this: http://jsfiddle.net/enhTd/1/ (tested in chrome and ff)
first: do what jfriend00 talked about:
in your transition function:
transition = function (opts) {
..
setTimeout(function() {
opts.to && opts.to();
}, 1)
}
second: the callback wasn't necessary in your slideDown() method:
slideDown = function (el, t) {
..
take this out:
/*callback: function (event) {
event.target.style.height = "";
}*/
});
from a design point of view.. the callback function wasn't adding any value.. you should just continue from where you left off when it comes to transitions.
third: disable slide down when the slide is already down.. but I'll leave that to you.
So I'm running some animations to bring in some divs in my first jQuery plugin:
$.fn.turnstile = function (options, callback) {
if (!this.length) {
return this;
}
count = this.length;
var opts = $.extend(true, {}, $.fn.turnstile.defaults, options)
var delayIt = 100;
if (opts.direction == "in") {
opts.deg = '0deg';
opts.trans = '0,0';
opts.opacity = 1;
} else if (opts.direction == "out") {
opts.deg = '-90deg';
opts.trans = "-100px, 200px";
opts.opacity = 0;
} else if (opts.direction == "back") {
opts.deg = '0deg';
opts.trans = '-2000px,0px';
opts.opacity = 0;
} else {
opts.deg = direction;
}
this.each(function (index) {
delayIt += opts.delayer;
$(this).show().delay(delayIt).transition({
perspective: '0px',
rotateY: opts.deg,
opacity: opts.opacity,
translate: opts.trans
}, 400, 'cubic-bezier(0.33,0.66,0.66,1)', function () {
if (opts.direction == "back" || opts.direction == "out") {
$(this).hide();
}
if (!--count && callback && typeof (callback) === "function") {
if ($(":animated").length === 0) {
callback.call(this);
}
}
});
});
return this;
};
Now, I'd like to call my callback when all animations are completed, which should mathematically be (delayIt+400)*count - but I can't get the callback to run when all of the animations are complete. As you might be able to tell from its current state, I've attempted to check for :animated, used the !--count condition, and even tried setting a timeout equal to the duration of the animations, but all seem to fire asynchronously. What is the best way to animate these and call something back when done?
Here's the info on the .transition() plugin.
Not tested with your code (i'm not familiar with .transition), but try this:
...
this.promise().done(function(){
alert('all done');
});
return this;
};
How do I know when I've stopped scrolling using Javascript?
You can add an event handler for the scroll event and start a timeout. Something like:
var timer = null;
window.addEventListener('scroll', function() {
if(timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(function() {
// do something
}, 150);
}, false);
This will start a timeout and wait 150ms. If a new scroll event occurred in the meantime, the timer is aborted and a new one is created. If not, the function will be executed. You probably have to adjust the timing.
Also note that IE uses a different way to attach event listeners, this should give a good introduction: quirksmode - Advanced event registration models
There isn't a "Stopped Scrolling" event. If you want to do something after the user has finished scrolling, you can set a timer in the "OnScroll" event. If you get another "OnScroll" event fired then reset the timer. When the timer finally does fire, then you can assume the scrolling has stopped. I would think 500 milliseconds would be a good duration to start with.
Here's some sample code that works in IE and Chrome:
<html>
<body onscroll="bodyScroll();">
<script language="javascript">
var scrollTimer = -1;
function bodyScroll() {
document.body.style.backgroundColor = "white";
if (scrollTimer != -1)
clearTimeout(scrollTimer);
scrollTimer = window.setTimeout("scrollFinished()", 500);
}
function scrollFinished() {
document.body.style.backgroundColor = "red";
}
</script>
<div style="height:2000px;">
Scroll the page down. The page will turn red when the scrolling has finished.
</div>
</body>
</html>
Here's a more modern, Promise-based solution I found on a repo called scroll-into-view-if-needed
Instead of using addEventListener on the scroll event it uses requestAnimationFrame to watch for frames with no movement and resolves when there have been 20 frames without movement.
function waitForScrollEnd () {
let last_changed_frame = 0
let last_x = window.scrollX
let last_y = window.scrollY
return new Promise( resolve => {
function tick(frames) {
// We requestAnimationFrame either for 500 frames or until 20 frames with
// no change have been observed.
if (frames >= 500 || frames - last_changed_frame > 20) {
resolve()
} else {
if (window.scrollX != last_x || window.scrollY != last_y) {
last_changed_frame = frames
last_x = window.scrollX
last_y = window.scrollY
}
requestAnimationFrame(tick.bind(null, frames + 1))
}
}
tick(0)
})
}
With async/await and then
await waitForScrollEnd()
waitForScrollEnd().then(() => { /* Do things */ })
(function( $ ) {
$(function() {
var $output = $( "#output" ),
scrolling = "<span id='scrolling'>Scrolling</span>",
stopped = "<span id='stopped'>Stopped</span>";
$( window ).scroll(function() {
$output.html( scrolling );
clearTimeout( $.data( this, "scrollCheck" ) );
$.data( this, "scrollCheck", setTimeout(function() {
$output.html( stopped );
}, 250) );
});
});
})( jQuery );
=======>>>>
Working Example here
I did something like this:
var scrollEvents = (function(document, $){
var d = {
scrolling: false,
scrollDirection : 'none',
scrollTop: 0,
eventRegister: {
scroll: [],
scrollToTop: [],
scrollToBottom: [],
scrollStarted: [],
scrollStopped: [],
scrollToTopStarted: [],
scrollToBottomStarted: []
},
getScrollTop: function(){
return d.scrollTop;
},
setScrollTop: function(y){
d.scrollTop = y;
},
isScrolling: function(){
return d.scrolling;
},
setScrolling: function(bool){
var oldVal = d.isScrolling();
d.scrolling = bool;
if(bool){
d.executeCallbacks('scroll');
if(oldVal !== bool){
d.executeCallbacks('scrollStarted');
}
}else{
d.executeCallbacks('scrollStopped');
}
},
getScrollDirection : function(){
return d.scrollDirection;
},
setScrollDirection : function(direction){
var oldDirection = d.getScrollDirection();
d.scrollDirection = direction;
if(direction === 'UP'){
d.executeCallbacks('scrollToTop');
if(direction !== oldDirection){
d.executeCallbacks('scrollToTopStarted');
}
}else if(direction === 'DOWN'){
d.executeCallbacks('scrollToBottom');
if(direction !== oldDirection){
d.executeCallbacks('scrollToBottomStarted');
}
}
},
init : function(){
d.setScrollTop($(document).scrollTop());
var timer = null;
$(window).scroll(function(){
d.setScrolling(true);
var x = d.getScrollTop();
setTimeout(function(){
var y = $(document).scrollTop();
d.setScrollTop(y);
if(x > y){
d.setScrollDirection('UP');
}else{
d.setScrollDirection('DOWN');
}
}, 100);
if(timer !== 'undefined' && timer !== null){
clearTimeout(timer);
}
timer = setTimeout(function(){
d.setScrolling(false);
d.setScrollDirection('NONE');
}, 200);
});
},
registerEvents : function(eventName, callback){
if(typeof eventName !== 'undefined' && typeof callback === 'function' && typeof d.eventRegister[eventName] !== 'undefined'){
d.eventRegister[eventName].push(callback);
}
},
executeCallbacks: function(eventName){
var callabacks = d.eventRegister[eventName];
for(var k in callabacks){
if(callabacks.hasOwnProperty(k)){
callabacks[k](d.getScrollTop());
}
}
}
};
return d;
})(document, $);
the code is available here: documentScrollEvents
Minor update in your answer. Use mouseover and out function.
$(document).ready(function() {
function ticker() {
$('#ticker li:first').slideUp(function() {
$(this).appendTo($('#ticker')).slideDown();
});
}
var ticke= setInterval(function(){
ticker();
}, 3000);
$('#ticker li').mouseover(function() {
clearInterval(ticke);
}).mouseout(function() {
ticke= setInterval(function(){ ticker(); }, 3000);
});
});
DEMO
I was trying too add a display:block property for social icons that was previously hidden on scroll event and then again hide after 2seconds. But
I too had a same problem as my code for timeout after first scroll would start automatically and did not had reset timeout idea. As it didn't had proper reset function.But after I saw David's idea on this question I was able to reset timeout even if someone again scrolled before actually completing previous timeout.
problem code shown below before solving
$(window).scroll(function(){
setTimeout(function(){
$('.fixed-class').slideUp('slow');
},2000);
});
edited and working code with reset timer if next scroll occurs before 2s
var timer=null;
$(window).scroll(function(){
$('.fixed-class').css("display", "block");
if(timer !== null) {
clearTimeout(timer);
}
timer=setTimeout(function(){
$('.fixed-class').slideUp('slow');
},2000);
});
My working code will trigger a hidden division of class named 'fixed-class' to show in block on every scroll. From start of latest scroll the timer will count 2 sec and then again change the display from block to hidden.
For more precision you can also check the scroll position:
function onScrollEndOnce(callback, target = null) {
let timeout
let targetTop
const startPosition = Math.ceil(document.documentElement.scrollTop)
if (target) {
targetTop = Math.ceil(target.getBoundingClientRect().top + document.documentElement.scrollTop)
}
function finish(removeEventListener = true) {
if (removeEventListener) {
window.removeEventListener('scroll', onScroll)
}
callback()
}
function isScrollReached() {
const currentPosition = Math.ceil(document.documentElement.scrollTop)
if (targetTop == null) {
return false
} else if (targetTop >= startPosition) {
return currentPosition >= targetTop
} else {
return currentPosition <= targetTop
}
}
function onScroll() {
if (timeout) {
clearTimeout(timeout)
}
if (isScrollReached()) {
finish()
} else {
timeout = setTimeout(finish, 500)
}
}
if (isScrollReached()) {
finish(false)
} else {
window.addEventListener('scroll', onScroll)
}
}
Usage example:
const target = document.querySelector('#some-element')
onScrollEndOnce(() => console.log('scroll end'), target)
window.scrollTo({
top: Math.ceil(target.getBoundingClientRect().top + document.documentElement.scrollTop),
behavior: 'smooth',
})
Here's an answer that doesn't use any sort of timer, thus in my case predicted when the scrolling actually ended, and is not just paused for a bit.
function detectScrollEnd(element, onEndHandler) {
let scrolling = false;
element.addEventListener('mouseup', detect);
element.addEventListener('scroll', detect);
function detect(e) {
if (e.type === 'scroll') {
scrolling = true;
} else {
if (scrolling) {
scrolling = false;
onEndHandler?.();
}
}
}
}