Can't set element's scrollLeft properties in VueJS - javascript

I'm actually coding an infinite slider in vuejs (using Nuxt 3 RC.10) and I'm facing a problem : my scrollLeft don't want to be reset.
Let me explain. I got two rows of images, and when the left side of the second row collides with the left side of the container, I want to set the scrollLeft to 0. This works perfectly on both browser with Svelte, but I did not succeed with Vuejs. For some reason the second row is glitching.
After debuging, my condition is comes true but the problem seems to come from instruction slider.value.scrollLeft = 0
Thanks in advance for your help
Here my component Slider:
(PS: the commented overflow: hidden is for test purposes.
<script setup>
const slider = ref(null)
const scrollEnabled = ref(true)
const centeredComputed = computed(() => {
return scrollEnabled.value ? "" : "justify-content: center;"
})
function scroll() {
let rows = document.getElementsByClassName('row')
slider.value.scrollLeft += 20
if (scrollEnabled.value && rows[1].getBoundingClientRect().left <= slider.value.getBoundingClientRect().left) {
slider.value.scrollLeft = 0
}
scrollEnabled.value = rows[0].clientWidth > slider.value.clientWidth
}
onMounted(() => {
slider.value.addEventListener('scroll', scroll)
window.addEventListener('load', scroll)
window.addEventListener('resize', scroll)
})
onUnmounted(() => {
window.removeEventListener('load', scroll)
window.removeEventListener('resize', scroll)
})
</script>
<template>
<div ref="slider" class="slider" :style="centeredComputed">
<div class="row">
<img v-for="id in 32" class="logo" :src="`../assets/images/partenaires/${id}.png`" :alt="'Partenaire numéro ' + id">
</div>
<div v-if="scrollEnabled" class="row">
<img v-for="id in 32" class="logo" :src="`../assets/images/partenaires/${id}.png`" :alt="'Partenaire numéro ' + id">
</div>
</div>
</template>
<style lang='scss' scoped>
.slider {
width: 100%;
height: 100%;
white-space: nowrap;
//overflow-x: hidden;
overflow: auto;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: flex-start;
}
.row {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: flex-start;
height: 100%;
}
.logo {
display: inline-flex;
margin: 0 2rem;
height: 100%;
}
</style>

Related

How to fade In/Out text + images

I'm trying to create a reviews area within my website, to display some clients reviews I gathered + their logo. For now, I've managed to change both (review+logo) every 5 seconds :)! it works!
What I'm trying to achieve now is to fade/out and fade/in the next review + logo. I'm not sure where should I search about it, can someone point me towards the right post or article? thanks
var review = new Array();
review.push("Text1");
review.push("Text2");
review.push("Text3");
var clientlogo = new Array();
clientlogo.push("");
clientlogo.push("");
clientlogo.push("");
var point = 0;
function changeText(){
$('.review').html(review[point]);
$('.client-logo').attr('src',clientlogo[point]);
if(point < ( review.length - 1 ) ){
point++;
}else{
point = 0;
}
}
setInterval(changeText, 5000); /*Call it here*/
changeText();
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #4d4d4d;
margin: 0 auto;
}
.review-container {
width: 400px;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #4d4d4d;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="review-container">
<div class="review"></div>
<img class="client-logo" src=""/>
</div>
jQuery includes stuff for fading: https://api.jquery.com/category/effects/fading/
It takes a callback function that's called once the animation is finished. You can use this to fade in the next review after the previous one is finished fading out.
previousReview.fadeOut(delay, function() { nextReview.fadeIn(delay) });
And then you can still use your setInterval call to know how often to run this.

CSS Partial Scroll Snapping

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>

How to deactivate button once clicked Javascript

I want to stop incrementing the number of individual likes if the photo was liked(clicked) once, and increment the total number of likes for each individual photo liked(clicked)
individual photo likes likesAfterAddition
global photo likes globalNumberOfLikes
For the moment it is increasing every time I click in both individual and global likes, I know it is not the right logic!
What logic can I use please?
//increment likes on click
function incrementLikesOnClick() {
const heartIcons = Array.from(document.getElementsByClassName('heartIcon')); // multiple heart icons
heartIcons.forEach((likeIcon, index) => likeIcon.addEventListener('click', () => {
const individualLikeBox = document.getElementsByClassName('under-photo-info');
const totalLikesDivBox = document.getElementById("likesBox");
likeIcon.classList.add('activeRed');
let likesAfterAddition = likesTable[index] + 1; // add 1 like to the individual current photo
likesTable.splice(index, 1, likesAfterAddition); // replace the old value from the Array with the new value
let sum = likesTable.reduce(function(a, b){return a + b;}); // return the sum of the array
let globalNumberOfLikes = sum; // the sum of the array
individualLikeBox[index].innerHTML = `<span'>${likesAfterAddition}</span>`
totalLikesDivBox.innerHTML = `<div class="Likes">${globalNumberOfLikes}<i class="fas fa-heart"></i></div>`
console.log(likesTable)
}))
}
instead of using for loop to set event listeners which is not efficient
you can use the feature of bubbling, so when any of dom element is clicked, the event will bubble up of its parent elements sequentially till it reaches the parent dom
//increment likes on click
function incrementLikesOnClick() {
document.addEventListener("DOMContentLoaded", function(event) {
// Your code to run since DOM is loaded and ready
document.addEventListener('click', () => {
let clicked = event.target;
//element with class heartIcon is clicked and it doesnt have activeRed class
if(clicked.classList.contains('heartIcon') && !clicked.classList.contains('activeRed')){
let productContainer = clicked.parentElement.parentElement; // till you reach the product container
const individualLikeBox = productContainer.getElementsByClassName('under-photo-info');
const totalLikesDivBox = productContainer.getElementById("likesBox");
clicked.classList.add('activeRed');
// ..whatever extra logic you want to add
}
});
});
}
If the like icon is a button (which I assume it is). U can just add a 'disabled' attribute to it as part of the event handler (for the 'click' eventListener).
'When present, it specifies that the button should be disabled.
A disabled button is unusable and un-clickable.' (source)
I would calculate the total likes based on the presence of an "active" class on each button.
const totalLikesEl = document.querySelector('#total-likes');
const updateTotalLikes = () => {
totalLikesEl.textContent = document.querySelectorAll('.like.active').length;
};
const toggleLike = (e) => {
const button = e.currentTarget.classList.toggle('active');
updateTotalLikes();
};
document.querySelectorAll('.like').forEach(likeBtn => {
likeBtn.addEventListener('click', toggleLike);
});
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
.cards {
display: flex;
flex-direction: row-wrap;
justify-content: center;
}
.card {
display: flex;
flex-direction: column;
padding: 0.25em;
margin: 0.5em;
border: thin solid grey;
}
.card-content {
background: grey;
width: 6em;
height: 6em;
}
.card-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 0.5em;
}
.like > .fa-heart {
color: grey;
}
.like.active > .fa-heart {
color: red;
}
.example-1 .card-content {
background: rgb(63,94,251);
background: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,168,1) 100%);
}
.example-2 .card-content {
background: rgb(251,63,94);
background: radial-gradient(circle, rgba(251, 63,94,1) 0%, rgba(168,252,70,1) 100%);
}
.example-3 .card-content {
background: rgb(94,63,251);
background: radial-gradient(circle, rgba(94,63,251,1) 0%, rgba(70,252,168,1) 100%);
}
.status {
text-align: center;
margin-top: 0.5em;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" rel="stylesheet"/>
<div class="cards">
<div class="card example-1">
<div class="card-content"></div>
<div class="card-actions">
<button class="like">
Like <i class="fas fa-heart"></i>
</button>
</div>
</div>
<div class="card example-2">
<div class="card-content"></div>
<div class="card-actions">
<button class="like">
Like <i class="fas fa-heart"></i>
</button>
</div>
</div>
<div class="card example-3">
<div class="card-content"></div>
<div class="card-actions">
<button class="like">
Like <i class="fas fa-heart"></i>
</button>
</div>
</div>
</div>
<div class="status">
<strong>Total Likes:</strong>
<span id="total-likes">0</span>
</div>

Creating an active status for links on a sticky nav bar in relation to their sections

I have been trying to make the links in my sticky nav bar highlight when on the corresponding section.
I have been able to get it to partially work using JavaScript with all links being highlighted when scrolling over the corresponding sections, however, the section 1 link is highlighted even if I'm on the landing page and have not scrolled at all. How can I make it so that section 1 isn't highlighted until I scroll to the section?
I've tried to use an if statement to set all links to not active if window.scrollY === 0, but this only works for the initial render and doesn't work after I have scrolled down and back.
Update
Adding another if statement at the start of the highlightNav function to check if window.scrollY === 0 has allowed me to remove the .active class from all the links, but ideally I want it to work when window.scrollY < 450 which doesn't seem to work.
My JavaScript is as follows:
const highlightNav = () => {
const links = document.querySelectorAll('.link');
const section = document.querySelectorAll('.section');
//updated if statement
//I tried using window.scrollY < 450 in the if statement below with no success
if (window.scrollY === 0) {
links.forEach((link) => link.classList.remove('active'));
};
if (window.scrollY !== 0) {
const changeLinkState = () => {
let index = section.length;
while(--index && window.scrollY + 450 < section[index].offsetTop) {};
links.forEach((link) => link.classList.remove('active'));
links[index].classList.add('active');
};
changeLinkState();
};
};
highlightNav();
window.addEventListener('scroll', highlightNav);
The HTML follows this structure:
<header>
<div class="nav-bar">
<nav>
About
Pricing
Contact
Sign in
</nav>
</div>
<section>
<!-- Landing Screen -->
</section>
</header>
<article>
<section id="about" class="section">
<!-- section 1 -->
</section>
<section id="price" class="section">
<!-- Section two -->
</section>
<section id="contact" class="section">
<!-- Section Three -->
</section>
</article>
css:
.nav-bar {
position: fixed;
top: 0px;
width: 100%;
height: 8vh;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #297094;
text-align: center;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.3);
}
nav {
width: 25%;
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
}
nav a {
padding: 0 10%;
border-left: 1px solid black;
height: 100%;
display: flex;
align-items: center;
font-size: 1.25rem;
color: white;
transition: background-color 600ms;
text-decoration: none;
white-space: nowrap;
}
nav a:hover {
background-color: #3B80A0;
cursor: pointer;
}
.active {
background-color: #3B80A0;
}
I'm very new to JavaScript so any help is welcome, thanks in advance.
Main issue:
Instead of mixing that 450 math into a while loop, define that amount as a constant with a descriptive name, then use it in your conditional.
A couple other notes:
The while loop in your posted code doesn't do anything, since there's nothing inside the {}
You call links.forEach((link) => link.classList.remove('active')); twice, so make that its own function (I've named it clearAllActive() here).
const highlightNav = () => {
const links = document.querySelectorAll('.link');
const sections = document.querySelectorAll('.section');
const clearAllActive = () => links.forEach(link => link.classList.remove('active'));
const highlightActive = () => {
// your highlighting code here
};
// don't start highlighting until we've scrolled at least this far
const minScrollBeforeHighlighting = 450;
if (window.scrollY < minScrollBeforeHighlighting) {
clearAllActive();
} else {
highlightActive();
};
};
highlightNav();
window.addEventListener('scroll', highlightNav);

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