On a responsive site I'm developing I have my own little lightbox-script which opens images fullscreen while maintaining their aspect ratio. It's pretty simple, uses 2 divs (outer fullscreen-div with black background "lbBlack" and inner div with image "lbImg"):
//super small lightbox ;)
$("#posts").on("click", ".img", function(event) {
$('#lbBlack').css('top',$(document).scrollTop());
$('#lbImg').attr('src', $(this).attr('src'));
$('#lbBlack').css('width',$(window).width());
$('#lbBlack').css('height',window.innerHeight);
$('#lbBlack').fadeIn(500);
$('#lbImg').css('margin-top',((window.innerHeight-$('#lbImg').height()))/2);
document.body.style.overflow="hidden";
document.ontouchmove = function(event){
event.preventDefault();
}
$('#lbBlack').on("click", "#lbImg, body", function(event) {
$('#lbBlack').fadeOut(500);
document.body.style.overflow="visible";
document.ontouchmove = function(event){
return true;
}
});
});
For iOS, I had to add the ontouchmove-prevention, because body-overflow-hidden wasn't enough to avoid scrolling while the lightbox is opened.
Now the "big problem" for this working solution above: I want to enable zooming on the image. This is prevented with the "ontouchmove"-code.
Any ideas?
HTML-code:
<body>
<div id="lbBlack">
<img id="lbImg">
</div>.....
CSS-code:
#lbBlack {
display: none;
position: absolute;
left: 0;
background-color: black;
z-index: 2001;
text-align: center;
}
#lbBlack #lbImg {
max-height: 100%;
max-width: 100%;
position: relative;
}
So I think what I am looking for is a method to prevent scrolling while still maintaining the possibility to zoom. I still don't get it why body-overflow:hidden still has the ability to scroll on iOS??
Well, Raphael,
this might not be perfect, but it should get you going in the right direction. I tested on Android, my buddy who handles the Apple stuff is unavailable at the moment. Scrolling and other moving is disabled, but you can zoom. One problem, however, is when you are actually in the process of pinch zooming you can move the picture around. You could always snap the picture back to the center after the pinch zoom is complete. (That might even look neat).
Notice I added a method to the jQuery prototype and a property to the jQuery.Event prototype.
/*global console, $*/
/*jslint browser: true*/
(function () {
"use strict";
$.fn.detectPinch = function () {
var numTouches = 0;
// each finger touch triggers touchstart
// (even if one finger is already touching)
$(document).on('touchstart', function (event) {
// if numTouches is more than 1, reset it to 0
// or else you can have numTouches >= 2 when
// actually only one finger is touching
numTouches = (numTouches > 1) ? 0 : numTouches;
// if numTouches is 0 or 1, increment it
numTouches = (numTouches < 2) ? numTouches += 1 : 2;
console.log(numTouches + ' start');
}).on('touchend', function (event) {
// another safety check: only decrement if > 0
numTouches = (numTouches > 0) ? numTouches -= 1 : 0;
console.log(numTouches + ' end');
});
// all event objects inherit this property
$.Event.prototype.isPinched = function () {
return (numTouches === 2) ? true : false;
};
return this;
};
$(document).ready(function (event) {
// call the method we added to the prototype
$(document).detectPinch();
$("#posts").on("click", "img", function (event) {
$(this).css('visibility', 'hidden');
$('#lbBlack').css('top', $(document).scrollTop());
$('#lbImg').attr('src', $(this).attr('src'));
$('#lbBlack').css('width', $(window).width());
$('#lbBlack').css('height', window.innerHeight);
$('#lbBlack').fadeIn(500);
$('#lbImg').css('margin-top', ((window.innerHeight - $('#lbImg').height())) / 2);
document.body.style.overflow = "hidden";
});
$('#lbBlack').on("click", "#lbImg, body", function (event) {
$('#lbBlack').fadeOut(500);
$('#posts img').css('visibility', 'visible');
document.body.style.overflow = "visible";
});
}).on('touchmove', function (event) {
// prevent one-finger movements
if (!event.isPinched()) {
event.preventDefault();
console.log('prevented');
}
});
}());
Related
This code nearly works but has a slight problem which is where I'm hoping for your help.
The Goal: This goal of this script is to call the parseScroll(); function one time when the user wheels using the mouse.
The Problem: The code initially works. However, if you wheel with your finger on the mouse mutiple times within short proximilty,
the parseScroll(); function isn't called. It does this because it
hasn't realized that the previous wheel has ended since because of the
debouncing algorithm in place to keep the function from being called a
thousand times.
(Update): I found this article which seems to address what I'm looking for. Could someone help me understand it and recreate it in pure JavaScript? http://demos111.mootools.net/Mousewheel
Side Note: This question is specific to OS X but I would appreciate it
if a windows user could tell me if it is doing what it is supposed to
do in windows since I don't have a windows machine to test it with.
Here is a replica of the script that is giving me problems.
window.addEventListener('load', function() {
var scrollStatus = {
wheeling: false,
functionCall: false
};
var scrollTimer = false;
window.addEventListener('wheel', function(e) {
scrollStatus.wheeling = true;
if (!scrollStatus.functionCall) {
parseScroll(e);
scrollStatus.functionCall = true;
}
window.clearInterval(scrollTimer);
scrollTimer = window.setTimeout(function() {
scrollStatus.wheeling = false;
scrollStatus.functionCall = false;
}, 500);
});
function parseScroll(e) {
//console.log(scrollStatus.functionCall)
console.log(e.deltaY)
if (e.deltaY > 0) {
console.log('scrolled down')
}
if (e.deltaY < 0) {
console.log('scrolled up')
}
}
});
html,
body {
width: 100%;
height: 100%;
background: #333;
overflow: hidden;
color: #fff;
}
Please wheel on your mouse and open your web inspector console to see resulting behavior.
Please ask questions in the comments and revisit the question as I may change the description as I find better ways to describe the problem.
I would like my solution to be in JavaScript.
The problem seems to be that debounce function, as you figured out. All you do is change the millisecond interval, and that should fix it.
NOTE: I took out the HTML and CSS to make things less cluttered. I also edited the JS a bit to make it shorter - hope that isn't a problem!
window.addEventListener('load', function() {
var scrollStatus = {
wheeling: false,
functionCall: false
};
var scrollTimer = false;
window.addEventListener('wheel', function(e) {
scrollStatus.wheeling = true;
if (!scrollStatus.functionCall) {
//parseScroll here
console.log(e.deltaY)
if (e.deltaY > 0) {
console.log('scrolled down')
}
if (e.deltaY < 0) {
console.log('scrolled up')
}
scrollStatus.functionCall = true;
}
window.clearInterval(scrollTimer);
scrollTimer = window.setTimeout(function() {
scrollStatus.wheeling = false;
scrollStatus.functionCall = false;
}, 50); //set this millisecond to your liking
});
});
Edit, Updated
Try defining handler as named function, calling .removeEventListener after parseScroll called
window.addEventListener('load', function() {
var scrollStatus = {
wheeling: false,
functionCall: false
};
function wheel(e) {
scrollStatus.wheeling = true;
if (!scrollStatus.functionCall) {
scrollStatus.functionCall = true;
parseScroll(e);
window.removeEventListener("wheel", wheel, false)
}
window.clearInterval(scrollTimer);
scrollTimer = window.setTimeout(function() {
scrollStatus.wheeling = false;
scrollStatus.functionCall = false;
}, 500);
}
var scrollTimer = false;
window.addEventListener('wheel', wheel, false);
function parseScroll(e) {
//console.log(scrollStatus.functionCall)
console.log(e.deltaY)
if (e.deltaY > 0) {
console.log('scrolled down')
}
if (e.deltaY < 0) {
console.log('scrolled up')
}
}
});
html,
body {
width: 100%;
height: 100%;
background: #333;
overflow: hidden;
color: #fff;
}
Please wheel on your mouse and open your web inspector console to see resulting behavior.
I need to make one div draggable and sets it's left position in according to mouse position.
I've searched a bit and this is what i have so far:
container = $('<div></div>').appendTo($('body')).addClass('container');
someText = $('<div>Some text</div>').appendTo(container);
slider = $('<div></div>').appendTo(container);
slider.addClass('slider');
var isDragging = false;
slider.on('mousedown', function () {
isDragging = true;
});
$(window).on('mouseup', function () {
console.log('mouseup');
isDragging = false;
});
container.on('mouseleave', function () {
console.log('mouseleave');
isDragging = false;
});
container.on('mousemove', function (e) {
if (isDragging) {
var newLeft = parseInt((e.pageX - container.offset().left), 10);
console.log(newLeft);
slider.css('left', newLeft);
}
});
http://jsfiddle.net/w9gxxuvw/2/
The white box is one which should be draggable, but there are a few drawbacks.
First of all, while i drag with my LPM down i select upper text.
Secondly, on chrome when i drag it fast, it doesn't fire mouse up event, so 'slider' just follows cursor while it's moving inside 'container' and i need to click somewhere to stop.
It's not necessary for me to use jQuery, but i don't won't to use another big framework nor jquery plugins.
You can prevent text selection with the user-select CSS property:
container = $('<div></div>').appendTo($('body')).addClass('container');
someText = $('<div>Some text</div>').appendTo(container);
slider = $('<div></div>').appendTo(container);
slider.addClass('slider');
var isDragging = false;
slider.on('mousedown', function () {
isDragging = true;
});
$(window).on('mouseup', function () {
isDragging = false;
});
container.on('mouseleave', function () {
isDragging = false;
});
container.on('mousemove', function (e) {
if (isDragging) {
var newLeft = parseInt((e.pageX - container.offset().left), 10);
slider.css('left', newLeft);
}
});
.container {
display:block;
width:400px;
height:100px;
background: red;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
user-select: none;
}
.slider {
display:block;
width:10px;
height:10px;
background: #fff;
position:relative;
left: 0%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
I cannot reproduce the problem of the draggable sticking to the mouse in Chrome 42, Firefox 36 or Safari 7. Above example runs flawlessly for me.
Preventing the default action for text selection in the script seems more logical, it has deeper support than CSS user-select as well. Since (most of) the events are connected in this function, I'd nest them. It'll allow for a bit of optimisation. It also makes sense to unbind the mousemove, after several events you may generally start to notice sluggish behaviour otherwise.
...
container.on('mousedown', function(e) {
e.preventDefault();
});
slider.on('mousedown', function() {
$(window).one('mouseup', function() {
console.log('mouseup');
container.off('mousemove');
});
container.on('mousemove', function(e) {
var newLeft = Math.round(e.pageX-container.offset().left);
console.log(newLeft);
slider.css('left', newLeft);
})
.one('mouseleave', function() {
console.log('mouseleave');
container.off('mousemove');
});
});
http://jsfiddle.net/w9gxxuvw/8/
I am writing a small jQuery function and I seem to be having trouble.
What I am trying to do here is when the user scrolls down the page by 90px, a div tag should animate down (from top:-50px to top:0), and vice-versa when they scroll back to the top of the page.
The problem I am having is that the animation seems to be very slow and unresponsive at times. I test in 3 different browsers and different computers but I am having no joy.
Here is my code:
// Show div
var scrollValue = "90";
// Animate functions
var showHead = function (){
$(".element").animate({top: "0"}, 250);
}
var hideHead = function (){
$(".element").animate({top: "-50px"}, 250);
}
$(window).scroll(function() {
if ($(this).scrollTop() > scrollValue) {
showHead();
} else {
hideHead();
}
});
The .element properties:
.element { positoin:fixed; top:-50px; }
Could anyone figure out why my code the hide/showHead functions are so sloppy?
Thanks,
Peter
The scroll event is triggered several times and even though it is rate-limited it keeps being a rather intensive operation. Actually, you may be queuing several animations and the fx stack may be growing very quickly.
One possibility you can try is stopping all previous animations before triggering a new one. You can do this by using .stop().
$(".element").stop().animate({top: "0"}, 250);
The .stop() function also provides some other options which you can use to tweak it even more.
Try this one :
$(window).scroll(function() {
if (window.scrollY > scrollValue) {
showHead();
} else {
hideHead();
}
});
scroll events occurred many time durring user scrolling.
You need to check if your animation is in progress before starting the animation again.
Try this :
// Show div
var scrollValue = "90";
var inProgress = false;
// Animate functions
var showHead = function () {
if(inProgress)
return false;
//Animate only if the animation is not in progress
inProgress = true;
$(".element").animate({
top: "0"
},250,function(){
inProgress = false; //Reset when animation is done
});
}
var hideHead = function () {
if(inProgress)
return false;
//Animate only if the animation is not in progress
inProgress = true;
$(".element").animate({
top: "-50px"
}, 250,function(){
inProgress = false; //Reset when animation is done
});
}
$(window).scroll(function () {
if ($(this).scrollTop() > scrollValue) {
showHead();
} else {
hideHead();
}
});
Assuming you have position:fixed (or some other sort of styling making the bar visible when necessary):
var scrollheight = 90;
var $el = $('.element');
function showHead(){
$el.stop().animate({
top: '0px'
}, 250);
}
function hideHead(){
$el.stop().animate({
top: '-50px'
}, 250);
}
$(window).scroll(function(){
if ($(window).scrollTop() > scrollheight){
showHead();
}else{
hideHead();
}
});
example: http://jsfiddle.net/L4LfL/
try using queue: false and as Alexander said use .stop()
here jsfiddle
http://jsfiddle.net/hwbPz/
I think I know why this is happening but I am not sure of the best way to tackle it. Here is a jsFiddle you can refer to.
If you attempt to open and close a sub-menu in the jsFiddle (Click the + icon next to any link) and then open it again before it has fully closed it will become stuck. Now open the menu and attempt to open one of it's child sub-menu's and you will see that it's parent doesn't expand to accommodate it.
I believe this problem is caused because during the hide procedure jQuery applies an inline height to the element and if you attempt to open it before it finishes animating it assumes that it is the final height of the element.
I considered storing the height of each element at the very start and using that to animate towards, however the problem with this approach is that menus with sub-menus height changes all the time depending on whether it's sub-menus are open and this value is never a constant.
Is there anyway to tell jQuery to ignore the element's inline height and calculate what it's true height should be?
Here is the jQuery used, for the HTML and CSS please see the jsFiddle as they are rather lengthy:
jQuery(document).ready(function($){
var legacyMode = $('html').hasClass('oldie');
var titles = {normal: "Show sub-topics", active: "Hide sub-topics"};
var sub_sections = $('ul#map li:has(ul.child)');
sub_sections.each(function() {
if (!$(this).find('li.active').length && !$(this).hasClass('active')) {
var toggle = $('<a class="toggle" href="#"></a>').attr('title', titles.normal).insertBefore($(this).children('ul.child'));
var child = $(this).children('ul.child').hide();
toggle.data('container', this);
}
});
$('a.toggle').click(function (event) {
event.preventDefault();
var target = $(this).siblings('ul.child');
if($(this).hasClass('active')) {
toggleDisplay(target, false);
$(this).removeClass('active').attr('title', titles.normal);
} else {
toggleDisplay(target, true);
$(this).addClass('active').attr('title', titles.active);
}
function toggleDisplay(target, on) {
var mode = (on) ? "show" : "hide";
if (!legacyMode) {
target.stop(true, false).animate({height: mode, opacity: mode}, 500);
} else {
// Omits opacity to avoid using proprietary filters in legacy browsers
target.stop(true, false).animate({height: mode}, 500);
}
}
});
});
This is happening because of the properties you're passing on the stop() method, before animate().
The second property in the method stop(true, false) specifies whether the animation should jump to the last frame or not. In you code, since it is false, it gets stuck at the stage the click is registered next on the anchor tag.
Change it to .stop(true, true) and it will work as expected!
Okay so I made a working solution... it isn't quite as straight forward as I would of hoped but anyway here is the working code:
jQuery(document).ready(function($){
var legacyMode = $('html').hasClass('oldie');
var titles = {normal: "Show sub-topics", active: "Hide sub-topics"};
var sub_sections = $('ul#map li:has(ul.child)');
sub_sections.each(function() {
if (!$(this).find('li.active').length && !$(this).hasClass('active')) {
var child = $(this).children('ul.child');
if (!legacyMode) {
child.css({height : '0px', opacity : 0, display: 'none'});
} else {
// Omits opacity to avoid using proprietary filters in legacy browsers
child.css({height : '0px', display: 'none'});
}
var toggle = $('<a class="toggle" href="#"></a>').attr('title', titles.normal).insertBefore(child);
toggle.data('container', this);
}
});
$('a.toggle').click(function (event) {
event.preventDefault();
var target = $(this).siblings('ul.child');
if($(this).hasClass('active')) {
toggleDisplay(target, false);
$(this).removeClass('active').attr('title', titles.normal);
} else {
toggleDisplay(target, true);
$(this).addClass('active').attr('title', titles.active);
}
function toggleDisplay(target, on) {
var targetOpacity = 0;
var targetHeight = 0;
if (on) {
// Get height of element once expanded by creating invisible clone
var clone = target.clone().attr("id", false).css({visibility:"hidden", display:"block", position:"absolute", height:""}).appendTo(target.parent());
targetHeight = clone.height();
targetOpacity = 1;
console.log(clone.height());
clone.remove();
target.css({display : 'block'});
}
if (!legacyMode) {
target.stop(true, false).animate({height: targetHeight + "px" , opacity: targetOpacity}, {
duration: 500,
complete: function() {
if (on) {
$(this).css({height : '', opacity : ''});
} else {
$(this).css({display : 'none'});
}
}
});
} else {
// Omits opacity to avoid using proprietary filters in legacy browsers
target.stop(true, false).animate({height: targetHeight + "px"}, {
duration: 500,
complete: function() {
if (on) {
$(this).css({height : ''});
} else {
$(this).css({display : 'none'});
}
}
});
}
}
});
});
See it in action: http://jsfiddle.net/xetCd/24/ (Click any combination of toggle's in any order any amount of times and it should stay smooth).
Tested with: IE7, IE8 (set var legacyMode to true for best results), Firefox 15, Chrome 23
How does it work? Well I no longer use the show and hide animation targets as these are not flexible enough. For this reason I have to hide the uls at initialization a little differently:
var child = $(this).children('ul.child');
if (!legacyMode) {
child.css({height : '0px', opacity : 0, display: 'none'});
} else {
// Omits opacity to avoid using proprietary filters in legacy browsers
child.css({height : '0px', display: 'none'});
}
var toggle = $('<a class="toggle" href="#"></a>').attr('title', titles.normal).insertBefore(child);
Now on to the juicy bit, how to calculate the target height of the element when it could be at any stage of the animation and have any amount of sub-menu's open or closed. I got around this by creating a visibility : hidden duplicate of the element and forcing it to take up it's regular height, I also gave it position : absolute so that it doesn't appear to take up any space in the document. I grab it's height and delete it:
var targetOpacity = 0;
var targetHeight = 0;
if (on) {
// Get height of element once expanded by creating invisible clone
var clone = target.clone().attr("id", false).css({visibility:"hidden", display:"block", position:"absolute", height:""}).appendTo(target.parent());
targetHeight = clone.height();
targetOpacity = 1;
console.log(clone.height());
clone.remove();
target.css({display : 'block'});
}
Then I animate it and make sure to reset the display and remove the height (so sub-menu's can expand their parent) properties:
if (!legacyMode) {
target.stop(true, false).animate({height: targetHeight + "px" , opacity: targetOpacity}, {
duration: 500,
complete: function() {
if (on) {
$(this).css({height : '', opacity : ''});
} else {
$(this).css({display : 'none'});
}
}
});
} else {
// Omits opacity to avoid using proprietary filters in legacy browsers
target.stop(true, false).animate({height: targetHeight + "px"}, {
duration: 500,
complete: function() {
if (on) {
$(this).css({height : ''});
} else {
$(this).css({display : 'none'});
}
}
});
}
Thanks for reading.
I ran into this same problem with jQuery 1.8.3, and found that upgrading to v1.12.2 fixed it. For anyone else running into this, that may be a solution. Let me know if this doesn't work for you.
How can I trigger a function when the browser window stops scrolling? Either by mouse wheel, click, space bar or arrow key? Is there any event for such action? I have tried to search online but couldn't get any solution. I'm fine with a jQuery solution.
There's no "event" but you could make your own, like this:
$(function() {
var timer;
$(window).scroll(function() {
clearTimeout(timer);
timer = setTimeout(function() {
$(window).trigger("scrollStop");
}, 250);
});
});
Then you could bind to it, like this:
$(window).bind("scrollStop", function() {
alert("No one has scrolled me in 250ms, where's the love?");
});
This creates an event, there's no "stop" for this, but you can define your own... in this case "stop" is defined as "hasn't scrolled in 250ms", you can adjust the timer to your liking, but that's the idea.
Also, if you're just doing one thing there's no need for an event, just put your code where I'm calling $(window).trigger("scrollStop") and it'll run n milliseconds after the scroll stops.
The Non-jQuery Javascript version of the chosen answer:
var elem = document.getElementById('test');
(() => {
var onScrollStop = (evt) => {
// you have scroll event as evt (for any additional info)
var scrollStopEvt = new CustomEvent('scrolling-stopped', {detail: 'foobar stopped :)'});
elem.dispatchEvent(scrollStopEvt);
}
var scrollStopLag = 300 // the duration to wait before onScrollStop is triggerred.
var timerID = 0;
const handleScroll = (evt) => {
clearInterval(timerID);
timerID = setTimeout(
() => onScrollStop(evt),
scrollStopLag
)
}
elem.addEventListener('scroll', handleScroll);
})()
elem.addEventListener(
'scrolling-stopped',
(evt) => console.log(evt.detail)
)
#test {
height: 300px;
width: 300px;
overflow: auto;
}
#test #test-inner {
height: 3000px;
background: linear-gradient(lightseagreen 0%, lightyellow 40%, lightcoral 100%);
}
<h4>Scroll inside the green box below:</h4>
<div id="test">
<div id="test-inner"></div>
</div>
Good Luck...
P.S. I am a BIG fan of jQuery :)