2413 lines
67 KiB
JavaScript
2413 lines
67 KiB
JavaScript
/*!
|
|
*
|
|
* Vanilla-DataTables
|
|
* Copyright (c) 2015-2017 Karl Saunders (http://mobius.ovh)
|
|
* Licensed under MIT (http://www.opensource.org/licenses/mit-license.php)
|
|
*
|
|
* Version: 1.6.13
|
|
*
|
|
*/
|
|
(function(root, factory) {
|
|
var plugin = "DataTable";
|
|
|
|
if (typeof exports === "object") {
|
|
module.exports = factory(plugin);
|
|
} else if (typeof define === "function" && define.amd) {
|
|
define([], factory(plugin));
|
|
} else {
|
|
root[plugin] = factory(plugin);
|
|
}
|
|
})(typeof global !== 'undefined' ? global : this.window || this.global, function(plugin) {
|
|
"use strict";
|
|
var win = window,
|
|
doc = document,
|
|
body = doc.body;
|
|
|
|
/**
|
|
* Default configuration
|
|
* @typ {Object}
|
|
*/
|
|
var defaultConfig = {
|
|
perPage: 10,
|
|
perPageSelect: [5, 10, 15, 20, 25],
|
|
|
|
sortable: true,
|
|
searchable: true,
|
|
|
|
// Pagination
|
|
nextPrev: true,
|
|
firstLast: false,
|
|
prevText: "‹",
|
|
nextText: "›",
|
|
firstText: "«",
|
|
lastText: "»",
|
|
ellipsisText: "…",
|
|
ascText: "▴",
|
|
descText: "▾",
|
|
truncatePager: true,
|
|
pagerDelta: 2,
|
|
|
|
fixedColumns: true,
|
|
fixedHeight: false,
|
|
|
|
header: true,
|
|
footer: false,
|
|
|
|
// Customise the display text
|
|
labels: {
|
|
placeholder: "Search...", // The search input placeholder
|
|
perPage: "{select} entries per page", // per-page dropdown label
|
|
noRows: "No entries found", // Message shown when there are no search results
|
|
info: "Showing {start} to {end} of {rows} entries" //
|
|
},
|
|
|
|
// Customise the layout
|
|
layout: {
|
|
top: "{select}{search}",
|
|
bottom: "{info}{pager}"
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check is item is object
|
|
* @return {Boolean}
|
|
*/
|
|
var isObject = function (val) {
|
|
return Object.prototype.toString.call(val) === "[object Object]";
|
|
};
|
|
|
|
/**
|
|
* Check is item is array
|
|
* @return {Boolean}
|
|
*/
|
|
var isArray = function (val) {
|
|
return Array.isArray(val);
|
|
};
|
|
|
|
/**
|
|
* Check for valid JSON string
|
|
* @param {String} str
|
|
* @return {Boolean|Array|Object}
|
|
*/
|
|
var isJson = function (str) {
|
|
var t = !1;
|
|
try {
|
|
t = JSON.parse(str);
|
|
} catch (e) {
|
|
return !1;
|
|
}
|
|
return !(null === t || (!isArray(t) && !isObject(t))) && t;
|
|
};
|
|
|
|
/**
|
|
* Merge objects (reccursive)
|
|
* @param {Object} r
|
|
* @param {Object} t
|
|
* @return {Object}
|
|
*/
|
|
var extend = function (src, props) {
|
|
for (var prop in props) {
|
|
if (props.hasOwnProperty(prop)) {
|
|
var val = props[prop];
|
|
if (val && isObject(val)) {
|
|
src[prop] = src[prop] || {};
|
|
extend(src[prop], val);
|
|
} else {
|
|
src[prop] = val;
|
|
}
|
|
}
|
|
}
|
|
return src;
|
|
};
|
|
|
|
/**
|
|
* Iterator helper
|
|
* @param {(Array|Object)} arr Any object, array or array-like collection.
|
|
* @param {Function} fn Callback
|
|
* @param {Object} scope Change the value of this
|
|
* @return {Void}
|
|
*/
|
|
var each = function (arr, fn, scope) {
|
|
var n;
|
|
if (isObject(arr)) {
|
|
for (n in arr) {
|
|
if (Object.prototype.hasOwnProperty.call(arr, n)) {
|
|
fn.call(scope, arr[n], n);
|
|
}
|
|
}
|
|
} else {
|
|
for (n = 0; n < arr.length; n++) {
|
|
fn.call(scope, arr[n], n);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add event listener to target
|
|
* @param {Object} el
|
|
* @param {String} e
|
|
* @param {Function} fn
|
|
*/
|
|
var on = function (el, e, fn) {
|
|
el.addEventListener(e, fn, false);
|
|
};
|
|
|
|
/**
|
|
* Create DOM element node
|
|
* @param {String} a nodeName
|
|
* @param {Object} b properties and attributes
|
|
* @return {Object}
|
|
*/
|
|
var createElement = function (a, b) {
|
|
var d = doc.createElement(a);
|
|
if (b && "object" == typeof b) {
|
|
var e;
|
|
for (e in b) {
|
|
if ("html" === e) {
|
|
d.innerHTML = b[e];
|
|
} else {
|
|
d.setAttribute(e, b[e]);
|
|
}
|
|
}
|
|
}
|
|
return d;
|
|
};
|
|
|
|
var flush = function (el, ie) {
|
|
if (el instanceof NodeList) {
|
|
each(el, function (e) {
|
|
flush(e, ie);
|
|
});
|
|
} else {
|
|
if (ie) {
|
|
while (el.hasChildNodes()) {
|
|
el.removeChild(el.firstChild);
|
|
}
|
|
} else {
|
|
el.innerHTML = "";
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create button helper
|
|
* @param {String} c
|
|
* @param {Number} p
|
|
* @param {String} t
|
|
* @return {Object}
|
|
*/
|
|
var button = function (c, p, t) {
|
|
return createElement("li", {
|
|
class: c,
|
|
html: '<a href="#" data-page="' + p + '">' + t + "</a>"
|
|
});
|
|
};
|
|
|
|
/**
|
|
* classList shim
|
|
* @type {Object}
|
|
*/
|
|
var classList = {
|
|
add: function (s, a) {
|
|
if (s.classList) {
|
|
s.classList.add(a);
|
|
} else {
|
|
if (!classList.contains(s, a)) {
|
|
s.className = s.className.trim() + " " + a;
|
|
}
|
|
}
|
|
},
|
|
remove: function (s, a) {
|
|
if (s.classList) {
|
|
s.classList.remove(a);
|
|
} else {
|
|
if (classList.contains(s, a)) {
|
|
s.className = s.className.replace(
|
|
new RegExp("(^|\\s)" + a.split(" ").join("|") + "(\\s|$)", "gi"),
|
|
" "
|
|
);
|
|
}
|
|
}
|
|
},
|
|
contains: function (s, a) {
|
|
if (s)
|
|
return s.classList ?
|
|
s.classList.contains(a) :
|
|
!!s.className &&
|
|
!!s.className.match(new RegExp("(\\s|^)" + a + "(\\s|$)"));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Bubble sort algorithm
|
|
*/
|
|
var sortItems = function (a, b) {
|
|
var c, d;
|
|
if (1 === b) {
|
|
c = 0;
|
|
d = a.length;
|
|
} else {
|
|
if (b === -1) {
|
|
c = a.length - 1;
|
|
d = -1;
|
|
}
|
|
}
|
|
for (var e = !0; e;) {
|
|
e = !1;
|
|
for (var f = c; f != d; f += b) {
|
|
if (a[f + b] && a[f].value > a[f + b].value) {
|
|
var g = a[f],
|
|
h = a[f + b],
|
|
i = g;
|
|
a[f] = h;
|
|
a[f + b] = i;
|
|
e = !0;
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Pager truncation algorithm
|
|
*/
|
|
var truncate = function (a, b, c, d, ellipsis) {
|
|
d = d || 2;
|
|
var j,
|
|
e = 2 * d,
|
|
f = b - d,
|
|
g = b + d,
|
|
h = [],
|
|
i = [];
|
|
if (b < 4 - d + e) {
|
|
g = 3 + e;
|
|
} else if (b > c - (3 - d + e)) {
|
|
f = c - (2 + e);
|
|
}
|
|
for (var k = 1; k <= c; k++) {
|
|
if (1 == k || k == c || (k >= f && k <= g)) {
|
|
var l = a[k - 1];
|
|
classList.remove(l, "active");
|
|
h.push(l);
|
|
}
|
|
}
|
|
each(h, function (c) {
|
|
var d = c.children[0].getAttribute("data-page");
|
|
if (j) {
|
|
var e = j.children[0].getAttribute("data-page");
|
|
if (d - e == 2) i.push(a[e]);
|
|
else if (d - e != 1) {
|
|
var f = createElement("li", {
|
|
class: "ellipsis",
|
|
html: '<a href="#">' + ellipsis + "</a>"
|
|
});
|
|
i.push(f);
|
|
}
|
|
}
|
|
i.push(c);
|
|
j = c;
|
|
});
|
|
|
|
return i;
|
|
};
|
|
|
|
/**
|
|
* Parse data to HTML table
|
|
*/
|
|
var dataToTable = function (data) {
|
|
var thead = false,
|
|
tbody = false;
|
|
|
|
data = data || this.options.data;
|
|
|
|
if (data.headings) {
|
|
thead = createElement("thead");
|
|
var tr = createElement("tr");
|
|
each(data.headings, function (col) {
|
|
var td = createElement("th", {
|
|
html: col
|
|
});
|
|
tr.appendChild(td);
|
|
});
|
|
|
|
thead.appendChild(tr);
|
|
}
|
|
|
|
if (data.data && data.data.length) {
|
|
tbody = createElement("tbody");
|
|
each(data.data, function (rows) {
|
|
if (data.headings) {
|
|
if (data.headings.length !== rows.length) {
|
|
throw new Error(
|
|
"The number of rows do not match the number of headings."
|
|
);
|
|
}
|
|
}
|
|
var tr = createElement("tr");
|
|
each(rows, function (value) {
|
|
var td = createElement("td", {
|
|
html: value
|
|
});
|
|
tr.appendChild(td);
|
|
});
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
if (thead) {
|
|
if (this.table.tHead !== null) {
|
|
this.table.removeChild(this.table.tHead);
|
|
}
|
|
this.table.appendChild(thead);
|
|
}
|
|
|
|
if (tbody) {
|
|
if (this.table.tBodies.length) {
|
|
this.table.removeChild(this.table.tBodies[0]);
|
|
}
|
|
this.table.appendChild(tbody);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Use moment.js to parse cell contents for sorting
|
|
* @param {String} content The datetime string to parse
|
|
* @param {String} format The format for moment to use
|
|
* @return {String|Boolean} Datatime string or false
|
|
*/
|
|
var parseDate = function (content, format) {
|
|
var date = false;
|
|
|
|
// moment() throws a fit if the string isn't a valid datetime string
|
|
// so we need to supply the format to the constructor (https://momentjs.com/docs/#/parsing/string-format/)
|
|
|
|
// Converting to YYYYMMDD ensures we can accurately sort the column numerically
|
|
|
|
if (format) {
|
|
switch (format) {
|
|
case "ISO_8601":
|
|
date = moment(content, moment.ISO_8601).format("YYYYMMDD");
|
|
break;
|
|
case "RFC_2822":
|
|
date = moment(content, "ddd, MM MMM YYYY HH:mm:ss ZZ").format("YYYYMMDD");
|
|
break;
|
|
case "MYSQL":
|
|
date = moment(content, "YYYY-MM-DD hh:mm:ss").format("YYYYMMDD");
|
|
break;
|
|
case "UNIX":
|
|
date = moment(content).unix();
|
|
break;
|
|
// User defined format using the data-format attribute or columns[n].format option
|
|
default:
|
|
date = moment(content, format).format("YYYYMMDD");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return date;
|
|
};
|
|
|
|
/**
|
|
* Columns API
|
|
* @param {Object} instance DataTable instance
|
|
* @param {Mixed} columns Column index or array of column indexes
|
|
*/
|
|
var Columns = function (dt) {
|
|
this.dt = dt;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Swap two columns
|
|
* @return {Void}
|
|
*/
|
|
Columns.prototype.swap = function (columns) {
|
|
if (columns.length && columns.length === 2) {
|
|
var cols = [];
|
|
|
|
// Get the current column indexes
|
|
each(this.dt.headings, function (h, i) {
|
|
cols.push(i);
|
|
});
|
|
|
|
var x = columns[0];
|
|
var y = columns[1];
|
|
var b = cols[y];
|
|
cols[y] = cols[x];
|
|
cols[x] = b;
|
|
|
|
this.order(cols);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reorder the columns
|
|
* @return {Array} columns Array of ordered column indexes
|
|
*/
|
|
Columns.prototype.order = function (columns) {
|
|
|
|
var a, b, c, d, h, s, cell,
|
|
temp = [
|
|
[],
|
|
[],
|
|
[],
|
|
[]
|
|
],
|
|
dt = this.dt;
|
|
|
|
// Order the headings
|
|
each(columns, function (column, x) {
|
|
h = dt.headings[column];
|
|
s = h.getAttribute("data-sortable") !== "false";
|
|
a = h.cloneNode(true);
|
|
a.originalCellIndex = x;
|
|
a.sortable = s;
|
|
|
|
temp[0].push(a);
|
|
|
|
if (dt.hiddenColumns.indexOf(column) < 0) {
|
|
b = h.cloneNode(true);
|
|
b.originalCellIndex = x;
|
|
b.sortable = s;
|
|
|
|
temp[1].push(b);
|
|
}
|
|
});
|
|
|
|
// Order the row cells
|
|
each(dt.data, function (row, i) {
|
|
c = row.cloneNode();
|
|
d = row.cloneNode();
|
|
|
|
c.dataIndex = d.dataIndex = i;
|
|
|
|
if (row.searchIndex !== null && row.searchIndex !== undefined) {
|
|
c.searchIndex = d.searchIndex = row.searchIndex;
|
|
}
|
|
|
|
// Append the cell to the fragment in the correct order
|
|
each(columns, function (column, x) {
|
|
cell = row.cells[column].cloneNode(true);
|
|
cell.data = row.cells[column].data;
|
|
c.appendChild(cell);
|
|
|
|
if (dt.hiddenColumns.indexOf(column) < 0) {
|
|
cell = row.cells[column].cloneNode(true);
|
|
cell.data = row.cells[column].data;
|
|
d.appendChild(cell);
|
|
}
|
|
});
|
|
|
|
temp[2].push(c);
|
|
temp[3].push(d);
|
|
});
|
|
|
|
dt.headings = temp[0];
|
|
dt.activeHeadings = temp[1];
|
|
|
|
dt.data = temp[2];
|
|
dt.activeRows = temp[3];
|
|
|
|
// Update
|
|
dt.update();
|
|
};
|
|
|
|
/**
|
|
* Hide columns
|
|
* @return {Void}
|
|
*/
|
|
Columns.prototype.hide = function (columns) {
|
|
if (columns.length) {
|
|
var dt = this.dt;
|
|
|
|
each(columns, function (column) {
|
|
if (dt.hiddenColumns.indexOf(column) < 0) {
|
|
dt.hiddenColumns.push(column);
|
|
}
|
|
});
|
|
|
|
this.rebuild();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Show columns
|
|
* @return {Void}
|
|
*/
|
|
Columns.prototype.show = function (columns) {
|
|
if (columns.length) {
|
|
var index, dt = this.dt;
|
|
|
|
each(columns, function (column) {
|
|
index = dt.hiddenColumns.indexOf(column);
|
|
if (index > -1) {
|
|
dt.hiddenColumns.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
this.rebuild();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check column(s) visibility
|
|
* @return {Boolean}
|
|
*/
|
|
Columns.prototype.visible = function (columns) {
|
|
var cols, dt = this.dt;
|
|
|
|
columns = columns || dt.headings.map(function (th) {
|
|
return th.originalCellIndex;
|
|
});
|
|
|
|
if (!isNaN(columns)) {
|
|
cols = dt.hiddenColumns.indexOf(columns) < 0;
|
|
} else if (isArray(columns)) {
|
|
cols = [];
|
|
each(columns, function (column) {
|
|
cols.push(dt.hiddenColumns.indexOf(column) < 0);
|
|
});
|
|
}
|
|
|
|
return cols;
|
|
};
|
|
|
|
/**
|
|
* Add a new column
|
|
* @param {Object} data
|
|
*/
|
|
Columns.prototype.add = function (data) {
|
|
var that = this,
|
|
td, th = document.createElement("th");
|
|
|
|
if (!this.dt.headings.length) {
|
|
this.dt.insert({
|
|
headings: [data.heading],
|
|
data: data.data.map(function (i) {
|
|
return [i];
|
|
})
|
|
});
|
|
this.rebuild();
|
|
return;
|
|
}
|
|
|
|
if (!this.dt.hiddenHeader) {
|
|
if (data.heading.nodeName) {
|
|
th.appendChild(data.heading);
|
|
} else {
|
|
th.innerHTML = data.heading;
|
|
}
|
|
} else {
|
|
th.innerHTML = "";
|
|
}
|
|
|
|
this.dt.headings.push(th);
|
|
|
|
each(this.dt.data, function (row, i) {
|
|
if (data.data[i]) {
|
|
td = document.createElement("td");
|
|
|
|
if (data.data[i].nodeName) {
|
|
td.appendChild(data.data[i]);
|
|
} else {
|
|
td.innerHTML = data.data[i];
|
|
}
|
|
|
|
td.data = td.innerHTML;
|
|
|
|
if (data.render) {
|
|
td.innerHTML = data.render.call(that, td.data, td, row);
|
|
}
|
|
|
|
row.appendChild(td);
|
|
}
|
|
});
|
|
|
|
if (data.type) {
|
|
th.setAttribute("data-type", data.type);
|
|
}
|
|
if (data.format) {
|
|
th.setAttribute("data-format", data.format);
|
|
}
|
|
|
|
if (data.hasOwnProperty("sortable")) {
|
|
th.sortable = data.sortable;
|
|
th.setAttribute("data-sortable", data.sortable === true ? "true" : "false");
|
|
}
|
|
|
|
this.rebuild();
|
|
|
|
this.dt.renderHeader();
|
|
};
|
|
|
|
/**
|
|
* Remove column(s)
|
|
* @param {Array|Number} select
|
|
* @return {Void}
|
|
*/
|
|
Columns.prototype.remove = function (select) {
|
|
if (isArray(select)) {
|
|
// Remove in reverse otherwise the indexes will be incorrect
|
|
select.sort(function (a, b) {
|
|
return b - a;
|
|
});
|
|
|
|
each(select, function (column) {
|
|
this.remove(column);
|
|
}, this);
|
|
} else {
|
|
this.dt.headings.splice(select, 1);
|
|
|
|
each(this.dt.data, function (row) {
|
|
row.removeChild(row.cells[select]);
|
|
});
|
|
}
|
|
|
|
this.rebuild();
|
|
};
|
|
|
|
/**
|
|
* Sort by column
|
|
* @param {int} column - The column no.
|
|
* @param {string} direction - asc or desc
|
|
* @return {void}
|
|
*/
|
|
Columns.prototype.sort = function (column, direction, init) {
|
|
|
|
var dt = this.dt;
|
|
|
|
// Check column is present
|
|
if (dt.hasHeadings && (column < 1 || column > dt.activeHeadings.length)) {
|
|
return false;
|
|
}
|
|
|
|
dt.sorting = true;
|
|
|
|
// Convert to zero-indexed
|
|
column = column - 1;
|
|
|
|
var dir,
|
|
rows = dt.data,
|
|
alpha = [],
|
|
numeric = [],
|
|
a = 0,
|
|
n = 0,
|
|
th = dt.activeHeadings[column];
|
|
|
|
column = th.originalCellIndex;
|
|
|
|
each(rows, function (tr) {
|
|
var cell = tr.cells[column];
|
|
var content = cell.hasAttribute('data-content') ? cell.getAttribute('data-content') : cell.data;
|
|
var num = content.replace(/(\$|\,|\s|%)/g, "");
|
|
|
|
// Check for date format and moment.js
|
|
if (th.getAttribute("data-type") === "date" && win.moment) {
|
|
var format = false,
|
|
formatted = th.hasAttribute("data-format");
|
|
|
|
if (formatted) {
|
|
format = th.getAttribute("data-format");
|
|
}
|
|
|
|
num = parseDate(content, format);
|
|
}
|
|
|
|
if (parseFloat(num) == num) {
|
|
numeric[n++] = {
|
|
value: Number(num),
|
|
row: tr
|
|
};
|
|
} else {
|
|
alpha[a++] = {
|
|
value: content,
|
|
row: tr
|
|
};
|
|
}
|
|
});
|
|
|
|
/* Sort according to direction (ascending or descending) */
|
|
var top, btm;
|
|
if (classList.contains(th, "asc") || direction == "asc") {
|
|
top = sortItems(alpha, -1);
|
|
btm = sortItems(numeric, -1);
|
|
dir = "descending";
|
|
classList.remove(th, "asc");
|
|
classList.add(th, "desc");
|
|
} else {
|
|
top = sortItems(numeric, 1);
|
|
btm = sortItems(alpha, 1);
|
|
dir = "ascending";
|
|
classList.remove(th, "desc");
|
|
classList.add(th, "asc");
|
|
}
|
|
|
|
/* Clear asc/desc class names from the last sorted column's th if it isn't the same as the one that was just clicked */
|
|
if (dt.lastTh && th != dt.lastTh) {
|
|
classList.remove(dt.lastTh, "desc");
|
|
classList.remove(dt.lastTh, "asc");
|
|
}
|
|
|
|
dt.lastTh = th;
|
|
|
|
/* Reorder the table */
|
|
rows = top.concat(btm);
|
|
|
|
dt.data = [];
|
|
var indexes = [];
|
|
|
|
each(rows, function (v, i) {
|
|
dt.data.push(v.row);
|
|
|
|
if (v.row.searchIndex !== null && v.row.searchIndex !== undefined) {
|
|
indexes.push(i);
|
|
}
|
|
}, dt);
|
|
|
|
dt.searchData = indexes;
|
|
|
|
this.rebuild();
|
|
|
|
dt.update();
|
|
|
|
if (!init) {
|
|
dt.emit("datatable.sort", column, dir);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Rebuild the columns
|
|
* @return {Void}
|
|
*/
|
|
Columns.prototype.rebuild = function () {
|
|
var a, b, c, d, dt = this.dt,
|
|
temp = [];
|
|
|
|
dt.activeRows = [];
|
|
dt.activeHeadings = [];
|
|
|
|
each(dt.headings, function (th, i) {
|
|
th.originalCellIndex = i;
|
|
th.sortable = th.getAttribute("data-sortable") !== "false";
|
|
if (dt.hiddenColumns.indexOf(i) < 0) {
|
|
dt.activeHeadings.push(th);
|
|
}
|
|
}, this);
|
|
|
|
// Loop over the rows and reorder the cells
|
|
each(dt.data, function (row, i) {
|
|
a = row.cloneNode();
|
|
b = row.cloneNode();
|
|
|
|
a.dataIndex = b.dataIndex = i;
|
|
|
|
if (row.searchIndex !== null && row.searchIndex !== undefined) {
|
|
a.searchIndex = b.searchIndex = row.searchIndex;
|
|
}
|
|
|
|
// Append the cell to the fragment in the correct order
|
|
each(row.cells, function (cell) {
|
|
c = cell.cloneNode(true);
|
|
c.data = cell.data;
|
|
a.appendChild(c);
|
|
|
|
if (dt.hiddenColumns.indexOf(cell.cellIndex) < 0) {
|
|
d = cell.cloneNode(true);
|
|
d.data = cell.data;
|
|
b.appendChild(d);
|
|
}
|
|
});
|
|
|
|
// Append the fragment with the ordered cells
|
|
temp.push(a);
|
|
dt.activeRows.push(b);
|
|
});
|
|
|
|
dt.data = temp;
|
|
|
|
dt.update();
|
|
};
|
|
|
|
/**
|
|
* Rows API
|
|
* @param {Object} instance DataTable instance
|
|
* @param {Array} rows
|
|
*/
|
|
var Rows = function (dt, rows) {
|
|
this.dt = dt;
|
|
this.rows = rows;
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Build a new row
|
|
* @param {Array} row
|
|
* @return {HTMLElement}
|
|
*/
|
|
Rows.prototype.build = function (row) {
|
|
var td, tr = createElement("tr");
|
|
|
|
var headings = this.dt.headings;
|
|
|
|
if (!headings.length) {
|
|
headings = row.map(function () {
|
|
return "";
|
|
});
|
|
}
|
|
|
|
each(headings, function (h, i) {
|
|
td = createElement("td");
|
|
|
|
// Fixes #29
|
|
if (!row[i] && !row[i].length) {
|
|
row[i] = "";
|
|
}
|
|
|
|
td.innerHTML = row[i];
|
|
|
|
td.data = row[i];
|
|
|
|
tr.appendChild(td);
|
|
});
|
|
|
|
return tr;
|
|
};
|
|
|
|
Rows.prototype.render = function (row) {
|
|
return row;
|
|
};
|
|
|
|
/**
|
|
* Add new row
|
|
* @param {Array} select
|
|
*/
|
|
Rows.prototype.add = function (data) {
|
|
|
|
if (isArray(data)) {
|
|
var dt = this.dt;
|
|
// Check for multiple rows
|
|
if (isArray(data[0])) {
|
|
each(data, function (row, i) {
|
|
dt.data.push(this.build(row));
|
|
}, this);
|
|
} else {
|
|
dt.data.push(this.build(data));
|
|
}
|
|
|
|
// We may have added data to an empty table
|
|
if ( dt.data.length ) {
|
|
dt.hasRows = true;
|
|
}
|
|
|
|
|
|
this.update();
|
|
|
|
dt.columns().rebuild();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove row(s)
|
|
* @param {Array|Number} select
|
|
* @return {Void}
|
|
*/
|
|
Rows.prototype.remove = function (select) {
|
|
|
|
var dt = this.dt;
|
|
|
|
if (isArray(select)) {
|
|
// Remove in reverse otherwise the indexes will be incorrect
|
|
select.sort(function (a, b) {
|
|
return b - a;
|
|
});
|
|
|
|
each(select, function (row, i) {
|
|
dt.data.splice(row, 1);
|
|
});
|
|
} else {
|
|
dt.data.splice(select, 1);
|
|
}
|
|
|
|
this.update();
|
|
dt.columns().rebuild();
|
|
};
|
|
|
|
/**
|
|
* Update row indexes
|
|
* @return {Void}
|
|
*/
|
|
Rows.prototype.update = function () {
|
|
each(this.dt.data, function (row, i) {
|
|
row.dataIndex = i;
|
|
});
|
|
};
|
|
|
|
////////////////////
|
|
// MAIN LIB //
|
|
////////////////////
|
|
|
|
var DataTable = function (table, options) {
|
|
this.initialized = false;
|
|
|
|
// user options
|
|
this.options = extend(defaultConfig, options);
|
|
|
|
if (typeof table === "string") {
|
|
table = document.querySelector(table);
|
|
}
|
|
|
|
this.initialLayout = table.innerHTML;
|
|
this.initialSortable = this.options.sortable;
|
|
|
|
// Disable manual sorting if no header is present (#4)
|
|
if (!this.options.header) {
|
|
this.options.sortable = false;
|
|
}
|
|
|
|
if (table.tHead === null) {
|
|
if (!this.options.data ||
|
|
(this.options.data && !this.options.data.headings)
|
|
) {
|
|
this.options.sortable = false;
|
|
}
|
|
}
|
|
|
|
if (table.tBodies.length && !table.tBodies[0].rows.length) {
|
|
if (this.options.data) {
|
|
if (!this.options.data.data) {
|
|
throw new Error(
|
|
"You seem to be using the data option, but you've not defined any rows."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.table = table;
|
|
|
|
this.init();
|
|
};
|
|
|
|
/**
|
|
* Add custom property or method to extend DataTable
|
|
* @param {String} prop - Method name or property
|
|
* @param {Mixed} val - Function or property value
|
|
* @return {Void}
|
|
*/
|
|
DataTable.extend = function(prop, val) {
|
|
if (typeof val === "function") {
|
|
DataTable.prototype[prop] = val;
|
|
} else {
|
|
DataTable[prop] = val;
|
|
}
|
|
};
|
|
|
|
var proto = DataTable.prototype;
|
|
|
|
/**
|
|
* Initialize the instance
|
|
* @param {Object} options
|
|
* @return {Void}
|
|
*/
|
|
proto.init = function (options) {
|
|
if (this.initialized || classList.contains(this.table, "dataTable-table")) {
|
|
return false;
|
|
}
|
|
|
|
var that = this;
|
|
|
|
this.options = extend(this.options, options || {});
|
|
|
|
// IE detection
|
|
this.isIE = !!/(msie|trident)/i.test(navigator.userAgent);
|
|
|
|
this.currentPage = 1;
|
|
this.onFirstPage = true;
|
|
|
|
this.hiddenColumns = [];
|
|
this.columnRenderers = [];
|
|
this.selectedColumns = [];
|
|
|
|
this.render();
|
|
|
|
setTimeout(function () {
|
|
that.emit("datatable.init");
|
|
that.initialized = true;
|
|
|
|
if (that.options.plugins) {
|
|
each(that.options.plugins, function(options, plugin) {
|
|
if (that[plugin] && typeof that[plugin] === "function") {
|
|
that[plugin] = that[plugin](options, {
|
|
each: each,
|
|
extend: extend,
|
|
classList: classList,
|
|
createElement: createElement
|
|
});
|
|
|
|
// Init plugin
|
|
if (options.enabled && that[plugin].init && typeof that[plugin].init === "function") {
|
|
that[plugin].init();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}, 10);
|
|
};
|
|
|
|
/**
|
|
* Render the instance
|
|
* @param {String} type
|
|
* @return {Void}
|
|
*/
|
|
proto.render = function (type) {
|
|
if (type) {
|
|
switch (type) {
|
|
case "page":
|
|
this.renderPage();
|
|
break;
|
|
case "pager":
|
|
this.renderPager();
|
|
break;
|
|
case "header":
|
|
this.renderHeader();
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
var that = this,
|
|
o = that.options,
|
|
template = "";
|
|
|
|
// Convert data to HTML
|
|
if (o.data) {
|
|
dataToTable.call(that);
|
|
}
|
|
|
|
if (o.ajax) {
|
|
var ajax = o.ajax;
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
var xhrProgress = function (e) {
|
|
that.emit("datatable.ajax.progress", e, xhr);
|
|
};
|
|
|
|
var xhrLoad = function (e) {
|
|
if (xhr.readyState === 4) {
|
|
that.emit("datatable.ajax.loaded", e, xhr);
|
|
|
|
if (xhr.status === 200) {
|
|
var obj = {};
|
|
obj.data = ajax.load ? ajax.load.call(that, xhr) : xhr.responseText;
|
|
|
|
obj.type = "json";
|
|
|
|
if (ajax.content && ajax.content.type) {
|
|
obj.type = ajax.content.type;
|
|
|
|
obj = extend(obj, ajax.content);
|
|
}
|
|
|
|
that.import(obj);
|
|
|
|
that.setColumns(true);
|
|
|
|
that.emit("datatable.ajax.success", e, xhr);
|
|
} else {
|
|
that.emit("datatable.ajax.error", e, xhr);
|
|
}
|
|
}
|
|
};
|
|
|
|
var xhrFailed = function (e) {
|
|
that.emit("datatable.ajax.error", e, xhr);
|
|
};
|
|
|
|
var xhrCancelled = function (e) {
|
|
that.emit("datatable.ajax.abort", e, xhr);
|
|
};
|
|
|
|
on(xhr, "progress", xhrProgress);
|
|
on(xhr, "load", xhrLoad);
|
|
on(xhr, "error", xhrFailed);
|
|
on(xhr, "abort", xhrCancelled);
|
|
|
|
that.emit("datatable.ajax.loading", xhr);
|
|
|
|
xhr.open("GET", typeof ajax === "string" ? o.ajax : o.ajax.url);
|
|
xhr.send();
|
|
}
|
|
|
|
// Store references
|
|
that.body = that.table.tBodies[0];
|
|
that.head = that.table.tHead;
|
|
that.foot = that.table.tFoot;
|
|
|
|
if (!that.body) {
|
|
that.body = createElement("tbody");
|
|
|
|
that.table.appendChild(that.body);
|
|
}
|
|
|
|
that.hasRows = that.body.rows.length > 0;
|
|
|
|
// Make a tHead if there isn't one (fixes #8)
|
|
if (!that.head) {
|
|
var h = createElement("thead");
|
|
var t = createElement("tr");
|
|
|
|
if (that.hasRows) {
|
|
each(that.body.rows[0].cells, function () {
|
|
t.appendChild(createElement("th"));
|
|
});
|
|
|
|
h.appendChild(t);
|
|
}
|
|
|
|
that.head = h;
|
|
|
|
that.table.insertBefore(that.head, that.body);
|
|
|
|
that.hiddenHeader = !o.ajax;
|
|
}
|
|
|
|
that.headings = [];
|
|
that.hasHeadings = that.head.rows.length > 0;
|
|
|
|
if (that.hasHeadings) {
|
|
that.header = that.head.rows[0];
|
|
that.headings = [].slice.call(that.header.cells);
|
|
}
|
|
|
|
// Header
|
|
if (!o.header) {
|
|
if (that.head) {
|
|
that.table.removeChild(that.table.tHead);
|
|
}
|
|
}
|
|
|
|
// Footer
|
|
if (o.footer) {
|
|
if (that.head && !that.foot) {
|
|
that.foot = createElement("tfoot", {
|
|
html: that.head.innerHTML
|
|
});
|
|
that.table.appendChild(that.foot);
|
|
}
|
|
} else {
|
|
if (that.foot) {
|
|
that.table.removeChild(that.table.tFoot);
|
|
}
|
|
}
|
|
|
|
// Build
|
|
that.wrapper = createElement("div", {
|
|
class: "dataTable-wrapper dataTable-loading"
|
|
});
|
|
|
|
// Template for custom layouts
|
|
template += "<div class='dataTable-top'>";
|
|
template += o.layout.top;
|
|
template += "</div>";
|
|
template += "<div class='dataTable-container'></div>";
|
|
template += "<div class='dataTable-bottom'>";
|
|
template += o.layout.bottom;
|
|
template += "</div>";
|
|
|
|
// Info placement
|
|
template = template.replace("{info}", "<div class='dataTable-info'></div>");
|
|
|
|
// Per Page Select
|
|
if (o.perPageSelect) {
|
|
var wrap = "<div class='dataTable-dropdown'><label>";
|
|
wrap += o.labels.perPage;
|
|
wrap += "</label></div>";
|
|
|
|
// Create the select
|
|
var select = createElement("select", {
|
|
class: "dataTable-selector"
|
|
});
|
|
|
|
// Create the options
|
|
each(o.perPageSelect, function (val) {
|
|
var selected = val === o.perPage;
|
|
var option = new Option(val, val, selected, selected);
|
|
select.add(option);
|
|
});
|
|
|
|
// Custom label
|
|
wrap = wrap.replace("{select}", select.outerHTML);
|
|
|
|
// Selector placement
|
|
template = template.replace("{select}", wrap);
|
|
} else {
|
|
template = template.replace("{select}", "");
|
|
}
|
|
|
|
// Searchable
|
|
if (o.searchable) {
|
|
var form =
|
|
"<div class='dataTable-search'><input class='dataTable-input' placeholder='" +
|
|
o.labels.placeholder +
|
|
"' type='text'></div>";
|
|
|
|
// Search input placement
|
|
template = template.replace("{search}", form);
|
|
} else {
|
|
template = template.replace("{search}", "");
|
|
}
|
|
|
|
if (that.hasHeadings) {
|
|
// Sortable
|
|
this.render("header");
|
|
}
|
|
|
|
// Add table class
|
|
classList.add(that.table, "dataTable-table");
|
|
|
|
// Paginator
|
|
var w = createElement("div", {
|
|
class: "dataTable-pagination"
|
|
});
|
|
var paginator = createElement("ul");
|
|
w.appendChild(paginator);
|
|
|
|
// Pager(s) placement
|
|
template = template.replace(/\{pager\}/g, w.outerHTML);
|
|
|
|
that.wrapper.innerHTML = template;
|
|
|
|
that.container = that.wrapper.querySelector(".dataTable-container");
|
|
|
|
that.pagers = that.wrapper.querySelectorAll(".dataTable-pagination");
|
|
|
|
that.label = that.wrapper.querySelector(".dataTable-info");
|
|
|
|
// Insert in to DOM tree
|
|
that.table.parentNode.replaceChild(that.wrapper, that.table);
|
|
that.container.appendChild(that.table);
|
|
|
|
// Store the table dimensions
|
|
that.rect = that.table.getBoundingClientRect();
|
|
|
|
// Convert rows to array for processing
|
|
that.data = [].slice.call(that.body.rows);
|
|
that.activeRows = that.data.slice();
|
|
that.activeHeadings = that.headings.slice();
|
|
|
|
// Update
|
|
that.update();
|
|
|
|
if (!o.ajax) {
|
|
that.setColumns();
|
|
}
|
|
|
|
// Fix height
|
|
this.fixHeight();
|
|
|
|
// Fix columns
|
|
that.fixColumns();
|
|
|
|
// Class names
|
|
if (!o.header) {
|
|
classList.add(that.wrapper, "no-header");
|
|
}
|
|
|
|
if (!o.footer) {
|
|
classList.add(that.wrapper, "no-footer");
|
|
}
|
|
|
|
if (o.sortable) {
|
|
classList.add(that.wrapper, "sortable");
|
|
}
|
|
|
|
if (o.searchable) {
|
|
classList.add(that.wrapper, "searchable");
|
|
}
|
|
|
|
if (o.fixedHeight) {
|
|
classList.add(that.wrapper, "fixed-height");
|
|
}
|
|
|
|
if (o.fixedColumns) {
|
|
classList.add(that.wrapper, "fixed-columns");
|
|
}
|
|
|
|
that.bindEvents();
|
|
};
|
|
|
|
/**
|
|
* Render the page
|
|
* @return {Void}
|
|
*/
|
|
proto.renderPage = function () {
|
|
if (this.hasRows && this.totalPages) {
|
|
if (this.currentPage > this.totalPages) {
|
|
this.currentPage = 1;
|
|
}
|
|
|
|
// Use a fragment to limit touching the DOM
|
|
var index = this.currentPage - 1,
|
|
frag = doc.createDocumentFragment();
|
|
|
|
if (this.hasHeadings) {
|
|
flush(this.header, this.isIE);
|
|
|
|
each(this.activeHeadings, function (th) {
|
|
this.header.appendChild(th);
|
|
}, this);
|
|
}
|
|
|
|
each(this.pages[index], function (row) {
|
|
frag.appendChild(this.rows().render(row));
|
|
}, this);
|
|
|
|
this.clear(frag);
|
|
|
|
this.onFirstPage = this.currentPage === 1;
|
|
this.onLastPage = this.currentPage === this.lastPage;
|
|
} else {
|
|
this.clear();
|
|
}
|
|
|
|
// Update the info
|
|
var current = 0,
|
|
f = 0,
|
|
t = 0,
|
|
items;
|
|
|
|
if (this.totalPages) {
|
|
current = this.currentPage - 1;
|
|
f = current * this.options.perPage;
|
|
t = f + this.pages[current].length;
|
|
f = f + 1;
|
|
items = !!this.searching ? this.searchData.length : this.data.length;
|
|
}
|
|
|
|
if (this.label && this.options.labels.info.length) {
|
|
// CUSTOM LABELS
|
|
var string = this.options.labels.info
|
|
.replace("{start}", f)
|
|
.replace("{end}", t)
|
|
.replace("{page}", this.currentPage)
|
|
.replace("{pages}", this.totalPages)
|
|
.replace("{rows}", items);
|
|
|
|
this.label.innerHTML = items ? string : "";
|
|
}
|
|
|
|
if (this.currentPage == 1) {
|
|
this.fixHeight();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Render the pager(s)
|
|
* @return {Void}
|
|
*/
|
|
proto.renderPager = function () {
|
|
flush(this.pagers, this.isIE);
|
|
|
|
if (this.totalPages > 1) {
|
|
var c = "pager",
|
|
frag = doc.createDocumentFragment(),
|
|
prev = this.onFirstPage ? 1 : this.currentPage - 1,
|
|
next = this.onlastPage ? this.totalPages : this.currentPage + 1;
|
|
|
|
// first button
|
|
if (this.options.firstLast) {
|
|
frag.appendChild(button(c, 1, this.options.firstText));
|
|
}
|
|
|
|
// prev button
|
|
if (this.options.nextPrev) {
|
|
frag.appendChild(button(c, prev, this.options.prevText));
|
|
}
|
|
|
|
var pager = this.links;
|
|
|
|
// truncate the links
|
|
if (this.options.truncatePager) {
|
|
pager = truncate(
|
|
this.links,
|
|
this.currentPage,
|
|
this.pages.length,
|
|
this.options.pagerDelta,
|
|
this.options.ellipsisText
|
|
);
|
|
}
|
|
|
|
// active page link
|
|
classList.add(this.links[this.currentPage - 1], "active");
|
|
|
|
// append the links
|
|
each(pager, function (p) {
|
|
classList.remove(p, "active");
|
|
frag.appendChild(p);
|
|
});
|
|
|
|
classList.add(this.links[this.currentPage - 1], "active");
|
|
|
|
// next button
|
|
if (this.options.nextPrev) {
|
|
frag.appendChild(button(c, next, this.options.nextText));
|
|
}
|
|
|
|
// first button
|
|
if (this.options.firstLast) {
|
|
frag.appendChild(button(c, this.totalPages, this.options.lastText));
|
|
}
|
|
|
|
// We may have more than one pager
|
|
each(this.pagers, function (pager) {
|
|
pager.appendChild(frag.cloneNode(true));
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Render the header
|
|
* @return {Void}
|
|
*/
|
|
proto.renderHeader = function () {
|
|
var that = this;
|
|
|
|
that.labels = [];
|
|
|
|
if (that.headings && that.headings.length) {
|
|
each(that.headings, function (th, i) {
|
|
|
|
that.labels[i] = th.textContent;
|
|
|
|
if (classList.contains(th.firstElementChild, "dataTable-sorter")) {
|
|
th.innerHTML = th.firstElementChild.innerHTML;
|
|
}
|
|
|
|
th.sortable = th.getAttribute("data-sortable") !== "false";
|
|
|
|
th.originalCellIndex = i;
|
|
if (that.options.sortable && th.sortable) {
|
|
var link = createElement("a", {
|
|
href: "#",
|
|
class: "dataTable-sorter",
|
|
html: th.innerHTML
|
|
});
|
|
|
|
th.innerHTML = "";
|
|
th.setAttribute("data-sortable", "");
|
|
th.appendChild(link);
|
|
}
|
|
});
|
|
}
|
|
|
|
that.fixColumns();
|
|
};
|
|
|
|
/**
|
|
* Bind event listeners
|
|
* @return {[type]} [description]
|
|
*/
|
|
proto.bindEvents = function () {
|
|
var that = this,
|
|
o = that.options;
|
|
|
|
// Per page selector
|
|
if (o.perPageSelect) {
|
|
var selector = that.wrapper.querySelector(".dataTable-selector");
|
|
if (selector) {
|
|
// Change per page
|
|
on(selector, "change", function (e) {
|
|
o.perPage = parseInt(this.value, 10);
|
|
that.update();
|
|
|
|
that.fixHeight();
|
|
|
|
that.emit("datatable.perpage", o.perPage);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Search input
|
|
if (o.searchable) {
|
|
that.input = that.wrapper.querySelector(".dataTable-input");
|
|
if (that.input) {
|
|
on(that.input, "keyup", function (e) {
|
|
that.search(this.value);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Pager(s) / sorting
|
|
on(that.wrapper, "click", function (e) {
|
|
var t = e.target;
|
|
if (t.nodeName.toLowerCase() === "a") {
|
|
if (t.hasAttribute("data-page")) {
|
|
that.page(t.getAttribute("data-page"));
|
|
e.preventDefault();
|
|
} else if (
|
|
o.sortable &&
|
|
classList.contains(t, "dataTable-sorter") &&
|
|
t.parentNode.getAttribute("data-sortable") != "false"
|
|
) {
|
|
that.columns().sort(that.activeHeadings.indexOf(t.parentNode) + 1);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Set up columns
|
|
* @return {[type]} [description]
|
|
*/
|
|
proto.setColumns = function (ajax) {
|
|
|
|
var that = this;
|
|
|
|
if (!ajax) {
|
|
each(that.data, function (row) {
|
|
each(row.cells, function (cell) {
|
|
cell.data = cell.innerHTML;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Check for the columns option
|
|
if (that.options.columns && that.headings.length) {
|
|
|
|
each(that.options.columns, function (data) {
|
|
|
|
// convert single column selection to array
|
|
if (!isArray(data.select)) {
|
|
data.select = [data.select];
|
|
}
|
|
|
|
if (data.hasOwnProperty("render") && typeof data.render === "function") {
|
|
that.selectedColumns = that.selectedColumns.concat(data.select);
|
|
|
|
that.columnRenderers.push({
|
|
columns: data.select,
|
|
renderer: data.render
|
|
});
|
|
}
|
|
|
|
// Add the data attributes to the th elements
|
|
each(data.select, function (column) {
|
|
var th = that.headings[column];
|
|
if (data.type) {
|
|
th.setAttribute("data-type", data.type);
|
|
}
|
|
if (data.format) {
|
|
th.setAttribute("data-format", data.format);
|
|
}
|
|
if (data.hasOwnProperty("sortable")) {
|
|
th.setAttribute("data-sortable", data.sortable);
|
|
}
|
|
|
|
if (data.hasOwnProperty("hidden")) {
|
|
if (data.hidden !== false) {
|
|
that.columns().hide(column);
|
|
}
|
|
}
|
|
|
|
if (data.hasOwnProperty("sort") && data.select.length === 1) {
|
|
that.columns().sort(data.select[0] + 1, data.sort, true);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (that.hasRows) {
|
|
each(that.data, function (row, i) {
|
|
row.dataIndex = i;
|
|
each(row.cells, function (cell) {
|
|
cell.data = cell.innerHTML;
|
|
});
|
|
});
|
|
|
|
if (that.selectedColumns.length) {
|
|
each(that.data, function (row) {
|
|
each(row.cells, function (cell, i) {
|
|
if (that.selectedColumns.indexOf(i) > -1) {
|
|
each(that.columnRenderers, function (o) {
|
|
if (o.columns.indexOf(i) > -1) {
|
|
cell.innerHTML = o.renderer.call(that, cell.data, cell, row);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
that.columns().rebuild();
|
|
}
|
|
|
|
that.render("header");
|
|
};
|
|
|
|
/**
|
|
* Destroy the instance
|
|
* @return {void}
|
|
*/
|
|
proto.destroy = function () {
|
|
this.table.innerHTML = this.initialLayout;
|
|
|
|
// Remove the className
|
|
classList.remove(this.table, "dataTable-table");
|
|
|
|
// Remove the containers
|
|
this.wrapper.parentNode.replaceChild(this.table, this.wrapper);
|
|
|
|
this.initialized = false;
|
|
};
|
|
|
|
/**
|
|
* Update the instance
|
|
* @return {Void}
|
|
*/
|
|
proto.update = function () {
|
|
classList.remove(this.wrapper, "dataTable-empty");
|
|
|
|
this.paginate(this);
|
|
this.render("page");
|
|
|
|
this.links = [];
|
|
|
|
var i = this.pages.length;
|
|
while (i--) {
|
|
var num = i + 1;
|
|
this.links[i] = button(i === 0 ? "active" : "", num, num);
|
|
}
|
|
|
|
this.sorting = false;
|
|
|
|
this.render("pager");
|
|
|
|
this.rows().update();
|
|
|
|
this.emit("datatable.update");
|
|
};
|
|
|
|
/**
|
|
* Sort rows into pages
|
|
* @return {Number}
|
|
*/
|
|
proto.paginate = function () {
|
|
var perPage = this.options.perPage,
|
|
rows = this.activeRows;
|
|
|
|
if (this.searching) {
|
|
rows = [];
|
|
|
|
each(this.searchData, function (index) {
|
|
rows.push(this.activeRows[index]);
|
|
}, this);
|
|
}
|
|
|
|
// Check for hidden columns
|
|
this.pages = rows
|
|
.map(function (tr, i) {
|
|
return i % perPage === 0 ? rows.slice(i, i + perPage) : null;
|
|
})
|
|
.filter(function (page) {
|
|
return page;
|
|
});
|
|
|
|
this.totalPages = this.lastPage = this.pages.length;
|
|
|
|
return this.totalPages;
|
|
};
|
|
|
|
/**
|
|
* Fix column widths
|
|
* @return {Void}
|
|
*/
|
|
proto.fixColumns = function () {
|
|
|
|
if (this.options.fixedColumns && this.activeHeadings && this.activeHeadings.length) {
|
|
|
|
var cells,
|
|
hd = false;
|
|
|
|
this.columnWidths = [];
|
|
|
|
// If we have headings we need only set the widths on them
|
|
// otherwise we need a temp header and the widths need applying to all cells
|
|
if (this.table.tHead) {
|
|
// Reset widths
|
|
each(this.activeHeadings, function (cell) {
|
|
cell.style.width = "";
|
|
}, this);
|
|
|
|
each(this.activeHeadings, function (cell, i) {
|
|
var ow = cell.offsetWidth;
|
|
var w = ow / this.rect.width * 100;
|
|
cell.style.width = w + "%";
|
|
this.columnWidths[i] = ow;
|
|
}, this);
|
|
} else {
|
|
cells = [];
|
|
|
|
// Make temperary headings
|
|
hd = createElement("thead");
|
|
var r = createElement("tr");
|
|
var c = this.table.tBodies[0].rows[0].cells;
|
|
each(c, function () {
|
|
var th = createElement("th");
|
|
r.appendChild(th);
|
|
cells.push(th);
|
|
});
|
|
|
|
hd.appendChild(r);
|
|
this.table.insertBefore(hd, this.body);
|
|
|
|
var widths = [];
|
|
each(cells, function (cell, i) {
|
|
var ow = cell.offsetWidth;
|
|
var w = ow / this.rect.width * 100;
|
|
widths.push(w);
|
|
this.columnWidths[i] = ow;
|
|
}, this);
|
|
|
|
each(this.data, function (row) {
|
|
each(row.cells, function (cell, i) {
|
|
if (this.columns(cell.cellIndex).visible())
|
|
cell.style.width = widths[i] + "%";
|
|
}, this);
|
|
}, this);
|
|
|
|
// Discard the temp header
|
|
this.table.removeChild(hd);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fix the container height;
|
|
* @return {Void}
|
|
*/
|
|
proto.fixHeight = function () {
|
|
if (this.options.fixedHeight) {
|
|
this.container.style.height = null;
|
|
this.rect = this.container.getBoundingClientRect();
|
|
this.container.style.height = this.rect.height + "px";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Perform a search of the data set
|
|
* @param {string} query
|
|
* @return {void}
|
|
*/
|
|
proto.search = function (query) {
|
|
if (!this.hasRows) return false;
|
|
|
|
var that = this;
|
|
|
|
query = query.toLowerCase();
|
|
|
|
this.currentPage = 1;
|
|
this.searching = true;
|
|
this.searchData = [];
|
|
|
|
if (!query.length) {
|
|
this.searching = false;
|
|
this.update();
|
|
this.emit("datatable.search", query, this.searchData);
|
|
classList.remove(this.wrapper, "search-results");
|
|
return false;
|
|
}
|
|
|
|
this.clear();
|
|
|
|
each(this.data, function (row, idx) {
|
|
var inArray = this.searchData.indexOf(row) > -1;
|
|
|
|
// https://github.com/Mobius1/Vanilla-DataTables/issues/12
|
|
var doesQueryMatch = query.split(" ").reduce(function (bool, word) {
|
|
var includes = false,
|
|
cell = null,
|
|
content = null;
|
|
|
|
for (var x = 0; x < row.cells.length; x++) {
|
|
cell = row.cells[x];
|
|
content = cell.hasAttribute('data-content') ? cell.getAttribute('data-content') : cell.textContent;
|
|
|
|
if (
|
|
content.toLowerCase().indexOf(word) > -1 &&
|
|
that.columns(cell.cellIndex).visible()
|
|
) {
|
|
includes = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bool && includes;
|
|
}, true);
|
|
|
|
if (doesQueryMatch && !inArray) {
|
|
row.searchIndex = idx;
|
|
this.searchData.push(idx);
|
|
} else {
|
|
row.searchIndex = null;
|
|
}
|
|
}, this);
|
|
|
|
classList.add(this.wrapper, "search-results");
|
|
|
|
if (!that.searchData.length) {
|
|
classList.remove(that.wrapper, "search-results");
|
|
|
|
that.setMessage(that.options.labels.noRows);
|
|
} else {
|
|
that.update();
|
|
}
|
|
|
|
this.emit("datatable.search", query, this.searchData);
|
|
};
|
|
|
|
/**
|
|
* Change page
|
|
* @param {int} page
|
|
* @return {void}
|
|
*/
|
|
proto.page = function (page) {
|
|
// We don't want to load the current page again.
|
|
if (page == this.currentPage) {
|
|
return false;
|
|
}
|
|
|
|
if (!isNaN(page)) {
|
|
this.currentPage = parseInt(page, 10);
|
|
}
|
|
|
|
if (page > this.pages.length || page < 0) {
|
|
return false;
|
|
}
|
|
|
|
this.render("page");
|
|
this.render("pager");
|
|
|
|
this.emit("datatable.page", page);
|
|
};
|
|
|
|
/**
|
|
* Sort by column
|
|
* @param {int} column - The column no.
|
|
* @param {string} direction - asc or desc
|
|
* @return {void}
|
|
*/
|
|
proto.sortColumn = function (column, direction) {
|
|
// Use columns API until sortColumn method is removed
|
|
this.columns().sort(column, direction);
|
|
};
|
|
|
|
/**
|
|
* Add new row data
|
|
* @param {object} data
|
|
*/
|
|
proto.insert = function (data) {
|
|
|
|
var that = this,
|
|
rows = [];
|
|
if (isObject(data)) {
|
|
if (data.headings) {
|
|
if (!that.hasHeadings && !that.hasRows) {
|
|
var tr = createElement("tr"),
|
|
th;
|
|
each(data.headings, function (heading) {
|
|
th = createElement("th", {
|
|
html: heading
|
|
});
|
|
|
|
tr.appendChild(th);
|
|
});
|
|
that.head.appendChild(tr);
|
|
|
|
that.header = tr;
|
|
that.headings = [].slice.call(tr.cells);
|
|
that.hasHeadings = true;
|
|
|
|
// Re-enable sorting if it was disabled due
|
|
// to missing header
|
|
that.options.sortable = that.initialSortable;
|
|
|
|
// Allow sorting on new header
|
|
that.render("header");
|
|
}
|
|
}
|
|
|
|
if (data.data && isArray(data.data)) {
|
|
rows = data.data;
|
|
}
|
|
} else if (isArray(data)) {
|
|
each(data, function (row) {
|
|
var r = [];
|
|
each(row, function (cell, heading) {
|
|
|
|
var index = that.labels.indexOf(heading);
|
|
|
|
if (index > -1) {
|
|
r[index] = cell;
|
|
}
|
|
});
|
|
rows.push(r);
|
|
});
|
|
}
|
|
|
|
if (rows.length) {
|
|
that.rows().add(rows);
|
|
|
|
that.hasRows = true;
|
|
}
|
|
|
|
that.update();
|
|
|
|
that.fixColumns();
|
|
};
|
|
|
|
/**
|
|
* Refresh the instance
|
|
* @return {void}
|
|
*/
|
|
proto.refresh = function () {
|
|
if (this.options.searchable) {
|
|
this.input.value = "";
|
|
this.searching = false;
|
|
}
|
|
this.currentPage = 1;
|
|
this.onFirstPage = true;
|
|
this.update();
|
|
|
|
this.emit("datatable.refresh");
|
|
};
|
|
|
|
/**
|
|
* Truncate the table
|
|
* @param {mixes} html - HTML string or HTMLElement
|
|
* @return {void}
|
|
*/
|
|
proto.clear = function (html) {
|
|
if (this.body) {
|
|
flush(this.body, this.isIE);
|
|
}
|
|
|
|
var parent = this.body;
|
|
if (!this.body) {
|
|
parent = this.table;
|
|
}
|
|
|
|
if (html) {
|
|
if (typeof html === "string") {
|
|
var frag = doc.createDocumentFragment();
|
|
frag.innerHTML = html;
|
|
}
|
|
|
|
parent.appendChild(html);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Export table to various formats (csv, txt or sql)
|
|
* @param {Object} options User options
|
|
* @return {Boolean}
|
|
*/
|
|
proto.export = function (options) {
|
|
if (!this.hasHeadings && !this.hasRows) return false;
|
|
|
|
var headers = this.activeHeadings,
|
|
rows = [],
|
|
arr = [],
|
|
i,
|
|
x,
|
|
str,
|
|
link;
|
|
|
|
var defaults = {
|
|
download: true,
|
|
skipColumn: [],
|
|
|
|
// csv
|
|
lineDelimiter: "\n",
|
|
columnDelimiter: ",",
|
|
|
|
// sql
|
|
tableName: "myTable",
|
|
|
|
// json
|
|
replacer: null,
|
|
space: 4
|
|
};
|
|
|
|
// Check for the options object
|
|
if (!isObject(options)) {
|
|
return false;
|
|
}
|
|
|
|
var o = extend(defaults, options);
|
|
|
|
if (o.type) {
|
|
if (o.type === "txt" || o.type === "csv") {
|
|
// Include headings
|
|
rows[0] = this.header;
|
|
}
|
|
|
|
// Selection or whole table
|
|
if (o.selection) {
|
|
// Page number
|
|
if (!isNaN(o.selection)) {
|
|
rows = rows.concat(this.pages[o.selection - 1]);
|
|
} else if (isArray(o.selection)) {
|
|
// Array of page numbers
|
|
for (i = 0; i < o.selection.length; i++) {
|
|
rows = rows.concat(this.pages[o.selection[i] - 1]);
|
|
}
|
|
}
|
|
} else {
|
|
rows = rows.concat(this.activeRows);
|
|
}
|
|
|
|
// Only proceed if we have data
|
|
if (rows.length) {
|
|
if (o.type === "txt" || o.type === "csv") {
|
|
str = "";
|
|
|
|
for (i = 0; i < rows.length; i++) {
|
|
for (x = 0; x < rows[i].cells.length; x++) {
|
|
// Check for column skip and visibility
|
|
if (
|
|
o.skipColumn.indexOf(headers[x].originalCellIndex) < 0 &&
|
|
this.columns(headers[x].originalCellIndex).visible()
|
|
) {
|
|
var text = rows[i].texts[x].textContent;
|
|
text = text.trim();
|
|
text = text.replace(/\s{2,}/g, ' ');
|
|
text = text.replace(/\n/g, ' ');
|
|
text = text.replace(/"/g, '""');
|
|
if (text.indexOf(",") > -1)
|
|
text = '"' + text + '"';
|
|
|
|
|
|
str += text + o.columnDelimiter;
|
|
}
|
|
}
|
|
// Remove trailing column delimiter
|
|
str = str.trim().substring(0, str.length - 1);
|
|
|
|
// Apply line delimiter
|
|
str += o.lineDelimiter;
|
|
}
|
|
|
|
// Remove trailing line delimiter
|
|
str = str.trim().substring(0, str.length - 1);
|
|
|
|
if (o.download) {
|
|
str = "data:text/csv;charset=utf-8," + str;
|
|
}
|
|
} else if (o.type === "sql") {
|
|
// Begin INSERT statement
|
|
str = "INSERT INTO `" + o.tableName + "` (";
|
|
|
|
// Convert table headings to column names
|
|
for (i = 0; i < headers.length; i++) {
|
|
// Check for column skip and column visibility
|
|
if (
|
|
o.skipColumn.indexOf(headers[i].originalCellIndex) < 0 &&
|
|
this.columns(headers[i].originalCellIndex).visible()
|
|
) {
|
|
str += "`" + headers[i].textContent + "`,";
|
|
}
|
|
}
|
|
|
|
// Remove trailing comma
|
|
str = str.trim().substring(0, str.length - 1);
|
|
|
|
// Begin VALUES
|
|
str += ") VALUES ";
|
|
|
|
// Iterate rows and convert cell data to column values
|
|
for (i = 0; i < rows.length; i++) {
|
|
str += "(";
|
|
|
|
for (x = 0; x < rows[i].cells.length; x++) {
|
|
// Check for column skip and column visibility
|
|
if (
|
|
o.skipColumn.indexOf(headers[x].originalCellIndex) < 0 &&
|
|
this.columns(headers[x].originalCellIndex).visible()
|
|
) {
|
|
str += '"' + rows[i].cells[x].textContent + '",';
|
|
}
|
|
}
|
|
|
|
// Remove trailing comma
|
|
str = str.trim().substring(0, str.length - 1);
|
|
|
|
// end VALUES
|
|
str += "),";
|
|
}
|
|
|
|
// Remove trailing comma
|
|
str = str.trim().substring(0, str.length - 1);
|
|
|
|
// Add trailing colon
|
|
str += ";";
|
|
|
|
if (o.download) {
|
|
str = "data:application/sql;charset=utf-8," + str;
|
|
}
|
|
} else if (o.type === "json") {
|
|
// Iterate rows
|
|
for (x = 0; x < rows.length; x++) {
|
|
arr[x] = arr[x] || {};
|
|
// Iterate columns
|
|
for (i = 0; i < headers.length; i++) {
|
|
// Check for column skip and column visibility
|
|
if (
|
|
o.skipColumn.indexOf(headers[i].originalCellIndex) < 0 &&
|
|
this.columns(headers[i].originalCellIndex).visible()
|
|
) {
|
|
arr[x][headers[i].textContent] = rows[x].cells[i].textContent;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert the array of objects to JSON string
|
|
str = JSON.stringify(arr, o.replacer, o.space);
|
|
|
|
if (o.download) {
|
|
str = "data:application/json;charset=utf-8," + str;
|
|
}
|
|
}
|
|
|
|
// Download
|
|
if (o.download) {
|
|
// Filename
|
|
o.filename = o.filename || "datatable_export";
|
|
o.filename += "." + o.type;
|
|
|
|
str = encodeURI(str);
|
|
|
|
// Create a link to trigger the download
|
|
link = document.createElement("a");
|
|
link.href = str;
|
|
link.download = o.filename;
|
|
|
|
// Append the link
|
|
body.appendChild(link);
|
|
|
|
// Trigger the download
|
|
link.click();
|
|
|
|
// Remove the link
|
|
body.removeChild(link);
|
|
}
|
|
|
|
return str;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Import data to the table
|
|
* @param {Object} options User options
|
|
* @return {Boolean}
|
|
*/
|
|
proto.import = function (options) {
|
|
var obj = false;
|
|
var defaults = {
|
|
// csv
|
|
lineDelimiter: "\n",
|
|
columnDelimiter: ","
|
|
};
|
|
|
|
// Check for the options object
|
|
if (!isObject(options)) {
|
|
return false;
|
|
}
|
|
|
|
options = extend(defaults, options);
|
|
|
|
if (options.data.length || isObject(options.data)) {
|
|
// Import CSV
|
|
if (options.type === "csv") {
|
|
obj = {
|
|
data: []
|
|
};
|
|
|
|
// Split the string into rows
|
|
var rows = options.data.split(options.lineDelimiter);
|
|
|
|
if (rows.length) {
|
|
|
|
if (options.headings) {
|
|
obj.headings = rows[0].split(options.columnDelimiter);
|
|
|
|
rows.shift();
|
|
}
|
|
|
|
each(rows, function (row, i) {
|
|
obj.data[i] = [];
|
|
|
|
// Split the rows into values
|
|
var values = row.split(options.columnDelimiter);
|
|
|
|
if (values.length) {
|
|
each(values, function (value) {
|
|
obj.data[i].push(value);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} else if (options.type === "json") {
|
|
var json = isJson(options.data);
|
|
|
|
// Valid JSON string
|
|
if (json) {
|
|
obj = json;
|
|
} else {
|
|
console.warn("That's not valid JSON!");
|
|
}
|
|
}
|
|
|
|
if (isObject(options.data)) {
|
|
obj = options.data;
|
|
}
|
|
|
|
if (obj) {
|
|
// Add the rows
|
|
this.insert(obj);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
/**
|
|
* Print the table
|
|
* @return {void}
|
|
*/
|
|
proto.print = function () {
|
|
var headings = this.activeHeadings;
|
|
var rows = this.activeRows;
|
|
var table = createElement("table");
|
|
var thead = createElement("thead");
|
|
var tbody = createElement("tbody");
|
|
|
|
var tr = createElement("tr");
|
|
each(headings, function (th) {
|
|
tr.appendChild(
|
|
createElement("th", {
|
|
html: th.textContent
|
|
})
|
|
);
|
|
});
|
|
|
|
thead.appendChild(tr);
|
|
|
|
each(rows, function (row) {
|
|
var tr = createElement("tr");
|
|
each(row.cells, function (cell) {
|
|
tr.appendChild(
|
|
createElement("td", {
|
|
html: cell.textContent
|
|
})
|
|
);
|
|
});
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
table.appendChild(thead);
|
|
table.appendChild(tbody);
|
|
|
|
// Open new window
|
|
var w = win.open();
|
|
|
|
// Append the table to the body
|
|
w.document.body.appendChild(table);
|
|
|
|
// Print
|
|
w.print();
|
|
};
|
|
|
|
/**
|
|
* Show a message in the table
|
|
* @param {string} message
|
|
*/
|
|
proto.setMessage = function (message) {
|
|
var colspan = 1;
|
|
|
|
if (this.hasRows) {
|
|
colspan = this.data[0].cells.length;
|
|
}
|
|
|
|
classList.add(this.wrapper, "dataTable-empty");
|
|
|
|
this.clear(
|
|
createElement("tr", {
|
|
html: '<td class="dataTables-empty" colspan="' +
|
|
colspan +
|
|
'">' +
|
|
message +
|
|
"</td>"
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Columns API access
|
|
* @return {Object} new Columns instance
|
|
*/
|
|
proto.columns = function (columns) {
|
|
return new Columns(this, columns);
|
|
};
|
|
|
|
/**
|
|
* Rows API access
|
|
* @return {Object} new Rows instance
|
|
*/
|
|
proto.rows = function (rows) {
|
|
return new Rows(this, rows);
|
|
};
|
|
|
|
/**
|
|
* Add custom event listener
|
|
* @param {String} event
|
|
* @param {Function} callback
|
|
* @return {Void}
|
|
*/
|
|
proto.on = function (event, callback) {
|
|
this.events = this.events || {};
|
|
this.events[event] = this.events[event] || [];
|
|
this.events[event].push(callback);
|
|
};
|
|
|
|
/**
|
|
* Remove custom event listener
|
|
* @param {String} event
|
|
* @param {Function} callback
|
|
* @return {Void}
|
|
*/
|
|
proto.off = function (event, callback) {
|
|
this.events = this.events || {};
|
|
if (event in this.events === false) return;
|
|
this.events[event].splice(this.events[event].indexOf(callback), 1);
|
|
};
|
|
|
|
/**
|
|
* Fire custom event
|
|
* @param {String} event
|
|
* @return {Void}
|
|
*/
|
|
proto.emit = function (event) {
|
|
this.events = this.events || {};
|
|
if (event in this.events === false) return;
|
|
for (var i = 0; i < this.events[event].length; i++) {
|
|
this.events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
|
|
}
|
|
};
|
|
|
|
return DataTable;
|
|
});
|