Last active
March 18, 2019 10:18
-
-
Save bwindels/b3a3ba3fa3792f23962d894866fa7b0f to your computer and use it in GitHub Desktop.
This file contains 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;<!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; | |
} | |
@keyframes width-animation { | |
from { margin-right: 0; } | |
to { margin-right: 75%; } | |
} | |
@keyframes rotate-animation { | |
from { transform: rotate(0); } | |
to { transform: rotate(360deg); } | |
} | |
li.loading { | |
background-color: white !important; | |
display: flex; | |
--spinner-bg-color: white; | |
justify-content: center; | |
} | |
.spinner { | |
background: -webkit-linear-gradient(left top, black 0%, var(--spinner-bg-color) 80%, var(--spinner-bg-color) 100%); | |
border-radius: 30px; | |
padding: 6px; | |
width: 40px; | |
height: 40px; | |
animation-timing-function: linear; | |
animation-iteration-count: infinite; | |
animation-play-state: running; | |
animation-duration: 2s; | |
animation-name: rotate-animation; | |
} | |
.spinner div { | |
height: 100%; | |
width: 100%; | |
border-radius: 30px; | |
background-color: var(--spinner-bg-color); | |
} | |
#timeline { | |
position: relative; | |
flex: 1; | |
overflow-y: auto; | |
border: 1px black solid; | |
/* | |
default is auto which keeps a node in view just like we want | |
but for now it's only supported in chrome and seems to break on | |
macOs while scrolling. So let's turn it off and do the position | |
restoring ourselves. | |
*/ | |
overflow-anchor: none; | |
} | |
#timeline.stickToBottom #tiles li:last-child { | |
background-color: green; | |
color: white; | |
} | |
#tiles { | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
justify-content: flex-end; | |
margin: 0; | |
padding: 10px; | |
list-style: none; | |
overflow-y: hidden; | |
} | |
#tiles li { | |
padding: 10px; | |
margin: 20px 0; | |
background-color: lightgrey; | |
} | |
#tiles li.pinned { | |
background-color: red; | |
color: white; | |
} | |
#settings { | |
z-index: 1; | |
position: fixed; | |
top: 10px; | |
left: 10px; | |
background-color: white; | |
border: 1px solid green; | |
padding: 10px; | |
} | |
#settings p { | |
margin: 5px; | |
} | |
.unfill { | |
position: relative; | |
background-color: lightgreen; | |
} | |
#unfill-percent { | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
width: 0%; | |
background-color: darkgreen; | |
color: white; | |
white-space: nowrap; | |
overflow: hidden; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="timeline" class="stickToBottom"> | |
<div id="settings"> | |
<p><input type="checkbox" id="append-check" autocomplete="off" checked><label id="status" for="append-check">Append nodes</label></p> | |
<p><input type="checkbox" id="loadattop-check" autocomplete="off" checked><label id="status" for="loadattop-check">Insert more when at top</label></p> | |
<p><input type="checkbox" id="resize-check" autocomplete="off" checked><label for="resize-check">Resize a node</label></p> | |
<div class="unfill"> | |
<div id="unfill-percent">bottom unfill distance</div> | |
<div class="unfill-label">bottom unfill distance</div> | |
</div> | |
</div> | |
<ol id="tiles"> | |
<li class="loading"><div class="spinner"><div></div></div></li> | |
<li>First post!</li> | |
</ol> | |
</div> | |
<script type="text/javascript"> | |
const timeline = document.getElementById("timeline"); | |
const tiles = document.getElementById("tiles"); | |
const unfillPercent = document.getElementById("unfill-percent"); | |
const loadAtTopCheck = document.getElementById("loadattop-check"); | |
const PAGE_SIZE = 200; | |
let pages = Math.ceil(timeline.clientHeight / PAGE_SIZE); | |
let bottomGrowth = 0; | |
function viewportBottom() { | |
// this is measured from top, so subtract from total height | |
return timeline.scrollHeight - (timeline.scrollTop + timeline.clientHeight); | |
} | |
function topFromBottom(node) { | |
return tiles.clientHeight - node.offsetTop; | |
} | |
function findLastNodeInViewport() { | |
const vpBottom = viewportBottom(); | |
// do binary search here | |
for (var i = tiles.children.length - 1; i >= 0; i--) { | |
const node = tiles.children[i]; | |
if ((tiles.clientHeight - node.offsetTop) > vpBottom) { | |
return node; | |
} | |
} | |
return tiles.firstElementChild.nextElementSibling; | |
} | |
tiles.style.height = `${getContainerHeight()}px`; | |
timeline.scrollTop = timeline.scrollHeight; | |
let pinnedNode = tiles.lastElementChild; | |
let pinnedTop = topFromBottom(pinnedNode); | |
let stickToBottom = true; | |
let prevScrollTop = timeline.scrollTop; | |
let scrollInProgress = null; | |
let lastScrollTimestamp = 0; | |
timeline.addEventListener("scroll", () => { | |
pinnedNode.classList.remove("pinned"); | |
const atBottom = isAtBottom(); | |
if (atBottom !== stickToBottom) { | |
if (atBottom) { | |
timeline.classList.add("stickToBottom"); | |
} else { | |
timeline.classList.remove("stickToBottom"); | |
} | |
stickToBottom = atBottom; | |
} | |
if (!atBottom) { | |
// const contentHeight = getContentHeight(); | |
// const minScrollTop = timeline.scrollHeight - contentHeight; | |
// if (timeline.scrollTop < minScrollTop) { | |
// setTimeout(() => { | |
// tiles.firstElementChild.scrollIntoView({block: "start", inline: "nearest", behaviour: "smooth"}); | |
// }, 1); | |
// timeline.scrollTop = minScrollTop; | |
// } | |
// save bottom node position | |
pinnedNode = findLastNodeInViewport(); | |
pinnedNode.classList.add("pinned"); | |
pinnedTop = topFromBottom(pinnedNode); | |
console.log("onscroll: viewportBottom()", viewportBottom(), "pinnedTop", pinnedTop); | |
// scrolling up and almost at top | |
if (loadAtTopCheck.checked && timeline.scrollTop < 250 && prevScrollTop > timeline.scrollTop) { | |
if (!alreadyUpdating) { | |
alreadyUpdating = true; | |
loadAtTop(); | |
updateHeight().then(() => { | |
alreadyUpdating = false; | |
}); | |
} | |
} | |
// // scrolling down and one screen above | |
// else if (timeline.scrollTop > (PAGE_SIZE + 100) && prevScrollTop < timeline.scrollTop) { | |
// incrementTopPage(-1); | |
// } | |
// we will need to do proper top unfilling here as well | |
checkBottomUnfillState(); | |
} | |
if (!scrollInProgress) { | |
const SCROLL_DEBOUNCE_INTERVAL = 100; | |
lastScrollTimestamp = Date.now(); | |
scrollInProgress = new Promise(resolve => { | |
function timer() { | |
const now = Date.now(); | |
const elapsed = now - lastScrollTimestamp; | |
if (elapsed >= SCROLL_DEBOUNCE_INTERVAL) { | |
resolve(); | |
scrollInProgress = null; | |
} else { | |
setTimeout(timer, SCROLL_DEBOUNCE_INTERVAL - elapsed); | |
} | |
} | |
setTimeout(timer, SCROLL_DEBOUNCE_INTERVAL); | |
}); | |
} else { | |
lastScrollTimestamp = Date.now(); | |
} | |
prevScrollTop = timeline.scrollTop; | |
}); | |
function isAtBottom() { | |
return timeline.scrollTop === timeline.scrollHeight - timeline.clientHeight; | |
} | |
function getContentHeight() { | |
const lastNode = tiles.lastElementChild; | |
// 30 is the 20px margin between items, and 10px padding of #tiles | |
return (lastNode.offsetTop + lastNode.clientHeight) - tiles.firstElementChild.offsetTop + (2 * 30); | |
} | |
function getContainerHeight() { | |
return bottomGrowth + (pages * PAGE_SIZE); | |
} | |
async function updateHeight() { | |
if (scrollInProgress) { | |
await scrollInProgress; | |
} | |
// paginate backwards | |
const currentHeight = getContainerHeight(); | |
const contentHeight = getContentHeight(); | |
const minHeight = timeline.clientHeight; | |
const height = Math.max(minHeight, contentHeight); | |
pages = Math.ceil(height / PAGE_SIZE); | |
bottomGrowth = 0; | |
const newHeight = getContainerHeight(); | |
if (stickToBottom) { | |
tiles.style.height = `${newHeight}px`; | |
timeline.scrollTop = timeline.scrollHeight; | |
console.log("updateHeight to", newHeight); | |
} else { | |
const oldTop = pinnedNode.offsetTop; | |
tiles.style.height = `${newHeight}px`; | |
const newTop = pinnedNode.offsetTop; | |
const topDiff = newTop - oldTop; | |
timeline.scrollTop = timeline.scrollTop + topDiff; | |
console.log("updateHeight to", newHeight, topDiff); | |
} | |
// console.log("setting pages to", pages, "pages at the top, with heightDiff of", heightDiff, "changed scrollTop from, to", oldScrollTop, timeline.scrollTop); | |
} | |
let alreadyUpdating = false; | |
async function restorePinnedPosition() { | |
if (stickToBottom) { | |
timeline.scrollTop = timeline.scrollHeight; | |
} else { | |
const newPinnedTop = topFromBottom(pinnedNode); | |
// did the distance to the bottom change? | |
// compensate by making #tiles higher to push | |
// things back down by the same amount | |
if (newPinnedTop !== pinnedTop) { | |
const bottomDiff = newPinnedTop - pinnedTop; | |
bottomGrowth += bottomDiff; | |
pinnedTop = newPinnedTop; | |
// console.log("restoring position: bottomDiff", bottomDiff, "bottomGrowth", bottomGrowth); | |
tiles.style.height = `${getContainerHeight()}px`; | |
} | |
} | |
// for content added above the pinnedNode | |
// we need to set scrollTop to compensate | |
// the height change. | |
if (!alreadyUpdating) { | |
const heightDiff = getContentHeight() - getContainerHeight(); | |
if (heightDiff > 0) { | |
alreadyUpdating = true; | |
await updateHeight(); | |
alreadyUpdating = false; | |
} | |
} | |
} | |
/* | |
function setHeight() { | |
const contentHeight = getContentHeight(); | |
const maxHeight = getContainerHeight(); | |
const minHeight = timeline.clientHeight; | |
const height = Math.min(Math.max(minHeight, contentHeight), maxHeight); | |
console.log("setHeight", minHeight, contentHeight, maxHeight, height); | |
tiles.style.height = `${height}px`; | |
return height; | |
} | |
*/ | |
let unfillHandle = null; | |
function checkBottomUnfillState() { | |
const UNFILL_TRESHHOLD = PAGE_SIZE * 11; | |
const UNFILL_AMOUNT = PAGE_SIZE * 10; | |
if (!unfillHandle) { | |
unfillHandle = setTimeout(() => { | |
if (pinnedTop > UNFILL_TRESHHOLD) { | |
const removedSpace = removeAtBottom(UNFILL_AMOUNT); | |
// don't break the resizing | |
resizeNode = tiles.lastElementChild; | |
console.log("unfilled ", removedSpace, " pixels at the bottom"); | |
restorePinnedPosition(); | |
} | |
const percent = Math.max(0, pinnedTop / UNFILL_TRESHHOLD); | |
unfillPercent.style.width = `${Math.ceil(percent * 100)}%`; | |
unfillHandle = null; | |
}, 1000); | |
} | |
} | |
function removeAtBottom(maxHeight) { | |
let removeFromNode = tiles.lastElementChild; | |
let prevNode = removeFromNode.previousElementSibling; | |
while(prevNode && topFromBottom(prevNode) < maxHeight && prevNode !== pinnedNode) { | |
removeFromNode = prevNode; | |
prevNode = removeFromNode.previousElementSibling; | |
} | |
const height = topFromBottom(removeFromNode); | |
while(removeFromNode) { | |
const tmp = removeFromNode.nextElementSibling; | |
removeFromNode.remove(); | |
removeFromNode = tmp; | |
} | |
return height; | |
} | |
const WORDS = ["foo", "bar", "hippo", "giraffe", "rollercoaster", "💩", "🌍"]; | |
let wordIdx = 0; | |
function createMessage() { | |
const li = document.createElement("li"); | |
++wordIdx; | |
if (wordIdx >= WORDS.length) wordIdx = 0; | |
const message = (WORDS[wordIdx]+" ").repeat(Math.ceil(Math.random() * 100)); | |
li.appendChild(document.createTextNode(message)); | |
return li; | |
} | |
function append() { | |
tiles.appendChild(createMessage()); | |
restorePinnedPosition(); | |
} | |
let appendTimer = setInterval(append, 500); | |
document.getElementById("append-check").addEventListener("change", () => { | |
if (appendTimer) { | |
clearInterval(appendTimer); | |
appendTimer = null; | |
} else { | |
appendTimer = setInterval(append, 500); | |
} | |
}); | |
async function loadAtTop() { | |
const frag = document.createDocumentFragment(); | |
for (var i = 10 - 1; i >= 0; i--) { | |
frag.appendChild(createMessage()); | |
} | |
tiles.insertBefore(frag, tiles.firstElementChild.nextElementSibling); | |
} | |
const SIZES = ["0 0 auto", "0 0 200px", "0 0 400px"]; | |
let sizeIdx = 0; | |
let resizeNode = tiles.lastElementChild; | |
function resizeLast() { | |
++sizeIdx; | |
if (sizeIdx >= SIZES.length) { | |
sizeIdx = 0; | |
} | |
resizeNode.style.flex = SIZES[sizeIdx]; | |
restorePinnedPosition(); | |
} | |
let resizeTimer = setInterval(resizeLast, 1789); | |
document.getElementById("resize-check").addEventListener("change", () => { | |
if (resizeTimer) { | |
clearInterval(resizeTimer); | |
resizeTimer = null; | |
} else { | |
resizeTimer = setInterval(resizeLast, 1789); | |
} | |
}); | |
window.addEventListener("resize", () => { | |
restorePinnedPosition(); | |
}); | |
</script> | |
</body> | |
</html> | |
flex-direction: column; | |
height: 100vh; | |
box-sizing: border-box; | |
} | |
@keyframes width-animation { | |
from { margin-right: 0; } | |
to { margin-right: 75%; } | |
} | |
@keyframes rotate-animation { | |
from { transform: rotate(0); } | |
to { transform: rotate(360deg); } | |
} | |
li.loading { | |
background-color: white !important; | |
display: flex; | |
--spinner-bg-color: white; | |
justify-content: center; | |
} | |
.spinner { | |
background: -webkit-linear-gradient(left top, black 0%, var(--spinner-bg-color) 80%, var(--spinner-bg-color) 100%); | |
border-radius: 30px; | |
padding: 6px; | |
width: 40px; | |
height: 40px; | |
animation-timing-function: linear; | |
animation-iteration-count: infinite; | |
animation-play-state: running; | |
animation-duration: 2s; | |
animation-name: rotate-animation; | |
} | |
.spinner div { | |
height: 100%; | |
width: 100%; | |
border-radius: 30px; | |
background-color: var(--spinner-bg-color); | |
} | |
#timeline { | |
position: relative; | |
flex: 1; | |
overflow-y: auto; | |
border: 1px black solid; | |
/* | |
default is auto which keeps a node in view just like we want | |
but for now it's only supported in chrome and seems to break on | |
macOs while scrolling. So let's turn it off and do the position | |
restoring ourselves. | |
*/ | |
overflow-anchor: none; | |
} | |
#timeline.stickToBottom #tiles li:last-child { | |
background-color: green; | |
color: white; | |
} | |
#tiles { | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
justify-content: flex-end; | |
margin: 0; | |
padding: 10px; | |
list-style: none; | |
overflow-y: hidden; | |
} | |
#tiles li { | |
padding: 10px; | |
margin: 20px 0; | |
background-color: lightgrey; | |
} | |
#tiles li.pinned { | |
background-color: red; | |
color: white; | |
} | |
#settings { | |
z-index: 1; | |
position: fixed; | |
top: 10px; | |
left: 10px; | |
background-color: white; | |
border: 1px solid green; | |
padding: 10px; | |
} | |
#settings p { | |
margin: 5px; | |
} | |
.unfill { | |
position: relative; | |
background-color: lightgreen; | |
} | |
#unfill-percent { | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
width: 0%; | |
background-color: darkgreen; | |
color: white; | |
white-space: nowrap; | |
overflow: hidden; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="timeline" class="stickToBottom"> | |
<div id="settings"> | |
<p><input type="checkbox" id="append-check" autocomplete="off" checked><label id="status" for="append-check">Append nodes</label></p> | |
<p><input type="checkbox" id="loadattop-check" autocomplete="off" checked><label id="status" for="loadattop-check">Insert more when at top</label></p> | |
<p><input type="checkbox" id="resize-check" autocomplete="off" checked><label for="resize-check">Resize a node</label></p> | |
<div class="unfill"> | |
<div id="unfill-percent">bottom unfill distance</div> | |
<div class="unfill-label">bottom unfill distance</div> | |
</div> | |
</div> | |
<ol id="tiles"> | |
<li class="loading"><div class="spinner"><div></div></div></li> | |
<li>First post!</li> | |
</ol> | |
</div> | |
<script type="text/javascript"> | |
const timeline = document.getElementById("timeline"); | |
const tiles = document.getElementById("tiles"); | |
const unfillPercent = document.getElementById("unfill-percent"); | |
const loadAtTopCheck = document.getElementById("loadattop-check"); | |
const PAGE_SIZE = 200; | |
let pages = Math.ceil(timeline.clientHeight / PAGE_SIZE); | |
let bottomGrowth = 0; | |
function viewportBottom() { | |
// this is measured from top, so subtract from total height | |
return timeline.scrollHeight - (timeline.scrollTop + timeline.clientHeight); | |
} | |
function topFromBottom(node) { | |
return tiles.clientHeight - node.offsetTop; | |
} | |
function findLastNodeInViewport() { | |
const vpBottom = viewportBottom(); | |
// do binary search here | |
for (var i = tiles.children.length - 1; i >= 0; i--) { | |
const node = tiles.children[i]; | |
if ((tiles.clientHeight - node.offsetTop) > vpBottom) { | |
return node; | |
} | |
} | |
return tiles.firstElementChild.nextElementSibling; | |
} | |
tiles.style.height = `${getContainerHeight()}px`; | |
timeline.scrollTop = timeline.scrollHeight; | |
let pinnedNode = tiles.lastElementChild; | |
let pinnedTop = topFromBottom(pinnedNode); | |
let stickToBottom = true; | |
let prevScrollTop = timeline.scrollTop; | |
let scrollInProgress = null; | |
let lastScrollTimestamp = 0; | |
timeline.addEventListener("scroll", () => { | |
pinnedNode.classList.remove("pinned"); | |
const atBottom = isAtBottom(); | |
if (atBottom !== stickToBottom) { | |
if (atBottom) { | |
timeline.classList.add("stickToBottom"); | |
} else { | |
timeline.classList.remove("stickToBottom"); | |
} | |
stickToBottom = atBottom; | |
} | |
if (!atBottom) { | |
// const contentHeight = getContentHeight(); | |
// const minScrollTop = timeline.scrollHeight - contentHeight; | |
// if (timeline.scrollTop < minScrollTop) { | |
// setTimeout(() => { | |
// tiles.firstElementChild.scrollIntoView({block: "start", inline: "nearest", behaviour: "smooth"}); | |
// }, 1); | |
// timeline.scrollTop = minScrollTop; | |
// } | |
// save bottom node position | |
pinnedNode = findLastNodeInViewport(); | |
pinnedNode.classList.add("pinned"); | |
pinnedTop = topFromBottom(pinnedNode); | |
console.log("onscroll: viewportBottom()", viewportBottom(), "pinnedTop", pinnedTop); | |
// scrolling up and almost at top | |
if (loadAtTopCheck.checked && timeline.scrollTop < 250 && prevScrollTop > timeline.scrollTop) { | |
if (!alreadyUpdating) { | |
alreadyUpdating = true; | |
loadAtTop(); | |
updateHeight().then(() => { | |
alreadyUpdating = false; | |
}); | |
} | |
} | |
// // scrolling down and one screen above | |
// else if (timeline.scrollTop > (PAGE_SIZE + 100) && prevScrollTop < timeline.scrollTop) { | |
// incrementTopPage(-1); | |
// } | |
// we will need to do proper top unfilling here as well | |
checkBottomUnfillState(); | |
} | |
if (!scrollInProgress) { | |
const SCROLL_DEBOUNCE_INTERVAL = 100; | |
lastScrollTimestamp = Date.now(); | |
scrollInProgress = new Promise(resolve => { | |
function timer() { | |
const now = Date.now(); | |
const elapsed = now - lastScrollTimestamp; | |
if (elapsed >= SCROLL_DEBOUNCE_INTERVAL) { | |
resolve(); | |
scrollInProgress = null; | |
} else { | |
setTimeout(timer, SCROLL_DEBOUNCE_INTERVAL - elapsed); | |
} | |
} | |
setTimeout(timer, SCROLL_DEBOUNCE_INTERVAL); | |
}); | |
} else { | |
lastScrollTimestamp = Date.now(); | |
} | |
prevScrollTop = timeline.scrollTop; | |
}); | |
function isAtBottom() { | |
return timeline.scrollTop === timeline.scrollHeight - timeline.clientHeight; | |
} | |
function getContentHeight() { | |
const lastNode = tiles.lastElementChild; | |
// 30 is the 20px margin between items, and 10px padding of #tiles | |
return (lastNode.offsetTop + lastNode.clientHeight) - tiles.firstElementChild.offsetTop + (2 * 30); | |
} | |
function getContainerHeight() { | |
return bottomGrowth + (pages * PAGE_SIZE); | |
} | |
async function updateHeight() { | |
if (scrollInProgress) { | |
await scrollInProgress; | |
} | |
// paginate backwards | |
const currentHeight = getContainerHeight(); | |
const contentHeight = getContentHeight(); | |
const minHeight = timeline.clientHeight; | |
const height = Math.max(minHeight, contentHeight); | |
pages = Math.ceil(height / PAGE_SIZE); | |
bottomGrowth = 0; | |
const newHeight = getContainerHeight(); | |
if (stickToBottom) { | |
tiles.style.height = `${newHeight}px`; | |
timeline.scrollTop = timeline.scrollHeight; | |
console.log("updateHeight to", newHeight); | |
} else { | |
const oldTop = pinnedNode.offsetTop; | |
tiles.style.height = `${newHeight}px`; | |
const newTop = pinnedNode.offsetTop; | |
const topDiff = newTop - oldTop; | |
timeline.scrollTop = timeline.scrollTop + topDiff; | |
console.log("updateHeight to", newHeight, topDiff); | |
} | |
// console.log("setting pages to", pages, "pages at the top, with heightDiff of", heightDiff, "changed scrollTop from, to", oldScrollTop, timeline.scrollTop); | |
} | |
let alreadyUpdating = false; | |
async function restorePinnedPosition() { | |
if (stickToBottom) { | |
timeline.scrollTop = timeline.scrollHeight; | |
} else { | |
const newPinnedTop = topFromBottom(pinnedNode); | |
// did the distance to the bottom change? | |
// compensate by making #tiles higher to push | |
// things back down by the same amount | |
if (newPinnedTop !== pinnedTop) { | |
const bottomDiff = newPinnedTop - pinnedTop; | |
bottomGrowth += bottomDiff; | |
pinnedTop = newPinnedTop; | |
// console.log("restoring position: bottomDiff", bottomDiff, "bottomGrowth", bottomGrowth); | |
tiles.style.height = `${getContainerHeight()}px`; | |
} | |
} | |
// for content added above the pinnedNode | |
// we need to set scrollTop to compensate | |
// the height change. | |
if (!alreadyUpdating) { | |
const heightDiff = getContentHeight() - getContainerHeight(); | |
if (heightDiff > 0) { | |
alreadyUpdating = true; | |
await updateHeight(); | |
alreadyUpdating = false; | |
} | |
} | |
} | |
/* | |
function setHeight() { | |
const contentHeight = getContentHeight(); | |
const maxHeight = getContainerHeight(); | |
const minHeight = timeline.clientHeight; | |
const height = Math.min(Math.max(minHeight, contentHeight), maxHeight); | |
console.log("setHeight", minHeight, contentHeight, maxHeight, height); | |
tiles.style.height = `${height}px`; | |
return height; | |
} | |
*/ | |
let unfillHandle = null; | |
function checkBottomUnfillState() { | |
const UNFILL_TRESHHOLD = PAGE_SIZE * 11; | |
const UNFILL_AMOUNT = PAGE_SIZE * 10; | |
if (!unfillHandle) { | |
unfillHandle = setTimeout(() => { | |
if (pinnedTop > UNFILL_TRESHHOLD) { | |
const removedSpace = removeAtBottom(UNFILL_AMOUNT); | |
// don't break the resizing | |
resizeNode = tiles.lastElementChild; | |
console.log("unfilled ", removedSpace, " pixels at the bottom"); | |
restorePinnedPosition(); | |
} | |
const percent = Math.max(0, pinnedTop / UNFILL_TRESHHOLD); | |
unfillPercent.style.width = `${Math.ceil(percent * 100)}%`; | |
unfillHandle = null; | |
}, 1000); | |
} | |
} | |
function removeAtBottom(maxHeight) { | |
let removeFromNode = tiles.lastElementChild; | |
let prevNode = removeFromNode.previousElementSibling; | |
while(prevNode && topFromBottom(prevNode) < maxHeight && prevNode !== pinnedNode) { | |
removeFromNode = prevNode; | |
prevNode = removeFromNode.previousElementSibling; | |
} | |
const height = topFromBottom(removeFromNode); | |
while(removeFromNode) { | |
const tmp = removeFromNode.nextElementSibling; | |
removeFromNode.remove(); | |
removeFromNode = tmp; | |
} | |
return height; | |
} | |
const WORDS = ["foo", "bar", "hippo", "giraffe", "rollercoaster", "💩", "🌍"]; | |
let wordIdx = 0; | |
function createMessage() { | |
const li = document.createElement("li"); | |
++wordIdx; | |
if (wordIdx >= WORDS.length) wordIdx = 0; | |
const message = (WORDS[wordIdx]+" ").repeat(Math.ceil(Math.random() * 100)); | |
li.appendChild(document.createTextNode(message)); | |
return li; | |
} | |
function append() { | |
tiles.appendChild(createMessage()); | |
restorePinnedPosition(); | |
} | |
let appendTimer = setInterval(append, 500); | |
document.getElementById("append-check").addEventListener("change", () => { | |
if (appendTimer) { | |
clearInterval(appendTimer); | |
appendTimer = null; | |
} else { | |
appendTimer = setInterval(append, 500); | |
} | |
}); | |
async function loadAtTop() { | |
const frag = document.createDocumentFragment(); | |
for (var i = 10 - 1; i >= 0; i--) { | |
frag.appendChild(createMessage()); | |
} | |
tiles.insertBefore(frag, tiles.firstElementChild.nextElementSibling); | |
} | |
const SIZES = ["0 0 auto", "0 0 200px", "0 0 400px"]; | |
let sizeIdx = 0; | |
let resizeNode = tiles.lastElementChild; | |
function resizeLast() { | |
++sizeIdx; | |
if (sizeIdx >= SIZES.length) { | |
sizeIdx = 0; | |
} | |
resizeNode.style.flex = SIZES[sizeIdx]; | |
restorePinnedPosition(); | |
} | |
let resizeTimer = setInterval(resizeLast, 1789); | |
document.getElementById("resize-check").addEventListener("change", () => { | |
if (resizeTimer) { | |
clearInterval(resizeTimer); | |
resizeTimer = null; | |
} else { | |
resizeTimer = setInterval(resizeLast, 1789); | |
} | |
}); | |
window.addEventListener("resize", () => { | |
restorePinnedPosition(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment