/*! * FooTable - Awesome Responsive Tables * http://themergency.com/footable * * Requires jQuery - http://jquery.com/ * * Copyright 2012 Steven Usher & Brad Vincent * Released under the MIT license * You are free to use FooTable in commercial projects as long as this copyright header is left intact. * * Date: 18 Nov 2012 */ (function ($, w, undefined) { w.footable = { options: { delay: 100, // The number of millseconds to wait before triggering the react event breakpoints: { // The different screen resolution breakpoints phone: 480, tablet: 1024 }, parsers: { // The default parser to parse the value out of a cell (values are used in building up row detail) alpha: function (cell) { return $(cell).data('value') || $.trim($(cell).text()); } }, toggleSelector: '.detail-row', //the selector to show/hide the detail row createDetail: function (element, data) { //creates the contents of the detail row for (var i = 0; i < data.length; i++) { element.append('
' + data[i].name + ' : ' + data[i].display + '
'); } }, classes: { loading : 'footable-loading', loaded : 'footable-loaded', sorted : 'footable-sorted', descending : 'footable-sorted-desc', indicator : 'footable-sort-indicator' }, debug: false // Whether or not to log information to the console. }, version: { major: 0, minor: 1, toString: function () { return w.footable.version.major + '.' + w.footable.version.minor; }, parse: function (str) { version = /(\d+)\.?(\d+)?\.?(\d+)?/.exec(str); return { major: parseInt(version[1]) || 0, minor: parseInt(version[2]) || 0, patch: parseInt(version[3]) || 0 }; } }, plugins: { _validate: function (plugin) { ///Simple validation of the to make sure any members called by Foobox actually exist. ///The object defining the plugin, this should implement a string property called "name" and a function called "init". if (typeof plugin['name'] !== 'string') { if (w.footable.options.debug == true) console.error('Validation failed, plugin does not implement a string property called "name".', plugin); return false; } if (!$.isFunction(plugin['init'])) { if (w.footable.options.debug == true) console.error('Validation failed, plugin "' + plugin['name'] + '" does not implement a function called "init".', plugin); return false; } if (w.footable.options.debug == true) console.log('Validation succeeded for plugin "' + plugin['name'] + '".', plugin); return true; }, registered: [], // An array containing all registered plugins. register: function (plugin, options) { ///Registers a and its default with Foobox. ///The plugin that should implement a string property called "name" and a function called "init". ///The default options to merge with the Foobox's base options. if (w.footable.plugins._validate(plugin)) { w.footable.plugins.registered.push(plugin); if (options != undefined && typeof options === 'object') $.extend(true, w.footable.options, options); if (w.footable.options.debug == true) console.log('Plugin "' + plugin['name'] + '" has been registered with the Foobox.', plugin); } }, init: function (instance) { ///Loops through all registered plugins and calls the "init" method supplying the current of the Foobox as the first parameter. ///The current instance of the Foobox that the plugin is being initialized for. for(var i = 0; i < w.footable.plugins.registered.length; i++){ try { w.footable.plugins.registered[i]['init'](instance); } catch(err) { if (w.footable.options.debug == true) console.error(err); } } } } }; var instanceCount = 0; $.fn.footable = function(options) { ///The main constructor call to initialize the plugin using the supplied . /// ///A JSON object containing user defined options for the plugin to use. Any options not supplied will have a default value assigned. ///Check the documentation or the default options object above for more information on available options. /// options=options||{}; var o=$.extend(true,{},w.footable.options,options); //merge user and default options return this.each(function () { instanceCount++; this.footable = new Footable(this, o, instanceCount); }); }; //helper for using timeouts function Timer() { ///Simple timer object created around a timeout. var t=this; t.id=null; t.busy=false; t.start=function (code,milliseconds) { ///Starts the timer and waits the specified amount of before executing the supplied . ///The code to execute once the timer runs out. ///The time in milliseconds to wait before executing the supplied . if (t.busy) {return;} t.stop(); t.id=setTimeout(function () { code(); t.id=null; t.busy=false; },milliseconds); t.busy=true; }; t.stop=function () { ///Stops the timer if its runnning and resets it back to its starting state. if(t.id!=null) { clearTimeout(t.id); t.id=null; t.busy=false; } }; }; function Footable(t, o, id) { ///Inits a new instance of the plugin. ///The main table element to apply this plugin to. ///The options supplied to the plugin. Check the defaults object to see all available options. ///The id to assign to this instance of the plugin. var ft = this; ft.id = id; ft.table = t; ft.options = o; ft.breakpoints = []; ft.breakpointNames = ''; ft.columns = { }; var opt = ft.options; var cls = opt.classes; // This object simply houses all the timers used in the footable. ft.timers = { resize: new Timer(), register: function (name) { ft.timers[name] = new Timer(); return ft.timers[name]; } }; w.footable.plugins.init(ft); ft.init = function() { var $window = $(w), $table = $(ft.table); if ($table.hasClass(cls.loaded)) { //already loaded FooTable for the table, so don't init again ft.raise('footable_already_initialized'); return; } $table.addClass(cls.loading); // Get the column data once for the life time of the plugin $table.find('> thead > tr > th, > thead > tr > td').each(function() { var data = ft.getColumnData(this); ft.columns[data.index] = data; var count = data.index + 1; //get all the cells in the column var $column = $table.find('> tbody > tr > td:nth-child(' + count + ')'); //add the className to the cells specified by data-class="blah" if (data.className != null) $column.not('.footable-cell-detail').addClass(data.className); }); // Create a nice friendly array to work with out of the breakpoints object. for(var name in opt.breakpoints) { ft.breakpoints.push({ 'name': name, 'width': opt.breakpoints[name] }); ft.breakpointNames += (name + ' '); } // Sort the breakpoints so the smallest is checked first ft.breakpoints.sort(function(a, b) { return a['width'] - b['width']; }); //bind the toggle selector click events ft.bindToggleSelectors(); ft.raise('footable_initializing'); $table.bind('footable_initialized', function (e) { //resize the footable onload ft.resize(); //remove the loading class $table.removeClass(cls.loading); //hides all elements within the table that have the attribute data-hide="init" $table.find('[data-init="hide"]').hide(); $table.find('[data-init="show"]').show(); //add the loaded class $table.addClass(cls.loaded); }); $window .bind('resize.footable', function () { ft.timers.resize.stop(); ft.timers.resize.start(function() { ft.raise('footable_resizing'); ft.resize(); ft.raise('footable_resized'); }, opt.delay); }); ft.raise('footable_initialized'); }; //moved this out into it's own function so that it can be called from other add-ons ft.bindToggleSelectors = function() { var $table = $(ft.table); $table.find(opt.toggleSelector).unbind('click.footable').bind('click.footable', function (e) { if ($table.is('.breakpoint')) { var $row = $(this).is('tr') ? $(this) : $(this).parents('tr:first'); ft.toggleDetail($row.get(0)); } return false; }); }; ft.parse = function(cell, column) { var parser = opt.parsers[column.type] || opt.parsers.alpha; return parser(cell); }; ft.getColumnData = function(th) { var $th = $(th), hide = $th.data('hide'); hide = hide || ''; hide = hide.split(','); var data = { 'index': $th.index(), 'hide': { }, 'type': $th.data('type') || 'alpha', 'name': $th.data('name') || $.trim($th.text()), 'ignore': $th.data('ignore') || false, 'className': $th.data('class') || null }; data.hide['default'] = ($th.data('hide')==="all") || ($.inArray('default', hide) >= 0); for(var name in opt.breakpoints) { data.hide[name] = ($th.data('hide')==="all") || ($.inArray(name, hide) >= 0); } var e = ft.raise('footable_column_data', { 'column': { 'data': data, 'th': th } }); return e.column.data; }; ft.getViewportWidth = function() { return window.innerWidth || (document.body ? document.body.offsetWidth : 0); }; ft.getViewportHeight = function() { return window.innerHeight || (document.body ? document.body.offsetHeight : 0); }; ft.hasBreakpointColumn = function(breakpoint) { for(var c in ft.columns) { if (ft.columns[c].hide[breakpoint]) { return true; } } return false; }; ft.resize = function() { var $table = $(ft.table); var info = { 'width': $table.width(), //the table width 'height': $table.height(), //the table height 'viewportWidth': ft.getViewportWidth(), //the width of the viewport 'viewportHeight': ft.getViewportHeight(), //the width of the viewport 'orientation': null }; info.orientation = info.viewportWidth > info.viewportHeight ? 'landscape' : 'portrait'; if (info.viewportWidth < info.width) info.width = info.viewportWidth; if (info.viewportHeight < info.height) info.height = info.viewportHeight; var pinfo = $table.data('footable_info'); $table.data('footable_info', info); // This (if) statement is here purely to make sure events aren't raised twice as mobile safari seems to do if (!pinfo || ((pinfo && pinfo.width && pinfo.width != info.width) || (pinfo && pinfo.height && pinfo.height != info.height))) { var current = null, breakpoint; for (var i = 0; i < ft.breakpoints.length; i++) { breakpoint = ft.breakpoints[i]; if (breakpoint && breakpoint.width && info.width <= breakpoint.width) { current = breakpoint; break; } } var breakpointName = (current == null ? 'default' : current['name']); var hasBreakpointFired = ft.hasBreakpointColumn(breakpointName); $table .removeClass('default breakpoint').removeClass(ft.breakpointNames) .addClass(breakpointName + (hasBreakpointFired ? ' breakpoint' : '')) .find('> thead > tr > th').each(function() { var data = ft.columns[$(this).index()]; var count = data.index + 1; //get all the cells in the column var $column = $table.find('> tbody > tr > td:nth-child(' + count + '), > tfoot > tr > td:nth-child(' + count + '), > colgroup > col:nth-child(' + count + ')').add(this); if (data.hide[breakpointName] == false) $column.show(); else $column.hide(); }) .end() .find('> tbody > tr.footable-detail-show').each(function() { ft.createOrUpdateDetailRow(this); }); $table.find('> tbody > tr.footable-detail-show:visible').each(function() { var $next = $(this).next(); if ($next.hasClass('footable-row-detail')) { if (breakpointName == 'default' && !hasBreakpointFired) $next.hide(); else $next.show(); } }); // adding .footable-last-column to the last th and td in order to allow for styling if the last column is hidden (which won't work using :last-child) $table.find('> thead > tr > th.footable-last-column,> tbody > tr > td.footable-last-column').removeClass('footable-last-column'); $table.find('> thead > tr > th:visible:last,> tbody > tr > td:visible:last').addClass('footable-last-column'); ft.raise('footable_breakpoint_' + breakpointName, { 'info': info }); } }; ft.toggleDetail = function(actualRow) { var $row = $(actualRow), created = ft.createOrUpdateDetailRow($row.get(0)), $next = $row.next(); if (!created && $next.is(':visible')) { $row.removeClass('footable-detail-show'); //only hide the next row if it's a detail row if($next.hasClass('footable-row-detail')) $next.find('.footable-row-detail-inner').slideUp(300, function() { $next.hide(); }); } else { $row.addClass('footable-detail-show'); $next.show(); $next.find('.footable-row-detail-inner').slideDown(300); } }; ft.createOrUpdateDetailRow = function (actualRow) { var $row = $(actualRow), $next = $row.next(), $detail, values = []; if ($row.is(':hidden')) return; //if the row is hidden for some readon (perhaps filtered) then get out of here $row.find('> td:hidden').each(function () { var column = ft.columns[$(this).index()]; if (column.ignore == true) return true; values.push({ 'name': column.name, 'value': ft.parse(this, column), 'display': $.trim($(this).html()) }); }); if(values.length == 0) //return if we don't have any data to show return; var colspan = $row.find('> td:visible').length; var exists = $next.hasClass('footable-row-detail'); if (!exists) { // Create $next = $('
'); $row.after($next); } $next.find('> td:first').attr('colspan', colspan); $detail = $next.find('.footable-row-detail-inner').empty(); opt.createDetail($detail, values); return !exists; }; ft.raise = function(eventName, args) { args = args || { }; var def = { 'ft': ft }; $.extend(true, def, args); var e = $.Event(eventName, def); if (!e.ft) { $.extend(true, e, def); } //pre jQuery 1.6 which did not allow data to be passed to event object constructor $(ft.table).trigger(e); return e; }; ft.init(); return ft; }; })(jQuery, window);