I'm having an issue with a chat I've written. I wrote the following code so that it would scroll to the bottom when the user either hadn't touched the chat or was scrolled near the bottom. It works fine on Chrome but on Safari, it will always set the attribute scrollTop to 3 when a new entry is added. It also will randomly snap to the top after a while even if there had been no entry. Any suggestions will be greatly appreciated! Thanks!
function writeChat(elem) {
const chatty = document.getElementById('chat');
const nDiv = newLine();
const chInfo = window.getComputedStyle(chatty, null);
nDiv.appendChild(elem);
const total = parseInt(chInfo.getPropertyValue('height')) + chatty.scrollTop + 18;
if(total >= chatty.scrollHeight || hasScrolled == false) {
chatty.appendChild(nDiv);
setTimeout(function(){ chatty.scrollTop = 1000000; }, 0.15);
} else {
chatty.appendChild(nDiv);
}};
function newLine(cls) {
const nDiv = document.createElement('div');
if(cls) {
nDiv.className = cls;
} else {
nDiv.className = "chatEntry";
}
return nDiv; };
css for the chat div:
position: relative;
overflow-y: auto;
word-wrap: break-word;
height: 89%;
width: 98%;
font-size: 2vw;
padding: 1% 2%;
z-index: 1;
-webkit-overflow-scrolling: touch;
A gif of how it looks on Safari:
On Safari
A gif of how it looks on Chrome:
On Chrome
Related
I am looking into creating a website which will serve as a a digital leaflet for a musical theatre. The idea is to have an autoscrolling credits list as landingpage. I've looked at examples on codepen to see how this effect is been achieved. But I would also like the user to interact and scroll themselves if they want to. When they stop scrolling the credits will turn back to autoscroll. I didn't find any example who tackles this issue. Does someone of you know a script (JS, or plain css…) that can help me with this?
The most straightforward way is to set up a requestAnimationFrame() function and increment the value accordingly, then set the scroll position to it.
Then add the wheel event to detect when a user scrolls (don't use the 'scroll' event though, it already gets called when you change the scrollTop value of the body), also don't forget to cancel the requestAnimationFrame() function. The code would look something like this:
let body = document.body,
starter = document.querySelector("h1"),
scroll_counter = 0,
scrolled,
auto_scroll_kicked = false;
starter.addEventListener("click", start_scrolling);
function start_scrolling() {
auto_scroll_kicked = true;
body.offsetHeight > scroll_counter
? (scroll_counter += 1.12)
: (scroll_counter = body.offsetHeight);
document.documentElement.scrollTop = scroll_counter;
scroller = window.requestAnimationFrame(start_scrolling);
if (scroll_counter >= body.offsetHeight) {
window.cancelAnimationFrame(scroller);
}
}
window.addEventListener("wheel", (e) => {
if (auto_scroll_kicked) {
window.cancelAnimationFrame(scroller);
scroll_counter = 0;
}
});
Play with the codepen if you'd like:
https://codepen.io/SaltyMedStudent/pen/QWqVwaR?editors=0010
There are many options to use: easing functions and etc, but hope this will suffice for now.
In your auto scroll routine before changing position check if previous position is the same as current scrolling position, if it's not - the user scrolled it:
let el = document.documentElement,
footer = document.getElementById("status").querySelectorAll("td"),
scroll_position = 0,
scroll_speed = 0,
scroll_delta = 1.12,
scroller,
status = "stopped";
el.addEventListener("click", scroll);
info();
function scroll(e)
{
if (e.type == "click")
{
window.cancelAnimationFrame(scroller);
scroll_position = el.scrollTop; //make sure we start from current position
scroll_speed++; //increase speed with each click
info("auto scroll");
}
//if previous position is different, this means user scrolled
if (scroll_position != el.scrollTop)
{
scroll_speed = 0;
info("stopped by user");
return;
}
el.scrollTop += scroll_delta * scroll_speed; //scroll to new position
scroll_position = el.scrollTop; //get the current position
//loop only if we didn't reach the bottom
if (el.scrollHeight - el.scrollTop - el.clientHeight > 0)
{
scroller = window.requestAnimationFrame(scroll); //loop
}
else
{
el.scrollTop = el.scrollHeight; //make sure it's all the way to the bottom
scroll_speed = 0;
info("auto stopped");
}
}
function info(s)
{
if (typeof s === "string")
status = s;
footer[1].textContent = el.scrollTop;
footer[3].textContent = scroll_speed;
footer[5].textContent = status;
}
//generate html demo sections
for(let i = 2, section = document.createElement("section"); i < 6; i++)
{
section = section.cloneNode(false);
section.textContent = "Section " + i;
document.body.appendChild(section);
}
//register scroll listener for displaying info
window.addEventListener("scroll", info);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body
{
font-family: "Roboto", Arial;
user-select: none;
}
section
{
min-height: 100vh;
font-size: 3em;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
section:nth-child(even)
{
background: #0b0d19;
}
section:nth-child(odd)
{
background: #131524;
}
#status
{
position: fixed;
bottom: 0;
color: #fff;
margin: 0.5em;
}
#status td:first-of-type
{
text-align: end;
padding-right: 0.4em;
}
#status td:last-of-type
{
font-weight: bold;
}
<section>
Click to start Scrolling
</section>
<table id="status">
<tr><td>position:</td><td></td></tr>
<tr><td>speed:</td><td></td></tr>
<tr><td>status:</td><td></td></tr>
</table>
Using a code snippet I found online https://codepen.io/mattyfours/pen/LNgOWx
I made slight modifications and now, although the scroll/fixed functionality works, my 'fixed' side jumps when scrolling. I added 'background-size: contain' onto the fixed side which only works when scrolling has commenced However, on page load/ when no scrolling has occurred the image remains at its full-size meaning once scrolling begins the image goes from full width to 'contained' and created a jump.
Github:
https://github.com/tavimba/fixed-scroll
The issue can be seen in about.html
javascript:
var window_height;
var header_height;
var doc_height;
var posTop_sticky1;
var posBottom_sticky1;
var posTop_s2;
var posBottom_s2;
$(document).ready(function() {
getValues();
});
$(window).scroll(function(event) {
var scroll = $(window).scrollTop();
if (scroll < posTop_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
}
if (scroll > posTop_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#sticky1 .sticky').addClass('fixy');
}
if (scroll > posBottom_sticky1) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#sticky1 .sticky').addClass('bottom');
$('.bottom').css({
'max-height': window_height + 'px'
});
}
if (scroll > posTop_s2 && scroll < posBottom_s2) {
$('.sticky').removeClass('fixy');
$('.sticky').removeClass('bottom');
$('#s2 .sticky').addClass('fixy');
}
});
function getValues() {
window_height = $(window).height();
doc_height = $(document).height();
header_height = $('header').height();
//get heights first
var height_sticky1 = $('#sticky1').height();
var height_s2 = $('#s2').height();
//get top position second
posTop_sticky1 = header_height;
posTop_s2 = posTop_sticky1 + height_sticky1;
//get bottom position 3rd
posBottom_sticky1 = posTop_s2 - header_height;
posBottom_s2 = doc_height;
}
var rtime;
var timeout = false;
var delta = 200;
$(window).resize(function() {
rtime = new Date();
if (timeout === false) {
timeout = true;
setTimeout(resizeend, delta);
}
});
function resizeend() {
if (new Date() - rtime < delta) {
setTimeout(resizeend, delta);
} else {
timeout = false;
getValues();
}
}
CSS:
section {
width: 100%;
min-height: 100%;
float: left;
position: relative;
}
header {
width: 100%;
height: 5vw;
background-color: black;
float: left;
}
.sticky {
height: 100%;
width: 60%;
float: left;
position: absolute;
}
.sticky.fixy {
position: fixed;
top: 0;
left: 0;
}
.sticky.bottom {
position: absolute;
bottom: 0;
}
.green {
background-image: url(../imgs/front%20view.jpg);
background-size: cover;
}
.stickyBg {
background-image: url(../imgs/bonnets.jpg);
background-size: cover;
}
.scrolling {
float: right;
width: 50%;
padding: 20px;
h5 {
margin-left: 135px;
}
p {
margin-left: 135px;
font-size: 1em;
line-height: 1.5;
}
}
The jump is caused by change of position from absolute to fixed in combination with 100% height.
Besides, the above code has the following flaws:
Max-height assignment looks inconsistent.
JS assumes exactly two sections in HTML: #section1 and #s2. The third section won't work.
Window resize is handled incorrectly. The half-page-scroll logic consists of the two steps: CalculateVars and AdjustDOMElementPositions. For the smooth look these two actions have to be done in 3 cases: onDocumentLoad, onResize and onScroll.
Global vars.
Looks like, it needs some refactoring to get work ;)
<section class="js-half-page-scroll-section"><!-- Get rid of id -->
...
</section>
function halfPageScroll() {
let scrollTop, windowHeight, headerHeight; // and some other common vars
// Calculate vars
scrollTop = $(window).scrollTop();
//...
let repositionSection = function($section) {
let sectionHeight; // and some other vars related to current section
// Some logic
}
$('.js-half-page-scroll-section').each((i, el) => repositionSection($(el)));
}
$(document).ready(halfPageScroll);
$(window).scroll(halfPageScroll);
$(window).resize(halfPageScroll); // TODO: add some debounce wrapper with timeouts
Is it possible to scroll to .project and make the background red without to scroll slow and near the class .project?
Basically I want the user to be able to scroll and get the red color displayed even if he or she scrolls quickly, but when the user is above or under projectPosition.top, the background should be the standard color (black).
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY == projectPosition.top) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
Thanks in advance.
Instead of listen for the scroll event you could use the Intersection Observer API which can monitor elements that come in and out of view. Every time an observed element either enters or leaves the view, a callback function is fired in which you can check if an element has entered or left the view, and handle accordingly.
It's also highly performant and saves you from some top and height calculations.
Check it out in the example below.
If you have any questions about it, please let me know.
Threshold
To trigger the callback whenever an element is fully into view, not partially, set the threshold option value to [1]. The default is [0], meaning that it is triggered whenever the element is in view by a minimum of 1px. [1] states that 100% of the element has to be in view to trigger. The value can range from 0 to 1 and can contain multiple trigger points. For example
const options = {
threshold: [0, 0.5, 1]
};
Which means, the start, halfway, and fully in to view.
const project = document.querySelector('.project');
const observerCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('red');
} else {
entry.target.classList.remove('red');
}
});
};
const options = {
threshold: [1]
}
const observer = new IntersectionObserver(observerCallback, options);
observer.observe(project);
.top,
.bottom{
height: 700px;
width: 100%;
}
.project {
background: black;
height: 200px;
width: 100%;
}
.project.red {
background: red;
}
<div class="top"></div>
<div class="project"></div>
<div class="bottom"></div>
To make it 'fast' you better will have to use the >= operator than ==:
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY >= projectPosition.top && scrollY <= projectPosition.top + projectPosition.height) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
I am trying to make an infinite scroll in my chat. I'm using the scroll event to check if scrolltop < clientHeight and call a function loadMore if it is. This works pretty well as long as you never scroll to the very top. I made a gif to show this (hopefully it makes sense):
If you still have more room to scroll when the older messages get loaded, you keep your place and the scroll bar gets pushed down.
But if you are scrolled all the way to the top when the older messages get loaded, the scroll bar stays pinned to the top and you lose your place (also the scroll event stops being fired, so you stop loading messages unless you scroll down a little)
Has anyone else experienced this? And what did you do to fix it? Any advice appreciated. Thanks!
updated the answer to support 2 directions (up or down) and loading paddings. Please run the snippet in expanded mode, inline preview frame is too small for the scrollable list.
var isLoadingAlready = false;
var upDirection = true; // to load records on top of the list; false to load them to the end of the list
var loadThreshold = 100; // distance to the edge (in pixels) to start loading
var howManyDataLoadsAvailable = 5;
if (upDirection){
$('.scroll')[0].scrollTop = 100000; // scrolling all the way down
$('.scroll').css('paddingTop', loadThreshold);
} else {
$('.scroll').css('paddingBottom', loadThreshold);
}
$('.scroll').on('scroll', function () {
var s = this; // picking DOM element
if (s) { // just to be sure/safe
var scrollableHeight = s.scrollHeight - s.clientHeight;
if (scrollableHeight > 0) {
var scrollTop = s.scrollTop;
var distToTheEdge = upDirection?scrollTop:scrollableHeight - scrollTop;
if (distToTheEdge < loadThreshold && !isLoadingAlready) {
isLoadingAlready = true;
loadMoreRecords(function () { // assuming you have a callback to allow next loading
isLoadingAlready = false;
});
}
}
}
});
loadMoreRecords();
function loadMoreRecords(doneCallback){
$('.scroll').addClass('loading');
// simulating the actual loading process with setTimeout
setTimeout(function(){
// simulated items to insert:
var items = [];
if (howManyDataLoadsAvailable-- > 0){
for (var i = 0; i < 10; i++){
items.push($('<li>').text('msg: '+(i+1)+', parts left: '+howManyDataLoadsAvailable));
}
}
var $se = $('.scroll'); // scrollable DOM element
var $ul = $('.scroll ul');
var se = $se[0];
if (upDirection) {
var hBefore = $ul.height();
$ul.prepend(items);
var hDiff = $ul.height() - hBefore;
se.scrollTop = Math.max(hDiff, loadThreshold);
} else {
$ul.append(items);
se.scrollTop = se.scrollHeight - se.clientHeight - Math.max(se.scrollHeight - se.clientHeight - se.scrollTop, loadThreshold);
}
$se.removeClass('loading');
if (typeof(doneCallback) === 'function'){
doneCallback();
}
}, 500);
}
.scroll{
overflow-y: auto;
max-height: 200px;
border: 2px dashed #aaa;
padding: 0.5em;
margin: 1em;
}
.scroll.loading{
background-color: #f5f5f5;
}
ul{
list-style: none;
padding: 0;
}
li{
padding: 0.5em;
border: 1px solid #eee;
border-radius: 0.5em;
margin: 0.2em;
animation: colorchange 1200ms;
background: white;
box-shadow: 1px 1px 5px 0px rgba(0,0,0,0.05);
}
#keyframes colorchange
{
0% {background: #def;}
100% {background: white;}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="scroll">
<ul></ul>
</div>
Have worked out a solution, see the bottom!
I'm experimenting with a responsive carousel (fluid). I have elements stacked on top of each other so that the width can be fluid depending on the width of the parent. The issue is I need the parent to have overflow hidden which is not possible with children that are absolute positioned.
Tip on cleaning up the JS are appreciated too!
Does anyone have any ideas how to improve this or alternatives? Heres the fiddle: http://jsfiddle.net/j35fy/5/
.carousel-wrap {
position: relative;
}
.carousel-item {
position: absolute;
top: 0;
}
$.fn.mwCarousel = function(options) {
//Default settings.
var settings = $.extend({
changeWait: 3000,
changeSpeed: 800,
reveal: false,
slide: true,
autoRotate: true
}, options );
var CHANGE_WAIT = settings.changeWait;
var CHANGE_SPEED = settings.changeSpeed;
var REVEAL = settings.reveal;
var SLIDE = settings.slide;
var AUTO_ROTATE = settings.autoRotate;
var $carouselWrap = $(this);
var SLIDE_COUNT = $carouselWrap.find('.carousel-item').length;
var rotateTimeout;
if (AUTO_ROTATE) {
rotateTimeout = setTimeout(function(){
rotateCarousel(SLIDE_COUNT-1);
}, CHANGE_WAIT);
}
function rotateCarousel(slide) {
if (slide === 0) {
slide = SLIDE_COUNT-1;
rotateTimeout = setTimeout(function(){
$('.carousel-item').css('margin', 0);
$('.carousel-item').show();
}, CHANGE_WAIT);
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
carouselItem.show();
var itemWidth = carouselItem.width();
carouselItem.animate({margin: 0}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeIn(CHANGE_SPEED);
}
slide = slide+1;
} else {
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
var itemWidth = carouselItem.width();
carouselItem.animate({marginLeft: -itemWidth, marginRight: itemWidth}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeOut(CHANGE_SPEED);
}
}
rotateTimeout = setTimeout(function(){
rotateCarousel(slide-1);
}, CHANGE_WAIT);
}
}
$('.carousel-wrap').mwCarousel();
Solution
The first slide actually never moves (last one visible) so that one is set to position: static and all works nicely.
I think by just changing your CSS you're actually there:
.carousel-wrap {
position: relative;
overflow:hidden;
height:80%;
width:90%;
}
Demo: http://jsfiddle.net/robschmuecker/j35fy/2/
Discovered the solution is in fact simple, as the first slide in the DOM (the last you see) never actually moves itself I can set that one slide to be position: static and thus the carousel wrap will set it's height accordingly.
http://jsfiddle.net/j35fy/7/
.container {
background: aliceblue;
padding: 3em;
}
.carousel-wrap {
position: relative;
overflow:hidden;
}
.carousel-item:first-child {
position:static;
}
.carousel-item {
position: absolute;
top: 0;
width: 100%;
}
img {
width: 100%;
}