Source: winjscontrib.ui.multipass-renderer.js

/* 
 * WinJS Contrib v2.1.0.6
 * licensed under MIT license (see http://opensource.org/licenses/MIT)
 * sources available at https://github.com/gleborgne/winjscontrib
 */

var WinJSContrib = WinJSContrib || {};
WinJSContrib.UI = WinJSContrib.UI || {};

(function () {
    "use strict";

    WinJSContrib.UI.MultiPassRenderer = WinJS.Class.define(
    /**
     * This control manage multi-pass rendering for improving performances when showing a list of items.
     * The renderer will render "shells" for the items to enable page layout, and render items content on demand, or when items scrolls to view.  
     * @class WinJSContrib.UI.MultiPassRenderer
     */
    function (element, options) {
        options = options || {};
        this.items = [];
        this.element = element;
        this._scrollProcessor = null;
        this._tolerance = 1;
        this._virtualize = false;
        this._scrollContainer = options.scrollContainer || null;
        this._multipass = options.multipass || false;
        this._orientation = options.orientation || '';
        this.itemClassName = options.itemClass || options.className || options.itemClassName;
        this.itemTemplate = WinJSContrib.Utils.getTemplate(options.template || options.itemTemplate);
        this.itemInvoked = options.invoked || options.itemInvoked;
        this.onitemContent = options.onitemContent;
        this._onScrollBinded = this._onScroll.bind(this);

        if (element) {
            element.className = element.className + ' mcn-items-ctrl mcn-layout-ctrl win-disposable';
        }

        //element.mcnRenderer = this;
        //WinJS.UI.setOptions(this, options);
    },
    /**
      * @lends WinJSContrib.UI.MultiPassRenderer.prototype
      */
    {
        dispose : function(){
            var ctrl = this;
            WinJSContrib.UI.untapAll(ctrl.element);
            WinJS.Utilities.disposeSubTree(ctrl.element);
            ctrl._unregisterScrollEvents();
            ctrl._scrollContainer = null;
            ctrl.element = null;
            ctrl._scrollProcessor = null;
            ctrl.rect = null;           
            ctrl.items = [];
        },

        clear: function () {
            var ctrl = this;
            WinJS.Utilities.disposeSubTree(ctrl.element);
            ctrl.element.innerHTML = '';
        },

        /**
         * kind of multipass, can be 'section', or 'item'
         * @type {String}
         * @field
         */
        multipass: {
            get: function () {
                return this._multipass;
            },
            set: function (val) {
                this._multipass = val;
                this.refreshScrollEvents();
            }
        },

        /**
         * tolerance for determining if the rendering should apply. Tolerance is expressed in scroll container proportion. For example, 1 means that tolerance is equal to scroll container size
         * @type {number}
         * @field
         */
        tolerance: {
            get: function () {
                return this._tolerance;
            },
            set: function (val) {
                this._tolerance = val;
            }
        },

        /**
         * indicate if renderer empty items out of sight
         * @type {boolean}
         * @field
         */
        virtualize: {
            get: function () {
                return this._virtualize;
            },
            set: function (val) {
                this._virtualize = val;
            }
        },

        /**
         * could be 'vertical' or 'horizontal'
         * @type {String}
         * @field
         */
        orientation: {
            get: function () {
                return this._orientation;
            },
            set: function (val) {
                this._orientation = val;
                this.refreshScrollEvents();
            }
        },

        /**
         * Element containing scroll. If scrollContainer is filled, items content will get rendered when coming into view
         * @type {HTMLElement}
         * @field
         */
        scrollContainer: {
            get: function () {
                return this._scrollContainer;
            },
            set: function (val) {
                this._unregisterScrollEvents();
                this._scrollContainer = val;
                this._registerScrollEvents();
                //this.checkRendering();
            }
        },

        _onScroll: function () {
            var ctrl = this;
            if (ctrl.scrollContainer && ctrl._scrollProcessor) {
                if (ctrl._scrollRequest) {
                    cancelAnimationFrame(ctrl._scrollRequest);
                }

                ctrl._scrollRequest = requestAnimationFrame(function () {
                    ctrl.checkRendering();
                });
            }
        },

        _unregisterScrollEvents: function () {
            this._scrollProcessor = null;
            this.clearOffsets();
            if (this.scrollContainer) {
                this.scrollContainer.removeEventListener('scroll', this._onScrollBinded);
            }
        },

        _registerScrollEvents: function () {
            var ctrl = this;
            if (ctrl.scrollContainer) {
                this.scrollContainer.addEventListener('scroll', this._onScrollBinded);

                if (ctrl.orientation == 'vertical') {
                    if (ctrl.multipass == 'section') {
                        ctrl._scrollProcessor = function () { ctrl._checkSection(ctrl._vIsInView); }
                    } else if (ctrl.multipass == 'item') {
                        ctrl._scrollProcessor = function () { ctrl._checkItem(ctrl._vIsInView); }
                    }
                } else {
                    if (ctrl.multipass == 'section') {
                        ctrl._scrollProcessor = function () { ctrl._checkSection(ctrl._hIsInView); }
                    } else if (ctrl.multipass == 'item') {
                        ctrl._scrollProcessor = function () { ctrl._checkItem(ctrl._hIsInView); }
                    }
                }
            }
        },

        /**
         * refresh scroll events associated to multi pass renderer
         */
        refreshScrollEvents: function () {
            this._unregisterScrollEvents();
            this._registerScrollEvents();
            this.clearOffsets();
        },

        _vIsInView: function (rect, scrollContainer, tolerance) {
            var pxTolerance = scrollContainer.clientHeight * tolerance;
            if (rect.y >= scrollContainer.scrollTop - pxTolerance && rect.y <= scrollContainer.scrollTop + scrollContainer.clientHeight + pxTolerance)
                return true;

            if (rect.y + rect.height >= scrollContainer.scrollTop - pxTolerance && rect.y + rect.height <= scrollContainer.scrollTop + scrollContainer.clientHeight + pxTolerance)
                return true;

            if (rect.y <= scrollContainer.scrollTop - pxTolerance && rect.y + rect.height >= scrollContainer.scrollTop + scrollContainer.clientHeight + pxTolerance)
                return true;

            return false;
        },

        _hIsInView: function (rect, scrollContainer, tolerance) {
            var pxTolerance = scrollContainer.clientWidth * (tolerance || 0);
            if (rect.x >= scrollContainer.scrollLeft - pxTolerance && rect.x <= scrollContainer.scrollLeft + scrollContainer.clientWidth + pxTolerance)
                return true;

            if (rect.x + rect.width >= scrollContainer.scrollLeft - pxTolerance && rect.x + rect.width <= scrollContainer.scrollLeft + scrollContainer.clientWidth + pxTolerance)
                return true;

            if (rect.x <= scrollContainer.scrollLeft - pxTolerance && rect.x + rect.width >= scrollContainer.scrollLeft + scrollContainer.clientWidth + pxTolerance)
                return true;

            return false;
        },

        /**
         * Clear cached offsets for bloc and for items
         */
        clearOffsets: function () {
            var ctrl = this;
            if (!ctrl.element)
                return;
            
            ctrl.rect = null;
            ctrl.items.forEach(function (item) {
                item.rect = null;
            });
        },

        /**
         * update ui related properties like cached offsets, scroll events, ...
         */
        updateLayout: function () {
            var ctrl = this;
            if (!ctrl.element)
                return;
            ctrl.refreshScrollEvents();
        },

        _checkSection: function (check, tolerance, noRender) {
            var ctrl = this;
            if (!ctrl.element)
                return;
            tolerance = tolerance || 0;

            if (!ctrl.rect && ctrl.items && ctrl.items.length) {
                ctrl.rect = WinJSContrib.UI.offsetFrom(ctrl.element, ctrl.scrollContainer);
            } else {
                ctrl.rect = ctrl.rect || { x: 0, y: 0, width: 0, height: 0 };
                ctrl.rect.width = ctrl.element.clientWidth;
                ctrl.rect.height = ctrl.element.clientHeight;
            }

            if (check(ctrl.rect, ctrl.scrollContainer, tolerance)) {
                if (noRender)
                    return true;

                ctrl.renderItemsContent();
                if (ctrl.onrendersection) {
                    ctrl.onrendersection();
                }
            } else if (ctrl.virtualize && tolerance > 0 && ctrl.items && ctrl.items.length && ctrl.items[0].rendered) {
                ctrl.items.forEach(function (item) {
                    item.empty();
                });
            }

            if (tolerance == 0 && ctrl.tolerance > 0) {
                setImmediate(function () {
                    ctrl._checkSection(check, ctrl.tolerance);
                });
            }
        },

        _checkItem: function (check, tolerance) {
            var ctrl = this;
            if (!ctrl.element)
                return;
            tolerance = tolerance || 0;
            var allRendered = true;

            var countR = function () {
                var countRendered = 0;
                ctrl.items.forEach(function (item) {
                    if (item.rendered) {
                        countRendered++;
                    }
                });
                console.log('rendered ' + countRendered);
            }

            if (ctrl._checkSection(check, tolerance, true)) {
                ctrl.items.forEach(function (item) {
                    if (!item.rect) {
                        item.rect = WinJSContrib.UI.offsetFrom(item.element, ctrl.scrollContainer);
                    }
                    allRendered = allRendered & item.rendered;

                    if (!item.rendered && check(item.rect, ctrl.scrollContainer, tolerance)) {
                        item.render();
                    } else if (item.rendered && ctrl.virtualize && tolerance > 0 && !check(item.rect, ctrl.scrollContainer, tolerance)) {
                        item.empty();
                    }
                });
                ctrl.allRendered = allRendered;
                //countR();
            } else if (tolerance > 0 && ctrl.items.length && (ctrl.items[0].rendered || ctrl.items[ctrl.items.length - 1].rendered)) {
                ctrl.items.forEach(function (item) {
                    if (!item.rect) {
                        item.rect = WinJSContrib.UI.offsetFrom(item.element, ctrl.scrollContainer);
                    }
                    if (ctrl.virtualize && !check(item.rect, ctrl.scrollContainer, tolerance)) {
                        item.empty();
                    }
                });
                //countR();
            }

            if (tolerance == 0 && ctrl.tolerance > 0) {
                setImmediate(function () {
                    ctrl._checkItem(check, ctrl.tolerance);
                });
            }


        },

        /**
         * render items shells to the page
         * @param {Array} items array of items to render
         * @param {Object} renderOptions options for rendering items, can override control options like item template
         */
        prepareItems: function (items, renderOptions) {
            var ctrl = this;
            items = items || [];
            renderOptions = renderOptions || {};
            var numItems = items.length;

            var itemInvoked = renderOptions.itemInvoked || ctrl.itemInvoked;
            if (typeof itemInvoked == 'string')
                itemInvoked = WinJSContrib.Utils.resolveMethod(ctrl.element, itemInvoked);
            var template = WinJSContrib.Utils.getTemplate(renderOptions.template) || ctrl.itemTemplate;
            var className = renderOptions.itemClassName || ctrl.itemClassName;
            var onitemInit = renderOptions.onitemInit || ctrl.onitemInit;
            var onitemContent = renderOptions.onitemContent || ctrl.onitemContent;
            var container = ctrl.element;
            var registereditems = ctrl.items;

            items.forEach(function (itemdata) {
                var item = new WinJSContrib.UI.MultiPassItem(ctrl, null, {
                    data: itemdata,
                    template: template,
                    className: className,
                    itemInvoked: itemInvoked,
                    onitemInit: onitemInit,
                    onitemContent: onitemContent
                });
                registereditems.push(item);
                container.appendChild(item.element);
            });
            //for (var i = 0 ; i < numItems; i++) {
            //    var itemdata = items[i];
            //    var item = new WinJSContrib.UI.MultiPassItem(ctrl, null, {
            //        data: itemdata,
            //        template: template,
            //        className: className,
            //        itemInvoked: itemInvoked,
            //        onitemInit: onitemInit,
            //        onitemContent: onitemContent
            //    });
            //    registereditems.push(item);
            //    container.appendChild(item.element);
            //}

            if (renderOptions.renderItems || !this.multipass) {
                ctrl.renderItemsContent();
            }
            //ctrl.element.style.display = '';
        },

        /**
         * check rendering of items, based on multipass configuration (force items on screen to render)
         */
        checkRendering: function () {
            var ctrl = this;
            if (ctrl.element && ctrl._scrollProcessor)
                ctrl._scrollProcessor();
        },

        /**
         * force rendering of all unrendered items
         */
        renderItemsContent: function () {
            var ctrl = this;
            if (!ctrl.element)
                return;

            ctrl.items.forEach(function (item) {
                if (!item.rendered) {
                    //setImmediate(function () {
                    item.render();
                    //});
                }
            });
            ctrl.allRendered = true;
        }
    });

    WinJSContrib.UI.MultiPassItem = WinJS.Class.define(
        /**
         * Item for multipass rendering
         * @class WinJSContrib.UI.MultiPassItem
         */
    function (renderer, element, options) {
        options = options || {};
        var item = this;
        item.renderer = renderer;
        item.element = element || document.createElement('DIV');
        item.element.className = item.element.className + ' ' + options.className + ' mcn-multipass-item unloaded win-disposable';
        item.element.winControl = item;

        item.itemInvoked = options.itemInvoked;
        item.itemDataPromise = WinJS.Promise.as(options.data);

        item.itemTemplate = options.template;
        item.rendered = false;
        item.onitemContent = options.onitemContent;
        if (options.onitemInit) {
            options.onitemInit(this);
        }
    },
    /**
     * @lends WinJSContrib.UI.MultiPassItem
     */
    {
        dispose: function () {
            var item = this;
            item.renderer = null;
            item.element = null;
            item.itemInvoked = null;
            item.itemData = null;
            item.itemDataPromise = null;
            item.itemTemplate = null;
            item.onitemContent = null;
        },

        /**
         * render item content
         */
        render: function (delayed) {
            var ctrl = this;

            if (ctrl.element && ctrl.itemTemplate && !ctrl.rendered) {

                ctrl.rendered = true;
                return ctrl._renderContent();
            }

            return WinJS.Promise.wrap(ctrl.contentElement);
        },

        /**
         * empty item and mark it as unrendered
         */
        empty: function () {
            var ctrl = this;
            if (ctrl.element && ctrl.rendered) {
                WinJSContrib.UI.untapAll(ctrl.element);
                ctrl.element.classList.remove('loaded');
                ctrl.element.innerHTML = '';
                ctrl.rendered = false;
            }
        },

        _renderItemContent: function (rendered) {
            var ctrl = this;
            if (!ctrl.element)
                return;

            ctrl.element.appendChild(rendered);

            if (ctrl.itemInvoked) {
                if (typeof ctrl.itemInvoked == 'string')
                    ctrl.itemInvoked = WinJSContrib.Utils.resolveMethod(ctrl.element, ctrl.itemInvoked);

                if (ctrl.itemInvoked) {
                    WinJSContrib.UI.tap(ctrl.element, function (arg) {
                        ctrl.itemInvoked(ctrl);
                    });
                }
            }

            if (ctrl.onitemContent) {
                ctrl.onitemContent(ctrl.itemData, rendered);
            }
            else if (ctrl.renderer.onitemContent) {
                ctrl.renderer.onitemContent(ctrl.itemData, rendered);
            }

            setImmediate(function () {
                if (ctrl.element) {
                    ctrl.element.classList.remove('unloaded');
                    ctrl.element.classList.add('loaded');
                }
            });

            ctrl.rendered = true;
            ctrl.contentElement = rendered;
            return rendered;
        },

        _renderContent: function () {
            var ctrl = this;

            if (ctrl.element && ctrl.itemTemplate) {
                return ctrl.itemDataPromise.then(function (data) {
                    ctrl.itemData = data;
                    return ctrl.itemTemplate.render(data).then(function (rendered) {
                        ctrl._renderItemContent(rendered);
                    });
                });
            }

            return WinJS.Promise.wrap();
        }
    });
})();