VirtualRenderStack

class webix.VirtualRenderStack()

Virtualrenderstack mixin

References

helpers
_event(), assert(), bind(), log().

Referenced by

views
dataview().

External references

Official documentation page.

Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
webix.VirtualRenderStack={
    $init:function(){
        webix.assert(this.render,"VirtualRenderStack :: Object must use RenderStack first");

        this._htmlmap={}; //init map of rendered elements

        //we need to repaint area each time when view resized or scrolling state is changed
        webix._event(this._viewobj,"scroll",webix.bind(this._render_visible_rows,this));
        if(webix.env.touch){
            this.attachEvent("onAfterScroll", webix.bind(this._render_visible_rows,this));
        }
        //here we store IDs of elemenst which doesn't loadede yet, but need to be rendered
        this._unrendered_area=[];
    },
    //return html object by item's ID. Can return null for not-rendering element
    getItemNode:function(search_id){
        //collection was filled in _render_visible_rows
        return this._htmlmap[search_id];
    },
    //adjust scrolls to make item visible
    showItem:function(id){
        var range = this._getVisibleRange();
        var ind = this.data.getIndexById(id);
        //we can't use DOM method for not-rendered-yet items, so fallback to pure math
        var dy = Math.floor(ind/range._dx)*range._y;
        var state = this.getScrollState();
        if (dy<state.y || dy + this._settings.height >= state.y + this._content_height)
            this.scrollTo(0, dy);
    },
    //repain self after changes in DOM
    //for add, delete, move operations - render is delayed, to minify performance impact
    render:function(id,data,type){
        if (!this.isVisible(this._settings.id) || this.$blockRender)
            return;

        if (webix.debug_render)
            webix.log("Render: "+this.name+"@"+this._settings.id);

        if (id){
            var cont = this.getItemNode(id);    //old html element
            switch(type){
                case "update":
                    if (!cont) return;
                    //replace old with new
                    var t = this._htmlmap[id] = this._toHTMLObject(data);
                    webix.html.insertBefore(t, cont);
                    webix.html.remove(cont);
                    break;
                default: // "move", "add", "delete"
                    /*
                        for all above operations, full repainting is necessary
                        but from practical point of view, we need only one repainting per thread
                        code below initiates double-thread-rendering trick
                    */
                    this._render_delayed();
                    break;
            }
        } else {
            //full repainting
            if (this.callEvent("onBeforeRender",[this.data])){
                this._htmlmap = {};                     //nulify links to already rendered elements
                this._render_visible_rows(null, true);
                // clear delayed-rendering, because we already have repaint view
                this._wait_for_render = false;
                this.callEvent("onAfterRender",[]);
            }
        }
    },
    //implement double-thread-rendering pattern
    _render_delayed:function(){
        //this flag can be reset from outside, to prevent actual rendering
        if (this._wait_for_render) return;
        this._wait_for_render = true;

        window.setTimeout(webix.bind(function(){
            this.render();
        },this),1);
    },
    //create empty placeholders, which will take space before rendering
    _create_placeholder:function(height){
        if(webix.env.maxHTMLElementSize)
            height = Math.min(webix.env.maxHTMLElementSize, height);
        var node = document.createElement("DIV");
            node.style.cssText = "height:"+height+"px; width:100%; overflow:hidden;";
        return node;
    },
    /*
        Methods get coordinatest of visible area and checks that all related items are rendered
        If, during rendering, some not-loaded items was detected - extra data loading is initiated.
        reset - flag, which forces clearing of previously rendered elements
    */
    _render_visible_rows:function(e,reset){
        this._unrendered_area=[]; //clear results of previous calls

        var viewport = this._getVisibleRange();    //details of visible view

        if (!this._dataobj.firstChild || reset){    //create initial placeholder - for all view space
            this._dataobj.innerHTML="";
            this._dataobj.appendChild(this._create_placeholder(viewport._max));
            //register placeholder in collection
            this._htmlrows = [this._dataobj.firstChild];
        }

        /*
            virtual rendering breaks all view on rows, because we know widht of item
            we can calculate how much items can be placed on single row, and knowledge
            of that, allows to calculate count of such rows

            each time after scrolling, code iterate through visible rows and render items
            in them, if they are not rendered yet

            both rendered rows and placeholders are registered in _htmlrows collection
        */

        //position of first visible row
        var t = viewport._from;

        while(t<=viewport._height){    //loop for all visible rows
            //skip already rendered rows
            while(this._htmlrows[t] && this._htmlrows[t]._filled && t<=viewport._height){
                t++;
            }
            //go out if all is rendered
            if (t>viewport._height) break;

            //locate nearest placeholder
            var holder = t;
            while (!this._htmlrows[holder]) holder--;
            var holder_row = this._htmlrows[holder];

            //render elements in the row
            var base = t*viewport._dx+(this.data.$min||0);    //index of rendered item
            if (base > (this.data.$max||Infinity)) break;    //check that row is in virtual bounds, defined by paging
            var nextpoint =  Math.min(base+viewport._dx-1,(this.data.$max?this.data.$max-1:Infinity));
            var node = this._create_placeholder(viewport._y);
            //all items in rendered row
            var range = this.data.getIndexRange(base, nextpoint);
            if (!range.length) break;

            var loading = { $template:"Loading" };
            for (var i=0; i<range.length; i++){
                if (!range[i])
                    this._unrendered_area.push(base+i);
                range[i] = this._toHTML(range[i]||loading);
            }

            node.innerHTML=range.join("");  //actual rendering
            for (var i=0; i < range.length; i++)                    //register all new elements for later usage in getItemNode
                this._htmlmap[this.data.getIdByIndex(base+i)]=node.childNodes[i];

            //correct placeholders
            var h = parseFloat(holder_row.style.height,10);
            var delta = (t-holder)*viewport._y;
            var delta2 = (h-delta-viewport._y);

            //add new row to the DOOM
            webix.html.insertBefore(node,delta?holder_row.nextSibling:holder_row,this._dataobj);
            this._htmlrows[t]=node;
            node._filled = true;

            /*
                if new row is at start of placeholder - decrease placeholder's height
                else if new row takes whole placeholder - remove placeholder from DOM
                else
                    we are inserting row in the middle of existing placeholder
                    decrease height of existing one, and add one more,
                    before the newly added row
            */
            if (delta <= 0 && delta2>0){
                holder_row.style.height = delta2+"px";
                this._htmlrows[t+1] = holder_row;
            } else {
                if (delta<0)
                    webix.html.remove(holder_row);
                else
                    holder_row.style.height = delta+"px";
                if (delta2>0){
                    var new_space = this._htmlrows[t+1] = this._create_placeholder(delta2);
                    webix.html.insertBefore(new_space,node.nextSibling,this._dataobj);
                }
            }


            t++;
        }

        //when all done, check for non-loaded items
        if (this._unrendered_area.length){
            //we have some data to load
            //detect borders
            var from = this._unrendered_area[0];
            var to = this._unrendered_area.pop()+1;
            if (to>from){
                //initiate data loading
                var count = to - from;
                if (this._maybe_loading_already(count, from)) return;

                count = Math.max(count, (this._settings.datafetch||this._settings.loadahead||0));
                this.loadNext(count, from);
            }
        }
    },
    //calculates visible view
    _getVisibleRange:function(){
        var state = this.getScrollState();
        var top = state.y;
        var width = this._content_width;
        var height = this._content_height;

        //size of single item
        var t = this.type;

        var dx = Math.floor(width/t.width)||1; //at least single item per row

        var min = Math.floor(top/t.height);                //index of first visible row
        var dy = Math.ceil((height+top)/t.height)-1;        //index of last visible row
        //total count of items, paging can affect this math
        var count = this.data.$max?(this.data.$max-this.data.$min):this.data.count();
        var max = Math.ceil(count/dx)*t.height;            //size of view in rows

        return { _from:min, _height:dy, _top:top, _max:max, _y:t.height, _dx:dx};
    },
    _cellPosition:function(id){
        var html = this.getItemNode(id);
        if (!html){
            this.showItem(id);
            this._render_visible_rows();
            html = this.getItemNode(id);
        }
        return {
            left:html.offsetLeft,
            top:html.offsetTop,
            height:html.offsetHeight,
            width:html.offsetWidth,
            parent:this._contentobj
        };
    }
};