I'm building a simple chat application and I'm facing an issue with setting the scroll position in iOS. It seems to work fine on other platforms, including browser. Ionic v5, using Capacitor, building with Appflow.
The container needs to be scrolled to the bottom by default when the data loads. The page then listens for new messages. If a new message is added then the page should scroll to the bottom if the scroll position is within the bottom 150% of the client height. If not, a FAB appears indicating a new message has appeared and the user can select the button to scroll to the button.
The markup is as so. I have a custom domChange directive which listens to dom changes in the div. I prefer to put the scroll logic here as I was experiencing bugs putting it in the message listener before.
<ion-content>
<div class="chat-container" (click)="containerClick();" (scroll)="scroll($event)" #chatContainer>
<div class="wrapper" *ngIf="messages.length > 0" (domChange)="onDomChange($event)">
<!-- Excuse my inline styling, this is just for debug -->
<div style="width:100%;text-align:center;padding:20px;" *ngIf="loadingMore"><ion-spinner></ion-spinner></div>
<ng-container *ngFor="let message of messages">
<chat-bubble
[sent]="message.from === authService.user.id"
[delivered]="message.delivered"
[position]="message.position"
[timestamp]="message.createdAt.getTime()"
[content]="message.content"
>
</chat-bubble>
</ng-container>
</div>
</div>
<ion-fab vertical="bottom" horizontal="center" slot="fixed" *ngIf="showScrollBtn">
<ion-fab-button size="small" (click)="scrollToBottom()"><ion-icon name="arrow-down-outline"></ion-icon></ion-fab-button>
</ion-fab>
</ion-content>
The CSS is relatively simple but I am use a column-reverse flexbox and I think this might be one of the causes to the issue. The column-reverse means the webview renders the scroll at the bottom by default.
.chat-container {
height: 100%;
flex-grow: 1;
overflow: auto;
display: flex;
flex-direction: column-reverse;
overflow-x: hidden;
padding: 0 16px 0 16px;
}
And then the relevant functions are:
public onDomChange(event: any) {
if(event.addedNodes.length > 0 && event.addedNodes[0].localName == 'chat-bubble') {
const scrollTop = (this.platform.is('ios') && !this.platform.is('mobileweb')) ? this.chatContainer.nativeElement.scrollTop + this.chatContainer.nativeElement.scrollHeight : this.chatContainer.nativeElement.scrollTop;
/*
* Because of the column-reverse iOS sees the bottom of the div as scrollTop = 0 and the top of the div as scrollTop = -scrollHeight
* Android sees it as scrollTop = scrollHeight at the bottom and scrollTop = 0 at the top
*/
const scrollHeight = this.chatContainer.nativeElement.scrollHeight;
const clientHeight = this.chatContainer.nativeElement.clientHeight * 1.5;
const newMessageIndex = this.messages[this.messages.length - 1].index;
if((this.messages.length <= 30 || scrollHeight - scrollTop <= clientHeight) && this.channel.lastConsumedMessageIndex < newMessageIndex) {
this.scrollToBottom();
} else if(this.channel.lastConsumedMessageIndex < newMessageIndex) {
this.showScrollBtn = true;
}
}
}
public scrollToBottom(): Promise<any> {
this.showScrollBtn = false;
this.chatContainer.nativeElement.scrollTop = this.chatContainer.nativeElement.scrollHeight;
return this.channel.setAllMessagesConsumed();
}
The scrollToBottom() function works absolutely fine when triggered by the button click event but doesn't seem to do anything when triggered in the domChange trigger. I think this is because the trigger is called before the chat bubble element has rendered but I'm not sure how to fix this.
Would appreciate any ideas/thoughts
Thanks in advance
Related
I am using Telerik Grid for Blazor WASM.
When data has changed on the server. I get notified via a SignalR connection.
I would like the affected rows to change background color and then return to the normal background color.
Could be a transition to red and fade back to the white or gray color.
I have seen many examples using hover and transitions. But this should be shown without user interaction and preferably delayed on items not in the current view. So when you scroll the grid and the items become visible, the animation starts.
Can AOS https://github.com/michalsnik/aos be used? Or will it only trigger on scroll?
The easiest way for me would be to set a class on the row in the row render event. But it’s a razor page so I can code a custom template.
Whatever can be done using :hover can be done if you add a class (then remove it after the transition). As for the appear only after scroll, you can check for the element is in view using the provided function.
function isScrolledIntoView(el) {
// from https://stackoverflow.com/a/22480938/3807365
var rect = el.getBoundingClientRect();
var elemTop = rect.top;
var elemBottom = rect.bottom;
// Only completely visible elements return true:
var isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
// Partially visible elements return true:
// isVisible = elemTop < window.innerHeight && elemBottom >= 0;
return isVisible;
}
var el = document.querySelector(".row")
window.addEventListener("scroll", function() {
if (isScrolledIntoView(el)) {
if (el.getAttribute("data-did-it")) {
return;
}
el.setAttribute("data-did-it", "true")
el.classList.add("active")
setTimeout(function() {
el.classList.remove("active")
}, 500)
}
})
.row {
transition: 500ms;
background: white;
}
.active {
background: yellow;
}
<div style="height: 400px">
scroll down
</div>
<div class="row">
this is a row
</div>
<div style="height: 400px">
scroll up
</div>
I have a react component that displays a list of images horizontally across the page.
I would like to be able to scroll horizontally through them without having to hold in the SHIFT button.
Is there a way I change the default for just this component using css or react?
Here is my code: https://codesandbox.io/s/team-grid-slicer-2db8d
You can create a ref and bind it to your div which you will scroll. Here is a working codesandbox
Basically you create a ref and assign it to the div
const scrollRef = useRef()
...
<div ref={scrollRef}>
And you listen to the changes on the wheel event within the div:
if(scrollRef.current){
scrollRef.addEventListener('wheel', /* your function */)
}
And you scroll to left, instead of down based on the wheel event's deltaY:
el.scrollTo({
left: el.scrollLeft + e.deltaY * 5,
behavior: "smooth"
});
PS: You can remove * 5 but it looks better imo
just add an id="container to your component and then add
onWheel={(e) => {
e.preventDefault()
var container = document.getElementById('container')
var containerScrollPosition = document.getElementById('container').scrollLeft
container.scrollTo({
top: 0,
left: containerScrollPosition + e.deltaY,
behaviour: 'smooth' //if you want smooth scrolling
})
}}
This will listen to the onWheel and scroll your component.
Is it possible to continue scrolling through a webpage even if you are touching inside an iframe? This problem only happens with iOS devices and I couldn't find any solutions for this!
My current page contains an iframe in the middle with width:95% and about 500px height, so when I reach the iframe I can't scroll any more (unless I touch very close to the sides).
Thanks
In my case, I had full access to the iframe and was dynamically inserting its content. Still, none of the solutions suggested by Brandon S worked. My solution:
Create a transparent div overlaying the iframe.
Capture any click events on the overlay and replicate them within the iframe (to allow the user to click on links/buttons)
This works because the overlaying div is part of the outer document, making it respond to touch/click events normally, and prevents the user from directly interacting with the iframe content.
Html Template:
<div style="position: relative;">
<div
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; opacity: 0;"
ng-click="$ctrl.handleOverlayClick($event)"
></div>
</div>
Controller (AngularJS component)
...
constructor ($document, $element) {
this.iframe = $document[0].createElement('iframe');
this.iframe.width = '100%';
this.iframe.height = '100';
this.iframe.sandbox = 'allow-same-origin allow-scripts allow-popups allow-forms allow-top-navigation';
const element = $element[0].children.item(0);
element.appendChild(this.iframe);
this.contentDocument = this.iframe.contentDocument;
}
handleOverlayClick ($event) {
// Overlay element is an invisible layer on top of the iframe. We use this to
// capture scroll events which would be in the iframe (which don't work properly on iPad Safari)
// When a click is detected, we propigate that through to the iframe so the user can click on links
const rect = $event.target.getBoundingClientRect();
const x = $event.clientX - rect.left; // x position within the iframe
const y = $event.clientY - rect.top; // y position within the iframe
// triggering click on underlaying element
const clickedElement = this.contentDocument.elementFromPoint(x, y);
clickedElement && clickedElement.click();
}
It sounds like the iframe is receiving the user's scroll event, instead of the page. This can happen when part of the iframe's content doesn't fit within the size of the iframe element.
A solution to this problem is to stop the iframe from ever trying to scroll. There are few ways to accomplish this:
In iframe's HTML, add this CSS:
html, body {
overflow: hidden
}
If you don't have access to the iframe's HTML (because maybe the iframe is loading a 3rd party's content), you can put a wrapper div around the iframe and disable scrolling that way. Add this to the parent page HTML:
<div style="overflow: hidden"><iframe src="example.com"></iframe></div>
You can add this to the parent page HTML CSS to make browser use momentum so that ends up scrolling past the bottom of the iframe and then scrolls the page:
*{
-webkit-overflow-scrolling: touch
}
Add the legacy "scrolling" attribute to the iframe to stop the iframe from trying to scroll:
<iframe src="example.com" scrolling="no"></iframe>
When embedding a Bootstrap app in an iframe, modal dialogs always open at the top of the iframe, not the top of the screen. As an example, go to http://getbootstrap.com/javascript/ and open an example modal on the page. Then using the sample code below which places the same bootstrap page in an iframe, find a modal and open it:
<html>
<head>
</head>
<body>
<table width="100%">
<tr><td colspan="2" style="height:80px;background-color:red;vertical-align:top">Here's some header content I don't control</td></tr>
<tr><td style="width:230px;height:10080px;background-color:red;vertical-align:top">Here's some sidebar content I don't control either</td>
<td valign="top">
<iframe width="100%" height="10000px"
scrolling="no" src="http://getbootstrap.com/javascript/">
</iframe>
</td>
</tr>
</table>
</body>
</html>
Demo in fiddle
How do I go about positioning the modal on the screen in this scenario?
UPDATE: Unfortunately, my iFrame cannot fill the screen, nor can I make it fixed since it needs to blend into the rest of the page and the page itself has enough content to scroll. This is not my design and I ultimately intend to rework the whole thing, but this is what I have to work around for now. As a temporary option, I'm using javascript to tell the iframe parent to scroll to the top where the modal dialog pops up. While this is acceptable, this isn't the desired behavior.
I'm using angularjs and the ui-bootstrap library in my code but as you can see above, it's a bootstrap issue.
If your iframe has the same document.domain as the parent window or it is a sub domain, you can use the code below inside the iframe:
$('#myModal').on('show.bs.modal', function (e) {
if (window.top.document.querySelector('iframe')) {
$('#myModal').css('top', window.top.scrollY); //set modal position
}
});
show.bs.modal will fire after you call $('#myModal').show()
window.top.scrollY will get the scrollY position from the parent window
In case your document.domain differs from the parent, you can hack it getting the onmousedown position inside the iframe. For example:
$('#htmlElement').on('mousedown', function (event) {
event.preventDefault();
$('#myModal').data('y', event.pageY); // store the mouseY position
$('#myModal').modal('show');
});
$('#myModal').on('show.bs.modal', function (e) {
var y = $('#myModal').data('y'); // gets the mouseY position
$('#myModal').css('top', y);
});
Quite old question but I don't see the solution/workaround I've found. It might be helpful for someone in the future.
I had the same issue - my iFrame doesn't fill the entire screen, it displays bootstrap's modal and it is loading content from different domain than the parent page.
TL;DR
Use window.postMessage() API - Documentation here. for communication between parent and iframe (cross-domain)
pass message with currentScrollPosition and Y position of your iframe
Reveive message and update modal's padding from the top
In my case the workaround was to use window.postMessage() API - Documentation here.
It requires to add some extra code to the parent and handle message in an iFrame.
You can add EventListener and listen to 'scroll' event. Each time the event handling function is invoked you can get currentScrollPosition like document.scrollingElement.scrollTop.
Keep in mind that your iframe can have some margin from the top in the parent page so you should also get its 'offset'.
After that you can post these two values to your iframe e.g. ncp_iframe.contentWindow.postMessage(message, '*');
Note that the message has to be a String value
After that in your iFrame you need to add EventListener and listen to 'message' event.
The event handling function will pass your 'message' in event.data property. Having that you can update modal padding. (Looks much better if you don't use animations e.g. fade, in classes);
Quick Example:
Parent:
window.addEventListener('scroll', function(event){
var myIframe = document.querySelector('#myIframe');
var topOffset = myIframe.getBoundingClientRect().top + window.scrollY;
var currentScroll = document.scrollingElement.scrollTop;
myIframe.contentWindow.postMessage(topOffset + ':' + currentScroll, '*');
});
iFrame:
window.addEventListener('message', function(event) {
var messageContent = event.data.split(':');
var topOffset = messageContent[0];
var currentScroll = messageContent[1];
//calculate padding value and update the modal top-padding
}, false);
This is a bug in most browsers (IE appears fine) where the elements are fixed to the iframe, not the window. Typically, if you need something to be relative to the main document, it has to be in the main document.
A workaround is to wrap your iframe in a fixed position div that takes up the whole width of the screen and then maximize the iframe within that. This appears to resolve the issue
HTML:
<div class="fixframe">
<iframe src="http://getbootstrap.com/javascript/"></iframe>
</div>
CSS:
.fixframe {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
}
.fixframe iframe {
height: 100%;
width: 100%;
}
Working Demo in fiddle
See Also:
position:fixed inside of an iframe
iframe with a css fixed position : possible?
position fixed div in iframe not working
It is because the class .modal has position: fixed attribute. Try position: relative instead.
I am trying to get my bottom header to stick to the bottom of the screen inside of my iframe application and have it always appear in view for the user even when the page is scrolling. I have no control over the outer iframe as it is on a different domain. The header itself must be inside of the iframe as I have no control outside the iframe. The iframe always expands to the height of its contents so that it has no scrollbars, but the bar still has to be visible in the viewport at all times.
Another thing to note: The iframe height should be the same height as its contents so their is no need for scroll bars
Chrome has a bug that doesn't fix elements with position:fixed if:
a) you use CSS3 transform in any element, and/or
b) you have a child element positioned outside the box of it's parent element
Oddly enough, the bug was reported back in 2009 and it's still open: https://code.google.com/p/chromium/issues/detail?id=20574
You might want to play around with position: fixed;
#element {
position: fixed;
z-index: 1000;
bottom: 0;
}
EDIT:
I'm sorry, I think I miss understood your post. If I'm reading it correctly you want to create a header bar similar to blogger but to keep it always in view of the user when he/she scrolls.
What you can do is create a container div, and then you can nest both your header and iframe inside that container. You can then play around with the positioning, although I'm not sure if the exact behavior that you're looking for is possible without some javascript.
EDIT 2:
After playing around a bit, I got something that I think might help (if I understand your problem correctly).
http://digitaldreamer.net/media/examples/iframe-site.html
http://digitaldreamer.net/media/examples/iframe.html
I had to look for a long time for a possible solution, and I think I have found one that is using the Intersection Observer API to detect the scrolled position of the iframe within the parent document without needing to access the parent document DOM.
I'm creating a bunch of hidden 100px high elements in the iframe. These are positioned absolutely underneath each other so that together they fill the height of the whole iframe document. An intersection observer then observes the intersection between the (top-level document) viewport and each of the hidden elements and calculates the scroll position of the iframe based on the values it returns. A ResizeObserver creates additional hidden elements if the height of the body increases.
This approach assumes that your iframe is always minimum 100px high. If you expect a smaller height, you need to adjust the hidden container height. The reason is that once a hidden container is 100% visible, the intersection observer does not emit the callback while the parent document is being scrolled (since the intersection ratio stays at 1). This is also the reason why I need a lot of small containers rather than observing the intersection with the iframe body itself.
const CONTAINER_HEIGHT = 100;
const threshold = [...Array(CONTAINER_HEIGHT + 1).keys()].map((i) => i / CONTAINER_HEIGHT);
/**
* Registers an intersection handler that detects the scrolled position of the current
* iframe within the browser viewport and calls a handler when it is first invoked and
* whenever the scrolled position changes. This allows to position elements within the
* iframe in a way that their position stays sticky in relation to the browser window.
* #param handler Is invoked when the function is first called and whenever the scroll
* position changes (for example due to the user scrolling the parent document). The
* "top" parameter is the number of pixels from the top of the browser viewport to the
* top of the iframe (if the top of the iframe is above the top of the browser viewport)
* or 0 (if the top of the iframe is below the top of the browser viewport). Positioning
* an element absolutely at this top position inside the iframe will simulate a sticky
* positioning at the top edge of the browser viewport.
* #returns Returns a callback that unregisters the handler.
*/
function registerScrollPositionHandler(handler: (top: number) => void): () => void {
const elementContainer = document.createElement('div');
Object.assign(elementContainer.style, {
position: 'absolute',
top: '0',
bottom: '0',
width: '1px',
pointerEvents: 'none',
overflow: 'hidden'
});
document.body.appendChild(elementContainer);
const elements: HTMLDivElement[] = [];
let intersectionObserver: IntersectionObserver | undefined = undefined;
const resizeObserver = new ResizeObserver(() => {
intersectionObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.intersectionRatio > 0 && (entry.intersectionRect.top > entry.boundingClientRect.top || entry.target === elements[0])) {
handler(entry.intersectionRect.top);
}
}
}, { threshold });
const count = Math.ceil(document.documentElement.offsetHeight / CONTAINER_HEIGHT);
for (let i = 0; i < count; i++) {
if (!elements[i]) {
elements[i] = document.createElement('div');
Object.assign(elements[i].style, {
position: 'absolute',
top: `${i * CONTAINER_HEIGHT}px`,
height: `${CONTAINER_HEIGHT}px`,
width: '100%'
});
elementContainer.appendChild(elements[i]);
intersectionObserver.observe(elements[i]);
}
}
});
resizeObserver.observe(document.documentElement);
return () => {
resizeObserver.disconnect();
intersectionObserver?.disconnect();
elementContainer.remove();
};
}
This example code should create a toolbar that is sticky at the top of the browser viewport:
<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; overflow: hidden; pointer-events: none; z-index: 90">
<div id="toolbar" style="position: absolute; top: 0; left: 0; right: 0; pointer-events: auto; transition: top 0.3s">
Line 1<br/>Line 2<br/>Line 3<br/>Line 4<br/>Line 5<br/>Line 6<br/>Line 7<br/>Line 8<br/>Line 9<br/>Line 10
</div>
</div>
<script>
registerScrollPositionHandler((top) => {
document.querySelector('#toolbar').style.top = `${top}px`;
});
</script>
Note that other than what you asked for, this will position the toolbar at the top of the viewport rather than at the bottom. Positioning at the bottom should also be possible, but is slightly more complex. If anyone requires a solution for this, please let me know in the comments and I will invest the time to adjust my answer.