~jonas-drange/online-services-common-js/navbar-autocomplete

« back to all changes in this revision

Viewing changes to build/tablescroll/tablescroll-debug.js

  • Committer: Stephen Stewart
  • Date: 2014-02-22 23:57:25 UTC
  • mfrom: (18.1.2 trunk)
  • Revision ID: stephen.stewart@canonical.com-20140222235725-iw6f15t9umws19xd
mergeĀ lp:~stephen-stewart/online-services-common-js/remove-u1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
YUI.add('tablescroll', function (Y, NAME) {
2
 
 
3
 
"use strict";
4
 
/*
5
 
 * A scrollable datatable based on some of the functionality of
6
 
 * slickgrids but using tables.
7
 
 *
8
 
 */
9
 
 
10
 
Y.namespace('u1');
11
 
Y.u1.TableScroll = Y.Base.create("TableScroll", Y.Widget, [], {
12
 
    initializer: function() {
13
 
        // Set width of scrollbars
14
 
        this.set("nScrollbarWidth", this.getScrollbarWidth());
15
 
 
16
 
        // Init vars
17
 
        var oNodeContainer = this.get("oNodeContainer"),
18
 
            oRowSpec = this.get("oRowSpec");
19
 
 
20
 
        // Set attrs for table pieces
21
 
        this.set("oNodeTable", oNodeContainer.one("table"));
22
 
        this.set("oNodeTbody", oNodeContainer.one("tbody"));
23
 
        this.set("oNodeThead", oNodeContainer.one("thead"));
24
 
        this.set("oNodeTfoot", oNodeContainer.one("tfoot"));
25
 
 
26
 
        // Init a stylenode for us to manipulate to provide column updates.
27
 
        this.styleNode = new Y.u1.StyleNode();
28
 
 
29
 
        // Row Cache
30
 
        this.rows = {};
31
 
        // dataFetchFlag
32
 
        this.timer = null;
33
 
        // Data Cache
34
 
        this.dataCache = new Y.Cache({max: this.get("nMaxDataCacheItems"), uniqueKeys: true});
35
 
 
36
 
        if (!oRowSpec || !oRowSpec.preDataTemplate || !oRowSpec.postDataTemplate) {
37
 
            window.alert("Please define oRowSpec attribute as an object with 'preDataTemplate' and 'postDataTemplate' template node properties.");
38
 
            return;
39
 
        } else {
40
 
            this.set('sRowPreDataTemplate', Y.one(oRowSpec.preDataTemplate).getContent());
41
 
            this.set('sRowPostDataTemplate', Y.one(oRowSpec.postDataTemplate).getContent());
42
 
        }
43
 
    },
44
 
    // Debug Mode
45
 
    debugInit: function() {
46
 
        var oNodeDebugCont = Y.Node.create("<div class='isroll-debug'><dl></dl></div>"),
47
 
            oNodeDl = oNodeDebugCont.one("dl"),
48
 
            oNodeNumTrs = Y.Node.create('<dt>Number Trs: </dt><dd id="tablescroll-ntrs"></dd>'),
49
 
            oNodeNumTotalRecords = Y.Node.create('<dt>Total No. of Records: </dt><dd id="tablescroll-totrs"></dd>'),
50
 
            oNodeNumDataPages = Y.Node.create('<dt>Number Data Pages: </dt><dd id="tablescroll-ndps"></dd>'),
51
 
            oNodeCurDataPages = Y.Node.create('<dt>Current Data Page: </dt><dd id="tablescroll-cdp"></dd>'),
52
 
            oNodeLastScrollTop = Y.Node.create('<dt>LastScrollTop: </dt><dd id="tablescroll-lst"></dd>'),
53
 
            oNodeTotalScrollHeight = Y.Node.create('<dt>Total Scroll Height: </dt><dd id="tablescroll-th"></dd>'),
54
 
            oNodeRangeVisibleTop = Y.Node.create('<dt>Range visible Top: </dt><dd id="tablescroll-orv-top"></dd>'),
55
 
            oNodeRangeVisibleMiddle = Y.Node.create('<dt>Range visible Middle: </dt><dd id="tablescroll-orv-middle"></dd>'),
56
 
            oNodeRangeVisibleBottom = Y.Node.create('<dt>Range visible Bottom: </dt><dd id="tablescroll-orv-bottom"></dd>'),
57
 
            toAppend = [oNodeNumTrs, oNodeNumDataPages, oNodeCurDataPages,
58
 
                        oNodeLastScrollTop, oNodeNumTotalRecords, oNodeTotalScrollHeight,
59
 
                        oNodeRangeVisibleTop, oNodeRangeVisibleMiddle, oNodeRangeVisibleBottom],
60
 
            i;
61
 
 
62
 
        for (i in toAppend) {
63
 
            oNodeDl.append(toAppend[i]);
64
 
        }
65
 
 
66
 
        Y.Node.one("body").append(oNodeDebugCont);
67
 
        oNodeDebugCont.setStyles({
68
 
            "position": "fixed",
69
 
            "z-index": 10,
70
 
            "top": "5px",
71
 
            "left": "5px",
72
 
            "width": "300px",
73
 
            "height": "250px",
74
 
            "background": "#ccc"
75
 
        });
76
 
        this.after("nLastScrollTopChange", function(e) {
77
 
            Y.Node.one("#tablescroll-lst").setContent(e.newVal);
78
 
        }, this);
79
 
        this.after("aCurrentDataPageChange", function(e) {
80
 
            Y.Node.one("#tablescroll-cdp").setContent(e.newVal);
81
 
        }, this);
82
 
        this.after("nNumTrsChange", function(e) {
83
 
            Y.Node.one("#tablescroll-ntrs").setContent(e.newVal);
84
 
        }, this);
85
 
        this.after("nTotalDataPagesChange", function(e) {
86
 
            Y.Node.one("#tablescroll-ndps").setContent(e.newVal);
87
 
        }, this);
88
 
        this.after("nScrollbarHeightChange", function(e) {
89
 
            Y.Node.one("#tablescroll-th").setContent(e.newVal);
90
 
        }, this);
91
 
        this.after("nTotalRecordsChange", function(e) {
92
 
            Y.Node.one("#tablescroll-totrs").setContent(e.newVal);
93
 
        }, this);
94
 
        this.after("nRangeVisibleTopChange", function(e) {
95
 
            Y.Node.one("#tablescroll-orv-top").setContent(e.newVal);
96
 
        }, this);
97
 
        this.after("nRangeVisibleMiddleChange", function(e) {
98
 
            Y.Node.one("#tablescroll-orv-middle").setContent(e.newVal);
99
 
        }, this);
100
 
        this.after("nRangeVisibleBottomChange", function(e) {
101
 
            Y.Node.one("#tablescroll-orv-bottom").setContent(e.newVal);
102
 
        }, this);
103
 
        this.set("nNumTrs", this.get("oNodeContainer").all("tbody tr").size());
104
 
    },
105
 
    renderUI: function() {
106
 
 
107
 
        var bDebugMode = this.get("bDebugMode"),
108
 
            nTheadHeight,
109
 
            oNodeContainer = this.get("oNodeContainer"),
110
 
            oNodeTheadClone,
111
 
            oNodeTheadCloneDiv,
112
 
            oNodeTheadCloneTable,
113
 
            oNodeThead,
114
 
            oNodeTable;
115
 
 
116
 
        if (bDebugMode === true) {
117
 
            this.debugInit();
118
 
        }
119
 
 
120
 
        oNodeThead = this.get("oNodeThead");
121
 
        oNodeTable = this.get("oNodeTable");
122
 
 
123
 
        // Get an identifier on the table so we can directly attach styles.
124
 
        if (!oNodeContainer.get("id")) {
125
 
            oNodeContainer.set("id", Y.guid());
126
 
        }
127
 
 
128
 
        // Thead cloning
129
 
        oNodeTheadClone = oNodeThead.cloneNode(true);
130
 
        this.set("oNodeTheadClone", oNodeTheadClone);
131
 
 
132
 
        // Clone our "real" thead and place it above the main table of content
133
 
        // Yeah this feels wrong, but there's little other options and building this
134
 
        // entire thing out of lists feels even more of a violation.
135
 
        nTheadHeight = oNodeThead.get("offsetHeight");
136
 
        oNodeTheadCloneDiv = Y.Node.create("<div class='tablescroll-thead'><table></table></div>");
137
 
        if (Y.UA.ie === 7) {
138
 
            oNodeTheadCloneDiv.setStyle("zoom", 1);
139
 
        }
140
 
        oNodeTheadCloneTable = oNodeTheadCloneDiv.one("table");
141
 
        oNodeTheadCloneTable.append(oNodeTheadClone);
142
 
        oNodeContainer.insertBefore(oNodeTheadCloneDiv, oNodeContainer);
143
 
        oNodeThead.remove();
144
 
 
145
 
        // Set styles on the Container Node
146
 
        oNodeContainer.setStyles({
147
 
            "height": this.get('nContainerHeight'),
148
 
            "position": "relative",
149
 
            "overflowX": "hidden",
150
 
            "overflowY": "scroll"
151
 
        });
152
 
 
153
 
        // Set some initial styles on our table
154
 
        oNodeTable.setStyles({"height": -nTheadHeight,
155
 
                              "display": "block"});
156
 
    },
157
 
    bindUI: function() {
158
 
        var oNodeContainer = this.get("oNodeContainer"),
159
 
 
160
 
        // Scroll and mousewheel events - these are checked for actual
161
 
        // scrolling in the callback
162
 
        scrollFunc;
163
 
 
164
 
        if (Y.UA.mobile > 0) {
165
 
            scrollFunc = Y.throttle(Y.bind(this.onScroll, this), 100);
166
 
        } else {
167
 
            scrollFunc = Y.bind(this.onScroll, this);
168
 
        }
169
 
        Y.on("mousewheel", scrollFunc);
170
 
        oNodeContainer.on("scroll", scrollFunc);
171
 
 
172
 
        // Update the table height when we know what data we are dealing with.
173
 
        // The first time this happens will be after the first data set loads.
174
 
        this.after("nTotalRecordsChange", this.onTotalRecordsChange, this);
175
 
        // Watch for the page change so we can work out what data we need to get
176
 
        this.after("aCurrentDataPageChange", this.onDataPageChange, this);
177
 
 
178
 
        this.after('visibleChange', this.afterVisibleChange, this);
179
 
 
180
 
        Y.on("windowresize", Y.bind(this.syncUI, this));
181
 
 
182
 
    },
183
 
    syncUI: function() {
184
 
        if (this.get("visible") === true) {
185
 
            this.renderWidget();
186
 
            this.setColumnWidths();
187
 
        }
188
 
    },
189
 
    setColumnWidths: function() {
190
 
        var aColumnSpec = this.get("aColumnSpec"),
191
 
            aTdRules,
192
 
            aEllipsisRules,
193
 
            aStyleRules,
194
 
            nHorizPaddingAndBorder,
195
 
            nFlexWidth,
196
 
            nFlexWidthRemainder,
197
 
            nFixedWidthTotal = 0,
198
 
            nLastWidth = 0,
199
 
            nPercWidthTotal = 0,
200
 
            nPixelWidth,
201
 
            nVertPaddingAndBorder,
202
 
            nWidth = null,
203
 
            oNodeTable = this.get("oNodeTable"),
204
 
            nCurrentTableWidth = oNodeTable.get("offsetWidth"),
205
 
            oNodeListTds,
206
 
            oNodeCurTd,
207
 
            oNodeFirstTd,
208
 
            oNodeStyle,
209
 
            sContainerId = this.get("oNodeContainer").get("id"),
210
 
            sExtraRules,
211
 
            sOrigContent,
212
 
            i,
213
 
            j;
214
 
 
215
 
        for (i=0, j=aColumnSpec.length; i < j; i++) {
216
 
            nPixelWidth = aColumnSpec[i].width;
217
 
            nFlexWidth = aColumnSpec[i].flexwidth;
218
 
 
219
 
            if (typeof nPixelWidth !== "undefined") {
220
 
                nFixedWidthTotal += parseInt(nPixelWidth, 10);
221
 
            }
222
 
            if (typeof nFlexWidth !== "undefined") {
223
 
                nPercWidthTotal += parseInt(nFlexWidth, 10);
224
 
            }
225
 
        }
226
 
 
227
 
        // Get the remaining width we have to play with for the flexible columns.
228
 
        nFlexWidthRemainder = nCurrentTableWidth - nFixedWidthTotal;
229
 
 
230
 
        // Get NodeList containing first row of Tds.
231
 
        oNodeListTds = this.get("oNodeTbody").all("tr:first-child td");
232
 
        if (oNodeListTds.size() === 0) {
233
 
            return;
234
 
        }
235
 
 
236
 
        // Webkit seems to have trouble calculating the padding + border unless something is taking up vertical space
237
 
        // To get around that we drop a div set to the height of the row and then swap it back out.
238
 
        oNodeFirstTd = oNodeListTds.item(0);
239
 
        sOrigContent = oNodeFirstTd.getContent();
240
 
        oNodeFirstTd.setContent(Y.Node.create("<div style='height:" + this.get("nRowHeight") + "px'></div>"));
241
 
        nVertPaddingAndBorder = oNodeFirstTd.get("offsetHeight") - parseInt(oNodeFirstTd.getComputedStyle("height"), 10);
242
 
        oNodeFirstTd.setContent(sOrigContent);
243
 
 
244
 
        // Initial Styles - this truncates the table-cells + ensure the height matches that of the rows.
245
 
        aTdRules = [
246
 
            "white-space: nowrap;",
247
 
            "position: absolute;",
248
 
            "overflow: hidden;",
249
 
            "height: "+ (this.get("nRowHeight") - nVertPaddingAndBorder)+ "px;"
250
 
        ];
251
 
 
252
 
        aStyleRules = [
253
 
            "#" + sContainerId + " table" +  "{ table-layout:fixed }",
254
 
            "#" + sContainerId + " tr" +  "{ width: " + nCurrentTableWidth +"px; }",
255
 
            "#" + sContainerId + " td { "+ aTdRules.join(" ") +"}"
256
 
        ];
257
 
 
258
 
        // Some rules for displaying an ellipsis for truncated text.
259
 
        // Only used if ellipsis = true in aColumnSpec
260
 
        aEllipsisRules = [
261
 
            "text-overflow: ellipsis;",
262
 
            "-o-text-overflow: ellipsis;",
263
 
            "-ms-text-overflow: ellipsis;"
264
 
        ];
265
 
 
266
 
        for (i=0, j=aColumnSpec.length; i < j; i++) {
267
 
            nPixelWidth = aColumnSpec[i].width;
268
 
            oNodeCurTd = oNodeListTds.item(i);
269
 
 
270
 
            nFlexWidth = aColumnSpec[i].flexwidth;
271
 
            if (typeof nPixelWidth !== "undefined") {
272
 
                nWidth = nPixelWidth;
273
 
            }
274
 
            if (typeof nFlexWidth !== "undefined") {
275
 
                nWidth = (nFlexWidthRemainder / 100) * nFlexWidth;
276
 
            }
277
 
            // Allow for padding + border in the width
278
 
            nHorizPaddingAndBorder = (oNodeCurTd.get("offsetWidth") - parseInt(oNodeCurTd.getComputedStyle("width"), 10));
279
 
            nWidth = nWidth - nHorizPaddingAndBorder;
280
 
            sExtraRules = "";
281
 
            if (aColumnSpec[i].ellipsis === true) {
282
 
                sExtraRules = aEllipsisRules.join(" ");
283
 
            }
284
 
            aStyleRules.push("#" + sContainerId + " .c" + (i+1) + "{ left: " + nLastWidth +"px; width:" + nWidth +"px; "+sExtraRules+"}");
285
 
 
286
 
            // Allow for padding + border for the next left value
287
 
            nLastWidth += nWidth + nHorizPaddingAndBorder;
288
 
        }
289
 
 
290
 
        // Blow away an existing styleNode so we can recreate it.
291
 
        oNodeStyle = this.styleNode.get("styleNode");
292
 
        if (oNodeStyle !== null) {
293
 
            oNodeStyle.remove();
294
 
        }
295
 
        this.styleNode.create(aStyleRules);
296
 
        this.updateThWidths();
297
 
    },
298
 
    updateThWidths: function() {
299
 
        var tableCells = this.get("oNodeTbody").all("tr:first-child td"),
300
 
            tableHeadings = this.get("oNodeTheadClone").all("th"),
301
 
            elmWidth,
302
 
            numTds = tableCells.size();
303
 
        tableCells.each(function(elm, i){
304
 
            // Bail on the last th as we want this to fill all space available.
305
 
            if (i+1 === numTds) {
306
 
                return;
307
 
            }
308
 
            elmWidth = elm.get("offsetWidth");
309
 
            if (elmWidth > 0) {
310
 
                tableHeadings.item(i).set("offsetWidth", elmWidth);
311
 
            }
312
 
        }, this);
313
 
    },
314
 
    onTotalRecordsChange: function(e){
315
 
        if (e.newVal === e.prevVal) {
316
 
            return;
317
 
        }
318
 
        var nScrollbarHeight = e.newVal * this.get('nRowHeight');
319
 
        this.get("oNodeTable").setStyle("height", nScrollbarHeight);
320
 
        this.set("nScrollbarHeight", nScrollbarHeight);
321
 
        this.set("nTotalDataPages", Math.ceil(nScrollbarHeight / (this.get("nRowHeight") * this.get("nRecordLimit"))));
322
 
        if (typeof e.prevVal !== "undefined") {
323
 
            this.dataCache.flush();
324
 
            this.syncUI();
325
 
        }
326
 
    },
327
 
    onDataPageChange: function(e){
328
 
 
329
 
        // If prev value matches the new value then bail
330
 
        if (e.prevVal === e.newVal) {
331
 
            return;
332
 
        }
333
 
 
334
 
        var aCurrentDataPage = e.newVal;
335
 
 
336
 
        // If timer is already in progress abort it.
337
 
        if (this.timer !== null) {
338
 
            clearTimeout(this.timer);
339
 
        }
340
 
 
341
 
        // Set-up a timer to check we're still on the same page after a delay.
342
 
        // This helps to avoid fetching data unecessarily if we are scrolling fast.
343
 
        this.timer = setTimeout(Y.bind(function(o){
344
 
            var pageList = [],
345
 
                nRecordLimit = this.get("nRecordLimit"),
346
 
                aCurrentDataPage = o.currentPage,
347
 
                offset, limit, page,
348
 
                i,
349
 
                j;
350
 
 
351
 
            if (this.get("aCurrentDataPage") === aCurrentDataPage) {
352
 
 
353
 
                // Build a list of what pages we require - checking that the cache value is undefined
354
 
                // It will be null if fetching is already in progress in which case we won't fetch it.
355
 
                for (i=0, j=aCurrentDataPage.length; i < j; i++) {
356
 
                    page = aCurrentDataPage[i];
357
 
                    // yui3's cache returns null if the key isn't present
358
 
                    if (this.dataCache.retrieve("is-" + page) === null) {
359
 
                        pageList.push(page);
360
 
                    }
361
 
                }
362
 
 
363
 
                // If there's nothing to fetch then exit at this point.
364
 
                if (pageList.length === 0){
365
 
                    return;
366
 
                }
367
 
 
368
 
                // The first item in the list will always be the earliest page we need.
369
 
                offset = (pageList[0] - 1) * nRecordLimit;
370
 
                limit = (nRecordLimit * pageList.length);
371
 
 
372
 
                // Mark this as pending data - so we don't make subsequent reqs for it inbetween now
373
 
                // and actually receiving the data.
374
 
                for (i=0, j=pageList.length; i<j; i++) {
375
 
                    this.dataCache.add("is-" + pageList[i], "pending");
376
 
                }
377
 
                // Merge the updated data.
378
 
                this.fetchData(offset, limit);
379
 
            }
380
 
            this.timer = null;
381
 
        }, this, {
382
 
            "currentPage": aCurrentDataPage
383
 
        }), this.get("nDataFetchDelay"));
384
 
 
385
 
    },
386
 
    /* Called following a successful first data load */
387
 
    onInitialData: function(){
388
 
        // Run onScroll - this will deal with the issue of loading a
389
 
        // non-zero scroll start point.
390
 
        this.onScroll();
391
 
    },
392
 
    /* Abstracted method to get data - assumes the datasource method supplied
393
 
     * in the config accepts an object with success/failure/offset/limit keys
394
 
     *
395
 
     * offset is the record to start from
396
 
     * limit is the limit of the number of records to fetch.
397
 
     * */
398
 
    fetchData: function(offset, limit) {
399
 
        this.get("oDataSource")[this.get("sDataSourceMethod")]({
400
 
            "success": Y.rbind(this.onIOSuccess, this, offset, limit),
401
 
            "failure": Y.rbind(this.onIOFailure, this, offset, limit),
402
 
            "offset": offset,
403
 
            "limit": limit
404
 
        });
405
 
    },
406
 
    /* following a successful data load */
407
 
    onIOSuccess: function(e, offset, limit) {
408
 
        var nRecordLimit = this.get("nRecordLimit"),
409
 
            records = this.get("fRecordListSchema")(e),
410
 
            startPage = Math.ceil((offset / nRecordLimit) + 1),
411
 
            nTotalRecords;
412
 
 
413
 
        // We always check for changes to the total number of records.
414
 
        // So we can update for new data as it's added behind the scenes.
415
 
        nTotalRecords = this.get("fRecordTotalSchema")(e);
416
 
        this.set("nTotalRecords",  nTotalRecords);
417
 
        this.set("nTotalPages", nTotalRecords / this.get("nRecordLimit"));
418
 
 
419
 
        if (!this.get("bLoadedInitialData")){
420
 
            this.onInitialData(e);
421
 
        }
422
 
 
423
 
        // Stash the current dataset away under the page num as a key.
424
 
        // Handle the case where we fetch two pages at once for when
425
 
        // we are straddling data pages.
426
 
        if (limit > nRecordLimit) {
427
 
            this.dataCache.add("is-" + startPage, records.slice(0, nRecordLimit));
428
 
            this.dataCache.add("is-" + (startPage+1), records.slice(nRecordLimit, limit));
429
 
        } else {
430
 
            this.dataCache.add("is-" + startPage, records);
431
 
        }
432
 
 
433
 
        this.clearRows();
434
 
        this.renderWidget();
435
 
 
436
 
        if (!this.get("bLoadedInitialData")){
437
 
            this.setColumnWidths();
438
 
            this.set("bLoadedInitialData", true);
439
 
        }
440
 
 
441
 
    },
442
 
    /* Failure to get data
443
 
     * This could simply mean network is temporarily bad
444
 
     * TODO: This function should allow for a retry
445
 
     * TODO: This needs to clear null values in the cache so retries can happen.
446
 
     * */
447
 
    onIOFailure: function() {
448
 
        Y.log("oh dear something went wrong here");
449
 
    },
450
 
    /* Reset the Data cache */
451
 
    clearDataCache: function(){
452
 
        this.dataCache.flush();
453
 
    },
454
 
    // Clear out the rows
455
 
    clearRows: function() {
456
 
        for (var i in this.rows){
457
 
            this.removeRow(i);
458
 
        }
459
 
    },
460
 
    removeRow: function(index) {
461
 
        // Remove the node
462
 
        this.rows[index].remove(true);
463
 
        // Remove from the cache
464
 
        delete this.rows[index];
465
 
    },
466
 
    /* Based on the current data page get the data for this row num */
467
 
    getRowData: function(nRow) {
468
 
        var nRecordLimit = this.get("nRecordLimit"),
469
 
            page = Math.ceil((nRow + 1) / nRecordLimit),
470
 
            offsetIndex = nRow,
471
 
            dataset;
472
 
 
473
 
        page = page > 0 ? page : 1;
474
 
        if (page > 1) {
475
 
            offsetIndex = Math.floor((nRow - ((page - 1) * nRecordLimit)));
476
 
        }
477
 
 
478
 
        // Y.log("rownum: " + (nRow + 1) + " nRow: " + nRow + " page: " + page + " offsetIndex: " + offsetIndex  );
479
 
 
480
 
        dataset = this.dataCache.retrieve("is-" + page);
481
 
        if (dataset && dataset.response !== "pending" && dataset.response[offsetIndex]) {
482
 
            return dataset.response[offsetIndex];
483
 
        }
484
 
    },
485
 
    /* Renders the current view of the visible data */
486
 
    renderWidget: function() {
487
 
        var oNodeContainer = this.get("oNodeContainer"),
488
 
            nRowHeight = this.get("nRowHeight"),
489
 
            nScrollTop = oNodeContainer.get("scrollTop"),
490
 
            nContainerHeight = this.get("nContainerHeight"),
491
 
            nBufferHeight = nContainerHeight,
492
 
            oNodeTbody = this.get("oNodeTbody"),
493
 
            bDebugMode = this.get("bDebugMode"),
494
 
            nTotalRecords = this.get('nTotalRecords'),
495
 
            top = Math.floor((nScrollTop-nBufferHeight)/nRowHeight),
496
 
            bottom = Math.ceil((nScrollTop + nContainerHeight + nBufferHeight)/nRowHeight),
497
 
            middle,
498
 
            node,
499
 
            i;
500
 
 
501
 
 
502
 
        this.top = top = Math.max(0, top);
503
 
        this.bottom = bottom = Math.min(this.get("nScrollbarHeight")/nRowHeight, bottom);
504
 
        middle = Math.ceil(top + ((bottom - top)/2));
505
 
 
506
 
        // Work out what data page(s) we are currently accessing
507
 
        // This will work out the data page that the top of the rendered rows
508
 
        // falls in as well as the ones that the bottom of the rendered rows sees.
509
 
        // This way if we load and we're straddling two consecutive pages of data we
510
 
        // make one request for data spanning those two pages.
511
 
        this.set("aCurrentDataPage", this.getCurrentDataPage());
512
 
 
513
 
        if (bDebugMode) {
514
 
            this.set("nRangeVisibleTop", top);
515
 
            this.set("nRangeVisibleMiddle", middle);
516
 
            this.set("nRangeVisibleBottom", bottom);
517
 
        }
518
 
 
519
 
        // Trash Cached rows above and below
520
 
        // where we are.
521
 
        for (i in this.rows){
522
 
            if (i < top || i > bottom) {
523
 
                this.fire("preRowDestroyed", {"node": this.rows[i]});
524
 
                this.rows[i].remove(true);
525
 
                delete this.rows[i];
526
 
            }
527
 
        }
528
 
 
529
 
        // render + append + cache the rows between top and bottom values
530
 
        for (i=top; i<=bottom; i++) {
531
 
            if (i > (nTotalRecords - 1)) {
532
 
                continue;
533
 
            }
534
 
            if (!this.rows[i]) {
535
 
                node = this.rows[i] = this.renderRow(i);
536
 
            } else {
537
 
                node = this.rows[i];
538
 
            }
539
 
            oNodeTbody.append(node);
540
 
        }
541
 
 
542
 
        if (bDebugMode) {
543
 
            // Get the number of trs currently in play.
544
 
            this.set("nNumTrs", this.get("oNodeContainer").all("tbody tr").size());
545
 
        }
546
 
    },
547
 
    /* Renders an individual row */
548
 
    renderRow: function(nRow) {
549
 
        var currentRow = { "tablescroll-rownum": nRow + 1 },
550
 
            nRowHeight = this.get("nRowHeight"),
551
 
            node, oRowData, rowData, fCustomRowRenderer;
552
 
 
553
 
        oRowData = this.getRowData(nRow);
554
 
        if (oRowData){
555
 
            rowData = Y.merge(oRowData, currentRow);
556
 
            fCustomRowRenderer = this.get("fCustomRowRenderer");
557
 
            if (fCustomRowRenderer && typeof fCustomRowRenderer === "function") {
558
 
                node = fCustomRowRenderer(rowData, nRow, this);
559
 
            } else {
560
 
                node = Y.Node.create(Y.substitute(this.get("sRowPostDataTemplate"), rowData, function(k,v){return Y.Escape.html(v);}));
561
 
            }
562
 
            this.fire("rowDataRendered", { "node": node, "data": oRowData });
563
 
        } else {
564
 
            node = Y.Node.create(Y.substitute(this.get("sRowPreDataTemplate"), currentRow, function(k,v){return Y.Escape.html(v);}));
565
 
            this.fire("rowRendered", { "node": node });
566
 
        }
567
 
        node.setStyles({
568
 
            "top": nRow * nRowHeight + "px",
569
 
            "height": nRowHeight  + "px",
570
 
            "position": "absolute"
571
 
        });
572
 
        return node;
573
 
    },
574
 
    getCurrentDataPage: function(){
575
 
        var nRowHeight = this.get("nRowHeight"),
576
 
            nRecordLimit = this.get("nRecordLimit"),
577
 
            pxHeightDataPage = (nRecordLimit * nRowHeight),
578
 
            topPage, bottomPage, dataPages;
579
 
 
580
 
        topPage = Math.ceil((this.top * nRowHeight) / pxHeightDataPage);
581
 
        topPage = topPage > 0 ? topPage : 1;
582
 
        bottomPage = Math.ceil((this.bottom * nRowHeight) / pxHeightDataPage);
583
 
 
584
 
        topPage = topPage > 0 ? topPage : 1;
585
 
        bottomPage = bottomPage > 0 ? bottomPage : 1;
586
 
 
587
 
        // Only pass a list of one page if they're the same.
588
 
        if (topPage === bottomPage) {
589
 
            dataPages = [topPage];
590
 
        } else {
591
 
            dataPages =  [topPage, bottomPage ];
592
 
        }
593
 
        return dataPages;
594
 
    },
595
 
    onScroll: function(){
596
 
        if (this.get("visible") === true) {
597
 
            // Handle scroll events but ignoring those fired by viewport scrollig.
598
 
            // To work that out we check for changes in scrollTop on the container.
599
 
            var scrollTop = this.get("oNodeContainer").get("scrollTop");
600
 
 
601
 
            // When initialised nLastScrollTop is null so this will execute
602
 
            // on first load too.
603
 
            if (this.get("nLastScrollTop") !== scrollTop) {
604
 
                // Work out what page of data we are on.
605
 
                this.renderWidget();
606
 
            }
607
 
            this.set("nLastScrollTop", scrollTop);
608
 
        }
609
 
    },
610
 
    /* A way to work out a scrollbar width for most browser implementations
611
 
     * TODO: This is likely to fail for UAs that don't provide scrollbars until
612
 
     * interactions take place e.g. IOS
613
 
     * */
614
 
    getScrollbarWidth: function() {
615
 
        var scrollbarWidth = 0,
616
 
            div = Y.Node.create('<div style="width:50px;height:50px;position:absolute;top:-999em;left:999em;"><div style="height:100px; width: 100%"></div></div>'),
617
 
            w1, w2;
618
 
        Y.one('body').append(div);
619
 
        w1 = parseInt(div.one("div").get("offsetWidth"), 10);
620
 
        div.setStyle('overflow-y', 'scroll');
621
 
        w2 = parseInt(div.one("div").get("offsetWidth"), 10);
622
 
        div.remove();
623
 
        scrollbarWidth = w1 - w2;
624
 
        // Return something for a zero result - this means that for OSX where scrollbars are
625
 
        // invisible until interacted with we will still have some space for it.
626
 
        return (scrollbarWidth !== 0) ? scrollbarWidth : 18;
627
 
    }
628
 
}, {
629
 
    NAME: "tablescroll",
630
 
    ATTRS: {
631
 
        /* The column definition */
632
 
        aColumnSpec: { value: null },
633
 
        oRowSpec: { value: null },
634
 
        bDebugMode: { value: false },
635
 
        bLoadedInitialData: { value: false },
636
 
        /* function that can data the data returned by fGetData and
637
 
         * return the list of records to utilise */
638
 
        fRecordListSchema: { value: null },
639
 
        /* function that can take the data returned by dGetData and
640
 
         * return the total number of records */
641
 
        fRecordTotalSchema: { value: null },
642
 
        /* Custom row renderer */
643
 
        fCustomRowRenderer: { value: null },
644
 
        /* Height of container in pixels */
645
 
        nContainerHeight: { value: 400 },
646
 
        /* The current page of data we are on */
647
 
        aCurrentDataPage: { value: [1,1] },
648
 
        /* A delay to introduce before we fetch data so we
649
 
         * don't fetch things when we're scrolling fast */
650
 
        nDataFetchDelay: { value: 250 },
651
 
        /* The current scrollTop value */
652
 
        nLastScrollTop: { value: null },
653
 
        /* The limit of records to fetch */
654
 
        nRecordLimit: { value: 50 },
655
 
        /* The height of the table rows in px */
656
 
        nRowHeight: { value: 50 },
657
 
        /* The scrollbar width is set here */
658
 
        nScrollbarWidth: { value: null },
659
 
        /* The scrollbar height is set here - this is based on num records + row height */
660
 
        nScrollbarHeight: { value: null },
661
 
        /* The number of tr objects used */
662
 
        nNumTrs: { value: null },
663
 
        /* Number of datasets to keep in the cache */
664
 
        nMaxDataCacheItems: { value: 10 },
665
 
        /* The total num of data pages there are */
666
 
        nTotalDataPages: { value: null },
667
 
        nRangeVisibleTop: { value: null },
668
 
        nRangeVisibleMiddle: { value: null },
669
 
        nRangeVisibleBottom: { value: null },
670
 
        /* Node object of the widget container */
671
 
        oNodeContainer: {
672
 
            setter: function(sel) {
673
 
                var n = Y.one(sel);
674
 
                return n;
675
 
            }
676
 
        },
677
 
        /* Table parts as Nodes */
678
 
        oNodeTable: { writeOnce: true },
679
 
        oNodeTbody: { writeOnce: true },
680
 
        oNodeTfoot: { writeOnce: true },
681
 
        oNodeThead: { writeOnce: true },
682
 
        oNodeTheadClone: { writeOnce: true },
683
 
 
684
 
        /* A DataIO instance for getting data for the table */
685
 
        oDataSource: { value: null },
686
 
        /* The method of the dataIO instance to use for fetching data */
687
 
        sDataSourceMethod: { value: null },
688
 
        /* The row template content as a string */
689
 
        sRowPreDataTemplate: { value: null },
690
 
        sRowPostDataTemplate: { value: null }
691
 
    }
692
 
});
693
 
 
694
 
 
695
 
}, '@VERSION@', {
696
 
    "requires": [
697
 
        "base-base",
698
 
        "base-pluginhost",
699
 
        "base-build",
700
 
        "cache-base",
701
 
        "cache-offline",
702
 
        "cache-plugin",
703
 
        "escape",
704
 
        "stylenode",
705
 
        "widget-base",
706
 
        "widget-htmlparser",
707
 
        "widget-uievents",
708
 
        "widget-skin",
709
 
        "event-resize"
710
 
    ]
711
 
});