Last active
February 20, 2019 16:05
-
-
Save bwindels/4aa3a0495264498cca67b8563b580889 to your computer and use it in GitHub Desktop.
align last event tile to bottom of viewport
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<style type="text/css"> | |
body { | |
padding: 10px; | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
height: 100vh; | |
box-sizing: border-box; | |
} | |
#timeline { | |
position: relative; | |
flex: 1; | |
overflow-y: auto; | |
border: 1px black solid; | |
} | |
#tiles { | |
margin: 0; | |
padding: 10px; | |
list-style: none; | |
} | |
#tiles li { | |
padding: 10px; | |
margin: 20px 0; | |
animation-iteration-count: infinite; | |
animation-direction: alternate; | |
animation-play-state: paused; | |
animation-play-state: running; | |
animation-timing-function: steps(2, end); | |
} | |
@keyframes height-animation-1 { | |
from { height: 400px; } | |
to { height: 100px; } | |
} | |
@keyframes height-animation-2 { | |
from { height: 200px; } | |
to { height: 50px; } | |
} | |
@keyframes height-animation-4 { | |
from { height: 300px; } | |
to { height: 200px; } | |
} | |
li.category1 { | |
background-color: red; | |
color: white; | |
animation-duration: 7s; | |
animation-name: height-animation-1; | |
} | |
li.category2 { | |
background-color: blue; | |
color: white; | |
animation-duration: 5s; | |
animation-name: height-animation-2; | |
} | |
li.category3 { | |
background-color: purple; | |
color: white; | |
height: 200px; | |
} | |
li.category4 { | |
background-color: green; | |
color: white; | |
animation-duration: 11s; | |
animation-name: height-animation-4; | |
} | |
</style> | |
</head> | |
<body> | |
<p> | |
This prototype emulates a timeline where new event tiles come in continuously at the end, and individual tiles resize constantly (like they do in Riot when an event is redacted, a preview is loaded, ... emulated here with looping CSS animations). The purple tiles stay the same height though. The prototype tries to align the most bottom visible tile to the bottom of the viewport. Try scrolling so the bottom most tile in the viewport is a purple one. Even though it's predecesing siblings are changing size, it should not move on the screen as the scrollTop is modified constantly to compensate. Also, if you scroll to the bottom of the last tile, it will stick to the bottom as new tiles come in, as you would expect from a chat application.</p> | |
</p> | |
<div id="timeline"> | |
<ol id="tiles"></ol> | |
</div> | |
<!-- | |
https://github.com/WICG/ResizeObserver/issues/3 | |
polyfill seems to behave a lot poorer than requestAnimationFrame polling, | |
but enable it here, get ResizeObserver.js from | |
https://raw.githubusercontent.com/que-etc/resize-observer-polyfill/master/dist/ResizeObserver.js | |
--> | |
<!-- <script type="text/javascript" src="ResizeObserver.js"></script> --> | |
<script type="text/javascript"> | |
const timelineNode = document.getElementById("timeline"); | |
const tilesNode = document.getElementById("tiles"); | |
function insertNewTile() { | |
const tile = document.createElement("li"); | |
tile.innerText = `Event tile created at ${new Date().toString()}`; | |
tile.className = `category${Math.ceil(Math.random() * 4)}`; | |
tilesNode.appendChild(tile); | |
} | |
for(let i = 0; i < 10; ++i) { | |
insertNewTile(); | |
} | |
async function appendTiles() { | |
for(let i = 0; i < 5000; ++i) { | |
const timeout = 1000 + Math.random() * 5000; | |
await new Promise(resolve => setTimeout(resolve, timeout)); | |
insertNewTile(); | |
} | |
} | |
function findLastNodeInView() { | |
// TODO: use binary search here | |
const top = timelineNode.scrollTop; | |
const bottom = top + timelineNode.clientHeight; | |
let node; | |
for (let n of tilesNode.children) { | |
if (n.offsetTop > bottom) { | |
break; | |
} else { | |
node = n; | |
} | |
} | |
const nodeBottom = node.offsetTop + node.clientHeight; | |
const bottomDiff = nodeBottom - bottom; | |
const isLast = node === tilesNode.lastElementChild; | |
const stickToBottom = isLast && bottomDiff <= 0; | |
return {node, bottomDiff, stickToBottom}; | |
} | |
let ignoreNextScrollEvent = false; | |
let lastInViewport = findLastNodeInView(); | |
timelineNode.addEventListener("scroll", evt => { | |
if (!ignoreNextScrollEvent) { | |
lastInViewport = findLastNodeInView(); | |
} | |
ignoreNextScrollEvent = false; | |
// console.log("scroll", evt); | |
// evt.preventDefault(); | |
}); | |
if (typeof ResizeObserver !== "undefined") { | |
const ro = new ResizeObserver(entries => { | |
repositionLastNode(); | |
}); | |
ro.observe(tilesNode); | |
} else { | |
// fall back to polling | |
let cachedHeight = tilesNode.clientHeight; | |
const frameCallback = () => { | |
const newHeight = tilesNode.clientHeight; | |
if (newHeight !== cachedHeight) { | |
cachedHeight = newHeight; | |
repositionLastNode(); | |
} | |
window.requestAnimationFrame(frameCallback); | |
}; | |
window.requestAnimationFrame(frameCallback); | |
} | |
function repositionLastNode() { | |
// console.log("resizing", lastInViewport); | |
// setting scrollTop triggers a scroll event, which messes up our logic | |
// it should only trigger one event though, so just ignore the next one | |
ignoreNextScrollEvent = true; | |
if (lastInViewport.stickToBottom) { | |
timelineNode.scrollTop = timelineNode.scrollHeight; | |
} else { | |
const node = lastInViewport.node; | |
const nodeBottom = node.offsetTop + node.clientHeight; | |
timelineNode.scrollTop = (nodeBottom - lastInViewport.bottomDiff) - timelineNode.clientHeight; | |
} | |
} | |
appendTiles(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment