Commit e108357490d8ff17a62b052f173b16344531ecbc
rewrite infinity scroll to use idle rendering with animation frame append loop
Matt McKegg committed on 6/28/2017, 6:03:42 AMParent: d14a37cca1e80d539fdba2a00bed84a118e0892f
Files changed
lib/pull-scroll.js | deleted |
lib/scroller.js | added |
modules/feed/html/rollup.js | changed |
modules/page/html/render/search.js | changed |
package.json | changed |
lib/pull-scroll.js | ||
---|---|---|
@@ -1,119 +1,0 @@ | ||
1 | -// forked because of weird non-filling and over-resuming problems | |
2 | -// should really PR this, but Dominic might not accept it :D | |
3 | - | |
4 | -var pull = require('pull-stream') | |
5 | -var Pause = require('pull-pause') | |
6 | -var Value = require('mutant/value') | |
7 | - | |
8 | -var next = 'undefined' === typeof setImmediate ? setTimeout : setImmediate | |
9 | -var buffer = Math.max(window.innerHeight * 2, 1000) | |
10 | - | |
11 | -var u = require('pull-scroll/utils'), | |
12 | - assertScrollable = u.assertScrollable, | |
13 | - isEnd = u.isEnd, | |
14 | - isVisible = u.isVisible | |
15 | - | |
16 | -module.exports = Scroller | |
17 | - | |
18 | - | |
19 | -function Scroller(scroller, content, render, isPrepend, isSticky, cb) { | |
20 | - assertScrollable(scroller) | |
21 | - var obs = Value(0) | |
22 | - | |
23 | - //if second argument is a function, | |
24 | - //it means the scroller and content elements are the same. | |
25 | - if('function' === typeof content) { | |
26 | - cb = isSticky | |
27 | - isPrepend = render | |
28 | - render = content | |
29 | - content = scroller | |
30 | - } | |
31 | - | |
32 | - if(!cb) cb = function (err) { if(err) throw err } | |
33 | - | |
34 | - scroller.addEventListener('scroll', scroll) | |
35 | - var pause = Pause(function () {}) | |
36 | - var queue = [] | |
37 | - | |
38 | - //apply some changes to the dom, but ensure that | |
39 | - //`element` is at the same place on screen afterwards. | |
40 | - | |
41 | - function add () { | |
42 | - if(queue.length) { | |
43 | - var m = queue.shift() | |
44 | - var r = render(m) | |
45 | - append(scroller, content, r, isPrepend, isSticky) | |
46 | - obs.set(queue.length) | |
47 | - } | |
48 | - } | |
49 | - | |
50 | - function scroll (ev) { | |
51 | - if (isEnd(scroller, buffer, isPrepend)) { | |
52 | - add() | |
53 | - pause.resume() | |
54 | - } | |
55 | - } | |
56 | - | |
57 | - pause.pause() | |
58 | - | |
59 | - //wait until the scroller has been added to the document | |
60 | - next(function next () { | |
61 | - if(scroller.parentElement) pause.resume() | |
62 | - else setTimeout(next, 100) | |
63 | - }) | |
64 | - | |
65 | - var stream = pull( | |
66 | - pause, | |
67 | - pull.drain(function (e) { | |
68 | - queue.push(e) | |
69 | - obs.set(queue.length) | |
70 | - | |
71 | - if(content.clientHeight < window.innerHeight) | |
72 | - add() | |
73 | - | |
74 | - if (isVisible(content)) { | |
75 | - if (isEnd(scroller, buffer, isPrepend)) | |
76 | - add() | |
77 | - } | |
78 | - | |
79 | - if(queue.length > 5) { | |
80 | - pause.pause() | |
81 | - } | |
82 | - | |
83 | - }, function (err) { | |
84 | - if(err) console.error(err) | |
85 | - cb ? cb(err) : console.error(err) | |
86 | - }) | |
87 | - ) | |
88 | - | |
89 | - stream.visible = add | |
90 | - stream.queue = obs | |
91 | - return stream | |
92 | -} | |
93 | - | |
94 | - | |
95 | -function append(scroller, list, el, isPrepend, isSticky) { | |
96 | - if(!el) return | |
97 | - var s = scroller.scrollHeight | |
98 | - var st = scroller.scrollTop | |
99 | - if(isPrepend && list.firstChild) | |
100 | - list.insertBefore(el, list.firstChild) | |
101 | - else | |
102 | - list.appendChild(el) | |
103 | - | |
104 | - //scroll down by the height of the thing added. | |
105 | - //if it added to the top (in non-sticky mode) | |
106 | - //or added it to the bottom (in sticky mode) | |
107 | - if(isPrepend !== isSticky) { | |
108 | - var d = (scroller.scrollHeight - s) | |
109 | - var before = scroller.scrollTop | |
110 | - //check whether the browser has moved the scrollTop for us. | |
111 | - //if you add an element that is not scrolled into view | |
112 | - //it no longer bumps the view down! but this check is still needed | |
113 | - //for firefox. | |
114 | - //this seems to be the behavior in recent chrome (also electron) | |
115 | - if(st === scroller.scrollTop) { | |
116 | - scroller.scrollTop = scroller.scrollTop + d | |
117 | - } | |
118 | - } | |
119 | -} |
lib/scroller.js | ||
---|---|---|
@@ -1,0 +1,60 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var Pause = require('pull-pause') | |
3 | +var Value = require('mutant/value') | |
4 | +var onceIdle = require('mutant/once-idle') | |
5 | +var computed = require('mutant/computed') | |
6 | + | |
7 | +module.exports = Scroller | |
8 | + | |
9 | +function Scroller (scroller, content, render, cb) { | |
10 | + var toRenderCount = Value(0) | |
11 | + var toAppendCount = Value(0) | |
12 | + | |
13 | + var queueLength = computed([toRenderCount, toAppendCount], (a, b) => a + b) | |
14 | + | |
15 | + var pause = Pause(function () {}) | |
16 | + var running = true | |
17 | + var appendQueue = [] | |
18 | + | |
19 | + function appendLoop () { | |
20 | + var distanceFromBottom = scroller.scrollHeight - (scroller.scrollTop + scroller.clientHeight) | |
21 | + while (appendQueue.length && distanceFromBottom < scroller.clientHeight) { | |
22 | + content.appendChild(appendQueue.shift()) | |
23 | + } | |
24 | + | |
25 | + toAppendCount.set(appendQueue.length) | |
26 | + if (queueLength() < 5) { | |
27 | + // queue running low, resume stream | |
28 | + pause.resume() | |
29 | + } | |
30 | + | |
31 | + if (running || queueLength()) { | |
32 | + window.requestAnimationFrame(appendLoop) | |
33 | + } | |
34 | + } | |
35 | + | |
36 | + var stream = pull( | |
37 | + pause, | |
38 | + pull.drain(function (msg) { | |
39 | + toRenderCount.set(toRenderCount() + 1) | |
40 | + | |
41 | + onceIdle(() => { | |
42 | + var element = render(msg) | |
43 | + appendQueue.push(element) | |
44 | + toRenderCount.set(toRenderCount() - 1) | |
45 | + }) | |
46 | + | |
47 | + if (queueLength() > 5) { | |
48 | + pause.pause() | |
49 | + } | |
50 | + }, function (err) { | |
51 | + running = false | |
52 | + cb ? cb(err) : console.error(err) | |
53 | + }) | |
54 | + ) | |
55 | + | |
56 | + stream.queue = queueLength | |
57 | + | |
58 | + appendLoop() | |
59 | + return stream | |
60 | +} |
modules/feed/html/rollup.js | ||
---|---|---|
@@ -1,9 +1,9 @@ | ||
1 | 1 | var nest = require('depnest') |
2 | 2 | var {Value, Proxy, Array: MutantArray, h, computed, map, when, onceTrue, throttle} = require('mutant') |
3 | 3 | var pull = require('pull-stream') |
4 | 4 | var Abortable = require('pull-abortable') |
5 | -var Scroller = require('../../../lib/pull-scroll') | |
5 | +var Scroller = require('../../../lib/scroller') | |
6 | 6 | var nextStepper = require('../../../lib/next-stepper') |
7 | 7 | var extend = require('xtend') |
8 | 8 | var paramap = require('pull-paramap') |
9 | 9 | |
@@ -56,9 +56,21 @@ | ||
56 | 56 | var newSinceRefresh = new Set() |
57 | 57 | var highlightItems = new Set() |
58 | 58 | |
59 | 59 | var container = h('Scroller', { |
60 | - style: { overflow: 'auto' } | |
60 | + style: { overflow: 'auto' }, | |
61 | + hooks: [(element) => { | |
62 | + // don't activate until added to DOM | |
63 | + refresh() | |
64 | + | |
65 | + // deactivate when removed from DOM | |
66 | + return () => { | |
67 | + if (abortLastFeed) { | |
68 | + abortLastFeed() | |
69 | + abortLastFeed = null | |
70 | + } | |
71 | + } | |
72 | + }] | |
61 | 73 | }, [ |
62 | 74 | h('div.wrapper', [ |
63 | 75 | h('section.prepend', prepend), |
64 | 76 | content, |
@@ -66,10 +78,8 @@ | ||
66 | 78 | ]) |
67 | 79 | ]) |
68 | 80 | |
69 | 81 | onceTrue(waitFor, () => { |
70 | - refresh() | |
71 | - | |
72 | 82 | // display pending updates |
73 | 83 | pull( |
74 | 84 | updateStream || pull( |
75 | 85 | getStream({old: false}), |
@@ -104,34 +114,36 @@ | ||
104 | 114 | |
105 | 115 | return result |
106 | 116 | |
107 | 117 | function refresh () { |
108 | - if (abortLastFeed) abortLastFeed() | |
109 | - updates.set(0) | |
110 | - content.set(h('section.content')) | |
118 | + onceTrue(waitFor, () => { | |
119 | + if (abortLastFeed) abortLastFeed() | |
120 | + updates.set(0) | |
121 | + content.set(h('section.content')) | |
111 | 122 | |
112 | - var abortable = Abortable() | |
113 | - abortLastFeed = abortable.abort | |
123 | + var abortable = Abortable() | |
124 | + abortLastFeed = abortable.abort | |
114 | 125 | |
115 | - highlightItems = newSinceRefresh | |
116 | - newSinceRefresh = new Set() | |
126 | + highlightItems = newSinceRefresh | |
127 | + newSinceRefresh = new Set() | |
117 | 128 | |
118 | - var done = Value(false) | |
119 | - var stream = nextStepper(getStream, {reverse: true, limit: 50}) | |
120 | - var scroller = Scroller(container, content(), renderItem, false, false, () => done.set(true)) | |
129 | + var done = Value(false) | |
130 | + var stream = nextStepper(getStream, {reverse: true, limit: 50}) | |
131 | + var scroller = Scroller(container, content(), renderItem, () => done.set(true)) | |
121 | 132 | |
122 | - // track loading state | |
123 | - loading.set(computed([done, scroller.queue], (done, queue) => { | |
124 | - return !done && queue < 5 | |
125 | - })) | |
133 | + // track loading state | |
134 | + loading.set(computed([done, scroller.queue], (done, queue) => { | |
135 | + return !done && queue < 5 | |
136 | + })) | |
126 | 137 | |
127 | - pull( | |
128 | - stream, | |
129 | - pull.filter(bumpFilter), | |
130 | - abortable, | |
131 | - api.feed.pull.rollup(rootFilter), | |
132 | - scroller | |
133 | - ) | |
138 | + pull( | |
139 | + stream, | |
140 | + pull.filter(bumpFilter), | |
141 | + abortable, | |
142 | + api.feed.pull.rollup(rootFilter), | |
143 | + scroller | |
144 | + ) | |
145 | + }) | |
134 | 146 | } |
135 | 147 | |
136 | 148 | function renderItem (item, opts) { |
137 | 149 | var partial = opts && opts.partial |
modules/page/html/render/search.js | ||
---|---|---|
@@ -1,7 +1,7 @@ | ||
1 | 1 | const { h, Struct, Value, when, computed } = require('mutant') |
2 | 2 | const pull = require('pull-stream') |
3 | -const Scroller = require('../../../../lib/pull-scroll') | |
3 | +const Scroller = require('../../../../lib/scroller') | |
4 | 4 | const TextNodeSearcher = require('text-node-searcher') |
5 | 5 | const whitespace = /\s+/ |
6 | 6 | const pullAbortable = require('pull-abortable') |
7 | 7 | var nest = require('depnest') |
@@ -69,9 +69,9 @@ | ||
69 | 69 | pull( |
70 | 70 | api.sbot.pull.log({old: false}), |
71 | 71 | pull.filter(matchesQuery), |
72 | 72 | realtimeAborter, |
73 | - Scroller(container, content, renderMsg, true, false) | |
73 | + Scroller(container, content, renderMsg) | |
74 | 74 | ) |
75 | 75 | |
76 | 76 | // pull( |
77 | 77 | // nextStepper(api.sbot.pull.search, {query: queryStr, reverse: true, limit: 500, live: false}), |
package.json | ||
---|---|---|
@@ -44,9 +44,8 @@ | ||
44 | 44 | "pull-notify": "^0.1.1", |
45 | 45 | "pull-pause": "~0.0.1", |
46 | 46 | "pull-ping": "^2.0.2", |
47 | 47 | "pull-pushable": "^2.0.1", |
48 | - "pull-scroll": "^1.0.4", | |
49 | 48 | "pull-stream": "~3.6.0", |
50 | 49 | "scuttlebot": "^10.0.7", |
51 | 50 | "secure-scuttlebutt": "^16.3.4", |
52 | 51 | "sorted-array-functions": "~1.0.0", |
Built with git-ssb-web