forked from cybrespace/pinafore
use IntersectionObserver for virtual scroll
This commit is contained in:
parent
94cab7aaf7
commit
5e3e56d454
|
@ -3344,6 +3344,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
|
||||||
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
|
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
|
||||||
},
|
},
|
||||||
|
"intersection-observer": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-8Zgt4ijlyvIrQVTA7MPb2W9+KhoetrAbxlh0RmTGxpx0+ZsAXvy7IsbNnZIrqZ6TddAdWeQj49x7Ph7Ir6KRkA=="
|
||||||
|
},
|
||||||
"intl-messageformat": {
|
"intl-messageformat": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-2.2.0.tgz",
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"font-awesome-svg-png": "^1.2.2",
|
"font-awesome-svg-png": "^1.2.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"idb": "^2.0.4",
|
"idb": "^2.0.4",
|
||||||
|
"intersection-observer": "^0.5.0",
|
||||||
"intl-relativeformat": "^2.1.0",
|
"intl-relativeformat": "^2.1.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
".............. status-toolbar";
|
".............. status-toolbar";
|
||||||
grid-template-columns: 58px 1fr;
|
grid-template-columns: 58px 1fr;
|
||||||
border-bottom: 1px solid var(--main-border);
|
border-bottom: 1px solid var(--main-border);
|
||||||
will-change: transform; /* TODO: is this necessary? */
|
/* will-change: transform; */ /* TODO: is this necessary? */
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.status-sidebar) {
|
:global(.status-sidebar) {
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
<VirtualList component="{{StatusListItem}}" items="{{statuses}}" />
|
<VirtualList component="{{StatusListItem}}" items="{{statuses}}" />
|
||||||
<button type="button" on:click="addMoreItems()">Add more items</button>
|
<button type="button" on:click="addMoreItems()">Add more items</button>
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
.timeline {
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../_utils/store'
|
import { store } from '../_utils/store'
|
||||||
import { getHomeTimeline } from '../_utils/mastodon/oauth'
|
import { getHomeTimeline } from '../_utils/mastodon/oauth'
|
||||||
|
|
|
@ -42,8 +42,6 @@
|
||||||
})
|
})
|
||||||
}, THROTTLE_TIME))
|
}, THROTTLE_TIME))
|
||||||
},
|
},
|
||||||
ondestroy () {
|
|
||||||
},
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
component: null
|
component: null
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="virtual-list-item"
|
<div class="virtual-list-item {{shown ? 'shown' : ''}}"
|
||||||
|
virtual-list-key="{{key}}"
|
||||||
ref:node
|
ref:node
|
||||||
style="transform: translate3d(0, {{offset}}px, 0);"
|
style="transform: translate3d(0, {{offset}}px, 0);"
|
||||||
>
|
>
|
||||||
|
@ -8,6 +9,12 @@
|
||||||
.virtual-list-item {
|
.virtual-list-item {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.shown {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
@ -15,28 +22,45 @@
|
||||||
|
|
||||||
let updateItemHeights = {}
|
let updateItemHeights = {}
|
||||||
let promise = Promise.resolve()
|
let promise = Promise.resolve()
|
||||||
|
let onIntersectionCallbacks = {}
|
||||||
|
|
||||||
|
let intersectionObserver = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
let key = entry.target.getAttribute('virtual-list-key')
|
||||||
|
onIntersectionCallbacks[key](entry)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate() {
|
oncreate() {
|
||||||
let key = this.get('key')
|
let key = this.get('key')
|
||||||
updateItemHeights[key] = this.refs.node.offsetHeight
|
onIntersectionCallbacks[key] = entry => {
|
||||||
promise.then(() => {
|
updateItemHeights[key] = entry.boundingClientRect.height
|
||||||
// update all item heights in one microtask batch for better perf
|
promise.then(() => {
|
||||||
let updatedKeys = Object.keys(updateItemHeights)
|
// update all item heights in one microtask batch for better perf
|
||||||
if (!updatedKeys.length) {
|
let updatedKeys = Object.keys(updateItemHeights)
|
||||||
return
|
if (!updatedKeys.length) {
|
||||||
}
|
return
|
||||||
// batch all updates to itemHeights for better perf
|
}
|
||||||
let itemHeights = this.store.get('itemHeights')
|
// batch all updates to itemHeights for better perf
|
||||||
for (key of updatedKeys) {
|
let itemHeights = this.store.get('itemHeights')
|
||||||
itemHeights[key] = updateItemHeights[key]
|
for (key of updatedKeys) {
|
||||||
}
|
itemHeights[key] = updateItemHeights[key]
|
||||||
this.store.set({
|
}
|
||||||
itemHeights: itemHeights
|
this.store.set({
|
||||||
|
itemHeights: itemHeights
|
||||||
|
})
|
||||||
|
updateItemHeights = {}
|
||||||
})
|
})
|
||||||
updateItemHeights = {}
|
}
|
||||||
})
|
intersectionObserver.observe(this.refs.node)
|
||||||
},
|
},
|
||||||
store: () => virtualListStore
|
ondestroy() {
|
||||||
|
intersectionObserver.unobserve(this.refs.node)
|
||||||
|
},
|
||||||
|
store: () => virtualListStore,
|
||||||
|
computed: {
|
||||||
|
'shown': ($itemHeights, key) => $itemHeights[key] > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -13,7 +13,12 @@ const importTimeline = () => import(
|
||||||
/* webpackChunkName: 'Timeline' */ '../_components/Timeline.html'
|
/* webpackChunkName: 'Timeline' */ '../_components/Timeline.html'
|
||||||
).then(mod => mod.default)
|
).then(mod => mod.default)
|
||||||
|
|
||||||
|
const importIntersectionObserver = () => import(
|
||||||
|
/* webpackChunkname: 'intersection-observer' */ 'intersection-observer'
|
||||||
|
)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
importURLSearchParams,
|
importURLSearchParams,
|
||||||
importTimeline
|
importTimeline,
|
||||||
|
importIntersectionObserver
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { init } from 'sapper/runtime.js'
|
import { init } from 'sapper/runtime.js'
|
||||||
import { importURLSearchParams } from '../routes/_utils/asyncModules'
|
import { importURLSearchParams } from '../routes/_utils/asyncModules'
|
||||||
|
import { importIntersectionObserver } from '../routes/_utils/asyncModules'
|
||||||
|
|
||||||
// polyfills
|
// polyfills
|
||||||
Promise.all([
|
Promise.all([
|
||||||
typeof URLSearchParams === 'undefined' && importURLSearchParams()
|
typeof URLSearchParams === 'undefined' && importURLSearchParams(),
|
||||||
|
typeof IntersectionObserver === 'undefined' && importIntersectionObserver()
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
// `routes` is an array of route objects injected by Sapper
|
// `routes` is an array of route objects injected by Sapper
|
||||||
init(document.querySelector('#sapper'), __routes__)
|
init(document.querySelector('#sapper'), __routes__)
|
||||||
|
|
Loading…
Reference in New Issue