/** View class responsible for rendering the `<thead>` section of a table. Used as the default `headerView` for `Y.DataTable.Base` and `Y.DataTable` classes. @module datatable @submodule datatable-head @since 3.5.0 **/ var Lang = Y.Lang, fromTemplate = Lang.sub, isArray = Lang.isArray, toArray = Y.Array; /** View class responsible for rendering the `<thead>` section of a table. Used as the default `headerView` for `Y.DataTable.Base` and `Y.DataTable` classes. Translates the provided array of column configuration objects into a rendered `<thead>` based on the data in those objects. The structure of the column data is expected to be a single array of objects, where each object corresponds to a `<th>`. Those objects may contain a `children` property containing a similarly structured array to indicate the nested cells should be grouped under the parent column's colspan in a separate row of header cells. E.g. <pre><code> new Y.DataTable.HeaderView({ container: tableNode, columns: [ { key: 'id' }, // no nesting { key: 'name', children: [ { key: 'firstName', label: 'First' }, { key: 'lastName', label: 'Last' } ] } ] }).render(); </code></pre> This would translate to the following visualization: <pre> --------------------- | | name | | |--------------- | id | First | Last | --------------------- </pre> Supported properties of the column objects include: * `label` - The HTML content of the header cell. * `key` - If `label` is not specified, the `key` is used for content. * `children` - Array of columns to appear below this column in the next row. * `headerTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells in this column only. * `abbr` - The content of the 'abbr' attribute of the `<th>` * `title` - The content of the 'title' attribute of the `<th>` * `className` - Adds this string of CSS classes to the column header Through the life of instantiation and rendering, the column objects will have the following properties added to them: * `id` - (Defaulted by DataTable) The id to assign the rendered column * `_colspan` - To supply the `<th>` attribute * `_rowspan` - To supply the `<th>` attribute * `_parent` - (Added by DataTable) If the column is a child of another column, this points to its parent column The column object is also used to provide values for {placeholder} tokens in the instance's `CELL_TEMPLATE`, so you can modify the template and include other column object properties to populate them. @class HeaderView @namespace DataTable @extends View @since 3.5.0 **/ Y.namespace('DataTable').HeaderView = Y.Base.create('tableHeader', Y.View, [], { // -- Instance properties ------------------------------------------------- /** Template used to create the table's header cell markup. Override this to customize how header cell markup is created. @property CELL_TEMPLATE @type {String} @default '<th id="{id}" colspan="{_colspan}" rowspan="{_rowspan}" class="{className}" scope="col" {_id}{abbr}{title}>{content}</th>' @since 3.5.0 **/ CELL_TEMPLATE: '<th id="{id}" colspan="{_colspan}" rowspan="{_rowspan}" class="{className}" scope="col" {_id}{abbr}{title}>{content}</th>', /** The data representation of the header rows to render. This is assigned by parsing the `columns` configuration array, and is used by the render() method. @property columns @type {Array[]} @default (initially unset) @since 3.5.0 **/ //TODO: should this be protected? //columns: null, /** Template used to create the table's header row markup. Override this to customize the row markup. @property ROW_TEMPLATE @type {String} @default '<tr>{content}</tr>' @since 3.5.0 **/ ROW_TEMPLATE: '<tr>{content}</tr>', /** The object that serves as the source of truth for column and row data. This property is assigned at instantiation from the `source` property of the configuration object passed to the constructor. @property source @type {Object} @default (initially unset) @since 3.5.0 **/ //TODO: should this be protected? //source: null, /** HTML templates used to create the `<thead>` containing the table headers. @property THEAD_TEMPLATE @type {String} @default '<thead class="{className}">{content}</thead>' @since 3.6.0 **/ THEAD_TEMPLATE: '<thead class="{className}"></thead>', // -- Public methods ------------------------------------------------------ /** Returns the generated CSS classname based on the input. If the `host` attribute is configured, it will attempt to relay to its `getClassName` or use its static `NAME` property as a string base. If `host` is absent or has neither method nor `NAME`, a CSS classname will be generated using this class's `NAME`. @method getClassName @param {String} token* Any number of token strings to assemble the classname from. @return {String} @protected **/ getClassName: function () { // TODO: add attribute with setter? to host to use property this.host // for performance var host = this.host, NAME = (host && host.constructor.NAME) || this.constructor.NAME; if (host && host.getClassName) { return host.getClassName.apply(host, arguments); } else { return Y.ClassNameManager.getClassName .apply(Y.ClassNameManager, [NAME].concat(toArray(arguments, 0, true))); } }, /** Creates the `<thead>` Node content by assembling markup generated by populating the `ROW_TEMPLATE` and `CELL_TEMPLATE` templates with content from the `columns` property. @method render @chainable @since 3.5.0 **/ render: function () { var table = this.get('container'), thead = this.theadNode || (this.theadNode = this._createTHeadNode()), columns = this.columns, defaults = { _colspan: 1, _rowspan: 1, abbr: '', title: '' }, i, len, j, jlen, col, html, content, values; if (thead && columns) { html = ''; if (columns.length) { for (i = 0, len = columns.length; i < len; ++i) { content = ''; for (j = 0, jlen = columns[i].length; j < jlen; ++j) { col = columns[i][j]; values = Y.merge( defaults, col, { className: this.getClassName('header'), content : col.label || col.key || ("Column " + (j + 1)) } ); values._id = col._id ? ' data-yui3-col-id="' + col._id + '"' : ''; if (col.abbr) { values.abbr = ' abbr="' + col.abbr + '"'; } if (col.title) { values.title = ' title="' + col.title + '"'; } if (col.className) { values.className += ' ' + col.className; } if (col._first) { values.className += ' ' + this.getClassName('first', 'header'); } if (col._id) { values.className += ' ' + this.getClassName('col', col._id); } content += fromTemplate( col.headerTemplate || this.CELL_TEMPLATE, values); } html += fromTemplate(this.ROW_TEMPLATE, { content: content }); } } thead.setHTML(html); if (thead.get('parentNode') !== table) { table.insertBefore(thead, table.one('tfoot, tbody')); } } this.bindUI(); return this; }, // -- Protected and private properties and methods ------------------------ /** Handles changes in the source's columns attribute. Redraws the headers. @method _afterColumnsChange @param {EventFacade} e The `columnsChange` event object @protected @since 3.5.0 **/ _afterColumnsChange: function (e) { this.columns = this._parseColumns(e.newVal); this.render(); }, /** Binds event subscriptions from the UI and the source (if assigned). @method bindUI @protected @since 3.5.0 **/ bindUI: function () { if (!this._eventHandles.columnsChange) { // TODO: How best to decouple this? this._eventHandles.columnsChange = this.after('columnsChange', Y.bind('_afterColumnsChange', this)); } }, /** Creates the `<thead>` node that will store the header rows and cells. @method _createTHeadNode @return {Node} @protected @since 3.6.0 **/ _createTHeadNode: function () { return Y.Node.create(fromTemplate(this.THEAD_TEMPLATE, { className: this.getClassName('columns') })); }, /** Destroys the instance. @method destructor @protected @since 3.5.0 **/ destructor: function () { (new Y.EventHandle(Y.Object.values(this._eventHandles))).detach(); }, /** Holds the event subscriptions needing to be detached when the instance is `destroy()`ed. @property _eventHandles @type {Object} @default undefined (initially unset) @protected @since 3.5.0 **/ //_eventHandles: null, /** Initializes the instance. Reads the following configuration properties: * `columns` - (REQUIRED) The initial column information * `host` - The object to serve as source of truth for column info @method initializer @param {Object} config Configuration data @protected @since 3.5.0 **/ initializer: function (config) { this.host = config.host; this.columns = this._parseColumns(config.columns); this._eventHandles = []; }, /** Translate the input column format into a structure useful for rendering a `<thead>`, rows, and cells. The structure of the input is expected to be a single array of objects, where each object corresponds to a `<th>`. Those objects may contain a `children` property containing a similarly structured array to indicate the nested cells should be grouped under the parent column's colspan in a separate row of header cells. E.g. <pre><code> [ { key: 'id' }, // no nesting { key: 'name', children: [ { key: 'firstName', label: 'First' }, { key: 'lastName', label: 'Last' } ] } ] </code></pre> would indicate two header rows with the first column 'id' being assigned a `rowspan` of `2`, the 'name' column appearing in the first row with a `colspan` of `2`, and the 'firstName' and 'lastName' columns appearing in the second row, below the 'name' column. <pre> --------------------- | | name | | |--------------- | id | First | Last | --------------------- </pre> Supported properties of the column objects include: * `label` - The HTML content of the header cell. * `key` - If `label` is not specified, the `key` is used for content. * `children` - Array of columns to appear below this column in the next row. * `abbr` - The content of the 'abbr' attribute of the `<th>` * `title` - The content of the 'title' attribute of the `<th>` * `headerTemplate` - Overrides the instance's `CELL_TEMPLATE` for cells in this column only. The output structure is basically a simulation of the `<thead>` structure with arrays for rows and objects for cells. Column objects have the following properties added to them: * `id` - (Defaulted by DataTable) The id to assign the rendered column * `_colspan` - Per the `<th>` attribute * `_rowspan` - Per the `<th>` attribute * `_parent` - (Added by DataTable) If the column is a child of another column, this points to its parent column The column object is also used to provide values for {placeholder} replacement in the `CELL_TEMPLATE`, so you can modify the template and include other column object properties to populate them. @method _parseColumns @param {Object[]} data Array of column object data @return {Array[]} An array of arrays corresponding to the header row structure to render @protected @since 3.5.0 **/ _parseColumns: function (data) { var columns = [], stack = [], rowSpan = 1, entry, row, col, children, parent, i, len, j; if (isArray(data) && data.length) { // don't modify the input array data = data.slice(); // First pass, assign colspans and calculate row count for // non-nested headers' rowspan stack.push([data, -1]); while (stack.length) { entry = stack[stack.length - 1]; row = entry[0]; i = entry[1] + 1; for (len = row.length; i < len; ++i) { row[i] = col = Y.merge(row[i]); children = col.children; Y.stamp(col); if (!col.id) { col.id = Y.guid(); } if (isArray(children) && children.length) { stack.push([children, -1]); entry[1] = i; rowSpan = Math.max(rowSpan, stack.length); // break to let the while loop process the children break; } else { col._colspan = 1; } } if (i >= len) { // All columns in this row are processed if (stack.length > 1) { entry = stack[stack.length - 2]; parent = entry[0][entry[1]]; parent._colspan = 0; for (i = 0, len = row.length; i < len; ++i) { // Can't use .length because in 3+ rows, colspan // needs to aggregate the colspans of children row[i]._parent = parent; parent._colspan += row[i]._colspan; } } stack.pop(); } } // Second pass, build row arrays and assign rowspan for (i = 0; i < rowSpan; ++i) { columns.push([]); } stack.push([data, -1]); while (stack.length) { entry = stack[stack.length - 1]; row = entry[0]; i = entry[1] + 1; for (len = row.length; i < len; ++i) { col = row[i]; children = col.children; columns[stack.length - 1].push(col); entry[1] = i; // collect the IDs of parent cols col._headers = [col.id]; for (j = stack.length - 2; j >= 0; --j) { parent = stack[j][0][stack[j][1]]; col._headers.unshift(parent.id); } if (children && children.length) { // parent cells must assume rowspan 1 (long story) // break to let the while loop process the children stack.push([children, -1]); break; } else { col._rowspan = rowSpan - stack.length + 1; } } if (i >= len) { // All columns in this row are processed stack.pop(); } } } for (i = 0, len = columns.length; i < len; i += col._rowspan) { col = columns[i][0]; col._first = true; } return columns; } });