3
Copyright 2012 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
7
YUI.add('datatable-core', function(Y) {
10
The core implementation of the `DataTable` and `DataTable.Base` Widgets.
13
@submodule datatable-core
17
var INVALID = Y.Attribute.INVALID_VALUE,
20
isFunction = Lang.isFunction,
21
isObject = Lang.isObject,
22
isArray = Lang.isArray,
23
isString = Lang.isString,
24
isNumber = Lang.isNumber,
25
fromTemplate = Lang.sub,
33
// TODO: add this to Y.Object
39
// Not doing a hasOwnProperty check on purpose
47
_API docs for this extension are included in the DataTable class._
49
Class extension providing the core API and structure for the DataTable Widget.
51
Use this class extension with Widget or another Base-based superclass to create
52
the basic DataTable API and composing class structure.
54
Notable about this architecture is that rendering and UI event management for
55
the header, body, and footer of the table are deferred to configurable classes
56
in the `headerView`, `bodyView`, and `footerView` attributes. In this extension
57
they have no default values, requiring implementers to supply their own classes
58
to render the table content.
64
Table = Y.namespace('DataTable').Core = function () {};
68
Columns to include in the rendered table.
70
If omitted, the attributes on the configured `recordType` or the first item
71
in the `data` collection will be used as a source.
73
This attribute takes an array of strings or objects (mixing the two is
74
fine). Each string or object is considered a column to be rendered.
75
Strings are converted to objects, so `columns: ['first', 'last']` becomes
76
`columns: [{ key: 'first' }, { key: 'last' }]`.
78
DataTable.Core only concerns itself with a few properties of columns.
79
All other properties are for use by the `headerView`, `bodyView`,
80
`footerView`, and any class extensions or plugins on the final class or
81
instance. See the descriptions of the view classes and feature class
82
extensions and plugins for details on the specific properties they read or
83
add to column definitions.
85
The properties that are referenced or assigned are:
87
* `key` - Used to identify the record field/attribute containing content for
88
this column. Also used to create a default Model if no `recordType` or
89
`data` are provided during construction. If `name` is not specified, this
90
is assigned to the `_id` property (with added incrementer if the key is
91
used by multiple columns).
92
* `children` - Traversed to initialize nested column objects
93
* `name` - Used in place of, or in addition to, the `key`. Useful for
94
columns that aren't bound to a field/attribute in the record data. This
95
is assigned to the `_id` property.
96
* `id` - For backward compatibility. Implementers can specify the id of
97
the header cell. This should be avoided, if possible, to avoid the
98
potential for creating DOM elements with duplicate IDs.
99
* `field` - For backward compatibility. Implementers should use `name`.
100
* `_id` - Assigned unique-within-this-instance id for a column. By order
101
of preference, assumes the value of `name`, `key`, `id`, or `_yuid`.
102
This is used by the `bodyView` and `headerView` as well as feature module
103
as a means to identify a specific column without ambiguity (such as
104
multiple columns using the same `key`.
105
* `_yuid` - Guid stamp assigned to the column object.
106
* `_parent` - Assigned to all child columns, referencing their parent
110
@type {Object[]|String[]}
111
@default (from `recordType` ATTRS or first item in the `data`)
115
// TODO: change to setter to coerce Columnset?
117
getter: '_getColumns'
121
Model subclass to use as the `model` for the ModelList stored in the `data`
124
If not provided, it will try really hard to figure out what to use. The
125
following attempts will be made to set a default value:
127
1. If the `data` attribute is set with a ModelList instance and its `model`
128
property is set, that will be used.
129
2. If the `data` attribute is set with a ModelList instance, and its
130
`model` property is unset, but it is populated, the `ATTRS` of the
131
`constructor of the first item will be used.
132
3. If the `data` attribute is set with a non-empty array, a Model subclass
133
will be generated using the keys of the first item as its `ATTRS` (see
134
the `_createRecordClass` method).
135
4. If the `columns` attribute is set, a Model subclass will be generated
136
using the columns defined with a `key`. This is least desirable because
137
columns can be duplicated or nested in a way that's not parsable.
138
5. If neither `data` nor `columns` is set or populated, a change event
139
subscriber will listen for the first to be changed and try all over
142
@attribute recordType
144
@default (see description)
148
setter: '_setRecordType',
153
The collection of data records to display. This attribute is a pass
154
through to a `data` property, which is a ModelList instance.
156
If this attribute is passed a ModelList or subclass, it will be assigned to
157
the property directly. If an array of objects is passed, a new ModelList
158
will be created using the configured `recordType` as its `model` property
159
and seeded with the array.
161
Retrieving this attribute will return the ModelList stored in the `data`
165
@type {ModelList|Object[]}
166
@default `new ModelList()`
176
The class or object to use for rendering the `<thead>` and column headers
177
for the table. This attribute is responsible for populating the the
178
instance's `head` property.
180
If a class constructor (function) is passed, an instance of that clas will
181
be created at `render()` time and assigned to `this.head`. If an object is
182
passed, `head` will be set immediately.
184
Valid objects or classes will have a `render()` method, though it is
185
recommended that they be subclasses of `Y.Base` or `Y.View`. If the object
186
or class supports events, its `addTarget()` method will be called to bubble
187
its events to this instance.
189
The core implementaion does not define a default `headerView`. Classes
190
built from this extension should define a default.
192
@attribute headerView
193
@type {Function|Object}
197
validator: '_validateView',
202
The class or object to use for rendering the `<tfoot>` and any relevant
203
content for it. This attribute is responsible for populating the the
204
instance's `foot` property.
206
If a class constructor (function) is passed, an instance of that clas will
207
be created at `render()` time and assigned to `this.foot`. If an object is
208
passed, `foot` will be set immediately.
210
Valid objects or classes will have a `render()` method, though it is
211
recommended that they be subclasses of `Y.Base` or `Y.View`. If the object
212
or class supports events, its `addTarget()` method will be called to bubble
213
its events to this instance.
215
The core implementaion does not define a default `footerView`. Classes
216
built from this extension should define a default if appropriate.
218
@attribute footerView
219
@type {Function|Object}
223
validator: '_validateView',
228
The class or object to use for rendering the `<tbody>` or `<tbody>`s and
229
all data row content for the table. This attribute is responsible for
230
populating the the instance's `body` property.
232
If a class constructor (function) is passed, an instance of that clas will
233
be created at `render()` time and assigned to `this.body`. If an object is
234
passed, `body` will be set immediately.
236
Valid objects or classes will have a `render()` method, though it is
237
recommended that they be subclasses of `Y.Base` or `Y.View`. If the object
238
or class supports events, its `addTarget()` method will be called to bubble
239
its events to this instance.
241
The core implementaion does not define a default `bodyView`. Classes
242
built from this extension should define a default.
245
@type {Function|Object}
249
validator: '_validateView',
254
Content for the `<table summary="ATTRIBUTE VALUE HERE">`. Values assigned
255
to this attribute will be HTML escaped for security.
259
@default '' (empty string)
264
// For paranoid reasons, the value is escaped on its way in because
265
// rendering can be based on string concatenation.
266
setter: Y.Escape.html
270
HTML content of an optional `<caption>` element to appear above the table.
271
Leave this config unset or set to a falsy value to remove the caption.
275
@default '' (empty string)
283
Deprecated as of 3.5.0. Passes through to the `data` attribute.
285
WARNING: `get('recordset')` will NOT return a Recordset instance as of
286
3.5.0. This is a break in backward compatibility.
289
@type {Object[]|Recordset}
290
@deprecated Use the `data` attribute
294
setter: '_setRecordset',
295
getter: '_getRecordset',
300
Deprecated as of 3.5.0. Passes through to the `columns` attribute.
302
If a Columnset object is passed, its raw object and array column data will
303
be extracted for use.
305
WARNING: `get('columnset')` will NOT return a Columnset instance as of
306
3.5.0. This is a break in backward compatibility.
309
@type {Object[]|Columnset}
310
@deprecated Use the `columns` attribute
314
setter: '_setColumnset',
315
getter: '_getColumnset',
320
Y.mix(Table.prototype, {
321
// -- Instance properties -------------------------------------------------
324
The HTML template used to create the caption Node if the `caption`
327
@property CAPTION_TEMPLATE
329
@default '<caption class="{className}"/>'
332
CAPTION_TEMPLATE: '<caption class="{className}"/>',
335
The HTML template used to create the table Node.
337
@property TABLE_TEMPLATE
339
@default '<table cellspacing="0" class="{className}"/>'
342
TABLE_TEMPLATE : '<table cellspacing="0" class="{className}"/>',
345
HTML template used to create table's `<tbody>` if configured with a
348
@property TBODY_TEMPLATE
350
@default '<tbody class="{className}"/>'
353
TBODY_TEMPLATE: '<tbody class="{className}"/>',
356
Template used to create the table's `<tfoot>` if configured with a
359
@property TFOOT_TEMPLATE
361
@default '<tfoot class="{className}"/>'
365
'<tfoot class="{className}"/>',
368
Template used to create the table's `<thead>` if configured with a
371
@property THEAD_TEMPLATE
373
@default '<thead class="{className}"/>'
377
'<thead class="{className}"/>',
380
The object or instance of the class assigned to `bodyView` that is
381
responsible for rendering and managing the table's `<tbody>`(s) and its
386
@default undefined (initially unset)
392
The object or instance of the class assigned to `footerView` that is
393
responsible for rendering and managing the table's `<tfoot>` and its
398
@default undefined (initially unset)
404
The object or instance of the class assigned to `headerView` that is
405
responsible for rendering and managing the table's `<thead>` and its
410
@default undefined (initially unset)
416
The ModelList that manages the table's data.
420
@default undefined (initially unset)
425
// -- Public methods ------------------------------------------------------
427
Pass through to `delegate()` called from the `contentBox`.
430
@param type {String} the event type to delegate
431
@param fn {Function} the callback function to execute. This function
432
will be provided the event object for the delegated event.
433
@param spec {String|Function} a selector that must match the target of the
434
event or a function to test target and its parents for a match
435
@param context {Object} optional argument that specifies what 'this' refers to
436
@param args* {any} 0..n additional arguments to pass on to the callback
437
function. These arguments will be added after the event object.
438
@return {EventHandle} the detach handle
441
delegate: function () {
442
var contentBox = this.get('contentBox');
444
return contentBox.delegate.apply(contentBox, arguments);
448
Returns the `<td>` Node from the given row and column index. Alternately,
449
the `seed` can be a Node. If so, the nearest ancestor cell is returned.
450
If the `seed` is a cell, it is returned. If there is no cell at the given
451
coordinates, `null` is returned.
453
Optionally, include an offset array or string to return a cell near the
454
cell identified by the `seed`. The offset can be an array containing the
455
number of rows to shift followed by the number of columns to shift, or one
456
of "above", "below", "next", or "previous".
458
<pre><code>// Previous cell in the previous row
459
var cell = table.getCell(e.target, [-1, -1]);
462
var cell = table.getCell(e.target, 'next');
463
var cell = table.getCell(e.taregt, [0, 1];</pre></code>
465
This is actually just a pass through to the `bodyView` instance's method
469
@param {Number[]|Node} seed Array of row and column indexes, or a Node that
470
is either the cell itself or a descendant of one.
471
@param {Number[]|String} [shift] Offset by which to identify the returned
476
getCell: function (seed, shift) {
477
return this.body && this.body.getCell &&
478
this.body.getCell.apply(this.body, arguments);
482
Gets the column configuration object for the given key, name, or index. For
483
nested columns, `name` can be an array of indexes, each identifying the index
484
of that column in the respective parent's "children" array.
486
If you pass a column object, it will be returned.
488
For columns with keys, you can also fetch the column with
489
`instance.get('columns.foo')`.
492
@param {String|Number|Number[]} name Key, "name", index, or index array to
494
@return {Object} the column configuration object
497
getColumn: function (name) {
498
var col, columns, i, len, cols;
500
if (isObject(name) && !isArray(name)) {
501
// TODO: support getting a column from a DOM node - this will cross
502
// the line into the View logic, so it should be relayed
504
// Assume an object passed in is already a column def
507
col = this.get('columns.' + name);
514
columns = this.get('columns');
516
if (isNumber(name) || isArray(name)) {
517
name = toArray(name);
520
for (i = 0, len = name.length - 1; cols && i < len; ++i) {
521
cols = cols[name[i]] && cols[name[i]].children;
524
return (cols && cols[name[i]]) || null;
531
Returns the Model associated to the record `id`, `clientId`, or index (not
532
row index). If none of those yield a Model from the `data` ModelList, the
533
arguments will be passed to the `bodyView` instance's `getRecord` method
536
If no Model can be found, `null` is returned.
539
@param {Number|String|Node} seed Record `id`, `clientId`, index, Node, or
540
identifier for a row or child element
544
getRecord: function (seed) {
545
var record = this.data.getById(seed) || this.data.getByClientId(seed);
548
if (isNumber(seed)) {
549
record = this.data.item(seed);
552
if (!record && this.body && this.body.getRecord) {
553
record = this.body.getRecord.apply(this.body, arguments);
557
return record || null;
561
Returns the `<tr>` Node from the given row index, Model, or Model's
562
`clientId`. If the rows haven't been rendered yet, or if the row can't be
563
found by the input, `null` is returned.
565
This is actually just a pass through to the `bodyView` instance's method
569
@param {Number|String|Model} id Row index, Model instance, or clientId
573
getRow: function (id) {
574
return this.body && this.body.getRow &&
575
this.body.getRow.apply(this.body, arguments);
579
Updates the UI with the current attribute state. Fires the `renderHeader`,
580
`renderBody`, and `renderFooter` events;
585
syncUI: function () {
586
this._uiSetCaption(this.get('caption'));
587
this._uiSetSummary(this.get('summary'));
590
this.fire('renderHeader', { view: this.head });
593
this.fire('renderBody', { view: this.body });
596
this.fire('renderFooter', { view: this.foot });
600
// -- Protected and private properties and methods ------------------------
603
Configuration object passed to the class constructor in `bodyView` during
606
This property is set by the `_initViewConfig` method at instantiation.
608
@property _bodyConfig
610
@default undefined (initially unset)
617
A map of column key to column configuration objects parsed from the
622
@default undefined (initially unset)
629
Configuration object passed to the class constructor in `footerView` during
632
This property is set by the `_initViewConfig` method at instantiation.
634
@property _footerConfig
636
@default undefined (initially unset)
640
//_footerConfig: null,
643
Configuration object passed to the class constructor in `headerView` during
646
This property is set by the `_initViewConfig` method at instantiation.
648
@property _headerConfig
650
@default undefined (initially unset)
654
//_headerConfig: null,
657
The Node instance of the table containing the data rows. This is set when
658
the table is rendered. It may also be set by progressive enhancement,
659
though this extension does not provide the logic to parse from source.
663
@default undefined (initially unset)
670
Configuration object used as the prototype of `_headerConfig`,
671
`_bodyConfig`, and `_footerConfig`. Add properties to this object if you
672
want them in all three of the other config objects.
674
This property is set by the `_initViewConfig` method at instantiation.
676
@property _viewConfig
678
@default undefined (initially unset)
685
Updates the `_columnMap` property in response to changes in the `columns`
688
@method _afterColumnsChange
689
@param {EventFacade} e The `columnsChange` event object
693
_afterColumnsChange: function (e) {
694
this._setColumnMap(e.newVal);
695
this._setDisplayColumns(e.newVal);
699
Updates the `modelList` attributes of the rendered views in response to the
700
`data` attribute being assigned a new ModelList.
702
@method _afterDataChange
703
@param {EventFacade} e the `dataChange` event
707
_afterDataChange: function (e) {
708
var modelList = e.newVal;
711
this.head.set('modelList', modelList);
714
this.body.set('modelList', modelList);
717
this.foot.set('modelList', modelList);
722
Subscribes to attribute change events to update the UI.
728
bindUI: function () {
729
// TODO: handle widget attribute changes
730
this.after('dataChange', Y.bind('_afterDataChange', this));
734
Creates a Model subclass from an array of attribute names or an object of
735
attribute definitions. This is used to generate a class suitable to
736
represent the data passed to the `data` attribute if no `recordType` is
739
@method _createRecordClass
740
@param {String[]|Object} attrs Names assigned to the Model subclass's
741
`ATTRS` or its entire `ATTRS` definition object
746
_createRecordClass: function (attrs) {
749
if (isArray(attrs)) {
752
for (i = 0, len = attrs.length; i < len; ++i) {
753
ATTRS[attrs[i]] = {};
755
} else if (isObject(attrs)) {
759
return Y.Base.create('record', Y.Model, [], null, { ATTRS: ATTRS });
763
Creates the `<table>`.
766
@return {Node} The `<table>` node
770
_createTable: function () {
771
return Y.Node.create(fromTemplate(this.TABLE_TEMPLATE, {
772
className: this.getClassName('table')
777
Creates a `<tbody>` node from the `TBODY_TEMPLATE`.
783
_createTBody: function () {
784
return Y.Node.create(fromTemplate(this.TBODY_TEMPLATE, {
785
className: this.getClassName('data')
790
Creates a `<tfoot>` node from the `TFOOT_TEMPLATE`.
796
_createTFoot: function () {
797
return Y.Node.create(fromTemplate(this.TFOOT_TEMPLATE, {
798
className: this.getClassName('footer')
803
Creates a `<thead>` node from the `THEAD_TEMPLATE`.
809
_createTHead: function () {
810
return Y.Node.create(fromTemplate(this.THEAD_TEMPLATE, {
811
className: this.getClassName('columns')
816
Calls `render()` on the `bodyView` class instance.
818
@method _defRenderBodyFn
819
@param {EventFacade} e The renderBody event
823
_defRenderBodyFn: function (e) {
828
Calls `render()` on the `footerView` class instance.
830
@method _defRenderFooterFn
831
@param {EventFacade} e The renderFooter event
835
_defRenderFooterFn: function (e) {
840
Calls `render()` on the `headerView` class instance.
842
@method _defRenderHeaderFn
843
@param {EventFacade} e The renderHeader event
847
_defRenderHeaderFn: function (e) {
852
Renders the `<table>` and, if there are associated Views, the `<thead>`,
853
`<tfoot>`, and `<tbody>` (empty until `syncUI`).
855
Assigns the generated table nodes to the `_tableNode`, `_theadNode`,
856
`_tfootNode`, and `_tbodyNode` properties. Assigns the instantiated Views
857
to the `head`, `foot`, and `body` properties.
860
@method _defRenderTableFn
861
@param {EventFacade} e The renderTable event
865
_defRenderTableFn: function (e) {
868
this._tableNode = this._createTable();
871
config = flatten(e.headerConfig || {});
872
config.container = this._theadNode = this._createTHead();
874
this.head = new e.headerView(config);
875
this.head.addTarget(this);
877
this._tableNode.insertBefore(this._theadNode,
878
this._tableNode.one('> tfoot, > tbody'));
882
config = flatten(e.footerConfig || {});
883
config.container = this._tfootNode = this._createTFoot();
885
this.foot = new e.footerView(config);
886
this.foot.addTarget(this);
888
this._tableNode.insertBefore(this._tfootNode,
889
this._tableNode.one('> tbody'));
893
config = flatten(e.bodyConfig || {});
894
config.container = this._tbodyNode = this._createTBody();
896
this.body = new e.bodyView(config);
897
this.body.addTarget(this);
899
this._tableNode.append(this._tbodyNode);
904
Contains column configuration objects for those columns believed to be intended for display in the `<tbody>`. Populated by `_setDisplayColumns`.
906
@property _displayColumns
908
@value undefined (initially not set)
912
//_displayColumns: null,
915
The getter for the `columns` attribute. Returns the array of column
916
configuration objects if `instance.get('columns')` is called, or the
917
specific column object if `instance.get('columns.columnKey')` is called.
920
@param {Object[]} columns The full array of column objects
921
@param {String} name The attribute name requested
922
(e.g. 'columns' or 'columns.foo');
926
_getColumns: function (columns, name) {
927
// Workaround for an attribute oddity (ticket #2529254)
928
// getter is expected to return an object if get('columns.foo') is called.
929
// Note 'columns.' is 8 characters
930
return name.length > 8 ? this._columnMap : columns;
934
Relays the `get()` request for the deprecated `columnset` attribute to the
937
THIS BREAKS BACKWARD COMPATIBILITY. 3.4.1 and prior implementations will
938
expect a Columnset instance returned from `get('columnset')`.
940
@method _getColumnset
941
@param {Object} ignored The current value stored in the `columnset` state
942
@param {String} name The attribute name requested
943
(e.g. 'columnset' or 'columnset.foo');
944
@deprecated This will be removed with the `columnset` attribute in a future
949
_getColumnset: function (_, name) {
950
return this.get(name.replace(/^columnset/, 'columns'));
954
The getter for the `data` attribute. Returns the ModelList stored in the
955
`data` property. If the ModelList is not yet set, it returns the current
956
raw data (presumably an empty array or `undefined`).
959
@param {Object[]|ModelList} val The current data stored in the attribute
963
_getData: function (val) {
964
return this.data || val;
968
Initializes the `_columnMap` property from the configured `columns`
969
attribute. If `columns` is not set, but `recordType` is, it uses the
970
`ATTRS` of that class. If neither are set, it temporarily falls back to an
971
empty array. `_initRecordType` will call back into this method if it finds
972
the `columnMap` empty.
978
_initColumns: function () {
979
var columns = this.get('columns'),
980
recordType = this.get('recordType');
982
// Default column definition from the configured recordType
984
// TODO: merge superclass attributes up to Model?
985
columns = (recordType && recordType.ATTRS) ?
986
keys(recordType.ATTRS) : [];
988
this.set('columns', columns, { silent: true });
991
this._setColumnMap(columns);
993
this._setDisplayColumns(columns);
997
Initializes the instance's `data` property from the value of the `data`
998
attribute. If the attribute value is a ModelList, it is assigned directly
999
to `this.data`. If it is an array, a ModelList is created, its `model`
1000
property is set to the configured `recordType` class, and it is seeded with
1001
the array data. This ModelList is then assigned to `this.data`.
1007
_initData: function () {
1008
var data = this.get('data'),
1011
if (isArray(data)) {
1012
recordType = this.get('recordType');
1015
data = new Y.ModelList();
1017
// _initRecordType is run before this, so recordType will be set
1018
// if the data array had any records. Otherwise, values is an
1019
// empty array, so no need to call reset();
1021
data.model = recordType;
1022
data.reset(values, { silent: true });
1025
// Make sure the attribute state object contains the ModelList.
1026
// TODO: maybe better would be to purge the attribute state value?
1027
this.set('data', data, { silent: true });
1032
this.data.addTarget(this);
1036
Publishes core events.
1042
_initEvents: function () {
1044
// Y.bind used to allow late binding for method override support
1047
defaultFn: Y.bind('_defRenderTableFn', this)
1050
defaultFn: Y.bind('_defRenderHeaderFn', this)
1053
defaultFn: Y.bind('_defRenderBodyFn', this)
1056
defaultFn: Y.bind('_defRenderFooterFn', this)
1062
Initializes the columns, `recordType` and data ModelList.
1068
initializer: function () {
1069
this._initColumns();
1071
this._initRecordType();
1075
this._initViewConfig();
1079
this.after('columnsChange', this._afterColumnsChange);
1081
// FIXME: this needs to be added to Widget._buildCfg.custom
1083
BIND: this._UI_ATTRS.BIND.concat(['caption', 'summary']),
1084
SYNC: this._UI_ATTRS.SYNC.concat(['caption', 'summary'])
1089
If the `recordType` attribute is not set, this method attempts to set a
1092
It tries the following methods to determine a default:
1094
1. If the `data` attribute is set with a ModelList with a `model` property,
1096
2. If the `data` attribute is set with a non-empty ModelList, the
1097
`constructor` of the first item is used.
1098
3. If the `data` attribute is set with a non-empty array and the first item
1099
is a Base subclass, its constructor is used.
1100
4. If the `data` attribute is set with a non-empty array a custom Model
1101
subclass is generated using the keys of the first item as its `ATTRS`.
1102
5. If the `_columnMap` property has keys, a custom Model subclass is
1103
generated using those keys as its `ATTRS`.
1105
Of none of those are successful, it subscribes to the change events for
1106
`columns`, `recordType`, and `data` to try again.
1108
If defaulting the `recordType` and the current `_columnMap` property is
1109
empty, it will call `_initColumns`.
1111
@method _initRecordType
1115
_initRecordType: function () {
1116
var data, columns, recordType, handle, columnKeys;
1118
if (!this.get('recordType')) {
1119
data = this.get('data');
1120
columns = this._columnMap;
1122
// Use the ModelList's specified Model class
1124
recordType = data.model;
1126
// Or if not configured, use the construct of the first Model
1127
} else if (data.size && data.size()) {
1128
recordType = data.model = data.item(0).constructor;
1130
// Or if the data is an array, build a class from the first item
1131
} else if (isArray(data) && data.length) {
1132
recordType = (data[0].constructor.ATTRS) ?
1133
data[0].constructor :
1134
this._createRecordClass(keys(data[0]));
1136
// Or if the columns were defined, build a class from the keys
1138
columnKeys = keys(columns);
1140
if (columnKeys.length) {
1141
recordType = this._createRecordClass(columnKeys);
1146
this.set('recordType', recordType, { silent: true });
1148
if (!columns || !columns.length) {
1149
this._initColumns();
1152
// FIXME: Edge case race condition with
1153
// new DT({ on/after: { <any of these changes> } }) OR
1154
// new DT().on( <any of these changes> )
1155
// where there's not enough info to assign this.data.model
1156
// at construction. The on/constructor subscriptions will be
1157
// executed before this subscription.
1158
handle = this.after(
1159
['columnsChange', 'recordTypeChange','dataChange'],
1161
// manually batch detach rather than manage separate
1162
// subs in case the change was inadequate to populate
1163
// recordType. But subs must be detached because the
1164
// subscriber recurses to _initRecordType, which would
1165
// result in duplicate subs.
1168
if (!this.data.model) {
1169
// FIXME: resubscribing if there's still not enough
1170
// info to populate recordType will place the new
1171
// subs later in the callback queue, opening the
1172
// race condition even more.
1173
this._initRecordType();
1175
// If recordType isn't set yet, _initRecordType
1176
// will have recreated this subscription.
1177
this.data.model = this.get('recordType');
1185
Initializes the `_viewConfig`, `_headerConfig`, `_bodyConfig`, and
1186
`_footerConfig` properties with the configuration objects that will be
1187
passed to the constructors of the `headerView`, `bodyView`, and
1190
Extensions can add to the config objects to deliver custom parameters at
1191
view instantiation. `_viewConfig` is used as the prototype of the other
1192
three config objects, so properties added here will be inherited by all
1195
@method _initViewConfig
1199
_initViewConfig: function () {
1200
this._viewConfig = {
1202
cssPrefix: this._cssPrefix
1205
// Use prototypal inheritance to share common configs from _viewConfig
1206
this._headerConfig = Y.Object(this._viewConfig);
1207
this._bodyConfig = Y.Object(this._viewConfig);
1208
this._footerConfig = Y.Object(this._viewConfig);
1212
Iterates the array of column configurations to capture all columns with a
1213
`key` property. Columns that are represented as strings will be replaced
1214
with objects with the string assigned as the `key` property. If a column
1215
has a `children` property, it will be iterated, adding any nested column
1216
keys to the returned map. There is no limit to the levels of nesting.
1218
All columns are assigned a `_yuid` stamp and `_id` property corresponding
1219
to the column's configured `name` or `key` property with any spaces
1220
replaced with dashes. If the same `name` or `key` appears in multiple
1221
columns, subsequent appearances will have their `_id` appended with an
1222
incrementing number (e.g. if column "foo" is included in the `columns`
1223
attribute twice, the first will get `_id` of "foo", and the second an `_id`
1224
of "foo1"). Columns that are children of other columns will have the
1225
`_parent` property added, assigned the column object to which they belong.
1227
The result is an object map with column keys as the property name and the
1228
corresponding column object as the associated value.
1230
@method _parseColumns
1231
@param {Object[]|String[]} columns The array of column names or
1232
configuration objects to scan
1236
_parseColumns: function (columns) {
1240
function genId(name) {
1241
// Sanitize the name for use in generated CSS classes.
1242
// TODO: is there more to do for other uses of _id?
1243
name = name.replace(/\s+/, '-');
1246
name += (keys[name]++);
1254
function process(cols, parent) {
1255
var i, len, col, key, yuid;
1257
for (i = 0, len = cols.length; i < len; ++i) {
1260
if (isString(col)) {
1261
// Update the array entry as well, so the attribute state array
1262
// contains the same objects.
1263
cols[i] = col = { key: col };
1266
yuid = Y.stamp(col);
1268
// For backward compatibility
1270
// Implementers can shoot themselves in the foot by setting
1271
// this config property to a non-unique value
1275
// Field is now known as "name" to avoid confusion with data
1276
// fields or schema.resultFields
1277
col.name = col.field;
1281
col._parent = parent;
1286
if (isArray(col.children)) {
1287
// Allow getColumn for parent columns if they have a name
1289
map[genId(col.name)] = col;
1292
process(col.children, col);
1296
// First in wins for multiple columns with the same key
1297
// because the first call to genId will return the same key,
1298
// which will then be overwritten by the subsequent
1299
// same-keyed column. So table.getColumn(key) would return
1300
// the last same-keyed column.
1301
if (key && !map[key]) {
1305
// Unique id based on the column's configured name or key,
1306
// falling back to the yuid. Duplicates will have a counter
1307
// added to the end.
1308
col._id = genId(col.name || col.key || col.id);
1310
//TODO: named columns can conflict with keyed columns
1322
Builds the table and attaches it to the DOM. This requires the host class
1323
to provide a `contentBox` attribute. This is typically provided by Widget.
1329
renderUI: function () {
1330
var contentBox = this.get('contentBox'),
1334
// _viewConfig is the prototype for _headerConfig et al.
1335
this._viewConfig.columns = this.get('columns');
1336
this._viewConfig.modelList = this.data;
1338
this.fire('renderTable', {
1339
headerView : this.get('headerView'),
1340
headerConfig: this._headerConfig,
1342
bodyView : this.get('bodyView'),
1343
bodyConfig : this._bodyConfig,
1345
footerView : this.get('footerView'),
1346
footerConfig: this._footerConfig
1349
table = this._tableNode;
1352
// off DOM or in an existing node attached to a different parentNode
1353
if (!table.inDoc() || !table.ancestor().compareTo(contentBox)) {
1354
contentBox.append(table);
1356
} else { Y.log('Problem rendering DataTable: table not created', 'warn', 'datatable'); // On the same line to allow builder to strip the else clause
1358
} else { Y.log('Problem rendering DataTable: contentBox not found', 'warn', 'datatable'); // On the same line to allow builder to strip the else clause
1363
Assigns the `_columnMap` property with the parsed results of the array of
1364
column definitions passed.
1366
@method _setColumnMap
1367
@param {Object[]|String[]} columns the raw column configuration objects or
1372
_setColumnMap: function (columns) {
1373
this._columnMap = this._parseColumns(columns);
1377
Relays attribute assignments of the deprecated `columnset` attribute to the
1378
`columns` attribute. If a Columnset is object is passed, its basic object
1381
@method _setColumnset
1382
@param {Array|Columnset} val The columnset value to relay
1383
@deprecated This will be removed with the deprecated `columnset` attribute
1388
_setColumnset: function (val) {
1389
if (val && Y.Columnset && val instanceof Y.Columnset) {
1390
val = val.get('definitions');
1393
this.set('columns', val);
1395
return isArray(val) ? val : INVALID;
1399
Accepts an object with `each` and `getAttrs` (preferably a ModelList or
1400
subclass) or an array of data objects. If an array is passes, it will
1401
create a ModelList to wrap the data. In doing so, it will set the created
1402
ModelList's `model` property to the class in the `recordType` attribute,
1403
which will be defaulted if not yet set.
1405
If the `data` property is already set with a ModelList, passing an array as
1406
the value will call the ModelList's `reset()` method with that array rather
1407
than replacing the stored ModelList wholesale.
1409
Any non-ModelList-ish and non-array value is invalid.
1415
_setData: function (val) {
1422
if (!this.data.model && val.length) {
1423
// FIXME: this should happen only once, but this is a side
1424
// effect in the setter. Bad form, but I need the model set
1425
// before calling reset()
1426
this.set('recordType', keys(val[0]));
1429
this.data.reset(val);
1431
// Return the instance ModelList to avoid storing unprocessed
1432
// data in the state and their vivified Model representations in
1433
// the instance's data property. Decreases memory consumption.
1436
// else pass through the array data, but don't assign this.data
1437
// Let the _initData process clean up.
1438
} else if (val && val.each && val.getAttrs) {
1440
// TODO: return true to decrease memory footprint?
1449
Stores an array of columns intended for display in the `_displayColumns`
1450
property. This method assumes that if a column configuration object does
1451
not have children, it is a display column.
1453
@method _setDisplayColumns
1454
@param {Object[]} columns Column config array to extract display columns from
1458
_setDisplayColumns: function (columns) {
1459
function extract(cols) {
1463
for (i = 0, len = cols.length; i < len; ++i) {
1467
display.push.apply(display, extract(col.children));
1476
this._displayColumns = extract(columns);
1480
Relays the value assigned to the deprecated `recordset` attribute to the
1481
`data` attribute. If a Recordset instance is passed, the raw object data
1482
will be culled from it.
1484
@method _setRecordset
1485
@param {Object[]|Recordset} val The recordset value to relay
1486
@deprecated This will be removed with the deprecated `recordset` attribute
1491
_setRecordset: function (val) {
1494
if (val && Y.Recordset && val instanceof Y.Recordset) {
1496
val.each(function (record) {
1497
data.push(record.get('data'));
1502
this.set('data', val);
1508
Accepts a Base subclass (preferably a Model subclass). Alternately, it will
1509
generate a custom Model subclass from an array of attribute names or an
1510
object defining attributes and their respective configurations (it is
1511
assigned as the `ATTRS` of the new class).
1513
Any other value is invalid.
1515
@method _setRecordType
1516
@param {Function|String[]|Object} val The Model subclass, array of
1517
attribute names, or the `ATTRS` definition for a custom model
1519
@return {Function} A Base/Model subclass
1523
_setRecordType: function (val) {
1526
// Duck type based on known/likely consumed APIs
1527
if (isFunction(val) && val.prototype.set && val.prototype.getAttrs) {
1529
} else if (isObject(val)) {
1530
modelClass = this._createRecordClass(val);
1533
return modelClass || INVALID;
1537
Creates, removes, or updates the table's `<caption>` element per the input
1538
value. Empty values result in the caption being removed.
1540
@method _uiSetCaption
1541
@param {HTML} htmlContent The content to populate the table caption
1545
_uiSetCaption: function (htmlContent) {
1546
var table = this._tableNode,
1547
caption = this._captionNode;
1551
this._captionNode = caption = Y.Node.create(
1552
fromTemplate(this.CAPTION_TEMPLATE, {
1553
className: this.getClassName('caption')
1556
table.prepend(this._captionNode);
1559
caption.setContent(htmlContent);
1561
} else if (caption) {
1562
caption.remove(true);
1564
delete this._captionNode;
1569
Updates the table's `summary` attribute with the input value.
1571
@method _uiSetSummary
1575
_uiSetSummary: function (summary) {
1577
this._tableNode.setAttribute('summary', summary);
1579
this._tableNode.removeAttribute('summary');
1584
Sets the `boundingBox` and table width per the input value.
1587
@param {Number|String} width The width to make the table
1591
_uiSetWidth: function (width) {
1592
var table = this._tableNode;
1594
if (isNumber(width)) {
1595
// DEF_UNIT from Widget
1596
width += this.DEF_UNIT;
1599
if (isString(width)) {
1600
this._uiSetDim('width', width);
1602
// Table width needs to account for borders
1603
table.setStyle('width', !width ? '' :
1604
(this.get('boundingBox').get('offsetWidth') -
1605
(parseInt(table.getComputedStyle('borderLeftWidth'), 10)|0) -
1606
(parseInt(table.getComputedStyle('borderLeftWidth'), 10)|0)) +
1609
table.setStyle('width', width);
1614
Verifies the input value is a function with a `render` method on its
1615
prototype. `null` is also accepted to remove the default View.
1617
@method _validateView
1621
_validateView: function (val) {
1622
// TODO support View instances?
1623
return val === null || (isFunction(val) && val.prototype.render);
1628
}, '3.5.1' ,{requires:['escape','model-list','node-event-delegate']});