Can't horizontal scroll past 200vw - javascript

I have this code that goes from vertical to horizontal and back to vertical scrolling.
If I set the min-width to 50vw I can only see 4 sections, if I set it to 100vw (shown in example) it will only show 2 section.
There seems to be a width limit of 200vw, why can't I display anything over that?
I don't really understand why and thus I don't understand how to fix it. Here is my code:
let lastKnownScrollPosition = 0;
let deltaY = 0;
window.addEventListener("scroll", wheelHandler);
document.querySelectorAll('.sticky-container').forEach(function(container) {
const stikyContainerHeight = (container.querySelector('main').offsetWidth + window.innerHeight);
container.setAttribute('style', 'height: ' + stikyContainerHeight + 'px');
});
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return rect.top <= 0 && rect.bottom > document.documentElement.clientHeight;
}
function wheelHandler(event) {
deltaY = window.scrollY - lastKnownScrollPosition;
lastKnownScrollPosition = window.scrollY;
const containerInViewPort = Array.from(document.querySelectorAll('.sticky-container')).filter(function(container) {
return isElementInViewport(container);
})[0];
if (!containerInViewPort) {
return;
}
var isPlaceHolderBelowTop = containerInViewPort.offsetTop < document.documentElement.scrollTop;
var isPlaceHolderBelowBottom = containerInViewPort.offsetTop + containerInViewPort.offsetHeight > document.documentElement.scrollTop;
let g_canScrollHorizontally = isPlaceHolderBelowTop && isPlaceHolderBelowBottom;
if (g_canScrollHorizontally) {
containerInViewPort.querySelector('main').scrollLeft += deltaY;
}
}
html,
body {
margin: 0;
font-family: sans-serif;
}
.vertical-section {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
main {
overflow-x: hidden;
display: flex;
position: sticky;
top: 0;
}
h1 {
margin: 0;
padding: 0;
}
section {
min-width: 100vw; /*Works perfeclty when it's 50vw*/
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 4ch;
}
section:nth-child(even) {
background-color: teal;
color: white;
}
<div class="vertical-section">
Content Before
</div>
<div class="sticky-container">
<main>
<section>
<h1>First</h1>
</section>
<section>
<h1>Second</h1>
</section>
<section>
<h1>Third</h1>
</section>
<section>
<h1>Fourth</h1>
</section>
<section>
<h1>Fifth</h1>
</section>
<section>
<h1>Last</h1>
</section>
</main>
</div>
<div class="vertical-section">
Content After
</div>
Can someone tell me what's going on here and how to fix it?

Basically when you scroll on the Y axis => up/down, the amount it will scroll from left to right, or on the X axis, is relevant to the height of the distance you are calculating in your forEach loop stikyContainerHeight.
For example, If your container is 500px wide using the 100vw css rule, and you have 6 elements total with 5 being past the right fold, this would basically be (500px * 6 = 3000px). When you scroll down in height, in order to get the width needed to show all 3000px, you must make your height the same in pixels. You can iterate over the elements that make up the main sections children and add those widths together or you can take the width of the screen and multiply it by the length of the parents sections children.
You are only adding enough distance in scrollable height to allow it to scroll two divs on the X axis. If you want it to scroll the entire distance, then get the width of the entire parent element and then multiply that by the amount of sections you are scrolling and apply to the stikyContainerHeight variable.
let lastKnownScrollPosition = 0;
let deltaY = 0;
// changed to a single query
const main = document.querySelector('.sticky-container');
// get the section elements so we can use its nodelist arrays length
const sec = document.querySelectorAll('.sticky-container section');
window.addEventListener("scroll", wheelHandler);
// no need for the forEach loop as there is only one element witht he class sticky-container
const stikyContainerHeight = main.getBoundingClientRect().width * sec.length;
main.setAttribute('style', 'height: ' + stikyContainerHeight + 'px');
// everything below here is the same
function isElementInViewport(el) {
const rect = el.getBoundingClientRect();
return rect.top <= 0 && rect.bottom > document.documentElement.clientHeight;
}
function wheelHandler(event) {
deltaY = window.scrollY - lastKnownScrollPosition;
lastKnownScrollPosition = window.scrollY;
const containerInViewPort = Array.from(document.querySelectorAll('.sticky-container')).filter(function(container) {
return isElementInViewport(container);
})[0];
if (!containerInViewPort) {
return;
}
var isPlaceHolderBelowTop = containerInViewPort.offsetTop < document.documentElement.scrollTop;
var isPlaceHolderBelowBottom = containerInViewPort.offsetTop + containerInViewPort.offsetHeight > document.documentElement.scrollTop;
let g_canScrollHorizontally = isPlaceHolderBelowTop && isPlaceHolderBelowBottom;
if (g_canScrollHorizontally) {
containerInViewPort.querySelector('main').scrollLeft += deltaY;
}
}
html,
body {
margin: 0;
font-family: sans-serif;
}
.vertical-section {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
main {
overflow-x: hidden;
display: flex;
position: sticky;
top: 0;
}
h1 {
margin: 0;
padding: 0;
}
section {
min-width: 100vw;
/*Works perfeclty when it's 50vw*/
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 4ch;
}
section:nth-child(even) {
background-color: teal;
color: white;
}
<div class="vertical-section">
Content Before
</div>
<div class="sticky-container">
<main>
<section>
<h1>First</h1>
</section>
<section>
<h1>Second</h1>
</section>
<section>
<h1>Third</h1>
</section>
<section>
<h1>Fourth</h1>
</section>
<section>
<h1>Fifth</h1>
</section>
<section>
<h1>Last</h1>
</section>
</main>
</div>
<div class="vertical-section">
Content After
</div>
You could also iterate over the sections themselves and concatenate their values in a forEach loop.
let stikyContainerHeight = Number();
sec.forEach(s => stikyContainerHeight += s.getBoundingClientRect().width);
main.setAttribute('style', 'height: ' + stikyContainerHeight + 'px');

Related

How do I fix my code such that the linear interpolation doesn't cross over?

I'm trying to make an infinite marquee (scrolling horizontal text), but on scroll it speeds up the translation of the HTML elements. I am achieving this effect by using a linear interpolation function.
You can see the effect on this site that I'm trying to remake: https://altsdigital.com/ It says "Not your usual SEO agency"
Mine almost works - the problem is that when my HTML resets its position - my text overlaps and briefly translates to the left before correcting. Keep your eyes on the left side of the page. You will see that the text overlaps at one brief moment then translates left (during it's movement to the right), it eventually corrects itself as it plays retaining the original gap. You can see in this screenshot the "t" and "I" are overlapping. Shortly after this, the text on the left translates left and there is a gap between the letters. I want it to have a gap and not briefly translate left.
I have no idea how to fix this - I've tried calling the lerp function on scroll but nothing seems to change. Thanks in advance.
Here's the code:
const lerp = (current, target, factor) => {
let holder = current * (1 - factor) + target * factor;
holder = parseFloat(holder).toFixed(3);
return holder;
};
class LoopingText {
constructor(DOMElements) {
this.DOMElements = DOMElements;
this.lerpingData = {
counterOne: { current: 0, target: 0 },
counterTwo: { current: 100, target: 100 },
};
this.interpolationFactor = 0.1;
this.direction = true;
this.speed = 0.2;
this.render();
this.onScroll();
}
onScroll() {
window.addEventListener("scroll", () => {
this.lerpingData["counterOne"].target += this.speed * 5;
this.lerpingData["counterTwo"].target += this.speed * 5;
});
}
lerp() {
for (const counter in this.lerpingData) {
this.lerpingData[counter].current = lerp(
this.lerpingData[counter].current,
this.lerpingData[counter].target,
this.interpolationFactor
);
}
this.lerpingData["counterOne"].target += this.speed;
this.lerpingData["counterTwo"].target += this.speed;
if (this.lerpingData["counterOne"].target < 100) {
this.DOMElements[0].style.transform = `translate(${this.lerpingData["counterOne"].current}%, 0%)`;
} else {
this.lerpingData["counterOne"].current = -100;
this.lerpingData["counterOne"].target = -100;
}
if (this.lerpingData["counterTwo"].target < 100) {
this.DOMElements[1].style.transform = `translate(${this.lerpingData["counterTwo"].current}%, 0%)`;
} else {
this.lerpingData["counterTwo"].current = -100;
this.lerpingData["counterTwo"].target = -100;
}
}
render() {
this.lerp();
window.requestAnimationFrame(() => this.render());
}
}
let textArray = document.getElementsByClassName("item");
new LoopingText(textArray);
#import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght#0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Poppins";
}
.hero-section {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
position: relative;
width: 100%;
}
.loop-container {
position: relative;
width: 100%;
display: flex;
/* padding-right: 24px; */
}
.item {
position: absolute;
font-size: 15rem;
white-space: nowrap;
margin: 0;
}
span {
transition: all 0.2s;
cursor: default;
}
.hover:hover {
color: gray;
transition: all 0.2s;
}
<body>
<section class="hero-section">
<div class="loop-container">
<div class="item">Infinite Horizontal Looping Text</div>
<div class="item">Infinite Horizontal Looping Text</div>
</div>
</section>
<section class="hero-section">
</section>
</body>

Scaling carousel centering selected div

I am working on a slider that displays 5 slides (squares). The selected slide is centered on the screen and it is bigger than the others. Users can click buttons to make the slider slide left or right selecting an adjacent square and centering it on the screen.
This is the HTML:
const slider = document.querySelector(".slider");
const slideLeft = document.querySelector(".slide-left");
const slideRight = document.querySelector(".slide-right");
const getSelectedSquare = () => document.querySelector(".selected");
let translate = 0;
function changeSelectedClass(direction) {
const selected = getSelectedSquare();
const map = {
left: selected.nextElementSibling,
right: selected.previousElementSibling,
}
const nextSelected = map[direction];
selected.classList.remove("selected");
nextSelected.classList.add("selected");
}
function centerSelected() {
const selected = getSelectedSquare();
const { width, left } = selected.getBoundingClientRect();
const distanceFromCenter = (window.innerWidth / 2) - left - (width / 2);
translate += distanceFromCenter;
slider.style.transform = `translateX(${translate}px)`;
}
slideRight.onclick = () => { changeSelectedClass("right"); centerSelected(); };
slideLeft.onclick = () => { changeSelectedClass("left"); centerSelected(); };
* {
margin: 0;
box-sizing: border-box;
}
body {
position: relative;
display: grid;
place-items: center;
min-height: 100vh;
}
.slider {
display: flex;
align-items: center;
gap: 10px;
}
.square {
width: 250px;
aspect-ratio: 1 / 1;
border: 1px solid black;
}
.square.selected {
background: lightpink;
}
.square:not(.selected) {
width: 200px;
aspect-ratio: 1 / 1;
}
<div class="container">
<div class="slider">
<div class="square"> 1 </div>
<div class="square">2</div>
<div class="square selected">3</div>
<div class="square">4</div>
<div class="square">5</div>
</div>
<div class="btn-wrapper">
<button class="btn slide-left">⇦</button>
<button class="btn slide-right">⇨</button>
</div>
</div>
The problem is that at the moment the selected div is not centered correctly when a button is pressed and the slider slides. The code works fine if all the divs have the same dimensions, but when scaling comes into play I'm missing something.
I'm no expert at all in animations and if the approach I took is wrong I would rather rebuild it, than make adjustments to make it work as it is.
Additional requirements are that the dimentions of the squares should be dynamic (eg. 15vw), the distance between them should be the same (here I'm using flex gap) and the squares should become smaller and smaller the furthest away from the selected one (this is not implemented yet in the code sample).
Since I'm learning new stuff, I'd rather not use external libraries (that do that exact thing (I know)).
A codepen of the work until now: https://codepen.io/mtenti/pen/ExvNdZL

Mobile browsers add a gap at the end of final element via css gap property

I'm back on Stack Overflow after a long time because I'm truly stuck at an issue I cannot get around even after hours piling up in front of the screen.
I have made a simple widget using CSS + HTML + JavaScript which scrolls elements in an overflowing-x container.
It works in a simple way, there is JavaScript code that adds a 205 value to the property scrollLeft of the overflowing container. The number comes from the fixed width of the images + the gap value which is 5px. Here is the code:
HTML:
<div id="controlContainer">
<a class="adButton" onclick="Scroll(-1)">❮</a>
<div id="topics">
<div class="adItem" onclick="ChangeTopic(1)">
<p>History</p>
<img src="images/other_samples/hundredgates.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(2)">
<p>Oceans</p>
<img src="images/other_samples/goldensea.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(3)">
<p>Sports</p>
<img src="images/other_samples/kite_surf.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(4)">
<p>Travel</p>
<img src="images/other_samples/antiparos_church.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(5)">
<p>Nightlife</p>
<img src="images/other_samples/nightlife.png">
</div>
</div>
<a class="adButton" onclick="Scroll(1)">❯</a>
</div>
CSS:
#controlContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 20px;
}
#topics {
display: inherit;
gap: 5px;
overflow:hidden;
white-space: nowrap;
scroll-behavior: smooth;
}
.adItem {
position: relative;
}
.adItem img {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 20px;
}
.adItem p {
position: absolute;
left: 16px;
top: 8px;
text-align: center;
color: #ffff;
font-family: Georgia, "Times New Roman", Times, serif;
font-size: 50px;
margin: 0px;
user-select: none;
pointer: default:
}
And finally JS, which still needs some work tbh:
var LastClick;
var Delay = 300;
var SelectedElement;
var adControl;
var currentScroll;
window.onload = function () {SelectedElement = document.getElementById("ad1"); adControl = document.getElementById("topics"); resizeController();};
window.onresize = debounce(() => resizeController());; //resize the container when the screen does
//window.addEventListener('DOMContentLoaded', (event) => {SelectedElement = document.getElementById("ad1")});
function Scroll(n) {
if (LastClick >= (Date.now() - Delay)) {
return;
}
if (n == 1) {
adControl.scrollLeft += 205;
checkPos();
} else if (n == -1) {
adControl.scrollLeft -= 205;
checkPos();
}
LastClick = Date.now();
console.log(adControl.scrollLeft);
}; // This function is what's handling scrolling. THey are called via onclick events on the HTML Button elements
function checkPos() {
var elementWidth = adControl.scrollLeft;
if (elementWidth % 5 === 0) {
// do nothing
} else {
var newWidth = Math.ceil(elementWidth/5)*5;
console.log("old width: %s, new width: %s", elementWidth, newWidth)
adControl.scrollLeft = newWidth;
}
}; //Some position checks... it basically calculates if scrollLeft is divisible by 5, because all images are 200px long plus the 5px gap, so that should always be a multiple of 5.
function ChangeTopic(id) {
SelectedElement.style.display = "none";
SelectedElement = document.getElementById("ad" + id);
SelectedElement.style.display = "flex";
}; //That just changes the topic of another element.
function debounce(func, timeout = 1000){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}; //This is a debounce function for the resize event, it prevents it from firing it too much.
function resizeController() {
adControl.style.maxWidth = "";
var elementWidth = adControl.offsetWidth;
var scroll = adControl.ScrollLeft;
var itemNo = (Math.floor(elementWidth / 200))
if (itemNo > 3) {
itemNo = 3
};
var newWidth = (itemNo*200);
newWidth = newWidth+(5*itemNo)
adControl.style.maxWidth = (newWidth + "px");
if (currentNo = itemNo) {
adControl.scrollLeft = scroll;
}
}; //resizes the container if need be (for mobile or tablet devices)
It actually works very well on Desktop, but on mobile, the CSS gap property which adds the gap between the images also adds a gap at the last element, like this:
That's even when I use a different browser from Firefox, like Chrome
On desktop, this gap does not exist, regardless of browser once again:
What is this? And how can I solve it? The main problem this causes is it will scroll in that tiny 5 gap space, which throws the position of my elements out of place, making them look like this:
I've thought of different methods like checking the property of ScrollLeft to detect when the view is out of the elements, but that property is completely unpredictable. For instance, when I scroll to the beginning of the element, it's not going to be necessarily zero, and even if I reach the end, the 205 value will be added even if there is not any space on the container. So that isn't reliable.
In short, I'd either need some kind of method to keep that gapping behaviour in check or solve the root problem altogether.
Yes... I'm not using any framework at all, my entire project is built on pure JavaScript. I'm not sure why I did this to myself, but oh well, all the challenge I guess.
Try and resize your font on the paragraph elements in your
div class="adItem" it appears to be overlapping the container and causing what would appear to be extra padding and i don't think it's happening on the others because the text is not long enough on others.
var LastClick;
var Delay = 300;
var SelectedElement;
var adControl;
var currentScroll;
window.onload = function () {SelectedElement = document.getElementById("ad1"); adControl = document.getElementById("topics"); resizeController();};
window.onresize = debounce(() => resizeController());; //resize the container when the screen does
//window.addEventListener('DOMContentLoaded', (event) => {SelectedElement = document.getElementById("ad1")});
function Scroll(n) {
if (LastClick >= (Date.now() - Delay)) {
return;
}
if (n == 1) {
adControl.scrollLeft += 205;
checkPos();
} else if (n == -1) {
adControl.scrollLeft -= 205;
checkPos();
}
LastClick = Date.now();
console.log(adControl.scrollLeft);
}; // This function is what's handling scrolling. THey are called via onclick events on the HTML Button elements
function checkPos() {
var elementWidth = adControl.scrollLeft;
if (elementWidth % 5 === 0) {
// do nothing
} else {
var newWidth = Math.ceil(elementWidth/5)*5;
console.log("old width: %s, new width: %s", elementWidth, newWidth)
adControl.scrollLeft = newWidth;
}
}; //Some position checks... it basically calculates if scrollLeft is divisible by 5, because all images are 200px long plus the 5px gap, so that should always be a multiple of 5.
function ChangeTopic(id) {
SelectedElement.style.display = "none";
SelectedElement = document.getElementById("ad" + id);
SelectedElement.style.display = "flex";
}; //That just changes the topic of another element.
function debounce(func, timeout = 1000){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}; //This is a debounce function for the resize event, it prevents it from firing it too much.
function resizeController() {
adControl.style.maxWidth = "";
var elementWidth = adControl.offsetWidth;
var scroll = adControl.ScrollLeft;
var itemNo = (Math.floor(elementWidth / 200))
if (itemNo > 3) {
itemNo = 3
};
var newWidth = (itemNo*200);
newWidth = newWidth+(5*itemNo)
adControl.style.maxWidth = (newWidth + "px");
if (currentNo = itemNo) {
adControl.scrollLeft = scroll;
}
}; //resizes the container if need be (for mobile or tablet devices)
#controlContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 20px;
}
#topics {
display: inherit;
gap: 5px;
overflow:hidden;
white-space: nowrap;
scroll-behavior: smooth;
}
.adItem {
position: relative;
}
.adItem img {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 20px;
}
.adItem p {
position: absolute;
left: 16px;
top: 8px;
text-align: center;
color: #ffff;
font-family: Georgia, "Times New Roman", Times, serif;
font-size: 50px;
margin: 0px;
user-select: none;
pointer: default:
}
<div id="controlContainer">
<a class="adButton" onclick="Scroll(-1)">❮</a>
<div id="topics">
<div class="adItem" onclick="ChangeTopic(1)">
<p>History</p>
<img src="images/other_samples/hundredgates.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(2)">
<p>Oceans</p>
<img src="images/other_samples/goldensea.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(3)">
<p>Sports</p>
<img src="images/other_samples/kite_surf.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(4)">
<p>Travel</p>
<img src="images/other_samples/antiparos_church.jpg">
</div>
<div class="adItem" onclick="ChangeTopic(5)">
<p>Nightlife</p>
<img src="images/other_samples/nightlife.png">
</div>
</div>
<a class="adButton" onclick="Scroll(1)">❯</a>
</div>

