Is Google Chrome on Windows having problems to render iframe scrollbars?
I wrote a very simple code to show what is happening (at least with me on chrome 52.0.2743.82 m):
<button>Toggle visibility</button>
<br />
<iframe scrolling="yes" seamless src="https://en.wikipedia.org/wiki/Lorem_ipsum" frameborder="0" style="width: 700px; height: 300px"></iframe>
<script type="text/javascript">
$("button").on("click", function() {
$("iframe").toggle();
});
</script>
Plunker link to code
When the page is loaded, the iframe as it scrollbar are visible.
Hide and show the iframe clicking the button. The scrollbar disappears.
This issue apparently occurs only in chrome.
Anyone is experiencing this too? Any fixes/workarounds?
It seems that bug appeared with the update Chrome 52.0.2743.82 (http://googlechromereleases.blogspot.fr/2016/07/stable-channel-update.html)
One possible workaround is to use the attribute visibility with position: absolute instead of display to show or hide the iframe.
A chrome bug ticket exists for this item: https://bugs.chromium.org/p/chromium/issues/detail?id=641881
I've had this problem, and using visibility instead of display: none wasn't an option.
My workaround was to set overflow: scroll on the <body> of the document being displayed in the iframe, whenever I set the iframe to be visible again. This seems to force the scrollbar to appear on the iframe again. You can then reset the overflow to its old value, and the scrollbar will remain on the iframe. You need to wait for a repaint before you can reset the overflow, though, so I put this in a timeout with delay 0.
function showIframe(iframe) {
var iframeBody = iframe.contentDocument.body;
$(iframe).show();
var oldOverflow = iframeBody.css("overflow");
iframeBody.css("overflow", "scroll");
window.setTimeout(function () {
iframeBody.css("overflow", oldOverflow);
}, 0);
}
There is a "flash" of scrollbar with this workaround if the iframe in question doesn't need to scroll, though, so it might be worth using the visibility workaround for that brief moment where the repaint is required, to avoid the flash.
Here's a workaround I've developed for an application I'm building. It has multiple <iframe> elements in a Foundation tab-control.
I used MutationObserver to observe when the <iframe>'s parent element (a Foundation div.tabs-content div.content element) becomes active, then I toggle the iframe's document's overflow property. The runtime effect is imperceivable.
I originally wanted to observe the <iframe> directly, however no DOM mutation events were raised when the iframe itself's changed display property, I guess because technically speaking element.style values are not part of the DOM-structure proper.
Here's my code (Vanilla.js, no jQuery). If you're using in your application you will want to replace my visibility-detection code with something that is applicable to your document:
window.addEventListener('DOMContentLoaded', function(e) {
var observer = new MutationObserver( onContentMutated );
var options = { attributes: true, childList: false, characterData: false, subtree: false, attributeFilter: ['class'] };
var iframeContainers = document.querySelectorAll('.tabs-content .content');
for(var i = 0; i < iframeContainers.length; i++) {
observer.observe( iframeContainers[i], options );
}
});
function onContentMutated(mutations) {
for(var i = 0; i < mutations.length; i++) {
var m = mutations[i];
var thisIsNowAnActiveTab = m.target.classList.contains('active');
if( thisIsNowAnActiveTab ) {
// get the corresponding iframe and fiddle with its DOM
var iframes = m.target.getElementsByTagName("iframe");
if( iframes.length == 0 ) continue;
var iframe = iframes[0];
iframe.contentWindow.document.documentElement.style.overflow = 'hidden';
// the timeout is to trigger Chrome to recompute the necessity of the scrollbars, which makes them visible again. Because the timeout period is 0 there should be no visible change to users.
setTimeout( function(s) {
s.overflow = 'auto';
}, 0, iframe.contentWindow.document.documentElement.style );
}
console.log( m.type );
}
}
For the given example you can do:
$("iframe").toggle(1)
In my case, it worked by setting back the height:
$("iframe").css("height", "100%")
I had a similar issue on Chrome with an iframe embedded into a jQuery UI tab. When the tab containing the iframe is first displayed, the scrollbar appears. But when I switch to another tab and back to the tab with the iframe then the scrollbar disappears. All the solutions proposed here didn't work for me.
Here is what I did to fix the issue :
First, I create the tabs :
$("#mytabs").tabs();
Then I bind a function to the event "tabsactivate" and I check if the target tab is the one containing the iframe. If it is the case I call a function fixChromeScrollBar() described later on :
$("#mytabs").on("tabsactivate", function(event, ui) {
if ($(event.originalEvent.target).attr("href") == "#mytab-with-iframe") {
fixChromeScrollBar();
}
});
And finally here is the function fixChromeScrollBar() which sets the overflow style attribute of the iframe body (as already said) to either "scroll" or "auto". I noticed that when I only define the "auto" or "scroll" value then if I switch to another tab and back to the iframe I lose the scrollbars. The only way to maintain them is to alternate between the two values each time the iframe appears. It is weird but it works :
function fixChromeScrollBar() {
var iFrameBody = $("#myiframe").contents().find("body");
var originalOverflow = $(iFrameBody).css("overflow");
if (originalOverflow == "visible" || originalOverflow == "auto") {
$(iFrameBody).css("overflow", "scroll");
} else {
$(iFrameBody).css("overflow", "auto");
}
}
You can notice that this method is called only if you switch to the tab containing the iframe so if you click multiple times on this tab without switching to another one this code will only be executed the first time.
Apparently setting src refreshes iframe in chrome, for given example code will be:
<script type="text/javascript">
$("button").on("click", function() {
$('iframe').toggle().attr('src', function(i, val) { return val; });
});
</script>
I adapted Dai's example to my own React IFrame component. I have an iframe within a tab panel, which is itself in a collapsible panel. When either of those get toggled, I force the iframe to repaint. It works wonderfully.
private iframe: HTMLIFrameElement;
private displayObserver: MutationObserver;
componentDidMount() {
// Detect style attribute changes for the containing collapsible components
// If the display goes from 'none' to something else, then we need to redraw the iframe
// so we get the scrollbar back as it should be.
if (isChrome()) {
this.displayObserver = new MutationObserver(this.onContentMutated);
const options = { attributes: true, childList: false, characterData: false, subtree: false, attributeFilter: ['style'] };
const tabPanelAncestor = this.findAncestor(this.iframe, '.tab-panel-content');
if (tabPanelAncestor) {
this.displayObserver.observe(tabPanelAncestor, options);
}
const collapsibleAncestor = this.findAncestor(this.iframe, '.collapsible');
if (collapsibleAncestor) {
this.displayObserver.observe(collapsibleAncestor, options);
}
}
}
private readonly onContentMutated = (mutations: Array<MutationRecord>) => {
R.forEach( (mutation) => {
const targetElement = mutation.target as Element;
const style = targetElement.getAttribute('style');
if (style && !style.match(/display: none/)) {
this.iframe.contentWindow.location.reload(true);
}
}, mutations);
}
private readonly findAncestor = (element: HTMLElement, sel: string): Node | null => {
if (typeof element.closest === 'function') {
return element.closest(sel) || null;
}
let ancestor: HTMLElement | null = element;
while (ancestor) {
if (ancestor.matches(sel)) {
return ancestor;
}
ancestor = ancestor.parentElement;
}
return null;
}
Related
I have a responsive web page that opens a modal when you tap a button. When the modal opens, it is set to take up the full width and height of the page using fixed positioning. The modal also has an input field in it.
On iOS devices, when the input field is focused, the keyboard opens. However, when it opens, it actually pushes the full document up out of the way such that half of my page goes above the top of the viewport. I can confirm that the actual html tag itself has been pushed up to compensate for the keyboard and that it has not happened via CSS or JavaScript.
Has anyone seen this before and, if so, is there a way to prevent it, or reposition things after the keyboard has opened? It's a problem because I need users to be able to see content at the top of the modal while, simultaneously, I'd like to auto-focus the input field.
first
<script type="text/javascript">
$(document).ready(function() {
document.ontouchmove = function(e){
e.preventDefault();
}
});
then this
input.onfocus = function () {
window.scrollTo(0, 0);
document.body.scrollTop = 0;
}
For anyone stumbling into this in React, I've managed to fix it adapting #ankurJos solution like this:
const inputElement = useRef(null);
useEffect(() => {
inputElement.current.onfocus = () => {
window.scrollTo(0, 0);
document.body.scrollTop = 0;
};
});
<input ref={inputElement} />
I struggled with this for awhile, I couldn't find something that worked well for me.
I ended up doing some JavaScript hackery to make it work.
What I found was that Safari wouldn't push the viewport if the input element was in the top half of the screen. That was the key to my little hack:
I intercept the focus event on the input object and instead redirect the focus to a invisible (by transform: translateX(-9999px)). Then once the keyboard is on screen (usually 200ms or so) I trigger the focus event on the original element which has since animated on screen.
It's a kind of complicated interaction, but it works really well.
function ensureOffScreenInput() {
let elem = document.querySelector("#__fake_input");
if (!elem) {
elem = document.createElement("input");
elem.style.position = "fixed";
elem.style.top = "0px";
elem.style.opacity = "0.1";
elem.style.width = "10px";
elem.style.height = "10px";
elem.style.transform = "translateX(-1000px)";
elem.type = "text";
elem.id = "__fake_input";
document.body.appendChild(elem);
}
return elem;
}
var node = document.querySelector('#real-input')
var fakeInput = ensureOffScreenInput();
function handleFocus(event) {
fakeInput.focus();
let last = event.target.getBoundingClientRect().top;
setTimeout(() => {
function detectMovement() {
const now = event.target.getBoundingClientRect().top;
const dist = Math.abs(last - now);
// Once any animations have stabilized, do your thing
if (dist > 0.01) {
requestAnimationFrame(detectMovement);
last = now;
} else {
event.target.focus();
event.target.addEventListener("focus", handleFocus, { once: true });
}
}
requestAnimationFrame(detectMovement);
}, 50);
}
node.addEventListener("focus", handleFocus, { once: true });
Personally I use this code in a Svelte action and it works really well in my Svelte PWA clone of Apple Maps.
Video of it working in a PWA clone of Apple Maps
You'll notice in the video that the auto-complete changes after the animation of the input into the top half of the viewport stabilizes. That's the focus switch back happening.
The only downside of this hack is that the focus handler on your original implementation will run twice, but there are ways to account for that with metadata.
you could also do this if you don't want scrollTo the top(0, 0)
window.scrollBy(0, 0)
const handleResize = () => {
document.getElementById('header').style.top = window.visualViewport.offsetTop.toString() + 'px'
}
if (window && window.visualViewport) visualViewport.addEventListener('resize', handleResize)
Source: https://rdavis.io/articles/dealing-with-the-visual-viewport
In some situations this issue can be mitigated by re-focusing the input element.
input.onfocus = function () {
this.blur();
this.focus();
}
Both IOS8 and Safari bowsers behave the same for input.focus() occuring after page load. They both zoom to the element and bring up the keyboard.(Not too sure if this will be help but have you tried using something like this?)
HTML IS
<input autofocus>
JS is
for (var i = 0; i < 5; i++) {
document.write("<br><button onclick='alert(this.innerHTML)'>" + i + "</button>");
}
//document.querySelector('input').focus();
CSS
button {
width: 300px;
height: 40px;
}
ALso you will have to use a user-agent workaround, you can use it for all IOS versions
if (!/iPad|iPhone|iPod/g.test(navigator.userAgent)) {
element.focus();
}
I am trying to detect a scroll on my page using JavaScript. So that I can change classes and attributes of some elements when user has scrolled certain amount of page. This is my JS function:
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
var change = window.setInterval(detectScroll, 5000);
}
and I am calling it when the page is loaded:
<body onload="detectScroll();">
However, I have this problem - I need to set up a really small interval so that the function gets called and the class is changed immediately. BUT then the page freezes and everything except the JS function works very slowly.
Is there any better way of achieving this in JavaScript?
Thanks for any advice/suggestion.
You are going to want to change a couple things. First, we can use onscroll instead of an interval. But you are also going to want to cache as much as possible to reduce the amount of calculations on your scroll. Even further, you should use requestAnimationFrame (or simply "debounce" in general for older browsers -- see the link). This ensures your work only happens when the browser is planning on repainting. For instance, while the user scrolls the actual scroll event may fire dozens of times but the page only repaints once. You only care about that single repaint and if we can avoid doing work for the other X times it will be all the more smoother:
// Get our header and its height and store them once
// (This assumes height is not changing with the class change).
var header = document.querySelector(".headerOrig");
var header_height = getComputedStyle(header).height.split('px')[0];
var fix_class = "changeColor";
// This is a simple boolean we will use to determine if we are
// waiting to check or not (in between animation frames).
var waitingtoCheck = false;
function checkHeaderHeight() {
if (window.pageYOffset > header_height) {
header.classList.add(fix_class);
}
if (window.pageYOffset < header_height) {
header.classList.remove(fix_class);
}
// Set waitingtoCheck to false so we will request again
// on the next scroll event.
waitingtoCheck = false;
}
function onWindowScroll() {
// If we aren't currently waiting to check on the next
// animation frame, then let's request it.
if (waitingtoCheck === false) {
waitingtoCheck = true;
window.requestAnimationFrame(checkHeaderHeight);
}
}
// Add the window scroll listener
window.addEventListener("scroll", onWindowScroll);
use onscroll instead of onload so you don't need to call the function with an interval.
Your dedectScroll function will be triggered automatically when any scroll appers if you use onscroll
<body onscroll="detectScroll();">
Your function is adding an interval recursively, you should add an event listener to the scroll event this way :
function detectScroll() {
var header = document.querySelector(".headerOrig"),
header_height = getComputedStyle(header).height.split('px')[0],
fix_class = "changeColor";
if( window.pageYOffset > header_height ) {
header.classList.add(fix_class);
}
if( window.pageYOffset < header_height ) {
header.classList.remove(fix_class);
}
}
window.addEventListener("scroll",detectScroll);
I want the <nav> to become fixed after the user passes the first "block" (height: 100%;). I decided to use Skrollr because it is the only way I know to make the "change" immediately, without bugs on mobile and in IE. So I did this:
$("nav").attr("data-" + $("header").height(), "position: fixed;");
This is works great (the <nav> is right after the <header>), until you resizes the page. So I did this:
$(window).resize(function () {
var style = $("nav").attr("style");
$('nav').each(function() {
var attributes = this.attributes;
var i = attributes.length;
while( i-- ){
this.removeAttributeNode(attributes[i]);
}
})
$("nav").attr("data-" + $("header").height(), "position: fixed;");
$("nav").attr({"data-0": "position: absolute;", "style": style});
});
It takes the new height, and add it as a attr and deletes all others attr (because if not it will add you a lot: data-500, data-501, data-502, data-503...) and by looking at the code - it works great. The problem is that the Skrollr doesn't "sees" the change. what should I do?
Well, Thats was easy. One line instead of 20 lines.
var s = skrollr.init({
constants: {
menuresize: function() {
return $("header").height();
},
vh: '100p'
}
});
and to the nav I add data-_menuresize="position: fixed;" (and it explaines what menuresize
means in the code above).
Enjoy :D!
I am loading an iframe inside a page, which the length of the content of the iframe will be changed from time to time. I have implemented the following solution. However the height is fixed as the dyniframesize only get call when iframe.onload.
It is possible to have the iframe to be resized accordingly from time to time the height of the iframe changed?
<iframe src ="popup.html" frameborder="0" marginheight="0" marginwidth="0" frameborder="0" scrolling="auto" id="ifm" name="ifm" onload="javascript:dyniframesize('ifm');" width="100%"></iframe>
function dyniframesize(down) {
var pTar = null;
if (document.getElementById){
pTar = document.getElementById(down);
}
else{
eval('pTar = ' + down + ';');
}
if (pTar && !window.opera){
//begin resizing iframe
pTar.style.display="block"
if (pTar.contentDocument && pTar.contentDocument.body.offsetHeight){
//ns6 syntax
pTar.height = pTar.contentDocument.body.offsetHeight +20;
pTar.width = pTar.contentDocument.body.scrollWidth+20;
}
else if (pTar.Document && pTar.Document.body.scrollHeight){
//ie5+ syntax
pTar.height = pTar.Document.body.scrollHeight;
pTar.width = pTar.Document.body.scrollWidth;
}
}
}
</script>
To my knowledge, there isn't a very natural way of doing this, but there are some options.
setInterval()
You could use setInterval() to repeatadly check the iframe's content height and adjust it if needed. This is simple, but inefficient.
Event Listeners
Any change of content in the iframe's content (and therefore height) is usually triggered by some event. Even if you're adding content from outside of the iframe, that "add" is probably triggered by a button click, for example. Here's something that might work for you: Live demo here (click).
var myIframe = document.getElementById('myIframe');
window.addEventListener('click', resizeIframe);
window.addEventListener('scroll', resizeIframe);
window.addEventListener('resize', resizeIframe);
myIframe.contentWindow.addEventListener('click', resizeIframe);
function resizeIframe() {
console.log('resize!');
}
Mutation Observer
This solution works in all up-to-date browsers - http://caniuse.com/mutationobserver
Live demo here (click).
It's really simple and should catch the relevant changes. If not, you could combine it with the event listener solution above to be sure things are updated!
var $myIframe = $('#myIframe');
var myIframe = $myIframe[0];
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
myIframe.addEventListener('load', function() {
setIframeHeight();
var target = myIframe.contentDocument.body;
var observer = new MutationObserver(function(mutations) {
setIframeHeight();
});
var config = {
attributes: true,
childList: true,
characterData: true,
subtree: true
};
observer.observe(target, config);
});
myIframe.src = 'iframe.html';
function setIframeHeight() {
$myIframe.height('auto');
var newHeight = $('html', myIframe.contentDocument).height();
$myIframe.height(newHeight);
}
Honorable mention to: overflow/underflow events.
Read about it here (there's A LOT to it).
and see my demo here (doesn't work in IE 11!).
This was a cool solution, but it's no longer working in IE. It might be possible to tweak it, but I'd rather use one of the above solutions, even if I had to fallback to a 1 second setInterval for older browsers, just because it's a lot simpler than this solution.
I wrote a small Library that uses mutationaObserver and eventListners, with fall backs to setInterval for IE8-10 and works for both same domain and cross domain. It can size both height and width.
http://davidjbradshaw.github.io/iframe-resizer/
iFrame View
<script type="text/javascript">
var height = $("body").outerHeight();
parent.SetIFrameHeight(height);
</script>
Main View
<script type="text/javascript">
SetIFrameHeight = function(height) {
$("#iFrameWrapper").height(height);
}
</script>
You can use custom events to signal between the iframe and the parent.
In the iframe:
if (window.parent !== window) {
window.oldIframeHeight = 0;
window.setInterval(function() {
var currentHeight = document.body.offsetHeight;
if (currentHeight !== window.oldIframeHeight) {
window.parent.postMessage(currentHeight.toString(), "*");
window.oldIframeHeight = currentHeight;
}
}, 100)
}
Then, in the parent:
window.addEventListener("message", function (e) {
var h = parseInt(e.data, 10);
if (!isNaN(h)) {
$('#iframe').height(h)
}
}, false);
Actually you should do it from iframe by accessing parent window and changing iframe's height.
Because document in iframe usually knows better if the it's body changed it's size (you can actually bind it to something like body.onload instead of using intervals to check if something has changed in iframe).
I have an iframe that can appear in a jQuery modal dialog, a popup window, or just as part of a page. The height of the content of this iframe can change as elements appear and disappear, and I need the containing iframe, along with the modal dialog or popup window where applicable, to change height as necessary to be 'just right' for the size of the content.
What's the best way to accomplish this? You can see the page where I need this behavior here.
Preferably there would be some event that will fire automatically if any content changes so I don't have to run some method everywhere that I make elements appear or disappear.
Thanks.
I think you need to do something like
/* creating jQuery special event to catch DOM content change */
var changeInterval;
jQuery.fn.contentchange = function(fn) {
return this.bind('contentchange', fn);
};
jQuery.event.special.contentchange = {
setup: function(data, namespaces) {
var self = this,
$this = $(this),
$originalContent = $this.html();
changeInterval = setInterval(function(){
if($originalContent != $this.html()) {
$originalContent = $this.html();
jQuery.event.special.contentchange.handler.call(self);
}
},500);
},
teardown: function(namespaces){
clearInterval(changeInterval);
},
handler: function(event) {
jQuery.event.handle.call(this, {type:'contentchange'})
}
};
/* end */
/* assigning our special event handler to iframe */
var iframe = $('iframe[src="login.htm"]')[0],
iDoc = iframe.contentWindow || iframe.contentDocument; // we love IE
if (iDoc.document && iDoc.document.body) {
$(iDoc.document.body).bind('contentchange', function(){
var currentHeight = $(this).outerHeight();
// we need to change iframe height as well as dialogs height
iframe.height = currentHeight;
$('#loginDialog').height(currentHeight);
})
}
/* end */
Sorry, haven't tested for myself, but (I feel) it'll work.
Hope it helps.