/*! * Methods for creating XNAT-specific <table> elements */ var XNAT = getObject(XNAT); (function(factory){ // add dependencies to 'imports' array var imports = [ 'xnat/init', 'lib/jquery/jquery' ]; if (typeof define === 'function' && define.amd) { define(imports, factory); } else if (typeof exports === 'object') { module.exports = factory(XNAT, jQuery); } else { return factory(XNAT, jQuery); } }(function(XNAT, $){ var table, element = window.spawn, undefined; /** * Constructor function for XNAT.table() * @param [opts] {Object} < table > Element attributes * @param [config] {Object} other config options * @constructor */ function Table(opts, config){ this.newTable = function(o, c){ o = o || opts || {}; c = c || config; this.opts = cloneObject(o); this.config = c ? cloneObject(c) : null; this.table = element('table', this.opts); this.$table = this.table$ = $(this.table); this.last = {}; // 'parent' gets reset on return of chained methods this.last.parent = this.table; // get 'last' item wrapped in jQuery this.last$ = function(el){ return $(this.last[el || 'parent']); }; this.setLast = function(el){ this.last.parent = this.last.child = this.last[el.tagName.toLowerCase()] = el; }; this.getLast = function(){ return this.last.child; }; this._rows = []; this._cols = 0; // how many columns? }; this.newTable(); } // alias prototype for less typing Table.p = Table.prototype; // return last item to use with jQuery methods // XNAT.table().tr().$('attr', ['title', 'foo']).td('Bar').$({ addClass: 'bar' }).getHTML(); // <table><tr title="foo"><td class="bar">Bar</td></tr></table> // yes, the HTML is shorter and simpler, but also harder to generate programmatically Table.p.$ = function(method, args){ var $el = $(this.getLast()); var methods = isPlainObject(method) ? method : null; args = args || []; if (!methods) { methods = {}; // force an object if not already methods[method] = args; } forOwn(methods, function(name, arg){ $el[name].apply($el, [].concat(arg)); }); return this; }; // jQuery methods we'd like to use: var $methods = [ 'append', 'prepend', 'addClass', 'find' ]; $methods.forEach(function(method){ Table.p[method] = function(args){ this.$(method, args); return this; } }); // create a single <td> element // just using a single argument // if you want to modify the <td> // you'll need to pass a config // object to set the properties // and use append or innerHTML // to add the cell content Table.p.td = function(opts, content){ var td = element('td', opts, content); this.last.td = td; this.last.child = td; this.last.tr.appendChild(td); return this; }; Table.p.th = function(opts, content){ var th = element('th', opts, content); this.last.th = th; this.last.child = th; this.last.tr.appendChild(th); this._cols++; // do this here? return this; }; Table.p.tr = function(opts, data){ var _this = this; var tr = element('tr', opts); //data = data || this.data || null; if (data) { this.last.tr = tr; [].concat(data).forEach(function(item, i){ //if (_this._cols && _this._cols > i) return; _this.td(item); }); } // only add <tr> elements to <table>, <thead>, <tbody>, and <tfoot> if (/(table|thead|tbody|tfoot)/i.test(this.last.parent.tagName)) { this.last.parent.appendChild(tr); } this.last.tr = tr; this.last.child = tr; // this.setLast(tr); // nullify last <th> and <td> elements since this is a new row this.last.th = this.last.td = null; return this; }; // create a <tr> with optional <td> elements // in the <tbody> Table.p.row = Table.p.addRow = function(data, opts){ data = data || []; this.tr(opts, data); return this; }; // add a <tr> to <tbody> Table.p.bodyRow = function(data, opts){ this.toBody().row(data, opts); return this; }; // create *multiple* <td> elements Table.p.tds = function(items, opts){ var _this = this; [].concat(items).forEach(function(item){ if (stringable(item)) { _this.td(opts, item); } // if 'item' isn't stringable, it will be an object else { _this.td(extend(true, {}, opts, item)); } }); // don't reset 'last' so we // keep using the parent <tr> return this; }; Table.p.rows = function(data, opts){ var _this = this, rows = [], cols = data[0].length; // first array length determines how many columns data = data || []; data.forEach(function(row){ row = row.slice(0, cols); rows.push(_this.tr(opts, row)) }); this._rows = rows; this.append(this._rows); return this; }; Table.p.thead = function(opts, data){ var head = element('thead', opts); this.table.appendChild(head); // this.last.child = head; this.setLast(head); return this; }; Table.p.tfoot = function(opts, data){ var foot = element('tfoot', opts); this.table.appendChild(foot); // this.last.child = foot; this.setLast(foot); return this; }; Table.p.tbody = function(opts, data){ var body = element('tbody', opts); this.table.appendChild(body); // this.last.child = body; this.setLast(body); return this; }; // reset last.parent to <tbody> Table.p.toBody = Table.p.closestBody = function(){ this.setLast(this.last.tbody || this.table); return this; }; // reset last.parent to <thead> Table.p.toHead = Table.p.closestHead = function(){ this.setLast(this.last.thead || this.table); return this; }; // add multiple rows of data? Table.p.appendBody = Table.p.appendToBody = function(data){ var _this = this; [].concat(data).forEach(function(row){ _this.toBody().addRow(row); }); return this; }; Table.p.get = function(){ return this.table; }; Table.p.$get = Table.p.get$ = function(){ return $(this.table); }; Table.p.getHTML = Table.p.html = function(){ return this.table.outerHTML; }; /** * Populate table with data * @param data {Array} array of row arrays * @returns {Table.p} Table.prototype */ Table.p.init = function(data){ var _this = this, obj = {}, header, cols = 0; // don't init twice? if (this.inited) { // run .init() again to // empty table and load new data this.table$.empty(); //this.newTable(); //return this } data = data || []; if (Array.isArray(data)) { obj.data = data; } else { obj = data || {}; } if (obj.header) { // if there's a 'header' property // set to true, pick the header from // the first row of data if (obj.header === true) { header = obj.data.shift(); } // otherwise it's set explicitly // as an array in the 'header' property // and that sets the number of columns else { header = obj.header; } } // set the number of columns based on // the header or first row of data cols = (header) ? header.length : (obj.data[0] || []).length; this._cols = cols; // add the header if (header) { this.thead().tr(); [].concat(header).forEach(function(item){ _this.th(item); }); } // always add <tbody> element on .init() this.tbody(); [].concat(obj.data || []).forEach(function(col){ var i = -1; // make a row! _this.tr(); // don't exceed column width of header or first column while (++i < cols) { _this.td(col[i]); } }); this.inited = true; return this; }; Table.p.render = function(container, empty){ var $container; if (container) { $container = $$(container); if (empty){ $container.empty(); } $container.append(this.table); } return this.table; }; // 'opts' are options for the <table> element // 'config' is for other configurable stuff table = function(opts, config){ return new Table(opts, config); }; // basic XNAT.dataTable widget table.dataTable = function(data, opts){ var tableData = data; // tolerate reversed arguments or spawner element object if (Array.isArray(opts) || data.spawnerElement) { tableData = opts; opts = getObject(data); } // don't modify original object opts = cloneObject(opts); var allItems = opts.header || (opts.items && opts.items === 'all'); // properties for spawned element opts.element = opts.element || {}; addClassName(opts.element, 'data-table xnat-table'); if (opts.sortable) { if (opts.sortable === true) { addClassName(opts.element, 'sortable'); } else { opts.sortable = opts.sortable.split(',').map(function(item){return item.trim()}); } } opts.element = extend(true, { id: opts.id || randomID('t', false), style: { width: opts.width || '100%' } }, opts.element); // initialize the table var newTable = new Table(opts.element); // create a div to hold the table // or message (if no data or error) var $tableContainer = $.spawn('div.data-table-container', [newTable.table]); var tableContainer = $tableContainer[0]; // if (opts.before) { // $tableContainer.prepend(opts.before); // } // add the table // $tableContainer.append(newTable.table); // if (opts.after) { // $tableContainer.append(opts.after); // } function createTable(rows){ var props = [], objRows = []; // convert object list to array list if (isPlainObject(rows)) { forOwn(rows, function(name, stuff){ objRows.push(stuff); }); rows = objRows; // now it's an array } // create header row if (!allItems && (opts.items || opts.properties)) { newTable.tr(); forOwn(opts.items||opts.properties, function(name, val){ // if 'val' is a string, it's the text for the <th> // if it's an object, get the 'label' property //var label = stringable(val) ? val+'' : val.label; props.push(name); // don't create <th> for items labeled as '~data' if (/^~data/.test(val)) { return; } newTable.th(val.label || val); if (/^~!/.test(val.label || val)) { $(newTable.last.th).html(name) .addClass('hidden') .dataAttr('prop', name); return; } //if (!opts.sortable) return; if (val.sort || opts.sortable === true || (opts.sortable||[]).indexOf(name) !== -1) { addClassName(newTable.last.th, 'sort'); newTable.last.th.appendChild(spawn('i', ' ')) } }); } else { if (allItems) { newTable.tr(); } forOwn(rows[0], function(name, val){ if (allItems) { newTable.th(name); if (/^~!/.test(val)) { addClassName(newTable.last.th, 'hidden'); } } props.push(name); }); } rows.forEach(function(item){ newTable.tr(); // iterate properties for each row props.forEach(function(name){ var hidden = false; var itemVal = item[name]; var cellObj = {}; var tdElement = { className: name, html: itemVal }; if (opts.items) { cellObj = opts.items[name]; if (typeof cellObj === 'string') { // set item label to '~data' to add as a // [data-*] attribute to the <tr> if (/^~data/.test(cellObj)) { var dataName = cellObj.split('.')[1] || name; newTable.last$('tr').dataAttr(dataName, itemVal); return; } hidden = /^~!/.test(cellObj); } else { if (cellObj.td || cellObj.element) { extend(true, tdElement, cellObj.td || cellObj.element); } if (cellObj.value) { // explicitly override value itemVal = cellObj.value; } if (cellObj.className) { addClassName(tdElement, cellObj.className); } // if (cellObj.apply) { // itemVal = eval(cellObj.apply).apply(item, [itemVal]); // } if (cellObj['call']) { if (isFunction(cellObj['call'])) { itemVal = cellObj['call'].call(item, itemVal) || itemVal; } else { itemVal = eval('('+cellObj['call'].trim()+')').call(item, itemVal) || itemVal; } } // special __VALUE__ string gets replaced if (cellObj.html || cellObj.content) { tdElement.html = (cellObj.html || cellObj.content).replace(/__VALUE__/g, itemVal); } else { tdElement.html = itemVal; } hidden = /^~!/.test(cellObj.label); } } newTable.td(tdElement); var $td = $(newTable.last.td); // evaluate jQuery methods if (cellObj.$) { if (typeof cellObj.$ === 'string') { eval('$(newTable.last.td).'+(cellObj.$).trim()); } else { forOwn(cellObj.$, function(method, args){ $td[method].apply($td, [].concat(args)) }); } } if (hidden) { $td.addClass('hidden'); } }); }); } function showMessage(){ tableContainer.innerHTML = ''; return { noData: function(msg){ tableContainer.innerHTML = '' + '<div class="no-data">' + (msg || 'Data not available.') + '</div>'; }, error: function(msg, error){ tableContainer.innerHTML = '' + '<div class="error">' + (msg || '') + (error ? '<br><br>' + error : '') + '</div>'; } }; } // if 'tableData' is a string, use as the url if (typeof tableData == 'string') { opts.url = tableData; } // request data for table rows if (opts.load || opts.url) { XNAT.xhr.get({ url: XNAT.url.rootUrl(opts.load||opts.url), dataType: opts.dataType || 'json', success: function(json){ // support custom path for returned data if (opts.path) { json = lookupObjectValue(json, opts.path); } else { // handle data returned in ResultSet.Result array json = (json.ResultSet && json.ResultSet.Result) ? json.ResultSet.Result : json; } // make sure there's data before rendering the table if (isEmpty(json)) { showMessage().noData(opts.messages ? opts.messages.noData || opts.messages.empty : '') } else { createTable(json); } }, error: function(obj, status, message){ var _msg = opts.messages ? opts.messages.error : ''; var _err = 'Error: ' + message; showMessage().error(_msg); } }); } else { createTable(tableData.data||tableData); // newTable.init(tableData); } if (opts.container) { $$(opts.container).append(tableContainer); } // add properties for Spawner compatibility newTable.element = newTable.spawned = tableContainer; newTable.get = function(){ return tableContainer; }; return tableContainer; }; // table with <input> elements in the cells table.inputTable = function(data, opts){ var tableData = data; // tolerate reversed arguments if (Array.isArray(opts)){ tableData = opts; opts = data; } tableData = tableData.map(function(row){ return row.map(function(cell){ if (/string|number/.test(typeof cell)) { return cell + '' } if (Array.isArray(cell)) { return element('input', extend(true, {}, cell[2], { name: cell[0], value: cell[1], data: { value: cell[1] } })); } cell = extend(true, cell, { data: {value: cell.value} }); return element('input', cell); }); }); opts = getObject(opts); addClassName(opts, 'input-table'); var newTable = new Table(opts); return newTable.init(tableData); }; XNAT.ui = getObject(XNAT.ui||{}); XNAT.ui.table = XNAT.table = table; XNAT.ui.inputTable = XNAT.inputTable = table.inputTable; }));