use IntersectionObserver for virtual scroll

This commit is contained in:
Nolan Lawson 2018-01-16 20:34:09 -08:00
parent 94cab7aaf7
commit 5e3e56d454
8 changed files with 63 additions and 23 deletions

5
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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) {

View file

@ -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'

View file

@ -42,8 +42,6 @@
}) })
}, THROTTLE_TIME)) }, THROTTLE_TIME))
}, },
ondestroy () {
},
data: () => ({ data: () => ({
component: null component: null
}), }),

View file

@ -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,11 +22,20 @@
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 => {
updateItemHeights[key] = entry.boundingClientRect.height
promise.then(() => { promise.then(() => {
// update all item heights in one microtask batch for better perf // update all item heights in one microtask batch for better perf
let updatedKeys = Object.keys(updateItemHeights) let updatedKeys = Object.keys(updateItemHeights)
@ -36,7 +52,15 @@
}) })
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>

View file

@ -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
} }

View file

@ -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__)