CSS Partial Scroll Snapping - javascript

Been playing around with the scroll snapping, looks like it saves a lot of head scratching with writing the functionality in JS.
Here's a challenge, has anyone out there found a way to selectively choose which children to snap and which children to freely scroll?
I think this will be useful for content rich pages that contain parts that wouldn't benefit from scroll snapping.
Here's an example of the problem:
https://codepen.io/nodelondon/pen/YzxWqLG
html {
background: #f2f2f2;
}
.scroll-container,
.scroll-area-none,
.scroll-area {
max-width: 850px;
height: 600px;
font-size: 60px;
}
.scroll-area-none {
scroll-snap-align: none;
background-color: black;
}
.scroll-container {
overflow: auto;
scroll-snap-type: y mandatory;
}
.scroll-area {
scroll-snap-align: start;
}
.scroll-container,
.scroll-area-none,
.scroll-area {
margin: 0 auto;
}
.scroll-area-none,
.scroll-area {
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.scroll-area:nth-of-type(4n+1) {
background: #49b293;
}
.scroll-area:nth-of-type(4n+2) {
background: #c94e4b;
}
.scroll-area:nth-of-type(4n+3) {
background: #4cc1be;
}
.scroll-area:nth-of-type(4n+4) {
background: #8360A6;
}
<div class="support-scrollsnap"></div>
<div class="scroll-container">
<div class="scroll-area-none">-1</div>
<div class="scroll-area-none">0</div>
<div class="scroll-area">1</div>
<div class="scroll-area">2</div>
<div class="scroll-area">3</div>
<div class="scroll-area">4</div>
<div class="scroll-area-none">5</div>
<div class="scroll-area-none">6</div>
</div>
Ideally, the boxes with -1,0,5 and 6 should be able to freely scroll but the mandatory boxes in between keep pulling you back.
If you're thinking of suggesting changing it to proximity, this is a good suggestion, but, with IOS Safari (On OSX Safari for me as well), unfortunately it still forces scroll snapping on boxes that have scroll-snap-align set to Start no matter where you are on the page.

Found that changing scroll-snap-type on documentElement causes the scroll to jump to the nearest snap-align element, which seems to look awful.
Looks like the simplest fix is working fine:
let t = window.scrollY;
requestAnimationFrame(() => window.scroll(0, t));

I propose the following logic:
Define which children block is at the top border of the scroll container. I am using the Element.getBoundingClientRect() method to compare positions of the scroll container and its children.
Check which scroll-snap-align property has this child block.
Set the scroll-snap-type property of the container as y proximity or y mandatory.
Handle the scroll event on desktop, On mobile this event works at the end of the scroll, so we need something else for mobile (may be jQuery Mobile).
Here is a working draft solution. But it requires optimizations and improvements such as scroll event throttling.
https://codepen.io/glebkema/pen/zYdPqeY
let scrollContainers = document.getElementsByClassName('scroll-container');
for (let sc of scrollContainers) {
sc.addEventListener('scroll', updateSnapType);
sc.addEventListener('touchstart', updateSnapType);
}
function updateSnapType(event) {
let parent = event.currentTarget;
let parentRect = parent.getBoundingClientRect();
for (let child of parent.children) {
let childRect = child.getBoundingClientRect();
if (childRect.top <= parentRect.top && parentRect.top < childRect.bottom) {
let childStyle = window.getComputedStyle(child);
let scrollSnapAlign = childStyle.getPropertyValue('scroll-snap-align');
console.log(child.innerText, scrollSnapAlign);
parent.style.scrollSnapType = "none" === scrollSnapAlign ? 'y proximity' : 'y mandatory';
break;
}
}
}
html {
background: #f2f2f2;
}
.scroll-container,
.scroll-area-none,
.scroll-area {
height: 100px;
font-size: 60px;
}
.scroll-container {
max-width: 850px;
margin: 15px auto;
overflow: auto;
scroll-snap-type: y mandatory;
}
.scroll-area {
scroll-snap-align: start;
}
.scroll-area-none {
scroll-snap-align: none;
background-color: black;
}
.scroll-area-none,
.scroll-area {
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.scroll-area:nth-of-type(4n+1) {
background: #49b293;
}
.scroll-area:nth-of-type(4n+2) {
background: #c94e4b;
}
.scroll-area:nth-of-type(4n+3) {
background: #4cc1be;
}
.scroll-area:nth-of-type(4n+4) {
background: #8360A6;
}
<div class="scroll-container">
<div class="scroll-area-none">1. -1</div>
<div class="scroll-area-none">1. 0</div>
<div class="scroll-area">1. 1</div>
<div class="scroll-area">1. 2</div>
<div class="scroll-area">1. 3</div>
<div class="scroll-area">1. 4</div>
<div class="scroll-area-none">1. 5</div>
<div class="scroll-area-none">1. 6</div>
</div>
<div class="scroll-container">
<div class="scroll-area-none">2. -1</div>
<div class="scroll-area-none">2. 0</div>
<div class="scroll-area">2. 1</div>
<div class="scroll-area">2. 2</div>
<div class="scroll-area">2. 3</div>
<div class="scroll-area">2. 4</div>
<div class="scroll-area-none">2. 5</div>
<div class="scroll-area-none">2. 6</div>
</div>

Related

How to make a sticky scrolling effect that stays at the top a little and immediately pops off

Sorry for such a dumb question for you, I've got a list of elements with overflow-y:auto, like this:
I'd like to make them stick for just a little bit when I'm scrolling and pop off when the next one is pushing it from the bottom.
It's to avoid this which IMO doesn't look very good.
I know I have to use position:sticky but I don't know how to achieve this without the elements staying there definitely as I scroll down
Please excuse the lengthy code, as the HTML and parts of the CSS were just meant to make things stand out.
The actual core of it is the JavaScript code, along with the parts of CSS I didn't comment on.
Obviously the code's readability can be improved; However I've intentionally left the original functions, rather than writing custom ones (e.g. makeTopElement(element), or getTopPosition(element)), as the code snippet has already become pretty long vertically.
const items = document.getElementsByClassName('item');
const itemHeight = items[0].getBoundingClientRect().height;
let currentTopIndex = 0;
let prevY = 0;
document.onscroll = function() {
let currY = window.pageYOffset;
// Scrolling down
if (currY > prevY &&
currentTopIndex < items.length - 1 &&
items[currentTopIndex + 1].getBoundingClientRect().top < itemHeight) {
items[currentTopIndex].classList.remove('top');
items[currentTopIndex].style.top = 'auto';
items[currentTopIndex].style.bottom = 0;
currentTopIndex ++;
items[currentTopIndex].classList.add('top');
}
// Scrolling up
else if (currY < prevY &&
currentTopIndex > 0 &&
items[currentTopIndex - 1].getBoundingClientRect().top > 0) {
items[currentTopIndex].classList.remove('top');
currentTopIndex --;
items[currentTopIndex].classList.add('top');
items[currentTopIndex].style.top = 0;
items[currentTopIndex].style.bottom = 'auto';
}
prevY = currY;
};
.wrapper {
height: 500px;
}
.itemWrapper {
height: 100px;
position: relative;
}
.item {
position: absolute;
/* These properties are here just to make things pretty */
background-color: MintCream;
padding: 10px;
text-align: center;
border: 1px solid #00e673;
width: 100%;
}
.top {
position: sticky;
top: 0;
/* These properties are here just to make things pretty */
background-color: red;
border-color: #990000;
}
<div class="wrapper">
<div class="itemWrapper">
<div class="item top">
first
</div>
</div>
<div class="itemWrapper">
<div class="item">
second
</div>
</div>
<div class="itemWrapper">
<div class="item">
third
</div>
</div>
</div>
to make this you would use css-snap:
for the list class: add the following css:
.parent {
overflow: scroll;
height: 200px;
scroll-snap-type: y mandatory;
scroll-snap-points-y: repeat(50px);
}
.child {
height: 50px;
margin: 10px;
scroll-snap-align: start;
}
replace:
.parent with your list class,
height: 200px with your list height, notice that you should give it specific height,
scroll-snap-points-y: repeat(50px) replace 50px with height of children, in my case i used 50px because child class has height of 50px.
.child with your element class
height: 50px height of children element.
.parent {
overflow: scroll;
height: 200px;
scroll-snap-type: y mandatory;
scroll-snap-points-y: repeat(50px);
}
.child {
height: 50px;
margin: 10px;
scroll-snap-align: start;
}
.child:nth-child(even) {
background-color: #ccc;
}
.child:nth-child(odd) {
background-color: #ddd;
}
<div class="parent">
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
<div class="child"><h1>testing</h1></div>
</div>

My script is not showing whats truly visible element wise

I found posts and online articles on how to do something like this but most examples are not in plain JavaScript. So this script works almost perfectly if all the sections are the same height for example 220px. So I thought I was getting closer in having this script working how I want it to work like overtime but then I realize
it had flaws when I decided to change the sections height and play around with the code more to see if it had any flaws that I was unaware of so basically this script is designed to output the name
of the sections that are visible but it is not showing the correct output for example if section 1 is the only one that is visible in the div it will say section-1 if multiple sections are visible it will say for example section-1,section-2 etc. Basically it should work like this regardless of the sections height
I know I have to change the code or altered it but I'm getting more confused the more I play around with it so how can I pull this off so I can always have the correct output? If I have to change my code completely to be able to do this then I don't mind.
This is my code
document.addEventListener('DOMContentLoaded',function(){
document.querySelector('#building').addEventListener('scroll',whichSectionsAreInSight);
function whichSectionsAreInSight(){
var building= document.querySelector('#building');
var top = building.scrollTop;
var bottom = top+building.offsetHeight;
var arr = [];
Array.prototype.forEach.call(
building.querySelectorAll('#building .sections'),
function(sections){
if ((sections.offsetTop < top && top <sections.offsetTop+sections.offsetHeight) || (sections.offsetTop < bottom && bottom < sections.offsetTop+sections.offsetHeight)){
arr.push(sections.id);
}
}
);
document.querySelector('#status').innerHTML = arr.join(',')
}
whichSectionsAreInSight();
});
h1{
margin: 0;
text-align: center;
}
#building{
background-color: gray;
height: 200px;
width: 200px;
overflow: auto;
}
.sections{
height: 80px;
width: 100%;
}
#section-1{
background-color: dodgerblue;
}
#section-2{
background-color: gold;
}
#section-3{
background-color: red;
}
#section-4{
background-color: gray;
height: 220px;
}
<p id='status'></p>
<div id='building'>
<div id='section-1' class='sections'><h1>Section 1</h1></div>
<div id='section-2' class='sections'><h1>Section 2</h1></div>
<div id='section-3' class='sections'><h1>Section 3</h1></div>
<div id='section-4' class='sections'><h1>Section 4</h1></div>
</div>
You were pretty close!
First off, you need to set the parent element to position:relative otherwise the parent that is being measured against is the document.
Also, the algorithm is simpler than what you had. Just make sure that the top of the element is less than the bottom of the parent, and the bottom of the element is greater than the top of the parent.
In your case this is offsetTop < bottom and offsetTop + offsetHeight > top
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('#building').addEventListener('scroll', whichSectionsAreInSight);
function whichSectionsAreInSight() {
var building = document.querySelector('#building');
var top = building.scrollTop;
var bottom = top + building.offsetHeight;
var arr = [];
Array.prototype.forEach.call(
building.querySelectorAll('#building .sections'),
function(section) {
if (section.offsetTop < bottom && section.offsetTop + section.offsetHeight > top) {
arr.push(section.id);
}
}
);
document.querySelector('#status').innerHTML = arr.join(',')
}
whichSectionsAreInSight();
});
h1 {
margin: 0;
text-align: center;
}
#building {
background-color: gray;
height: 200px;
width: 200px;
overflow: auto;
position: relative;
}
.sections {
height: 80px;
width: 100%;
}
#section-1 {
background-color: dodgerblue;
}
#section-2 {
background-color: gold;
}
#section-3 {
background-color: red;
}
#section-4 {
background-color: gray;
height: 220px;
}
<p id='status'></p>
<div id='building'>
<div id='section-1' class='sections'>
<h1>Section 1</h1>
</div>
<div id='section-2' class='sections'>
<h1>Section 2</h1>
</div>
<div id='section-3' class='sections'>
<h1>Section 3</h1>
</div>
<div id='section-4' class='sections'>
<h1>Section 4</h1>
</div>
</div>

