In IOS8 Safari there is a new bug with position fixed.
If you focus a textarea that is in a fixed panel, safari will scroll you to the bottom of the page.
This makes all sorts of UIs impossible to work with, since you have no way of entering text into textareas without scrolling your page all the way down and losing your place.
Is there any way to workaround this bug cleanly?
#a {
height: 10000px;
background: linear-gradient(red, blue);
}
#b {
position: fixed;
bottom: 20px;
left: 10%;
width: 100%;
height: 300px;
}
textarea {
width: 80%;
height: 300px;
}
<html>
<body>
<div id="a"></div>
<div id="b"><textarea></textarea></div>
</body>
</html>
Based on this good analysis of this issue, I've used this in html and body elements in css:
html,body{
-webkit-overflow-scrolling : touch !important;
overflow: auto !important;
height: 100% !important;
}
I think it's working great for me.
The best solution I could come up with is to switch to using position: absolute; on focus and calculating the position it was at when it was using position: fixed;. The trick is that the focus event fires too late, so touchstart must be used.
The solution in this answer mimics the correct behavior we had in iOS 7 very closely.
Requirements:
The body element must have positioning in order to ensure proper positioning when the element switches to absolute positioning.
body {
position: relative;
}
The Code (Live Example):
The following code is a basic example for the provided test-case, and can be adapted for your specific use-case.
//Get the fixed element, and the input element it contains.
var fixed_el = document.getElementById('b');
var input_el = document.querySelector('textarea');
//Listen for touchstart, focus will fire too late.
input_el.addEventListener('touchstart', function() {
//If using a non-px value, you will have to get clever, or just use 0 and live with the temporary jump.
var bottom = parseFloat(window.getComputedStyle(fixed_el).bottom);
//Switch to position absolute.
fixed_el.style.position = 'absolute';
fixed_el.style.bottom = (document.height - (window.scrollY + window.innerHeight) + bottom) + 'px';
//Switch back when focus is lost.
function blured() {
fixed_el.style.position = '';
fixed_el.style.bottom = '';
input_el.removeEventListener('blur', blured);
}
input_el.addEventListener('blur', blured);
});
Here is the same code without the hack for comparison.
Caveat:
If the position: fixed; element has any other parent elements with positioning besides body, switching to position: absolute; may have unexpected behavior. Due to the nature of position: fixed; this is probably not a major issue, since nesting such elements is not common.
Recommendations:
While the use of the touchstart event will filter out most desktop environments, you will probably want to use user-agent sniffing so that this code will only run for the broken iOS 8, and not other devices such as Android and older iOS versions. Unfortunately, we don't yet know when Apple will fix this issue in iOS, but I would be surprised if it is not fixed in the next major version.
I found a method that works without the need to change to position absolute!
Full uncommented code
var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
scrollPos = $(document).scrollTop();
});
var savedScrollPos = scrollPos;
function is_iOS() {
var iDevices = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
];
while (iDevices.length) {
if (navigator.platform === iDevices.pop()){ return true; }
}
return false;
}
$('input[type=text]').on('touchstart', function(){
if (is_iOS()){
savedScrollPos = scrollPos;
$('body').css({
position: 'relative',
top: -scrollPos
});
$('html').css('overflow','hidden');
}
})
.blur(function(){
if (is_iOS()){
$('body, html').removeAttr('style');
$(document).scrollTop(savedScrollPos);
}
});
Breaking it down
First you need to have the fixed input field toward the top of the page in the HTML (it's a fixed element so it should semantically make sense to have it near the top anyway):
<!DOCTYPE HTML>
<html>
<head>
<title>Untitled</title>
</head>
<body>
<form class="fixed-element">
<input class="thing-causing-the-issue" type="text" />
</form>
<div class="everything-else">(content)</div>
</body>
</html>
Then you need to save the current scroll position into global variables:
//Always know the current scroll position
var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
scrollPos = $(document).scrollTop();
});
//need to be able to save current scroll pos while keeping actual scroll pos up to date
var savedScrollPos = scrollPos;
Then you need a way to detect iOS devices so it doesn't affect things that don't need the fix (function taken from https://stackoverflow.com/a/9039885/1611058)
//function for testing if it is an iOS device
function is_iOS() {
var iDevices = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
];
while (iDevices.length) {
if (navigator.platform === iDevices.pop()){ return true; }
}
return false;
}
Now that we have everything we need, here is the fix :)
//when user touches the input
$('input[type=text]').on('touchstart', function(){
//only fire code if it's an iOS device
if (is_iOS()){
//set savedScrollPos to the current scroll position
savedScrollPos = scrollPos;
//shift the body up a number of pixels equal to the current scroll position
$('body').css({
position: 'relative',
top: -scrollPos
});
//Hide all content outside of the top of the visible area
//this essentially chops off the body at the position you are scrolled to so the browser can't scroll up any higher
$('html').css('overflow','hidden');
}
})
//when the user is done and removes focus from the input field
.blur(function(){
//checks if it is an iOS device
if (is_iOS()){
//Removes the custom styling from the body and html attribute
$('body, html').removeAttr('style');
//instantly scrolls the page back down to where you were when you clicked on input field
$(document).scrollTop(savedScrollPos);
}
});
I was able to fix this for select inputs by adding an event listener to the necessary select elements, then scrolling by an offset of one pixel when the select in question gains focus.
This isn't necessarily a good solution, but it's much simpler and more reliable than the other answers I've seen here. The browser seems to re-render/re-calculate the position: fixed; attribute based on the offset supplied in the window.scrollBy() function.
document.querySelector(".someSelect select").on("focus", function() {window.scrollBy(0, 1)});
Much like Mark Ryan Sallee suggested, I found that dynamically changing the height and overflow of my background element is the key - this gives Safari nothing to scroll to.
So after the modal's opening animation finishes, change the background's styling:
$('body > #your-background-element').css({
'overflow': 'hidden',
'height': 0
});
When you close the modal change it back:
$('body > #your-background-element').css({
'overflow': 'auto',
'height': 'auto'
});
While other answers are useful in simpler contexts, my DOM was too complicated (thanks SharePoint) to use the absolute/fixed position swap.
Cleanly? no.
I recently had this problem myself with a fixed search field in a sticky header, the best you can do at the moment is keep the scroll position in a variable at all times and upon selection make the fixed element's position absolute instead of fixed with a top position based on the document's scroll position.
This is however very ugly and still results in some strange back and forth scrolling before landing on the right place, but it is the closest I could get.
Any other solution would involve overriding the default scroll mechanics of the browser.
Haven't dealt with this particular bug, but maybe put an overflow: hidden; on the body when the text area is visible (or just active, depending on your design). This may have the effect of not giving the browser anywhere "down" to scroll to.
A possible solution would be to replace the input field.
Monitor click events on a div
focus a hidden input field to render the keyboard
replicate the content of the hidden input field into the fake input field
function focus() {
$('#hiddeninput').focus();
}
$(document.body).load(focus);
$('.fakeinput').bind("click",function() {
focus();
});
$("#hiddeninput").bind("keyup blur", function (){
$('.fakeinput .placeholder').html(this.value);
});
#hiddeninput {
position:fixed;
top:0;left:-100vw;
opacity:0;
height:0px;
width:0;
}
#hiddeninput:focus{
outline:none;
}
.fakeinput {
width:80vw;
margin:15px auto;
height:38px;
border:1px solid #000;
color:#000;
font-size:18px;
padding:12px 15px 10px;
display:block;
overflow:hidden;
}
.placeholder {
opacity:0.6;
vertical-align:middle;
}
<input type="text" id="hiddeninput"></input>
<div class="fakeinput">
<span class="placeholder">First Name</span>
</div>
codepen
None of these solutions worked for me because my DOM is complicated and I have dynamic infinite scroll pages, so I had to create my own.
Background: I am using a fixed header and an element further down that sticks below it once the user scrolls that far down. This element has a search input field. In addition, I have dynamic pages added during forward and backwards scroll.
Problem: In iOS, anytime the user clicked on the input in the fixed element, the browser would scroll all the way to the top of the page. This not only caused undesired behavior, it also triggered my dynamic page add at the top of the page.
Expected Solution: No scroll in iOS (none at all) when the user clicks on the input in the sticky element.
Solution:
/*Returns a function, that, as long as it continues to be invoked, will not
be triggered. The function will be called after it stops being called for
N milliseconds. If `immediate` is passed, trigger the function on the
leading edge, instead of the trailing.*/
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this, args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
function is_iOS() {
var iDevices = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
];
while (iDevices.length) {
if (navigator.platform === iDevices.pop()) { return true; }
}
return false;
}
$(document).on("scrollstop", debounce(function () {
//console.log("Stopped scrolling!");
if (is_iOS()) {
var yScrollPos = $(document).scrollTop();
if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
$('#searchBarDiv').css('position', 'absolute');
$('#searchBarDiv').css('top', yScrollPos + 50 + 'px'); //50 for fixed header
}
else {
$('#searchBarDiv').css('position', 'inherit');
}
}
},250,true));
$(document).on("scrollstart", debounce(function () {
//console.log("Started scrolling!");
if (is_iOS()) {
var yScrollPos = $(document).scrollTop();
if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
$('#searchBarDiv').css('position', 'fixed');
$('#searchBarDiv').css('width', '100%');
$('#searchBarDiv').css('top', '50px'); //50 for fixed header
}
}
},250,true));
Requirements: JQuery mobile is required for the startsroll and stopscroll functions to work.
Debounce is included to smooth out any lag created by the sticky element.
Tested in iOS10.
I just jumped over something like this yesterday by setting height of #a to max visible height (body height was in my case) when #b is visible
ex:
<script>
document.querySelector('#b').addEventListener('focus', function () {
document.querySelector('#a').style.height = document.body.clientHeight;
})
</script>
ps: sorry for late example, just noticed it was needed.
This is now fixed in iOS 10.3!
Hacks should no longer be needed.
I had the issue, below lines of code resolved it for me -
html{
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
I have been trying to find a working example of this but I think I am missing something very basic. I have a function to move the position of an element that is relative, I want to stop the element moving once the left element goes past 300px. I am using this code to move the element:
function tele_right(){
$(".tele-wrapper").animate({"left": "+=15px"}, 25);
}
I wanted to use something like this code to do something once in the DOM the left position hits 300:
if($('.tele-wrapper').css('left') == '300px') {
console.log('yay');
}
Any help would be much appreciated.
you can simply achieve this using offset() and a setTimeout: DEMO
function tele_right(){
var left=$(".tele-wrapper").offset().left;
if(left<300){
$(".tele-wrapper").animate({"left": "+=15px"}, 25);
setTimeout(tele_right,25);
}
else{
alert('passed 300 pixels');
}
}
tele_right();
If I was you, I'd just set the left property of your element to 0, then would do the animation all the way to 300px and define its duration to the desired value as follows:
$(document).ready(function() {
$(".tele-wrapper").animate({"left": "300px"}, 1000);
// 1000ms = 1 second, set this to whatever you like
});
.tele-wrapper {
position:relative;
left:0
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="tele-wrapper">tele-wrapper</div>
Here's my simple code.It contains division, which moves on dragging with mouse (on x axis) anywhere on screen.Everything works perfect on first drag, but on second division comes back to it's original position, which is centre of screen.I know why does this happen, but can't figure out how to fix it.My point is to continue moving division from coords where previous drags moved it.
<!DOCTYPE html>
<html>
<head>
</head>
<body style="overflow:hidden">
<style>
#screen
{
width:200px;
height:300px;
position:absolute;
top:0;bottom:0;left:0;right:0;
margin:auto;
border-style:solid;
}
</style>
<script>
var exit=0;
document.onmousedown=function(e)
{
exit=1;
x=e.pageX;
document.onmousemove= function(e)
{
if(exit==1)
{
y=e.pageX;
final=x-y;
document.getElementById("screen").innerHTML=final;
document.getElementById("screen").style.left=final+"px";
}
};
};
document.onmouseup=function(e)
{
exit=0;
}
</script>
<div id="screen">
</body>
</html>
You want to store the "final" value so that you can use it as an offset on the next click. What's happening is that you are using the mouse down and mousemove X difference to move the object, but on second click you are not taking into consideration that the object was offseted by the previous "final" value and it has to be moved with that offset in mind!
Here's the code snippet working, and the jsfiddle link below:
var exit=0;
var final = 0;
document.onmousedown=function(e)
{
exit=1;
x=e.pageX + final;
document.onmousemove= function(e)
{
if(exit==1)
{
y=e.pageX;
final=x-y;
document.getElementById("screen").innerHTML=final;
document.getElementById("screen").style.left=final+"px";
}
};
};
document.onmouseup=function(e)
{
exit=0;
}
jsfiddle:
http://jsfiddle.net/dcasadevall/NELWW/
I'm trying to make some DOM element rotate smoothly around a fixed point. I'm writing this from scratch using jQuery and no matter what update speed I choose for the setInterval or how small I go with the amount of degrees the orbit advances on each loop, I get this janky staircase animation effect. I've tried using jquery's .animate instead of the .css hoping it would smooth things out but I cant seem to get it to work. Any help is appreciated.
In other words, it's not as smooth as rotating an image in HTML5 canvas. I want to make it smoother.
Here is a jsFiddle demonstrating the issue.
Notice how the animation is not quite smooth?
For reference, here is the code:
HTML
<div id="div"></div>
<div class="dot"></div>
<button class="stop">STOP</button>
<button class="start">START</button>
CSS
#div{
position:absolute;
height: 20px;
width: 20px;
background-color: #000;
}
.dot{
position:absolute;
width: 5px;
height: 5px;
background-color: #000;
}
button{
position:absolute;
}
.stop{
top:200px;
}
.start{
top:225px;
}
THE ALL IMPORTANT JAVASCRIPT
$(document).ready(function(){
$('#div').data('angle', 90);
var interval;
$('.stop').on('click', function(){
if(interval){
clearInterval(interval);
interval = undefined;
}
});
$('.start').on('click', function(){
if(!interval){
interval = setBoxInterval();
}
});
interval = setBoxInterval();
});
function drawOrbitingBox(degrees){
var centerX = 100,
centerY = 100,
div = $('#div'),
orbitRadius = 50;
//dot might not be perfectly centered
$('.dot').css({left:centerX, top:centerY});
//given degrees (in degrees, not radians), return the next x and y coords
function coords(degrees){
return {left:centerX + (orbitRadius * Math.cos((degrees*Math.PI)/180)),
top :centerY - (orbitRadius * Math.sin((degrees*Math.PI)/180))};
}
//increment the angle of the object and return new coords through coords()
function addDegrees(jqObj, degreeIncrement){
var newAngle = jqObj.data('angle') + degreeIncrement;
jqObj.data('angle', newAngle);
return coords(newAngle);
}
//change the left and top css property to simulate movement
// I've tried changing this to .animate() and using the difference
// between current and last position to no avail
div.css(addDegrees(div, degrees), 1);
}
function setBoxInterval(){
var interval = window.setInterval(function(){
drawOrbitingBox(-0.2); //This is the degree increment
}, 10); //This is the amount of time it takes to increment position by the degree increment
return interval;
}
I'd rather not resort to external libraries/plugins but I will if that's the accepted way of doing this kind of stuff. Thank you for your time.
That's because the value you set for top and left properties is rounded up. You should try using CSS Transforms.
Combining CSS Animations/Transitions and CSS Transforms you should also be able to get the animation without JavaScript.
Oh, I run into that myself!
There is actually nothing you can do, the stuttering you see is the pixel size. The pixel is the minimal step for css based animations, you can't do "half pixels" or "0.2 pixels". You will see that the same keeps happening with css3 animations.
The only solution is to speed up your animation, i'm afraid.
Also, cosndsider using rquestAnimationFrame instead of interval: https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
I have an HTML element that I need to track another element. Specifically, I need to have the top left and top right corners of both elements be positioned the same. When a window gets resized, the resize event gets triggered and I can adjust the position of the dependent element. However, if the element being tracked is repositioned (but not resized), I do not see any DOM event.
How can we find out if a DOM element has been moved? We are using the latest jQuery.
Here is a code sample.
Note that elementOne and mouseTracking divs are there to show elements that get moved for "some" reason that is outside the control of my code.
This code works for the elementOne case.
MouseTrackingTracker does not track a moving element.
ResizerTracker does not put the border around the complete text in the overflow case.
I would like the trackingDivs to move and resize no matter the reason for the tracked element's reasons for changing.
This code relies on the window resize being the hooked event. Hooking some event that fires when the element changes its dimensions is closer to what I need.
<!DOCTYPE html>
<html>
<head>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6/jquery.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.js"></script>
<style type="text/css">
#elementOne { float : right;width : 200px; display:inline-block}
#resizer { float : left; display:inline-block}
.trackedDiv { width:50px; height:50px; background-color: blue }
.trackingDiv { position:absolute; z-index: 1; border:3px green; border-style: solid;}
</style>
<script>
$(function() {
$( window ).bind("resize",function(){
$("#elementOne").trigger("reposition");
$("#mouseTracking").trigger("reposition");
$("#resizer").trigger("reposition");
});
var repositionFunction = function(selfish, element){
var self = $(selfish);
var offset = self.offset();
var selfTop = offset.top;
var selfLeft = offset.left;
var selfWidth = self.width();
var selfHeight = self.height();
$(element).css({
top: selfTop,
left: selfLeft,
width : selfWidth,
height : selfHeight
});
}
$(document).mousemove(function(ev){
$("#mouseTracking").position({
my: "left bottom",
of: ev,
offset: "3 -3",
collision: "fit"
});
});
var timedShort = function() {
$('#resizer').html("Really short").resize();
setTimeout(timedLong, 10000);
}
var timedLong = function() {
$('#resizer').html("Really longggggggggggggggggggggggggggggggggggggggggggggggggggg text").resize();
setTimeout(timedShort, 10000);
}
setTimeout(timedLong, 10000);
$("#elementOne").bind("reposition",
function() { repositionFunction(this, "#elementOneTracker"); });
$("#mouseTracking").bind("reposition",
function() { repositionFunction(this, "#mouseTrackingTracker"); });
$("#resizer").bind("reposition",
function() { repositionFunction(this, "#resizerTracker"); });
});
</script>
</head>
<body>
<div class="trackedDiv" id="mouseTracking">tracks mouse</div>
<div class="trackingDiv" id="mouseTrackingTracker"></div>
<div style="clear:both;"></div>
<div class="trackedDiv" id="resizer">resizer: resizes</div>
<div class="trackingDiv" id="resizerTracker"></div>
<div class="trackedDiv" id="elementOne">elementOne: floats to the right</div>
<div class="trackingDiv" id="elementOneTracker"></div>
</body>
</html>
You can fire custom events with jquery whenever you reposition the element.
$( window ).bind("resize",function(){
$("#elementOne").css({
top: 200,
left: 200
}).trigger("reposition");
});
// and now you can listen to a "reposition event"
$("#elementOne").bind("reposition",function(){
var self = $(this);
$("#elementTwo").css({
top: self.css("top"),
left: self.css("left")
});
});
So you can provide event hooks yourself with some manual coding, which is useful since cool events like DOMAttrModified and so on, are not fully supported in all browsers. The downside, you have to do it all yourself.
Unfortunately, there are no reliable events to tell you when an element moves or is resized. You could resort to polling the element, though that won't necessarily be the most performant solution:
setInterval(repositionElement, 10);
Another option is to make your element "track" the other element purely through CSS. For this to work, you'll need a "wrapper" around the element you're tracking, and the other element:
#wrapper-around-element-to-track
{
position: relative;
}
#tracked-element
{
position: absolute;
/* set top and left to position, if necessary */
}
#tracking-element
{
position: absolute;
/* set top and left to position, if necessary */
}
Since you're already using jQuery, you can also use the resize event plugin to simulate the resize event on any element, but if I recall the last time I looked at it, it simply does the polling like I mentioned.
There is the DOMAttrModified event, but its only impleneted in Firefox and Chrome. But as you need a JavaScript function to start the element moving, you can firing a custom event with Jquery in this place.