UNPKG

jquery.tabulator

Version:

Interactive table generation plugin for jQuery UI

2,006 lines (1,491 loc) 127 kB
/* * This file is part of the Tabulator package. * * (c) Oliver Folkerd <oliver.folkerd@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * Full Documentation & Demos can be found at: http://olifolkerd.github.io/tabulator/ * */ (function(){ 'use strict'; //polyfill for Array.find method if (!Array.prototype.find) { Array.prototype.find = function (predicate, thisValue) { var arr = Object(this); if (typeof predicate !== 'function') { throw new TypeError(); } for(var i=0; i < arr.length; i++) { if (i in arr) { var elem = arr[i]; if (predicate.call(thisValue, elem, i, arr)) { return elem; } } } return undefined; } } $.widget("ui.tabulator", { data:[],//array to hold data for table activeData:[],//array to hold data that is active in the DOM selectedRows:[], //array to hold currently selected rows selecting:false, //is selection currently happening selectPrev:[], //hold jQuery element for previously selected row to handle changing direction when selecting firstRender:true, //layout table widths correctly on first render mouseDrag:false, //mouse drag tracker; mouseDragWidth:false, //starting width of colum on mouse drag mouseDragElement:false, //column being dragged mouseDragOut:false, //catch to prevent mouseup on col drag triggering click on sort sortCurCol:null,//column name of currently sorted column sortCurDir:null,//column name of currently sorted column filterField:null, //field to be filtered on data render filterValue:null, //value to match on filter filterType:null, //filter type paginationCurrentPage:1, // pagination page paginationMaxPage:1, // pagination maxpage progressiveRenderTimer:null, //timer for progressiver rendering progressiveRenderFill:false, //initial fill of a progressive rendering table columnList:[], //an array of all acutal columns ignoring column groupings responsiveColumnList:[], //array of responsive columns responsiveColumnIndex:0, //current possition in responsive array columnFrozenLeft:[], //list of frozen columns left columnFrozenRight:[], //list of frozen columns right loaderDiv: $("<div class='tablulator-loader'><div class='tabulator-loader-msg'></div></div>"), //loader blockout div lang:{}, // hold current locale text defaultLang:{ //hold default locale text "columns":{ }, "pagination":{ "first":"First", "first_title":"First Page", "last":"Last", "last_title":"Last Page", "prev":"Prev", "prev_title":"Prev Page", "next":"Next", "next_title":"Next Page", }, "headerFilters":{ "default":"filter column...", "columns":{ } } }, //setup options options: { colMinWidth:"40px", //minimum global width for a column colResizable:true, //resizable columns colVertAlign:"top", //vertical alignment of column headers height:false, //height of tabulator fitColumns:false, //fit colums to width of screen; movableCols:false, //enable movable columns movableRows:false, //enable movable rows movableRowHandle:"<div></div><div></div><div></div>", //handle for movable rows locale:"en-gb", //durrent system language langs:{}, persistentLayout:false, //store cookie with column _styles persistentLayoutID:"", //id for stored cookie pagination:false, //enable pagination paginationSize:false, //size of pages paginationElement:false, //element to hold pagination numbers paginationDataReceived:{ //pagination data received from the server "current_page":"current_page", "last_page":"last_page", "data":"data", }, paginationDataSent:{ //pagination data sent to the server "page":"page", "size":"size", "sort":"sort", "sort_dir":"sort_dir", "filter":"filter", "filter_value":"filter_value", "filter_type":"fitler_type", }, paginator:false, //pagination url string builder progressiveRender:false, //enable progressive rendering progressiveRenderSize:20, //block size for progressive rendering progressiveRenderMargin:200, //disance in px before end of scroll before progressive render is triggered headerFilterPlaceholder: "", //placeholder text to display in header filters headerFilterColumnPlaceholders:{}, //placeholders by column tooltips: false, //Tool tip value tooltipsHeader: false, //Tool tip for headers columns:false,//store for colum header info data:false, //store for initial table data if set at construction index:"id", sortable:true, //global default for sorting dateFormat: "dd/mm/yyyy", //date format to be used for sorting sortBy:"id", //defualt column to sort by sortDir:"desc", //default sort direction groupBy:false, //enable table grouping and set field to group by groupStartOpen:true, //starting state of group groupHeader:function(value, count, data){ //header layout function return value + "<span>(" + count + " " + ((count === 1) ? "item" : "items") + ")</span>"; }, rowFormatter:false, //row formatter callback addRowPos:"bottom", //position to insert blank rows, top|bottom selectable:"highlight", //highlight rows on hover selectableRollingSelection:true, //roll selection once maximum number of selectable rows is reached selectablePersistence:true, // maintain selection when table view is updated selectableCheck:function(data, row){return true;}, //check wheather row is selectable responsiveLayout:false, //enable responsive column layout ajaxURL:false, //url for ajax loading ajaxParams:{}, //params for ajax loading ajaxConfig:"get", //ajax request type showLoader:true, //show loader while data loading loader:"<div class='tabulator-loading'>Loading Data</div>", //loader element loaderError:"<div class='tabulator-error'>Loading Error</div>", //loader element //Callbacks from events rowClick:function(){}, rowDblClick:function(){}, rowAdded:function(){}, rowDeleted:function(){}, rowContext:function(){}, rowMoved:function(){}, rowUpdated:function(){}, rowSelectionChanged:function(){}, cellEdited:function(){}, colMoved:function(){}, colTitleChanged:function(){}, dataLoading:function(){}, dataLoaded:function(){}, dataLoadError:function(){}, dataEdited:function(){}, ajaxResponse:false, dataFiltering:function(){}, dataFiltered:function(){}, dataSorting:function(){}, dataSorted:function(){}, renderStarted:function(){}, renderComplete:function(){}, pageLoaded:function(){}, localized:function(){}, tableBuilding:function(){}, tableBuilt:function(){}, }, ////////////////// Element Construction ////////////////// //constructor _create: function(){ var self = this; var element = self.element; //initialize arrays self.selectedRows = []; self.selectPrev = []; self.columnList = []; //prevent column array being copied over when not explicitly set if(!self.options.columns){ self.options.columns = []; } if(element.is("table")){ self._parseTable(); }else{ self._buildElement(); } }, //parse table element to create data set _parseTable:function(){ var self = this; var element = self.element; var options = self.options; var rows = $("tbody tr", element); var headers = $("th", element); var hasIndex = false; var columns = options.columns; //find column if it has already been defined function search(title){ var match = false; $.each(columns, function(index, column) { if(column.title === title){ match = column; return false; } }); return match; } //get attributes of cell var attributes = element[0].attributes; function attribValue(value){ if(value === "true"){ return true; } if(value === "false"){ return false; } return value; } //check for tablator inline options for(var index in attributes){ var attrib = attributes[index]; var name; if(attrib && attrib.name && attrib.name.indexOf("tabulator-") === 0){ name = attrib.name.replace("tabulator-", ""); for(var key in options){ if(key.toLowerCase() == name){ options[key] = attribValue(attrib.value); } } } } //build columns from table header if they havnt been set; if(headers.length){ //list of possible attributes var attribList = ["title", "field", "align", "width", "minWidth", "frozen", "sortable", "sorter", "formatter", "onClick", "onDblClick", "onContext", "editable", "editor", "visible", "cssClass", "tooltip", "tooltipHeader", "editableTitle", "headerFilter", "mutator", "mutateType", "accessor"]; //create column array from headers headers.each(function(index){ var header = $(this); var exists = false; var attributes = header[0].attributes; var col = search(header.text()); if(col){ exists = true; }else{ col = {title:header.text()}; } if(!col.field) { col.field = header.text().toLowerCase().replace(" ", "_"); } $("td:eq(" + index + ")", rows).data("field", col.field) var width = header.attr("width"); if(width && !col.width) { col.width = width; } if(col.field == options.index){ hasIndex = true; } //check for tablator inline options for(var index in attributes){ var attrib = attributes[index]; var name; if(attrib && attrib.name && attrib.name.indexOf("tabulator-") === 0){ name = attrib.name.replace("tabulator-", ""); attribList.forEach(function(key){ if(key.toLowerCase() == name){ col[key] = attribValue(attrib.value); } }); } } if(!exists){ columns.push(col) } }); }else{ //create blank table headers headers = $("tr:first td", element); headers.each(function(index){ var col = {title:"", field:"col" + index}; $("td:eq(" + index + ")", rows).data("field", col.field) var width = $(this).attr("width"); if(width){ col.width = width; } columns.push(col); }); } self.data = []; //iterate through table rows and build data set rows.each(function(rowIndex){ var item = {}; //create index if the dont exist in table if(!hasIndex){ item[options.index] = rowIndex; } //add row data to item $("td", $(this)).each(function(colIndex){ item[$(this).data("field")] = $(this).html(); }); self.data.push(item); }); //create new element var newElement = $("<div></div>"); //transfer attributes to new element var attributes = element.prop("attributes"); // loop through attributes and apply them on div $.each(attributes, function(){ newElement.attr(this.name, this.value); }); // replace table with div element element.replaceWith(newElement); options.data = self.data; newElement.tabulator(options); }, //build tabulator element _buildElement: function(){ var self = this; var options = self.options; var element = self.element; options.tableBuilding(); //set current locale self.setLocale(self.options.locale); //// backwards compatability options adjustments //// //old persistan column layout adjustment if( typeof options.columnLayoutCookie != 'undefined'){ options.persistentLayout = options.columnLayoutCookie; options.persistentLayoutID = options.columnLayoutCookieID; } //ajax type backwards compatability if(options.ajaxType){ options.ajaxConfig = options.ajaxType; } ///////////////////////////////////////////////////// //setup persistent layout storage if needed if(self.options.persistentLayout){ //determine persistent layout storage type self.options.persistentLayout = self.options.persistentLayout !== true ? self.options.persistentLayout : (typeof window.localStorage !== 'undefined' ? "local" : "cookie"); //set storage tag self.options.persistentLayoutID = "tabulator-" + (self.options.persistentLayoutID ? self.options.persistentLayoutID : self.element.attr("id") ? self.element.attr("id") : ""); } options.colMinWidth = isNaN(options.colMinWidth) ? options.colMinWidth : options.colMinWidth + "px"; if(options.height){ options.height = isNaN(options.height) ? options.height : options.height + "px"; element.css({"height": options.height}); } element.addClass("tabulator").attr("role", "grid"); element.empty(); self.header = $("<div class='tabulator-header'></div>") self.tableHolder = $("<div class='tabulator-tableHolder'></div>"); var scrollTop = 0; var scrollLeft = 0; self.tableHolder.scroll(function(){ //scroll header along with table body var holder = $(this); var left = holder.scrollLeft(); self.header.scrollLeft(left); var hozAdjust = 0; //adjust for vertical scrollbar moving table when present var scrollWidth = self.header[0].scrollWidth - self.element.innerWidth(); if(left > scrollWidth){ hozAdjust = left - scrollWidth self.header.css("margin-left", -(hozAdjust)); }else{ self.header.css("margin-left", 0); } //keep frozen columns fixed in position self._calcFrozenColumnsPos(hozAdjust + 3); //trigger progressive rendering on scroll if(self.options.progressiveRender && scrollTop != holder.scrollTop() && scrollTop < holder.scrollTop()){ if(holder[0].scrollHeight - holder.innerHeight() - holder.scrollTop() < self.options.progressiveRenderMargin){ if(self.options.progressiveRender == "remote"){ if(self.paginationCurrentPage <= self.paginationMaxPage){ self._renderTable(true); } }else{ if(self.paginationCurrentPage < self.paginationMaxPage){ self.paginationCurrentPage++; self._renderTable(true); } } } } scrollTop = holder.scrollTop(); }); //create scrollable table holder self.table = $("<div class='tabulator-table'></div>"); //build pagination footer if needed if(options.pagination){ if(options.pagination === true){ options.pagination = "local"; //convert old pagination style to new } if(!options.paginationElement){ options.paginationElement = $("<div class='tabulator-footer'></div>"); self.footer = options.paginationElement; } self.paginator = $("<span class='tabulator-paginator'><span class='tabulator-page' data-page='first' role='button' aria-label='" + self.lang.pagination.first_title + "' title='" + self.lang.pagination.first_title + "'>" + self.lang.pagination.first + "</span><span class='tabulator-page' data-page='prev' role='button' aria-label='" + self.lang.pagination.prev_title + "' title='" + self.lang.pagination.prev_title + "'>" + self.lang.pagination.prev + "</span><span class='tabulator-pages'></span><span class='tabulator-page' data-page='next' role='button' aria-label='" + self.lang.pagination.next_title + "' title='" + self.lang.pagination.next_title + "'>" + self.lang.pagination.next + "</span><span class='tabulator-page' data-page='last' role='button' aria-label='" + self.lang.pagination.last_title + "' title='" + self.lang.pagination.last_title + "'>" + self.lang.pagination.last + "</span></span>"); self.paginator.on("click", ".tabulator-page", function(){ if(!$(this).hasClass("disabled")){ self.setPage($(this).data("page")); } }); options.paginationElement.append(self.paginator); } //layout columns if(options.persistentLayout){ self._getPersistentCol(); }else{ self._colLayout(); } }, //set options _setOption: function(option, value){ var self = this; //block update if option cannot be updated this way if(["columns"].indexOf(option) > -1){ return false; } //set option to value $.Widget.prototype._setOption.apply(this, arguments); //trigger appropriate table response if(["colMinWidth", "colResizable", "fitColumns", "movableCols", "movableRows", "movableRowHandle", "sortable", "groupBy", "groupHeader", "rowFormatter", "selectable"].indexOf(option) > -1){ //triger rerender self._renderTable(); }else if(["height", "pagination", "paginationSize", "tooltips"].indexOf(option) > -1){ //triger render/reset page if(self.options.pagination){ self.setPage(1); }else{ self._renderTable(); } }else if(["dateFormat", "sortBy", "sortDir"].indexOf(option) > -1){ //trigger sort if(self.sortCurCol){ self.sort(self.sortCurCol, self.sortCurDir); } }else if(["index"].indexOf(option) > -1){ //trigger reparse data self._parseData(self.data); }else if(["paginationElement"].indexOf(option) > -1){ //trigger complete redraw } }, ////////////////// Localization Functions ////////////////// //set current locale setLocale:function(desiredLocale){ var self = this; var locale = false; //hold the matching locale //fill in any matching languge values function traverseLang(trans, path){ for(var prop in trans){ if(typeof trans[prop] == "object"){ if(!path[prop]){ path[prop] = {}; } traverseLang(trans[prop], path[prop]); }else{ path[prop] = trans[prop]; } } } //determing correct locale to load if(desiredLocale === true && navigator.language){ //get local from system desiredLocale = navigator.language.toLowerCase(); } if(desiredLocale){ if(self.options.langs[desiredLocale]){ locale = desiredLocale; }else{ //see if matching top level local is present var prefix = desiredLocale.split("-")[0]; if(self.options.langs[prefix]){ locale = prefix; } } } if(self.options.headerFilterPlaceholder){ self.defaultLang.headerFilters.default = self.options.headerFilterPlaceholder; } if(self.options.headerFilterColumnPlaceholders){ self.defaultLang.headerFilters.columns = self.options.headerFilterColumnPlaceholders; } //load default lang template self.lang = $.extend(true, [], self.defaultLang); if(locale){ traverseLang(self.options.langs[locale], self.lang); } self.options.locale = locale; //update ui elements that need translating if(!self.firstRender){ self._updateLocaleText(); } //triger localized callback self.options.localized(locale, self.lang); return locale; }, //quick update of elements with local based text _updateLocaleText:function(){ var self = this; //update column titles self.columnList.forEach(function(column){ if(column.field){ $(".tabulator-col[data-field=" + column.field + "] .tabulator-col-title", self.header).text(self.lang.columns[column.field] || column.title); } }); //update pagination if enabled if(self.options.pagination){ for(var prop in self.lang.pagination){ var propParts = prop.split("_"); var element = $(".tabulator-paginator .tabulator-page[data-page=" + propParts[0] + "]", self.element); if (propParts.length > 1){ element.attr("title",self.lang.pagination[prop]) .attr("aria-label",self.lang.pagination[prop]); }else{ element.text(self.lang.pagination[prop]); } } } //redraw incase column headers change width self.redraw(true); }, //return the current locale getLocale:function(){ var self = this; return self.options.locale; }, //return the language definitions for the curent locale getLang:function(){ var self = this; var lang = self.options.langs[self.options.locale] return lang ? lang : false; }, ////////////////// General Public Functions ////////////////// //get number of elements in dataset dataCount:function(){ return this.data.length; }, //redraw list without updating data redraw:function(fullRedraw){ var self = this; //redraw columns if(self.options.fitColumns || self.options.responsiveLayout || fullRedraw){ self._colRender(); } //reposition loader if present if(self.element.innerHeight() > 0){ var msg = $(".tabulator-loader-msg", self.loaderDiv); msg.css({"margin-top":(self.element.innerHeight() / 2) - (msg.outerHeight()/2)}) } self._calcFrozenColumnsPos(); //trigger row restyle self._styleRows(true); if(fullRedraw){ self._renderTable(); } }, //trigger file download download:function(type, filename, options){ var self = this; //create temporary link element to trigger download var element = document.createElement('a'); if(typeof type === "function"){ //create the download link element.setAttribute('href', type(self.columnList, self.activeData, options)); }else{ switch(type){ case "csv": var delimiter = options && options.delimiter ? options.delimiter : ","; //get field lists var titles = []; var fields = []; self.columnList.forEach(function(column){ if(column.field){ titles.push('"' + String(column.title).split('"').join('""') + '"'); fields.push(column.field); } }) //generate header row var fileContents = [titles.join(delimiter)]; //generate each row of the table self.activeData.forEach(function(row){ var rowData = []; fields.forEach(function(field){ var value = typeof row[field] == "object" ? JSON.stringify(row[field]) : row[field]; //escape uotation marks rowData.push('"' + String(value).split('"').join('""') + '"'); }) fileContents.push(rowData.join(delimiter)); }); //create the download link element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(fileContents.join("\n"))); break; case "json": var fileContents = JSON.stringify(self.activeData, null, '\t'); //create the download link element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(fileContents)); break; default: return false; break; } } //set file title element.setAttribute('download', filename || "Tabulator." + (typeof type === "function" ? "txt" : type)); //trigger download element.style.display = 'none'; document.body.appendChild(element); element.click(); //remove temporary link element document.body.removeChild(element); return true; }, ////////////////// Column Manipulation ////////////////// //set column style cookie _setPersistentCol:function(){ var self = this; //parse styles from columns function parseCols(columns){ var cols = []; columns.forEach(function(column){ var style = { field: column.field, width: column.width, visible: column.visible, }; if(column.columns){ style.title = column.title; style.columns = parseCols(column.columns); } cols.push(style); }) return cols; } //create array of column styles only var columnStyles = parseCols(self.options.columns); //JSON format column data var data = JSON.stringify(columnStyles); if(self.options.persistentLayout == "cookie"){ //set cookie expiration far in the future var expDate = new Date(); expDate.setDate(expDate.getDate() + 10000); //save cookie document.cookie = self.options.persistentLayoutID + "=" + data + "; expires=" + expDate.toUTCString(); }else{ //save data to local storage localStorage.setItem(self.options.persistentLayoutID, data); } }, //set Column style cookie _getPersistentCol:function(){ var self = this; var colString = ""; if(self.options.persistentLayout == "cookie"){ //find cookie var cookie = document.cookie; var cookiePos = cookie.indexOf(self.options.persistentLayoutID + "="); //if cookie exists, decode and load column data into tabulator if(cookiePos > -1){ cookie = cookie.substr(cookiePos); var end = cookie.indexOf(";"); if(end > -1){ cookie = cookie.substr(0, end); } colString = cookie.replace(self.options.persistentLayoutID + "=", ""); } }else{ //find loocal storage value var colString = localStorage.getItem(self.options.persistentLayoutID); } if(colString){ self.setColumns(JSON.parse(colString), true); }else{ self._colLayout(); } }, //set tabulator columns setColumns: function(columns, update){ var self = this; //update column properties function updateCols(oldCols, newCols){ newCols.forEach(function(item, to){ var type = item.columns ? "group" : (item.field ? "field" : "object"); var from = search(oldCols, item, type); if(from !== false){ var column = oldCols.splice(from, 1)[0]; column.width = item.width; column.visible = item.visible; oldCols.splice(to , 0, column); if(type == "group"){ updateCols(column.columns, item.columns); } } }); } //find matching column function search(columns, col, type){ var match = false; $.each(columns, function(i, column){; switch(type){ case "group": if(col.title === column.title && col.columns.length === column.columns.length){ match = i; } break; case "field": if(col.field === column.field){ match = i; } break; case "object": if(col === column){ match = i; } break; } if(match !== false){ return false; } }); return match; } if(Array.isArray(columns)){ //if updating columns work through exisiting column data if(update){ updateCols(self.options.columns, columns); }else{ // if replaceing columns, replace columns array with new self.options.columns = columns; } //Trigger Redraw self._colLayout(true); if(self.options.persistentLayout){ self._setPersistentCol(); } } }, //get tabulator columns getColumns: function(){ var self = this; return self.options.columns; }, //add column addColumn:function(newCol, before, field){ var self = this; if(newCol){ if(field){ if(typeof field == "string"){ var match = self._findColumn(field); if(match){ match.parent.splice(before ? match.index : match.index + 1, 0, newCol); } }else{ var match = self._findColumn(self.columnList[field]); if(match){ match.parent.splice(before ? match.index : match.index + 1, 0, newCol); } } }else{ self.options.columns.splice(before ? 0 : self.options.columns.length + 1, 0, newCol); } self.setColumns(self.options.columns); } }, //delete column deleteColumn:function(field){ var self = this; if(field){ var match = self._findColumn(field); if(match){ match.parent.splice(match.index, 1); } self.setColumns(self.options.columns); } }, //traverse all columns and trigger callback on each column _traverseColumns:function(callback, columns){ var self = this; columns = columns ? columns : self.options.columns; $.each(columns, function(i, column){ if(column.columns){ self._traverseColumns(callback, column.columns); }else{ callback(column); } }); }, //find column _findColumn:function(field, columns){ var self = this; var match = false; columns = columns ? columns : self.options.columns; $.each(columns, function(i, column){ switch(typeof field){ case "object": if(column == field){ match = {parent:columns, column:column, index:i}; return false; } break; case "function": if(field(column)){ match = {parent:columns, column:column, index:i}; return false; } break; default: if(column.field == field){ match = {parent:columns, column:column, index:i}; return false; } } if(column.columns){ var children = self._findColumn(field, column.columns); if(children){ match = children; return false; } } }); return match; }, _setColGroupVisibility:function(column, visibility){ var self = this; var group = column.parent().closest(".tabulator-col-group"); console.log("group", visibility, group.length, group) if(group.length){ var visCols = $(".tabulator-col:visible", group); if(visCols.length || visibility){ group.show(); }else{ group.hide(); } self._setColGroupVisibility(group, visibility); } }, _setColVisibility:function(column, visibility){ var self = this; if(column){ column.visible = visibility; var elements = $(".tabulator-col[data-index=" + column.index + "], .tabulator-cell[data-index=" + column.index + "]", self.element) if(visibility){ elements.show(); }else{ elements.hide(); } self._setColGroupVisibility($(".tabulator-col[data-index=" + column.index + "]"), visibility); self._renderTable(); if(self.options.persistentLayout){ self._setPersistentCol(); } } }, //hide column hideCol: function(field){ var self = this; var match = self._findColumn(field); if(match){ self._setColVisibility(match.column, false); } }, //show column showCol: function(field){ var self = this; var match = self._findColumn(field); if(match){ self._setColVisibility(match.column, true); } }, //toggle column visibility toggleCol: function(field){ var self = this; var match = self._findColumn(field); if(match){ self._setColVisibility(match.column, !match.column.visible); } }, ////////////////// Row Manipulation ////////////////// //find row in data set _findRow: function(id){ var self = this; var row; if(typeof id == "object"){ row = self.data.find(function(item){ return item === id; }); }else{ row = self.data.find(function(item){ return item[self.options.index] == id; }); } return row; }, //delete row from table by id deleteRow: function(item){ var self = this; var itemType = typeof item; var id = (itemType == "number" || itemType == "string") ? item : item.data("data")[self.options.index]; var row = (itemType == "number" || itemType == "string") ? $("[data-id='" + item + "']", self.element) : item; if(row.length){ var rowData = row.data("data"); rowData.tabulator_delete_row = true; self.deselectRow(row); //remove from data var line = self.data.find(function(item){ return item.tabulator_delete_row; }); if(line){ line = self.data.indexOf(line); if(line > -1){ //remove row from data self.data.splice(line, 1); } } //remove from active data line = self.activeData.find(function(item){ return item.tabulator_delete_row; }); if(line){ line = self.activeData.indexOf(line); if(line > -1){ //remove row from data self.activeData.splice(line, 1); } } var group = row.closest(".tabulator-group"); row.remove(); if(self.options.groupBy){ var rows = $(".tabulator-row", group); var length = rows.length; if(length){ var data = []; rows.each(function(){ data.push($(this).data("data")); }); var header = $(".tabulator-group-header", group); var arrow = $(".tabulator-arrow", header).clone(true,true); header.empty(); header.append(arrow).append(self.options.groupHeader(group.data("value"), length, data)); }else{ group.remove(); } } //style table rows self._styleRows(); //align column widths self._colRender(!self.firstRender); self.options.rowDeleted(id); self.options.dataEdited(self.data); if(self.options.pagination){ self.paginationMaxPage = Math.max(1, Math.ceil(self.activeData.length/self.options.paginationSize)); self._layoutPageSelector(); self.setPage(self.paginationCurrentPage); } } }, //add blank row to table addRow:function(item, top, indexRowVal){ var self = this; var line, activeLine, indexRow, indexID; if(item){ item[self.options.index] = item[self.options.index] ? item[self.options.index] : 0; }else{ item = {id:0}; } //Apply mutators if present item = self._mutateData(item); //create blank row var row = self._renderRow(item); var top = typeof top == "undefined" ? self.options.addRowPos : top; if(top === "top"){ top = true; } if(top === "bottom"){ top = false; } //work out row to put new row next to if(typeof indexRowVal !== "undefined"){ indexRow = indexRowVal instanceof jQuery ? indexRowVal : $(".tabulator-row[data-id='" + indexRowVal + "']", self.element); indexID = indexRowVal instanceof jQuery ? indexRowVal.data("id") : indexRowVal; console.log("index defined", indexID, indexRow) } // initial place to append the row in the table, by default in the table row container var newRowContainer = self.table; // if group are used, look for the corresponding row of the group. if(!indexRow || !indexRow.length){ //create new line at top or bottom of table or group if(self.options.groupBy){ var groupVal = typeof(self.options.groupBy) == "function" ? self.options.groupBy(item) : item[self.options.groupBy]; var group = $(".tabulator-group[data-value='" + groupVal + "']", self.table); //if group does not exist, build it if(group.length == 0){ group = self._renderGroup(groupVal); self.table.append(group); self._renderGroupHeader(group); } //set the place to append the row to the corresponding group container. newRowContainer = group; } //append to top or bottom of table based on preference if(top){ if (self.activeData !== self.data){ self.activeData.unshift(item); } self.data.unshift(item); newRowContainer.prepend(row); }else{ if (self.activeData !== self.data){ self.activeData.push(item); } self.data.push(item); newRowContainer.append(row); } }else{ //insert new line next to existing row if (self.activeData !== self.data){ //remove from active data activeLine = self.activeData.find(function(item){ return item[self.options.index] == indexID; }); activeLine = self.activeData.indexOf(activeLine); } line = self.data.find(function(item){ return item[self.options.index] == indexID; }); line = self.data.indexOf(line); if(line > -1){ if(top){ if (self.activeData !== self.data){ self.activeData.splice(activeLine, 0, item); } self.data.splice(line, 0, item); if(indexRow){ indexRow.before(row); } }else{ if (self.activeData !== self.data){ self.activeData.splice(activeLine+1, 0, item); } self.data.splice(line+1, 0, item); if(indexRow){ indexRow.after(row); } } } } //align column widths self._colRender(!self.firstRender); //style table rows self._styleRows(); //triger event self.options.rowAdded(item, row); self.options.dataEdited(self.data); }, //update row if it exits or create if it dosnt updateOrAddRow:function(index, item){ var self = this; if(!self.updateRow(index, item)){ self.addRow(item); } }, //update row data updateRow:function(index, item, bulk){ var self = this; var itemType = typeof index; console.log("type", itemType, index) var id = (itemType == "number" || itemType == "string") ? index : index.data("data")[self.options.index]; var row = (itemType == "number" || itemType == "string") ? $("[data-id='" + index + "']", self.element) : index; if(row.length){ var rowData = row.data("data"); //Apply mutators if present item = self._mutateData(item); //makesure there are differences between the new and old data before updating if(JSON.stringify(rowData) !== JSON.stringify(item)){ //update row data for (var attrname in item) { rowData[attrname] = item[attrname]; } //render new row var newRow = self._renderRow(rowData); //replace old row with new row row.replaceWith(newRow); if(!bulk){ //align column widths self._colRender(!self.firstRender); //style table rows self._styleRows(); } //triger event self.options.rowUpdated(id, item, newRow); if(!bulk){ self.options.dataEdited(self.data); } } return true; } return false; }, //return jQuery object for row getRow:function(index){ var self = this; var row = $(".tabulator-row[data-id='" + index + "']", self.element); return row.length ? row : false; }, //scroll to sepcified row scrollToRow:function(item){ var self = this; var itemType = typeof item; var id = (itemType == "number" || itemType == "string") ? item : item.data("data")[self.options.index]; var row = (itemType == "number" || itemType == "string") ? $("[data-id='" + item + "']", self.element) : item; if(row){ self.tableHolder.stop(); self.tableHolder.animate({ scrollTop: row.offset().top - self.tableHolder.offset().top + self.tableHolder.scrollTop() }, 1000); } }, ////////////////// Data Manipulation ////////////////// //get array of data from the table getData:function(filteredData){ var self = this; //clone data array with deep copy to isolate internal data from returned result var outputData = $.extend(true, [], filteredData === true ? self.activeData: self.data); //check for accessors and return the processed data return self._applyAccessors(outputData); }, getRowData:function(row){ var self = this; var rowData; if(row instanceof jQuery){ rowData = row.data("data"); }else{ rowData = self.data.find(function(item){ return item[self.options.index] === row; }); } //clone data object with deep copy to isolate internal data from returned result var outputData = $.extend(true, [], rowData || []); //check for accessors and return the processed data return self._applyAccessors([outputData])[0]; }, //update existing data updateData:function(data, orAdd){ var self = this; if(data){ data.forEach(function(item){ //update each row in turn var success = self.updateRow(item[self.options.index], item, true); if(!success){ self.addRow(item); } }); //align column widths self._colRender(!self.firstRender); //style table rows self._styleRows(); self.options.dataEdited(self.data); } }, //update data if it exits or create if it dosnt updateOrAddData:function(data){ this.updateData(data, true); }, //apply any column accessors to the data before returing the result _applyAccessors:function(data){ var self = this; self._traverseColumns(function(col){ if(col.accessor){ var accessor = typeof col.accessor === "function" ? col.accessor : self.accessors[col.accessor]; data.forEach(function(item, j){ item[col.field] = accessor(item[col.field], item); }); } }) return data; }, ///////////////// Pagination Data Loading ////////////////// //parse paginated data _parsePageData:function(data, update){ var self = this; var options = self.options; self.paginationMaxPage = parseInt(data[options.paginationDataReceived.last_page]); self._layoutPageSelector(); self._parseData(data[options.paginationDataReceived.data], update); }, _getRemotePageData:function(update){ var self = this; var options = self.options; if(typeof options.paginator == "function"){ var url = options.paginator(options.ajaxURL, self.paginationCurrentPage, options.paginationSize, options.ajaxParams); self._getAjaxData(url, {}, null, update); }else{ var pageParams = {}; //clone ajax params into page request params for (var attrname in options.ajaxParams) { pageParams[attrname] = options.ajaxParams[attrname]; } //set page number pageParams[options.paginationDataSent.page] = self.paginationCurrentPage; //set page size if defined if(options.paginationSize){ pageParams[options.paginationDataSent.size] = options.paginationSize; } //set sort data if defined if(self.sortCurCol && typeof self.sortCurCol.field === "string"){ pageParams[options.paginationDataSent.sort] = self.sortCurCol.field; pageParams[options.paginationDataSent.sort_dir] = self.sortCurDir; } //set filter data if defined if(self.filterField && typeof self.filterField === "string"){ pageParams[options.paginationDataSent.filter] = self.filterField; pageParams[options.paginationDataSent.filter_value] = self.filterValue; pageParams[options.paginationDataSent.filter_type] = self.filterType; } self._getAjaxData(options.ajaxURL, pageParams, null, update); } }, ////////////////// Data Loading ////////////////// //load data setData:function(data, params, config){ var self = this; self.options.dataLoading(data, params); if(params){ self.options.ajaxParams = params; } //show loader if needed self._showLoader(this, this.options.loader); if(typeof(data) === "string"){ if (data.indexOf("{") == 0 || data.indexOf("[") == 0){ //data is a json encoded string self._parseData(jQuery.parseJSON(data)); }else{ self.options.ajaxURL = data; if(self.options.pagination == "remote"){ self.setPage(1); }else{ //assume data is url, make ajax call to url to get data self._getAjaxData(data, self.options.ajaxParams, config); } } }else{ if(data){ //asume data is already an object self._parseData(data); }else{ //no data provided, check if ajaxURL is present; if(this.options.ajaxURL){ if(self.options.pagination == "remote"){ self.setPage(1); }else{ self._getAjaxData(this.options.ajaxURL, self.options.ajaxParams, config); } }else{ //empty data self._parseData([]); } } } }, //clear data clear:function(){ this.table.empty(); this.data = []; this._filterData(); }, //get json data via ajax _getAjaxData:function(url, params, config, update){ var self = this; var options = self.options; var ajaxConfig = { url: url, type: "GET", data: params || self.options.ajaxParams, async: true, dataType:"json", success: function (data){ var returnedData = typeof self.options.ajaxResponse === "function" ? self.options.ajaxResponse(url, params ? params : self.options.ajaxParams, data) : data; if(self.options.pagination == "remote" || self.options.progressiveRender == "remote"){ self._parsePageData(returnedData, update); }else{ self._parseData(returnedData, update); } }, error: function (xhr, textStatus, errorThrown){ console.error("Tablulator ERROR (ajax get): " + xhr.status + " - " + errorThrown); self.options.dataLoadError(xhr, textStatus, errorThrown); self._showLoader(self, self.options.loaderError); }, } function configUpdate(configData){ if (configData){ if(typeof configData == "string"){ ajaxConfig.type = configData; }else if(typeof configData == "object"){ for(var key in configData){ ajaxConfig[key] = configData[key]; } } } } //configure ajax request configUpdate(self.options.ajaxConfig); configUpdate(config); $.ajax(ajaxConfig); }, //parse and index data _parseData:function(data, update){ var self = this; if(Array.isArray(data)){ var newData = []; if(data.length){ if(typeof(data[0][self.options.index]) == "undefined"){ self.options.index = "_index"; $.each(data, function(i, item){ newData[i] = item; newData[i]["_index"] = i; }); }else{ $.each(data, function(i, item){ newData.push(item); }); } } self.options.dataLoaded(newData); var mutatedData = self._mutateData(newData); if(update){ self.data = mutatedData; self.data = self.data.concat(mutatedData); self.activeData = self.activeData.concat(mutatedData); if(self.progressiveRenderFill){ self._renderTable(true); } }else{ self.data = mutatedData; self._clearSelection(); //filter incomming data self._filterData(); } } }, //applu mutators if present _mutateData:function(data){ var self = this; self._traverseColumns(function(col){ if(col.mutator && col.mutateType !== "edit"){ var mutator = typeof col.mutator === "function" ? col.mutator : self.mutators[col.mutator]; if(Array.isArray(data)){ data.forEach(function(item, j){ if(typeof item[col.field] != "undefined"){ item[col.field] = mutator(item[col.field], "data", item); } }); }else{ if(typeof data[col.field] != "undefined"){ data[col.field] = mutator(data[col.field], "data", data); } } } }); return data; }, ////////////////// Data Filtering ////////////////// //filter data in table setFilter:function(field, type, value){ var self = this; if(!self.options.selectablePersistence){ self._clearSelection(); } self.options.dataFiltering(field, type, value); //set filter if(field){ //set filter self.filterField = field; self.filterType = typeof(value) == "undefined" ? "=" : type; self.filterValue = typeof(value) == "undefined" ? type : value; }else{ //clear filter self.filterField = null; self.filterType = null; self.filterValue = null; } //render table this._filterData(); }, //clear filter clearFilter:function(){ var self = this; self.filterField = null; self.filterType = null; self.filterValue = null; //clear header filter values var headerFilters = $(".tabulator-header-filter", self.header); $("input, select", headerFilters).val(""); //render table this._filterData(); }, //get current filter info getFilter:function(){ var self = this; if(self.filterField){ var filter = { "field":self.filterField, "type":self.filterType, "value":self.filterValue, }; return filter; }else{ return false; } }, //filter data set _filterData:function(){ var self = this; //filter data set if(self.filterField){ self.activeData = self.data.filter(function(row){ return self._filterRow(row); }); }else{ self.activeData = self.data; } //set the max pages available given the filter results if(self.options.pagination == "local"){ self.paginationMaxPage = Math.max(1, Math.ceil(self.activeData.length/self.options.paginationSize)); } self.options.dataFiltered(self.activeData, self.filterField, self.filterType, self.filterValue); //sort or render data if(self.sortCurCol && self.options.pagination != "remote"){ self.sort(self.sortCurCol, self.sortCurDir); }else{ //determine pagination information / render table if(self.options.pagination == "local"){ self.setPage(1); }else{ self._renderTable(); } } }, //check if row data matches filter _filterRow:function(row){ var self = this; // if no filter set display row if(!self.filterField){ return true; }else{ if(typeof(self.filterField) == "function"){ return self.filterField(row); }else{ var value = row[self.filterField]; var term = self.filterValue; switch(self.filterType){ case "=": //equal to return value == term ? true : false; break; case "<": //less than return value < term ? true : false; break; case "<=": //less than or equal too return value <= term ? true : false; break; case ">": //greater than return value > term ? true : false; break; case ">=": //greater than or equal too return value >= term ? true : false; break; case "!=": //not equal to return value != term ? true : false; break; case "like": //text like if(value === null){ return term === value ? true : false; }else{ return value.toLowerCase().indexOf(term.toLowerCase()) > -1 ? true : false; } break; default: return false; } } } return false; }, ////////////////// Data Sorting ////////////////// //return the current sorter getSort:function(){ var self = this; return { "field" : self.sortCurCol ? self.sortCurCol.field : false, "dir" : self.sortCurDir || false, }; }, //handle user clicking on column header sort _sortClick: function(column, element){ var self