Visible intersectionObserver

I am trying to learn about Javascript's IntersectionObserver.
After reading several articles and the documentation I have decided to make a CodePen to try it myself: IntersectionObserver CodePen
I would like to display the "block that is visible" on the top message. The CodePen "almost" works, but not completely. Sometimes it shows the correct block, sometimes it doesn't.
Here is my JS:
let message = document.querySelector('#block-number');
// INTERSECTION OBSERVER STUFF
const io = new IntersectionObserver(entries => {
if(entries[0].isIntersecting) {
message.innerHTML = entries[0].target.textContent;
}
}, {
threshold: [.25]
});
// ELEMENTS TO OBSERVE
const blk1 = document.querySelector('#block1');
const blk2 = document.querySelector('#block2');
const blk3 = document.querySelector('#block3');
const blk4 = document.querySelector('#block4');
const blk5 = document.querySelector('#block5');
const blk6 = document.querySelector('#block6');
// START OBSERVING ELEMENTS
io.observe(blk1);
io.observe(blk2);
io.observe(blk3);
io.observe(blk4);
io.observe(blk5);
io.observe(blk6);
Any ideas on what i am doing wrong?
i have also tried (without luck) something like:
if(entries[0].intersectionRatio !== 0)
Thank you!
The function passed to the IntersectionObserved is executed when the intersection state changes. So what happens when you are at block 3 and scroll a bit so block 4 is shown? The intersection changes for block 4 and so the message is changed. WHen you scroll back up the intersection is changed again for block 4, but it does not enter the if condition. The intersection for block 3 on the other hand is not changed - it was visible before, even though not fully, it's visible still.
There are few ways you can fix this.
One is to define intersection ratio, and going above and below that ratio will be considered change in the state (pass options hash as second argument, containing threshold key with value 0 - 1, e.g. 0.5 for 50% visibility)
You can also add the same observer for all of the blocks and iterate trough entries in the function, checking which block has the best intersection ratio.
You have set the threshold to 25%.
The problem with this, is that the previous block leaves its last 25% of the viewport after the next block enters the viewport by 25%.
This was easy to see with the following console.log:
console.log(entries[0].target.textContent, ": ", entries[0].intersectionRatio)
let message = document.querySelector('#block-number');
// INTERSECTION OBSERVER STUFF
const io = new IntersectionObserver(entries => {
if(entries[0].isIntersecting ) {
console.log(entries[0].target.textContent, ": ", entries[0].intersectionRatio)
message.innerHTML = entries[0].target.textContent;
}
}, {
threshold: [.25]
});
// ELEMENTS TO OBSERVE
const blk1 = document.querySelector('#block1');
const blk2 = document.querySelector('#block2');
const blk3 = document.querySelector('#block3');
const blk4 = document.querySelector('#block4');
const blk5 = document.querySelector('#block5');
const blk6 = document.querySelector('#block6');
// START OBSERVING ELEMENTS
io.observe(blk1);
io.observe(blk2);
io.observe(blk3);
io.observe(blk4);
io.observe(blk5);
io.observe(blk6);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: roboto;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.container {
background-color: #eee;
width: 100%;
height: 100%;
min-height: 100vh;
}
.message {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 80px;
background-color: #ef9b8d;
color: white;
}
.blocks {
padding-top: 100px;
}
.block {
height: 85vh;
width: 90vw;
margin: 0 auto 15vh;
background-color: #999;
color: white;
}
<div class="message center">Displaying <span id="block-number">Block 1</span></div>
<div class="blocks">
<div id="block1" class="block center">Block 1</div>
<div id="block2" class="block center">Block 2</div>
<div id="block3" class="block center">Block 3</div>
<div id="block4" class="block center">Block 4</div>
<div id="block5" class="block center">Block 5</div>
<div id="block6" class="block center">Block 6</div>
</div>
To fix this, simply raise the threshold. (Depending on how much of the block needs to enter the viewport for you to consider it the current block)
Demo:
let message = document.querySelector('#block-number');
// INTERSECTION OBSERVER STUFF
const io = new IntersectionObserver(entries => {
if(entries[0].isIntersecting ) {
console.log(entries[0].target.textContent, ": ", entries[0].intersectionRatio)
message.innerHTML = entries[0].target.textContent;
}
}, {
threshold: [.8] // raised the threshold
});
// ELEMENTS TO OBSERVE
const blk1 = document.querySelector('#block1');
const blk2 = document.querySelector('#block2');
const blk3 = document.querySelector('#block3');
const blk4 = document.querySelector('#block4');
const blk5 = document.querySelector('#block5');
const blk6 = document.querySelector('#block6');
// START OBSERVING ELEMENTS
io.observe(blk1);
io.observe(blk2);
io.observe(blk3);
io.observe(blk4);
io.observe(blk5);
io.observe(blk6);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: roboto;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.container {
background-color: #eee;
width: 100%;
height: 100%;
min-height: 100vh;
}
.message {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 80px;
background-color: #ef9b8d;
color: white;
}
.blocks {
padding-top: 100px;
}
.block {
height: 85vh;
width: 90vw;
margin: 0 auto 15vh;
background-color: #999;
color: white;
}
<div class="message center">Displaying <span id="block-number">Block 1</span></div>
<div class="blocks">
<div id="block1" class="block center">Block 1</div>
<div id="block2" class="block center">Block 2</div>
<div id="block3" class="block center">Block 3</div>
<div id="block4" class="block center">Block 4</div>
<div id="block5" class="block center">Block 5</div>
<div id="block6" class="block center">Block 6</div>
</div>

