2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('datatable-scroll', function(Y) {
11 * Extends DataTable base to enable x,y, and xy scrolling.
13 * @submodule datatable-scroll
20 YgetClassName = Y.ClassNameManager.getClassName,
21 DATATABLE = "datatable",
22 CLASS_HEADER = YgetClassName(DATATABLE, "hd"),
23 CLASS_BODY = YgetClassName(DATATABLE, "bd"),
24 CLASS_SCROLLABLE = YgetClassName(DATATABLE, "scrollable"),
25 CONTAINER_HEADER = '<div class="'+CLASS_HEADER+'"></div>',
26 CONTAINER_BODY = '<div class="'+CLASS_BODY+'"></div>',
27 TEMPLATE_TABLE = '<table></table>';
30 * Adds scrolling to DataTable.
31 * @class DataTableScroll
32 * @extends Plugin.Base
34 function DataTableScroll() {
35 DataTableScroll.superclass.constructor.apply(this, arguments);
38 Y.mix(DataTableScroll, {
41 NAME: "dataTableScroll",
46 * @description The width for the table. Set to a string (ex: "200px", "20em") if you want the table to scroll in the x direction.
58 * @description The height for the table. Set to a string (ex: "200px", "20em") if you want the table to scroll in the y-direction.
71 * @description The scrolling direction for the table.
80 var w = this.get('width'),
81 h = this.get('height');
100 * @description The hexadecimal colour value to set on the top-right of the table if a scrollbar exists.
102 * @attribute COLOR_COLUMNFILLER
106 COLOR_COLUMNFILLER: {
108 validator: YLang.isString,
109 setter: function(param) {
110 if (this._headerContainerNode) {
111 this._headerContainerNode.setStyle('backgroundColor', param);
118 Y.extend(DataTableScroll, Y.Plugin.Base, {
121 * @description The table node created in datatable-base
123 * @property _parentTableNode
127 _parentTableNode: null,
131 * @description The THEAD node which resides within the table node created in datatable-base
133 * @property _parentTheadNode
137 _parentTheadNode: null,
141 * @description The TBODY node which resides within the table node created in datatable-base
143 * @property _parentTbodyNode
147 _parentTbodyNode: null,
151 * @description The TBODY Message node which resides within the table node created in datatable-base
153 * @property _parentMsgNode
157 _parentMsgNode: null,
161 * @description The contentBox specified for the datatable in datatable-base
163 * @property _parentContainer
167 _parentContainer: null,
171 * @description The DIV node that contains all the scrollable elements (a table with a tbody on it)
173 * @property _bodyContainerNode
177 _bodyContainerNode: null,
181 * @description The DIV node that contains a table with a THEAD in it (which syncs its horizontal scroll with the _bodyContainerNode above)
183 * @property _headerContainerNode
187 _headerContainerNode: null,
190 //--------------------------------------
192 //--------------------------------------
196 initializer: function(config) {
197 var dt = this.get("host");
198 this._parentContainer = dt.get('contentBox');
199 this._parentContainer.addClass(CLASS_SCROLLABLE);
203 /////////////////////////////////////////////////////////////////////////////
205 // Set up Table Nodes
207 /////////////////////////////////////////////////////////////////////////////
210 * @description Set up methods to fire after host methods execute
212 * @method _setUpNodes
215 _setUpNodes: function() {
217 this.afterHostMethod("_addTableNode", this._setUpParentTableNode);
218 this.afterHostMethod("_addTheadNode", this._setUpParentTheadNode);
219 this.afterHostMethod("_addTbodyNode", this._setUpParentTbodyNode);
220 this.afterHostMethod("_addMessageNode", this._setUpParentMessageNode);
221 //this.beforeHostMethod('renderUI', this._removeCaptionNode);
222 this.afterHostMethod("renderUI", this.renderUI);
223 this.afterHostMethod("syncUI", this.syncUI);
225 if (this.get('_scroll') !== 'x') {
226 this.afterHostMethod('_attachTheadThNode', this._attachTheadThNode);
227 this.afterHostMethod('_attachTbodyTdNode', this._attachTbodyTdNode);
233 * @description Stores the main <table> node provided by the host as a private property
235 * @method _setUpParentTableNode
238 _setUpParentTableNode: function() {
239 this._parentTableNode = this.get('host')._tableNode;
244 * @description Stores the main <thead> node provided by the host as a private property
246 * @method _setUpParentTheadNode
249 _setUpParentTheadNode: function() {
250 this._parentTheadNode = this.get('host')._theadNode;
254 * @description Stores the main <tbody> node provided by the host as a private property
256 * @method _setUpParentTbodyNode
259 _setUpParentTbodyNode: function() {
260 this._parentTbodyNode = this.get('host')._tbodyNode;
265 * @description Stores the main <tbody> message node provided by the host as a private property
267 * @method _setUpParentMessageNode
270 _setUpParentMessageNode: function() {
271 this._parentMsgNode = this.get('host')._msgNode;
274 /////////////////////////////////////////////////////////////////////////////
278 /////////////////////////////////////////////////////////////////////////////
281 * @description Primary rendering method that takes the datatable rendered in
282 * the host, and splits it up into two separate <divs> each containing two
283 * separate tables (one containing the head and one containing the body).
284 * This method fires after renderUI is called on datatable-base.
289 renderUI: function() {
290 //Y.Profiler.start('render');
291 this._createBodyContainer();
292 this._createHeaderContainer();
293 this._setContentBoxDimensions();
294 //Y.Profiler.stop('render');
295 //console.log(Y.Profiler.getReport("render"));
300 * @description Post rendering method that is responsible for creating a column
301 * filler, and performing width and scroll synchronization between the <th>
302 * elements and the <td> elements.
303 * This method fires after syncUI is called on datatable-base
309 //Y.Profiler.start('sync');
310 this._removeCaptionNode();
313 //Y.Profiler.stop('sync');
314 //console.log(Y.Profiler.getReport("sync"));
319 * @description Remove the caption created in base. Scrolling datatables dont support captions.
321 * @method _removeCaptionNode
324 _removeCaptionNode: function() {
325 this.get('host')._captionNode.remove();
326 //Y.DataTable.Base.prototype.createCaption = function(v) {/*do nothing*/};
327 //Y.DataTable.Base.prototype._uiSetCaption = function(v) {/*do nothing*/};
331 * @description Adjusts the width of the TH and the TDs to make sure that the two are in sync
333 * Implementation Details:
334 * Compares the width of the TH liner div to the the width of the TD node. The TD liner width
335 * is not actually used because the TD often stretches past the liner if the parent DIV is very
336 * large. Measuring the TD width is more accurate.
338 * Instead of measuring via .get('width'), 'clientWidth' is used, as it returns a number, whereas
339 * 'width' returns a string, In IE6, 'clientWidth' is not supported, so 'offsetWidth' is used.
340 * 'offsetWidth' is not as accurate on Chrome,FF as 'clientWidth' - thus the need for the fork.
342 * @method _syncWidths
345 _syncWidths: function() {
346 var th = YNode.all('#'+this._parentContainer.get('id')+' .yui3-datatable-hd table thead th'), //nodelist of all THs
347 td = YNode.one('#'+this._parentContainer.get('id')+' .yui3-datatable-bd table .yui3-datatable-data').get('firstChild').get('children'), //nodelist of all TDs in 1st row
350 thWidth, tdWidth, thLiner, tdLiner,
352 //stylesheet = new YStyleSheet('columnsSheet'),
356 This for loop goes through the first row of TDs in the table.
357 In a table, the width of the row is equal to the width of the longest cell in that column.
358 Therefore, we can observe the widths of the cells in the first row only, as they will be the same in all the cells below (in each respective column)
360 for (i=0, len = th.size(); i<len; i++) {
362 //className = '.'+td.item(i).get('classList')._nodes[0];
363 //If a width has not been already set on the TD:
364 //if (td.item(i).get('firstChild').getStyle('width') === "auto") {
366 //Get the liners for the TH and the TD cell in question
367 thLiner = th.item(i).get('firstChild'); //TODO: use liner API - how? this is a node.
368 tdLiner = td.item(i).get('firstChild');
371 If browser is not IE - get the clientWidth of the Liner div and the TD.
372 Note: We are not getting the width of the TDLiner, we are getting the width of the actual cell.
373 Why? Because when the table is set to auto width, the cell will grow to try to fit the table in its container.
374 The liner could potentially be much smaller than the cell width.
376 TODO: Explore if there is a better way using only LINERS widths
379 thWidth = thLiner.get('clientWidth'); //TODO: this should actually be done with getComputedStyle('width') but this messes up columns. Explore this option.
380 tdWidth = td.item(i).get('clientWidth');
383 //IE wasn't recognizing clientWidths, so we are using offsetWidths.
384 //TODO: should use getComputedStyle('width') because offsetWidth will screw up when padding is changed.
386 thWidth = thLiner.get('offsetWidth');
387 tdWidth = td.item(i).get('offsetWidth');
388 //thWidth = parseFloat(thLiner.getComputedStyle('width').split('px')[0]);
389 //tdWidth = parseFloat(td.item(i).getComputedStyle('width').split('px')[0]); /* TODO: for some reason, using tdLiner.get('clientWidth') doesn't work - why not? */
392 //if TH is bigger than TD, enlarge TD Liner
393 if (thWidth > tdWidth) {
394 tdLiner.setStyle('width', (thWidth - 20 + 'px'));
395 //thLiner.setStyle('width', (tdWidth - 20 + 'px'));
396 //stylesheet.set(className,{'width': (thWidth - 20 + 'px')});
399 //if TD is bigger than TH, enlarge TH Liner
400 else if (tdWidth > thWidth) {
401 thLiner.setStyle('width', (tdWidth - 20 + 'px'));
402 tdLiner.setStyle('width', (tdWidth - 20 + 'px')); //if you don't set an explicit width here, when the width is set in line 368, it will auto-shrink the widths of the other cells (because they dont have an explicit width)
403 //stylesheet.set(className,{'width': (tdWidth - 20 + 'px')});
410 //stylesheet.enable();
415 * @description Adds the approriate width to the liner divs of the TH nodes before they are appended to DOM
417 * @method _attachTheadThNode
420 _attachTheadThNode: function(o) {
421 var w = o.column.get('width') || 'auto';
424 o.th.get('firstChild').setStyles({width: w, overflow:'hidden'}); //TODO: use liner API but liner is undefined here (not created?)
430 * @description Adds the appropriate width to the liner divs of the TD nodes before they are appended to DOM
432 * @method _attachTbodyTdNode
435 _attachTbodyTdNode: function(o) {
436 var w = o.column.get('width') || 'auto';
439 o.td.get('firstChild').setStyles({width: w, overflow: 'hidden'}); //TODO: use liner API but liner is undefined here (not created?)
440 //o.td.setStyles({'width': w, 'overflow': 'hidden'});
446 * @description Creates the body DIV that contains all the data.
448 * @method _createBodyContainer
451 _createBodyContainer: function() {
452 var bd = YNode.create(CONTAINER_BODY),
453 onScrollFn = Y.bind("_onScroll", this);
455 this._bodyContainerNode = bd;
456 this._setStylesForTbody();
458 bd.appendChild(this._parentTableNode);
459 this._parentContainer.appendChild(bd);
460 bd.on('scroll', onScrollFn);
464 * @description Creates the DIV that contains a <table> with all the headers.
466 * @method _createHeaderContainer
469 _createHeaderContainer: function() {
470 var hd = YNode.create(CONTAINER_HEADER),
471 tbl = YNode.create(TEMPLATE_TABLE);
473 this._headerContainerNode = hd;
475 //hd.setStyle('backgroundColor',this.get("COLOR_COLUMNFILLER"));
476 this._setStylesForThead();
477 tbl.appendChild(this._parentTheadNode);
479 this._parentContainer.prepend(hd);
484 * @description Creates styles for the TBODY based on what type of table it is.
486 * @method _setStylesForTbody
489 _setStylesForTbody: function() {
490 var dir = this.get('_scroll'),
491 w = this.get('width') || "",
492 h = this.get('height') || "",
493 el = this._bodyContainerNode,
494 styles = {width:"", height:h};
497 //X-Scrolling tables should not have a Y-Scrollbar so overflow-y is hidden. THe width on x-scrolling tables must be set by user.
498 styles.overflowY = 'hidden';
501 else if (dir === 'y') {
502 //Y-Scrolling tables should not have a X-Scrollbar so overflow-x is hidden. The width isn't neccessary because it can be auto.
503 styles.overflowX = 'hidden';
506 else if (dir === 'xy') {
511 //scrolling is set to 'null' - ie: width and height are not set. Don't have any type of scrolling.
512 styles.overflowX = 'hidden';
513 styles.overflowY = 'hidden';
517 el.setStyles(styles);
523 * @description Creates styles for the THEAD based on what type of datatable it is.
525 * @method _setStylesForThead
528 _setStylesForThead: function() {
529 var w = this.get('width') || "",
530 el = this._headerContainerNode;
533 el.setStyles({'width': w, 'overflow': 'hidden'});
538 * @description Sets an auto width on the content box if it doesn't exist or if its a y-datatable.
540 * @method _setContentBoxDimensions
543 _setContentBoxDimensions: function() {
545 if (this.get('_scroll') === 'y' || (!this.get('width'))) {
546 this._parentContainer.setStyle('width', 'auto');
551 /////////////////////////////////////////////////////////////////////////////
555 /////////////////////////////////////////////////////////////////////////////
558 * @description Ensures that scrolling is synced across the two tables
563 _onScroll: function() {
564 this._headerContainerNode.set('scrollLeft', this._bodyContainerNode.get('scrollLeft'));
568 * @description Syncs padding around scrollable tables, including Column header right-padding
569 * and container width and height.
571 * @method _syncScroll
574 _syncScroll : function() {
577 this._syncScrollOverhang();
580 this._headerContainerNode.set('scrollLeft', this._bodyContainerNode.get('scrollLeft'));
582 if(!this.get("width")) {
584 document.body.style += '';
590 * @description Snaps container width for y-scrolling tables.
592 * @method _syncScrollY
595 _syncScrollY : function() {
596 var tBody = this._parentTbodyNode,
597 tBodyContainer = this._bodyContainerNode,
599 // X-scrolling not enabled
600 if(!this.get("width")) {
601 // Snap outer container width to content
602 w = (tBodyContainer.get('scrollHeight') > tBodyContainer.get('clientHeight')) ?
603 // but account for y-scrollbar since it is visible
604 (tBody.get('parentNode').get('clientWidth') + 19) + "px" :
605 // no y-scrollbar, just borders
606 (tBody.get('parentNode').get('clientWidth') + 2) + "px";
607 this._parentContainer.setStyle('width', w);
612 * @description Snaps container height for x-scrolling tables in IE. Syncs message TBODY width.
613 * Taken from YUI2 ScrollingDataTable.js
615 * @method _syncScrollX
618 _syncScrollX: function() {
619 var tBody = this._parentTbodyNode,
620 tBodyContainer = this._bodyContainerNode,
622 this._headerContainerNode.set('scrollLeft', this._bodyContainerNode.get('scrollLeft'));
624 if(!this.get('height') && (YUA.ie)) {
625 w = (tBodyContainer.get('scrollWidth') > tBodyContainer.get('offsetWidth')) ?
626 (tBody.get('parentNode').get('offsetHeight') + 18) + "px" :
627 tBody.get('parentNode').get('offsetHeight') + "px";
629 tBodyContainer.setStyle('height', w);
632 if (tBody.get('rows').length === 0) {
633 this._parentMsgNode.get('parentNode').setStyle('width', this._parentTheadNode.get('parentNode').get('offsetWidth')+'px');
636 this._parentMsgNode.get('parentNode').setStyle('width', "");
642 * @description Adds/removes Column header overhang as necesary.
643 * Taken from YUI2 ScrollingDataTable.js
645 * @method _syncScrollOverhang
648 _syncScrollOverhang: function() {
649 var tBodyContainer = this._bodyContainerNode,
652 //when its both x and y scrolling
653 if ((tBodyContainer.get('scrollHeight') > tBodyContainer.get('clientHeight')) || (tBodyContainer.get('scrollWidth') > tBodyContainer.get('clientWidth'))) {
657 this._setOverhangValue(padding);
659 //After the widths have synced, there is a wrapping issue in the headerContainer in IE6. The header does not span the full
660 //length of the table (does not cover all of the y-scrollbar). By adding this line in when there is a y-scroll, the header will span correctly.
661 //TODO: this should not really occur on this.get('_scroll') === y - it should occur when scrollHeight > clientHeight, but clientHeight is not getting recognized in IE6?
662 if (YUA.ie !== 0 && this.get('_scroll') === 'y' && this._bodyContainerNode.get('scrollHeight') > this._bodyContainerNode.get('offsetHeight'))
664 this._headerContainerNode.setStyle('width', this._parentContainer.get('width'));
670 * @description Sets Column header overhang to given width.
671 * Taken from YUI2 ScrollingDataTable.js with minor modifications
673 * @method _setOverhangValue
674 * @param nBorderWidth {Number} Value of new border for overhang.
677 _setOverhangValue: function(borderWidth) {
678 var host = this.get('host'),
679 cols = host.get('columnset').get('definitions'),
680 //lastHeaders = cols[cols.length-1] || [],
682 value = borderWidth + "px solid " + this.get("COLOR_COLUMNFILLER"),
683 children = YNode.all('#'+this._parentContainer.get('id')+ ' .' + CLASS_HEADER + ' table thead th');
685 children.item(len-1).setStyle('borderRight', value);
690 Y.namespace("Plugin").DataTableScroll = DataTableScroll;
697 }, '3.3.0' ,{requires:['datatable-base','plugin','stylesheet']});