1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import util from '../util.js';
|
19 | import platform from '../platform.js';
|
20 |
|
21 | export class LazyRepeatDelegate {
|
22 |
|
23 | constructor(userDelegate, templateElement = null) {
|
24 | if (typeof userDelegate !== 'object' || userDelegate === null) {
|
25 | util.throw('"delegate" parameter must be an object');
|
26 | }
|
27 | this._userDelegate = userDelegate;
|
28 |
|
29 | if (!(templateElement instanceof Element) && templateElement !== null) {
|
30 | util.throw('"templateElement" parameter must be an instance of Element or null');
|
31 | }
|
32 | this._templateElement = templateElement;
|
33 | }
|
34 |
|
35 | get itemHeight() {
|
36 | return this._userDelegate.itemHeight;
|
37 | }
|
38 |
|
39 | |
40 |
|
41 |
|
42 | hasRenderFunction() {
|
43 | return this._userDelegate._render instanceof Function;
|
44 | }
|
45 |
|
46 | |
47 |
|
48 |
|
49 | _render() {
|
50 | this._userDelegate._render.apply(this._userDelegate, arguments);
|
51 | }
|
52 |
|
53 | |
54 |
|
55 |
|
56 |
|
57 | loadItemElement(index, done) {
|
58 | if (this._userDelegate.loadItemElement instanceof Function) {
|
59 | this._userDelegate.loadItemElement(index, done);
|
60 | } else {
|
61 | const element = this._userDelegate.createItemContent(index, this._templateElement);
|
62 | if (!(element instanceof Element)) {
|
63 | util.throw('"createItemContent" must return an instance of Element');
|
64 | }
|
65 |
|
66 | done({element});
|
67 | }
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 | countItems() {
|
74 | const count = this._userDelegate.countItems();
|
75 | if (typeof count !== 'number') {
|
76 | util.throw('"countItems" must return a number');
|
77 | }
|
78 | return count;
|
79 | }
|
80 |
|
81 | |
82 |
|
83 |
|
84 |
|
85 |
|
86 | updateItem(index, item) {
|
87 | if (this._userDelegate.updateItemContent instanceof Function) {
|
88 | this._userDelegate.updateItemContent(index, item);
|
89 | }
|
90 | }
|
91 |
|
92 | |
93 |
|
94 |
|
95 | calculateItemHeight(index) {
|
96 | if (this._userDelegate.calculateItemHeight instanceof Function) {
|
97 | const height = this._userDelegate.calculateItemHeight(index);
|
98 |
|
99 | if (typeof height !== 'number') {
|
100 | util.throw('"calculateItemHeight" must return a number');
|
101 | }
|
102 |
|
103 | return height;
|
104 | }
|
105 |
|
106 | return 0;
|
107 | }
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 | destroyItem(index, item) {
|
114 | if (this._userDelegate.destroyItem instanceof Function) {
|
115 | this._userDelegate.destroyItem(index, item);
|
116 | }
|
117 | }
|
118 |
|
119 | |
120 |
|
121 |
|
122 | destroy() {
|
123 | if (this._userDelegate.destroy instanceof Function) {
|
124 | this._userDelegate.destroy();
|
125 | }
|
126 |
|
127 | this._userDelegate = this._templateElement = null;
|
128 | }
|
129 | }
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | export class LazyRepeatProvider {
|
135 |
|
136 | |
137 |
|
138 |
|
139 |
|
140 | constructor(wrapperElement, delegate) {
|
141 | if (!(delegate instanceof LazyRepeatDelegate)) {
|
142 | util.throw('"delegate" parameter must be an instance of LazyRepeatDelegate');
|
143 | }
|
144 |
|
145 | this._wrapperElement = wrapperElement;
|
146 | this._delegate = delegate;
|
147 | this._insertIndex = (this._wrapperElement.children[0] && this._wrapperElement.children[0].tagName === 'ONS-LAZY-REPEAT') ? 1 : 0;
|
148 |
|
149 | if (wrapperElement.tagName.toLowerCase() === 'ons-list') {
|
150 | wrapperElement.classList.add('lazy-list');
|
151 | }
|
152 |
|
153 | this._pageContent = this._findPageContentElement(wrapperElement);
|
154 |
|
155 | if (!this._pageContent) {
|
156 | util.throw('LazyRepeat must be descendant of a Page element');
|
157 | }
|
158 |
|
159 | this.lastScrollTop = this._pageContent.scrollTop;
|
160 | this.padding = 0;
|
161 | this._topPositions = [0];
|
162 | this._renderedItems = {};
|
163 |
|
164 | if (!this._delegate.itemHeight && !this._delegate.calculateItemHeight(0)) {
|
165 | this._unknownItemHeight = true;
|
166 | }
|
167 |
|
168 | this._addEventListeners();
|
169 | this._onChange();
|
170 | }
|
171 |
|
172 | get padding() {
|
173 | return parseInt(this._wrapperElement.style.paddingTop, 10);
|
174 | }
|
175 |
|
176 | set padding(newValue) {
|
177 | this._wrapperElement.style.paddingTop = newValue + 'px';
|
178 | }
|
179 |
|
180 | _findPageContentElement(wrapperElement) {
|
181 | const pageContent = util.findParent(wrapperElement, '.page__content');
|
182 |
|
183 | if (pageContent) {
|
184 | return pageContent;
|
185 | }
|
186 |
|
187 | const page = util.findParent(wrapperElement, 'ons-page');
|
188 | if (page) {
|
189 | const content = util.findChild(page, '.content');
|
190 | if (content) {
|
191 | return content;
|
192 | }
|
193 | }
|
194 |
|
195 | return null;
|
196 | }
|
197 |
|
198 | _checkItemHeight(callback) {
|
199 | this._delegate.loadItemElement(0, item => {
|
200 | if (!this._unknownItemHeight) {
|
201 | util.throw('Invalid state');
|
202 | }
|
203 |
|
204 | this._wrapperElement.appendChild(item.element);
|
205 |
|
206 | const done = () => {
|
207 | this._delegate.destroyItem(0, item);
|
208 | item.element && item.element.remove();
|
209 | delete this._unknownItemHeight;
|
210 | callback();
|
211 | };
|
212 |
|
213 | this._itemHeight = item.element.offsetHeight;
|
214 |
|
215 | if (this._itemHeight > 0) {
|
216 | done();
|
217 | return;
|
218 | }
|
219 |
|
220 |
|
221 |
|
222 | this._wrapperElement.style.visibility = 'hidden';
|
223 | item.element.style.visibility = 'hidden';
|
224 |
|
225 | setImmediate(() => {
|
226 | this._itemHeight = item.element.offsetHeight;
|
227 | if (this._itemHeight == 0) {
|
228 | util.throw('Invalid state: "itemHeight" must be greater than zero');
|
229 | }
|
230 | this._wrapperElement.style.visibility = '';
|
231 | done();
|
232 | });
|
233 | });
|
234 | }
|
235 |
|
236 | get staticItemHeight() {
|
237 | return this._delegate.itemHeight || this._itemHeight;
|
238 | }
|
239 | _countItems() {
|
240 | return this._delegate.countItems();
|
241 | }
|
242 |
|
243 | _getItemHeight(i) {
|
244 |
|
245 | if (Object.prototype.hasOwnProperty.call(this._renderedItems, i)) {
|
246 | if (!Object.prototype.hasOwnProperty.call(this._renderedItems[i], 'height')) {
|
247 | this._renderedItems[i].height = this._renderedItems[i].element.offsetHeight;
|
248 | }
|
249 | return this._renderedItems[i].height;
|
250 | }
|
251 |
|
252 |
|
253 | if (this._topPositions[i + 1] && this._topPositions[i]) {
|
254 | return this._topPositions[i + 1] - this._topPositions[i];
|
255 | }
|
256 |
|
257 | return this.staticItemHeight || this._delegate.calculateItemHeight(i);
|
258 | }
|
259 |
|
260 | _calculateRenderedHeight() {
|
261 | return Object.keys(this._renderedItems).reduce((a, b) => a + this._getItemHeight(+(b)), 0);
|
262 | }
|
263 |
|
264 | _onChange() {
|
265 | this._render();
|
266 | }
|
267 |
|
268 | _lastItemRendered() {
|
269 | return Math.max(...Object.keys(this._renderedItems));
|
270 | }
|
271 |
|
272 | _firstItemRendered() {
|
273 | return Math.min(...Object.keys(this._renderedItems));
|
274 | }
|
275 |
|
276 | refresh() {
|
277 | const forceRender = { forceScrollDown: true };
|
278 | const firstItemIndex = this._firstItemRendered();
|
279 |
|
280 | if (util.isInteger(firstItemIndex)) {
|
281 | this._wrapperElement.style.height = this._topPositions[firstItemIndex] + this._calculateRenderedHeight() + 'px';
|
282 | this.padding = this._topPositions[firstItemIndex];
|
283 | forceRender.forceFirstIndex = firstItemIndex;
|
284 | }
|
285 |
|
286 | this._removeAllElements();
|
287 | this._render(forceRender);
|
288 | this._wrapperElement.style.height = 'inherit';
|
289 | }
|
290 |
|
291 | _render({forceScrollDown = false, forceFirstIndex, forceLastIndex} = {}) {
|
292 | if (this._unknownItemHeight) {
|
293 | return this._checkItemHeight(this._render.bind(this, arguments[0]));
|
294 | }
|
295 |
|
296 | const isScrollUp = !forceScrollDown && this.lastScrollTop > this._pageContent.scrollTop;
|
297 | this.lastScrollTop = this._pageContent.scrollTop;
|
298 | const keep = {};
|
299 |
|
300 | const offset = this._wrapperElement.getBoundingClientRect().top;
|
301 | const limit = 4 * window.innerHeight - offset;
|
302 | const count = this._countItems();
|
303 |
|
304 | const items = [];
|
305 | const start = forceFirstIndex || Math.max(0, this._calculateStartIndex(offset) - 30);
|
306 | let i = start;
|
307 |
|
308 | for (let top = this._topPositions[i]; i < count && top < limit; i++) {
|
309 | if (i >= this._topPositions.length) {
|
310 | this._topPositions.length += 100;
|
311 | }
|
312 |
|
313 | this._topPositions[i] = top;
|
314 | top += this._getItemHeight(i);
|
315 | }
|
316 |
|
317 | if (this._delegate.hasRenderFunction && this._delegate.hasRenderFunction()) {
|
318 | return this._delegate._render(start, i, () => {
|
319 | this.padding = this._topPositions[start];
|
320 | });
|
321 | }
|
322 |
|
323 | if (isScrollUp) {
|
324 | for (let j = i - 1; j >= start; j--) {
|
325 | keep[j] = true;
|
326 | this._renderElement(j, isScrollUp);
|
327 | }
|
328 | } else {
|
329 | const lastIndex = forceLastIndex || Math.max(i - 1, ...Object.keys(this._renderedItems));
|
330 | for (let j = start; j <= lastIndex; j++) {
|
331 | keep[j] = true;
|
332 | this._renderElement(j, isScrollUp);
|
333 | }
|
334 | }
|
335 |
|
336 | Object.keys(this._renderedItems).forEach(key => keep[key] || this._removeElement(key, isScrollUp));
|
337 | }
|
338 |
|
339 | |
340 |
|
341 |
|
342 |
|
343 | _renderElement(index, isScrollUp) {
|
344 | const item = this._renderedItems[index];
|
345 | if (item) {
|
346 | this._delegate.updateItem(index, item);
|
347 | return;
|
348 | }
|
349 |
|
350 | this._delegate.loadItemElement(index, item => {
|
351 | if (isScrollUp) {
|
352 | this._wrapperElement.insertBefore(item.element, this._wrapperElement.children[this._insertIndex]);
|
353 | this.padding = this._topPositions[index];
|
354 | item.height = this._topPositions[index + 1] - this._topPositions[index];
|
355 | } else {
|
356 | this._wrapperElement.appendChild(item.element);
|
357 | }
|
358 |
|
359 | this._renderedItems[index] = item;
|
360 | });
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 | _removeElement(index, isScrollUp = true) {
|
368 | index = +(index);
|
369 | const item = this._renderedItems[index];
|
370 | this._delegate.destroyItem(index, item);
|
371 |
|
372 | if (isScrollUp) {
|
373 | this._topPositions[index + 1] = undefined;
|
374 | } else {
|
375 | this.padding = this.padding + this._getItemHeight(index);
|
376 | }
|
377 |
|
378 | if (item.element.parentElement) {
|
379 | item.element.parentElement.removeChild(item.element);
|
380 | }
|
381 |
|
382 | delete this._renderedItems[index];
|
383 | }
|
384 |
|
385 | _removeAllElements() {
|
386 | Object.keys(this._renderedItems).forEach(key => this._removeElement(key));
|
387 | }
|
388 |
|
389 | _recalculateTopPositions(start, end) {
|
390 | for (let i = start; i <= end; i++) {
|
391 | this._topPositions[i + 1] = this._topPositions[i] + this._getItemHeight(i);
|
392 | }
|
393 | }
|
394 |
|
395 | _calculateStartIndex(current) {
|
396 | const firstItemIndex = this._firstItemRendered();
|
397 | const lastItemIndex = this._lastItemRendered();
|
398 |
|
399 |
|
400 | this._recalculateTopPositions(firstItemIndex, lastItemIndex);
|
401 |
|
402 | let start = 0;
|
403 | let end = this._countItems() - 1;
|
404 |
|
405 |
|
406 | for (;;) {
|
407 | const middle = Math.floor((start + end) / 2);
|
408 | const value = current + this._topPositions[middle];
|
409 |
|
410 | if (end < start) {
|
411 | return 0;
|
412 | } else if (value <= 0 && value + this._getItemHeight(middle) > 0) {
|
413 | return middle;
|
414 | } else if (isNaN(value) || value >= 0) {
|
415 | end = middle - 1;
|
416 | } else {
|
417 | start = middle + 1;
|
418 | }
|
419 | }
|
420 | }
|
421 |
|
422 | _debounce(func, wait, immediate) {
|
423 | let timeout;
|
424 | return function() {
|
425 | const callNow = immediate && !timeout;
|
426 | clearTimeout(timeout);
|
427 | if (callNow) {
|
428 | func.apply(this, arguments);
|
429 | } else {
|
430 | timeout = setTimeout(() => {
|
431 | timeout = null;
|
432 | func.apply(this, arguments);
|
433 | }, wait);
|
434 | }
|
435 | };
|
436 | }
|
437 |
|
438 | _doubleFireOnTouchend() {
|
439 | this._render();
|
440 | this._debounce(this._render.bind(this), 100);
|
441 | }
|
442 |
|
443 | _addEventListeners() {
|
444 | util.bindListeners(this, ['_onChange', '_doubleFireOnTouchend']);
|
445 |
|
446 | if (platform.isIOS()) {
|
447 | this._boundOnChange = this._debounce(this._boundOnChange, 30);
|
448 | }
|
449 |
|
450 | this._pageContent.addEventListener('scroll', this._boundOnChange, true);
|
451 |
|
452 | if (platform.isIOS()) {
|
453 | util.addEventListener(this._pageContent, 'touchmove', this._boundOnChange, { capture: true, passive: true });
|
454 | this._pageContent.addEventListener('touchend', this._boundDoubleFireOnTouchend, true);
|
455 | }
|
456 |
|
457 | window.document.addEventListener('resize', this._boundOnChange, true);
|
458 | }
|
459 |
|
460 | _removeEventListeners() {
|
461 | this._pageContent.removeEventListener('scroll', this._boundOnChange, true);
|
462 |
|
463 | if (platform.isIOS()) {
|
464 | util.removeEventListener(this._pageContent, 'touchmove', this._boundOnChange, { capture: true, passive: true });
|
465 | this._pageContent.removeEventListener('touchend', this._boundDoubleFireOnTouchend, true);
|
466 | }
|
467 |
|
468 | window.document.removeEventListener('resize', this._boundOnChange, true);
|
469 | }
|
470 |
|
471 | destroy() {
|
472 | this._removeAllElements();
|
473 | this._delegate.destroy();
|
474 | this._parentElement = this._delegate = this._renderedItems = null;
|
475 | this._removeEventListeners();
|
476 | }
|
477 | }
|
478 |
|