Show only a few users and hide others

Can anyone explain how to make a user list like as shown in the image below...
I'm making a project in Meteor and using Materialize for template and I want to display the list of assigned users. If there are more than a particular count(say 5) of users i want them to be displayed like on that image... I have tried googling this and haven't found anything useful. I also checked the Materialize website and found nothing useful. So if anyone has an idea please help share it.
Ok so this is the output html, in this case i only have one member but in real case I will have more:
<div class="row"> ==$0
<label class="active members_padding_card_view">Members</label>
<div class="toolBarUsers flex" style="float:right;">
<dic class="other-profile" style="background-color:#f06292;">
<span>B</span>
</div>
This is the .js code
Template.profile.helpers({
randomInitials: function () {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
var nLetter = chars.charAt(Math.floor(Math.random()*chars.length));
var sLetter = chars.charAt(Math.floor(Math.random()*chars.length));
return nLetter + sLetter;
},
tagColor: function () {
var colors = ["#e57373","#f06292","#ba68c8","#9575cd","#7986cb","#64b5f6","#4fc3f7","#4dd0e1","#4db6ac","#81c784","#aed581","#dce775","#fff176","#ffd54f","#ffb74d","#ff8a65","#a1887f","#e0e0e0","#90a4ae"];
return colors[Math.floor(Math.random()*colors.length)];
},
randomAllowed : function(possible) {
var count = Math.floor((Math.random() * possible) + 1);
if(count == 1) {
return;
}
return "none";
},
membersList() {
const instance = Template.instance();
const cardDataId = new Mongo.ObjectID(instance.data.cardData._id.valueOf());
return CardDataMembers.find({lkp_card_data_fkeyi_ref: cardDataId});
},
memberData: function() {
// We use this helper inside the {{#each posts}} loop, so the context
// will be a post object. Thus, we can use this.xxxx from above memberList
return Meteor.users.findOne(this.lkp_user_fkeyi_ref);
},
showMembers() {
const instance = Template.instance();
const cardDataId = new Mongo.ObjectID(instance.data.cardData._id.valueOf());
let membersCount = CardDataMembers.find({lkp_card_data_fkeyi_ref: cardDataId}).count();
////console.log(membersCount);
if (membersCount > 0) {
$('.modal-trigger').leanModal();
return true;
} else {
return false;
}
},
});
Right now if I add a lot of users I get this:
This can be done in many ways, but I've used CSS Flexbox.
I've used two <div>s one contains single user circles having class .each-user that is expanding (for reference I've taken 6) and another contains the total number of users having class .total-users.
It's a bit confusing but if you look into my code below or see this Codepen you'll get to know everything.
html, body {
width: 100%;
height: 100%;
margin: 0;
font-family: Roboto;
}
.container {
display: flex;
align-content: center;
justify-content: center;
margin-top: 20px;
}
/* Contains all the circles */
.users-holder {
display: flex;
}
/* Contains all circles (those without total value written on it) */
.each-user {
display: flex;
flex-wrap: wrap;
padding: 0 10px;
max-width: 300px;
height: 50px;
overflow: hidden;
}
/* Circle Styling */
.circle {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
}
.each-user .circle {
background: #00BCD4;
}
.each-user .circle:last-child {
margin-right: 0;
}
/* Circle showing total */
.total-users {
padding: 0;
margin-bottom:
}
.total-users .circle {
background: #3F51B5;
margin: 0;
position: relative;
}
.total-users .circle .txt {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
}
<div class="container">
<div class="users-holder">
<div class="total-users">
<div class="circle">
<span class="txt">+12</span>
</div>
</div>
<div class="each-user">
<div class="circle user-circle"></div>
<div class="circle user-circle"></div>
<div class="circle user-circle"></div>
<div class="circle user-circle"></div>
<div class="circle user-circle"></div>
<!-- Sixth Circle -->
<div class="circle"></div>
</div>
</div>
</div>
Hope this helps!
I've used jQuery. See this https://jsfiddle.net/q86x7mjh/26/
HTML:
<div class="user-list-container">
<div class="total-circle hidden"><span></span></div>
<div class="user-circle"><span>T</span></div>
<div class="user-circle"><span>C</span></div>
<div class="user-circle"><span>U</span></div>
<div class="user-circle"><span>M</span></div>
<div class="user-circle"><span>R</span></div>
<div class="user-circle"><span>Z</span></div>
<div class="user-circle"><span>N</span></div>
<div class="user-circle"><span>O</span></div>
<div class="user-circle"><span>M</span></div>
<div>
jQuery:
var items_to_show = 5;
if($('.user-circle').length > items_to_show){
var hide = $('.user-circle').length - items_to_show;
for(var i = 0; i < hide; i++){
$('.user-circle').eq(i).addClass('hidden');
}
$('.total-circle').removeClass('hidden');
$('.total-circle span').text('+' + hide);
}
So after quite some time I have solved the problem. I am posting my answer here for anyone that will in the future experience a similar issue...
Have a good day!
I have added the following lines of code to my template:
return CardDataMembers.find({lkp_card_data_fkeyi_ref: cardDataId},{sort: {createdAt: -1}, limit: 3});
diffMembers(){
const instance = Template.instance();
const cardDataId = new Mongo.ObjectID(instance.data.cardData._id.valueOf());
const limit = 3;
const allMembersOnCard = CardDataMembers.find({lkp_card_data_fkeyi_ref: cardDataId}).count();
let remainingMembers = allMembersOnCard - limit;
return remainingMembers;
},
And in the HTML included:
<div class="other-profile" style="background-color:#dedede;">
<span>+{{diffMembers}}</span>
</div>

