|
1 YUI.add('datatable-head', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 View class responsible for rendering the `<thead>` section of a table. Used as |
|
5 the default `headerView` for `Y.DataTable.Base` and `Y.DataTable` classes. |
|
6 |
|
7 @module datatable |
|
8 @submodule datatable-head |
|
9 @since 3.5.0 |
|
10 **/ |
|
11 var Lang = Y.Lang, |
|
12 fromTemplate = Lang.sub, |
|
13 isArray = Lang.isArray, |
|
14 toArray = Y.Array; |
|
15 |
|
16 /** |
|
17 View class responsible for rendering the `<thead>` section of a table. Used as |
|
18 the default `headerView` for `Y.DataTable.Base` and `Y.DataTable` classes. |
|
19 |
|
20 Translates the provided array of column configuration objects into a rendered |
|
21 `<thead>` based on the data in those objects. |
|
22 |
|
23 |
|
24 The structure of the column data is expected to be a single array of objects, |
|
25 where each object corresponds to a `<th>`. Those objects may contain a |
|
26 `children` property containing a similarly structured array to indicate the |
|
27 nested cells should be grouped under the parent column's colspan in a separate |
|
28 row of header cells. E.g. |
|
29 |
|
30 <pre><code> |
|
31 new Y.DataTable.HeaderView({ |
|
32 container: tableNode, |
|
33 columns: [ |
|
34 { key: 'id' }, // no nesting |
|
35 { key: 'name', children: [ |
|
36 { key: 'firstName', label: 'First' }, |
|
37 { key: 'lastName', label: 'Last' } ] } |
|
38 ] |
|
39 }).render(); |
|
40 </code></pre> |
|
41 |
|
42 This would translate to the following visualization: |
|
43 |
|
44 <pre> |
|
45 --------------------- |
|
46 | | name | |
|
47 | |--------------- |
|
48 | id | First | Last | |
|
49 --------------------- |
|
50 </pre> |
|
51 |
|
52 Supported properties of the column objects include: |
|
53 |
|
54 * `label` - The HTML content of the header cell. |
|
55 * `key` - If `label` is not specified, the `key` is used for content. |
|
56 * `children` - Array of columns to appear below this column in the next |
|
57 row. |
|
58 * `headerTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells in this |
|
59 column only. |
|
60 * `abbr` - The content of the 'abbr' attribute of the `<th>` |
|
61 * `title` - The content of the 'title' attribute of the `<th>` |
|
62 * `className` - Adds this string of CSS classes to the column header |
|
63 |
|
64 Through the life of instantiation and rendering, the column objects will have |
|
65 the following properties added to them: |
|
66 |
|
67 * `id` - (Defaulted by DataTable) The id to assign the rendered column |
|
68 * `_colspan` - To supply the `<th>` attribute |
|
69 * `_rowspan` - To supply the `<th>` attribute |
|
70 * `_parent` - (Added by DataTable) If the column is a child of another |
|
71 column, this points to its parent column |
|
72 |
|
73 The column object is also used to provide values for {placeholder} tokens in the |
|
74 instance's `CELL_TEMPLATE`, so you can modify the template and include other |
|
75 column object properties to populate them. |
|
76 |
|
77 @class HeaderView |
|
78 @namespace DataTable |
|
79 @extends View |
|
80 @since 3.5.0 |
|
81 **/ |
|
82 Y.namespace('DataTable').HeaderView = Y.Base.create('tableHeader', Y.View, [], { |
|
83 // -- Instance properties ------------------------------------------------- |
|
84 |
|
85 /** |
|
86 Template used to create the table's header cell markup. Override this to |
|
87 customize how header cell markup is created. |
|
88 |
|
89 @property CELL_TEMPLATE |
|
90 @type {String} |
|
91 @default '<th id="{id}" colspan="{_colspan}" rowspan="{_rowspan}" class="{className}" scope="col" {_id}{abbr}{title}>{content}</th>' |
|
92 @since 3.5.0 |
|
93 **/ |
|
94 CELL_TEMPLATE: |
|
95 '<th id="{id}" colspan="{_colspan}" rowspan="{_rowspan}" class="{className}" scope="col" {_id}{abbr}{title}>{content}</th>', |
|
96 |
|
97 /** |
|
98 The data representation of the header rows to render. This is assigned by |
|
99 parsing the `columns` configuration array, and is used by the render() |
|
100 method. |
|
101 |
|
102 @property columns |
|
103 @type {Array[]} |
|
104 @default (initially unset) |
|
105 @since 3.5.0 |
|
106 **/ |
|
107 //TODO: should this be protected? |
|
108 //columns: null, |
|
109 |
|
110 /** |
|
111 Template used to create the table's header row markup. Override this to |
|
112 customize the row markup. |
|
113 |
|
114 @property ROW_TEMPLATE |
|
115 @type {String} |
|
116 @default '<tr>{content}</tr>' |
|
117 @since 3.5.0 |
|
118 **/ |
|
119 ROW_TEMPLATE: |
|
120 '<tr>{content}</tr>', |
|
121 |
|
122 /** |
|
123 The object that serves as the source of truth for column and row data. |
|
124 This property is assigned at instantiation from the `source` property of |
|
125 the configuration object passed to the constructor. |
|
126 |
|
127 @property source |
|
128 @type {Object} |
|
129 @default (initially unset) |
|
130 @since 3.5.0 |
|
131 **/ |
|
132 //TODO: should this be protected? |
|
133 //source: null, |
|
134 |
|
135 /** |
|
136 HTML templates used to create the `<thead>` containing the table headers. |
|
137 |
|
138 @property THEAD_TEMPLATE |
|
139 @type {String} |
|
140 @default '<thead class="{className}">{content}</thead>' |
|
141 @since 3.6.0 |
|
142 **/ |
|
143 THEAD_TEMPLATE: '<thead class="{className}"></thead>', |
|
144 |
|
145 // -- Public methods ------------------------------------------------------ |
|
146 |
|
147 /** |
|
148 Returns the generated CSS classname based on the input. If the `host` |
|
149 attribute is configured, it will attempt to relay to its `getClassName` |
|
150 or use its static `NAME` property as a string base. |
|
151 |
|
152 If `host` is absent or has neither method nor `NAME`, a CSS classname |
|
153 will be generated using this class's `NAME`. |
|
154 |
|
155 @method getClassName |
|
156 @param {String} token* Any number of token strings to assemble the |
|
157 classname from. |
|
158 @return {String} |
|
159 @protected |
|
160 **/ |
|
161 getClassName: function () { |
|
162 // TODO: add attribute with setter? to host to use property this.host |
|
163 // for performance |
|
164 var host = this.host, |
|
165 NAME = (host && host.constructor.NAME) || |
|
166 this.constructor.NAME; |
|
167 |
|
168 if (host && host.getClassName) { |
|
169 return host.getClassName.apply(host, arguments); |
|
170 } else { |
|
171 return Y.ClassNameManager.getClassName |
|
172 .apply(Y.ClassNameManager, |
|
173 [NAME].concat(toArray(arguments, 0, true))); |
|
174 } |
|
175 }, |
|
176 |
|
177 /** |
|
178 Creates the `<thead>` Node content by assembling markup generated by |
|
179 populating the `ROW_TEMPLATE` and `CELL_TEMPLATE` templates with content |
|
180 from the `columns` property. |
|
181 |
|
182 @method render |
|
183 @chainable |
|
184 @since 3.5.0 |
|
185 **/ |
|
186 render: function () { |
|
187 var table = this.get('container'), |
|
188 thead = this.theadNode || |
|
189 (this.theadNode = this._createTHeadNode()), |
|
190 columns = this.columns, |
|
191 defaults = { |
|
192 _colspan: 1, |
|
193 _rowspan: 1, |
|
194 abbr: '', |
|
195 title: '' |
|
196 }, |
|
197 i, len, j, jlen, col, html, content, values; |
|
198 |
|
199 if (thead && columns) { |
|
200 html = ''; |
|
201 |
|
202 if (columns.length) { |
|
203 for (i = 0, len = columns.length; i < len; ++i) { |
|
204 content = ''; |
|
205 |
|
206 for (j = 0, jlen = columns[i].length; j < jlen; ++j) { |
|
207 col = columns[i][j]; |
|
208 values = Y.merge( |
|
209 defaults, |
|
210 col, { |
|
211 className: this.getClassName('header'), |
|
212 content : col.label || col.key || |
|
213 ("Column " + (j + 1)) |
|
214 } |
|
215 ); |
|
216 |
|
217 values._id = col._id ? |
|
218 ' data-yui3-col-id="' + col._id + '"' : ''; |
|
219 |
|
220 if (col.abbr) { |
|
221 values.abbr = ' abbr="' + col.abbr + '"'; |
|
222 } |
|
223 |
|
224 if (col.title) { |
|
225 values.title = ' title="' + col.title + '"'; |
|
226 } |
|
227 |
|
228 if (col.className) { |
|
229 values.className += ' ' + col.className; |
|
230 } |
|
231 |
|
232 if (col._first) { |
|
233 values.className += ' ' + this.getClassName('first', 'header'); |
|
234 } |
|
235 |
|
236 if (col._id) { |
|
237 values.className += |
|
238 ' ' + this.getClassName('col', col._id); |
|
239 } |
|
240 |
|
241 content += fromTemplate( |
|
242 col.headerTemplate || this.CELL_TEMPLATE, values); |
|
243 } |
|
244 |
|
245 html += fromTemplate(this.ROW_TEMPLATE, { |
|
246 content: content |
|
247 }); |
|
248 } |
|
249 } |
|
250 |
|
251 thead.setHTML(html); |
|
252 |
|
253 if (thead.get('parentNode') !== table) { |
|
254 table.insertBefore(thead, table.one('tfoot, tbody')); |
|
255 } |
|
256 } |
|
257 |
|
258 this.bindUI(); |
|
259 |
|
260 return this; |
|
261 }, |
|
262 |
|
263 // -- Protected and private properties and methods ------------------------ |
|
264 |
|
265 /** |
|
266 Handles changes in the source's columns attribute. Redraws the headers. |
|
267 |
|
268 @method _afterColumnsChange |
|
269 @param {EventFacade} e The `columnsChange` event object |
|
270 @protected |
|
271 @since 3.5.0 |
|
272 **/ |
|
273 _afterColumnsChange: function (e) { |
|
274 this.columns = this._parseColumns(e.newVal); |
|
275 |
|
276 this.render(); |
|
277 }, |
|
278 |
|
279 /** |
|
280 Binds event subscriptions from the UI and the source (if assigned). |
|
281 |
|
282 @method bindUI |
|
283 @protected |
|
284 @since 3.5.0 |
|
285 **/ |
|
286 bindUI: function () { |
|
287 if (!this._eventHandles.columnsChange) { |
|
288 // TODO: How best to decouple this? |
|
289 this._eventHandles.columnsChange = |
|
290 this.after('columnsChange', |
|
291 Y.bind('_afterColumnsChange', this)); |
|
292 } |
|
293 }, |
|
294 |
|
295 /** |
|
296 Creates the `<thead>` node that will store the header rows and cells. |
|
297 |
|
298 @method _createTHeadNode |
|
299 @return {Node} |
|
300 @protected |
|
301 @since 3.6.0 |
|
302 **/ |
|
303 _createTHeadNode: function () { |
|
304 return Y.Node.create(fromTemplate(this.THEAD_TEMPLATE, { |
|
305 className: this.getClassName('columns') |
|
306 })); |
|
307 }, |
|
308 |
|
309 /** |
|
310 Destroys the instance. |
|
311 |
|
312 @method destructor |
|
313 @protected |
|
314 @since 3.5.0 |
|
315 **/ |
|
316 destructor: function () { |
|
317 (new Y.EventHandle(Y.Object.values(this._eventHandles))).detach(); |
|
318 }, |
|
319 |
|
320 /** |
|
321 Holds the event subscriptions needing to be detached when the instance is |
|
322 `destroy()`ed. |
|
323 |
|
324 @property _eventHandles |
|
325 @type {Object} |
|
326 @default undefined (initially unset) |
|
327 @protected |
|
328 @since 3.5.0 |
|
329 **/ |
|
330 //_eventHandles: null, |
|
331 |
|
332 /** |
|
333 Initializes the instance. Reads the following configuration properties: |
|
334 |
|
335 * `columns` - (REQUIRED) The initial column information |
|
336 * `host` - The object to serve as source of truth for column info |
|
337 |
|
338 @method initializer |
|
339 @param {Object} config Configuration data |
|
340 @protected |
|
341 @since 3.5.0 |
|
342 **/ |
|
343 initializer: function (config) { |
|
344 this.host = config.host; |
|
345 this.columns = this._parseColumns(config.columns); |
|
346 |
|
347 this._eventHandles = []; |
|
348 }, |
|
349 |
|
350 /** |
|
351 Translate the input column format into a structure useful for rendering a |
|
352 `<thead>`, rows, and cells. The structure of the input is expected to be a |
|
353 single array of objects, where each object corresponds to a `<th>`. Those |
|
354 objects may contain a `children` property containing a similarly structured |
|
355 array to indicate the nested cells should be grouped under the parent |
|
356 column's colspan in a separate row of header cells. E.g. |
|
357 |
|
358 <pre><code> |
|
359 [ |
|
360 { key: 'id' }, // no nesting |
|
361 { key: 'name', children: [ |
|
362 { key: 'firstName', label: 'First' }, |
|
363 { key: 'lastName', label: 'Last' } ] } |
|
364 ] |
|
365 </code></pre> |
|
366 |
|
367 would indicate two header rows with the first column 'id' being assigned a |
|
368 `rowspan` of `2`, the 'name' column appearing in the first row with a |
|
369 `colspan` of `2`, and the 'firstName' and 'lastName' columns appearing in |
|
370 the second row, below the 'name' column. |
|
371 |
|
372 <pre> |
|
373 --------------------- |
|
374 | | name | |
|
375 | |--------------- |
|
376 | id | First | Last | |
|
377 --------------------- |
|
378 </pre> |
|
379 |
|
380 Supported properties of the column objects include: |
|
381 |
|
382 * `label` - The HTML content of the header cell. |
|
383 * `key` - If `label` is not specified, the `key` is used for content. |
|
384 * `children` - Array of columns to appear below this column in the next |
|
385 row. |
|
386 * `abbr` - The content of the 'abbr' attribute of the `<th>` |
|
387 * `title` - The content of the 'title' attribute of the `<th>` |
|
388 * `headerTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells |
|
389 in this column only. |
|
390 |
|
391 The output structure is basically a simulation of the `<thead>` structure |
|
392 with arrays for rows and objects for cells. Column objects have the |
|
393 following properties added to them: |
|
394 |
|
395 * `id` - (Defaulted by DataTable) The id to assign the rendered |
|
396 column |
|
397 * `_colspan` - Per the `<th>` attribute |
|
398 * `_rowspan` - Per the `<th>` attribute |
|
399 * `_parent` - (Added by DataTable) If the column is a child of another |
|
400 column, this points to its parent column |
|
401 |
|
402 The column object is also used to provide values for {placeholder} |
|
403 replacement in the `CELL_TEMPLATE`, so you can modify the template and |
|
404 include other column object properties to populate them. |
|
405 |
|
406 @method _parseColumns |
|
407 @param {Object[]} data Array of column object data |
|
408 @return {Array[]} An array of arrays corresponding to the header row |
|
409 structure to render |
|
410 @protected |
|
411 @since 3.5.0 |
|
412 **/ |
|
413 _parseColumns: function (data) { |
|
414 var columns = [], |
|
415 stack = [], |
|
416 rowSpan = 1, |
|
417 entry, row, col, children, parent, i, len, j; |
|
418 |
|
419 if (isArray(data) && data.length) { |
|
420 // don't modify the input array |
|
421 data = data.slice(); |
|
422 |
|
423 // First pass, assign colspans and calculate row count for |
|
424 // non-nested headers' rowspan |
|
425 stack.push([data, -1]); |
|
426 |
|
427 while (stack.length) { |
|
428 entry = stack[stack.length - 1]; |
|
429 row = entry[0]; |
|
430 i = entry[1] + 1; |
|
431 |
|
432 for (len = row.length; i < len; ++i) { |
|
433 row[i] = col = Y.merge(row[i]); |
|
434 children = col.children; |
|
435 |
|
436 Y.stamp(col); |
|
437 |
|
438 if (!col.id) { |
|
439 col.id = Y.guid(); |
|
440 } |
|
441 |
|
442 if (isArray(children) && children.length) { |
|
443 stack.push([children, -1]); |
|
444 entry[1] = i; |
|
445 |
|
446 rowSpan = Math.max(rowSpan, stack.length); |
|
447 |
|
448 // break to let the while loop process the children |
|
449 break; |
|
450 } else { |
|
451 col._colspan = 1; |
|
452 } |
|
453 } |
|
454 |
|
455 if (i >= len) { |
|
456 // All columns in this row are processed |
|
457 if (stack.length > 1) { |
|
458 entry = stack[stack.length - 2]; |
|
459 parent = entry[0][entry[1]]; |
|
460 |
|
461 parent._colspan = 0; |
|
462 |
|
463 for (i = 0, len = row.length; i < len; ++i) { |
|
464 // Can't use .length because in 3+ rows, colspan |
|
465 // needs to aggregate the colspans of children |
|
466 row[i]._parent = parent; |
|
467 parent._colspan += row[i]._colspan; |
|
468 } |
|
469 } |
|
470 stack.pop(); |
|
471 } |
|
472 } |
|
473 |
|
474 // Second pass, build row arrays and assign rowspan |
|
475 for (i = 0; i < rowSpan; ++i) { |
|
476 columns.push([]); |
|
477 } |
|
478 |
|
479 stack.push([data, -1]); |
|
480 |
|
481 while (stack.length) { |
|
482 entry = stack[stack.length - 1]; |
|
483 row = entry[0]; |
|
484 i = entry[1] + 1; |
|
485 |
|
486 for (len = row.length; i < len; ++i) { |
|
487 col = row[i]; |
|
488 children = col.children; |
|
489 |
|
490 columns[stack.length - 1].push(col); |
|
491 |
|
492 entry[1] = i; |
|
493 |
|
494 // collect the IDs of parent cols |
|
495 col._headers = [col.id]; |
|
496 |
|
497 for (j = stack.length - 2; j >= 0; --j) { |
|
498 parent = stack[j][0][stack[j][1]]; |
|
499 |
|
500 col._headers.unshift(parent.id); |
|
501 } |
|
502 |
|
503 if (children && children.length) { |
|
504 // parent cells must assume rowspan 1 (long story) |
|
505 |
|
506 // break to let the while loop process the children |
|
507 stack.push([children, -1]); |
|
508 break; |
|
509 } else { |
|
510 col._rowspan = rowSpan - stack.length + 1; |
|
511 } |
|
512 } |
|
513 |
|
514 if (i >= len) { |
|
515 // All columns in this row are processed |
|
516 stack.pop(); |
|
517 } |
|
518 } |
|
519 } |
|
520 |
|
521 for (i = 0, len = columns.length; i < len; i += col._rowspan) { |
|
522 col = columns[i][0]; |
|
523 |
|
524 col._first = true; |
|
525 } |
|
526 |
|
527 return columns; |
|
528 } |
|
529 }); |
|
530 |
|
531 |
|
532 }, '@VERSION@', {"requires": ["datatable-core", "view", "classnamemanager"]}); |