diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ebda23ef..85c980a6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-root', @@ -9,21 +10,21 @@ export class AppComponent { private title = 'app works!'; private datasource = { - get: (index: number, count: number, success) => { + get: (index: number, count: number) => Observable.create(observer => { console.log('requested index = ' + index + ', count = ' + count); setTimeout(() => { - let result = []; + let data = []; for (let i = index; i <= index + count - 1; i++) { - result.push({ + data.push({ id: i, text: "item #" + i }); } - console.log('resolved ' + result.length + ' items'); - success(result); + console.log('resolved ' + data.length + ' items (index = ' + index + ', count = ' + count + ')'); + observer.next(data); }, 50); - } - }; + }) + }; constructor() { } diff --git a/src/app/ui-scroll/data.ts b/src/app/ui-scroll/data.ts index 2ec82f4a..b169151f 100644 --- a/src/app/ui-scroll/data.ts +++ b/src/app/ui-scroll/data.ts @@ -12,6 +12,8 @@ class Data { static eof = false; static position = 0; + static lastIndex = null; + static setSource(datasource: any) { if (!datasource || typeof datasource !== 'object' || typeof datasource.get !== 'function') { throw new Error('Invalid datasource!'); @@ -28,6 +30,38 @@ class Data { return 'i-' + self.scrollerId + '-' + index.toString(); } + static getFirstVisibleItemIndex() { + for(let i = 0; i < self.items.length; i++) { + if(!self.items[i].invisible) { + return i; + } + } + return -1; + } + + static getFirstVisibleItem() { + const index = self.getFirstVisibleItemIndex(); + if(index >= 0) { + return self.items[index]; + } + } + + static getLastVisibleItemIndex() { + for(let i = self.items.length - 1; i >= 0; i--) { + if(!self.items[i].invisible) { + return i; + } + } + return -1; + } + + static getLastVisibleItem() { + const index = self.getLastVisibleItemIndex(); + if(index >= 0) { + return self.items[index]; + } + } + static initialize(context) { self.setSource(context.datasource); self.setScrollerId(); diff --git a/src/app/ui-scroll/debouncedRound.ts b/src/app/ui-scroll/debouncedRound.ts new file mode 100644 index 00000000..f9ec2ab8 --- /dev/null +++ b/src/app/ui-scroll/debouncedRound.ts @@ -0,0 +1,26 @@ +let timer = null; +let next = null; + +const runTimer = (delay) => { + timer = setTimeout(() => { + timer = null; + if(next) { + next(); + next = null; + runTimer(delay); + } + }, delay) +}; + +const debouncedRound = (cb, delay) => { + if(!timer) { + cb(); + } + else { + next = cb; + clearTimeout(timer); + } + runTimer(delay); +}; + +export default debouncedRound \ No newline at end of file diff --git a/src/app/ui-scroll/direction.ts b/src/app/ui-scroll/direction.ts index 214ff042..0a01eb53 100644 --- a/src/app/ui-scroll/direction.ts +++ b/src/app/ui-scroll/direction.ts @@ -26,6 +26,10 @@ class Direction { } return null; } + + static isValid(value) { + return value === self.top || value === self.bottom; + } } const self = Direction; diff --git a/src/app/ui-scroll/processes/clip.ts b/src/app/ui-scroll/processes/clip.ts deleted file mode 100644 index 6539a53a..00000000 --- a/src/app/ui-scroll/processes/clip.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Data from '../data' -import Elements from '../elements' -import Direction from '../direction' - -class Clip { - - static run(direction) { - if (direction === Direction.top) { - self.runTop(); - } - if (direction === Direction.bottom) { - self.runBottom(); - } - Data.position = Elements.viewport.scrollTop; - } - - static runTop() { - let viewportParams = Elements.viewport.getBoundingClientRect(); - let viewportTop = viewportParams.top; - let delta = viewportParams.height * Data.padding; - let i, min, firstElementTop, lastElementBottom; - for (i = 0, min = 0; i < Data.items.length; i++) { - const item = Data.items[i]; - if (min === i) { - if (item.invisible) { - min++; - continue; - } - } - let elementParams = item.element.getBoundingClientRect(); - if (elementParams.bottom < viewportTop - delta) { - if (i === min) { - firstElementTop = elementParams.top; - } - lastElementBottom = elementParams.bottom; - continue; - } - if (i === min) { - break; - } - for (let j = i - 1; j >= min; j--) { - Data.items[j].element.style.display = 'none'; - } - let cutHeight = lastElementBottom - firstElementTop; - let paddingHeight = parseInt(Elements.paddingTop.style.height, 10) || 0; - Elements.paddingTop.style.height = (paddingHeight + cutHeight) + 'px'; - Data.items.splice(min, i); - return true; - } - return false; - } - - static runBottom() { - let viewportParams = Elements.viewport.getBoundingClientRect(); - let viewportBottom = viewportParams.bottom; - let delta = viewportParams.height * Data.padding; - let limit = Data.items.length - 1; - let i, firstElementTop, lastElementBottom; - for (i = limit; i >= 0; i--) { - let elementParams = Data.items[i].element.getBoundingClientRect(); - if (elementParams.top > viewportBottom + delta) { - if (i === limit) { - lastElementBottom = elementParams.bottom; - } - firstElementTop = elementParams.top; - continue; - } - if (i === limit) { - break; - } - for (let j = i + 1; j <= limit; j++) { - Data.items[j].element.style.display = 'none'; - } - let cutHeight = lastElementBottom - firstElementTop; - let paddingHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; - Elements.paddingBottom.style.height = (paddingHeight + cutHeight) + 'px'; - Data.items.splice(i + 1, limit - i); - return true; - } - return false; - } - -} - -const self = Clip; -export default Clip diff --git a/src/app/ui-scroll/processes/fetch.ts b/src/app/ui-scroll/processes/fetch.ts deleted file mode 100644 index b8dde9e8..00000000 --- a/src/app/ui-scroll/processes/fetch.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Data from '../data' -import Elements from '../elements' -import Direction from '../direction' -import Process from './index' - -class Fetch { - - static pendingTop = false; - static pendingBottom = false; - - static run(direction) { - if (direction === Direction.top) { - self.runTop(); - } - if (direction === Direction.bottom) { - self.runBottom(); - } - } - - static shouldLoadBottom() { - if (self.pendingBottom) { - return false; - } - if (!Data.items.length) { - return true; - } - let lastElement = Data.items[Data.items.length - 1].element; - let viewportBottom = Elements.viewport.getBoundingClientRect().bottom; - let lastElementBottom = lastElement.getBoundingClientRect().bottom; - return lastElementBottom <= viewportBottom; - } - - static shouldLoadTop() { - if (self.pendingTop) { - return false; - } - if (!Data.items.length) { - return true; - } - let viewportTop = Elements.viewport.getBoundingClientRect().top; - let firstElementTop = Data.items[0].element.getBoundingClientRect().top; - return firstElementTop >= viewportTop; - } - - static runTop() { - if (self.shouldLoadTop()) { - self.pendingTop = true; - let start = (Data.items.length ? Data.items[0].$index : Data.startIndex) - Data.bufferSize; - Data.source.get(start, Data.bufferSize, (result) => { - self.pendingTop = false; - Data.bof = result.length !== Data.bufferSize; - - let items = result.map((item, index) => ({ - $index: start + index, - scope: item, - invisible: true - })); - Data.items = [...items, ...Data.items]; - - Process.render.run(items, Direction.top); - }); - } - } - - static runBottom() { - if (self.shouldLoadBottom()) { - self.pendingBottom = true; - let start = Data.items.length ? Data.items[Data.items.length - 1].$index + 1 : Data.startIndex; - Data.source.get(start, Data.bufferSize, (result) => { - self.pendingBottom = false; - Data.eof = result.length !== Data.bufferSize; - - let items = result.map((item, index) => ({ - $index: start + index, - scope: item, - invisible: true - })); - Data.items = [...Data.items, ...items]; - - Process.render.run(items, Direction.bottom); - }); - - } - } - -} - -const self = Fetch; -export default Fetch diff --git a/src/app/ui-scroll/processes/index.ts b/src/app/ui-scroll/processes/index.ts deleted file mode 100644 index 6fef8589..00000000 --- a/src/app/ui-scroll/processes/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Fetch from './fetch' -import Render from './render' -import Clip from './clip' -import Adjust from './adjust' - -export default class Process { - static fetch = Fetch; - static render = Render; - static clip = Clip; - static adjust = Adjust; -} diff --git a/src/app/ui-scroll/ui-scroll.component.ts b/src/app/ui-scroll/ui-scroll.component.ts index a4afc84b..cbd614c1 100644 --- a/src/app/ui-scroll/ui-scroll.component.ts +++ b/src/app/ui-scroll/ui-scroll.component.ts @@ -1,41 +1,48 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {ContentChild, TemplateRef, ElementRef, Renderer} from '@angular/core'; +import {Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy, Input} from '@angular/core'; +import {ContentChild, TemplateRef, ElementRef, Renderer2} from '@angular/core'; import {HostListener} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {AsyncSubject} from 'rxjs/AsyncSubject'; -import Process from './processes/index'; +import Workflow from './workflow'; import Elements from './elements'; import Data from './data'; import Direction from './direction'; +import debouncedRound from './debouncedRound'; + @Component({ selector: 'ui-scroll', + changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './ui-scroll.component.html', styleUrls: ['./ui-scroll.component.css'] }) -export class UiScrollComponent implements OnInit { +export class UiScrollComponent implements OnInit, OnDestroy { @Input() datasource; @ContentChild(TemplateRef) templateVariable: TemplateRef; onScrollListener: Function; - constructor(private elementRef: ElementRef, private renderer: Renderer) { + constructor(private changeDetector: ChangeDetectorRef, private elementRef: ElementRef, private renderer: Renderer2) { } ngOnInit() { Elements.initialize(this.elementRef); Data.initialize(this); - this.onScrollListener = this.renderer.listen(Elements.viewport, 'scroll', (event) => { - const direction = Direction.byScrollTop(); - Process.fetch.run(direction); - }); - Process.fetch.run(Direction.bottom); - Process.fetch.run(Direction.top); + Workflow.initialize(this); + this.onScrollListener = this.renderer.listen(Elements.viewport, 'scroll', (event) => + debouncedRound(() => Workflow.run(event), 25) + ); + Workflow.run(Direction.bottom); + Workflow.run(Direction.top); + } + + ngOnDestroy() { + this.onScrollListener(); } } diff --git a/src/app/ui-scroll/workflow.ts b/src/app/ui-scroll/workflow.ts new file mode 100644 index 00000000..c6a445fa --- /dev/null +++ b/src/app/ui-scroll/workflow.ts @@ -0,0 +1,71 @@ +import { Observable } from 'rxjs/Rx' + +import Fetch from './workflow/fetch' +import Render from './workflow/render' +import Clip from './workflow/clip' +import Adjust from './workflow/adjust' + +import Elements from './elements' +import Direction from './direction' +import Data from './data' + +const Workflow = { + + runChangeDetector: (items) => null, + + initialize: (context) => { + Workflow.runChangeDetector = (items) => { + context.changeDetector.markForCheck(); + return items; + } + }, + + cycle: (direction) => + Observable.create(observer => { + Fetch.run(direction) + .then(items => Workflow.runChangeDetector(items)) + .then(items => Render.run(items, direction)) + .then(items => { + Adjust.run(direction, items); + Data.position = Elements.viewport.scrollTop; + + if(Clip.run(Direction.opposite(direction))) { + Workflow.runChangeDetector(null); + } + Data.position = Elements.viewport.scrollTop; + + console.log(direction + ' cycle is done'); + observer.next(direction); + observer.complete(); + }) + .catch(error => { + console.log('Done ' + direction); + error && console.error(error); + observer.complete(); + }) + }), + + run: (param) => { + let direction; + if(typeof param === 'string') { + direction = param; + } + else { + // scroll event + console.log('FIRE!') + direction = Direction.byScrollTop(); + } + if(!Direction.isValid(direction)) { + return; + } + + const run = () => + Workflow.cycle(direction).subscribe(run); + + run(); + } +} + +export default Workflow + +// fetch -> render -> adjust + clip + fetch \ No newline at end of file diff --git a/src/app/ui-scroll/processes/adjust.ts b/src/app/ui-scroll/workflow/adjust.ts similarity index 95% rename from src/app/ui-scroll/processes/adjust.ts rename to src/app/ui-scroll/workflow/adjust.ts index 409f647c..0314e7c2 100644 --- a/src/app/ui-scroll/processes/adjust.ts +++ b/src/app/ui-scroll/workflow/adjust.ts @@ -1,60 +1,59 @@ -import Elements from '../elements' -import Direction from '../direction' -import Data from '../data' -import Process from './index' - -class Adjust { - - static run(direction, items) { - if (direction === Direction.top) { - self.runTop(items); - } - if (direction === Direction.bottom) { - self.runBottom(items); - } - } - - static processInvisibleItems(items) { - for (let i = items.length - 1; i >= 0; i--) { - let element = items[i].element.children[0]; - element.style.left = ''; - element.style.position = ''; - delete items[i].invisible; - } - return Math.abs(items[0].element.getBoundingClientRect().top - items[items.length - 1].element.getBoundingClientRect().bottom); - } - - static runBottom(items) { - const height = self.processInvisibleItems(items); - const _paddingBottomHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; - const paddingBottomHeight = Math.max(_paddingBottomHeight - height, 0); - Elements.paddingBottom.style.height = paddingBottomHeight + 'px'; - } - - static runTop(items) { - const _scrollTop = Elements.viewport.scrollTop; - let height = self.processInvisibleItems(items); - - // now need to make "height" pixels top - // 1) via paddingTop - let _paddingTopHeight = parseInt(Elements.paddingTop.style.height, 10) || 0; - let paddingTopHeight = Math.max(_paddingTopHeight - height, 0); - Elements.paddingTop.style.height = paddingTopHeight + 'px'; - let paddingDiff = height - (_paddingTopHeight - paddingTopHeight); - // 2) via scrollTop - if (paddingDiff > 0) { - height = paddingDiff; - Elements.viewport.scrollTop += height; - const diff = height - Elements.viewport.scrollTop - _scrollTop; - if (diff > 0) { - let paddingHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; - Elements.paddingBottom.style.height = (paddingHeight + diff) + 'px'; - Elements.viewport.scrollTop += diff; - } - } - } - -} - -const self = Adjust; -export default Adjust +import Elements from '../elements' +import Direction from '../direction' +import Data from '../data' + +class Adjust { + + static run(direction, items) { + if (direction === Direction.top) { + self.runTop(items); + } + if (direction === Direction.bottom) { + self.runBottom(items); + } + } + + static processInvisibleItems(items) { + for (let i = items.length - 1; i >= 0; i--) { + let element = items[i].element.children[0]; + element.style.left = ''; + element.style.position = ''; + delete items[i].invisible; + } + return Math.abs(items[0].element.getBoundingClientRect().top - items[items.length - 1].element.getBoundingClientRect().bottom); + } + + static runBottom(items) { + const height = self.processInvisibleItems(items); + const _paddingBottomHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; + const paddingBottomHeight = Math.max(_paddingBottomHeight - height, 0); + Elements.paddingBottom.style.height = paddingBottomHeight + 'px'; + } + + static runTop(items) { + const _scrollTop = Elements.viewport.scrollTop; + let height = self.processInvisibleItems(items); + + // now need to make "height" pixels top + // 1) via paddingTop + let _paddingTopHeight = parseInt(Elements.paddingTop.style.height, 10) || 0; + let paddingTopHeight = Math.max(_paddingTopHeight - height, 0); + Elements.paddingTop.style.height = paddingTopHeight + 'px'; + let paddingDiff = height - (_paddingTopHeight - paddingTopHeight); + // 2) via scrollTop + if (paddingDiff > 0) { + height = paddingDiff; + Elements.viewport.scrollTop += height; + const diff = height - Elements.viewport.scrollTop - _scrollTop; + if (diff > 0) { + let paddingHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; + Elements.paddingBottom.style.height = (paddingHeight + diff) + 'px'; + Elements.viewport.scrollTop += diff; + } + } + } + +} + +const self = Adjust; +export default Adjust diff --git a/src/app/ui-scroll/workflow/clip.ts b/src/app/ui-scroll/workflow/clip.ts new file mode 100644 index 00000000..a0b39a58 --- /dev/null +++ b/src/app/ui-scroll/workflow/clip.ts @@ -0,0 +1,109 @@ +import Data from '../data' +import Elements from '../elements' +import Direction from '../direction' + +class Clip { + + static run(direction) { + if (direction === Direction.top) { + return self.runTop(); + } + if (direction === Direction.bottom) { + return self.runBottom(); + } + } + + static runTop() { + let viewportParams = Elements.viewport.getBoundingClientRect(); + let viewportTop = viewportParams.top; + let delta = viewportParams.height * Data.padding; + let bottomLimit = viewportTop - delta; + let limit = Data.items.length - 1; + let i = 0, min = 0, cutHeight, found = -1; + + const startIndex = Data.getFirstVisibleItemIndex(); + const lastItem = Data.items[limit]; + const lastElementBottom = lastItem.element.getBoundingClientRect().bottom; + + // edge case: all items should be clipped + if (lastElementBottom < bottomLimit) { + cutHeight = Math.abs(Data.items[startIndex].element.getBoundingClientRect().top - lastElementBottom); + found = limit; + } + else { + for (i = startIndex; i <= limit; i++) { + const item = Data.items[i]; + if(item.element.getBoundingClientRect().bottom > bottomLimit) { + found = i - 1; + break; + } + } + if(found >= startIndex) { + cutHeight = Data.items[found].element.getBoundingClientRect().bottom - + Data.items[startIndex].element.getBoundingClientRect().top; + } + } + + if(found >= startIndex) { + for (i = startIndex; i <= found; i++) { + Data.items[i].element.style.display = 'none'; + } + let paddingHeight = parseInt(Elements.paddingTop.style.height, 10) || 0; + Elements.paddingTop.style.height = (paddingHeight + cutHeight) + 'px'; + Data.lastIndex = found === limit ? Data.items[limit].$index : null; + Data.items.splice(0, found + 1); + return true; + } + + return false; + } + + static runBottom() { + let viewportParams = Elements.viewport.getBoundingClientRect(); + let viewportBottom = viewportParams.bottom; + let delta = viewportParams.height * Data.padding; + let topLimit = viewportBottom + delta; + let limit = Data.items.length - 1; + let i, cutHeight, found = -1; + + const endIndex = Data.getLastVisibleItemIndex(); + const firstItem = Data.items[0]; + const firstElementTop = firstItem.element.getBoundingClientRect().top; + + // edge case: all items should be clipped + if (firstElementTop > topLimit) { + cutHeight = Math.abs(Data.items[endIndex].element.getBoundingClientRect().bottom - firstElementTop); + found = 0; + } + else { + for (i = 0; i <= endIndex; i++) { + const element = Data.items[i].element; + if(element.getBoundingClientRect().top > topLimit) { + found = i; + break; + } + } + if(found >= 0) { + cutHeight = Data.items[endIndex].element.getBoundingClientRect().bottom - + Data.items[found].element.getBoundingClientRect().top; + } + } + + if(found >= 0) { + for (i = found; i <= endIndex; i++) { + Data.items[i].element.style.display = 'none'; + } + let paddingHeight = parseInt(Elements.paddingBottom.style.height, 10) || 0; + Elements.paddingBottom.style.height = (paddingHeight + cutHeight) + 'px'; + Data.lastIndex = found === 0 ? Data.items[0].$index : null; + Data.items.splice(found, limit + 1 - found); + return true; + } + + return false; + } + +} + +const self = Clip; +export default Clip diff --git a/src/app/ui-scroll/workflow/fetch.ts b/src/app/ui-scroll/workflow/fetch.ts new file mode 100644 index 00000000..d6240ad9 --- /dev/null +++ b/src/app/ui-scroll/workflow/fetch.ts @@ -0,0 +1,104 @@ +import Data from '../data' +import Elements from '../elements' +import Direction from '../direction' + +class Fetch { + + static pendingTop = false; + static pendingBottom = false; + + static run(direction) { + if (direction === Direction.top) { + return self.runTop(); + } + if (direction === Direction.bottom) { + return self.runBottom(); + } + } + + static shouldLoadBottom() { + if (self.pendingBottom) { + return false; + } + const lastItem = Data.getLastVisibleItem(); + if (!lastItem) { + return true; + } + const viewportBottom = Elements.viewport.getBoundingClientRect().bottom; + const lastElementBottom = lastItem.element.getBoundingClientRect().bottom; + return lastElementBottom <= viewportBottom; + } + + static shouldLoadTop() { + if (self.pendingTop) { + return false; + } + const firstItem = Data.getFirstVisibleItem(); + if (!firstItem) { + return true; + } + const viewportTop = Elements.viewport.getBoundingClientRect().top; + const firstElementTop = firstItem.element.getBoundingClientRect().top; + return firstElementTop >= viewportTop; + } + + static runTop() { + return new Promise((resolve, reject) => { + if (self.shouldLoadTop()) { + self.pendingTop = true; + const start = (Data.items.length ? + Data.items[0].$index : + (Data.lastIndex !== null ? Data.lastIndex : Data.startIndex)) + - Data.bufferSize; + Data.source.get(start, Data.bufferSize).subscribe((result) => { + self.pendingTop = false; + Data.bof = result.length !== Data.bufferSize; + + const items = result.map((item, index) => ({ + $index: start + index, + scope: item, + invisible: true + })); + Data.items = [...items, ...Data.items]; + + resolve(items); + }); + } + else { + reject(); + } + }); + } + + static runBottom() { + return new Promise((resolve, reject) => { + if (self.shouldLoadBottom()) { + self.pendingBottom = true; + const start = Data.items.length ? + Data.items[Data.items.length - 1].$index + 1 : + (Data.lastIndex !== null ? Data.lastIndex + 1 : Data.startIndex); + + Data.source.get(start, Data.bufferSize).subscribe((result) => { + self.pendingBottom = false; + Data.eof = result.length !== Data.bufferSize; + + const items = result.map((item, index) => ({ + $index: start + index, + scope: item, + invisible: true + })); + Data.items = [...Data.items, ...items]; + + resolve(items); + }); + } + else { + reject(); + } + }); + } + +} + +const self = Fetch; +export default Fetch \ No newline at end of file diff --git a/src/app/ui-scroll/processes/render.ts b/src/app/ui-scroll/workflow/render.ts similarity index 58% rename from src/app/ui-scroll/processes/render.ts rename to src/app/ui-scroll/workflow/render.ts index 076dda43..71bde517 100644 --- a/src/app/ui-scroll/processes/render.ts +++ b/src/app/ui-scroll/workflow/render.ts @@ -1,44 +1,41 @@ -import Elements from '../elements' -import Direction from '../direction' -import Data from '../data' -import Process from './index' - -class Render { - - static renderPending = false; - - static run(items = null, direction = null) { - self.renderPending = true; - setTimeout(() => { - self.renderPending = false; - if (items) { - self.setElements(items); - } - Process.adjust.run(direction, items); - Data.position = Elements.viewport.scrollTop; - - Process.clip.run(Direction.opposite(direction)); - Process.fetch.run(direction); - }); - } - - static setElements(items) { - items.forEach(item => { - for (let i = Elements.viewport.childNodes.length - 1; i >= 0; i--) { - let node = Elements.viewport.childNodes[i]; - if (node.id) { - if (node.id === Data.getItemId(item.$index)) { - item.element = node; - } - } - } - if (!item.element) { // todo: just remove this - throw new Error('Can not associate item with element'); - } - }); - } - -} - -const self = Render; -export default Render +import Elements from '../elements' +import Data from '../data' + +class Render { + + static renderPending = false; + + static run(items = null, direction = null) { + return new Promise((resolve, reject) => { + self.renderPending = true; + setTimeout(() => { + self.renderPending = false; + if (items) { + self.setElements(items); + resolve(items); + } + reject(); + }); + }); + } + + static setElements(items) { + items.forEach(item => { + for (let i = Elements.viewport.childNodes.length - 1; i >= 0; i--) { + let node = Elements.viewport.childNodes[i]; + if (node.id) { + if (node.id === Data.getItemId(item.$index)) { + item.element = node; + } + } + } + if (!item.element) { // todo: just remove this + throw new Error('Can not associate item with element'); + } + }); + } + +} + +const self = Render; +export default Render \ No newline at end of file