Scrollable div to stick to bottom, when outer div changes in size

Here is an example chat app ->
The idea here is to have the .messages-container take up as much of the screen as it can. Within .messages-container, .scroll holds the list of messages, and in case there are more messages then the size of the screen, scrolls.
Now, consider this case:
The user scrolls to the bottom of the conversation
The .text-input, dynamically gets bigger
Now, instead of the user staying scrolled to the bottom of the conversation, the text-input increases, and they no longer see the bottom.
One way to fix it, if we are using react, calculate the height of text-input, and if anything changes, let .messages-container know
componentDidUpdate() {
window.setTimeout(_ => {
const newHeight = this.calcHeight();
if (newHeight !== this._oldHeight) {
this.props.onResize();
}
this._oldHeight = newHeight;
});
}
But, this causes visible performance issues, and it's sad to be passing messages around like this.
Is there a better way? Could I use css in such a way, to express that when .text-input-increases, I want to essentially shift up all of .messages-container
2:nd revision of this answer
Your friend here is flex-direction: column-reverse; which does all you ask while align the messages at the bottom of the message container, just like for example Skype and many other chat apps do.
.chat-window{
display:flex;
flex-direction:column;
height:100%;
}
.chat-messages{
flex: 1;
height:100%;
overflow: auto;
display: flex;
flex-direction: column-reverse;
}
.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }
The downside with flex-direction: column-reverse; is a bug in IE/Edge/Firefox, where the scrollbar doesn't show, which your can read more about here: Flexbox column-reverse and overflow in Firefox/IE
The upside is you have ~ 90% browser support on mobile/tablets and ~ 65% for desktop, and counting as the bug gets fixed, ...and there is a workaround.
// scroll to bottom
function updateScroll(el){
el.scrollTop = el.scrollHeight;
}
// only shift-up if at bottom
function scrollAtBottom(el){
return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}
In the below code snippet I've added the 2 functions from above, to make IE/Edge/Firefox behave in the same way flex-direction: column-reverse; does.
function addContent () {
var msgdiv = document.getElementById('messages');
var msgtxt = document.getElementById('inputs');
var atbottom = scrollAtBottom(msgdiv);
if (msgtxt.value.length > 0) {
msgdiv.innerHTML += msgtxt.value + '<br/>';
msgtxt.value = "";
} else {
msgdiv.innerHTML += 'Long long content ' + (tempCounter++) + '!<br/>';
}
/* if at bottom and is IE/Edge/Firefox */
if (atbottom && (!isWebkit || isEdge)) {
updateScroll(msgdiv);
}
}
function resizeInput () {
var msgdiv = document.getElementById('messages');
var msgtxt = document.getElementById('inputs');
var atbottom = scrollAtBottom(msgdiv);
if (msgtxt.style.height == '120px') {
msgtxt.style.height = 'auto';
} else {
msgtxt.style.height = '120px';
}
/* if at bottom and is IE/Edge/Firefox */
if (atbottom && (!isWebkit || isEdge)) {
updateScroll(msgdiv);
}
}
/* fix for IE/Edge/Firefox */
var isWebkit = ('WebkitAppearance' in document.documentElement.style);
var isEdge = ('-ms-accelerator' in document.documentElement.style);
var tempCounter = 6;
function updateScroll(el){
el.scrollTop = el.scrollHeight;
}
function scrollAtBottom(el){
return (el.scrollTop + 5 >= (el.scrollHeight - el.offsetHeight));
}
html, body { height:100%; margin:0; padding:0; }
.chat-window{
display:flex;
flex-direction:column;
height:100%;
}
.chat-messages{
flex: 1;
height:100%;
overflow: auto;
display: flex;
flex-direction: column-reverse;
}
.chat-input { border-top: 1px solid #999; padding: 20px 5px }
.chat-input-text { width: 60%; min-height: 40px; max-width: 60%; }
/* temp. buttons for demo */
button { width: 12%; height: 44px; margin-left: 5%; vertical-align: top; }
/* begin - fix for hidden scrollbar in IE/Edge/Firefox */
.chat-messages-text{ overflow: auto; }
#media screen and (-webkit-min-device-pixel-ratio:0) {
.chat-messages-text{ overflow: visible; }
/* reset Edge as it identifies itself as webkit */
#supports (-ms-accelerator:true) { .chat-messages-text{ overflow: auto; } }
}
/* hide resize FF */
#-moz-document url-prefix() { .chat-input-text { resize: none } }
/* end - fix for hidden scrollbar in IE/Edge/Firefox */
<div class="chat-window">
<div class="chat-messages">
<div class="chat-messages-text" id="messages">
Long long content 1!<br/>
Long long content 2!<br/>
Long long content 3!<br/>
Long long content 4!<br/>
Long long content 5!<br/>
</div>
</div>
<div class="chat-input">
<textarea class="chat-input-text" placeholder="Type your message here..." id="inputs"></textarea>
<button onclick="addContent();">Add msg</button>
<button onclick="resizeInput();">Resize input</button>
</div>
</div>
Side note 1: The detection method is not fully tested, but it should work on newer browsers.
Side note 2: Attach a resize event handler for the chat-input might be more efficient then calling the updateScroll function.
Note: Credits to HaZardouS for reusing his html structure
You just need one CSS rule set:
.messages-container, .scroll {transform: scale(1,-1);}
That's it, you're done!
How it works: First, it vertically flips the container element so that the top becomes the bottom (giving us the desired scroll orientation), then it flips the content element so that the messages won't be upside down.
This approach works in all modern browsers. It does have a strange side effect, though: when you use a mouse wheel in the message box, the scroll direction is reversed. This can be fixed with a few lines of JavaScript, as shown below.
Here's a demo and a fiddle to play with:
//Reverse wheel direction
document.querySelector('.messages-container').addEventListener('wheel', function(e) {
if(e.deltaY) {
e.preventDefault();
e.currentTarget.scrollTop -= e.deltaY;
}
});
//The rest of the JS just handles the test buttons and is not part of the solution
send = function() {
var inp = document.querySelector('.text-input');
document.querySelector('.scroll').insertAdjacentHTML('beforeend', '<p>' + inp.value);
inp.value = '';
inp.focus();
}
resize = function() {
var inp = document.querySelector('.text-input');
inp.style.height = inp.style.height === '50%' ? null : '50%';
}
html,body {height: 100%;margin: 0;}
.conversation {
display: flex;
flex-direction: column;
height: 100%;
}
.messages-container {
flex-shrink: 10;
height: 100%;
overflow: auto;
}
.messages-container, .scroll {transform: scale(1,-1);}
.text-input {resize: vertical;}
<div class="conversation">
<div class="messages-container">
<div class="scroll">
<p>Message 1<p>Message 2<p>Message 3<p>Message 4<p>Message 5
<p>Message 6<p>Message 7<p>Message 8<p>Message 9<p>Message 10<p>Message 11<p>Message 12<p>Message 13<p>Message 14<p>Message 15<p>Message 16<p>Message 17<p>Message 18<p>Message 19<p>Message 20
</div>
</div>
<textarea class="text-input" autofocus>Your message</textarea>
<div>
<button id="send" onclick="send();">Send input</button>
<button id="resize" onclick="resize();">Resize input box</button>
</div>
</div>
Edit: thanks to #SomeoneSpecial for suggesting a simplification to the scroll code!
Please try the following fiddle - https://jsfiddle.net/Hazardous/bypxg25c/. Although the fiddle is currently using jQuery to grow/resize the text area, the crux is in the flex related styles used for the messages-container and input-container classes -
.messages-container{
order:1;
flex:0.9 1 auto;
overflow-y:auto;
display:flex;
flex-direction:row;
flex-wrap:nowrap;
justify-content:flex-start;
align-items:stretch;
align-content:stretch;
}
.input-container{
order:2;
flex:0.1 0 auto;
}
The flex-shrink value is set to 1 for .messages-container and 0 for .input-container. This ensures that messages-container shrinks when there is a reallocation of size.
I've moved text-input within messages, absolute positioned it to the bottom of the container and given messages enough bottom padding to space accordingly.
Run some code to add a class to conversation, which changes the height of text-input and bottom padding of messages using a nice CSS transition animation.
The JavaScript runs a "scrollTo" function at the same time as the CSS transition is running to keep the scroll at the bottom.
When the scroll comes off the bottom again, we remove the class from conversation
Hope this helps.
https://jsfiddle.net/cnvzLfso/5/
var doScollCheck = true;
var objConv = document.querySelector('.conversation');
var objMessages = document.querySelector('.messages');
var objInput = document.querySelector('.text-input');
function scrollTo(element, to, duration) {
if (duration <= 0) {
doScollCheck = true;
return;
}
var difference = to - element.scrollTop;
var perTick = difference / duration * 10;
setTimeout(function() {
element.scrollTop = element.scrollTop + perTick;
if (element.scrollTop === to) {
doScollCheck = true;
return;
}
scrollTo(element, to, duration - 10);
}, 10);
}
function resizeInput(atBottom) {
var className = 'bigger',
hasClass;
if (objConv.classList) {
hasClass = objConv.classList.contains(className);
} else {
hasClass = new RegExp('(^| )' + className + '( |$)', 'gi').test(objConv.className);
}
if (atBottom) {
if (!hasClass) {
doScollCheck = false;
if (objConv.classList) {
objConv.classList.add(className);
} else {
objConv.className += ' ' + className;
}
scrollTo(objMessages, (objMessages.scrollHeight - objMessages.offsetHeight) + 50, 500);
}
} else {
if (hasClass) {
if (objConv.classList) {
objConv.classList.remove(className);
} else {
objConv.className = objConv.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
}
}
objMessages.addEventListener('scroll', function() {
if (doScollCheck) {
var isBottom = ((this.scrollHeight - this.offsetHeight) === this.scrollTop);
resizeInput(isBottom);
}
});
html,
body {
height: 100%;
width: 100%;
background: white;
}
body {
margin: 0;
padding: 0;
}
.conversation {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
position: relative;
}
.messages {
overflow-y: scroll;
padding: 10px 10px 60px 10px;
-webkit-transition: padding .5s;
-moz-transition: padding .5s;
transition: padding .5s;
}
.text-input {
padding: 10px;
-webkit-transition: height .5s;
-moz-transition: height .5s;
transition: height .5s;
position: absolute;
bottom: 0;
height: 50px;
background: white;
}
.conversation.bigger .messages {
padding-bottom: 110px;
}
.conversation.bigger .text-input {
height: 100px;
}
.text-input input {
height: 100%;
}
<div class="conversation">
<div class="messages">
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is a message content
</p>
<p>
This is the last message
</p>
<div class="text-input">
<input type="text" />
</div>
</div>
</div>
You write;
Now, consider this case:
The user scrolls to the bottom of the conversation
The .text-input, dynamically gets bigger
Wouldn't the method that dynamically sets the .text-input be the logical place to fire this.props.onResize().
To whom it may concern,
The answers above did not suffice my question.
The solution I found was to make my innerWidth and innerHeight variable constant - as the innerWidth of the browser changes on scroll to adapt for the scrollbar.
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
OR FOR REACT
this.setState({width: window.innerWidth, height: window.innerHeight})
In other words, to ignore it, you must make everything constant as if it were never scrolling. Do remember to update these on Resize / Orientation Change !
IMHO current answer is not a correct one:
1/ flex-direction: column-reverse; reverses the order of messages - I didn't want that.
2/ javascript there is also a bit hacky and obsolete
If you want to make it like a PRO use spacer-box which has properties:
flex-grow: 1;
flex-basis: 0;
and is located above messages. It pushes them down to the chat input.
When user is typing new messages and input height is growing the scrollbar moves up, but when the message is sent (input is cleared) scrollbar is back at bottom.
Check my snippet:
body {
background: #ccc;
}
.chat {
display: flex;
flex-direction: column;
width: 300px;
max-height: 300px;
max-width: 90%;
background: #fff;
}
.spacer-box {
flex-basis: 0;
flex-grow: 1;
}
.messages {
display: flex;
flex-direction: column;
overflow-y: auto;
flex-grow: 1;
padding: 24px 24px 4px;
}
.footer {
padding: 4px 24px 24px;
}
#chat-input {
width: 100%;
max-height: 100px;
overflow-y: auto;
border: 1px solid pink;
outline: none;
user-select: text;
white-space: pre-wrap;
overflow-wrap: break-word;
}
<div class="chat">
<div class="messages">
<div class="spacer-box"></div>
<div class="message">1</div>
<div class="message">2</div>
<div class="message">3</div>
<div class="message">4</div>
<div class="message">5</div>
<div class="message">6</div>
<div class="message">7</div>
<div class="message">8</div>
<div class="message">9</div>
<div class="message">10</div>
<div class="message">11</div>
<div class="message">12</div>
<div class="message">13</div>
<div class="message">14</div>
<div class="message">15</div>
<div class="message">16</div>
<div class="message">17</div>
<div class="message">18</div>
</div>
<div class="footer">
<div contenteditable role="textbox" id="chat-input"></div>
</div>
<div>
Hope I could help :)
Cheers

Categories