javascriptalgorithmprogressive

Large list rendering in JavaScript


I am trying to render the list based on virtual rendering concept. I am facing some minor issues, but they are not blocking the behaviour. Here is the working fiddle http://jsfiddle.net/53N36/9/ and Here are my problems

  1. Last items are not visible, I assume some where I missed indexing.(Fixed, Please see the edit)
  2. How to calculate scrollPosition if I want to add custom scroll to this.
  3. Is this the best method or any other?

I have tested it with 700000 items and 70 items in chrome. Below is the code

(function () {
var list = (function () {
    var temp = [];
    for (var i = 0, l = 70; i < l; i++) {
        temp.push("list-item-" + (i + 1));
    }
    return temp;
}());

function listItem(text, id) {
    var _div = document.createElement('div');
    _div.innerHTML = text;
    _div.className = "listItem";
    _div.id = id;
    return _div;
}
var listHold = document.getElementById('listHolder'),
    ht = listHold.clientHeight,
    wt = listHold.clientWidth,
    ele = listItem(list[0], 'item0'),
    frag = document.createDocumentFragment();
listHold.appendChild(ele);
var ht_ele = ele.clientHeight,
    filled = ht_ele,
    filledIn = [0];
for (var i = 1, l = list.length; i < l; i++) {
    if (filled + ht_ele < ht) {
        filled += ht_ele;
        ele = listItem(list[i], 'item' + i);
        frag.appendChild(ele);
    } else {
        filledIn.push(i);
        break;
    }
}
listHold.appendChild(frag.cloneNode(true));
var elements = document.querySelectorAll('#listHolder .listItem');

function MouseWheelHandler(e) {
    var e = window.event || e;
    var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
    console.log(delta);
    //if(filledIn[0] != 0 && filledIn[0] != list.length){
    if (delta == -1) {
        var start = filledIn[0] + 1,
            end = filledIn[1] + 1,
            counter = 0;
        if (list[start] && list[end]) {
            for (var i = filledIn[0]; i < filledIn[1]; i++) {
                if (list[i]) {
                    (function (a) {
                        elements[counter].innerHTML = list[a];
                    }(i));
                    counter++;
                }
            }
            filledIn[0] = start;
            filledIn[1] = end;
        }
    } else {
        var start = filledIn[0] - 1,
            end = filledIn[1] - 1,
            counter = 0;
        if (list[start] && list[end]) {
            for (var i = start; i < end; i++) {
                if (list[i]) {
                    (function (a) {
                        elements[counter].innerHTML = list[a];
                    }(i));
                    counter++;
                }
            }
            filledIn[0] = start;
            filledIn[1] = end;
        }
    }
    //}
}
if (listHold.addEventListener) {
    listHold.addEventListener("mousewheel", MouseWheelHandler, false);
    listHold.addEventListener("DOMMouseScroll", MouseWheelHandler, false);
} else listHold.attachEvent("onmousewheel", MouseWheelHandler);
}());

Please suggest me on this.

EDIT: I have tried again and I am able to fix the indexing issue. http://jsfiddle.net/53N36/26/ But how can I calculate the scroll position based on the array list currently displayed.


Solution

  • Is this the best method or any other?
    I think something that would make this much easier is not to try to handle scrolling yourself.
    In this fiddle I show that you can let the browser handle scrolling for you, even though we are using virtual rendering.

    Using .scrollTop I detect where the browser thinks the user is looking, and I draw in items based on that.
    You'll note that if you set hidescrollbar to false and the user uses it to scroll, my method still runs fine.

    Therefore, to calculate scroll position you can just use .scrollTop.
    And as for custom scrolling, just make sure you influence the .scrollTop of #listHolder and recall refreshWindow()

    CODE FROM FIDDLE

    (function () {
        //CHANGE THESE IF YOU WANT
        var hidescrollbar = false;
        var numberofitems = 700000;
        //
    
        var holder = document.getElementById('listHolder');
        var view = null;
    
        //get the height of a single item
        var itemHeight = (function() {
            //generate a fake item
            var div = document.createElement('div');
            div.className = 'listItem';
            div.innerHTML = 'testing height';
            holder.appendChild(div);
    
            //get its height and remove it
            var output = div.offsetHeight;
            holder.removeChild(div);
            return output;
        })();
    
        //faster to instantiate empty-celled array
        var items = Array(numberofitems);
        //fill it in with data
        for (var index = 0; index < items.length; ++index)
            items[index] = 'item-' + index;
    
        //displays a suitable number of items
        function refreshWindow() {
            //remove old view
            if (view != null)
                holder.removeChild(view);
            //create new view
            view = holder.appendChild(document.createElement('div'));
    
            var firstItem = Math.floor(holder.scrollTop / itemHeight);
            var lastItem = firstItem + Math.ceil(holder.offsetHeight / itemHeight) + 1;
            if (lastItem + 1 >= items.length)
                lastItem = items.length - 1;
    
            //position view in users face
            view.id = 'view';
            view.style.top = (firstItem * itemHeight) + 'px';
    
            var div;
            //add the items
            for (var index = firstItem; index <= lastItem; ++index) {
                div = document.createElement('div');
                div.innerHTML = items[index];
                div.className = "listItem";
                view.appendChild(div);
            }
            console.log('viewing items ' + firstItem + ' to ' + lastItem);
        }
    
        refreshWindow();
    
        document.getElementById('heightForcer').style.height = (items.length * itemHeight) + 'px';
        if (hidescrollbar) {
            //work around for non-chrome browsers, hides the scrollbar
            holder.style.width = (holder.offsetWidth * 2 - view.offsetWidth) + 'px';
        }
    
        function delayingHandler() {
            //wait for the scroll to finish
            setTimeout(refreshWindow, 10);
        }
        if (holder.addEventListener)
            holder.addEventListener("scroll", delayingHandler, false);
        else
            holder.attachEvent("onscroll", delayingHandler);
    }());
    
    <div id="listHolder">
        <div id="heightForcer"></div>
    </div>
    
    html, body {
        width:100%;
        height:100%;
        padding:0;
        margin:0
    }
    body{
        overflow:hidden;
    }
    .listItem {
        border:1px solid gray;
        padding:0 5px;
        width: margin : 1px 0px;
    }
    #listHolder {
        position:relative;
        height:100%;
        width:100%;
        background-color:#CCC;
        box-sizing:border-box;
        overflow:auto;
    }
    /*chrome only
    #listHolder::-webkit-scrollbar{
        display:none;
    }*/
    #view{
        position:absolute;
        width:100%;
    }