element.getBoundingClientRect constantly returning the initial value

I've got a div with text that follows the cursor (setting its transform:translate in the mousemove event).
One more thing it does, is setting the maxWidth accordingly if the popup would go past the viewport's right border - the text div gets thinner, but higher.
While the above works well, for whatev reason div.getBoundingClientRect refuses to report the text div's height after the max-width has been applied - it constantly returns the initial value. This problem occurs only within the script, though. Should I query it from the console, the proper (updated) height is returned. Why is that so?
const div = document.querySelector("#div");
const p = document.querySelector("#p");
document.addEventListener("mousemove", e => {
div.style.maxWidth = "400px";
div.style.transform = `translate(${e.pageX}px, ${e.pageY}px)`;
const bRect = div.getBoundingClientRect();
p.innerHTML = bRect.height;
if (document.documentElement.clientWidth < bRect.right) {
div.style.maxWidth = document.documentElement.clientWidth - e.pageX + "px";
}
;
});
#div {
max-width: 400px;
display: grid;
place-items: center;
outline: 1px solid black;
position: absolute;
pointer-events: none;
word-break: break-all;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
<p id="p"></p>
<div id="div">
Here is some text.Here is some text.Here is some text.Here is some text.Here is some text.Here is some text.
</div>
Please simply move your cursor towards the right border.
In your code you are changing the div's max width after the BoundingClientRect is already calculated. So it first outputs the initial value and only then changes the max width and hence the height.
const bRect = div.getBoundingClientRect();
p.innerHTML = bRect.height;
if (document.documentElement.clientWidth < bRect.right) {
div.style.maxWidth = document.documentElement.clientWidth - e.pageX + "px";
}
If you want to print the height after the div was resized, you have to call getBoundingClientRect again:
const div = document.querySelector("#div");
const p = document.querySelector("#p");
document.addEventListener("mousemove", e => {
div.style.maxWidth = "400px";
div.style.transform = `translate(${e.pageX}px, ${e.pageY}px)`;
const bRect = div.getBoundingClientRect();
if (document.documentElement.clientWidth < bRect.right) {
div.style.maxWidth = document.documentElement.clientWidth - e.pageX + "px";
}
p.innerHTML = div.getBoundingClientRect().height;
});
#div {
max-width: 400px;
display: grid;
place-items: center;
outline: 1px solid black;
position: absolute;
pointer-events: none;
word-break: break-all;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: auto;
}
<p id="p"></p>
<div id="div">
Here is some text.Here is some text.Here is some text.Here is some text.Here
is some text.Here is some text.
</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