mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-23 01:58:09 +03:00
12910 lines
429 KiB
JavaScript
Vendored
12910 lines
429 KiB
JavaScript
Vendored
/*
|
|
* This combined file was created by the DataTables downloader builder:
|
|
* https://datatables.net/download
|
|
*
|
|
* To rebuild or modify this file with the latest versions of the included
|
|
* software please visit:
|
|
* https://datatables.net/download/#bs5/dt-3.0.4
|
|
*
|
|
* Included libraries:
|
|
* DataTables 3.0.4
|
|
*/
|
|
|
|
/*! DataTables 3.0.4
|
|
* Copyright (c) SpryMedia Ltd - datatables.net/license
|
|
*/
|
|
|
|
(function(factory){
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD
|
|
define([], function () {
|
|
return factory(window, document);
|
|
});
|
|
}
|
|
else if (typeof exports === 'object') {
|
|
// CommonJS
|
|
var cjsRequires = function (root) { };
|
|
|
|
if (typeof window === 'undefined') {
|
|
module.exports = function (root) {
|
|
if (! root) {
|
|
// CommonJS environments without a window global must pass a
|
|
// root. This will give an error otherwise
|
|
root = window;
|
|
}
|
|
|
|
cjsRequires(root);
|
|
return factory(root, root.document);
|
|
};
|
|
}
|
|
else {
|
|
cjsRequires(window);
|
|
module.exports = factory(window, window.document);
|
|
}
|
|
}
|
|
else {
|
|
// Browser
|
|
window.DataTable = factory(window, document);
|
|
}
|
|
}(function(window, document) {
|
|
'use strict';
|
|
|
|
|
|
|
|
// A collection of the regular expressions used throughout the code base. Not all are here
|
|
// just the ones that need to be reused - no need to dump single use expressions here.
|
|
// https://en.wikipedia.org/wiki/Foreign_exchange_market
|
|
// - \u20BD - Russian ruble.
|
|
// - \u20a9 - South Korean Won
|
|
// - \u20BA - Turkish Lira
|
|
// - \u20B9 - Indian Rupee
|
|
// - R - Brazil (R$) and South Africa
|
|
// - fr - Swiss Franc
|
|
// - kr - Swedish krona, Norwegian krone and Danish krone
|
|
// - \u2009 is thin space and \u202F is narrow no-break space, both used in many
|
|
// - Ƀ - Bitcoin
|
|
// - Ξ - Ethereum
|
|
// standards as thousands separators.
|
|
const reFormattedNumeric = /['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi;
|
|
const reHtml = /<([^>]*>)/g;
|
|
// Escape regular expression special characters
|
|
const reRegexCharacters = new RegExp('(\\' +
|
|
[
|
|
'/',
|
|
'.',
|
|
'*',
|
|
'+',
|
|
'?',
|
|
'|',
|
|
'(',
|
|
')',
|
|
'[',
|
|
']',
|
|
'{',
|
|
'}',
|
|
'\\',
|
|
'$',
|
|
'^',
|
|
'-',
|
|
].join('|\\') +
|
|
')', 'g');
|
|
// This is not strict ISO8601 - Date.parse() is quite lax, although
|
|
// implementations differ between browsers.
|
|
const reDate = /^\d{2,4}[./-]\d{1,2}[./-]\d{1,2}([T ]{1}\d{1,2}[:.]\d{2}([.:]\d{2})?)?$/;
|
|
const reNewLines = /[\r\n\u2028]/g;
|
|
const isoTimezone = /[T\s]\d{2}.*?(Z|[+-]\d{2}(?::?\d{2})?)$/;
|
|
|
|
var regex = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
isoTimezone: isoTimezone,
|
|
reDate: reDate,
|
|
reFormattedNumeric: reFormattedNumeric,
|
|
reHtml: reHtml,
|
|
reNewLines: reNewLines,
|
|
reRegexCharacters: reRegexCharacters
|
|
});
|
|
|
|
const maxStrLen = Math.pow(2, 28);
|
|
/**
|
|
* DataTables default string normalisation. Remove diacritics from a string by
|
|
* decomposing it and then removing non-ascii characters.
|
|
*
|
|
* This function is replaceable if the user wishes to use a different library
|
|
* for normalising a string.
|
|
*
|
|
* @param val Value to normalise (if a string)
|
|
* @param both Include both the normalised and original in the return
|
|
* @returns Normalised string, or original value if not a string
|
|
*/
|
|
let _normalize = function (val, both) {
|
|
if (typeof val !== 'string') {
|
|
return val;
|
|
}
|
|
// It is faster to just run `normalize` than it is to check if
|
|
// we need to with a regex! (Check as it isn't available in old
|
|
// Safari)
|
|
var res = val.normalize ? val.normalize('NFD') : val;
|
|
// Equally, here we check if a regex is needed or not
|
|
return res.length !== val.length
|
|
? (both === true ? val + ' ' : '') + res.replace(/[\u0300-\u036f]/g, '')
|
|
: res;
|
|
};
|
|
/**
|
|
* DataTables default string HTML stripping from a string
|
|
*
|
|
* This function is replaceable if the user wishes to use a different library
|
|
* for stripping HTML from a string.
|
|
*
|
|
* @param input Value to strip HTML from
|
|
* @param replacement Value to replace the tags with
|
|
* @returns Stripped value
|
|
*/
|
|
let _stripHtml = function (input, replacement = '') {
|
|
if (!input || typeof input !== 'string') {
|
|
return input;
|
|
}
|
|
// Irrelevant check to workaround CodeQL's false positive on the regex
|
|
if (input.length > maxStrLen) {
|
|
throw new Error('Exceeded max str len');
|
|
}
|
|
let previous;
|
|
let next = input.replace(reHtml, replacement); // Complete tags
|
|
// Safety for incomplete script tag - use do / while to ensure that
|
|
// we get all instances
|
|
do {
|
|
previous = next;
|
|
next = next.replace(/<script/i, '');
|
|
} while (next !== previous);
|
|
// T must be a string, but TS can't seem to figure that out
|
|
return previous;
|
|
};
|
|
/**
|
|
* DataTables default HTML entity escaping.
|
|
*
|
|
* This function is replaceable if the user wishes to use a different library
|
|
* for escaping HTML entities in a string.
|
|
*
|
|
* @param val Value to escape HTML in
|
|
* @returns Escaped value
|
|
*/
|
|
let _escapeHtml = function (val) {
|
|
let d = Array.isArray(val) ? val.join(',') : val;
|
|
return typeof d === 'string'
|
|
? d
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
: d;
|
|
};
|
|
/**
|
|
* Escape regular expression characters in a string
|
|
*
|
|
* @param val String to escape
|
|
* @returns String with regex characters escaped
|
|
*/
|
|
function escapeRegex(val) {
|
|
return val.replace(reRegexCharacters, '\\$1');
|
|
}
|
|
function escapeHtml(mixed) {
|
|
var type = typeof mixed;
|
|
if (type === 'function') {
|
|
_escapeHtml = mixed;
|
|
return;
|
|
}
|
|
else if (type === 'string' || Array.isArray(mixed)) {
|
|
return _escapeHtml(mixed);
|
|
}
|
|
return mixed;
|
|
}
|
|
function normalize(mixed, both) {
|
|
var type = typeof mixed;
|
|
if (type !== 'function') {
|
|
return _normalize(mixed, both);
|
|
}
|
|
_normalize = mixed;
|
|
}
|
|
function stripHtml(mixed, replacement) {
|
|
const type = typeof mixed;
|
|
if (type === 'function') {
|
|
_stripHtml = mixed;
|
|
return;
|
|
}
|
|
else if (type === 'string') {
|
|
return _stripHtml(mixed, replacement);
|
|
}
|
|
return mixed;
|
|
}
|
|
|
|
var string = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
escapeHtml: escapeHtml,
|
|
escapeRegex: escapeRegex,
|
|
normalize: normalize,
|
|
stripHtml: stripHtml
|
|
});
|
|
|
|
const _re_dic = {};
|
|
/**
|
|
* Get integer value
|
|
*
|
|
* @param s Value
|
|
* @returns Int, or null if not a number
|
|
*/
|
|
function intVal(s) {
|
|
var integer = parseInt(s, 10);
|
|
return !isNaN(integer) && isFinite(s) ? integer : null;
|
|
}
|
|
// Convert from a formatted number with characters other than `.` as the
|
|
// decimal place, to a JavaScript number
|
|
function numToDecimal(num, decimalPoint) {
|
|
// Cache created regular expressions for speed as this function is called often
|
|
if (!_re_dic[decimalPoint]) {
|
|
_re_dic[decimalPoint] = new RegExp(escapeRegex(decimalPoint), 'g');
|
|
}
|
|
return typeof num === 'string' && decimalPoint !== '.'
|
|
? num.replace(/\./g, '').replace(_re_dic[decimalPoint], '.')
|
|
: num;
|
|
}
|
|
|
|
var conv = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
intVal: intVal,
|
|
numToDecimal: numToDecimal
|
|
});
|
|
|
|
function arrayLike(test) {
|
|
return (test && // Exists
|
|
typeof test !== 'string' && // Is not a string
|
|
test.length !== undefined && // Has a length
|
|
test.nodeType === undefined // Is not a text node
|
|
);
|
|
}
|
|
/**
|
|
* Determine if the input is a Dom instance
|
|
*
|
|
* @param input Value to check
|
|
* @returns true if it is a Dom instance, false otherwise
|
|
*/
|
|
function dom(input) {
|
|
return input && typeof input === 'object' && input._isDom;
|
|
}
|
|
/**
|
|
* Determine if the input is an HTML element
|
|
*
|
|
* @param input Value to check
|
|
* @returns true if an HTML element was passed in
|
|
*/
|
|
function element(input) {
|
|
return typeof input === 'object' && input.nodeName;
|
|
}
|
|
/**
|
|
* Check if a value is empty or not. Note that a string with `-` is considered
|
|
* empty
|
|
*
|
|
* @param d Value to check
|
|
* @returns `true` if empty, `false` otherwise
|
|
*/
|
|
function empty(d) {
|
|
return !d || d === true || d === '-' ? true : false;
|
|
}
|
|
/**
|
|
* Check if a string is HTML. Note that a string without HTML in it can be
|
|
* considered to be HTML still!
|
|
*
|
|
* @todo Can we drop this?
|
|
* @param d
|
|
* @returns
|
|
*/
|
|
function html(d) {
|
|
return empty(d) || typeof d === 'string';
|
|
}
|
|
/**
|
|
* Is a string a number surrounded by HTML?
|
|
*
|
|
* @param d Value to check
|
|
* @param decimalPoint Decimal place character
|
|
* @param formatted Consider formatted numbers
|
|
* @param allowEmpty Allow empty to be considered as a number
|
|
* @returns True if a number, null otherwise
|
|
*/
|
|
function htmlNum(d, decimalPoint, formatted, allowEmpty) {
|
|
if (allowEmpty && empty(d)) {
|
|
return true;
|
|
}
|
|
// input and select strings mean that this isn't just a number
|
|
if (typeof d === 'string' && d.match(/<(input|select)/i)) {
|
|
return null;
|
|
}
|
|
return !html(d)
|
|
? null
|
|
: num$1(stripHtml(d), decimalPoint, formatted, allowEmpty)
|
|
? true
|
|
: null;
|
|
}
|
|
/**
|
|
* Determine if an input is a jQuery instance
|
|
*
|
|
* @param input Value to check
|
|
* @returns true if it is a jQuery instance, false otherwise
|
|
*/
|
|
function jquery(input) {
|
|
return input && typeof input.jquery === 'string';
|
|
}
|
|
/**
|
|
* Check if a given value is numeric, taking into account if it might be
|
|
* formatted or uses a decimal point that is not a period.
|
|
*
|
|
* @param d Value to check
|
|
* @param decimalPoint DP character
|
|
* @param formatted Allow the number to be formatted or not
|
|
* @param allowEmpty Allow an empty value to be considered a number
|
|
* @returns `true` if numeric
|
|
*/
|
|
function num$1(d, decimalPoint, formatted, allowEmpty) {
|
|
let type = typeof d;
|
|
if (type === 'number' || type === 'bigint') {
|
|
return true;
|
|
}
|
|
// If empty return immediately so there must be a number if it is a
|
|
// formatted string (this stops the string "k", or "kr", etc being detected
|
|
// as a formatted number for currency
|
|
if (allowEmpty && empty(d)) {
|
|
return true;
|
|
}
|
|
if (decimalPoint && type === 'string') {
|
|
d = numToDecimal(d, decimalPoint);
|
|
}
|
|
if (formatted && type === 'string') {
|
|
d = d.replace(reFormattedNumeric, '');
|
|
}
|
|
return !isNaN(parseFloat(d)) && isFinite(d);
|
|
}
|
|
/**
|
|
* Determine if a value is a plain object or not
|
|
*
|
|
* @param value Value to check
|
|
* @returns true if is a plain object, otherwise false
|
|
*/
|
|
function plainObject(value) {
|
|
if (typeof value !== 'object' || value === null) {
|
|
return false;
|
|
}
|
|
let proto = Object.getPrototypeOf(value);
|
|
return proto === null || proto === Object.prototype;
|
|
}
|
|
|
|
var is = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
arrayLike: arrayLike,
|
|
dom: dom,
|
|
element: element,
|
|
empty: empty,
|
|
html: html,
|
|
htmlNum: htmlNum,
|
|
jquery: jquery,
|
|
num: num$1,
|
|
plainObject: plainObject
|
|
});
|
|
|
|
/**
|
|
* Object iteration function, executing a callback for each key in the object
|
|
*
|
|
* @param input Input object
|
|
* @param fn Function to execute
|
|
*/
|
|
function each(input, fn) {
|
|
if (!input) {
|
|
return;
|
|
}
|
|
let keys = Object.keys(input);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
let key = keys[i];
|
|
fn(key, input[key], i);
|
|
}
|
|
}
|
|
/**
|
|
* Merge the contents of two or more objects into the first object.
|
|
*
|
|
* @param out Object to be assigned the properties
|
|
* @param inputs Objects to take the values from
|
|
* @returns The `output`, just for convenience - output === the return.
|
|
*/
|
|
function assign(out, ...inputs) {
|
|
let output = Object(out);
|
|
// Can't just use `Object.assign` as it will assign `undefined` as a regular
|
|
// value to the target.
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
let options = inputs[i];
|
|
// Filter inputs
|
|
if (options != null) {
|
|
// Extend the base object
|
|
for (let name in options) {
|
|
let copy = options[name];
|
|
// Prevent Object.prototype pollution
|
|
// Prevent never-ending loop
|
|
if (name === '__proto__' || output === copy) {
|
|
continue;
|
|
}
|
|
// Ignore undefined values (this is why we can't use Object.assign)
|
|
if (copy !== undefined) {
|
|
output[name] = copy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
/**
|
|
* Deep merge the contents of two or more objects into the first object. This
|
|
* breaks references for both objects and array.
|
|
*
|
|
* @param out Object to be assigned the properties
|
|
* @param inputs Objects to take the values from
|
|
* @returns The `output`, just for convenience - output === the return.
|
|
*/
|
|
function assignDeep(out, ...inputs) {
|
|
if (!out) {
|
|
return {};
|
|
}
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
let input = inputs[i];
|
|
if (!input) {
|
|
continue;
|
|
}
|
|
for (const [key, value] of Object.entries(input)) {
|
|
if (Array.isArray(value)) {
|
|
if (!Array.isArray(out[key])) {
|
|
out[key] = [];
|
|
}
|
|
assignDeep(out[key], value);
|
|
}
|
|
else if (plainObject(value)) {
|
|
if (!plainObject(out[key])) {
|
|
out[key] = {};
|
|
}
|
|
assignDeep(out[key], value);
|
|
}
|
|
else if (input[key] !== undefined) {
|
|
out[key] = input[key];
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Deep merge objects, but shallow copy arrays. The reason we need to do this,
|
|
* is that we don't want to deep copy array init values (such as aaSorting)
|
|
* since the dev wouldn't be able to override them, but we do want to deep copy
|
|
* objects.
|
|
*
|
|
* @param out Object to extend
|
|
* @param extender Object from which the properties will be applied to out
|
|
* @param breakRefs If true, then arrays will be sliced to take an independent
|
|
* copy with the exception of the `data` or `aaData` parameters if they are
|
|
* present. This is so you can pass in a collection to DataTables and have
|
|
* that used as your data source without breaking the references
|
|
* @returns out Reference, just for convenience - out === the return.
|
|
*/
|
|
function assignDeepObjects(out, extender, breakRefs = false) {
|
|
let val;
|
|
for (let prop in extender) {
|
|
if (Object.prototype.hasOwnProperty.call(extender, prop)) {
|
|
val = extender[prop];
|
|
if (plainObject(val)) {
|
|
if (!plainObject(out[prop])) {
|
|
out[prop] = {};
|
|
}
|
|
assignDeepObjects(out[prop], val, breakRefs);
|
|
}
|
|
else if (breakRefs &&
|
|
prop !== 'data' &&
|
|
prop !== 'aaData' &&
|
|
Array.isArray(val)) {
|
|
out[prop] = val.slice();
|
|
}
|
|
else {
|
|
out[prop] = val;
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Map entries to an array
|
|
*
|
|
* @param obj In object
|
|
* @param fn Map transform function. Same signature as `each`
|
|
* @returns Result
|
|
*/
|
|
function map$1(obj, fn) {
|
|
let out = [];
|
|
each(obj, (key, val) => {
|
|
out.push(fn(key, val));
|
|
});
|
|
return out;
|
|
}
|
|
|
|
var object = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
assign: assign,
|
|
assignDeep: assignDeep,
|
|
assignDeepObjects: assignDeepObjects,
|
|
each: each,
|
|
map: map$1
|
|
});
|
|
|
|
const defaults$5 = {
|
|
cache: true,
|
|
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
headers: {},
|
|
traditional: false,
|
|
url: location.href
|
|
};
|
|
/**
|
|
* Trigger an Ajax call to the server based on the configuration parameters
|
|
* passed in.
|
|
*
|
|
* @param optionsIn Ajax options
|
|
* @returns The XHR request
|
|
*/
|
|
function ajax(optionsIn) {
|
|
let xhr = new XMLHttpRequest();
|
|
let options = assign({}, defaults$5, optionsIn);
|
|
let urlParams = queryParams(options);
|
|
let method = httpMethod(options);
|
|
let sendData = null;
|
|
// Allow the data to be sent to the server as a simple JSON string -
|
|
// primarily to be used with POST / PUT
|
|
if (options.submitAs === 'json' && options.data) {
|
|
options.data = JSON.stringify(options.data);
|
|
if (!options.contentType) {
|
|
options.contentType = 'application/json; charset=utf-8';
|
|
}
|
|
}
|
|
xhr.open(method, options.url +
|
|
(urlParams
|
|
? (options.url.includes('?') ? '&' : '?') + urlParams
|
|
: ''), true, options.username || null, options.password || null);
|
|
// Content type for FormData requests gets set by the browser.
|
|
if (options.contentType && !(options.data instanceof FormData)) {
|
|
xhr.setRequestHeader('Content-Type', options.contentType);
|
|
}
|
|
// Add a X-Request-With header, as jQuery does so and some server-side
|
|
// platforms look for it. Only for same domain though.
|
|
if (options.headers &&
|
|
!options.headers['X-Requested-With'] &&
|
|
!isCrossDomain(options.url)) {
|
|
options.headers['X-Requested-With'] = 'XMLHttpRequest';
|
|
}
|
|
// Add an accept header specifically for JSON data types, again to match
|
|
// how jQuery operates for this.
|
|
if (options.dataType === 'json' &&
|
|
options.headers &&
|
|
!options.headers['accepts']) {
|
|
options.headers['Accept'] =
|
|
'application/json, text/javascript, */*; q=0.01';
|
|
}
|
|
each(options.headers, (key, val) => {
|
|
xhr.setRequestHeader(key, val);
|
|
});
|
|
if (options.data instanceof FormData) {
|
|
sendData = options.data;
|
|
}
|
|
else if (method !== 'GET' && options.data) {
|
|
if (typeof options.data === 'string') {
|
|
sendData = options.data;
|
|
}
|
|
else {
|
|
sendData = serialize(options.data, options.traditional);
|
|
sendData = convertSpaces(sendData, options);
|
|
// So beforeSend matches how jQuery behaves
|
|
options.data = sendData;
|
|
}
|
|
}
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState != 4) {
|
|
return;
|
|
}
|
|
let responseData = xhr.responseText;
|
|
let statusText = 'success';
|
|
if (xhr.status === 0) {
|
|
return; // aborted
|
|
}
|
|
else if (xhr.status === 204 || method === 'HEAD') {
|
|
statusText = 'nocontent';
|
|
}
|
|
else if (xhr.status === 304) {
|
|
statusText = 'notmodified';
|
|
}
|
|
else if (xhr.status >= 400) {
|
|
statusText = 'error';
|
|
}
|
|
// Return data type handling
|
|
if (options.dataType === 'json') {
|
|
try {
|
|
responseData = JSON.parse(responseData);
|
|
}
|
|
catch (e) {
|
|
statusText = 'parsererror';
|
|
}
|
|
}
|
|
else if (!options.dataType) {
|
|
// Data type is undefined, so attempt to parse as JSON if possible,
|
|
// but no error if it can't be
|
|
try {
|
|
responseData = JSON.parse(responseData);
|
|
}
|
|
catch (e) {
|
|
// noop
|
|
}
|
|
}
|
|
if (statusText === 'success') {
|
|
callback(options.success, responseData, statusText, xhr);
|
|
}
|
|
else {
|
|
callback(options.error, xhr, statusText, xhr.statusText);
|
|
}
|
|
callback(options.complete, xhr, statusText);
|
|
};
|
|
if (options.beforeSend) {
|
|
if (options.beforeSend.call(options, xhr, options) === false) {
|
|
xhr.abort();
|
|
return xhr;
|
|
}
|
|
}
|
|
xhr.send(sendData);
|
|
return xhr;
|
|
}
|
|
// Expose defaults and serialisation method
|
|
ajax.defaults = defaults$5;
|
|
ajax.serialize = serialize;
|
|
/**
|
|
* Run callback functions (allowing for none, one or array)
|
|
*
|
|
* @param fnIn Function(s) to run
|
|
* @param arg1 Parameters to pass to the function(s)
|
|
* @param arg2 Parameters to pass to the function(s)
|
|
* @param arg3 Parameters to pass to the function(s)
|
|
*/
|
|
function callback(fnIn, arg1, arg2, arg3) {
|
|
if (!fnIn) {
|
|
return;
|
|
}
|
|
let fnArr = Array.isArray(fnIn) ? fnIn : [fnIn];
|
|
for (let i = 0; i < fnArr.length; i++) {
|
|
fnArr[i](arg1, arg2, arg3);
|
|
}
|
|
}
|
|
/**
|
|
* For form submission with x-www-form-urlencoded, spaces should be submitted as
|
|
* `+`. See the jQuery discussion on the topic here:
|
|
* https://github.com/jquery/jquery/issues/2658#issuecomment-149024872
|
|
*
|
|
* @param sendData Serialised form of the data to submit
|
|
* @param options Ajax options
|
|
* @returns Query string
|
|
*/
|
|
function convertSpaces(sendData, options) {
|
|
return (options.contentType || '').indexOf('application/x-www-form-urlencoded') === 0
|
|
? sendData.replace(/%20/g, '+')
|
|
: sendData;
|
|
}
|
|
/**
|
|
* Determine if a url is a cross domain request or not
|
|
*
|
|
* @param url URL to check
|
|
* @returns True if cross domain, false otherwise
|
|
*/
|
|
function isCrossDomain(url) {
|
|
// Use the current page as the base to handle relative URLs correctly
|
|
const target = new URL(url, window.location.origin);
|
|
return target.origin !== window.location.origin;
|
|
}
|
|
/**
|
|
* Get the HTTP method from the Ajax request options
|
|
*
|
|
* @param options Ajax options
|
|
* @returns HTTP verb
|
|
*/
|
|
function httpMethod(options) {
|
|
let method = 'GET';
|
|
if (options.type) {
|
|
method = options.type;
|
|
}
|
|
if (options.method) {
|
|
method = options.method;
|
|
}
|
|
return method.toUpperCase();
|
|
}
|
|
/**
|
|
* Get the query parameters based on the options (method, cache and data all
|
|
* need to be considered).
|
|
*
|
|
* @param options Ajax options
|
|
* @returns URL string
|
|
*/
|
|
function queryParams(options) {
|
|
let requestParams = [];
|
|
if (httpMethod(options) === 'GET') {
|
|
// Construct URL parameters string
|
|
requestParams.push(serialize(options.data, options.traditional));
|
|
}
|
|
// If a DELETE method is used there are a number of servers which will
|
|
// reject the request if it has a body. So we need to append to the URL.
|
|
//
|
|
// http://stackoverflow.com/questions/15088955
|
|
// http://bugs.jquery.com/ticket/11586
|
|
if (httpMethod(options) === 'DELETE' &&
|
|
(options.deleteBody === undefined || options.deleteBody === true)) {
|
|
requestParams.push(serialize(options.data, options.traditional));
|
|
delete options.data;
|
|
}
|
|
if (options.cache === false) {
|
|
requestParams.push(serialize({ _: +new Date() }));
|
|
}
|
|
return convertSpaces(requestParams.filter(d => !!d).join('&'), options);
|
|
}
|
|
/**
|
|
* Convert an object into a list of parameters for a query request. Supports
|
|
* jQuery traditional option for legacy applications.
|
|
*
|
|
* @param obj Object to convert
|
|
* @param traditional If jQuery old style should be used
|
|
* @returns Parameter-ized string
|
|
*/
|
|
function serialize(obj, traditional = false) {
|
|
var params = [];
|
|
if (obj === undefined || obj === null) {
|
|
return '';
|
|
}
|
|
serializeNested(params, obj, traditional);
|
|
return params.join('&');
|
|
}
|
|
/**
|
|
* Recursive serialisation function
|
|
*
|
|
* @param params Array to write the serialised parameters to
|
|
* @param obj Object / array to serialise
|
|
* @param traditional Traditional flag for legacy
|
|
* @param scope Recursive scope
|
|
*/
|
|
function serializeNested(params, obj, traditional, scope = '') {
|
|
let array = Array.isArray(obj);
|
|
for (let key in obj) {
|
|
let value = obj[key];
|
|
let nestDown = Array.isArray(value) || (!traditional && plainObject(value));
|
|
if (scope) {
|
|
// Non-scalar values need the index set on the host
|
|
let index = !array || nestDown ? key : '';
|
|
key = traditional ? scope : scope + '[' + index + ']';
|
|
}
|
|
if (!scope && array) {
|
|
serializeAdd(params, value.name, value.value);
|
|
}
|
|
else if (nestDown) {
|
|
// Nest down
|
|
serializeNested(params, value, traditional, key);
|
|
}
|
|
else {
|
|
serializeAdd(params, key, value);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Add a name / value pair to the list of parameters
|
|
*
|
|
* @param params Parameter values
|
|
* @param name Parameter name
|
|
* @param value Parameter value
|
|
*/
|
|
function serializeAdd(params, name, value) {
|
|
// Allow the input to be a function to match how jQuery operates
|
|
let strVal = typeof value === 'function' ? value() : value;
|
|
params.push(encodeURIComponent(name) +
|
|
'=' +
|
|
encodeURIComponent(strVal === null ? '' : strVal));
|
|
}
|
|
|
|
/**
|
|
* Determine if all values in the array are unique. This means we can short
|
|
* cut the _unique method at the cost of a single loop. A sorted array is used
|
|
* to easily check the values.
|
|
*
|
|
* @param src Source array
|
|
* @return true if all unique, false otherwise
|
|
*/
|
|
function allUnique(src) {
|
|
if (src.length < 2) {
|
|
return true;
|
|
}
|
|
var sorted = src.slice().sort();
|
|
var last = sorted[0];
|
|
for (var i = 1, iLen = sorted.length; i < iLen; i++) {
|
|
if (sorted[i] === last) {
|
|
return false;
|
|
}
|
|
last = sorted[i];
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Flatten an array
|
|
*
|
|
* Surprisingly this is faster than [].concat.apply
|
|
* https://jsperf.com/flatten-an-array-loop-vs-reduce/2
|
|
*
|
|
* @param out Array to write to
|
|
* @param val Source array, or single value
|
|
* @returns Flattened array
|
|
*/
|
|
function flatten(out, val) {
|
|
if (Array.isArray(val) || arrayLike(val)) {
|
|
for (var i = 0; i < val.length; i++) {
|
|
flatten(out, val[i]);
|
|
}
|
|
}
|
|
else {
|
|
out.push(val);
|
|
}
|
|
return out;
|
|
}
|
|
function intersection(a1, a2) {
|
|
return a1.filter(item => a2.includes(item));
|
|
}
|
|
/**
|
|
* Pluck items from an array of objects, or from a nested array of objects
|
|
*
|
|
* @param a Array to get values from
|
|
* @param prop Property to read values from
|
|
* @param prop2 Inner property to get values from if a 2D array
|
|
* @returns Array of read values
|
|
*/
|
|
function pluck(a, prop, prop2) {
|
|
let out = [], i = 0, iLen = a.length;
|
|
// Could have the test in the loop for slightly smaller code, but speed
|
|
// is essential here
|
|
if (prop2 !== undefined) {
|
|
for (; i < iLen; i++) {
|
|
if (a[i] && a[i][prop]) {
|
|
out.push(a[i][prop][prop2]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (; i < iLen; i++) {
|
|
if (a[i]) {
|
|
out.push(a[i][prop]);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Basically the same as _pluck, but rather than looping over the source array we use `order`
|
|
* as the indexes to pick from the source array
|
|
*
|
|
* @param a Array to get values from
|
|
* @param order Indexes to pick
|
|
* @param prop Property to read values from
|
|
* @param prop2 Inner property to get values from if a 2D array
|
|
* @returns Array of read values
|
|
*/
|
|
function pluckOrder(a, order, prop, prop2) {
|
|
let out = [], i = 0, iLen = order.length;
|
|
// Could have the test in the loop for slightly smaller code, but speed
|
|
// is essential here
|
|
if (prop2 !== undefined) {
|
|
for (; i < iLen; i++) {
|
|
if (a[order[i]] && a[order[i]][prop]) {
|
|
out.push(a[order[i]][prop][prop2]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (; i < iLen; i++) {
|
|
if (a[order[i]]) {
|
|
out.push(a[order[i]][prop]);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
function range(len, start) {
|
|
var out = [];
|
|
var end;
|
|
if (start === undefined) {
|
|
start = 0;
|
|
end = len;
|
|
}
|
|
else {
|
|
end = start;
|
|
start = len;
|
|
}
|
|
for (var i = start; i < end; i++) {
|
|
out.push(i);
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Remove all falsy values from an array
|
|
*
|
|
* @param a Source array
|
|
* @returns A new array, with empty values removed
|
|
*/
|
|
function removeEmpty(a) {
|
|
var out = [];
|
|
for (var i = 0, iLen = a.length; i < iLen; i++) {
|
|
if (a[i]) {
|
|
// careful - will remove all falsy values!
|
|
out.push(a[i]);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Join data from an array, but only for specific columns.
|
|
*
|
|
* Performance testing for this available here:
|
|
* https://jsperf.app/vejijo/2/preview.
|
|
*
|
|
* @param src Data source array to pick from
|
|
* @param use Indexes we want from the array
|
|
* @returns Joined string
|
|
*/
|
|
function selectiveJoin(src, use) {
|
|
if (typeof use === 'number') {
|
|
return '' + src[use];
|
|
}
|
|
if (use.length === 0) {
|
|
return '';
|
|
}
|
|
let result = '' + src[use[0]];
|
|
for (let i = 1; i < use.length; i++) {
|
|
result += ' ' + src[use[i]];
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Find the unique elements in a source array.
|
|
*
|
|
* @param src Source array
|
|
* @return Array of unique items
|
|
*/
|
|
function unique(src) {
|
|
if (Array.from && Set) {
|
|
return Array.from(new Set(src));
|
|
}
|
|
if (allUnique(src)) {
|
|
return src.slice();
|
|
}
|
|
// A faster unique method is to use object keys to identify used values,
|
|
// but this doesn't work with arrays or objects, which we must also
|
|
// consider. See jsperf.app/compare-array-unique-versions/4 for more
|
|
// information.
|
|
var out = [], val, i, iLen = src.length, j, k = 0;
|
|
again: for (i = 0; i < iLen; i++) {
|
|
val = src[i];
|
|
for (j = 0; j < k; j++) {
|
|
if (out[j] === val) {
|
|
continue again;
|
|
}
|
|
}
|
|
out.push(val);
|
|
k++;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
var array = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
flatten: flatten,
|
|
intersection: intersection,
|
|
pluck: pluck,
|
|
pluckOrder: pluckOrder,
|
|
range: range,
|
|
removeEmpty: removeEmpty,
|
|
selectiveJoin: selectiveJoin,
|
|
unique: unique
|
|
});
|
|
|
|
// Private variable that is used to match action syntax in the data property object
|
|
const __reArray = /\[.*?\]$/;
|
|
const __reFn = /\(\)$/;
|
|
/**
|
|
* Split string on periods, taking into account escaped periods
|
|
*
|
|
* @param str String to split
|
|
* @return Split string
|
|
*/
|
|
function splitObjNotation(str) {
|
|
const parts = str.match(/(\\.|[^.])+/g) || [''];
|
|
return parts.map(function (s) {
|
|
return s.replace(/\\\./g, '.');
|
|
});
|
|
}
|
|
/**
|
|
* Create a function that will read data a common data point from different (but same structure)
|
|
* data objects. This is primarily used to get data for a specific cell in a single column, but it
|
|
* can also be used in other places, such as when using JSON notation.
|
|
*
|
|
* @param dataPoint The data point to get
|
|
* @returns Function to get a data point's value from a source.
|
|
*/
|
|
function get$1(dataPoint) {
|
|
if (dataPoint === null) {
|
|
// Give an empty string for rendering / sorting etc
|
|
return function (data) {
|
|
// type, row and meta also passed, but not used
|
|
return data;
|
|
};
|
|
}
|
|
else if (typeof dataPoint === 'function') {
|
|
return function (data, type, row, meta) {
|
|
return dataPoint(data, type, row, meta);
|
|
};
|
|
}
|
|
else if (typeof dataPoint === 'string' &&
|
|
(dataPoint.indexOf('.') !== -1 ||
|
|
dataPoint.indexOf('[') !== -1 ||
|
|
dataPoint.indexOf('(') !== -1)) {
|
|
/* If there is a . in the source string then the data source is in a
|
|
* nested object so we loop over the data for each level to get the next
|
|
* level down. On each loop we test for undefined, and if found immediately
|
|
* return. This allows entire objects to be missing and sDefaultContent to
|
|
* be used if defined, rather than throwing an error
|
|
*/
|
|
let fetchData = function (data, type, src) {
|
|
let arrayNotation, funcNotation, out, innerSrc;
|
|
if (src !== '') {
|
|
let a = splitObjNotation(src);
|
|
for (let i = 0, iLen = a.length; i < iLen; i++) {
|
|
// Check if we are dealing with special notation
|
|
arrayNotation = a[i].match(__reArray);
|
|
funcNotation = a[i].match(__reFn);
|
|
if (arrayNotation) {
|
|
// Array notation
|
|
a[i] = a[i].replace(__reArray, '');
|
|
// Condition allows simply [] to be passed in
|
|
if (a[i] !== '') {
|
|
data = data[a[i]];
|
|
}
|
|
out = [];
|
|
// Get the remainder of the nested object to get
|
|
a.splice(0, i + 1);
|
|
innerSrc = a.join('.');
|
|
// Traverse each entry in the array getting the properties requested
|
|
if (Array.isArray(data)) {
|
|
for (let j = 0, jLen = data.length; j < jLen; j++) {
|
|
out.push(fetchData(data[j], type, innerSrc));
|
|
}
|
|
}
|
|
// If a string is given in between the array notation indicators, that
|
|
// is used to join the strings together, otherwise an array is returned
|
|
let join = arrayNotation[0].substring(1, arrayNotation[0].length - 1);
|
|
data = join === '' ? out : out.join(join);
|
|
// The inner call to fetchData has already traversed through the remainder
|
|
// of the source requested, so we exit from the loop
|
|
break;
|
|
}
|
|
else if (funcNotation) {
|
|
// Function call
|
|
a[i] = a[i].replace(__reFn, '');
|
|
data = data[a[i]]();
|
|
continue;
|
|
}
|
|
if (data === null || data[a[i]] === null) {
|
|
return null;
|
|
}
|
|
else if (data === undefined || data[a[i]] === undefined) {
|
|
return undefined;
|
|
}
|
|
data = data[a[i]];
|
|
}
|
|
}
|
|
return data;
|
|
};
|
|
return function (data, type) {
|
|
// row and meta also passed, but not used
|
|
return fetchData(data, type, dataPoint);
|
|
};
|
|
}
|
|
else if (plainObject(dataPoint)) {
|
|
// Build an object of get functions, and wrap them in a single call
|
|
let o = {};
|
|
each(dataPoint, function (key, val) {
|
|
if (val) {
|
|
o[key] = get$1(val);
|
|
}
|
|
});
|
|
return function (data, type, row, meta) {
|
|
let t = o[type] || o._;
|
|
return t !== undefined ? t(data, type, row, meta) : data;
|
|
};
|
|
}
|
|
else {
|
|
// Array or flat object mapping
|
|
return function (data) {
|
|
// row and meta also passed, but not used
|
|
return data[dataPoint];
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Write a value into an existing data store
|
|
*
|
|
* @param dataPoint The data point to write to
|
|
*/
|
|
function set$1(dataPoint) {
|
|
if (dataPoint === null) {
|
|
// Nothing to do when the data source is null
|
|
return function () { };
|
|
}
|
|
else if (typeof dataPoint === 'function') {
|
|
return function (data, val, meta) {
|
|
dataPoint(data, 'set', val, meta);
|
|
};
|
|
}
|
|
else if (typeof dataPoint === 'string' &&
|
|
(dataPoint.indexOf('.') !== -1 ||
|
|
dataPoint.indexOf('[') !== -1 ||
|
|
dataPoint.indexOf('(') !== -1)) {
|
|
// Like the get, we need to get data from a nested object
|
|
let setData = function (data, val, src) {
|
|
let a = splitObjNotation(src), b;
|
|
let aLast = a[a.length - 1];
|
|
let arrayNotation, funcNotation, o, innerSrc;
|
|
for (let i = 0, iLen = a.length - 1; i < iLen; i++) {
|
|
// Protect against prototype pollution
|
|
if (a[i] === '__proto__' || a[i] === 'constructor') {
|
|
throw new Error('Cannot set prototype values');
|
|
}
|
|
// Check if we are dealing with an array notation request
|
|
arrayNotation = a[i].match(__reArray);
|
|
funcNotation = a[i].match(__reFn);
|
|
if (arrayNotation) {
|
|
a[i] = a[i].replace(__reArray, '');
|
|
data[a[i]] = [];
|
|
// Get the remainder of the nested object to set so we can recurse
|
|
b = a.slice();
|
|
b.splice(0, i + 1);
|
|
innerSrc = b.join('.');
|
|
// Traverse each entry in the array setting the properties requested
|
|
if (Array.isArray(val)) {
|
|
for (let j = 0, jLen = val.length; j < jLen; j++) {
|
|
o = {};
|
|
setData(o, val[j], innerSrc);
|
|
data[a[i]].push(o);
|
|
}
|
|
}
|
|
else {
|
|
// We've been asked to save data to an array, but it
|
|
// isn't array data to be saved. Best that can be done
|
|
// is to just save the value.
|
|
data[a[i]] = val;
|
|
}
|
|
// The inner call to setData has already traversed through the remainder
|
|
// of the source and has set the data, thus we can exit here
|
|
return;
|
|
}
|
|
else if (funcNotation) {
|
|
// Function call
|
|
a[i] = a[i].replace(__reFn, '');
|
|
data = data[a[i]](val);
|
|
}
|
|
// If the nested object doesn't currently exist - since we are
|
|
// trying to set the value - create it
|
|
if (data[a[i]] === null || data[a[i]] === undefined) {
|
|
data[a[i]] = {};
|
|
}
|
|
data = data[a[i]];
|
|
}
|
|
// Last item in the input - i.e, the actual set
|
|
if (aLast.match(__reFn)) {
|
|
// Function call
|
|
data = data[aLast.replace(__reFn, '')](val);
|
|
}
|
|
else {
|
|
// If array notation is used, we just want to strip it and use the property name
|
|
// and assign the value. If it isn't used, then we get the result we want anyway
|
|
data[aLast.replace(__reArray, '')] = val;
|
|
}
|
|
};
|
|
return function (data, val) {
|
|
// meta is also passed in, but not used
|
|
return setData(data, val, dataPoint);
|
|
};
|
|
}
|
|
else if (plainObject(dataPoint)) {
|
|
/* Unlike get, only the underscore (global) option is used for for
|
|
* setting data since we don't know the type here. This is why an object
|
|
* option is not documented for `mData` (which is read/write), but it is
|
|
* for `render` which is read only.
|
|
*/
|
|
return set$1(dataPoint._);
|
|
}
|
|
else {
|
|
// Array or flat object mapping
|
|
return function (data, val) {
|
|
// meta is also passed in, but not used
|
|
data[dataPoint] = val;
|
|
};
|
|
}
|
|
}
|
|
|
|
var data = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
get: get$1,
|
|
set: set$1
|
|
});
|
|
|
|
// Can be assigned in DateTable.use()
|
|
var __bootstrap;
|
|
var __foundation;
|
|
var __luxon$1;
|
|
var __moment$1;
|
|
var __dateTime;
|
|
var __dataTable;
|
|
var __jquery;
|
|
/**
|
|
* Set the libraries that DataTables uses, or the global objects.
|
|
* Note that the arguments can be either way around (legacy support)
|
|
* and the second is optional. See docs.
|
|
*/
|
|
function external (arg1, arg2) {
|
|
// Reverse arguments for legacy support
|
|
var module = typeof arg1 === 'string' ? arg2 : arg1;
|
|
var type = typeof arg2 === 'string' ? arg2 : arg1;
|
|
// Getter
|
|
if (module === undefined && typeof type === 'string') {
|
|
switch (type) {
|
|
case 'lib':
|
|
case 'jq':
|
|
return __jquery !== undefined ? __jquery : window.jQuery || null;
|
|
case 'win':
|
|
return window;
|
|
case 'datatable':
|
|
return __dataTable;
|
|
case 'datetime':
|
|
return __dateTime;
|
|
case 'luxon':
|
|
return __luxon$1 || window.luxon || null;
|
|
case 'moment':
|
|
return __moment$1 || window.moment || null;
|
|
case 'bootstrap':
|
|
// Use local if set, otherwise try window, which could be undefined
|
|
return __bootstrap || window.bootstrap || null;
|
|
case 'foundation':
|
|
// Ditto
|
|
return __foundation || window.Foundation || null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
// Setter
|
|
if (type === 'lib' ||
|
|
type === 'jq' ||
|
|
(module && module.fn && module.fn.jquery)) {
|
|
__jquery = module;
|
|
jQuerySetup();
|
|
}
|
|
else if (type === 'datatable' || (module && module.isDataTable)) {
|
|
__dataTable = module;
|
|
}
|
|
else if (type === 'win' || (module && module.document)) {
|
|
window = module;
|
|
document = module.document;
|
|
}
|
|
else if (type === 'datetime' || (module && module.type === 'DateTime')) {
|
|
__dateTime = module;
|
|
}
|
|
else if (type === 'luxon' || (module && module.FixedOffsetZone)) {
|
|
__luxon$1 = module;
|
|
}
|
|
else if (type === 'moment' || (module && module.isMoment)) {
|
|
__moment$1 = module;
|
|
}
|
|
else if (type === 'bootstrap' ||
|
|
(module && module.Modal && module.Modal.NAME === 'modal')) {
|
|
// This is currently for BS5 only. BS3/4 attach to jQuery, so no need to use `.use()`
|
|
__bootstrap = module;
|
|
}
|
|
else if (type === 'foundation' || (module && module.Reveal)) {
|
|
__foundation = module;
|
|
}
|
|
}
|
|
/**
|
|
* Attach jQuery to DataTables
|
|
*/
|
|
function jQuerySetup() {
|
|
if (!__dataTable || !__jquery) {
|
|
return;
|
|
}
|
|
// Provide access to the host jQuery object (circular reference)
|
|
__dataTable.$ = __jquery;
|
|
// jQuery integration - expose the core function.
|
|
__jquery.fn.dataTable = __dataTable;
|
|
// jQuery wrapper - returning a DataTable instance
|
|
__jquery.fn.DataTable = function (options) {
|
|
let table = new __dataTable(this.toArray(), options);
|
|
return table;
|
|
};
|
|
// Legacy aliases
|
|
__jquery.fn.dataTableSettings = __dataTable.ext.settings;
|
|
__jquery.fn.dataTableExt = __dataTable.ext;
|
|
// All properties that are available to $.fn.dataTable should also be available
|
|
// on $.fn.DataTable
|
|
each(__dataTable, function (prop, val) {
|
|
__jquery.fn.DataTable[prop] = val;
|
|
});
|
|
}
|
|
|
|
function debounce(fn, timeout = 250) {
|
|
let timer;
|
|
return function (...args) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
fn.call(this, ...args);
|
|
}, timeout);
|
|
};
|
|
}
|
|
function throttle(fn, freq = 200) {
|
|
let last, timer;
|
|
return function (...args) {
|
|
const now = +new Date();
|
|
if (last && now < last + freq) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
last = undefined;
|
|
fn.call(this, ...args);
|
|
}, freq);
|
|
}
|
|
else {
|
|
last = now;
|
|
fn.call(this, ...args);
|
|
}
|
|
};
|
|
}
|
|
|
|
var timer = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
debounce: debounce,
|
|
throttle: throttle
|
|
});
|
|
|
|
/**
|
|
* Provide a common method for plug-ins to check the version of DataTables being
|
|
* used, in order to ensure compatibility.
|
|
*
|
|
* @param version1 Version string to check for, in the format "X.Y.Z". Note that
|
|
* the formats "X" and "X.Y" are also acceptable.
|
|
* @param version2 As above, but optional. If not given the current DataTables
|
|
* version will be used.
|
|
* @returns true if this version of DataTables is greater or equal to the
|
|
* required version, or false if this version of DataTales is not suitable
|
|
*/
|
|
function check$1(version1, version2) {
|
|
let dt = external('datatable');
|
|
var parts1 = version2 ? version2.split('.') : dt.ext.version.split('.');
|
|
var parts2 = version1.split('.');
|
|
var int1, int2;
|
|
for (var i = 0, iLen = parts2.length; i < iLen; i++) {
|
|
int1 = parseInt(parts1[i], 10) || 0;
|
|
int2 = parseInt(parts2[i], 10) || 0;
|
|
// Parts are the same, keep comparing
|
|
if (int1 === int2) {
|
|
continue;
|
|
}
|
|
// Parts are different, return immediately
|
|
return int1 > int2;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
var version = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
check: check$1
|
|
});
|
|
|
|
// Note that the aliased properties are for compatibility with DataTables 2-
|
|
// which had a set of `util` functions.
|
|
var util = {
|
|
ajax,
|
|
array,
|
|
conv,
|
|
data,
|
|
/** @see timer.debounce */
|
|
debounce: debounce,
|
|
/** @see string.normalize */
|
|
diacritics: normalize,
|
|
/** @see string.escapeHtml */
|
|
escapeHtml: escapeHtml,
|
|
/** @see string.escapeRegex */
|
|
escapeRegex: escapeRegex,
|
|
external,
|
|
/** @see data.get */
|
|
get: get$1,
|
|
is,
|
|
object,
|
|
regex,
|
|
/** @see data.set */
|
|
set: set$1,
|
|
string,
|
|
/** @see string.stripHtml */
|
|
stripHtml: stripHtml,
|
|
/** @see timer.throttle */
|
|
throttle: throttle,
|
|
timer,
|
|
/** @see array.unique */
|
|
unique: unique,
|
|
version
|
|
};
|
|
|
|
/** Each element with an event attached needs a unique id */
|
|
let _uidCounter = 1;
|
|
/**
|
|
* All wrapped event handlers are stored in this array so we can refer back to
|
|
* them for removal. Each entry in the array is for a unique element, using the
|
|
* index to refer to it (the `uid` that is attached to the element).
|
|
*/
|
|
const _eventStore = [];
|
|
/**
|
|
* Get a unique id that can be assigned to an element.
|
|
*
|
|
* @returns UID
|
|
*/
|
|
function getUid(el) {
|
|
if (!el._event_uid) {
|
|
el._event_uid = _uidCounter++;
|
|
}
|
|
return el._event_uid;
|
|
}
|
|
/**
|
|
* Get all event handlers that have been assigned to an element
|
|
*
|
|
* @param el Element
|
|
* @returns Array of functions
|
|
*/
|
|
function get(el) {
|
|
let uid = el._event_uid;
|
|
if (!uid || !_eventStore[uid]) {
|
|
return null;
|
|
}
|
|
return _eventStore[uid];
|
|
}
|
|
/**
|
|
* Store an event handler for an element (does not apply it)
|
|
*
|
|
* @param el Element
|
|
* @param wrapper Function to set
|
|
*/
|
|
function set(el, wrapper) {
|
|
let uid = getUid(el);
|
|
if (_eventStore[uid] === undefined) {
|
|
_eventStore[uid] = [];
|
|
}
|
|
_eventStore[uid].push(wrapper);
|
|
}
|
|
/**
|
|
* Remove an event handler from an element's store
|
|
*
|
|
* @param el Element
|
|
* @param wrapper Function to set
|
|
* @returns void
|
|
*/
|
|
function remove$1(el, wrapper) {
|
|
let store = get(el);
|
|
if (!store) {
|
|
return;
|
|
}
|
|
let idx = store.indexOf(wrapper);
|
|
if (idx !== -1) {
|
|
store.splice(idx, 1);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We need an events library that has many of the features of jQuery's event
|
|
* handling - this is for backwards compatibility. Developers who have used
|
|
* jQuery to listen for events should not need to change their code!
|
|
*
|
|
* DataTables uses the following features of jQuery events, which need to be
|
|
* fully supported:
|
|
*
|
|
* * Namespaces
|
|
* * Custom events
|
|
* * Custom event object properties
|
|
* * Arguments for custom events
|
|
*
|
|
* Frustratingly while `dispatchEvent` will trigger all events listened to by
|
|
* jQuery (including namespaces with a shim of `jQuery.event.fix` to add the
|
|
* namespace and rnamespace properties), there is no way for `dispatchEvent` to
|
|
* pass arguments to custom event handlers.
|
|
*
|
|
* Also the inverse doesn't hold - using `addEventListener` followed by
|
|
* `$().trigger()` doesn't trigger the event handler. Therefore using both
|
|
* `dispatchEvent` and `$().trigger()` to fire off both event handlers would
|
|
* risk triggering some event handlers twice.
|
|
*
|
|
* Because of the backwards compatibility constraint and the complications given
|
|
* above, this library will act as a simple proxy to jQuery, if jQuery is
|
|
* present. If it is not, then it will implement the features described above
|
|
* itself. While this is not ideal (I'd prefer to have a way to trigger jQuery
|
|
* listened for events independently of triggering those added with this
|
|
* library, or any other `addEventListener` call), it does ensure backwards
|
|
* compatibility.
|
|
*/
|
|
const _mouseEvents = [
|
|
'click',
|
|
'dblclick',
|
|
'mousedown',
|
|
'mouseenter',
|
|
'mouseleave',
|
|
'mousemove',
|
|
'mouseout',
|
|
'mouseover',
|
|
'mouseup'
|
|
];
|
|
/**
|
|
* Add a property to an event object.
|
|
*
|
|
* @param event Event
|
|
* @param name Property name
|
|
* @param value Value to give the value
|
|
*/
|
|
function setEventProp(event, name, value) {
|
|
// Can't use do `event[name] =` - event object can't always have properties
|
|
// added like that.
|
|
Object.defineProperty(event, name, {
|
|
configurable: true,
|
|
get() {
|
|
return value;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Check that an element matches a given selector for a given event (ie its
|
|
* target)
|
|
*
|
|
* @param el Root element
|
|
* @param selector CSS selector
|
|
* @param event Event that
|
|
* @returns The matching element if there is one
|
|
*/
|
|
function delegateTarget(el, selector, event) {
|
|
// Its a delegate - get all elements that match the selector as
|
|
// descendants from the element the event was triggered on
|
|
let elements = Array.from(el.querySelectorAll(selector));
|
|
let target = event.target;
|
|
// The event might originate below our target, so need to climb the ladder
|
|
for (; target && target !== this; target = target.parentNode) {
|
|
// Needs to happen for all matched descendants
|
|
for (let element of elements) {
|
|
// And only call it when matched
|
|
if (element !== target) {
|
|
continue;
|
|
}
|
|
return target;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get the event name and namespaces from a string
|
|
*
|
|
* @param original event name and dot delimited namespaces
|
|
* @returns Object with split parts
|
|
*/
|
|
function parseEventName(original) {
|
|
if (!original) {
|
|
return {
|
|
eventName: null,
|
|
namespaces: []
|
|
};
|
|
}
|
|
let parts = original.split('.');
|
|
let name = parts.shift();
|
|
let isHover = false;
|
|
let isFocus = false;
|
|
// mouse[enter|leave] and focus|blur don't bubble, but do have counterparts
|
|
// which do, so we make use of them, as this allows event delegation on
|
|
// those event names to work as expected.
|
|
if (name === 'mouseenter') {
|
|
name = 'mouseover';
|
|
isHover = true;
|
|
}
|
|
else if (name === 'mouseleave') {
|
|
name = 'mouseout';
|
|
isHover = true;
|
|
}
|
|
else if (name === 'focus') {
|
|
name = 'focusin';
|
|
isFocus = true;
|
|
}
|
|
else if (name === 'blur') {
|
|
name = 'focusout';
|
|
isFocus = true;
|
|
}
|
|
else if (name === 'ready') {
|
|
name = 'DOMContentLoaded';
|
|
}
|
|
return {
|
|
eventName: name,
|
|
isFocus,
|
|
isHover,
|
|
namespaces: parts
|
|
};
|
|
}
|
|
/**
|
|
* Add an event listener to a function
|
|
*
|
|
* @param el The element to add an event handler to
|
|
* @param nameFull Event name. This can optionally be followed by a dot
|
|
* separated list of namespaces, a la jQuery. This allows for easy event
|
|
* removal and also matching triggering.
|
|
* @param handler Callback function to execute
|
|
* @param selector Delegate selector. `null` for non-delegate events
|
|
* @param one Indicate if the event handler should execute only once and then be
|
|
* removed.
|
|
*/
|
|
function add(el, nameFull, handler, selector, one) {
|
|
let jq = external('jq');
|
|
if (jq) {
|
|
let method = one ? 'one' : 'on';
|
|
if (selector) {
|
|
jq(el)[method](nameFull, selector, handler);
|
|
}
|
|
else {
|
|
jq(el)[method](nameFull, handler);
|
|
}
|
|
return;
|
|
}
|
|
// No jQuery - add the event ourselves
|
|
let { eventName, namespaces, isFocus, isHover } = parseEventName(nameFull);
|
|
if (!eventName) {
|
|
return;
|
|
}
|
|
// Special handling for the "ready" event - it will trigger when the content
|
|
// is ready, but also if it is already ready, when added.
|
|
if (el === document && eventName === 'DOMContentLoaded' && nameFull.includes('ready')) {
|
|
if (document.readyState === 'complete') {
|
|
handler(new Event('DOMContentLoaded'));
|
|
return;
|
|
}
|
|
}
|
|
// Create a function that will be the actual event handler, and performs any
|
|
// logic we need, such as delegate handling and adding properties.
|
|
let wrapped = function (event) {
|
|
let callScope = el; // Scope for the callback function
|
|
// If the event has a namespace (from a trigger), then the handler
|
|
// should only be run if there is at least one namespace being listened
|
|
// for that matches. This is an OR operation.
|
|
if (event.namespace &&
|
|
!intersection(namespaces, event.namespace.split('.')).length) {
|
|
return;
|
|
}
|
|
// If a special (overridden) type, then we need to check that they apply
|
|
if (!selector &&
|
|
((isFocus && event.target !== el) ||
|
|
(isHover &&
|
|
event.relatedTarget &&
|
|
el.contains(event.relatedTarget)))) {
|
|
return;
|
|
}
|
|
// For delegate events, determine if the target matches our selector
|
|
if (selector) {
|
|
let dTarget = delegateTarget(el, selector, event);
|
|
if (!dTarget) {
|
|
return;
|
|
}
|
|
if (isHover &&
|
|
event.relatedTarget &&
|
|
dTarget.contains(event.relatedTarget)) {
|
|
return;
|
|
}
|
|
callScope = dTarget;
|
|
}
|
|
// Set the properties that jQuery adds to the event object
|
|
setEventProp(event, 'currentTarget', callScope);
|
|
setEventProp(event, 'delegateTarget', el);
|
|
setEventProp(event, 'relatedTarget', event.relatedTarget);
|
|
// If it was triggered, extra data can be passed through using the
|
|
// arguments passed to trigger.
|
|
let retVal = handler.apply(callScope, [event, ...(event._args || [])]);
|
|
if (one) {
|
|
remove(el, eventName, handler, selector);
|
|
}
|
|
if (retVal === false) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
event.result = retVal;
|
|
};
|
|
wrapped.delegateSelector = selector;
|
|
wrapped.original = handler;
|
|
wrapped.one = one;
|
|
wrapped.type = eventName;
|
|
wrapped.namespaces = namespaces;
|
|
set(el, wrapped);
|
|
el.addEventListener(eventName, wrapped);
|
|
}
|
|
/**
|
|
* Remove an event from an element
|
|
*
|
|
* @param el The element to remove the event(s) from
|
|
* @param nameFull Event name and / or dot separated namespaces
|
|
* @param handler The function to remove (optional)
|
|
* @param selector Delegate selector (optional)
|
|
*/
|
|
function remove(el, nameFull, handler, selector) {
|
|
let jq = external('jq');
|
|
if (jq) {
|
|
if (selector) {
|
|
jq(el).off(nameFull, selector, handler);
|
|
}
|
|
else {
|
|
jq(el).off(nameFull, handler);
|
|
}
|
|
return;
|
|
}
|
|
// No jQuery - do it our way
|
|
let { eventName, namespaces } = parseEventName(nameFull);
|
|
let removeEvents = [];
|
|
let stored = get(el);
|
|
if (stored === null) {
|
|
return;
|
|
}
|
|
if (eventName && selector && handler) {
|
|
removeEvents = stored.filter(wrapped => wrapped.type === eventName &&
|
|
wrapped.delegateSelector === selector &&
|
|
wrapped.original === handler);
|
|
}
|
|
else if (eventName && selector) {
|
|
removeEvents = stored.filter(wrapped => wrapped.type === eventName &&
|
|
wrapped.delegateSelector === selector);
|
|
}
|
|
else if (eventName && handler) {
|
|
removeEvents = stored.filter(wrapped => wrapped.type === eventName && wrapped.original === handler);
|
|
}
|
|
else if (eventName) {
|
|
removeEvents = stored.filter(wrapped => wrapped.type === eventName);
|
|
}
|
|
else {
|
|
// No name, use all events
|
|
removeEvents = stored;
|
|
}
|
|
// If namespaces were given then we need to filter down to just those event
|
|
// handlers which have the given namespaces
|
|
if (namespaces.length) {
|
|
removeEvents = removeEvents.filter(
|
|
// The event needs to match all of the namespaces given in order to
|
|
// be removed - this matches jQuery's behaviour. The event could
|
|
// have other namespaces. Do this by filtering to just the filtering
|
|
// namespaces and check that the length matches
|
|
ev => ev.namespaces.filter(ns => namespaces.includes(ns)).length ===
|
|
namespaces.length);
|
|
}
|
|
removeEvents.forEach(wrapped => {
|
|
remove$1(el, wrapped);
|
|
el.removeEventListener(wrapped.type, wrapped);
|
|
});
|
|
}
|
|
/**
|
|
* Trigger an event on an element. Can have extra data given, which is useful
|
|
* for custom events.
|
|
*
|
|
* @param el Element to trigger the event on
|
|
* @param nameFull Event name with optional dot separated namespaces
|
|
* @param bubbles If the event should bubble up through the DOM or not
|
|
* @param args Array of arguments to pass to the event handler
|
|
* @param eventProps Object of extra parameters to attach to the event object
|
|
* @param returnEvent Indicate if the return should be the event object (for
|
|
* further processing) or the default prevented state.
|
|
* @returns `true` if default was NOT prevented, `false` if default was
|
|
* prevented. If `returnEvent` is `true` then the return will be the event
|
|
* object.
|
|
*/
|
|
function trigger(el, nameFull, bubbles = false, args = [], eventProps = null, returnEvent = false) {
|
|
let jq = external('jq');
|
|
if (jq) {
|
|
let method = bubbles ? 'trigger' : 'triggerHandler';
|
|
let ev = jq.Event(nameFull);
|
|
each(eventProps, (key, val) => {
|
|
setEventProp(ev, key, val);
|
|
});
|
|
jq(el)[method](ev, args || []);
|
|
if (returnEvent) {
|
|
ev.defaultPrevented = ev.isDefaultPrevented();
|
|
return ev;
|
|
}
|
|
// See note below regarding the inversion
|
|
return !ev.isDefaultPrevented();
|
|
}
|
|
// No jQuery
|
|
let { eventName, namespaces } = parseEventName(nameFull);
|
|
if (!eventName) {
|
|
return false;
|
|
}
|
|
let isMouseEvent = _mouseEvents.includes(eventName.toLowerCase());
|
|
let event = isMouseEvent
|
|
? new MouseEvent(eventName, { bubbles, cancelable: true })
|
|
: new Event(eventName, { bubbles, cancelable: true });
|
|
// Set the extra properties for the event
|
|
setEventProp(event, 'namespace', namespaces.join('.'));
|
|
setEventProp(event, '_args', args || []);
|
|
each(eventProps, (key, val) => setEventProp(event, key, val));
|
|
el.dispatchEvent(event);
|
|
// A lot of the old DataTables stuff checks for a `false` return to prevent
|
|
// the default action. To maintain compatibility we return an inverted
|
|
// `defaultPrevented` here - i.e. it becomes `do default`.
|
|
return returnEvent ? event : !event.defaultPrevented;
|
|
}
|
|
|
|
// Window level functions
|
|
var win = {
|
|
/**
|
|
* Get the height of the window, excluding a horizontal scrollbar if it is
|
|
* present.
|
|
*
|
|
* @returns Height in pixels
|
|
*/
|
|
height() {
|
|
var _a;
|
|
return ((_a = document.querySelector('html')) === null || _a === void 0 ? void 0 : _a.clientHeight) || 0;
|
|
},
|
|
/**
|
|
* Remove an event handler from the window
|
|
*
|
|
* @param name Event name (can include or just be a namespace)
|
|
* @param cb Event callback function
|
|
*/
|
|
off(name, cb = null) {
|
|
remove(window, name, cb, null);
|
|
},
|
|
/**
|
|
* Add an event handler to the window
|
|
*
|
|
* @param name Event name (can include a namespace)
|
|
* @param cb Event callback function
|
|
*/
|
|
on(name, cb) {
|
|
add(window, name, cb, null, false);
|
|
},
|
|
/**
|
|
* Add an event handler to the window that will execute just once
|
|
*
|
|
* @param name Event name (can include a namespace)
|
|
* @param cb Event callback function
|
|
*/
|
|
one(name, cb) {
|
|
add(window, name, cb, null, true);
|
|
},
|
|
/**
|
|
* Get the left scroll offset of the window / document
|
|
*
|
|
* @param set Set the scroll position
|
|
* @returns Window X scroll offset in pixels
|
|
*/
|
|
scrollLeft(set) {
|
|
if (set !== undefined) {
|
|
window.scrollX = set;
|
|
}
|
|
return window.scrollX;
|
|
},
|
|
/**
|
|
* Get the top scroll offset of the window / document
|
|
*
|
|
* @param set Set the scroll position
|
|
* @returns Window Y scroll offset in pixels
|
|
*/
|
|
scrollTop(set) {
|
|
if (set !== undefined) {
|
|
window.scrollY = set;
|
|
}
|
|
return window.scrollY;
|
|
},
|
|
/**
|
|
* Get the width of the window, excluding a vertical scrollbar if it is
|
|
* present.
|
|
*
|
|
* @returns Width in pixels
|
|
*/
|
|
width() {
|
|
var _a;
|
|
return ((_a = document.querySelector('html')) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;
|
|
}
|
|
};
|
|
|
|
function create$3(name) {
|
|
let el = document.createElement(name);
|
|
return new Dom(el);
|
|
}
|
|
function select(selector) {
|
|
return new Dom(selector);
|
|
}
|
|
/**
|
|
* `Dom` is a class that provides a chaining UI for simple DOM manipulation and
|
|
* selection.
|
|
*/
|
|
class Dom {
|
|
/**
|
|
* `Dom` is used for selection and manipulation of the DOM elements in a
|
|
* chaining interface.
|
|
*
|
|
* @param selector
|
|
*/
|
|
constructor(selector) {
|
|
/** Number of elements in the array */
|
|
this.length = 0;
|
|
/** Flag to indicate that this is a Dom instance */
|
|
this._isDom = true;
|
|
if (selector) {
|
|
this.add(selector);
|
|
}
|
|
}
|
|
/**
|
|
* Add an element (or multiple) to the instance. Will ensure uniqueness.
|
|
*
|
|
* @param selector Element(s) to add
|
|
* @param sort Indicate if the element should be added in document order.
|
|
* @returns Self for chaining
|
|
*/
|
|
add(selector, sort = true) {
|
|
if (selector) {
|
|
if (typeof selector === 'string') {
|
|
let elements = Array.from(document.querySelectorAll(selector));
|
|
addArray(this, elements);
|
|
}
|
|
else if (selector instanceof Dom) {
|
|
addArray(this, selector.get());
|
|
}
|
|
else if (typeof selector === 'object' &&
|
|
!selector.nodeName && // <select> has a length!
|
|
selector.length !== undefined) {
|
|
// Array-like - could be a jQuery instance or DataTables API
|
|
// instance
|
|
let arrayLike = selector;
|
|
for (let i = 0; i < arrayLike.length; i++) {
|
|
addArray(this, arrayLike[i]);
|
|
}
|
|
sort = false;
|
|
}
|
|
else {
|
|
addArray(this, selector);
|
|
}
|
|
}
|
|
if (sort) {
|
|
this.sort();
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Insert the given content to each item in the result set.
|
|
*
|
|
* Limit your result set to a single item!
|
|
*
|
|
* @param content The content to append
|
|
* @returns Self for chaining
|
|
*/
|
|
append(content) {
|
|
if (!content) {
|
|
return this;
|
|
}
|
|
if (!arrayLike(content)) {
|
|
content = [content];
|
|
}
|
|
// Generate a flat array of the content to be added, with nulls removed
|
|
// this means it will be an array of nodes and / or strings
|
|
let flatContent = flatten([], content).filter(c => !!c);
|
|
/// Got a string somewhere in it, so need to use insertAdjacentHTML
|
|
if (flatContent.find(val => typeof val === 'string')) {
|
|
return this.each(el => {
|
|
for (let i = 0; i < flatContent.length; i++) {
|
|
if (typeof flatContent[i] === 'string') {
|
|
el.insertAdjacentHTML('beforeend', flatContent[i]);
|
|
}
|
|
else {
|
|
el.append(flatContent[i]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// Otherwise we can use a document fragment for a single mutation
|
|
// on the main document
|
|
return this.each(el => {
|
|
let fragment = new DocumentFragment();
|
|
for (let i = 0; i < flatContent.length; i++) {
|
|
fragment.append(flatContent[i]);
|
|
}
|
|
el.append(fragment);
|
|
});
|
|
}
|
|
/**
|
|
* Append the current data set items to the element from the selector
|
|
*
|
|
* @param selector
|
|
*/
|
|
appendTo(selector) {
|
|
let inst = selector instanceof Dom ? selector : new Dom(selector);
|
|
inst.append(this);
|
|
return this;
|
|
}
|
|
attr(name, value) {
|
|
if (typeof name === 'string' && value === undefined) {
|
|
return this.count() ? this[0].getAttribute(name) : null;
|
|
}
|
|
return this.each(el => {
|
|
if (typeof name === 'string') {
|
|
if (value !== undefined && value !== null) {
|
|
el.setAttribute(name, typeof value === 'string' ? value : value.toString());
|
|
}
|
|
}
|
|
else {
|
|
each(name, (key, val) => {
|
|
if (val !== undefined && val !== null) {
|
|
el.setAttribute(key, val);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Remove an attribute on each element in the result set
|
|
*
|
|
* @param attr Attribute to remove
|
|
* @returns Self for chaining
|
|
*/
|
|
attrRemove(attr) {
|
|
return this.each(el => el.removeAttribute(attr));
|
|
}
|
|
/**
|
|
* Blur on the target elements
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
blur() {
|
|
return this.each(el => el.blur());
|
|
}
|
|
/**
|
|
* Get the child from all elements in the result set
|
|
*
|
|
* @param selector Query string that the child much match to be selected
|
|
* @returns New Dom instance with children as the result set
|
|
*/
|
|
children(selector) {
|
|
return this.map(el => {
|
|
let children = Array.from(el.children);
|
|
return selector
|
|
? children.filter(child => child.matches(selector))
|
|
: children;
|
|
});
|
|
}
|
|
/**
|
|
* Add one or more class names to the result set
|
|
*
|
|
* @param name Class name(s) to set
|
|
* @returns Self for chaining
|
|
*/
|
|
classAdd(name) {
|
|
if (!name) {
|
|
return this;
|
|
}
|
|
let names = stringArrays(name);
|
|
return this.each(el => {
|
|
names.filter(n => n).forEach(n => el.classList.add(n));
|
|
});
|
|
}
|
|
/**
|
|
* Check if the first element in the result set has the given class
|
|
*
|
|
* @param name Class name to check for
|
|
* @returns Self for chaining
|
|
*/
|
|
classHas(name) {
|
|
return this.count() ? this[0].classList.contains(name) : false;
|
|
}
|
|
/**
|
|
* Remove the given class(s) from all elements in the result set
|
|
*
|
|
* @param name Class name to remove
|
|
* @returns Self for chaining
|
|
*/
|
|
classRemove(name) {
|
|
if (!name) {
|
|
return this;
|
|
}
|
|
let names = stringArrays(name);
|
|
return this.each(el => {
|
|
names.filter(n => n).forEach(n => el.classList.remove(n));
|
|
});
|
|
}
|
|
/**
|
|
* Toggle a class on all elements in the result set
|
|
*
|
|
* @param name Class name(s) to toggle - space separated
|
|
* @param toggle Toggle on or off
|
|
* @returns Self for chaining
|
|
*/
|
|
classToggle(name, toggle) {
|
|
let names = Array.isArray(name) ? name : name.split(' ');
|
|
return this.each(el => {
|
|
names.filter(n => n).forEach(n => el.classList.toggle(n, toggle));
|
|
});
|
|
}
|
|
/**
|
|
* Clone the nodes in the result set and return a new instance
|
|
*
|
|
* @param deep Include the subtree (`true`) or not (`false` - default)
|
|
* @returns New Dom instance with new elements
|
|
*/
|
|
clone(deep = false) {
|
|
return this.map(el => el.cloneNode(deep));
|
|
}
|
|
/**
|
|
* Find the closest ancestor for each element in the result set
|
|
*
|
|
* @param selector
|
|
* @returns New Dom instance when the matching ancestors
|
|
*/
|
|
closest(selector) {
|
|
if (typeof selector === 'string') {
|
|
return this.map(el => el.closest(selector));
|
|
}
|
|
return this.map(el => {
|
|
// Traverse up the tree seeing if the element matches
|
|
while (el.parentElement) {
|
|
if (el.parentElement === selector) {
|
|
return selector;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
// Nothing found
|
|
return null;
|
|
});
|
|
}
|
|
/**
|
|
* Determine if the result set contains the element specified. Shorthand for
|
|
* .find().count()
|
|
*
|
|
* @param input Element / selector to look for
|
|
* @returns true if it does contain, false otherwise
|
|
*/
|
|
contains(input) {
|
|
return this.find(input).count() !== 0;
|
|
}
|
|
/**
|
|
* Get the number of elements in the current result set
|
|
*
|
|
* @returns Number of elements
|
|
*/
|
|
count() {
|
|
return this.length;
|
|
}
|
|
css(rule, value) {
|
|
// String getter
|
|
if (typeof rule === 'string' && value === undefined) {
|
|
return this.length ? getComputedStyle(this[0])[rule] : null;
|
|
}
|
|
return this.each(el => {
|
|
if (typeof rule === 'string') {
|
|
// String setter
|
|
el.style[rule] = value;
|
|
}
|
|
else {
|
|
// Object set of rules
|
|
Object.assign(el.style, rule);
|
|
}
|
|
});
|
|
}
|
|
data(name, value) {
|
|
if (!name) {
|
|
let out = {};
|
|
if (!this.count()) {
|
|
return out;
|
|
}
|
|
util.object.each(this[0].dataset, (key, val) => {
|
|
out[key] = dataConvert(val);
|
|
});
|
|
return out;
|
|
}
|
|
if (typeof name === 'string' && value === undefined) {
|
|
return this.length ? dataConvert(this[0].dataset[name]) : null;
|
|
}
|
|
if (typeof name === 'string') {
|
|
this.each(el => (el.dataset[name] = JSON.stringify(value)));
|
|
}
|
|
else {
|
|
each(name, (key, val) => {
|
|
this.each(el => (el.dataset[key] = JSON.stringify(val)));
|
|
});
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Remove the elements in the result set from the document. Does not remove
|
|
* event listeners.
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
detach() {
|
|
return this.each(el => el.remove());
|
|
}
|
|
/**
|
|
* Remove the child elements from each element in the result set from the
|
|
* document. Does not remove event listeners.
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
detachChildren() {
|
|
return this.each(el => {
|
|
el.replaceChildren();
|
|
});
|
|
}
|
|
/**
|
|
* Iterate over each item in the result set and perform an action
|
|
*
|
|
* @param callback Callback function
|
|
* @returns Self for chaining
|
|
*/
|
|
each(callback) {
|
|
for (let i = 0; i < this.length; i++) {
|
|
let el = this[i];
|
|
callback.call(el, el, i);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Inverse iteration over each item in the result set and perform an action
|
|
*
|
|
* @param callback Callback function
|
|
* @returns Self for chaining
|
|
*/
|
|
eachReverse(callback) {
|
|
for (let i = this.length - 1; i >= 0; i--) {
|
|
let el = this[i];
|
|
callback.call(el, el, i);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Remove all children
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
empty() {
|
|
// TODO should remove event listeners
|
|
return this.each(el => {
|
|
var _a;
|
|
if (el.replaceChildren) {
|
|
el.replaceChildren();
|
|
}
|
|
else {
|
|
while (el.childNodes.length) {
|
|
(_a = el.firstChild) === null || _a === void 0 ? void 0 : _a.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Get a new Dom instance with just a specific element from the result set
|
|
*
|
|
* @param idx The element to use
|
|
* @returns New Dom instance
|
|
*/
|
|
eq(idx) {
|
|
return idx < this.count() ? new Dom(this.get(idx)) : new Dom();
|
|
}
|
|
get(idx) {
|
|
return idx !== undefined ? this[idx] : Array.from(this);
|
|
}
|
|
/**
|
|
* Call focus on the target elements
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
focus() {
|
|
return this.each(el => el.focus());
|
|
}
|
|
/**
|
|
* Reduce the result set based on a given filter, which can be a CSS
|
|
* selector, an element or array of elements.
|
|
*
|
|
* @param filter Optional selector or function that the result set element
|
|
* would need to match to be selected.
|
|
* @returns New Dom instance containing the filters elements
|
|
*/
|
|
filter(filter) {
|
|
return this.map(el => {
|
|
if (filter === undefined) {
|
|
return el;
|
|
}
|
|
if (typeof filter === 'function') {
|
|
return filter(el) ? el : null;
|
|
}
|
|
// Direct match - allows an element to be given as the filter
|
|
if (typeof filter !== 'string') {
|
|
if (arrayLike(filter)) {
|
|
return Array.from(filter).includes(el) ? el : null;
|
|
}
|
|
return filter === el ? el : null;
|
|
}
|
|
// CSS selector
|
|
if (!el.matches(filter)) {
|
|
return null;
|
|
}
|
|
// If there is a pseudo child selector, want to check that the
|
|
// element is actually in the document, if not, then
|
|
// `:first-child` (etc) will match detached elements, which is
|
|
// not desirable.
|
|
if (!el.parentNode &&
|
|
(filter.match(/:\w+-child/) || filter.match(/:\w+-of-type/))) {
|
|
return null;
|
|
}
|
|
return el;
|
|
});
|
|
}
|
|
/**
|
|
* Get all matching descendants
|
|
*
|
|
* @param input Elements to find
|
|
* @returns A new Dom instance with all matching elements
|
|
*/
|
|
find(input) {
|
|
if (input === null) {
|
|
return new Dom();
|
|
}
|
|
// Text based selector - loop over each element in the result set, doing
|
|
// the search on each and adding to a new instance.
|
|
if (typeof input === 'string') {
|
|
return this.map(el => Array.from(el.querySelectorAll(input)));
|
|
}
|
|
let selector = input instanceof Dom ? input.get() : input;
|
|
// Otherwise its an element, that we need to see if one of the elements
|
|
// in the result set is a parent of the given target
|
|
let hasParent = false;
|
|
this.each(el => {
|
|
if (new Dom(selector).closest(el).count()) {
|
|
hasParent = true;
|
|
}
|
|
});
|
|
return new Dom(hasParent ? selector : []);
|
|
}
|
|
/**
|
|
* Get the last element in the result set
|
|
*
|
|
* @returns New instance with just the selected item
|
|
*/
|
|
first() {
|
|
return new Dom(this.length ? this[0] : null);
|
|
}
|
|
height(include) {
|
|
if (!this.count()) {
|
|
return 0;
|
|
}
|
|
if (include === undefined ||
|
|
include === 'withPadding' ||
|
|
include === 'withBorder' ||
|
|
include === 'withMargin' ||
|
|
include === 'inner' ||
|
|
include === 'outer') {
|
|
let el = this[0];
|
|
let computed = window.getComputedStyle(this[0]);
|
|
let rectHeight = el.getBoundingClientRect().height;
|
|
if (!include || include === 'content') {
|
|
// Content. Minus scrollbar if there is one. This is basically
|
|
// clientHeight minus padding, but that isn't fractional, so use
|
|
// the bounding rect.
|
|
let barHeight = el.offsetHeight -
|
|
parseFloat(computed.borderTop) -
|
|
parseFloat(computed.borderBottom) -
|
|
el.clientHeight;
|
|
return (rectHeight -
|
|
parseFloat(computed.paddingTop) -
|
|
parseFloat(computed.paddingBottom) -
|
|
parseFloat(computed.borderTop) -
|
|
parseFloat(computed.borderBottom) -
|
|
barHeight);
|
|
}
|
|
else if (include === 'withPadding' || include === 'inner') {
|
|
return (rectHeight -
|
|
parseFloat(computed.borderTop) -
|
|
parseFloat(computed.borderBottom));
|
|
}
|
|
else if (include === 'withBorder') {
|
|
return rectHeight;
|
|
}
|
|
else {
|
|
// withMargin
|
|
return (rectHeight +
|
|
parseFloat(computed.marginTop) +
|
|
parseFloat(computed.marginBottom));
|
|
}
|
|
}
|
|
else {
|
|
// Setter
|
|
return this.each(el => (el.style.height =
|
|
typeof include === 'string' ? include : include + 'px'));
|
|
}
|
|
}
|
|
/**
|
|
* Hide an element by setting it to `display: none`
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
hide() {
|
|
return this.each(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
}
|
|
html(data) {
|
|
if (data !== undefined) {
|
|
return this.each(el => {
|
|
el.innerHTML = data;
|
|
});
|
|
}
|
|
else {
|
|
return this.count() ? this[0].innerHTML : null;
|
|
}
|
|
}
|
|
/**
|
|
* Boolean return check on if an item in the result set matches the selector
|
|
* given. Only one need match.
|
|
*
|
|
* @param selector Selector to match against
|
|
* @returns Boolean true if there is a match
|
|
*/
|
|
is(selector) {
|
|
return this.filter(selector).count() > 0;
|
|
}
|
|
/**
|
|
* Determine if the first element in the result set is in the document or
|
|
* not
|
|
*
|
|
* @returns true if is, false if detached
|
|
*/
|
|
isAttached() {
|
|
if (this.count() === 0) {
|
|
return false;
|
|
}
|
|
return document.body.contains(this[0]);
|
|
}
|
|
/**
|
|
* Determine if the first element in the result set is visible or not.
|
|
*
|
|
* @returns Visibility flag
|
|
*/
|
|
isVisible() {
|
|
if (this.count() === 0) {
|
|
return false;
|
|
}
|
|
let el = this[0];
|
|
return !!(el.offsetWidth ||
|
|
el.offsetHeight ||
|
|
el.getClientRects().length);
|
|
}
|
|
/**
|
|
* Get the index of an element from among its siblings
|
|
*
|
|
* @returns Element index
|
|
*/
|
|
index() {
|
|
if (this.count()) {
|
|
let el = this[0];
|
|
return Array.from(el.parentNode.children).indexOf(el);
|
|
}
|
|
return -1;
|
|
}
|
|
/**
|
|
* Insert each element in the result set after a target node
|
|
*
|
|
* @param target Element after which the insert should happen
|
|
* @returns Self for chaining
|
|
*/
|
|
insertAfter(target) {
|
|
let nodes = elementArray(target);
|
|
return this.eachReverse(el => {
|
|
nodes.forEach(n => { var _a; return (_a = n === null || n === void 0 ? void 0 : n.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(el, n.nextSibling); });
|
|
});
|
|
}
|
|
/**
|
|
* Insert each element in the result set before a target node
|
|
*
|
|
* @param target Element before which the insert should happen
|
|
* @returns Self for chaining
|
|
*/
|
|
insertBefore(target) {
|
|
let nodes = elementArray(target);
|
|
return this.each(el => {
|
|
nodes.forEach(n => { var _a; return (_a = n === null || n === void 0 ? void 0 : n.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(el, n); });
|
|
});
|
|
}
|
|
/**
|
|
* Get the last element in the result set
|
|
*
|
|
* @returns New instance with just the selected item
|
|
*/
|
|
last() {
|
|
let s = this;
|
|
return new Dom(s.length ? s[s.length - 1] : null);
|
|
}
|
|
/**
|
|
* Create a new Dom instance based on the results from a callback function
|
|
* which is executed per element in the result set.
|
|
*
|
|
* @param fn Function to get the elements to add to the new instance
|
|
* @returns New Dom instance with the results from the callback
|
|
*/
|
|
map(fn) {
|
|
let next = new Dom();
|
|
this.each(el => {
|
|
// Don't reorder the items
|
|
next.add(fn(el), false);
|
|
});
|
|
return next;
|
|
}
|
|
/**
|
|
* Create an array of any data type based on a function returning a value
|
|
* from each element in the result set.
|
|
*
|
|
* @param fn Mapping function
|
|
* @returns Array of returned objects.
|
|
*/
|
|
mapTo(fn) {
|
|
let result = [];
|
|
this.each((el, idx) => result.push(fn(el, idx)));
|
|
return result;
|
|
}
|
|
off(arg1, arg2, arg3) {
|
|
let { handler, names, selector } = normaliseEventParams(arg1, arg2, arg3);
|
|
return this.each(el => {
|
|
names.forEach(name => {
|
|
remove(el, name, handler, selector);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Get the offset of the first element in the result set. The offset is the
|
|
* coordinates of the element relative to the document.
|
|
*
|
|
* @returns Object with top and left offset
|
|
*/
|
|
offset() {
|
|
if (!this.count()) {
|
|
return {
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
}
|
|
let box = this[0].getBoundingClientRect();
|
|
let docElem = document.documentElement;
|
|
return {
|
|
top: box.top + window.pageYOffset - docElem.clientTop,
|
|
left: box.left + window.pageXOffset - docElem.clientLeft
|
|
};
|
|
}
|
|
/**
|
|
* Get the offset parents of the elements in the result set.
|
|
*
|
|
* Departure from jQuery - it won't go up to `html`
|
|
*
|
|
* @returns Instance with the result set as the offset parents
|
|
*/
|
|
offsetParent() {
|
|
return this.map(el => el.offsetParent || document.body);
|
|
}
|
|
on(arg1, arg2, arg3) {
|
|
let { handler, names, selector } = normaliseEventParams(arg1, arg2, arg3);
|
|
return this.each(el => {
|
|
names
|
|
.filter(n => n !== null)
|
|
.forEach(name => {
|
|
add(el, name, handler, selector, false);
|
|
});
|
|
});
|
|
}
|
|
one(arg1, arg2, arg3) {
|
|
let { handler, names, selector } = normaliseEventParams(arg1, arg2, arg3);
|
|
return this.each(el => {
|
|
names
|
|
.filter(n => n !== null)
|
|
.forEach(name => {
|
|
add(el, name, handler, selector, true);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Get the parent element for each element in the result set
|
|
*
|
|
* @param filter Optional selector that the parent element would need to
|
|
* match to be selected.
|
|
* @returns New Dom instance containing the parent elements
|
|
*/
|
|
parent(filter) {
|
|
return this.map(el => {
|
|
let parent = el.parentElement;
|
|
if (filter) {
|
|
return (parent === null || parent === void 0 ? void 0 : parent.matches(filter)) ? parent : null;
|
|
}
|
|
return parent;
|
|
});
|
|
}
|
|
/**
|
|
* Get the position of the first element in the result set. The position is
|
|
* the coordinates relative to the offset parent.
|
|
*
|
|
* @returns Object with top and left position coordinates
|
|
*/
|
|
position() {
|
|
if (!this.count()) {
|
|
return {
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
}
|
|
let el = this[0];
|
|
let { marginTop, marginLeft } = getComputedStyle(el);
|
|
return {
|
|
top: el.offsetTop - parseInt(marginTop),
|
|
left: el.offsetLeft - parseInt(marginLeft)
|
|
};
|
|
}
|
|
/**
|
|
* Prepend the given content to each item in the result set.
|
|
*
|
|
* You should limit your result set to a single item!
|
|
*
|
|
* @param content Item(s) to prepend
|
|
* @returns Self for chaining
|
|
*/
|
|
prepend(content) {
|
|
return this.each(el => {
|
|
if (content instanceof Dom) {
|
|
// Reverse the array, so if there are multiple elements, they
|
|
// end up being added sequentially, just like jQuery
|
|
let itemsReversed = Array.from(content).reverse();
|
|
itemsReversed.forEach(item => el.prepend(item));
|
|
}
|
|
else if (typeof content === 'string') {
|
|
el.insertAdjacentHTML('afterbegin', content);
|
|
}
|
|
else {
|
|
el.prepend(content);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Append the current data set items to the element from the selector
|
|
*
|
|
* @param selector Select item to insert result sets into
|
|
* @returns Self for chaining
|
|
*/
|
|
prependTo(selector) {
|
|
if (selector instanceof Dom) {
|
|
selector.prepend(this);
|
|
}
|
|
else {
|
|
new Dom(selector).prepend(this);
|
|
}
|
|
return this;
|
|
}
|
|
prop(name, value) {
|
|
if (typeof name === 'string' && value === undefined) {
|
|
return this.count() ? this[0][name] : null;
|
|
}
|
|
return this.each(el => {
|
|
el[name] = value;
|
|
});
|
|
}
|
|
/**
|
|
* Remove a property from all elements in the result set
|
|
*
|
|
* @param name Property name to remove
|
|
* @returns Self for chaining
|
|
*/
|
|
propRemove(name) {
|
|
return this.each(el => {
|
|
delete el[name];
|
|
});
|
|
}
|
|
/**
|
|
* Removed all nodes in the result set from the document
|
|
*
|
|
* @returns Self for chaining
|
|
*/
|
|
remove() {
|
|
// TODO this should remove event listeners
|
|
return this.each(el => el.remove());
|
|
}
|
|
/**
|
|
* Replace the elements in the result set with those given.
|
|
*
|
|
* @param replacer Element(s) to insert in place of the originals
|
|
* @returns Self
|
|
*/
|
|
replaceWith(replacer) {
|
|
return this.each(el => {
|
|
if (replacer instanceof Dom) {
|
|
el.replaceWith(...replacer.get());
|
|
}
|
|
else {
|
|
el.replaceWith(replacer);
|
|
}
|
|
});
|
|
}
|
|
scrollLeft(val) {
|
|
if (val === undefined) {
|
|
return this.count() ? this[0].scrollLeft : 0;
|
|
}
|
|
return this.each(el => (el.scrollLeft = val));
|
|
}
|
|
scrollTop(val) {
|
|
if (val === undefined) {
|
|
return this.count() ? this[0].scrollTop : 0;
|
|
}
|
|
return this.each(el => (el.scrollTop = val));
|
|
}
|
|
/**
|
|
* Get the siblings of all elements in the result set
|
|
*
|
|
* @returns New Dom instance containing the sibling elements
|
|
*/
|
|
siblings() {
|
|
return this.map(el => {
|
|
return el.parentElement
|
|
? Array.from(el.parentElement.children).filter(child => child !== el)
|
|
: [];
|
|
});
|
|
}
|
|
/**
|
|
* Set the elements in the result set to display as blocks
|
|
*
|
|
* @returns Self for chaining
|
|
* @todo Could be smarter with hide, since some elements might have been a
|
|
* grid or flex before being hidden.
|
|
*/
|
|
show() {
|
|
return this.each(el => {
|
|
el.style.display = 'block';
|
|
});
|
|
}
|
|
/**
|
|
* Sort the DOM elements into document order.
|
|
*
|
|
* This is normally not needed as elements selected with a DOM selector are
|
|
* automatically sorted in document order. However, in the case of elements
|
|
* being added as an array, their order will be retained. In such as case
|
|
* you might wish to sort them in document order.
|
|
*/
|
|
sort() {
|
|
Array.prototype.sort.call(this, documentOrder);
|
|
return this;
|
|
}
|
|
text(txt) {
|
|
if (txt === undefined) {
|
|
return this.count() ? this[0].textContent : null;
|
|
}
|
|
return this.each(el => {
|
|
el.textContent = txt;
|
|
});
|
|
}
|
|
/**
|
|
* Perform a CSS transition - i.e. an animation. Note this isn't nearly as
|
|
* comprehensive as an animation library, nor is it meant to be. It is for
|
|
* simple transitions such as fading in only.
|
|
*
|
|
* To set up something like a fade in, do `dom.css({opacity:
|
|
* 0}).transition({opacity: 1})`.
|
|
*
|
|
* @param css CSS properties to transition
|
|
* @param duration Transition duration
|
|
* @param ease CSS easing function name
|
|
* @param cb Callback function
|
|
* @returns Self for chaining
|
|
*/
|
|
transition(css, duration, ease, cb) {
|
|
if (!this.count()) {
|
|
return this;
|
|
}
|
|
if (!duration && duration !== 0) {
|
|
duration = 400;
|
|
}
|
|
if (!ease) {
|
|
ease = '';
|
|
}
|
|
if (!cb) {
|
|
cb = () => { };
|
|
}
|
|
if (Dom.transitions && duration !== 0) {
|
|
let first = this[0];
|
|
// If there was an existing transition, cancel its callback
|
|
if (first._dom_tra) {
|
|
clearTimeout(first._dom_tra);
|
|
delete first._dom_tra;
|
|
}
|
|
setTimeout(() => {
|
|
this.css('transition', 'all ' + duration + 'ms ' + ease);
|
|
this.css(css);
|
|
}, 0);
|
|
first._dom_tra = setTimeout(() => {
|
|
delete first._dom_tra;
|
|
this.css('transition', '');
|
|
cb.call(this);
|
|
}, duration);
|
|
}
|
|
else {
|
|
this.css(css);
|
|
cb.call(this);
|
|
}
|
|
return this;
|
|
}
|
|
trigger(name, bubbles = true, args = null, props = null, returnEvent = false) {
|
|
let { names } = normaliseEventParams(name);
|
|
let ret = [];
|
|
this.each(el => {
|
|
names
|
|
.filter(n => n !== null)
|
|
.forEach(name => {
|
|
ret.push(trigger(el, name, bubbles, args, props, returnEvent));
|
|
});
|
|
});
|
|
return ret;
|
|
}
|
|
val(value) {
|
|
if (value === undefined) {
|
|
// Getter
|
|
if (!this.count()) {
|
|
return null;
|
|
}
|
|
let el = this[0];
|
|
if (el.options && el.multiple) {
|
|
return Array.from(el.options)
|
|
.filter(opt => opt.selected)
|
|
.map(opt => opt.value);
|
|
}
|
|
return el.value;
|
|
}
|
|
// Setter
|
|
return this.each((el) => {
|
|
if (el.options && el.multiple) {
|
|
let valArr = Array.isArray(value) ? value : [value];
|
|
Array.from(el.options).forEach(opt => (opt.selected = valArr.includes(opt.value)));
|
|
}
|
|
else {
|
|
// This works for select elements as well in modern browsers
|
|
el.value = value;
|
|
}
|
|
});
|
|
}
|
|
width(include) {
|
|
if (!this.count()) {
|
|
return 0;
|
|
}
|
|
if (include === undefined ||
|
|
include === 'withPadding' ||
|
|
include === 'withBorder' ||
|
|
include === 'withMargin' ||
|
|
include === 'inner' ||
|
|
include === 'outer') {
|
|
let el = this[0];
|
|
let computed = window.getComputedStyle(el);
|
|
let rectWidth = el.getBoundingClientRect().width;
|
|
if (!include || include === 'content') {
|
|
// Content. Minus scrollbar if there is one. This is basically
|
|
// clientWidth minus padding, but that isn't fractional, so use
|
|
// the bounding rect.
|
|
let barWidth = el.offsetWidth -
|
|
parseFloat(computed.borderLeft) -
|
|
parseFloat(computed.borderRight) -
|
|
el.clientWidth;
|
|
return (rectWidth -
|
|
parseFloat(computed.paddingLeft) -
|
|
parseFloat(computed.paddingRight) -
|
|
parseFloat(computed.borderLeft) -
|
|
parseFloat(computed.borderRight) -
|
|
barWidth);
|
|
}
|
|
else if (include === 'withPadding' || include === 'inner') {
|
|
return (rectWidth -
|
|
parseFloat(computed.borderLeft) -
|
|
parseFloat(computed.borderRight));
|
|
}
|
|
else if (include === 'withBorder') {
|
|
return rectWidth;
|
|
}
|
|
else {
|
|
// withMargin
|
|
return (rectWidth +
|
|
parseFloat(computed.marginLeft) +
|
|
parseFloat(computed.marginRight));
|
|
}
|
|
}
|
|
else {
|
|
// Setter
|
|
return this.each(el => (el.style.width =
|
|
typeof include === 'string' ? include : include + 'px'));
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Create a new element and wrap in a `Dom` instance (alias of `create`)
|
|
*
|
|
* @param name Element name to create
|
|
* @returns Dom instance for manipulating the new element
|
|
*/
|
|
Dom.c = create$3;
|
|
/**
|
|
* Create a new element and wrap in a `Dom` instance (alias of `c`)
|
|
*
|
|
* @param name Element name to create
|
|
* @returns Dom instance for manipulating the new element
|
|
*/
|
|
Dom.create = create$3;
|
|
/**
|
|
* Select items from the document and wrap in a `Dom` instance (alias of
|
|
* `select`)
|
|
*
|
|
* @param selector Items to select
|
|
* @returns Dom instance for manipulating the selected items
|
|
*/
|
|
Dom.s = select;
|
|
/**
|
|
* Select items from the document and wrap in a `Dom` instance (alias of
|
|
* `s`)
|
|
*
|
|
* @param selector Items to select
|
|
* @returns Dom instance for manipulating the selected items
|
|
*/
|
|
Dom.select = select;
|
|
/**
|
|
* Flag to indicate if transitions (animations) should be allowed. Set to
|
|
* false to disable and have it jump to the end.
|
|
*/
|
|
Dom.transitions = true;
|
|
/**
|
|
* Window object methods
|
|
*/
|
|
Dom.w = win;
|
|
// Aliases for jQuery-likeness. Not exposed via Typescript, but that might
|
|
// change.
|
|
Dom.prototype.addClass = Dom.prototype.classAdd;
|
|
Dom.prototype.hasClass = Dom.prototype.classHas;
|
|
Dom.prototype.removeClass = Dom.prototype.classRemove;
|
|
/**
|
|
* Convert a data value into a typed value
|
|
*
|
|
* @param val Data to convert
|
|
* @returns Converted value
|
|
*/
|
|
function dataConvert(val) {
|
|
if (val === undefined) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(val);
|
|
}
|
|
catch (e) {
|
|
return val;
|
|
}
|
|
}
|
|
function normaliseEventParams(name, arg2, arg3) {
|
|
let selector;
|
|
let handler;
|
|
let names = name ? name.split(' ').map(str => str.trim()) : [null];
|
|
if (typeof arg2 === 'string') {
|
|
selector = arg2;
|
|
handler = arg3;
|
|
}
|
|
else {
|
|
selector = null;
|
|
handler = arg2;
|
|
}
|
|
return {
|
|
handler,
|
|
names,
|
|
selector
|
|
};
|
|
}
|
|
function documentOrder(a, b) {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
let position = a.compareDocumentPosition(b);
|
|
if (position & Node.DOCUMENT_POSITION_DISCONNECTED) {
|
|
// One is disconnected - find which
|
|
if (document.body.contains(a)) {
|
|
return -1;
|
|
}
|
|
else if (document.body.contains(b)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
else if (position & Node.DOCUMENT_POSITION_FOLLOWING ||
|
|
position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
|
return -1;
|
|
}
|
|
else if (position & Node.DOCUMENT_POSITION_PRECEDING ||
|
|
position & Node.DOCUMENT_POSITION_CONTAINS) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
function elementArray(target) {
|
|
return dom(target)
|
|
? target.get()
|
|
: Array.isArray(target)
|
|
? target
|
|
: [target];
|
|
}
|
|
function addArray(store, el) {
|
|
if (util.is.arrayLike(el)) {
|
|
for (var i = 0; i < el.length; i++) {
|
|
let e = el[i];
|
|
if (e !== null && e !== undefined) {
|
|
store[store.length] = e;
|
|
store.length++;
|
|
}
|
|
}
|
|
}
|
|
else if (el !== null && el !== undefined) {
|
|
store[store.length] = el;
|
|
store.length++;
|
|
}
|
|
}
|
|
function stringArrays(name) {
|
|
let names = [];
|
|
let add = function (n) {
|
|
names.push.apply(names, n.split(' '));
|
|
};
|
|
if (Array.isArray(name)) {
|
|
name.forEach(n => add(n));
|
|
}
|
|
else {
|
|
add(name);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
const features = {};
|
|
const legacy = [];
|
|
/**
|
|
* Create a new feature that can be used for layout
|
|
*
|
|
* @param name The name of the new feature.
|
|
* @param cb A function that will create the elements and event listeners for
|
|
* the feature being added.
|
|
* @param legacyChar
|
|
*/
|
|
function register$2(name, cb, legacyChar = '') {
|
|
features[name] = cb;
|
|
if (legacyChar) {
|
|
legacy.push({
|
|
cFeature: legacyChar,
|
|
fnInit: cb
|
|
});
|
|
}
|
|
}
|
|
|
|
var classes$1 = {
|
|
container: 'dt-container',
|
|
empty: {
|
|
row: 'dt-empty'
|
|
},
|
|
info: {
|
|
container: 'dt-info'
|
|
},
|
|
layout: {
|
|
row: 'dt-layout-row',
|
|
cell: 'dt-layout-cell',
|
|
tableRow: 'dt-layout-table',
|
|
tableCell: '',
|
|
start: 'dt-layout-start',
|
|
end: 'dt-layout-end',
|
|
full: 'dt-layout-full'
|
|
},
|
|
length: {
|
|
container: 'dt-length',
|
|
select: 'dt-input'
|
|
},
|
|
order: {
|
|
canAsc: 'dt-orderable-asc',
|
|
canDesc: 'dt-orderable-desc',
|
|
isAsc: 'dt-ordering-asc',
|
|
isDesc: 'dt-ordering-desc',
|
|
none: 'dt-orderable-none',
|
|
position: 'sorting_'
|
|
},
|
|
processing: {
|
|
container: 'dt-processing'
|
|
},
|
|
scrolling: {
|
|
body: 'dt-scroll-body',
|
|
container: 'dt-scroll',
|
|
footer: {
|
|
self: 'dt-scroll-foot',
|
|
inner: 'dt-scroll-footInner'
|
|
},
|
|
header: {
|
|
self: 'dt-scroll-head',
|
|
inner: 'dt-scroll-headInner'
|
|
}
|
|
},
|
|
search: {
|
|
container: 'dt-search',
|
|
input: 'dt-input'
|
|
},
|
|
table: 'dataTable',
|
|
tbody: {
|
|
cell: '',
|
|
row: ''
|
|
},
|
|
thead: {
|
|
cell: '',
|
|
row: ''
|
|
},
|
|
tfoot: {
|
|
cell: '',
|
|
row: ''
|
|
},
|
|
paging: {
|
|
active: 'current',
|
|
button: 'dt-paging-button',
|
|
container: 'dt-paging',
|
|
disabled: 'disabled',
|
|
nav: ''
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Compute what number buttons to show in the paging control
|
|
*
|
|
* @param page Current page
|
|
* @param pages Total number of pages
|
|
* @param buttons Target number of number buttons
|
|
* @param addFirstLast Indicate if page 1 and end should be included
|
|
* @returns Buttons to show
|
|
*/
|
|
function pagingNumbers(page, pages, buttons, addFirstLast) {
|
|
let numbers = [], half = Math.floor(buttons / 2), before = addFirstLast ? 2 : 1, after = addFirstLast ? 1 : 0;
|
|
if (pages <= buttons) {
|
|
numbers = range(0, pages);
|
|
}
|
|
else if (buttons === 1) {
|
|
// Single button - current page only
|
|
numbers = [page];
|
|
}
|
|
else if (buttons === 3) {
|
|
// Special logic for just three buttons
|
|
if (page <= 1) {
|
|
numbers = [0, 1, 'ellipsis'];
|
|
}
|
|
else if (page >= pages - 2) {
|
|
numbers = range(pages - 2, pages);
|
|
numbers.unshift('ellipsis');
|
|
}
|
|
else {
|
|
numbers = ['ellipsis', page, 'ellipsis'];
|
|
}
|
|
}
|
|
else if (page <= half) {
|
|
numbers = range(0, buttons - before);
|
|
numbers.push('ellipsis');
|
|
if (addFirstLast) {
|
|
numbers.push(pages - 1);
|
|
}
|
|
}
|
|
else if (page >= pages - 1 - half) {
|
|
numbers = range(pages - (buttons - before), pages);
|
|
numbers.unshift('ellipsis');
|
|
if (addFirstLast) {
|
|
numbers.unshift(0);
|
|
}
|
|
}
|
|
else {
|
|
numbers = range(page - half + before, page + half - after);
|
|
numbers.push('ellipsis');
|
|
numbers.unshift('ellipsis');
|
|
if (addFirstLast) {
|
|
numbers.push(pages - 1);
|
|
numbers.unshift(0);
|
|
}
|
|
}
|
|
return numbers;
|
|
}
|
|
var pager = {
|
|
simple: function () {
|
|
return ['previous', 'next'];
|
|
},
|
|
full: function () {
|
|
return ['first', 'previous', 'next', 'last'];
|
|
},
|
|
numbers: function () {
|
|
return ['numbers'];
|
|
},
|
|
simple_numbers: function () {
|
|
return ['previous', 'numbers', 'next'];
|
|
},
|
|
full_numbers: function () {
|
|
return ['first', 'previous', 'numbers', 'next', 'last'];
|
|
},
|
|
first_last: function () {
|
|
return ['first', 'last'];
|
|
},
|
|
first_last_numbers: function () {
|
|
return ['first', 'numbers', 'last'];
|
|
},
|
|
// For testing and plug-ins to use
|
|
_numbers: pagingNumbers,
|
|
// Number of number buttons - legacy, use `numbers` option for paging feature
|
|
numbers_length: 7
|
|
};
|
|
|
|
const defaults$4 = {
|
|
addedClasses: [],
|
|
cells: [],
|
|
data: [],
|
|
details: undefined,
|
|
detailsShow: undefined,
|
|
displayData: null,
|
|
idx: -1,
|
|
orderCache: null,
|
|
searchCellCache: null,
|
|
searchRowCache: null,
|
|
src: 'dom',
|
|
tr: null
|
|
};
|
|
/**
|
|
* Create a new object that is a row model
|
|
*
|
|
* @param parts Values to assign, otherwise the defaults will be used
|
|
* @returns New object
|
|
*/
|
|
function create$2(parts = {}) {
|
|
return util.object.assignDeep({}, defaults$4, parts);
|
|
}
|
|
|
|
/**
|
|
* Add a data array to the table, creating DOM node etc. This is the parallel to
|
|
* gatherData, but for adding rows from a JavaScript source, rather than a
|
|
* DOM source.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param dataIn data array to be added
|
|
* @param tr TR element to add to the table - optional. If not given, DataTables
|
|
* will create a row automatically
|
|
* @param tds Array of TD|TH elements for the row - must be given if tr is.
|
|
* @returns >=0 if successful (index of new data entry), -1 if failed
|
|
*/
|
|
function addData(settings, dataIn, tr, tds) {
|
|
/* Create the object for storing information about this new row */
|
|
var rowIdx = settings.data.length;
|
|
var row = create$2({
|
|
src: tr ? 'dom' : 'data',
|
|
idx: rowIdx
|
|
});
|
|
row.data = dataIn;
|
|
settings.data.push(row);
|
|
var columns = settings.columns;
|
|
for (var i = 0, iLen = columns.length; i < iLen; i++) {
|
|
// Invalidate the column types as the new data needs to be revalidated
|
|
columns[i].type = null;
|
|
}
|
|
/* Add to the display array */
|
|
settings.displayMaster.push(rowIdx);
|
|
var id = settings.rowIdFn(dataIn);
|
|
if (id !== undefined) {
|
|
settings.ids[id] = row;
|
|
}
|
|
/* Create the DOM information, or register it if already present */
|
|
if (tr || !settings.features.deferRender) {
|
|
createTr(settings, rowIdx, tr, tds);
|
|
}
|
|
return rowIdx;
|
|
}
|
|
/**
|
|
* Add one or more TR elements to the table. Generally we'd expect to
|
|
* use this for reading data from a DOM sourced table, but it could be
|
|
* used for an TR element. Note that if a TR is given, it is used (i.e.
|
|
* it is not cloned).
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rows The TR element(s) to add to the table
|
|
* @returns Array of indexes for the added rows
|
|
*/
|
|
function addTr(settings, rows) {
|
|
return rows.mapTo(el => {
|
|
let row = getRowElementsFromNode(settings, el);
|
|
return addData(settings, row.data, el, row.cells);
|
|
});
|
|
}
|
|
/**
|
|
* Get the data for a given cell from the internal cache, taking into account
|
|
* data mapping
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rowIdx data row id
|
|
* @param colIdx Column index
|
|
* @param type data get type ('display', 'type' 'filter|search' 'sort|order')
|
|
* @returns Cell data
|
|
*/
|
|
function getCellData(settings, rowIdx, colIdx, type) {
|
|
if (type === 'search') {
|
|
type = 'filter';
|
|
}
|
|
else if (type === 'order') {
|
|
type = 'sort';
|
|
}
|
|
var row = settings.data[rowIdx];
|
|
if (!row) {
|
|
return undefined;
|
|
}
|
|
var draw = settings.drawCount;
|
|
var col = settings.columns[colIdx];
|
|
var rowData = row.data;
|
|
var defaultContent = col.defaultContent;
|
|
var cellData = col.dataGet(rowData, type, {
|
|
settings: settings,
|
|
row: rowIdx,
|
|
col: colIdx
|
|
});
|
|
// Allow for a node being returned for non-display types
|
|
if (type !== 'display' &&
|
|
cellData &&
|
|
typeof cellData === 'object' &&
|
|
cellData.nodeName) {
|
|
cellData = cellData.innerHTML;
|
|
}
|
|
if (cellData === undefined) {
|
|
if (settings.drawError != draw && defaultContent === null) {
|
|
log(settings, 0, 'Requested unknown parameter ' +
|
|
(typeof col.data == 'function'
|
|
? '{function}'
|
|
: "'" + col.data + "'") +
|
|
' for row ' +
|
|
rowIdx +
|
|
', column ' +
|
|
colIdx, 4);
|
|
settings.drawError = draw;
|
|
}
|
|
return defaultContent;
|
|
}
|
|
// When the data source is null and a specific data type is requested (i.e.
|
|
// not the original data), we can use default column data
|
|
if ((cellData === rowData || cellData === null) &&
|
|
defaultContent !== null &&
|
|
type !== undefined) {
|
|
cellData = defaultContent;
|
|
}
|
|
else if (typeof cellData === 'function') {
|
|
// If the data source is a function, then we run it and use the return,
|
|
// executing in the scope of the data object (for instances)
|
|
return cellData.call(rowData);
|
|
}
|
|
if (cellData === null && type === 'display') {
|
|
return '';
|
|
}
|
|
if (type === 'filter') {
|
|
var formatters = ext.type.search;
|
|
if (col.type && formatters[col.type]) {
|
|
cellData = formatters[col.type](cellData);
|
|
}
|
|
}
|
|
return cellData;
|
|
}
|
|
/**
|
|
* Set the value for a specific cell, into the internal data cache
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rowIdx data row id
|
|
* @param colIdx Column index
|
|
* @param val Value to set
|
|
*/
|
|
function setCellData(settings, rowIdx, colIdx, val) {
|
|
let row = settings.data[rowIdx];
|
|
if (row) {
|
|
let col = settings.columns[colIdx];
|
|
let rowData = row.data;
|
|
col.dataSet(rowData, val, {
|
|
settings: settings,
|
|
row: rowIdx,
|
|
col: colIdx
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Write a value to a cell
|
|
*
|
|
* @param td Cell
|
|
* @param val Value
|
|
*/
|
|
function writeCell(td, val) {
|
|
let cell = Dom.s(td);
|
|
if (val && typeof val === 'object' && val.nodeName) {
|
|
cell.empty().append(val);
|
|
}
|
|
else {
|
|
cell.html(val);
|
|
}
|
|
}
|
|
/**
|
|
* Return an array with the full table data
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns array {array} aData Master data array
|
|
*/
|
|
function getDataMaster(settings) {
|
|
return util.array.pluck(settings.data, 'data');
|
|
}
|
|
/**
|
|
* Nuke the table
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function clearTable(settings) {
|
|
settings.data.length = 0;
|
|
settings.displayMaster.length = 0;
|
|
settings.display.length = 0;
|
|
settings.ids = {};
|
|
}
|
|
/**
|
|
* Mark cached data as invalid such that a re-read of the data will occur when
|
|
* the cached data is next requested. Also update from the data source object.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rowIdx Row index to invalidate
|
|
* @param src Source to invalidate from: undefined, 'auto', 'dom' or 'data'
|
|
* @param colIdx Column index to invalidate. If undefined the whole row will be
|
|
* invalidated
|
|
*/
|
|
function invalidateRow(settings, rowIdx, src, colIdx) {
|
|
var row = settings.data[rowIdx];
|
|
var i, iLen;
|
|
if (!row) {
|
|
return;
|
|
}
|
|
// Remove the cached data for the row
|
|
row.orderCache = null;
|
|
row.searchCellCache = null;
|
|
row.displayData = null;
|
|
// Are we reading last data from DOM or the data object?
|
|
if (src === 'dom' || ((!src || src === 'auto') && row.src === 'dom')) {
|
|
// Read the data from the DOM
|
|
row.data = getRowElementsFromModel(settings, row, colIdx).data;
|
|
}
|
|
else {
|
|
// Reading from data object, update the DOM
|
|
var cells = row.cells;
|
|
var display = getRowDisplay(settings, rowIdx);
|
|
if (cells.length) {
|
|
if (colIdx !== undefined) {
|
|
writeCell(cells[colIdx], display[colIdx]);
|
|
}
|
|
else {
|
|
for (i = 0, iLen = cells.length; i < iLen; i++) {
|
|
writeCell(cells[i], display[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
invalidColumn(settings, colIdx);
|
|
// Update DataTables special `DT_*` attributes for the row
|
|
rowAttributes(settings, row);
|
|
callbackFire(settings, null, 'rowInvalidate', [settings, rowIdx, colIdx], false);
|
|
}
|
|
/**
|
|
* Column specific invalidation
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param colIdx Column index to invalidate, or all columns if not given
|
|
*/
|
|
function invalidColumn(settings, colIdx) {
|
|
// Column specific invalidation
|
|
var cols = settings.columns;
|
|
if (colIdx !== undefined) {
|
|
// Type - the data might have changed
|
|
cols[colIdx].type = null;
|
|
// Max length string. Its a fairly cheep recalculation, so not worth
|
|
// something more complicated
|
|
cols[colIdx].wideStrings = null;
|
|
}
|
|
else {
|
|
for (let i = 0, iLen = cols.length; i < iLen; i++) {
|
|
cols[i].type = null;
|
|
cols[i].wideStrings = null;
|
|
}
|
|
}
|
|
settings.containerWidth = -1;
|
|
}
|
|
/**
|
|
* Get the cells and data for a given row - from a <tr> element
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param row TR element from which to read data or existing row object from
|
|
* which to re-read the data from the cells
|
|
*/
|
|
function getRowElementsFromNode(settings, row) {
|
|
let data = settings.rowReadObject ? {} : [];
|
|
let cells = Dom.s(row).children('th, td');
|
|
let id = row.getAttribute('id');
|
|
cells.each((el, idx) => {
|
|
readCellData(settings, el, data, idx);
|
|
});
|
|
if (id) {
|
|
util.set(settings.rowId)(data, id);
|
|
}
|
|
return {
|
|
data: data,
|
|
cells: cells.get()
|
|
};
|
|
}
|
|
/**
|
|
* Get the cells and data for a given row - from an existing row model
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param row Existing row object from which to re-read the data from the cells
|
|
* @param colIdx Optional column index
|
|
*/
|
|
function getRowElementsFromModel(settings, row, colIdx) {
|
|
let tds = row.cells;
|
|
for (let i = 0; i < tds.length; i++) {
|
|
if (colIdx === undefined || colIdx === i) {
|
|
readCellData(settings, tds[i], row.data, i);
|
|
}
|
|
}
|
|
// Read the ID from the DOM if present
|
|
if (row.tr) {
|
|
let id = row.tr.getAttribute('id');
|
|
if (id) {
|
|
util.set(settings.rowId)(row.data, id);
|
|
}
|
|
}
|
|
return {
|
|
data: row.data,
|
|
cells: tds
|
|
};
|
|
}
|
|
/**
|
|
* Read data from a cell into the data source object
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param cell The HTML cell element to read from
|
|
* @param data Data object / array to store data into
|
|
* @param colIdx The column index for the cell
|
|
*/
|
|
function readCellData(settings, cell, data, colIdx) {
|
|
let column = settings.columns[colIdx];
|
|
let contents = cell.innerHTML.trim();
|
|
if (column.attrSrc) {
|
|
// If we are working with attributes from the cell as values
|
|
let dataPoint = column.data;
|
|
let setter = util.set(dataPoint._);
|
|
let attr = function (str, cell) {
|
|
if (typeof str === 'string') {
|
|
let idx = str.indexOf('@');
|
|
if (idx !== -1) {
|
|
let att = str.substring(idx + 1);
|
|
let setter = util.set(str);
|
|
setter(data, cell.getAttribute(att));
|
|
}
|
|
}
|
|
};
|
|
setter(data, contents);
|
|
attr(dataPoint.sort, cell);
|
|
attr(dataPoint.type, cell);
|
|
attr(dataPoint.filter, cell);
|
|
}
|
|
else {
|
|
if (!column.setter) {
|
|
// Cache the setter function
|
|
column.setter = util.set(column.data);
|
|
}
|
|
column.setter(data, contents);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the node required for the processing node
|
|
*
|
|
* @param ctx DataTables settings object
|
|
*/
|
|
function processingHtml(ctx) {
|
|
var table = ctx.table;
|
|
var scrolling = ctx.scroll.x !== '' || ctx.scroll.y !== '';
|
|
if (ctx.features.processing) {
|
|
var n = Dom
|
|
.c('div')
|
|
.attr('id', ctx.tableId + '_processing')
|
|
.attr('role', 'status')
|
|
.classAdd(ctx.classes.processing.container)
|
|
.html(ctx.language.processing)
|
|
.append(Dom
|
|
.c('div')
|
|
.append(Dom.c('div'))
|
|
.append(Dom.c('div'))
|
|
.append(Dom.c('div'))
|
|
.append(Dom.c('div')));
|
|
// Different positioning depending on if scrolling is enabled or not
|
|
if (scrolling) {
|
|
n.prependTo(Dom.s(ctx.tableWrapper).find('div.dt-scroll').get(0));
|
|
}
|
|
else {
|
|
n.insertBefore(table);
|
|
}
|
|
Dom.s(table).on('processing.dt.DT', (e, s, show) => {
|
|
n.css('display', show ? 'block' : 'none');
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Display or hide the processing indicator
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param show Show the processing indicator (true) or not (false)
|
|
*/
|
|
function processingDisplay(ctx, show) {
|
|
// Ignore cases when we are still redrawing
|
|
if (ctx.doingDraw && show === false) {
|
|
return;
|
|
}
|
|
callbackFire(ctx, null, 'processing', [ctx, show]);
|
|
}
|
|
/**
|
|
* Show the processing element if an action takes longer than a given time
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param enable Do (true) or not (false) async processing (local feature enablement)
|
|
* @param run Function to run
|
|
*/
|
|
function processingRun(ctx, enable, run) {
|
|
if (!enable) {
|
|
// Immediate execution, synchronous
|
|
run();
|
|
}
|
|
else {
|
|
processingDisplay(ctx, true);
|
|
// Allow the processing display to show if needed
|
|
setTimeout(function () {
|
|
run();
|
|
processingDisplay(ctx, false);
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function renderer(ctx, type) {
|
|
var render = ctx.renderer;
|
|
var host = ext.renderer[type];
|
|
if (plainObject(render) && render[type]) {
|
|
// Specific renderer for this type. If available use it, otherwise use
|
|
// the default.
|
|
return host[render[type]] || host._;
|
|
}
|
|
else if (typeof render === 'string') {
|
|
// Common renderer - if there is one available for this type use it,
|
|
// otherwise use the default
|
|
return host[render] || host._;
|
|
}
|
|
// Use the default
|
|
return host._;
|
|
}
|
|
|
|
/**
|
|
* Recalculate the column widths, if needed (by a column having been
|
|
* invalidated)
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function columnWidths(settings) {
|
|
if (settings.columns.map(c => c.wideStrings).includes(null)) {
|
|
calculateColumnWidths(settings);
|
|
}
|
|
}
|
|
/**
|
|
* Calculate the width of columns for the table
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function calculateColumnWidths(settings) {
|
|
// Not interested in doing column width calculation if auto-width is disabled
|
|
if (!settings.features.autoWidth) {
|
|
return;
|
|
}
|
|
var table = settings.table, columns = settings.columns, scroll = settings.scroll, scrollY = scroll.y, scrollX = scroll.x, visibleColumns = getColumns(settings, 'visible'), tableWidthAttr = table.getAttribute('width'), // from DOM element
|
|
tableContainer = table.parentElement, i, j, column, columnIdx;
|
|
var styleWidth = table.style.width;
|
|
var containerWidth = wrapperWidth(settings);
|
|
// Don't re-run for the same width as the last time
|
|
if (containerWidth === settings.containerWidth) {
|
|
return false;
|
|
}
|
|
settings.containerWidth = containerWidth;
|
|
// If there is no width applied as a CSS style or as an attribute, we assume that
|
|
// the width is intended to be 100%, which is usually is in CSS, but it is very
|
|
// difficult to correctly parse the rules to get the final result.
|
|
if (!styleWidth && !tableWidthAttr) {
|
|
table.style.width = '100%';
|
|
styleWidth = '100%';
|
|
}
|
|
if (styleWidth && styleWidth.indexOf('%') !== -1) {
|
|
tableWidthAttr = styleWidth;
|
|
}
|
|
// Let plug-ins know that we are doing a recalc, in case they have changed any of the
|
|
// visible columns their own way (e.g. Responsive uses display:none).
|
|
callbackFire(settings, null, 'column-calc', [{ visible: visibleColumns }], false);
|
|
// Construct a worst case table with the widest, assign any user defined
|
|
// widths, then insert it into the DOM and allow the browser to do all
|
|
// the hard work of calculating table widths
|
|
var tmpTable = Dom
|
|
.s(table.cloneNode())
|
|
.css('visibility', 'hidden')
|
|
.css('margin', '0')
|
|
.attrRemove('id');
|
|
// Clean up the table body
|
|
tmpTable.append(Dom.c('tbody'));
|
|
// Clone the table header and footer - we can't use the header / footer
|
|
// from the cloned table, since if scrolling is active, the table's
|
|
// real header and footer are contained in different table tags
|
|
tmpTable
|
|
.append(settings.thead.cloneNode(true))
|
|
.append(settings.tfoot.cloneNode(true));
|
|
// Remove any assigned widths from the footer (from scrolling)
|
|
tmpTable.find('tfoot th, tfoot td').css('width', '');
|
|
// Apply custom sizing to the cloned header
|
|
tmpTable.find('thead th, thead td').each(cell => {
|
|
// Get the `width` from the header layout
|
|
var width = columnsSumWidth(settings, cell, true);
|
|
if (width) {
|
|
cell.style.width = width;
|
|
// For scrollX we need to force the column width otherwise the
|
|
// browser will collapse it. If this width is smaller than the
|
|
// width the column requires, then it will have no effect
|
|
if (scrollX) {
|
|
cell.style.minWidth = width;
|
|
Dom.s(cell).append(Dom.c('div').css({
|
|
width: width,
|
|
margin: '0',
|
|
padding: '0',
|
|
border: '0',
|
|
height: '1px'
|
|
}));
|
|
}
|
|
}
|
|
else {
|
|
cell.style.width = '';
|
|
}
|
|
});
|
|
// Get the widest strings for each of the visible columns and add them to
|
|
// our table to create a "worst case"
|
|
var longestData = [];
|
|
for (i = 0; i < visibleColumns.length; i++) {
|
|
longestData.push(getWideStrings(settings, visibleColumns[i]));
|
|
}
|
|
if (longestData.length) {
|
|
for (i = 0; i < longestData[0].length; i++) {
|
|
var tr = Dom.c('tr').appendTo(tmpTable.find('tbody'));
|
|
for (j = 0; j < visibleColumns.length; j++) {
|
|
columnIdx = visibleColumns[j];
|
|
column = columns[columnIdx];
|
|
var longest = longestData[j][i] || '';
|
|
var autoClass = ext.type.className[column.type];
|
|
var padding = column.contentPadding || (scrollX ? '-' : '');
|
|
var text = longest + padding;
|
|
var cell = Dom
|
|
.c('td')
|
|
.classAdd(autoClass)
|
|
.classAdd(column.className)
|
|
.appendTo(tr);
|
|
if (longest.indexOf('<') === -1 &&
|
|
longest.indexOf('&') === -1) {
|
|
cell.text(text);
|
|
}
|
|
else {
|
|
cell.html(text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Tidy the temporary table - remove name attributes so there aren't
|
|
// duplicated in the dom (radio elements for example)
|
|
tmpTable.find('[name]').attrRemove('name');
|
|
// Table has been built, attach to the document so we can work with it.
|
|
// A holding element is used, positioned at the top of the container
|
|
// with minimal height, so it has no effect on if the container scrolls
|
|
// or not. Otherwise it might trigger scrolling when it actually isn't
|
|
// needed
|
|
var holder = Dom
|
|
.c('div')
|
|
.css(scrollX || scrollY
|
|
? {
|
|
position: 'absolute',
|
|
top: '0',
|
|
left: '0',
|
|
height: '1px',
|
|
right: '0',
|
|
overflow: 'hidden'
|
|
}
|
|
: {})
|
|
.append(tmpTable)
|
|
.appendTo(tableContainer);
|
|
// When scrolling (X or Y) we want to set the width of the table as
|
|
// appropriate. However, when not scrolling leave the table width as it
|
|
// is. This results in slightly different, but I think correct behaviour
|
|
if (scrollX) {
|
|
tmpTable.css('width', 'auto').attrRemove('width');
|
|
// If there is no width attribute or style, then allow the table to
|
|
// collapse
|
|
if (tmpTable.width() < tableContainer.clientWidth && tableWidthAttr) {
|
|
tmpTable.width(tableContainer.clientWidth);
|
|
}
|
|
}
|
|
else if (scrollY) {
|
|
tmpTable.width(tableContainer.clientWidth);
|
|
}
|
|
else if (tableWidthAttr) {
|
|
tmpTable.width(tableWidthAttr);
|
|
}
|
|
// Get the width of each column in the constructed table
|
|
var total = 0;
|
|
var bodyCells = tmpTable.find('tbody tr').eq(0).children();
|
|
for (i = 0; i < visibleColumns.length; i++) {
|
|
// Use getBounding for sub-pixel accuracy, which we then want to round
|
|
// up!
|
|
var bounding = bodyCells.get(i).getBoundingClientRect().width;
|
|
// Total is tracked to remove any sub-pixel errors as the outerWidth
|
|
// of the table might not equal the total given here
|
|
total += bounding;
|
|
// Width for each column to use
|
|
columns[visibleColumns[i]].width = stringToCss(bounding);
|
|
}
|
|
table.style.width = stringToCss(total);
|
|
// Finished with the table - ditch it
|
|
holder.remove();
|
|
// If there is a width attr, we want to attach an event listener which
|
|
// allows the table sizing to automatically adjust when the window is
|
|
// resized. Use the width attr rather than CSS, since we can't know if the
|
|
// CSS is a relative value or absolute - DOM read is always px.
|
|
if (tableWidthAttr) {
|
|
table.style.width = stringToCss(tableWidthAttr);
|
|
}
|
|
if ((tableWidthAttr || scrollX) && !settings.reszEvt) {
|
|
var resize = util.throttle(function () {
|
|
var newWidth = wrapperWidth(settings);
|
|
// Don't do it if destroying or the container width is 0
|
|
if (!settings.destroying && newWidth !== 0) {
|
|
adjustColumnSizing(settings);
|
|
}
|
|
});
|
|
// For browsers that support it (~2020 onwards for wide support) we can watch for the
|
|
// container changing width.
|
|
if (window.ResizeObserver) {
|
|
// This is a tricky beast - if the element is visible when `.observe()` is called,
|
|
// then the callback is immediately run. Which we don't want. If the element isn't
|
|
// visible, then it isn't run, but we want it to run when it is then made visible.
|
|
// This flag allows the above to be satisfied.
|
|
var first = Dom.s(settings.tableWrapper).isVisible();
|
|
// Use an empty div to attach the observer so it isn't impacted by height changes
|
|
var resizer = Dom
|
|
.c('div')
|
|
.css({
|
|
width: '100%',
|
|
height: '0'
|
|
})
|
|
.classAdd('dt-autosize')
|
|
.appendTo(settings.tableWrapper);
|
|
settings.resizeObserver = new ResizeObserver(function (e) {
|
|
if (first) {
|
|
first = false;
|
|
}
|
|
else {
|
|
resize();
|
|
}
|
|
});
|
|
settings.resizeObserver.observe(resizer.get(0));
|
|
}
|
|
else {
|
|
// For old browsers, the best we can do is listen for a window
|
|
// resize
|
|
window.addEventListener('resize', resize);
|
|
settings.windowResizeCb = resize; // For removal in `destroy`
|
|
}
|
|
settings.reszEvt = true;
|
|
}
|
|
}
|
|
/**
|
|
* Get the width of the DataTables wrapper element
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns Width
|
|
*/
|
|
function wrapperWidth(settings) {
|
|
let wrapper = Dom.s(settings.tableWrapper);
|
|
return wrapper.isVisible() ? wrapper.width() : 0;
|
|
}
|
|
/**
|
|
* Get the widest strings for each column.
|
|
*
|
|
* It is very difficult to determine what the widest string actually is due to variable character
|
|
* width and kerning. Doing an exact calculation with the DOM or even Canvas would kill performance
|
|
* and this is a critical point, so we use two techniques to determine a collection of the longest
|
|
* strings from the column, which will likely contain the widest strings:
|
|
*
|
|
* 1) Get the top three longest strings from the column
|
|
* 2) Get the top three widest words (i.e. an unbreakable phrase)
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param colIdx column of interest
|
|
* @returns Array of the longest strings
|
|
*/
|
|
function getWideStrings(settings, colIdx) {
|
|
var column = settings.columns[colIdx];
|
|
// Do we need to recalculate (i.e. was invalidated), or just use the cached data?
|
|
if (!column.wideStrings) {
|
|
var allStrings = [];
|
|
var collection = [];
|
|
// Create an array with the string information for the column
|
|
for (var i = 0, iLen = settings.displayMaster.length; i < iLen; i++) {
|
|
var rowIdx = settings.displayMaster[i];
|
|
var data = getRowDisplay(settings, rowIdx)[colIdx];
|
|
var cellString = data && typeof data === 'object' && data.nodeType
|
|
? data.innerHTML
|
|
: data + '';
|
|
// Remove id / name attributes from elements so they
|
|
// don't interfere with existing elements
|
|
cellString = cellString
|
|
.replace(/id=".*?"/g, '')
|
|
.replace(/name=".*?"/g, '');
|
|
// Don't want script, dialog or template tags in the width
|
|
// calculations as they are hidden content
|
|
cellString = cellString
|
|
.replace(/<script[\s\S]*?<\/script(?:\s[^>]*)?>/gi, ' ')
|
|
.replace(/<dialog[\s\S]*?<\/dialog(?:\s[^>]*)?>/gi, ' ')
|
|
.replace(/<template[\s\S]*?<\/template(?:\s[^>]*)?>/gi, ' ');
|
|
var noHtml = util.string
|
|
.stripHtml(cellString, ' ')
|
|
.replace(/ /g, ' ');
|
|
collection.push({
|
|
str: cellString,
|
|
len: noHtml.length
|
|
});
|
|
allStrings.push(noHtml);
|
|
}
|
|
// Order and then cut down to the size we need
|
|
collection
|
|
.sort(function (a, b) {
|
|
return b.len - a.len;
|
|
})
|
|
.splice(3);
|
|
column.wideStrings = collection.map(function (item) {
|
|
return item.str;
|
|
});
|
|
// Longest unbroken string
|
|
const parts = allStrings.join(' ').split(' ');
|
|
parts.sort(function (a, b) {
|
|
return b.length - a.length;
|
|
});
|
|
if (parts.length) {
|
|
column.wideStrings.push(parts[0]);
|
|
}
|
|
if (parts.length > 1) {
|
|
column.wideStrings.push(parts[1]);
|
|
}
|
|
if (parts.length > 2) {
|
|
column.wideStrings.push(parts[3]);
|
|
}
|
|
}
|
|
return column.wideStrings;
|
|
}
|
|
/**
|
|
* Append a CSS unit (only if required) to a string
|
|
*
|
|
* @param s Value to css-ify
|
|
* @returns Value with css unit
|
|
*/
|
|
function stringToCss(s) {
|
|
if (s === null) {
|
|
return '0px';
|
|
}
|
|
if (typeof s == 'number') {
|
|
return s < 0 ? '0px' : s + 'px';
|
|
}
|
|
// Check it has a unit character already
|
|
return s.match(/\d$/) ? s + 'px' : s;
|
|
}
|
|
/**
|
|
* Re-insert the `col` elements for current visibility
|
|
*
|
|
* @param settings DT settings
|
|
*/
|
|
function colGroup(settings) {
|
|
var cols = settings.columns;
|
|
settings.colgroup.empty();
|
|
for (var i = 0; i < cols.length; i++) {
|
|
if (cols[i].visible) {
|
|
settings.colgroup.append(cols[i].colEl);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scrolling setup
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns Node to add to the DOM
|
|
*/
|
|
function featureTable(settings) {
|
|
let table = Dom.s(settings.table);
|
|
let scroll = settings.scroll;
|
|
let scrollX = scroll.x;
|
|
let scrollY = scroll.y;
|
|
// No scrolling or x-scrolling only
|
|
if (scrollY === '' && scrollX === '') {
|
|
return table.get(0);
|
|
}
|
|
let classes = settings.classes.scrolling;
|
|
let caption = settings.captionNode;
|
|
let captionSide = caption
|
|
? caption._captionSide
|
|
: null;
|
|
let tableCloneHeader = table.clone(false);
|
|
let tableCloneFooter = table.clone(false);
|
|
let footer = table.children('tfoot');
|
|
let size = function (s) {
|
|
return !s ? '100%' : stringToCss(s);
|
|
};
|
|
/*
|
|
* The HTML structure that we want to generate in this function is:
|
|
* div - scroller
|
|
* div - scroll head
|
|
* div - scroll head inner
|
|
* table - scroll head table
|
|
* thead - thead
|
|
* div - scroll body
|
|
* table - table (master table)
|
|
* thead - thead clone for sizing
|
|
* tbody - tbody
|
|
* div - scroll foot
|
|
* div - scroll foot inner
|
|
* table - scroll foot table
|
|
* tfoot - tfoot
|
|
*/
|
|
let scroller = Dom.c('div')
|
|
.classAdd(classes.container)
|
|
.attr('role', 'table')
|
|
.append(Dom.c('div')
|
|
.classAdd(classes.header.self)
|
|
.css({
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
border: '0',
|
|
width: scrollX ? size(scrollX) : '100%'
|
|
})
|
|
.attr('role', 'none')
|
|
.append(Dom.c('div')
|
|
.classAdd(classes.header.inner)
|
|
.css({
|
|
'box-sizing': 'content-box',
|
|
width: scroll.xInner || '100%'
|
|
})
|
|
.attr('role', 'none')
|
|
.append(tableCloneHeader
|
|
.attrRemove('id')
|
|
.css('margin-left', '0')
|
|
.append(captionSide === 'top' ? caption : null)
|
|
.append(table.children('thead')))))
|
|
.append(Dom.c('div')
|
|
.classAdd(classes.body)
|
|
.css({
|
|
position: 'relative',
|
|
overflow: 'auto',
|
|
width: size(scrollX)
|
|
})
|
|
.attr('role', 'none')
|
|
.append(table));
|
|
if (footer.count()) {
|
|
scroller.append(Dom.c('div')
|
|
.classAdd(classes.footer.self)
|
|
.css({
|
|
overflow: 'hidden',
|
|
border: '0',
|
|
width: scrollX ? size(scrollX) : '100%'
|
|
})
|
|
.attr('role', 'none')
|
|
.append(Dom.c('div')
|
|
.classAdd(classes.footer.inner)
|
|
.attr('role', 'none')
|
|
.append(tableCloneFooter
|
|
.attrRemove('id')
|
|
.css('margin-left', '0')
|
|
.append(captionSide === 'bottom' ? caption : null)
|
|
.append(table.children('tfoot')))));
|
|
}
|
|
let children = scroller.children();
|
|
let scrollHead = children.eq(0);
|
|
let scrollBody = children.eq(1);
|
|
let scrollFoot = children.eq(2);
|
|
// When the body is scrolled, then we also want to scroll the header and
|
|
// footer. Note that each element has its own scroll listener, and that in
|
|
// turn sets the scroll for the other elements. However this doesn't lead to
|
|
// an infinite loop as `scroll` is only triggered if the value changes.
|
|
scrollBody.on('scroll.DT', () => {
|
|
let scrollLeft = scrollBody.scrollLeft();
|
|
scrollHead.scrollLeft(scrollLeft);
|
|
scrollFoot.scrollLeft(scrollLeft);
|
|
});
|
|
scrollHead.on('scroll.DT', () => {
|
|
let scrollLeft = scrollHead.scrollLeft();
|
|
scrollBody.scrollLeft(scrollLeft);
|
|
scrollFoot.scrollLeft(scrollLeft);
|
|
});
|
|
scrollFoot.on('scroll.DT', () => {
|
|
let scrollLeft = scrollFoot.scrollLeft();
|
|
scrollHead.scrollLeft(scrollLeft);
|
|
scrollBody.scrollLeft(scrollLeft);
|
|
});
|
|
scrollBody.css('max-height', size(scrollY));
|
|
if (!scroll.collapse) {
|
|
scrollBody.css('height', size(scrollY));
|
|
}
|
|
settings.scrollHead = scrollHead;
|
|
settings.scrollBody = scrollBody;
|
|
settings.scrollFoot = scrollFoot;
|
|
// On redraw - align columns
|
|
settings.callbacks.draw.push(scrollDraw);
|
|
// Aria roles - because we break the table up into parts we need to be very
|
|
// explicit with the roles to create the accessability tree for the table,
|
|
// otherwise browser's attempt to "fix" the tree by filling in what it
|
|
// thinks are gaps. The static elements that we can assign roles to are done
|
|
// here. Dynamic ones are done in the draw function below.
|
|
table.attr('role', 'none');
|
|
table.find('tbody').attr('role', 'rowgroup');
|
|
tableCloneHeader.attr('role', 'none');
|
|
tableCloneFooter.attr('role', 'none');
|
|
settings.colgroup.find('colgroup').attr('role', 'none');
|
|
// Move the info feature's aria desc by to the new "table"
|
|
let describedBy = table.attr('aria-describedby');
|
|
if (describedBy) {
|
|
scroller.attr('aria-describedby', describedBy);
|
|
table.attrRemove('aria-describedby');
|
|
}
|
|
return scroller.get(0);
|
|
}
|
|
/**
|
|
* Update the header, footer and body tables for resizing - i.e. column
|
|
* alignment.
|
|
*
|
|
* Welcome to the most horrible function DataTables. The process that this
|
|
* function follows is basically:
|
|
* 1. Re-create the table inside the scrolling div
|
|
* 2. Correct colgroup > col values if needed
|
|
* 3. Copy colgroup > col over to header and footer
|
|
* 4. Clean up
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function scrollDraw(settings) {
|
|
// Given that this is such a monster function, a lot of variables are use
|
|
// to try and keep the minimised size as small as possible
|
|
let scroll = settings.scroll, barWidth = scroll.barWidth, divHeader = settings.scrollHead, divHeaderInner = divHeader.children('div'), divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.scrollBody, divBody = divBodyEl, divFooter = settings.scrollFoot, divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = Dom.s(settings.thead), table = Dom.s(settings.table), footer = Dom.s(settings.tfoot), browser = settings.browser, headerCopy, footerCopy;
|
|
// If the scrollbar visibility has changed from the last draw, we need to
|
|
// adjust the column sizes as the table width will have changed to account
|
|
// for the scrollbar
|
|
let scrollBarVis = divBodyEl.get(0).scrollHeight > divBodyEl.get(0).clientHeight;
|
|
if (settings.scrollBarVis !== scrollBarVis &&
|
|
settings.scrollBarVis !== undefined) {
|
|
settings.scrollBarVis = scrollBarVis;
|
|
adjustColumnSizing(settings);
|
|
return; // adjust column sizing will call this function again
|
|
}
|
|
else {
|
|
settings.scrollBarVis = scrollBarVis;
|
|
}
|
|
header.find('thead').attr('role', 'rowgroup');
|
|
footer.find('tfoot').attr('role', 'rowgroup');
|
|
// 1. Re-create the table inside the scrolling div
|
|
// Remove the old minimised thead and tfoot elements in the inner table
|
|
table.children('thead, tfoot').remove();
|
|
// Clone the current header and footer elements and then place it into the
|
|
// inner table
|
|
headerCopy = header.clone(true).prependTo(table);
|
|
headerCopy.find('th, td').attrRemove('tabindex');
|
|
headerCopy.find('[id]').attrRemove('id');
|
|
if (footer.count()) {
|
|
footerCopy = footer.clone(true).prependTo(table);
|
|
footerCopy.find('[id]').attrRemove('id');
|
|
}
|
|
// 2. Correct colgroup > col values if needed
|
|
// It is possible that the cell sizes are smaller than the content, so we need to
|
|
// correct colgroup>col for such cases. This can happen if the auto width detection
|
|
// uses a cell which has a longer string, but isn't the widest! For example
|
|
// "Chief Executive Officer (CEO)" is the longest string in the demo, but
|
|
// "Systems Administrator" is actually the widest string since it doesn't collapse.
|
|
// Note the use of translating into a column index to get the `col` element. This
|
|
// is because of Responsive which might remove `col` elements, knocking the alignment
|
|
// of the indexes out.
|
|
if (settings.display.length) {
|
|
// Get the column sizes from the first row in the table. This should really be a
|
|
// [].find, but it wasn't supported in Chrome until Sept 2015, and DT has 10 year
|
|
// browser support
|
|
let firstTr = null;
|
|
let start = dataSource(settings) !== 'ssp' ? settings.displayStart : 0;
|
|
for (let i = start; i < start + settings.display.length; i++) {
|
|
let idx = settings.display[i];
|
|
let row = settings.data[idx];
|
|
if (row) {
|
|
let tr = row.tr;
|
|
if (tr) {
|
|
firstTr = tr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (firstTr) {
|
|
let colSizes = Dom.s(firstTr)
|
|
.children('th, td')
|
|
.mapTo(function (cell, idx) {
|
|
return {
|
|
idx: visibleToColumnIndex(settings, idx),
|
|
width: Dom.s(cell).width('outer')
|
|
};
|
|
});
|
|
// Check against what the colgroup > col is set to and correct if needed
|
|
for (let i = 0; i < colSizes.length; i++) {
|
|
let colEl = settings.columns[colSizes[i].idx].colEl;
|
|
colEl.css('width', colSizes[i].width + 'px');
|
|
if (scroll.x) {
|
|
colEl.css('minWidth', colSizes[i].width + 'px');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 3. Copy the colgroup over to the header and footer
|
|
divHeaderTable.find('colgroup').remove();
|
|
divHeaderTable.append(settings.colgroup.clone(true));
|
|
if (footer) {
|
|
divFooterTable.find('colgroup').remove();
|
|
divFooterTable.append(settings.colgroup.clone(true));
|
|
}
|
|
// "Hide" the header and footer that we used for the sizing. We need to keep
|
|
// the content of the cell so that the width applied to the header and body
|
|
// both match, but we want to hide it completely.
|
|
headerCopy.find('th, td').each(function (el) {
|
|
Dom.c('div')
|
|
.classAdd('dt-scroll-sizing')
|
|
.append(Array.from(el.childNodes))
|
|
.appendTo(el);
|
|
});
|
|
if (footerCopy) {
|
|
footerCopy.find('th, td').each(function (el) {
|
|
Dom.c('div')
|
|
.classAdd('dt-scroll-sizing')
|
|
.append(Array.from(el.childNodes))
|
|
.appendTo(el);
|
|
});
|
|
}
|
|
// 4. Clean up
|
|
// Figure out if there are scrollbar present - if so then we need the header and footer to
|
|
// provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
|
|
let isScrolling = Math.floor(table.height()) > divBodyEl.get(0).clientHeight ||
|
|
divBody.css('overflow-y') == 'scroll';
|
|
let paddingSide = 'padding' + (browser.scrollbarLeft ? 'Left' : 'Right');
|
|
// Set the width's of the header and footer tables
|
|
let outerWidth = table.width('withPadding');
|
|
divHeaderTable.css('width', stringToCss(outerWidth));
|
|
divHeaderInner
|
|
.css('width', stringToCss(outerWidth))
|
|
.css(paddingSide, isScrolling ? barWidth + 'px' : '0px');
|
|
if (footer.count()) {
|
|
divFooterTable.css('width', stringToCss(outerWidth));
|
|
divFooterInner
|
|
.css('width', stringToCss(outerWidth))
|
|
.css(paddingSide, isScrolling ? barWidth + 'px' : '0px');
|
|
}
|
|
// Correct DOM ordering for colgroup - comes before the thead
|
|
table.children('colgroup').prependTo(table);
|
|
// Remove tabindex from the hidden row elements
|
|
table.find('thead, tfoot').find('[tabindex]').attrRemove('tabindex');
|
|
// Dynamic ARIA roles - see setup for details on why this is needed
|
|
table
|
|
.find('thead, tfoot')
|
|
.attr('role', 'none')
|
|
.find('[role]')
|
|
.attrRemove('role');
|
|
table.find('tbody tr:not([role])').attr('role', 'row');
|
|
table.find('tbody td:not([role]), tbody th:not([role])').attr('role', 'cell');
|
|
scrollAria(headerCopy);
|
|
scrollAria(footerCopy);
|
|
// Adjust the position of the header in case we loose the y-scrollbar
|
|
divBody.trigger('scroll');
|
|
// If sorting or filtering has occurred, jump the scrolling back to the top
|
|
// only if we aren't holding the position
|
|
if ((settings.wasOrdered || settings.wasFiltered) && !settings.drawHold) {
|
|
divBodyEl.scrollTop(0);
|
|
}
|
|
}
|
|
/**
|
|
* Apply ARIA roles for the header / footer of a scrolling table
|
|
* @param element
|
|
*/
|
|
function scrollAria(element) {
|
|
if (element) {
|
|
element.find('tfoot:not([role])').attr('role', 'rowgroup');
|
|
element.find('tr:not([role])').attr('role', 'row');
|
|
element.find('th:not([role])').attr('role', 'columnheader');
|
|
element.find('td:not([role])').attr('role', 'cell');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the options to the page HTML for the table
|
|
*
|
|
* @param ctx DataTables context
|
|
*/
|
|
function createLayout(ctx) {
|
|
var classes = ctx.classes;
|
|
// Wrapper div around everything DataTables controls
|
|
var insert = Dom
|
|
.c('div')
|
|
.attr('id', ctx.tableId + '_wrapper')
|
|
.classAdd(classes.container)
|
|
.insertBefore(ctx.table);
|
|
ctx.tableWrapper = insert.get(0);
|
|
if (ctx.dom) {
|
|
// Legacy
|
|
legacyDom(ctx, ctx.dom, insert);
|
|
}
|
|
else {
|
|
var top = convert(ctx, ctx.layout, 'top');
|
|
var bottom = convert(ctx, ctx.layout, 'bottom');
|
|
var render = renderer(ctx, 'layout');
|
|
// Everything above - the renderer will actually insert the contents into the document
|
|
top.forEach(function (item) {
|
|
render(ctx, insert, item);
|
|
});
|
|
// The table - always the center of attention
|
|
render(ctx, insert, {
|
|
full: {
|
|
contents: [featureTable(ctx)],
|
|
items: [],
|
|
table: true
|
|
}
|
|
});
|
|
// Everything below
|
|
bottom.forEach(function (item) {
|
|
render(ctx, insert, item);
|
|
});
|
|
}
|
|
// Processing floats on top, so it isn't an inserted feature
|
|
processingHtml(ctx);
|
|
}
|
|
/**
|
|
* Expand the layout items into an object for the rendering function
|
|
*/
|
|
function layoutItems(row, align, items) {
|
|
if (Array.isArray(items)) {
|
|
for (var i = 0; i < items.length; i++) {
|
|
layoutItems(row, align, items[i]);
|
|
}
|
|
return;
|
|
}
|
|
var rowCell = row[align]; // can't be undefined - will have been created by getRow
|
|
// If it is an object, then there can be multiple features contained in it
|
|
if (util.is.plainObject(items)) {
|
|
// Is it an cell object already, with rowId, etc. A feature plugin cannot
|
|
// be named "features" due to this check
|
|
if (items.features) {
|
|
if (items.rowId) {
|
|
row.id = items.rowId;
|
|
}
|
|
if (items.rowClass) {
|
|
row.className = items.rowClass;
|
|
}
|
|
rowCell.id = items.id;
|
|
rowCell.className = items.className;
|
|
layoutItems(row, align, items.features);
|
|
}
|
|
else {
|
|
// An object of features and configuration options - e.g. `{paging: {startEnd: false}}`
|
|
util.object.each(items, (key, val) => {
|
|
rowCell.items.push({
|
|
feature: key,
|
|
opts: val
|
|
});
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// Otherwise, it is a function, node or Dom / jQuery instance and can just get added
|
|
rowCell.items.push(items);
|
|
}
|
|
}
|
|
/**
|
|
* Find, or create a layout row and setup a target cell in it
|
|
*
|
|
* @param rows Rows array to search for the target row. Is mutated when a row is
|
|
* added if not found.
|
|
* @param rowNum Row index to get
|
|
* @param align Where the cell position is
|
|
* @returns The row
|
|
*/
|
|
function getRow(rows, rowNum, align) {
|
|
var row;
|
|
// Find existing rows
|
|
for (var i = 0; i < rows.length; i++) {
|
|
row = rows[i];
|
|
if (row.rowNum === rowNum) {
|
|
// full is on its own, but start and end share a row
|
|
if ((align === 'full' && row.full) ||
|
|
((align === 'start' || align === 'end') &&
|
|
(row.start || row.end))) {
|
|
if (!row[align]) {
|
|
row[align] = {
|
|
contents: [],
|
|
items: []
|
|
};
|
|
}
|
|
return row;
|
|
}
|
|
}
|
|
}
|
|
// If we get this far, then there was no match, create a new row
|
|
row = {
|
|
rowNum: rowNum
|
|
};
|
|
row[align] = {
|
|
contents: [],
|
|
items: []
|
|
};
|
|
rows.push(row);
|
|
return row;
|
|
}
|
|
/**
|
|
* Convert a `layout` object given by a user to the object structure needed
|
|
* for the renderer. This is done twice, once for above and once for below
|
|
* the table. Ordering must also be considered.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param layout Layout object to convert
|
|
* @param side `top` or `bottom`
|
|
* @returns Converted array structure - one item for each row.
|
|
*/
|
|
function convert(settings, layout, side) {
|
|
var rows = [];
|
|
// Split out into an array
|
|
util.object.each(layout, function (pos, items) {
|
|
var parts = pos.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/);
|
|
if (items === null || !parts) {
|
|
return;
|
|
}
|
|
var rowNum = parts[2] ? parseInt(parts[2]) : 0;
|
|
var align = parts[3] ? parts[3].toLowerCase() : 'full';
|
|
// Filter out the side we aren't interested in
|
|
if (parts[1] !== side) {
|
|
return;
|
|
}
|
|
// Only really a type check
|
|
if (align !== 'full' && align !== 'start' && align !== 'end') {
|
|
return;
|
|
}
|
|
// Get or create the row we should attach to
|
|
var row = getRow(rows, rowNum, align);
|
|
layoutItems(row, align, items);
|
|
});
|
|
// Order by item identifier
|
|
rows.sort(function (a, b) {
|
|
var order1 = a.rowNum || 0;
|
|
var order2 = b.rowNum || 0;
|
|
// If both in the same row, then the row with `full` comes first
|
|
if (order1 === order2) {
|
|
var ret = a.full && !b.full ? -1 : 1;
|
|
return side === 'bottom' ? ret * -1 : ret;
|
|
}
|
|
return order2 - order1;
|
|
});
|
|
// Invert for below the table
|
|
if (side === 'bottom') {
|
|
rows.reverse();
|
|
}
|
|
for (var row = 0; row < rows.length; row++) {
|
|
delete rows[row].rowNum;
|
|
resolve(settings, rows[row]);
|
|
}
|
|
return rows;
|
|
}
|
|
/**
|
|
* Convert the contents of a row's layout object to nodes that can be inserted
|
|
* into the document by a renderer. Execute functions, look up plug-ins, etc.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param row Layout object for this row
|
|
*/
|
|
function resolve(settings, row) {
|
|
var getFeature = function (feature, opts) {
|
|
if (!ext.features[feature]) {
|
|
log(settings, 0, 'Unknown feature: ' + feature);
|
|
}
|
|
return ext.features[feature].apply(this, [settings, opts]);
|
|
};
|
|
// Resolve items in the `contents` array from being an identifier, such as
|
|
// the name of a feature, into the node to display.
|
|
var resolve = function (item) {
|
|
if (!row[item]) {
|
|
return;
|
|
}
|
|
row[item].contents = row[item].items
|
|
.filter(item => !!item)
|
|
.map(item => {
|
|
if (typeof item === 'string') {
|
|
return getFeature(item, null);
|
|
}
|
|
else if (util.is.plainObject(item)) {
|
|
// If it's an object, it just has feature and opts properties from
|
|
// the transform in _layoutArray
|
|
return getFeature(item.feature, item.opts);
|
|
}
|
|
else if (typeof item.node === 'function') {
|
|
return item.node(settings);
|
|
}
|
|
else if (typeof item === 'function') {
|
|
var inst = item(settings);
|
|
return typeof inst.node === 'function' ? inst.node() : inst;
|
|
}
|
|
else if (item.nodeName) {
|
|
// An HTML element
|
|
return item;
|
|
}
|
|
else if (item instanceof Dom) {
|
|
return item.get(0);
|
|
}
|
|
else if (item.length) {
|
|
// Possibly jQuery
|
|
return item[0];
|
|
}
|
|
});
|
|
};
|
|
resolve('start');
|
|
resolve('end');
|
|
resolve('full');
|
|
}
|
|
/**
|
|
* Draw the table with the legacy DOM property
|
|
*
|
|
* @param settings DT settings instance
|
|
* @param layout DOM string
|
|
* @param insert Insert point
|
|
*/
|
|
function legacyDom(settings, layout, insert) {
|
|
let parts = layout.match(/(".*?")|('.*?')|./g);
|
|
let featureNode, option, newNode, next, attr;
|
|
if (!parts) {
|
|
return;
|
|
}
|
|
for (let i = 0; i < parts.length; i++) {
|
|
featureNode = null;
|
|
option = parts[i];
|
|
if (option == '<') {
|
|
// New container div
|
|
newNode = Dom.c('div');
|
|
// Check to see if we should append an id and/or a class name to the container
|
|
next = parts[i + 1];
|
|
if (next[0] == "'" || next[0] == '"') {
|
|
attr = next.replace(/['"]/g, '');
|
|
let id = '', className;
|
|
/* The attribute can be in the format of "#id.class", "#id" or "class" This logic
|
|
* breaks the string into parts and applies them as needed
|
|
*/
|
|
if (attr.indexOf('.') != -1) {
|
|
let split = attr.split('.');
|
|
id = split[0];
|
|
className = split[1];
|
|
}
|
|
else if (attr[0] == '#') {
|
|
id = attr;
|
|
}
|
|
else {
|
|
className = attr;
|
|
}
|
|
newNode.attr('id', id.substring(1)).classAdd(className);
|
|
i++; // Move along the position array
|
|
}
|
|
insert.append(newNode.get()); // TODO
|
|
insert = newNode;
|
|
}
|
|
else if (option == '>') {
|
|
// End container div
|
|
insert = insert.parent();
|
|
}
|
|
else if (option == 't') {
|
|
// Table
|
|
featureNode = featureTable(settings);
|
|
}
|
|
else {
|
|
ext.feature.forEach(function (feature) {
|
|
if (option == feature.cFeature) {
|
|
featureNode = feature.fnInit(settings);
|
|
}
|
|
});
|
|
}
|
|
// Add to the display
|
|
if (featureNode) {
|
|
// TODO when doing the full dom update, won't need this check
|
|
insert.append(featureNode instanceof Dom ? featureNode.get() : featureNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
function sortInit(settings) {
|
|
var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])';
|
|
if (settings.orderHandler) {
|
|
columnOrderingCells(settings, notSelector)
|
|
.each(el => {
|
|
sortAttachListener(settings, el, '');
|
|
});
|
|
}
|
|
// Need to resolve the user input array into our internal structure
|
|
var order = [];
|
|
sortResolve(settings, order, settings.order);
|
|
settings.order = order;
|
|
}
|
|
/**
|
|
* Attach event listeners to a node that will trigger ordering on a column
|
|
*
|
|
* @param settings DataTables context
|
|
* @param node Node to attach to
|
|
* @param selector Delegate selector
|
|
* @param column Column index to target
|
|
* @param callback Callback for when done
|
|
*/
|
|
function sortAttachListener(settings, node, selector, column, callback) {
|
|
bindAction(node, selector, function (e) {
|
|
var run = false;
|
|
var columns = column === undefined
|
|
? columnsFromHeader(e.target)
|
|
: typeof column === 'function'
|
|
? column()
|
|
: Array.isArray(column)
|
|
? column
|
|
: [column];
|
|
if (columns.length) {
|
|
for (var i = 0, iLen = columns.length; i < iLen; i++) {
|
|
var ret = sortAdd(settings, columns[i], i, e.shiftKey);
|
|
if (ret !== false) {
|
|
run = true;
|
|
}
|
|
// If the first entry is no sort, then subsequent
|
|
// sort columns are ignored
|
|
if (settings.order.length === 1 &&
|
|
settings.order[0][1] === '') {
|
|
break;
|
|
}
|
|
}
|
|
if (run) {
|
|
processingRun(settings, true, function () {
|
|
sort(settings);
|
|
sortDisplay(settings, settings.display);
|
|
reDraw(settings, false, false);
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Sort the display array to match the master's order
|
|
*
|
|
* @param settings DataTables context
|
|
* @param display The display array
|
|
*/
|
|
function sortDisplay(settings, display) {
|
|
if (display.length < 2) {
|
|
return;
|
|
}
|
|
var master = settings.displayMaster;
|
|
var masterMap = {};
|
|
var map = {};
|
|
var i;
|
|
// Rather than needing an `indexOf` on master array, we can create a map
|
|
for (i = 0; i < master.length; i++) {
|
|
masterMap[master[i]] = i;
|
|
}
|
|
// And then cache what would be the indexOf from the display
|
|
for (i = 0; i < display.length; i++) {
|
|
map[display[i]] = masterMap[display[i]];
|
|
}
|
|
display.sort(function (a, b) {
|
|
// Short version of this function is simply `master.indexOf(a) - master.indexOf(b);`
|
|
return map[a] - map[b];
|
|
});
|
|
}
|
|
/**
|
|
* Convert the API variants that can be used for defining the order into our
|
|
* internal OrderColumn array.
|
|
*
|
|
* @param settings DataTable context object
|
|
* @param nestedSort Array to write the resolve values to
|
|
* @param sortItem Source object / array from user (It is really an `Order`
|
|
* but due to `aaSorting` being used for input and the internal structure
|
|
* it is currently any).
|
|
* @todo Split aaSorting into unresolved and resolved parameters (in state.ts as
|
|
* well)
|
|
*/
|
|
function sortResolve(settings, nestedSort, sortItem // TODO typing
|
|
) {
|
|
var push = function (a) {
|
|
if (plainObject(a)) {
|
|
let orderIdx = a;
|
|
let orderName = a;
|
|
if (orderIdx.idx !== undefined) {
|
|
// Index based ordering
|
|
nestedSort.push([orderIdx.idx, orderIdx.dir]);
|
|
}
|
|
else if (orderName.name) {
|
|
// Name based ordering
|
|
var cols = pluck(settings.columns, 'name');
|
|
var idx = cols.indexOf(orderName.name);
|
|
if (idx !== -1) {
|
|
nestedSort.push([idx, orderName.dir]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Plain column index and direction pair
|
|
nestedSort.push(a);
|
|
}
|
|
};
|
|
if (plainObject(sortItem)) {
|
|
// Object
|
|
push(sortItem);
|
|
}
|
|
else if (Array.isArray(sortItem) && typeof sortItem[0] === 'number') {
|
|
// 1D array
|
|
push(sortItem);
|
|
}
|
|
else if (Array.isArray(sortItem)) {
|
|
// 2D array
|
|
for (var z = 0; z < sortItem.length; z++) {
|
|
push(sortItem[z]); // Object or array
|
|
}
|
|
}
|
|
}
|
|
function sortFlatten(settings) {
|
|
var i, k, kLen, aSort = [], extSort = ext.type.order, aoColumns = settings.columns, dataSort, colIdx, type, srcCol, fixed = settings.orderFixed, fixedObj = plainObject(fixed), nestedSort = [];
|
|
if (!settings.features.ordering) {
|
|
return aSort;
|
|
}
|
|
// Build the sort array, with pre-fix and post-fix options if they have been
|
|
// specified
|
|
if (Array.isArray(fixed)) {
|
|
sortResolve(settings, nestedSort, fixed);
|
|
}
|
|
if (fixedObj && fixed.pre) {
|
|
sortResolve(settings, nestedSort, fixed.pre);
|
|
}
|
|
sortResolve(settings, nestedSort, settings.order);
|
|
if (fixedObj && fixed.post) {
|
|
sortResolve(settings, nestedSort, fixed.post);
|
|
}
|
|
for (i = 0; i < nestedSort.length; i++) {
|
|
srcCol = nestedSort[i][0];
|
|
if (aoColumns[srcCol]) {
|
|
dataSort = aoColumns[srcCol].orderData;
|
|
for (k = 0, kLen = dataSort.length; k < kLen; k++) {
|
|
colIdx = dataSort[k];
|
|
type = aoColumns[colIdx].type || 'string';
|
|
if (nestedSort[i]._idx === undefined) {
|
|
nestedSort[i]._idx = aoColumns[colIdx].orderSequence.indexOf(nestedSort[i][1]);
|
|
}
|
|
if (nestedSort[i][1]) {
|
|
aSort.push({
|
|
src: srcCol,
|
|
col: colIdx,
|
|
dir: nestedSort[i][1],
|
|
index: nestedSort[i]._idx,
|
|
type: type,
|
|
formatter: extSort[type + '-pre'],
|
|
sorter: extSort[type + '-' + nestedSort[i][1]]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return aSort;
|
|
}
|
|
/**
|
|
* Change the order of the table
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param col Column to perform sort on
|
|
* @param dir Direction to sort on
|
|
*/
|
|
function sort(ctx, col, dir) {
|
|
var i, iLen, aiOrig = [], extSort = ext.type.order, data = ctx.data, sortCol, displayMaster = ctx.displayMaster, aSort;
|
|
// Make sure the columns all have types defined
|
|
columnTypes(ctx);
|
|
// Allow a specific column to be sorted, which will _not_ alter the display
|
|
// master
|
|
if (col !== undefined) {
|
|
var srcCol = ctx.columns[col];
|
|
aSort = [
|
|
{
|
|
src: col,
|
|
col: col,
|
|
dir: dir || '',
|
|
index: 0,
|
|
type: srcCol.type,
|
|
formatter: extSort[srcCol.type + '-pre'],
|
|
sorter: extSort[srcCol.type + '-' + dir]
|
|
}
|
|
];
|
|
displayMaster = displayMaster.slice();
|
|
}
|
|
else {
|
|
aSort = sortFlatten(ctx);
|
|
}
|
|
for (i = 0, iLen = aSort.length; i < iLen; i++) {
|
|
sortCol = aSort[i];
|
|
// Load the data needed for the sort, for each cell
|
|
sortData(ctx, sortCol.col);
|
|
}
|
|
/* No sorting required if server-side or no sorting array */
|
|
if (dataSource(ctx) != 'ssp' && aSort.length !== 0) {
|
|
// Reset the initial positions on each pass so we get a stable sort
|
|
for (i = 0, iLen = displayMaster.length; i < iLen; i++) {
|
|
aiOrig[i] = i;
|
|
}
|
|
// If the first sort is desc, then reverse the array to preserve original
|
|
// order, just in reverse
|
|
if (aSort.length && aSort[0].dir === 'desc' && ctx.orderDescReverse) {
|
|
aiOrig.reverse();
|
|
}
|
|
/* Do the sort - here we want multi-column sorting based on a given data source (column)
|
|
* and sorting function (from oSort) in a certain direction. It's reasonably complex to
|
|
* follow on its own, but this is what we want (example two column sorting):
|
|
* fnLocalSorting = function(a,b){
|
|
* var test;
|
|
* test = oSort['string-asc']('data11', 'data12');
|
|
* if (test !== 0)
|
|
* return test;
|
|
* test = oSort['numeric-desc']('data21', 'data22');
|
|
* if (test !== 0)
|
|
* return test;
|
|
* return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
|
|
* }
|
|
* Basically we have a test for each sorting column, if the data in that column is equal,
|
|
* test the next column. If all columns match, then we use a numeric sort on the row
|
|
* positions in the original data array to provide a stable sort.
|
|
*/
|
|
displayMaster.sort(function (a, b) {
|
|
var _a, _b;
|
|
var x, y, k, test, sortItem, len = aSort.length, dataA = (_a = data[a]) === null || _a === void 0 ? void 0 : _a.orderCache, dataB = (_b = data[b]) === null || _b === void 0 ? void 0 : _b.orderCache;
|
|
for (k = 0; k < len; k++) {
|
|
sortItem = aSort[k];
|
|
// Data, which may have already been through a `-pre` function
|
|
x = dataA[sortItem.col];
|
|
y = dataB[sortItem.col];
|
|
if (sortItem.sorter) {
|
|
// If there is a custom sorter (`-asc` or `-desc`) for this
|
|
// data type, use it
|
|
test = sortItem.sorter(x, y);
|
|
if (test !== 0) {
|
|
return test;
|
|
}
|
|
}
|
|
else {
|
|
// Otherwise, use generic sorting
|
|
test = x < y ? -1 : x > y ? 1 : 0;
|
|
if (test !== 0) {
|
|
return sortItem.dir === 'asc' ? test : -test;
|
|
}
|
|
}
|
|
}
|
|
x = aiOrig[a];
|
|
y = aiOrig[b];
|
|
return x < y ? -1 : x > y ? 1 : 0;
|
|
});
|
|
}
|
|
else if (aSort.length === 0) {
|
|
// Apply index order
|
|
displayMaster.sort(function (x, y) {
|
|
return x < y ? -1 : x > y ? 1 : 0;
|
|
});
|
|
}
|
|
if (col === undefined) {
|
|
// Tell the draw function that we have sorted the data
|
|
ctx.wasOrdered = true;
|
|
ctx.sortDetails = aSort;
|
|
callbackFire(ctx, null, 'order', [ctx, aSort]);
|
|
}
|
|
return displayMaster;
|
|
}
|
|
/**
|
|
* Function to run on user sort request
|
|
*
|
|
* @param settings dataTables settings object
|
|
* @param colIdx column sorting index
|
|
* @param addIndex Counter
|
|
* @param shift Shift click add
|
|
*/
|
|
function sortAdd(settings, colIdx, addIndex, shift) {
|
|
var col = settings.columns[colIdx];
|
|
var sorting = settings.order;
|
|
var asSorting = col.orderSequence;
|
|
var nextSortIdx;
|
|
var next = function (a, overflow) {
|
|
var idx = a._idx;
|
|
if (idx === undefined) {
|
|
idx = asSorting.indexOf(a[1]);
|
|
}
|
|
return idx + 1 < asSorting.length ? idx + 1 : overflow ? null : 0;
|
|
};
|
|
if (!col.orderable) {
|
|
return false;
|
|
}
|
|
// Convert to 2D array if needed
|
|
if (typeof sorting[0] === 'number') {
|
|
sorting = settings.order = [sorting];
|
|
}
|
|
// If appending the sort then we are multi-column sorting
|
|
if ((shift || addIndex) && settings.features.orderMulti) {
|
|
// Are we already doing some kind of sort on this column?
|
|
var sortIdx = pluck(sorting, '0').indexOf(colIdx);
|
|
if (sortIdx !== -1) {
|
|
// Yes, modify the sort
|
|
nextSortIdx = next(sorting[sortIdx], true);
|
|
if (nextSortIdx === null && sorting.length === 1) {
|
|
nextSortIdx = 0; // can't remove sorting completely
|
|
}
|
|
if (nextSortIdx === null || asSorting[nextSortIdx] === '') {
|
|
sorting.splice(sortIdx, 1);
|
|
}
|
|
else {
|
|
sorting[sortIdx][1] = asSorting[nextSortIdx];
|
|
sorting[sortIdx]._idx = nextSortIdx;
|
|
}
|
|
}
|
|
else if (shift) {
|
|
// No sort on this column yet, being added by shift click
|
|
// add it as itself
|
|
sorting.push([colIdx, asSorting[0], 0]);
|
|
sorting[sorting.length - 1]._idx = 0;
|
|
}
|
|
else {
|
|
// No sort on this column yet, being added from a colspan
|
|
// so add with same direction as first column
|
|
sorting.push([colIdx, sorting[0][1], 0]);
|
|
sorting[sorting.length - 1]._idx = 0;
|
|
}
|
|
}
|
|
else if (sorting.length && sorting[0][0] == colIdx) {
|
|
// Single column - already sorting on this column, modify the sort
|
|
nextSortIdx = next(sorting[0]);
|
|
if (nextSortIdx) {
|
|
sorting.length = 1;
|
|
sorting[0][1] = asSorting[nextSortIdx];
|
|
sorting[0]._idx = nextSortIdx;
|
|
}
|
|
else {
|
|
sorting.length = 1;
|
|
sorting[0][1] = asSorting[0];
|
|
sorting[0]._idx = 0;
|
|
}
|
|
}
|
|
else {
|
|
// Single column - sort only on this column
|
|
sorting.length = 0;
|
|
sorting.push([colIdx, asSorting[0]]);
|
|
sorting[0]._idx = 0;
|
|
}
|
|
}
|
|
/**
|
|
* Set the sorting classes on table's body, Note: it is safe to call this function
|
|
* when bSort and bSortClasses are false
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function sortingClasses(settings) {
|
|
var oldSort = settings.lastOrder;
|
|
var sortClass = settings.classes.order.position;
|
|
var sortFlat = sortFlatten(settings);
|
|
var features = settings.features;
|
|
var i, iLen, colIdx;
|
|
if (features.ordering && features.orderClasses) {
|
|
// Remove old sorting classes
|
|
for (i = 0, iLen = oldSort.length; i < iLen; i++) {
|
|
colIdx = oldSort[i].src;
|
|
// Remove column sorting
|
|
Dom.s(pluck(settings.data, 'cells', colIdx)).classRemove(sortClass + (i < 2 ? i + 1 : 3));
|
|
}
|
|
// Add new column sorting
|
|
for (i = 0, iLen = sortFlat.length; i < iLen; i++) {
|
|
colIdx = sortFlat[i].src;
|
|
Dom.s(pluck(settings.data, 'cells', colIdx)).classAdd(sortClass + (i < 2 ? i + 1 : 3));
|
|
}
|
|
}
|
|
settings.lastOrder = sortFlat;
|
|
}
|
|
/**
|
|
* Get the data to sort a column, be it from cache, fresh (populating the
|
|
* cache), or from a sort formatter
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param colIdx Column index
|
|
*/
|
|
function sortData(settings, colIdx) {
|
|
// Custom sorting function - provided by the sort data type
|
|
var column = settings.columns[colIdx];
|
|
var customSort = ext.order[column.orderDataType];
|
|
var customData;
|
|
if (customSort) {
|
|
customData = customSort.call(settings.instance, settings, colIdx, columnIndexToVisible(settings, colIdx));
|
|
}
|
|
// Use / populate cache
|
|
var row, cellData;
|
|
var formatter = ext.type.order[column.type + '-pre'];
|
|
var data = settings.data;
|
|
for (var rowIdx = 0; rowIdx < data.length; rowIdx++) {
|
|
// Sparse array
|
|
if (!data[rowIdx]) {
|
|
continue;
|
|
}
|
|
row = data[rowIdx];
|
|
if (row && !row.orderCache) {
|
|
row.orderCache = [];
|
|
}
|
|
if (row && (!row.orderCache[colIdx] || customSort)) {
|
|
cellData = customSort
|
|
? customData[rowIdx] // If there was a custom sort function, use data from there
|
|
: getCellData(settings, rowIdx, colIdx, 'sort');
|
|
row.orderCache[colIdx] = formatter
|
|
? formatter(cellData, settings)
|
|
: cellData;
|
|
}
|
|
}
|
|
}
|
|
|
|
const defaults$3 = {
|
|
boundary: false,
|
|
caseInsensitive: true,
|
|
columns: null,
|
|
exact: false,
|
|
regex: false,
|
|
return: false,
|
|
search: '',
|
|
smart: true
|
|
};
|
|
/**
|
|
* Create a new search options object
|
|
*
|
|
* @param parts Values to assign, otherwise the defaults will be used
|
|
* @returns New object
|
|
*/
|
|
function create$1(parts = {}) {
|
|
return util.object.assignDeep({}, defaults$3, parts);
|
|
}
|
|
|
|
/**
|
|
* Alter the display settings to change the page
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param action Paging action to take: "first", "previous", "next" or "last" or
|
|
* page number to jump to (integer)
|
|
* @param redraw Automatically draw the update or not
|
|
* @returns true page has changed, false - no change
|
|
*/
|
|
function pageChange(settings, action, redraw) {
|
|
var start = settings.displayStart, len = settings.pageLength, records = recordsDisplay(settings);
|
|
if (records === 0 || len === -1) {
|
|
start = 0;
|
|
}
|
|
else if (typeof action === 'number') {
|
|
start = action * len;
|
|
if (start > records) {
|
|
start = 0;
|
|
}
|
|
}
|
|
else if (action == 'first') {
|
|
start = 0;
|
|
}
|
|
else if (action == 'previous') {
|
|
start = len >= 0 ? start - len : 0;
|
|
if (start < 0) {
|
|
start = 0;
|
|
}
|
|
}
|
|
else if (action == 'next') {
|
|
if (start + len < records) {
|
|
start += len;
|
|
}
|
|
}
|
|
else if (action == 'last') {
|
|
start = Math.floor((records - 1) / len) * len;
|
|
}
|
|
else if (action === 'ellipsis') {
|
|
return;
|
|
}
|
|
else {
|
|
log(settings, 0, 'Unknown paging action: ' + action, 5);
|
|
}
|
|
var changed = settings.displayStart !== start;
|
|
settings.displayStart = start;
|
|
callbackFire(settings, null, changed ? 'page' : 'page-nc', [settings]);
|
|
if (changed && redraw) {
|
|
draw(settings);
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/**
|
|
* State information for a table
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function saveState(settings) {
|
|
if (settings.loadingState) {
|
|
return;
|
|
}
|
|
// Sort state saving uses [[idx, order]] structure.
|
|
var sorting = [];
|
|
sortResolve(settings, sorting, settings.order);
|
|
/* Store the interesting variables */
|
|
var columns = settings.columns;
|
|
var state = {
|
|
columns: settings.columns.map(function (col, i) {
|
|
return {
|
|
name: col.name,
|
|
visible: col.visible,
|
|
search: Object.assign({}, settings.searches[i])
|
|
};
|
|
}),
|
|
length: settings.pageLength,
|
|
order: sorting.map(function (sort) {
|
|
// If a column name is available, use it
|
|
return columns[sort[0]] && columns[sort[0]].name
|
|
? [columns[sort[0]].name, sort[1]]
|
|
: sort.slice();
|
|
}),
|
|
search: Object.assign({}, settings.searches['*']),
|
|
searchGroups: Object.keys(settings.searches)
|
|
.filter(c => c.includes(',')) // Limit to only multi-column subsets
|
|
.map(c => Object.assign({}, settings.searches[c])),
|
|
start: settings.displayStart,
|
|
time: +new Date()
|
|
};
|
|
settings.stateSaved = state;
|
|
callbackFire(settings, 'stateSaveParams', 'stateSaveParams', [
|
|
settings,
|
|
state
|
|
]);
|
|
if (settings.features.stateSave && !settings.destroying) {
|
|
settings.stateSaveCallback.call(settings.instance, settings, state);
|
|
}
|
|
}
|
|
/**
|
|
* Attempt to load a saved table state
|
|
*
|
|
* @param settings dataTables settings object
|
|
* @param callback Callback to execute when the state has been loaded
|
|
*/
|
|
function loadState(settings, callback) {
|
|
if (!settings.features.stateSave) {
|
|
callback();
|
|
return;
|
|
}
|
|
var loaded = function (state, ignoreTime = false) {
|
|
implementState(settings, state, ignoreTime, callback);
|
|
};
|
|
var state = settings.stateLoadCallback.call(settings.instance, settings, loaded);
|
|
if (state !== undefined) {
|
|
implementState(settings, state, false, callback);
|
|
}
|
|
// otherwise, wait for the loaded callback to be executed
|
|
return true;
|
|
}
|
|
function implementState(settings, s, ignoreTime, callback) {
|
|
var i, iLen;
|
|
var columns = settings.columns;
|
|
var currentNames = pluck(settings.columns, 'name');
|
|
settings.loadingState = true;
|
|
// When StateRestore was introduced the state could now be implemented at
|
|
// any time Not just initialisation. To do this an api instance is required
|
|
// in some places
|
|
var api = settings.initDone ? new Api(settings) : null;
|
|
if (!ignoreTime) {
|
|
if (!s || !s.time) {
|
|
settings.loadingState = false;
|
|
callback();
|
|
return;
|
|
}
|
|
// Reject old data
|
|
var duration = settings.stateDuration;
|
|
if (duration > 0 && s.time < +new Date() - duration * 1000) {
|
|
settings.loadingState = false;
|
|
callback();
|
|
return;
|
|
}
|
|
}
|
|
// Allow custom and plug-in manipulation functions to alter the saved data
|
|
// set and cancelling of loading by returning false
|
|
var abStateLoad = callbackFire(settings, 'stateLoadParams', 'stateLoadParams', [settings, s]);
|
|
if (abStateLoad.indexOf(false) !== -1) {
|
|
settings.loadingState = false;
|
|
callback();
|
|
return;
|
|
}
|
|
// Store the saved state so it might be accessed at any time
|
|
settings.stateLoaded = assignDeep({}, s);
|
|
// This is needed for ColReorder, which has to happen first to allow all
|
|
// the stored indexes to be usable. It is not publicly documented.
|
|
callbackFire(settings, null, 'stateLoadInit', [settings, s], true);
|
|
// Page Length
|
|
if (s.length !== undefined) {
|
|
// If already initialised just set the value directly so that the select
|
|
// element is also updated
|
|
if (api) {
|
|
api.page.len(s.length);
|
|
}
|
|
else {
|
|
settings.pageLength = s.length;
|
|
}
|
|
}
|
|
// Restore key features
|
|
if (s.start !== undefined) {
|
|
if (api === null) {
|
|
settings.displayStart = s.start;
|
|
settings.displayStartInit = s.start;
|
|
}
|
|
else {
|
|
pageChange(settings, s.start / settings.pageLength);
|
|
}
|
|
}
|
|
// Order
|
|
if (s.order !== undefined) {
|
|
settings.order = [];
|
|
for (let i = 0; i < s.order.length; i++) {
|
|
let col = s.order[i];
|
|
let set = [col[0], col[1]];
|
|
// A column name was stored and should be used for restore
|
|
if (typeof col[0] === 'string') {
|
|
// Find the name from the current list of column names
|
|
let idx = currentNames.indexOf(col[0]);
|
|
if (idx < 0) {
|
|
// If the column was not found ignore it and continue
|
|
continue;
|
|
}
|
|
set[0] = idx;
|
|
}
|
|
else if (set[0] >= columns.length) {
|
|
// If the column index is out of bounds ignore it and continue
|
|
continue;
|
|
}
|
|
settings.order.push(set);
|
|
}
|
|
}
|
|
// Search
|
|
if (s.search !== undefined) {
|
|
Object.assign(settings.searches['*'], s.search);
|
|
}
|
|
if (s.searchGroups) {
|
|
s.searchGroups.forEach(group => {
|
|
if (group.columns) {
|
|
let index = group.columns.join(',');
|
|
settings.searches[index] = create$1(group);
|
|
}
|
|
});
|
|
}
|
|
// Columns
|
|
if (s.columns) {
|
|
var set = s.columns;
|
|
var incoming = pluck(s.columns, 'name');
|
|
// Check if it is a 2.2 style state object with a `name` property for
|
|
// the columns, and if the name was defined. If so, then create a new
|
|
// array that will map the state object given, to the current columns
|
|
// (don't bother if they are already matching tho).
|
|
if (incoming.join('').length &&
|
|
incoming.join('') !== currentNames.join('')) {
|
|
set = [];
|
|
// For each column, try to find the name in the incoming array
|
|
for (i = 0; i < currentNames.length; i++) {
|
|
if (currentNames[i] != '') {
|
|
var idx = incoming.indexOf(currentNames[i]);
|
|
if (idx >= 0) {
|
|
set.push(s.columns[idx]);
|
|
}
|
|
else {
|
|
// No matching column name in the state's columns, so
|
|
// this might be a new column and thus can't have a
|
|
// state already.
|
|
set.push({});
|
|
}
|
|
}
|
|
else {
|
|
// If no name, but other columns did have a name, then there
|
|
// is no knowing where this one came from originally so it
|
|
// can't be restored.
|
|
set.push({});
|
|
}
|
|
}
|
|
}
|
|
// If the number of columns to restore is different from current, then
|
|
// all bets are off.
|
|
if (set.length === columns.length) {
|
|
for (i = 0, iLen = set.length; i < iLen; i++) {
|
|
var col = set[i];
|
|
// Visibility
|
|
if (col.visible !== undefined) {
|
|
// If the api is defined, the table has been initialised so
|
|
// we need to use it rather than internal settings
|
|
if (api) {
|
|
// Don't redraw the columns on every iteration of this
|
|
// loop, we will do this at the end instead
|
|
api.column(i).visible(col.visible, false);
|
|
}
|
|
else {
|
|
columns[i].visible = col.visible;
|
|
}
|
|
}
|
|
// Search
|
|
if (col.search !== undefined) {
|
|
Object.assign(settings.searches[i], col.search);
|
|
// If out of order due to a change in order from named
|
|
// columns we need to make sure the index is correct
|
|
settings.searches[i].columns = [i];
|
|
}
|
|
}
|
|
// If the api is defined then we need to adjust the columns once the
|
|
// visibility has been changed
|
|
if (api) {
|
|
api.one('draw', function () {
|
|
api.columns.adjust();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
settings.loadingState = false;
|
|
callbackFire(settings, 'stateLoaded', 'stateLoaded', [settings, s]);
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
* Draw the table for the first time, adding all required features
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function initialise(settings) {
|
|
var i;
|
|
var init = settings.init;
|
|
var deferLoading = settings.deferLoading;
|
|
var dataSrc = dataSource(settings);
|
|
// Ensure that the table data is fully initialised
|
|
if (!settings.initialised) {
|
|
setTimeout(function () {
|
|
initialise(settings);
|
|
}, 200);
|
|
return;
|
|
}
|
|
// Build the header / footer for the table
|
|
buildHead(settings, 'header');
|
|
buildHead(settings, 'footer');
|
|
// Load the table's state (if needed) and then render around it and draw
|
|
loadState(settings, function () {
|
|
// Then draw the header / footer
|
|
drawHead(settings, settings.header);
|
|
drawHead(settings, settings.footer);
|
|
// Cache the paging start point, as the first redraw will reset it
|
|
var iAjaxStart = settings.displayStartInit;
|
|
// Local data load
|
|
// Check if there is data passing into the constructor
|
|
if (init && init.data) {
|
|
for (i = 0; i < init.data.length; i++) {
|
|
addData(settings, init.data[i]);
|
|
}
|
|
}
|
|
else if (deferLoading || dataSrc == 'dom') {
|
|
// Grab the data from the page
|
|
addTr(settings, Dom.s(settings.tbody).children('tr'));
|
|
}
|
|
// Filter not yet applied - copy the display master
|
|
settings.display = settings.displayMaster.slice();
|
|
// Enable features
|
|
createLayout(settings);
|
|
sortInit(settings);
|
|
colGroup(settings);
|
|
/* Okay to show that something is going on now */
|
|
processingDisplay(settings, true);
|
|
callbackFire(settings, null, 'preInit', [settings], true);
|
|
// If there is default sorting required - let's do it. The sort function
|
|
// will do the drawing for us. Otherwise we draw the table regardless of
|
|
// the Ajax source - this allows the table to look initialised for Ajax
|
|
// sourcing data (show 'loading' message possibly)
|
|
reDraw(settings);
|
|
// Server-side processing init complete is done by _fnAjaxUpdateDraw
|
|
if (dataSrc != 'ssp' || deferLoading) {
|
|
// if there is an ajax source load the data
|
|
if (dataSrc == 'ajax') {
|
|
buildAjax(settings, {}, function (json) {
|
|
var aData = ajaxDataSrc(settings, json, false);
|
|
// Got the data - add it to the table
|
|
for (i = 0; i < aData.length; i++) {
|
|
addData(settings, aData[i]);
|
|
}
|
|
// Reset the init display for cookie saving. We've already
|
|
// done a filter, and therefore cleared it before. So we
|
|
// need to make it appear 'fresh'
|
|
settings.displayStartInit = iAjaxStart;
|
|
reDraw(settings);
|
|
processingDisplay(settings, false);
|
|
initComplete(settings);
|
|
});
|
|
}
|
|
else {
|
|
initComplete(settings);
|
|
processingDisplay(settings, false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Draw the table for the first time, adding all required features
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function initComplete(settings) {
|
|
if (settings.initDone) {
|
|
return;
|
|
}
|
|
var args = [settings, settings.json];
|
|
settings.initDone = true;
|
|
// If the footer element is empty after initialisation, then remove it
|
|
let tfoot = Dom.s(settings.tfoot);
|
|
if (tfoot.children().count() === 0) {
|
|
tfoot.remove();
|
|
}
|
|
// Table is fully set up and we have data, so calculate the
|
|
// column widths
|
|
adjustColumnSizing(settings);
|
|
callbackFire(settings, null, 'plugin-init', args, true);
|
|
callbackFire(settings, 'init', 'init', args, true);
|
|
}
|
|
|
|
/**
|
|
* Create an Ajax call based on the table's settings, taking into account that
|
|
* parameters can have multiple forms, and backwards compatibility.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param data Data to send to the server, required by DataTables - may be
|
|
* augmented by developer callbacks
|
|
* @param fn Callback function to run when data is obtained
|
|
*/
|
|
function buildAjax(settings, data, fn) {
|
|
var ajaxData;
|
|
var ajaxConfig = settings.ajax;
|
|
var instance = settings.instance;
|
|
var callback = function (json) {
|
|
var status = settings.jqXHR ? settings.jqXHR.status : null;
|
|
if (json === null || (typeof status === 'number' && status == 204)) {
|
|
json = {};
|
|
ajaxDataSrc(settings, json, []);
|
|
}
|
|
var error = json.error || json.sError;
|
|
if (error) {
|
|
log(settings, 0, error);
|
|
}
|
|
// Microsoft often wrap JSON as a string in another JSON object Let's
|
|
// handle that automatically
|
|
if (json.d && typeof json.d === 'string') {
|
|
try {
|
|
json = JSON.parse(json.d);
|
|
}
|
|
catch (e) {
|
|
// noop
|
|
}
|
|
}
|
|
settings.json = json;
|
|
invalidColumn(settings);
|
|
callbackFire(settings, null, 'xhr', [settings, json, settings.jqXHR], true);
|
|
fn(json);
|
|
};
|
|
if (util.is.plainObject(ajaxConfig) && ajaxConfig.data) {
|
|
ajaxData = ajaxConfig.data;
|
|
var newData = typeof ajaxData === 'function'
|
|
? ajaxData(data, settings) // fn can manipulate data or return
|
|
: ajaxData; // an object or array to merge
|
|
// If the function returned something, use that alone
|
|
data =
|
|
typeof ajaxData === 'function' && newData
|
|
? newData
|
|
: util.object.assignDeep(data, newData);
|
|
// Remove the data property as we've resolved it already and don't want
|
|
// jQuery to do it again (it is restored at the end of the function)
|
|
delete ajaxConfig.data;
|
|
}
|
|
var baseAjax = {
|
|
url: typeof ajaxConfig === 'string' ? ajaxConfig : '',
|
|
data: data,
|
|
success: callback,
|
|
dataType: 'json',
|
|
cache: false,
|
|
type: settings.serverMethod,
|
|
error: function (xhr, error) {
|
|
var ret = callbackFire(settings, null, 'xhr', [settings, null, settings.jqXHR], true);
|
|
if (ret.indexOf(false) === -1) {
|
|
if (error == 'parsererror') {
|
|
log(settings, 0, 'Invalid JSON response', 1);
|
|
}
|
|
else if (xhr.readyState === 4) {
|
|
log(settings, 0, 'Ajax error', 7);
|
|
}
|
|
}
|
|
processingDisplay(settings, false);
|
|
}
|
|
};
|
|
// If `ajax` option is an object, extend and override our default base
|
|
if (util.is.plainObject(ajaxConfig)) {
|
|
util.object.assign(baseAjax, ajaxConfig);
|
|
}
|
|
// Store the data submitted for the API
|
|
settings.ajaxData = data;
|
|
// Allow plug-ins and external processes to modify the data
|
|
callbackFire(settings, null, 'preXhr', [settings, data, baseAjax], true);
|
|
if (typeof ajaxConfig === 'function') {
|
|
// Is a function - let the caller define what needs to be done
|
|
settings.jqXHR = ajaxConfig.call(instance, data, callback, settings);
|
|
}
|
|
else if (ajaxConfig &&
|
|
typeof ajaxConfig !== 'string' &&
|
|
ajaxConfig.url === '') {
|
|
// No url, so don't load any data. Just apply an empty data array
|
|
// to the object for the callback.
|
|
var empty = {};
|
|
ajaxDataSrc(settings, empty, []);
|
|
callback(empty);
|
|
}
|
|
else {
|
|
// Object to extend the base settings
|
|
settings.jqXHR = util.ajax(baseAjax);
|
|
}
|
|
// Restore for next time around
|
|
if (ajaxData) {
|
|
ajaxConfig.data = ajaxData;
|
|
}
|
|
}
|
|
/**
|
|
* Update the table using an Ajax call
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns Block the table drawing or not
|
|
*/
|
|
function ajaxUpdate(settings) {
|
|
settings.drawCount++;
|
|
processingDisplay(settings, true);
|
|
buildAjax(settings, ajaxParameters(settings), function (json) {
|
|
ajaxUpdateDraw(settings, json);
|
|
});
|
|
}
|
|
function functionOrValue(val) {
|
|
return typeof val === 'function' ? 'function' : val.toString();
|
|
}
|
|
/**
|
|
* Build up the parameters in an object needed for a server-side processing
|
|
* request.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns Block the table drawing or not
|
|
*/
|
|
function ajaxParameters(settings) {
|
|
var columns = settings.columns, features = settings.features, searches = settings.searches, searchesFixed = settings.searchesFixed, colData = function (idx, prop) {
|
|
return typeof columns[idx][prop] === 'function'
|
|
? 'function'
|
|
: columns[idx][prop];
|
|
};
|
|
return {
|
|
draw: settings.drawCount,
|
|
columns: columns.map(function (column, i) {
|
|
return {
|
|
data: colData(i, 'data'),
|
|
name: column.name,
|
|
searchable: column.searchable,
|
|
orderable: column.orderable,
|
|
search: {
|
|
value: searches[i]
|
|
? functionOrValue(searches[i].search)
|
|
: '',
|
|
regex: searches[i] ? searches[i].regex : false,
|
|
fixed: searchesFixed[i]
|
|
? Object.keys(searchesFixed[i]).map(name => ({
|
|
name: name,
|
|
term: functionOrValue(searchesFixed[i][name].search)
|
|
}))
|
|
: []
|
|
}
|
|
};
|
|
}),
|
|
order: sortFlatten(settings).map(function (val) {
|
|
return {
|
|
column: val.col,
|
|
dir: val.dir,
|
|
name: colData(val.col, 'name')
|
|
};
|
|
}),
|
|
start: settings.displayStart,
|
|
length: features.paging ? settings.pageLength : -1,
|
|
search: {
|
|
value: functionOrValue(searches['*'].search),
|
|
regex: searches['*'].regex,
|
|
fixed: Object.keys(settings.searchesFixed['*']).map(name => ({
|
|
name: name,
|
|
term: functionOrValue(settings.searchesFixed['*'][name].search)
|
|
})),
|
|
groups: Object.keys(settings.searches)
|
|
.filter(c => c.includes(',')) // Limit to only multi-column subsets
|
|
.map(c => ({
|
|
columns: settings.searches[c].columns || [],
|
|
term: functionOrValue(settings.searches[c].search)
|
|
})),
|
|
groupsFixed: Object.keys(settings.searchesFixed)
|
|
.filter(c => c.includes(',')) // Limit to only multi-column subsets
|
|
.map(c => {
|
|
let searches = settings.searchesFixed[c];
|
|
return Object.keys(searches).map(n => ({
|
|
columns: searches[n].columns || [],
|
|
name: n,
|
|
term: functionOrValue(searches[n].search)
|
|
}));
|
|
})
|
|
.flat()
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Data the data from the server (nuking the old) and redraw the table
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param json json data return from the server.
|
|
*/
|
|
function ajaxUpdateDraw(settings, json) {
|
|
var data = ajaxDataSrc(settings, json, false);
|
|
var drawUnique = ajaxDataSrcParam(settings, 'draw', json);
|
|
var recordsTotal = ajaxDataSrcParam(settings, 'recordsTotal', json);
|
|
var recordsFiltered = ajaxDataSrcParam(settings, 'recordsFiltered', json);
|
|
var existingTypes = settings.columns.map(c => c.type).join(',');
|
|
if (drawUnique !== undefined) {
|
|
// Protect against out of sequence returns
|
|
if (drawUnique * 1 < settings.drawCount) {
|
|
return;
|
|
}
|
|
settings.drawCount = drawUnique * 1;
|
|
}
|
|
// No data in returned object, so rather than an array, we show an empty
|
|
// table
|
|
if (!data) {
|
|
data = [];
|
|
}
|
|
clearTable(settings);
|
|
settings.recordsTotal = parseInt(recordsTotal, 10);
|
|
settings.recordsDisplay = parseInt(recordsFiltered, 10);
|
|
for (var i = 0, iLen = data.length; i < iLen; i++) {
|
|
addData(settings, data[i]);
|
|
}
|
|
settings.display = settings.displayMaster.slice();
|
|
columnTypes(settings, existingTypes);
|
|
draw(settings, true);
|
|
initComplete(settings);
|
|
processingDisplay(settings, false);
|
|
}
|
|
/**
|
|
* Get the data from the JSON data source to use for drawing a table.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param json Data source object / array from the server
|
|
* @param write Array or object to write the data to
|
|
* @return Array of data to use
|
|
*/
|
|
function ajaxDataSrc(settings, json, write) {
|
|
var dataProp = 'data';
|
|
if (util.is.plainObject(settings.ajax) &&
|
|
settings.ajax.dataSrc !== undefined) {
|
|
// Could in inside a `dataSrc` object, or not!
|
|
var dataSrc = settings.ajax.dataSrc;
|
|
// string, function and object are valid types
|
|
if (typeof dataSrc === 'string' || typeof dataSrc === 'function') {
|
|
dataProp = dataSrc;
|
|
}
|
|
else if (dataSrc.data !== undefined) {
|
|
dataProp = dataSrc.data;
|
|
}
|
|
}
|
|
if (!write) {
|
|
if (dataProp === 'data') {
|
|
// If the default, then we still want to support the old style, and
|
|
// safely ignore it if possible
|
|
return json.aaData || json[dataProp];
|
|
}
|
|
return dataProp !== '' ? util.get(dataProp)(json) : json;
|
|
}
|
|
// set
|
|
util.set(dataProp)(json, write);
|
|
}
|
|
/**
|
|
* Very similar to ajaxDataSrc, but for the other SSP properties
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param param Target parameter
|
|
* @param json JSON data
|
|
* @returns Resolved value
|
|
*/
|
|
function ajaxDataSrcParam(settings, param, json) {
|
|
var dataSrc = util.is.plainObject(settings.ajax)
|
|
? settings.ajax.dataSrc // TODO
|
|
: null;
|
|
if (dataSrc && dataSrc[param]) {
|
|
// Get from custom location
|
|
return util.data.get(dataSrc[param])(json);
|
|
}
|
|
// else - Default behaviour
|
|
var old = '';
|
|
// Legacy support
|
|
if (param === 'draw') {
|
|
old = 'sEcho';
|
|
}
|
|
else if (param === 'recordsTotal') {
|
|
old = 'iTotalRecords';
|
|
}
|
|
else if (param === 'recordsFiltered') {
|
|
old = 'iTotalDisplayRecords';
|
|
}
|
|
return json[old] !== undefined ? json[old] : json[param];
|
|
}
|
|
|
|
const __filter_div = Dom.c('div').get(0);
|
|
const __filter_div_textContent = __filter_div.textContent !== undefined;
|
|
/**
|
|
* Filter the table using both the global filter and column based filtering
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function filterComplete(settings) {
|
|
settings.columns;
|
|
// In server-side processing all filtering is done by the server, so no
|
|
// point hanging around here
|
|
if (dataSource(settings) != 'ssp') {
|
|
// Check if any of the rows were invalidated
|
|
filterData(settings);
|
|
// Start from the full data set
|
|
settings.display = settings.displayMaster.slice();
|
|
// Column set filters first
|
|
util.object.each(settings.searches, (key, s) => {
|
|
filter(settings.display, settings, s.search, s);
|
|
});
|
|
// Fixed (named) filters next
|
|
util.object.each(settings.searchesFixed, function (columns) {
|
|
util.object.each(settings.searchesFixed[columns], function (name, s) {
|
|
filter(settings.display, settings, s.search, s);
|
|
});
|
|
});
|
|
// And finally legacy global filtering
|
|
filterCustom(settings);
|
|
}
|
|
// Tell the draw function we have been filtering
|
|
settings.wasFiltered = true;
|
|
callbackFire(settings, null, 'search', [settings]);
|
|
}
|
|
/**
|
|
* Apply custom filtering functions
|
|
*
|
|
* This is legacy now that we have named functions, but it is widely used
|
|
* from 1.x, so it is not yet deprecated.
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function filterCustom(settings) {
|
|
let filters = ext.search;
|
|
let displayRows = settings.display;
|
|
let row, rowIdx;
|
|
for (let i = 0, iLen = filters.length; i < iLen; i++) {
|
|
let rows = [];
|
|
// Loop over each row and see if it should be included
|
|
for (let j = 0, jen = displayRows.length; j < jen; j++) {
|
|
rowIdx = displayRows[j];
|
|
row = settings.data[rowIdx];
|
|
if (row &&
|
|
filters[i](settings, row.searchCellCache, rowIdx, row.data, j)) {
|
|
rows.push(rowIdx);
|
|
}
|
|
}
|
|
// So the array reference doesn't break set the results into the
|
|
// existing array
|
|
displayRows.length = 0;
|
|
arrayApply(displayRows, rows);
|
|
}
|
|
}
|
|
/**
|
|
* Filter the data table based on user input and draw the table
|
|
*
|
|
* @param searchRows
|
|
* @param settings
|
|
* @param input
|
|
* @param options
|
|
* @returns
|
|
*/
|
|
function filter(searchRows, settings, input, options) {
|
|
if (input === '') {
|
|
return;
|
|
}
|
|
let i = 0;
|
|
let matched = [];
|
|
// Search term can be a function, regex or string - if a string we apply our
|
|
// smart filtering regex (assuming the options require that)
|
|
let searchFunc = typeof input === 'function' ? input : null;
|
|
let rpSearch = input instanceof RegExp
|
|
? input
|
|
: searchFunc
|
|
? null
|
|
: filterCreateSearch(input, options);
|
|
let columns = options.columns
|
|
? options.columns
|
|
: util.array.range(settings.columns.length);
|
|
// Then for each row, does the test pass. If not, lop the row from the array
|
|
for (i = 0; i < searchRows.length; i++) {
|
|
let row = settings.data[searchRows[i]];
|
|
if (row) {
|
|
// Get the data array based on the columns to include in the search
|
|
let data = util.array.selectiveJoin(row.searchCellCache, columns);
|
|
// Run the search action
|
|
if ((searchFunc &&
|
|
searchFunc(data, row.data, searchRows[i], columns.length === 1 ? columns[0] : columns // compat
|
|
)) ||
|
|
(rpSearch && typeof data === 'string' && rpSearch.test(data))) {
|
|
matched.push(searchRows[i]);
|
|
}
|
|
}
|
|
}
|
|
// Mutate the searchRows array
|
|
searchRows.length = matched.length;
|
|
for (i = 0; i < matched.length; i++) {
|
|
searchRows[i] = matched[i];
|
|
}
|
|
}
|
|
/**
|
|
* Build a regular expression object suitable for searching a table
|
|
*/
|
|
function filterCreateSearch(searchIn, inOpts) {
|
|
let not = [];
|
|
let options = Object.assign({}, {
|
|
boundary: false,
|
|
caseInsensitive: true,
|
|
exact: false,
|
|
regex: false,
|
|
smart: true
|
|
}, inOpts);
|
|
let search = typeof searchIn !== 'string' ? searchIn.toString() : searchIn;
|
|
// Remove diacritics if normalize is set up to do so
|
|
search = util.diacritics(search);
|
|
if (options.exact) {
|
|
return new RegExp('^' + util.escapeRegex(search) + '$', options.caseInsensitive ? 'i' : '');
|
|
}
|
|
search = options.regex ? search : util.escapeRegex(search);
|
|
if (options.smart) {
|
|
/* For smart filtering we want to allow the search to work regardless of
|
|
* word order. We also want double quoted text to be preserved, so word
|
|
* order is important - a la google. And a negative look around for
|
|
* finding rows which don't contain a given string.
|
|
*
|
|
* So this is the sort of thing we want to generate:
|
|
*
|
|
* ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$
|
|
*/
|
|
let parts = search.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g) || [
|
|
''
|
|
];
|
|
let a = parts.map(function (word) {
|
|
let negative = false;
|
|
let m;
|
|
// Determine if it is a "does not include"
|
|
if (word.charAt(0) === '!') {
|
|
negative = true;
|
|
word = word.substring(1);
|
|
}
|
|
// Strip the quotes from around matched phrases
|
|
if (word.charAt(0) === '"') {
|
|
m = word.match(/^"(.*)"$/);
|
|
word = m ? m[1] : word;
|
|
}
|
|
else if (word.charAt(0) === '\u201C') {
|
|
// Smart quote match (iPhone users)
|
|
m = word.match(/^\u201C(.*)\u201D$/);
|
|
word = m ? m[1] : word;
|
|
}
|
|
// For our "not" case, we need to modify the string that is
|
|
// allowed to match at the end of the expression.
|
|
if (negative) {
|
|
if (word.length > 1) {
|
|
not.push('(?!' + word + ')');
|
|
}
|
|
word = '';
|
|
}
|
|
return word.replace(/"/g, '');
|
|
});
|
|
let match = not.length ? not.join('') : '';
|
|
let boundary = options.boundary ? '\\b' : '';
|
|
search =
|
|
'^(?=.*?' +
|
|
boundary +
|
|
a.join(')(?=.*?' + boundary) +
|
|
')(' +
|
|
match +
|
|
'.)*$';
|
|
}
|
|
return new RegExp(search, options.caseInsensitive ? 'i' : '');
|
|
}
|
|
// Update the filtering data for each row if needed (by invalidation or first
|
|
// run)
|
|
function filterData(settings) {
|
|
let columns = settings.columns;
|
|
let data = settings.data;
|
|
let column;
|
|
let j, jen, cellData, row;
|
|
let wasInvalidated = false;
|
|
for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {
|
|
if (!data[rowIdx]) {
|
|
continue;
|
|
}
|
|
row = data[rowIdx];
|
|
if (row && !row.searchCellCache) {
|
|
const rowFilterData = [];
|
|
for (j = 0, jen = columns.length; j < jen; j++) {
|
|
column = columns[j];
|
|
if (column.searchable) {
|
|
cellData = getCellData(settings, rowIdx, j, 'filter');
|
|
// Search in DataTables is string based
|
|
if (cellData === null) {
|
|
cellData = '';
|
|
}
|
|
if (typeof cellData !== 'string' && cellData.toString) {
|
|
cellData = cellData.toString();
|
|
}
|
|
}
|
|
else {
|
|
cellData = '';
|
|
}
|
|
// If it looks like there is an HTML entity in the string,
|
|
// attempt to decode it so sorting works as expected. Note that
|
|
// we could use a single line of jQuery to do this, but the DOM
|
|
// method used here is much faster
|
|
// https://jsperf.com/html-decode
|
|
if (cellData.indexOf && cellData.indexOf('&') !== -1) {
|
|
__filter_div.innerHTML = cellData;
|
|
cellData = __filter_div_textContent
|
|
? __filter_div.textContent
|
|
: __filter_div.innerText;
|
|
}
|
|
if (cellData.replace) {
|
|
cellData = cellData.replace(/[\r\n\u2028]/g, '');
|
|
}
|
|
rowFilterData.push(cellData);
|
|
}
|
|
row.searchCellCache = rowFilterData;
|
|
row.searchRowCache = rowFilterData.join(' ');
|
|
wasInvalidated = true;
|
|
}
|
|
}
|
|
return wasInvalidated;
|
|
}
|
|
|
|
/**
|
|
* Render and cache a row's display data for the columns, if required
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rowIdx Row index
|
|
* @returns Array with display information
|
|
*/
|
|
function getRowDisplay(settings, rowIdx) {
|
|
var rowModal = settings.data[rowIdx];
|
|
var columns = settings.columns;
|
|
if (!rowModal) {
|
|
return [];
|
|
}
|
|
if (!rowModal.displayData) {
|
|
// Need to render and cache
|
|
rowModal.displayData = [];
|
|
for (var colIdx = 0, len = columns.length; colIdx < len; colIdx++) {
|
|
rowModal.displayData.push(getCellData(settings, rowIdx, colIdx, 'display'));
|
|
}
|
|
}
|
|
return rowModal.displayData;
|
|
}
|
|
/**
|
|
* Create a new TR element (and it's TD children) for a row
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param rowIdx Row to consider
|
|
* @param trIn TR element to add to the table - optional. If not given,
|
|
* DataTables will create a row automatically
|
|
* @param tds Array of TD|TH elements for the row - must be given if trIn is.
|
|
*/
|
|
function createTr(settings, rowIdx, trIn, tds) {
|
|
var row = settings.data[rowIdx], cells = [], tr, td, column, i, iLen, create, trClass = settings.classes.tbody.row;
|
|
if (row && row.tr === null) {
|
|
let rowData = row.data;
|
|
tr = trIn || document.createElement('tr');
|
|
row.tr = tr;
|
|
row.cells = cells;
|
|
Dom.s(tr).classAdd(trClass);
|
|
// Use a private property on the node to allow reserve mapping from the node
|
|
// to the aoData array for fast look up
|
|
tr._DT_RowIndex = rowIdx;
|
|
// Special parameters can be given by the data source to be used on the
|
|
// row
|
|
rowAttributes(settings, row);
|
|
/* Process each column */
|
|
for (i = 0, iLen = settings.columns.length; i < iLen; i++) {
|
|
column = settings.columns[i];
|
|
create = trIn && tds && tds[i] ? false : true;
|
|
td = create
|
|
? document.createElement(column.cellType)
|
|
: tds[i];
|
|
if (!td) {
|
|
log(settings, 0, 'Incorrect column count', 18);
|
|
}
|
|
td._DT_CellIndex = {
|
|
row: rowIdx,
|
|
column: i
|
|
};
|
|
cells.push(td);
|
|
var display = getRowDisplay(settings, rowIdx);
|
|
// Need to create the HTML if new, or if a rendering function is
|
|
// defined
|
|
if (create ||
|
|
((column.render || column.data !== i) &&
|
|
(!util.is.plainObject(column.data) ||
|
|
(column.data &&
|
|
column.data._ !== i + '.display')))) {
|
|
writeCell(td, display[i]);
|
|
}
|
|
// column class
|
|
Dom.s(td).classAdd(column.className);
|
|
// Visibility - add or remove as required
|
|
if (column.visible && create) {
|
|
tr.appendChild(td);
|
|
}
|
|
else if (!column.visible && !create) {
|
|
td.parentNode.removeChild(td);
|
|
}
|
|
if (column.createdCell) {
|
|
column.createdCell.call(settings.instance, td, getCellData(settings, rowIdx, i), rowData, rowIdx, i);
|
|
}
|
|
}
|
|
callbackFire(settings, 'rowCreated', 'row-created', [
|
|
tr,
|
|
rowData,
|
|
rowIdx,
|
|
cells
|
|
]);
|
|
}
|
|
else if (row) {
|
|
Dom.s(row.tr).classAdd(trClass);
|
|
}
|
|
}
|
|
/**
|
|
* Add attributes to a row based on the special `DT_*` parameters in a data
|
|
* source object.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param row Row object for the row to be modified
|
|
*/
|
|
function rowAttributes(settings, row) {
|
|
var tr = row.tr;
|
|
var data = row.data;
|
|
if (tr) {
|
|
var id = settings.rowIdFn(data);
|
|
if (id) {
|
|
tr.id = id;
|
|
}
|
|
if (data.DT_RowClass) {
|
|
// Remove any classes added by DT_RowClass before
|
|
var a = data.DT_RowClass.split(' ');
|
|
row.addedClasses = row.addedClasses
|
|
? util.unique(row.addedClasses.concat(a))
|
|
: a;
|
|
Dom.s(tr)
|
|
.classRemove(row.addedClasses.join(' '))
|
|
.classAdd(data.DT_RowClass);
|
|
}
|
|
if (data.DT_RowAttr) {
|
|
Dom.s(tr).attr(data.DT_RowAttr);
|
|
}
|
|
if (data.DT_RowData) {
|
|
Dom.s(tr).data(data.DT_RowData);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Create the HTML header for the table
|
|
*
|
|
* @param settings DataTable instance
|
|
* @param side If the header or footer should be used
|
|
* @returns
|
|
*/
|
|
function buildHead(settings, side) {
|
|
let classes = settings.classes;
|
|
let columns = settings.columns;
|
|
let i, iLen, row;
|
|
let target = Dom.s(side === 'header' ? settings.thead : settings.tfoot);
|
|
let titleProp = side === 'header' ? 'title' : side;
|
|
// Footer might be defined
|
|
if (!target) {
|
|
return;
|
|
}
|
|
// If no cells yet and we have content for them, then create
|
|
if (side === 'header' ||
|
|
util.array.pluck(settings.columns, titleProp).join('')) {
|
|
row = target.find('tr');
|
|
// Add a row if needed
|
|
if (!row.count()) {
|
|
row = Dom.c('tr').appendTo(target);
|
|
}
|
|
// Add the number of cells needed to make up to the number of columns
|
|
if (row.count() === 1) {
|
|
let cellCount = 0;
|
|
row.find('td, th').each(el => {
|
|
cellCount += el.colSpan;
|
|
});
|
|
for (i = cellCount, iLen = columns.length; i < iLen; i++) {
|
|
Dom.c('th')
|
|
.html(columns[i][titleProp] || '')
|
|
.appendTo(row);
|
|
}
|
|
}
|
|
}
|
|
let detected = detectHeader(settings, target.get(0), true);
|
|
if (side === 'header') {
|
|
settings.header = detected;
|
|
target.find('tr').classAdd(classes.thead.row);
|
|
}
|
|
else {
|
|
settings.footer = detected;
|
|
target.find('tr').classAdd(classes.tfoot.row);
|
|
}
|
|
// Every cell needs to be passed through the renderer
|
|
target
|
|
.children('tr')
|
|
.children('th, td')
|
|
.each(el => {
|
|
// Should just be able to do `renderer(settings, side)` here but
|
|
// Typescript doesn't like it, despite it already being constrained!
|
|
let runner = side === 'header'
|
|
? renderer(settings, 'header')
|
|
: renderer(settings, 'footer');
|
|
runner(settings, Dom.s(el), classes);
|
|
});
|
|
}
|
|
/**
|
|
* Build a layout structure for a header or footer
|
|
*
|
|
* @param settings DataTables settings
|
|
* @param source Source layout array
|
|
* @param incColumns What columns should be included
|
|
* @returns Layout array in column index order
|
|
*/
|
|
function headerLayout(settings, source, incColumns) {
|
|
var row, column, cell;
|
|
var local = [];
|
|
var structure = [];
|
|
var columns = settings.columns;
|
|
var columnCount = columns.length;
|
|
var rowspan, colspan;
|
|
if (!source) {
|
|
return;
|
|
}
|
|
// Default is to work on only visible columns
|
|
if (!incColumns) {
|
|
incColumns = util.array.range(columnCount).filter(function (idx) {
|
|
return columns[idx].visible;
|
|
});
|
|
}
|
|
// Make a copy of the master layout array, but with only the columns we want
|
|
for (row = 0; row < source.length; row++) {
|
|
// Remove any columns we haven't selected
|
|
local[row] = source[row].slice().filter(function (c, i) {
|
|
return incColumns.includes(i);
|
|
});
|
|
// Prep the structure array - it needs an element for each row
|
|
structure.push([]);
|
|
}
|
|
for (row = 0; row < local.length; row++) {
|
|
for (column = 0; column < local[row].length; column++) {
|
|
rowspan = 1;
|
|
colspan = 1;
|
|
// Check to see if there is already a cell (row/colspan) covering
|
|
// our target insert point. If there is, then there is nothing to
|
|
// do.
|
|
if (structure[row][column] === undefined) {
|
|
cell = local[row][column].cell;
|
|
// Expand for rowspan
|
|
while (local[row + rowspan] !== undefined &&
|
|
local[row][column].cell == local[row + rowspan][column].cell) {
|
|
structure[row + rowspan][column] = null;
|
|
rowspan++;
|
|
}
|
|
// And for colspan
|
|
while (local[row][column + colspan] !== undefined &&
|
|
local[row][column].cell == local[row][column + colspan].cell) {
|
|
// Which also needs to go over rows
|
|
for (var k = 0; k < rowspan; k++) {
|
|
structure[row + k][column + colspan] = null;
|
|
}
|
|
colspan++;
|
|
}
|
|
var titleSpan = Dom.s(cell).find('.dt-column-title');
|
|
structure[row][column] = {
|
|
cell: cell,
|
|
colspan: colspan,
|
|
rowspan: rowspan,
|
|
title: titleSpan.count()
|
|
? titleSpan.html()
|
|
: Dom.s(cell).html()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return structure;
|
|
}
|
|
/**
|
|
* Draw the header (or footer) element based on the column visibility states.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param source Layout array from detectHeader
|
|
*/
|
|
function drawHead(settings, source) {
|
|
let layout = headerLayout(settings, source);
|
|
let tr;
|
|
if (!layout) {
|
|
return;
|
|
}
|
|
for (let row = 0; row < source.length; row++) {
|
|
tr = source[row].row;
|
|
// All cells are going to be replaced, so empty out the row
|
|
if (tr) {
|
|
Dom.s(tr).detachChildren();
|
|
}
|
|
for (let column = 0; column < layout[row].length; column++) {
|
|
let point = layout[row][column];
|
|
if (point) {
|
|
Dom.s(point.cell)
|
|
.appendTo(tr)
|
|
.attr('rowspan', point.rowspan)
|
|
.attr('colspan', point.colspan);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Insert the required TR nodes into the table for display
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param ajaxComplete true after ajax call to complete rendering
|
|
*/
|
|
function draw(settings, ajaxComplete) {
|
|
// Allow for state saving and a custom start position
|
|
setStartPosition(settings);
|
|
// Provide a pre-callback function which can be used to cancel the draw is
|
|
// false is returned
|
|
var aPreDraw = callbackFire(settings, 'preDraw', 'preDraw', [settings]);
|
|
if (aPreDraw.indexOf(false) !== -1) {
|
|
processingDisplay(settings, false);
|
|
return;
|
|
}
|
|
var rowEls = [];
|
|
var rowCount = 0;
|
|
var isServerSide = dataSource(settings) == 'ssp';
|
|
var display = settings.display;
|
|
var start = settings.displayStart;
|
|
var end = displayEnd(settings);
|
|
var columns = settings.columns;
|
|
var body = Dom.s(settings.tbody);
|
|
settings.doingDraw = true;
|
|
/* Server-side processing draw intercept */
|
|
if (settings.deferLoading) {
|
|
settings.deferLoading = false;
|
|
settings.drawCount++;
|
|
processingDisplay(settings, false);
|
|
}
|
|
else if (!isServerSide) {
|
|
settings.drawCount++;
|
|
}
|
|
else if (!settings.destroying && !ajaxComplete) {
|
|
// Show loading message for server-side processing
|
|
if (settings.drawCount === 0) {
|
|
body.empty().append(_emptyRow(settings));
|
|
}
|
|
ajaxUpdate(settings);
|
|
return;
|
|
}
|
|
if (display.length !== 0) {
|
|
var iStart = isServerSide ? 0 : start;
|
|
var iEnd = isServerSide ? settings.data.length : end;
|
|
for (var j = iStart; j < iEnd; j++) {
|
|
var dataIdx = display[j];
|
|
var data = settings.data[dataIdx];
|
|
// Row has been deleted - can't be displayed
|
|
if (data === null) {
|
|
continue;
|
|
}
|
|
// Row node hasn't been created yet
|
|
if (data.tr === null) {
|
|
createTr(settings, dataIdx);
|
|
}
|
|
var nRow = data.tr;
|
|
// Add various classes as needed
|
|
for (var i = 0; i < columns.length; i++) {
|
|
var col = columns[i];
|
|
var td = data.cells[i];
|
|
Dom.s(td)
|
|
.classAdd(col.type ? ext.type.className[col.type] : null) // auto class
|
|
.classAdd(settings.classes.tbody.cell); // all cells
|
|
}
|
|
// Row callback functions - might want to manipulate the row
|
|
// rowCount and j are not currently documented. Are they at all
|
|
// useful?
|
|
callbackFire(settings, 'row', null, [
|
|
nRow,
|
|
data.data,
|
|
rowCount,
|
|
j,
|
|
dataIdx
|
|
]);
|
|
rowEls.push(nRow);
|
|
rowCount++;
|
|
}
|
|
}
|
|
else {
|
|
rowEls[0] = _emptyRow(settings);
|
|
}
|
|
/* Header and footer callbacks */
|
|
callbackFire(settings, 'header', 'header', [
|
|
Dom.s(settings.thead).children('tr').get(0),
|
|
getDataMaster(settings),
|
|
start,
|
|
end,
|
|
display
|
|
]);
|
|
callbackFire(settings, 'footer', 'footer', [
|
|
Dom.s(settings.tfoot).children('tr').get(0),
|
|
getDataMaster(settings),
|
|
start,
|
|
end,
|
|
display
|
|
]);
|
|
body.detachChildren().append(rowEls);
|
|
// Empty table needs a specific class
|
|
Dom.s(settings.tableWrapper).classToggle('dt-empty-footer', Dom.s(settings.tfoot).find('tr').count() === 0);
|
|
// Call all required callback functions for the end of a draw
|
|
callbackFire(settings, 'draw', 'draw', [settings], true);
|
|
// Draw is complete, sorting and filtering must be as well
|
|
settings.wasOrdered = false;
|
|
settings.wasFiltered = false;
|
|
settings.doingDraw = false;
|
|
}
|
|
/**
|
|
* Redraw the table - taking account of the various features which are enabled
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param holdPosition Keep the current paging position. By default the paging
|
|
* is reset to the first page
|
|
* @param recompute Indicate if a rebuild of sort and filter should happen
|
|
*/
|
|
function reDraw(settings, holdPosition, recompute) {
|
|
let features = settings.features, doSort = features.ordering, doFilter = features.searching;
|
|
if (recompute === undefined || recompute === true) {
|
|
// Resolve any column types that are unknown due to addition or
|
|
// invalidation
|
|
columnTypes(settings);
|
|
columnWidths(settings);
|
|
if (doSort) {
|
|
sort(settings);
|
|
}
|
|
if (doFilter) {
|
|
filterComplete(settings);
|
|
}
|
|
else {
|
|
// No filtering, so we want to just use the display master
|
|
settings.display = settings.displayMaster.slice();
|
|
}
|
|
}
|
|
if (holdPosition !== true) {
|
|
settings.displayStart = 0;
|
|
}
|
|
else {
|
|
// Keep position, but make sure that there is actually data to display,
|
|
// otherwise we need to rewind a bit (e.g. if rows were deleted)
|
|
lengthOverflow(settings);
|
|
}
|
|
// Let any modules know about the draw hold position state (used by
|
|
// scrolling internally)
|
|
settings.drawHold = holdPosition;
|
|
draw(settings);
|
|
settings.api.one('draw', function () {
|
|
settings.drawHold = false;
|
|
});
|
|
}
|
|
/**
|
|
* Table is empty - create a row with an empty message in it
|
|
*
|
|
* @param settings DataTables context
|
|
*/
|
|
function _emptyRow(settings) {
|
|
let lang = settings.language;
|
|
let zero = lang.zeroRecords;
|
|
let dataSrc = dataSource(settings);
|
|
// Make use of the fact that settings.json is only set once the initial data
|
|
// has been loaded. Show loading when that isn't the case
|
|
if ((dataSrc === 'ssp' || dataSrc === 'ajax') && !settings.json) {
|
|
zero = lang.loadingRecords;
|
|
}
|
|
else if (lang.emptyTable && recordsTotal(settings) === 0) {
|
|
zero = lang.emptyTable;
|
|
}
|
|
return Dom
|
|
.c('tr')
|
|
.append(Dom
|
|
.c('td')
|
|
.attr('colSpan', visibleColumns(settings))
|
|
.classAdd(settings.classes.empty.row)
|
|
.html(zero))
|
|
.get(0);
|
|
}
|
|
/**
|
|
* Use the DOM source to create up an array of header cells. The idea here is to
|
|
* create a layout grid (array) of rows x columns, which contains a reference to
|
|
* the cell at that point in the grid (regardless of col/rowspan), such that any
|
|
* column / row could be removed and the new grid constructed.
|
|
*
|
|
* @param settings DataTables context
|
|
* @param thead thead / tbody element
|
|
* @param write If cells should be written (if required)
|
|
* @returns Calculated layout array
|
|
*/
|
|
function detectHeader(settings, thead, write) {
|
|
let columns = settings.columns;
|
|
let rows = Dom.s(thead).children('tr');
|
|
let row, loopCell;
|
|
let i, k, l, len, shifted, column, colspan, rowspan;
|
|
let titleRow = settings.titleRow;
|
|
let isHeader = thead && thead.nodeName.toLowerCase() === 'thead';
|
|
let layout = [];
|
|
let isUnique;
|
|
let shift = function (a, b, j) {
|
|
let d = a[b];
|
|
while (d[j]) {
|
|
j++;
|
|
}
|
|
return j;
|
|
};
|
|
// We know how many rows there are in the layout - so prep it
|
|
for (i = 0, len = rows.count(); i < len; i++) {
|
|
layout.push([]);
|
|
}
|
|
for (i = 0, len = rows.count(); i < len; i++) {
|
|
row = rows.get(i);
|
|
column = 0;
|
|
// For every cell in the row..
|
|
loopCell = row.firstChild;
|
|
while (loopCell) {
|
|
if (loopCell.nodeName.toUpperCase() == 'TD' ||
|
|
loopCell.nodeName.toUpperCase() == 'TH') {
|
|
let cell = Dom.s(loopCell);
|
|
let cols = [];
|
|
// Get the col and rowspan attributes from the DOM and sanitise
|
|
// them
|
|
colspan = parseInt(cell.attr('colspan') || '1') || 1;
|
|
rowspan = parseInt(cell.attr('rowspan') || '1') || 1;
|
|
colspan =
|
|
!colspan || colspan === 0 || colspan === 1 ? 1 : colspan;
|
|
rowspan =
|
|
!rowspan || rowspan === 0 || rowspan === 1 ? 1 : rowspan;
|
|
// There might be colspan cells already in this row, so shift
|
|
// our target accordingly
|
|
shifted = shift(layout, i, column);
|
|
// Cache calculation for unique columns
|
|
isUnique = colspan === 1 ? true : false;
|
|
// Perform header setup
|
|
if (write) {
|
|
if (isUnique) {
|
|
// Allow column options to be set from HTML attributes
|
|
columnOptions(settings, shifted, escapeObject(cell.data()));
|
|
// Get the width for the column. This can be defined
|
|
// from the width attribute, style attribute or
|
|
// `columns.width` option
|
|
let columnDef = columns[shifted];
|
|
let width = cell.attr('width') || null;
|
|
let t = cell
|
|
.get(0)
|
|
.style.width.match(/width:\s*(\d+[pxem%]+)/);
|
|
if (t) {
|
|
width = t[1];
|
|
}
|
|
columnDef.widthOrig = columnDef.width || width;
|
|
if (isHeader) {
|
|
// Column title handling - can be user set, or read
|
|
// from the DOM This happens before the render, so
|
|
// the original is still in place
|
|
if (columnDef.title !== null &&
|
|
!columnDef.autoTitle) {
|
|
if ((titleRow === true && i === 0) || // top row
|
|
(titleRow === false &&
|
|
i === rows.count() - 1) || // bottom row
|
|
titleRow === i || // specific row
|
|
titleRow === null) {
|
|
cell.html(columnDef.title);
|
|
}
|
|
}
|
|
if (!columnDef.title && isUnique) {
|
|
columnDef.title = util.string.stripHtml(cell.html());
|
|
columnDef.autoTitle = true;
|
|
}
|
|
}
|
|
else {
|
|
// Footer specific operations
|
|
if (columnDef.footer) {
|
|
cell.html(columnDef.footer);
|
|
}
|
|
}
|
|
// Fall back to the aria-label attribute on the table
|
|
// header if no ariaTitle is provided.
|
|
if (!columnDef.ariaTitle) {
|
|
columnDef.ariaTitle =
|
|
cell.attr('aria-label') || columnDef.title;
|
|
}
|
|
// Column specific class names
|
|
if (columnDef.className) {
|
|
cell.classAdd(columnDef.className);
|
|
}
|
|
}
|
|
// Wrap the column title so we can write to it in future
|
|
if (cell.find('div.dt-column-title').count() === 0) {
|
|
Dom.c('div')
|
|
.classAdd('dt-column-title')
|
|
.append(Array.from(cell.get(0).childNodes))
|
|
.appendTo(cell);
|
|
}
|
|
if (settings.orderIndicators &&
|
|
isHeader &&
|
|
cell.filter(':not([data-dt-order=disable])').count() !==
|
|
0 &&
|
|
cell.parent(':not([data-dt-order=disable])').count() !==
|
|
0 &&
|
|
cell.find('div.dt-column-order').count() === 0) {
|
|
Dom.c('div')
|
|
.classAdd('dt-column-order')
|
|
.appendTo(cell);
|
|
}
|
|
// We need to wrap the elements in the header in another
|
|
// element to use flexbox layout for those elements
|
|
var headerFooter = isHeader ? 'header' : 'footer';
|
|
if (cell.find('div.dt-column-' + headerFooter).count() ===
|
|
0) {
|
|
Dom.c('div')
|
|
.classAdd('dt-column-' + headerFooter)
|
|
.append(Array.from(cell.get(0).childNodes))
|
|
.appendTo(cell);
|
|
}
|
|
}
|
|
// If there is col / rowspan, copy the information into the
|
|
// layout grid
|
|
for (l = 0; l < colspan; l++) {
|
|
for (k = 0; k < rowspan; k++) {
|
|
layout[i + k][shifted + l] = {
|
|
cell: cell.get(0),
|
|
unique: isUnique
|
|
};
|
|
layout[i + k].row = row;
|
|
}
|
|
cols.push(shifted + l);
|
|
}
|
|
// Assign an attribute so spanning cells can still be identified
|
|
// as belonging to a column
|
|
cell.attr('data-dt-column', util.unique(cols).join(','));
|
|
}
|
|
loopCell = loopCell.nextSibling;
|
|
}
|
|
}
|
|
return layout;
|
|
}
|
|
/**
|
|
* Set the start position for draw
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function setStartPosition(settings) {
|
|
var bServerSide = dataSource(settings) == 'ssp';
|
|
var iInitDisplayStart = settings.displayStartInit;
|
|
// Check and see if we have an initial draw position from state saving
|
|
if (iInitDisplayStart !== undefined && iInitDisplayStart !== -1) {
|
|
settings.displayStart = bServerSide
|
|
? iInitDisplayStart
|
|
: iInitDisplayStart >= recordsDisplay(settings)
|
|
? 0
|
|
: iInitDisplayStart;
|
|
settings.displayStartInit = -1;
|
|
}
|
|
}
|
|
/**
|
|
* Get the number of records in the current record set, before filtering
|
|
*
|
|
* @param ctx DataTables settings object
|
|
*/
|
|
function recordsTotal(ctx) {
|
|
return dataSource(ctx) == 'ssp'
|
|
? ctx.recordsTotal * 1
|
|
: ctx.displayMaster.length;
|
|
}
|
|
/**
|
|
* Get the number of records in the current record set, after filtering
|
|
*
|
|
* @param ctx DataTables settings object
|
|
*/
|
|
function recordsDisplay(ctx) {
|
|
return dataSource(ctx) == 'ssp'
|
|
? ctx.recordsDisplay * 1
|
|
: ctx.display.length;
|
|
}
|
|
/**
|
|
* Get the display end point - display index
|
|
*
|
|
* @param ctx DataTables settings object
|
|
*/
|
|
function displayEnd(ctx) {
|
|
var len = ctx.pageLength, start = ctx.displayStart, calc = start + len, records = ctx.display.length, features = ctx.features, paginate = features.paging;
|
|
if (features.serverSide) {
|
|
return paginate === false || len === -1
|
|
? start + records
|
|
: Math.min(start + len, ctx.recordsDisplay);
|
|
}
|
|
else {
|
|
return !paginate || calc > records || len === -1 ? records : calc;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log an error message
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param level log error messages, or display them to the user
|
|
* @param msg error message
|
|
* @param tn Technical note id to get more information about the error.
|
|
*/
|
|
function log(ctx, level, msg, tn) {
|
|
msg =
|
|
'DataTables warning: ' +
|
|
(ctx ? 'table id=' + ctx.tableId + ' - ' : '') +
|
|
msg;
|
|
if (tn) {
|
|
msg +=
|
|
'. For more information about this error, please see ' +
|
|
'https://datatables.net/tn/' +
|
|
tn;
|
|
}
|
|
{
|
|
// Backwards compatibility pre 1.10
|
|
var type = ext.sErrMode || ext.errMode;
|
|
if (ctx) {
|
|
callbackFire(ctx, null, 'dt-error', [ctx, tn, msg], true);
|
|
}
|
|
if (type == 'alert') {
|
|
alert(msg);
|
|
}
|
|
else if (type == 'throw') {
|
|
throw new Error(msg);
|
|
}
|
|
else if (typeof type == 'function') {
|
|
type(ctx, tn, msg);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* See if a property is defined on one object, if so assign it to the other
|
|
* object
|
|
*
|
|
* @param ret target object
|
|
* @param src source object
|
|
* @param name property
|
|
* @param mappedName name to map too - optional, name used if not given
|
|
*/
|
|
function map(ret, src, name, mappedName) {
|
|
if (Array.isArray(name)) {
|
|
for (let i = 0; i < name.length; i++) {
|
|
let val = name[i];
|
|
if (Array.isArray(val)) {
|
|
map(ret, src, val[0], val[1]);
|
|
}
|
|
else {
|
|
map(ret, src, val);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (mappedName === undefined) {
|
|
mappedName = name;
|
|
}
|
|
if (src[name] !== undefined) {
|
|
ret[mappedName] = src[name];
|
|
}
|
|
}
|
|
/**
|
|
* Bind an event handler to allow a click or return key to activate the callback.
|
|
* This is good for accessibility since a return on the keyboard will have the
|
|
* same effect as a click, if the element has focus.
|
|
*
|
|
* @param n Element to bind the action to
|
|
* @param selector Selector (for delegated events)
|
|
* @param fn Callback function for when the event is triggered
|
|
*/
|
|
function bindAction(n, selector, fn) {
|
|
Dom.s(n)
|
|
.on('click.DT', selector, function (e) {
|
|
fn(e);
|
|
})
|
|
.on('keypress.DT', selector, function (e) {
|
|
if (e.which === 13) {
|
|
e.preventDefault();
|
|
fn(e);
|
|
}
|
|
})
|
|
.on('selectstart.DT', selector, function () {
|
|
// Don't want a double click resulting in text selection
|
|
return false;
|
|
});
|
|
}
|
|
/**
|
|
* Register a callback function. Easily allows a callback function to be added
|
|
* to an array store of callback functions that can then all be called together.
|
|
*
|
|
* @param settings dataTables settings object
|
|
* @param store Name of the array storage for the callbacks in settings
|
|
* @param fn Function to be called back
|
|
*/
|
|
function callbackReg(ctx, store, fn) {
|
|
if (fn) {
|
|
ctx.callbacks[store].push(fn);
|
|
}
|
|
}
|
|
/**
|
|
* Fire callback functions and trigger events. Note that the loop over the
|
|
* callback array store is done backwards! Further note that you do not want to
|
|
* fire off triggers in time sensitive applications (for example cell creation)
|
|
* as its slow.
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param callbackArr Name of the array storage for the callbacks in the context
|
|
* @param eventName Name of the custom event to trigger. If null no trigger is
|
|
* fired
|
|
* @param args Array of arguments to pass to the callback function / trigger
|
|
* @param bubbles True if the event should bubble
|
|
*/
|
|
function callbackFire(ctx, callbackArr, eventName, args, bubbles = false) {
|
|
var ret = [];
|
|
if (callbackArr) {
|
|
ret = ctx.callbacks[callbackArr]
|
|
.slice()
|
|
.reverse()
|
|
.map(function (val) {
|
|
return val.apply(ctx.instance, args);
|
|
});
|
|
}
|
|
if (eventName !== null) {
|
|
let table = Dom.s(ctx.table);
|
|
let result = table.trigger(eventName + '.dt', bubbles, args, {
|
|
dt: ctx.api
|
|
});
|
|
// If not yet attached to the document, trigger the event
|
|
// on the body directly to sort of simulate the bubble
|
|
if (bubbles && table.closest('body').count() === 0) {
|
|
Dom.s('body').trigger(eventName + '.dt', bubbles, args, {
|
|
dt: ctx.api
|
|
});
|
|
}
|
|
ret.push(result[0]);
|
|
}
|
|
return ret;
|
|
}
|
|
function lengthOverflow(ctx) {
|
|
var start = ctx.displayStart, end = displayEnd(ctx), len = ctx.pageLength;
|
|
// If we have space to show extra rows (backing up from the end point - then
|
|
// do so
|
|
if (start >= end) {
|
|
start = end - len;
|
|
}
|
|
// Keep the start record on the current page
|
|
start -= start % len;
|
|
if (len === -1 || start < 0) {
|
|
start = 0;
|
|
}
|
|
ctx.displayStart = start;
|
|
}
|
|
/**
|
|
* Detect the data source being used for the table. Used to simplify the code a
|
|
* little (ajax) and to make it compress a little smaller.
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @returns Data source
|
|
*/
|
|
function dataSource(ctx) {
|
|
if (ctx.features.serverSide) {
|
|
return 'ssp';
|
|
}
|
|
else if (ctx.ajax) {
|
|
return 'ajax';
|
|
}
|
|
return 'dom';
|
|
}
|
|
/**
|
|
* Common replacement for language strings
|
|
*
|
|
* @param ctx DataTables settings object
|
|
* @param str String with values to replace
|
|
* @param entries Plural number for _ENTRIES_ - can be undefined
|
|
* @returns String
|
|
*/
|
|
function macros(ctx, str, entries) {
|
|
// When infinite scrolling, we are always starting at 1. _iDisplayStart is
|
|
// used only internally
|
|
var formatter = ctx.formatNumber, start = ctx.displayStart + 1, len = ctx.pageLength, vis = recordsDisplay(ctx), max = recordsTotal(ctx), all = len === -1;
|
|
return str
|
|
.replace(/_START_/g, formatter(start, ctx))
|
|
.replace(/_END_/g, formatter(displayEnd(ctx), ctx))
|
|
.replace(/_MAX_/g, formatter(max, ctx))
|
|
.replace(/_TOTAL_/g, formatter(vis, ctx))
|
|
.replace(/_PAGE_/g, formatter(all ? 1 : Math.ceil(start / len), ctx))
|
|
.replace(/_PAGES_/g, formatter(all ? 1 : Math.ceil(vis / len), ctx))
|
|
.replace(/_ENTRIES_/g, ctx.api.i18n('entries', '', entries))
|
|
.replace(/_ENTRIES-MAX_/g, ctx.api.i18n('entries', '', max))
|
|
.replace(/_ENTRIES-TOTAL_/g, ctx.api.i18n('entries', '', vis));
|
|
}
|
|
/**
|
|
* Add elements to an array as quickly as possible, but stack safe.
|
|
*
|
|
* @param arr Array to add the data to
|
|
* @param data Data array that is to be added
|
|
*/
|
|
function arrayApply(arr, data) {
|
|
if (!data) {
|
|
return;
|
|
}
|
|
// Chrome can throw a max stack error if apply is called with
|
|
// too large an array, but apply is faster.
|
|
if (data.length < 10000) {
|
|
arr.push.apply(arr, data);
|
|
}
|
|
else {
|
|
for (var i = 0; i < data.length; i++) {
|
|
arr.push(data[i]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Add one or more listeners to the table
|
|
*
|
|
* @param that JQ for the table
|
|
* @param name Event name
|
|
* @param src Listener(s)
|
|
*/
|
|
function listener(that, name, src) {
|
|
let srcArr = Array.isArray(src) ? src : [src];
|
|
for (var i = 0; i < srcArr.length; i++) {
|
|
that.on(name + '.dt.DT', srcArr[i]);
|
|
}
|
|
}
|
|
/**
|
|
* Escape HTML entities in strings, in an object
|
|
*/
|
|
function escapeObject(obj) {
|
|
if (ext.escape.attributes) {
|
|
each(obj, function (key, val) {
|
|
obj[key] = escapeHtml(val);
|
|
});
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
const store = {
|
|
className: {},
|
|
detect: [],
|
|
render: {},
|
|
search: {},
|
|
order: {}
|
|
};
|
|
// Common function to remove new lines, strip HTML and diacritic control
|
|
function _filterString(stripHtml, normalize) {
|
|
return function (str) {
|
|
if (util.is.empty(str) || typeof str !== 'string') {
|
|
return str;
|
|
}
|
|
str = str.replace(util.regex.reNewLines, ' ');
|
|
if (stripHtml) {
|
|
str = util.stripHtml(str);
|
|
}
|
|
{
|
|
str = util.diacritics(str, false);
|
|
}
|
|
return str;
|
|
};
|
|
}
|
|
function __numericReplace(d, decimalPlace, re1, re2) {
|
|
if (d !== 0 && (!d || d === '-')) {
|
|
return -Infinity;
|
|
}
|
|
if (typeof d === 'number' || typeof d === 'bigint') {
|
|
return d;
|
|
}
|
|
// If a decimal place other than `.` is used, it needs to be given to the
|
|
// function so we can detect it and replace with a `.` which is the only
|
|
// decimal place JavaScript recognises - it is not locale aware.
|
|
if (decimalPlace) {
|
|
d = util.conv.numToDecimal(d, decimalPlace);
|
|
}
|
|
if (typeof d === 'string') {
|
|
if (re1) {
|
|
d = d.replace(re1, '');
|
|
}
|
|
if (re2) {
|
|
d = d.replace(re2, '');
|
|
}
|
|
}
|
|
return d * 1;
|
|
}
|
|
function register$1(name, prop, val) {
|
|
if (!prop) {
|
|
return {
|
|
className: store.className[name],
|
|
detect: store.detect.find(function (fn) {
|
|
return fn._name === name;
|
|
}),
|
|
order: {
|
|
pre: store.order[name + '-pre'],
|
|
asc: store.order[name + '-asc'],
|
|
desc: store.order[name + '-desc']
|
|
},
|
|
render: store.render[name],
|
|
search: store.search[name]
|
|
};
|
|
}
|
|
var setProp = function (prop2, propVal) {
|
|
store[prop2][name] = propVal;
|
|
};
|
|
var setDetect = function (detect) {
|
|
// `detect` can be a function or an object - we set a name
|
|
// property for either - that is used for the detection
|
|
Object.defineProperty(detect, '_name', { value: name });
|
|
var idx = store.detect.findIndex(function (item) {
|
|
return item._name === name;
|
|
});
|
|
if (idx === -1) {
|
|
store.detect.unshift(detect);
|
|
}
|
|
else {
|
|
store.detect.splice(idx, 1, detect);
|
|
}
|
|
};
|
|
var setOrder = function (obj) {
|
|
store.order[name + '-pre'] = obj.pre; // can be undefined
|
|
store.order[name + '-asc'] = obj.asc; // can be undefined
|
|
store.order[name + '-desc'] = obj.desc; // can be undefined
|
|
};
|
|
// prop is optional
|
|
if (val === undefined) {
|
|
val = prop;
|
|
prop = undefined;
|
|
}
|
|
if (prop === 'className') {
|
|
setProp('className', val);
|
|
}
|
|
else if (prop === 'detect') {
|
|
setDetect(val);
|
|
}
|
|
else if (prop === 'order') {
|
|
setOrder(val);
|
|
}
|
|
else if (prop === 'render') {
|
|
setProp('render', val);
|
|
}
|
|
else if (prop === 'search') {
|
|
setProp('search', val);
|
|
}
|
|
else if (!prop) {
|
|
if (val.className) {
|
|
setProp('className', val.className);
|
|
}
|
|
if (val.detect !== undefined) {
|
|
setDetect(val.detect);
|
|
}
|
|
if (val.order) {
|
|
setOrder(val.order);
|
|
}
|
|
if (val.render !== undefined) {
|
|
setProp('render', val.render);
|
|
}
|
|
if (val.search !== undefined) {
|
|
setProp('search', val.search);
|
|
}
|
|
}
|
|
}
|
|
// Get a list of types
|
|
function types() {
|
|
return store.detect.map(function (detect) {
|
|
return detect._name;
|
|
});
|
|
}
|
|
var __diacriticSort = function (a, b) {
|
|
a = a !== null && a !== undefined ? a.toString().toLowerCase() : '';
|
|
b = b !== null && b !== undefined ? b.toString().toLowerCase() : '';
|
|
// Checked for `navigator.languages` support in `oneOf` so this code can't execute in old
|
|
// Safari and thus can disable this check
|
|
// eslint-disable-next-line compat/compat
|
|
return a.localeCompare(b, navigator.languages[0] || navigator.language, {
|
|
numeric: true,
|
|
ignorePunctuation: true
|
|
});
|
|
};
|
|
var __diacriticHtmlSort = function (a, b) {
|
|
a = util.stripHtml(a);
|
|
b = util.stripHtml(b);
|
|
return __diacriticSort(a, b);
|
|
};
|
|
//
|
|
// Built in data types
|
|
//
|
|
register$1('string', {
|
|
detect: function () {
|
|
return 'string';
|
|
},
|
|
order: {
|
|
pre: function (a) {
|
|
// This is a little complex, but faster than always calling toString,
|
|
// http://jsperf.com/tostring-v-check
|
|
return util.is.empty(a) && typeof a !== 'boolean'
|
|
? ''
|
|
: typeof a === 'string'
|
|
? a.toLowerCase()
|
|
: !a.toString
|
|
? ''
|
|
: a.toString();
|
|
}
|
|
},
|
|
search: _filterString(false)
|
|
});
|
|
register$1('string-utf8', {
|
|
detect: {
|
|
allOf: function () {
|
|
return true;
|
|
},
|
|
oneOf: function (d) {
|
|
// At least one data point must contain a non-ASCII character
|
|
// This line will also check if navigator.languages is supported or not. If not (Safari 10.0-)
|
|
// this data type won't be supported.
|
|
// eslint-disable-next-line compat/compat
|
|
return (!util.is.empty(d) &&
|
|
navigator.languages &&
|
|
typeof d === 'string' &&
|
|
!!d.match(/[^\x00-\x7F]/));
|
|
}
|
|
},
|
|
order: {
|
|
asc: __diacriticSort,
|
|
desc: function (a, b) {
|
|
return __diacriticSort(a, b) * -1;
|
|
}
|
|
},
|
|
search: _filterString(false)
|
|
});
|
|
register$1('html', {
|
|
detect: {
|
|
allOf: function (d) {
|
|
return (util.is.empty(d) ||
|
|
(typeof d === 'string' && d.indexOf('<') !== -1));
|
|
},
|
|
oneOf: function (d) {
|
|
// At least one data point must contain a `<`
|
|
return (!util.is.empty(d) &&
|
|
typeof d === 'string' &&
|
|
d.indexOf('<') !== -1);
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (a) {
|
|
return util.is.empty(a)
|
|
? ''
|
|
: a.replace
|
|
? util.stripHtml(a).trim().toLowerCase()
|
|
: a + '';
|
|
}
|
|
},
|
|
search: _filterString(true)
|
|
});
|
|
register$1('html-utf8', {
|
|
detect: {
|
|
allOf: function (d) {
|
|
return (util.is.empty(d) ||
|
|
(typeof d === 'string' && d.indexOf('<') !== -1));
|
|
},
|
|
oneOf: function (d) {
|
|
// At least one data point must contain a `<` and a non-ASCII character
|
|
// eslint-disable-next-line compat/compat
|
|
return (navigator.languages &&
|
|
!util.is.empty(d) &&
|
|
typeof d === 'string' &&
|
|
d.indexOf('<') !== -1 &&
|
|
typeof d === 'string' &&
|
|
!!d.match(/[^\x00-\x7F]/));
|
|
}
|
|
},
|
|
order: {
|
|
asc: __diacriticHtmlSort,
|
|
desc: function (a, b) {
|
|
return __diacriticHtmlSort(a, b) * -1;
|
|
}
|
|
},
|
|
search: _filterString(true)
|
|
});
|
|
register$1('date', {
|
|
className: 'dt-type-date',
|
|
detect: {
|
|
allOf: function (d) {
|
|
// V8 tries _very_ hard to make a string passed into `Date.parse()`
|
|
// valid, so we need to use a regex to restrict date formats. Use a
|
|
// plug-in for anything other than ISO8601 style strings
|
|
if (d && !(d instanceof Date) && !util.regex.reDate.test(d)) {
|
|
return null;
|
|
}
|
|
var parsed = Date.parse(d);
|
|
return (parsed !== null && !isNaN(parsed)) || util.is.empty(d);
|
|
},
|
|
oneOf: function (d) {
|
|
// At least one entry must be a date or a string with a date
|
|
return (d instanceof Date ||
|
|
(typeof d === 'string' && util.regex.reDate.test(d)));
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (d) {
|
|
var ts = Date.parse(d);
|
|
return isNaN(ts) ? -Infinity : ts;
|
|
}
|
|
}
|
|
});
|
|
register$1('html-num-fmt', {
|
|
className: 'dt-type-numeric',
|
|
detect: {
|
|
allOf: function (d, settings) {
|
|
var decimal = settings.language.decimal;
|
|
return util.is.htmlNum(d, decimal, true, false);
|
|
},
|
|
oneOf: function (d, settings) {
|
|
// At least one data point must contain a numeric value
|
|
var decimal = settings.language.decimal;
|
|
return util.is.htmlNum(d, decimal, true, false);
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (d, s) {
|
|
var dp = s.language.decimal;
|
|
return __numericReplace(d, dp, util.regex.reHtml, util.regex.reFormattedNumeric);
|
|
}
|
|
},
|
|
search: _filterString(true)
|
|
});
|
|
register$1('html-num', {
|
|
className: 'dt-type-numeric',
|
|
detect: {
|
|
allOf: function (d, settings) {
|
|
var decimal = settings.language.decimal;
|
|
return util.is.htmlNum(d, decimal, false, true);
|
|
},
|
|
oneOf: function (d, settings) {
|
|
// At least one data point must contain a numeric value
|
|
var decimal = settings.language.decimal;
|
|
return util.is.htmlNum(d, decimal, false, false);
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (d, s) {
|
|
var dp = s.language.decimal;
|
|
return __numericReplace(d, dp, util.regex.reHtml);
|
|
}
|
|
},
|
|
search: _filterString(true)
|
|
});
|
|
register$1('num-fmt', {
|
|
className: 'dt-type-numeric',
|
|
detect: {
|
|
allOf: function (d, settings) {
|
|
var decimal = settings.language.decimal;
|
|
return util.is.num(d, decimal, true, true);
|
|
},
|
|
oneOf: function (d, settings) {
|
|
// At least one data point must contain a numeric value
|
|
var decimal = settings.language.decimal;
|
|
return util.is.num(d, decimal, true, false);
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (d, s) {
|
|
var dp = s.language.decimal;
|
|
return __numericReplace(d, dp, util.regex.reFormattedNumeric);
|
|
}
|
|
}
|
|
});
|
|
register$1('num', {
|
|
className: 'dt-type-numeric',
|
|
detect: {
|
|
allOf: function (d, settings) {
|
|
var decimal = settings.language.decimal;
|
|
return util.is.num(d, decimal, false, true);
|
|
},
|
|
oneOf: function (d, settings) {
|
|
// At least one data point must contain a numeric value
|
|
var decimal = settings.language.decimal;
|
|
return util.is.num(d, decimal, false, false);
|
|
}
|
|
},
|
|
order: {
|
|
pre: function (d, s) {
|
|
var dp = s.language.decimal;
|
|
return __numericReplace(d, dp);
|
|
}
|
|
}
|
|
});
|
|
|
|
/*
|
|
* Public helper functions. These aren't used internally by DataTables, or
|
|
* called by any of the options passed into DataTables, but they can be used
|
|
* externally by developers working with DataTables. They are helper functions
|
|
* to make working with DataTables a little bit easier.
|
|
*/
|
|
/**
|
|
* Common logic for moment, luxon or a date action.
|
|
*
|
|
* Happens after __mldObj, so don't need to call `resolveWindowsLibs` again
|
|
*/
|
|
function __mld(dtLib, momentFn, luxonFn, dateFn, arg1) {
|
|
if (__moment) {
|
|
return dtLib[momentFn](arg1);
|
|
}
|
|
else if (__luxon) {
|
|
return dtLib[luxonFn](arg1);
|
|
}
|
|
return dateFn ? dtLib[dateFn](arg1) : dtLib;
|
|
}
|
|
var __mlWarning = false;
|
|
var __luxon;
|
|
var __moment;
|
|
/**
|
|
*
|
|
*/
|
|
function resolveWindowLibs() {
|
|
__luxon = util.external('luxon');
|
|
__moment = util.external('moment');
|
|
}
|
|
function __mldObj(d, format, locale) {
|
|
var dt;
|
|
resolveWindowLibs();
|
|
if (__moment) {
|
|
dt = __moment(d, format, locale, true);
|
|
if (!dt.isValid()) {
|
|
return null;
|
|
}
|
|
}
|
|
else if (__luxon) {
|
|
dt =
|
|
format && typeof d === 'string'
|
|
? __luxon.DateTime.fromFormat(d, format)
|
|
: __luxon.DateTime.fromISO(d);
|
|
if (!dt.isValid) {
|
|
return null;
|
|
}
|
|
dt = dt.setLocale(locale);
|
|
}
|
|
else if (!format) {
|
|
// No format given, must be ISO
|
|
dt = new Date(d);
|
|
}
|
|
else {
|
|
if (!__mlWarning) {
|
|
alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17');
|
|
}
|
|
__mlWarning = true;
|
|
}
|
|
return dt;
|
|
}
|
|
// Wrapper for date, datetime and time which all operate the same way with the
|
|
// exception of the output string for auto locale support
|
|
function __mlHelper(localeString) {
|
|
return function (from, to, locale, def) {
|
|
// Luxon and Moment support
|
|
// Argument shifting
|
|
if (arguments.length === 0) {
|
|
locale = 'en';
|
|
to = null; // means toLocaleString
|
|
from = null; // means iso8601
|
|
}
|
|
else if (arguments.length === 1) {
|
|
locale = 'en';
|
|
to = from;
|
|
from = null;
|
|
}
|
|
else if (arguments.length === 2) {
|
|
locale = to;
|
|
to = from;
|
|
from = null;
|
|
}
|
|
var typeName = 'datetime' + (to ? '-' + to : '');
|
|
// Add type detection and sorting specific to this date format - we need
|
|
// to be able to identify date type columns as such, rather than as
|
|
// numbers in extensions. Hence the need for this.
|
|
if (!store.order[typeName + '-pre']) {
|
|
register$1(typeName, {
|
|
detect: function (d) {
|
|
// The renderer will give the value to type detect as the
|
|
// type!
|
|
return d === typeName ? typeName : false;
|
|
},
|
|
order: {
|
|
pre: function (d) {
|
|
// The renderer gives us Moment, Luxon or Date objects
|
|
// for the sorting, all of which have a `valueOf` which
|
|
// gives milliseconds epoch
|
|
return d.valueOf();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (!store.className[typeName]) {
|
|
store.className[typeName] = 'dt-right';
|
|
}
|
|
return function (d, type) {
|
|
// Allow for a default value
|
|
if (d === null || d === undefined) {
|
|
if (def === '--now') {
|
|
// We treat everything as UTC further down, so no changes
|
|
// are made, as such need to get the local date / time as if
|
|
// it were UTC
|
|
var local = new Date();
|
|
d = new Date(Date.UTC(local.getFullYear(), local.getMonth(), local.getDate(), local.getHours(), local.getMinutes(), local.getSeconds()));
|
|
}
|
|
else {
|
|
d = '';
|
|
}
|
|
}
|
|
if (type === 'type') {
|
|
// Typing uses the type name for fast matching
|
|
return typeName;
|
|
}
|
|
if (d === '') {
|
|
return type !== 'sort'
|
|
? ''
|
|
: __mldObj('0000-01-01 00:00:00', null, locale);
|
|
}
|
|
// Shortcut. If `from` and `to` are the same, we are using the
|
|
// renderer to format for ordering, not display - its already in the
|
|
// display format.
|
|
if (to !== null &&
|
|
from === to &&
|
|
type !== 'sort' &&
|
|
type !== 'type' &&
|
|
!(d instanceof Date)) {
|
|
return d;
|
|
}
|
|
// Determine if there is a timezone. If there is, we want to reuse
|
|
// it for the output, so the timezone doesn't change between the
|
|
// input and output.
|
|
let options = {};
|
|
let tzMatch = typeof d === 'string' ? d.match(util.regex.isoTimezone) : null;
|
|
if (tzMatch) {
|
|
options.timeZone = tzMatch[1] === 'Z' ? 'UTC' : tzMatch[1];
|
|
}
|
|
// Get a Date object (Luxon, moment or Date)
|
|
var dt = __mldObj(d, from, locale);
|
|
if (dt === null) {
|
|
return d;
|
|
}
|
|
if (type === 'sort') {
|
|
return dt;
|
|
}
|
|
var formatted = to === null
|
|
? __mld(dt, 'toDate', 'toJSDate', '')[localeString](navigator.language, options)
|
|
: __mld(dt, 'format', 'toFormat', 'toISOString', to);
|
|
// XSS protection
|
|
return type === 'display' ? util.escapeHtml(formatted) : formatted;
|
|
};
|
|
};
|
|
}
|
|
// Based on locale, determine standard number formatting
|
|
// Fallback for legacy browsers is US English
|
|
var __thousands = ',';
|
|
var __decimal = '.';
|
|
if (window.Intl !== undefined) {
|
|
try {
|
|
var num = new Intl.NumberFormat().formatToParts(100000.1);
|
|
for (var i = 0; i < num.length; i++) {
|
|
if (num[i].type === 'group') {
|
|
__thousands = num[i].value;
|
|
}
|
|
else if (num[i].type === 'decimal') {
|
|
__decimal = num[i].value;
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
// noop
|
|
}
|
|
}
|
|
/**
|
|
* Register a date / time format for DataTables to use.
|
|
*
|
|
* @param format The date / time format to detect data in. Please refer to the
|
|
* Moment.js or Luxon document for the full list of tokens, depending on which
|
|
* of the two libraries you are using.
|
|
* @param locale The locale to pass to Moment.js / Luxon.
|
|
*/
|
|
function datetime(format, locale) {
|
|
var typeName = 'datetime-' + format;
|
|
if (!locale) {
|
|
locale = 'en';
|
|
}
|
|
if (!store.order[typeName]) {
|
|
register$1(typeName, {
|
|
detect: function (d) {
|
|
var dt = __mldObj(d, format, locale);
|
|
return d === '' || dt ? typeName : false;
|
|
},
|
|
order: {
|
|
pre: function (d) {
|
|
return __mldObj(d, format, locale) || 0;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (!store.className[typeName]) {
|
|
store.className[typeName] = 'dt-right';
|
|
}
|
|
}
|
|
/**
|
|
* Helpers for `columns.render`.
|
|
*/
|
|
var helpers = {
|
|
date: __mlHelper('toLocaleDateString'),
|
|
datetime: __mlHelper('toLocaleString'),
|
|
time: __mlHelper('toLocaleTimeString'),
|
|
number: function (thousands, decimal, precision, prefix, postfix) {
|
|
// Auto locale detection
|
|
if (thousands === null || thousands === undefined) {
|
|
thousands = __thousands;
|
|
}
|
|
if (decimal === null || decimal === undefined) {
|
|
decimal = __decimal;
|
|
}
|
|
return {
|
|
display: function (d) {
|
|
if (typeof d !== 'number' && typeof d !== 'string') {
|
|
return d;
|
|
}
|
|
if (d === '' || d === null) {
|
|
return d;
|
|
}
|
|
var flo = typeof d === 'number' ? d : parseFloat(d);
|
|
var negative = flo < 0 ? '-' : '';
|
|
var abs = Math.abs(flo);
|
|
// Scientific notation for large and small numbers
|
|
if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0)) {
|
|
var exp = flo.toExponential(precision).split(/e\+?/);
|
|
return exp[0] + ' x 10<sup>' + exp[1] + '</sup>';
|
|
}
|
|
// If NaN then there isn't much formatting that we can do - just
|
|
// return immediately, escaping any HTML (this was supposed to
|
|
// be a number after all)
|
|
if (isNaN(flo)) {
|
|
return util.escapeHtml(d);
|
|
}
|
|
flo = flo.toFixed(precision);
|
|
var absPart = Math.abs(flo);
|
|
var intPart = Math.abs(parseInt(flo, 10));
|
|
var floatPart = precision
|
|
? decimal +
|
|
(absPart - intPart).toFixed(precision).substring(2)
|
|
: '';
|
|
// If zero, then can't have a negative prefix
|
|
if (intPart === 0 && parseFloat(floatPart) === 0) {
|
|
negative = '';
|
|
}
|
|
return (negative +
|
|
(prefix || '') +
|
|
intPart
|
|
.toString()
|
|
.replace(/\B(?=(\d{3})+(?!\d))/g, thousands) +
|
|
floatPart +
|
|
(postfix || ''));
|
|
}
|
|
};
|
|
},
|
|
text: function () {
|
|
return {
|
|
display: util.escapeHtml,
|
|
filter: util.escapeHtml
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Column options that can be given to DataTables at initialisation time.
|
|
*/
|
|
const defaults$2 = {
|
|
ariaTitle: '',
|
|
cellType: 'td',
|
|
className: '',
|
|
contentPadding: '',
|
|
createdCell: null,
|
|
data: null,
|
|
defaultContent: null,
|
|
footer: null,
|
|
name: '',
|
|
orderable: true,
|
|
orderData: null,
|
|
orderDataType: 'std',
|
|
orderSequence: ['asc', 'desc', ''],
|
|
render: null,
|
|
search: null,
|
|
searchable: true,
|
|
title: null,
|
|
type: null,
|
|
visible: true,
|
|
width: null
|
|
};
|
|
|
|
/**
|
|
* Internal settings object used for individual columns. Instances are held in
|
|
* the setting object's `columns` array and contains all the information that
|
|
* DataTables needs about each individual column.
|
|
*
|
|
* Note that this object is related to the column defaults but this one is the
|
|
* internal data store for DataTables's cache of columns. It should NOT be
|
|
* manipulated outside of DataTables. Any configuration should be done through
|
|
* the initialisation options.
|
|
*/
|
|
class Settings {
|
|
constructor() {
|
|
/**
|
|
* Flag to indicate if HTML5 data attributes should be used as the data
|
|
* source for filtering or sorting. True is either are.
|
|
*/
|
|
this.attrSrc = false;
|
|
this.ariaTitle = '';
|
|
/**
|
|
* The class to apply to all cells in the table's `tbody`` for the column
|
|
*/
|
|
this.className = null;
|
|
/**
|
|
* When DataTables calculates the column widths to assign to each column, it
|
|
* finds the longest string in each column and then constructs a temporary
|
|
* table and reads the widths from that. The problem with this is that "mmm"
|
|
* is much wider then "iiii", but the latter is a longer string - thus the
|
|
* calculation can go wrong (doing it properly and putting it into an DOM
|
|
* object and measuring that is horribly(!) slow). Thus as a "work around"
|
|
* we provide this option. It will append its value to the text that is
|
|
* found to be the longest string for the column - i.e. padding.
|
|
*/
|
|
this.contentPadding = null;
|
|
/**
|
|
* Property to read the value for the cells in the column from the data
|
|
* source array / object. If null, then the default content is used, if a
|
|
* function is given then the return from the function is used.
|
|
*/
|
|
this.data = null;
|
|
/**
|
|
* Allows a default value to be given for a column's data, and will be used
|
|
* whenever a null data source is encountered (this can be because mData is
|
|
* set to null, or because the data source itself is null).
|
|
*/
|
|
this.defaultContent = null;
|
|
/**
|
|
* Name for the column, allowing reference to the column by name as well as
|
|
* by index (needs a lookup to work by name).
|
|
*/
|
|
this.name = null;
|
|
/**
|
|
* A list of the columns that sorting should occur on when this column is
|
|
* sorted. That this property is an array allows multi-column sorting to be
|
|
* defined for a column (for example first name / last name columns would
|
|
* benefit from this). The values are integers pointing to the columns to be
|
|
* sorted on (typically it will be a single integer pointing at itself, but
|
|
* that doesn't need to be the case).
|
|
*/
|
|
this.orderData = [];
|
|
/**
|
|
* Custom sorting data type - defines which of the available plug-ins in
|
|
* afnSortData the custom sorting will use - if any is defined.
|
|
*/
|
|
this.orderDataType = 'std';
|
|
/**
|
|
* Class to be applied to the header element when sorting on this column
|
|
*/
|
|
this.orderingClass = null;
|
|
/**
|
|
* Define the sorting directions that are applied to the column, in sequence
|
|
* as the column is repeatedly sorted upon - i.e. the first value is used as
|
|
* the sorting direction when the column if first sorted (clicked on). Sort
|
|
* it again (click again) and it will move on to the next index. Repeat
|
|
* until loop.
|
|
*/
|
|
this.orderSequence = [];
|
|
/**
|
|
* Partner property to mData which is used (only when defined) to get the
|
|
* data - i.e. it is basically the same as mData, but without the 'set'
|
|
* option, and also the data fed to it is the result from mData. This is the
|
|
* rendering method to match the data method of mData.
|
|
*/
|
|
this.render = null;
|
|
/**
|
|
* Title of the column - what is seen in the TH element (nTh).
|
|
*/
|
|
this.title = null;
|
|
/**
|
|
* Store for manual type assignment using the `column.type` option. This
|
|
* is held in store so we can manipulate the column's `type` property.
|
|
*/
|
|
this.typeManual = null;
|
|
/** Cached longest strings from a column */
|
|
this.wideStrings = null;
|
|
/**
|
|
* Width of the column
|
|
*/
|
|
this.width = null;
|
|
/**
|
|
* Width of the column when it was first "encountered"
|
|
*/
|
|
this.widthOrig = null;
|
|
}
|
|
}
|
|
|
|
const browser = {
|
|
barWidth: -1,
|
|
scrollbarLeft: false
|
|
};
|
|
const hungarianToCamelRe = /^(a|aa|ai|ao|as|b|fn|i|m|o|s)([A-Z])([a-z].*$)/;
|
|
/**
|
|
* Take an object which has hungarian notation parameters and convert them to
|
|
* camelCase style. This is to allow compatibility with DataTables 1.9 and
|
|
* earlier which only used hungarian notation, and also with DataTables 1.10 - 2
|
|
* which allowed it to be used.
|
|
*/
|
|
function hungarianToCamel(user) {
|
|
if (!user) {
|
|
return user;
|
|
}
|
|
let userKeys = Object.keys(user);
|
|
let userAny = user;
|
|
for (let i = 0; i < userKeys.length; i++) {
|
|
let userKey = userKeys[i];
|
|
let match = userKey.match(hungarianToCamelRe);
|
|
// Is the key in hungarian notation?
|
|
if (match) {
|
|
// If so map it down
|
|
user[match[2].toLowerCase() + match[3]] = userAny[userKey];
|
|
}
|
|
// Recurse down through the object
|
|
if (util.is.plainObject(userAny[userKey])) {
|
|
hungarianToCamel(userAny[userKey]);
|
|
}
|
|
}
|
|
return user;
|
|
}
|
|
/**
|
|
* Map one parameter onto another
|
|
*
|
|
* @param o Object to map
|
|
* @param newKey The new parameter name
|
|
* @param oldKey The old parameter name
|
|
*/
|
|
function compatMap(o, newKey, oldKey) {
|
|
if (o[oldKey] !== undefined) {
|
|
o[newKey] = o[oldKey];
|
|
}
|
|
}
|
|
/**
|
|
* Provide backwards compatibility for the main DT options. Note that the new
|
|
* options are mapped onto the old parameters, so this is an external interface
|
|
* change only.
|
|
*
|
|
* @param init Object to map
|
|
* @param defaults Indicate if the object is the defaults object or not
|
|
*/
|
|
function compatOpts(init, defaults = false) {
|
|
// Convert any old style parameters to camelCase
|
|
hungarianToCamel(init);
|
|
// Map old parameter names to new
|
|
compatMap(init, 'ordering', 'sort');
|
|
compatMap(init, 'orderMulti', 'sortMulti');
|
|
compatMap(init, 'orderClasses', 'sortClasses');
|
|
compatMap(init, 'orderCellsTop', 'sortCellsTop');
|
|
compatMap(init, 'order', 'sorting');
|
|
compatMap(init, 'orderFixed', 'sortingFixed');
|
|
compatMap(init, 'paging', 'paginate');
|
|
compatMap(init, 'pagingType', 'paginationType');
|
|
compatMap(init, 'pageLength', 'displayLength');
|
|
compatMap(init, 'searching', 'filter');
|
|
compatMap(init, 'stateDuration', 'cookieDuration');
|
|
// Boolean initialisation of x-scrolling
|
|
if (typeof init.scrollX === 'boolean') {
|
|
init.scrollX = init.scrollX ? '100%' : '';
|
|
}
|
|
// Objects for ordering
|
|
if (typeof init.ordering === 'object') {
|
|
init.orderIndicators =
|
|
init.ordering.indicators !== undefined
|
|
? init.ordering.indicators
|
|
: true;
|
|
init.orderHandler =
|
|
init.ordering.handler !== undefined ? init.ordering.handler : true;
|
|
if (!defaults) {
|
|
init.ordering = true;
|
|
}
|
|
}
|
|
else if (init.ordering === false) {
|
|
init.orderIndicators = false;
|
|
init.orderHandler = false;
|
|
}
|
|
else if (init.ordering === true) {
|
|
init.orderIndicators = true;
|
|
init.orderHandler = true;
|
|
}
|
|
// Which cells are the title cells?
|
|
if (typeof init.orderCellsTop === 'boolean') {
|
|
init.titleRow = init.orderCellsTop;
|
|
}
|
|
// Column search objects are in an array, so it needs to be converted
|
|
// element by element
|
|
var searchCols = init.searchCols;
|
|
if (searchCols) {
|
|
for (var i = 0, iLen = searchCols.length; i < iLen; i++) {
|
|
if (searchCols[i]) {
|
|
hungarianToCamel(searchCols[i]);
|
|
}
|
|
}
|
|
}
|
|
// Enable search delay if server-side processing is enabled
|
|
if (init.serverSide && !init.searchDelay) {
|
|
init.searchDelay = 400;
|
|
}
|
|
// Language
|
|
if (init.language && init.language.url && !init.language.ajax) {
|
|
init.language.ajax = init.language.url;
|
|
}
|
|
}
|
|
/**
|
|
* Provide backwards compatibility for column options. Note that the new options
|
|
* are mapped onto the old parameters, so this is an external interface change
|
|
* only.
|
|
*
|
|
* @param init Object to map
|
|
*/
|
|
function compatCols(init) {
|
|
// Convert any old style parameters to camelCase
|
|
hungarianToCamel(init);
|
|
// typeof columnDefaults
|
|
compatMap(init, 'orderable', 'sortable');
|
|
compatMap(init, 'orderData', 'dataSort');
|
|
compatMap(init, 'orderSequence', 'sorting');
|
|
compatMap(init, 'orderDataType', 'sortDataType');
|
|
compatMap(init, 'className', 'class');
|
|
// orderData can be given as an integer
|
|
var dataSort = init.aDataSort;
|
|
var orderData = init.orderData;
|
|
if (typeof dataSort === 'number') {
|
|
init.orderData = [dataSort];
|
|
}
|
|
if (typeof orderData === 'number') {
|
|
init.orderData = [orderData];
|
|
}
|
|
// Backwards compatibility for mDataProp from 1.9-
|
|
if (init.dataProp !== undefined && !init.data) {
|
|
init.data = init.dataProp;
|
|
}
|
|
}
|
|
/**
|
|
* Browser feature detection for capabilities, quirks
|
|
*
|
|
* @param ctx DataTables settings object
|
|
*/
|
|
function browserDetect(ctx) {
|
|
// We don't need to do this every time DataTables is constructed, the values
|
|
// calculated are specific to the browser and OS configuration which we
|
|
// don't expect to change between initialisations
|
|
if (browser.barWidth === -1) {
|
|
// Scrolling feature / quirks detection
|
|
var n = Dom
|
|
.c('div')
|
|
.css({
|
|
position: 'fixed',
|
|
top: '0',
|
|
left: -1 * window.pageXOffset + 'px', // allow for scrolling
|
|
height: '1px',
|
|
width: '1px',
|
|
overflow: 'hidden'
|
|
})
|
|
.append(Dom
|
|
.c('div')
|
|
.css({
|
|
position: 'absolute',
|
|
top: '1px',
|
|
left: '1px',
|
|
width: '100px',
|
|
overflow: 'scroll'
|
|
})
|
|
.append(Dom.c('div').css({
|
|
width: '100%',
|
|
height: '10px'
|
|
})))
|
|
.appendTo('body');
|
|
var outer = n.children();
|
|
var inner = outer.children();
|
|
browser.barWidth = outer.get(0).offsetWidth - outer.get(0).clientWidth;
|
|
browser.scrollbarLeft = Math.round(inner.offset().left) !== 1;
|
|
n.remove();
|
|
}
|
|
Object.assign(ctx.browser, browser);
|
|
ctx.scroll.barWidth = browser.barWidth;
|
|
}
|
|
|
|
/**
|
|
* Add a column to the list used for the table with default values
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function addColumn(settings) {
|
|
// Add column to aoColumns array
|
|
let columnIdx = settings.columns.length;
|
|
let column = util.object.assign({}, new Settings(), defaults$2, {
|
|
orderData: defaults$2.orderData
|
|
? defaults$2.orderData
|
|
: [columnIdx],
|
|
data: defaults$2.data ? defaults$2.data : columnIdx,
|
|
idx: columnIdx,
|
|
searchFixed: {},
|
|
colEl: Dom
|
|
.c('col')
|
|
.attr('data-dt-column', columnIdx)
|
|
});
|
|
settings.columns.push(column);
|
|
// Legacy support for `searchCols` property. If set, and there is a value
|
|
// for this column, then it should be applied to the search. The new, column
|
|
// specific `search` option is applied in `columnOptions`, but we always
|
|
// want the search object for the column to exist.
|
|
let searchCols = settings.searchCols;
|
|
settings.searches[columnIdx] = create$1(searchCols[columnIdx]
|
|
? hungarianToCamel(searchCols[columnIdx])
|
|
: {});
|
|
settings.searches[columnIdx].columns = [columnIdx];
|
|
}
|
|
/**
|
|
* Apply options for a column
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param colIdx column index to consider
|
|
* @param options Column configuration options
|
|
*/
|
|
function columnOptions(settings, colIdx, options) {
|
|
var column = settings.columns[colIdx];
|
|
/* User specified column options */
|
|
if (options !== undefined && options !== null) {
|
|
// Backwards compatibility
|
|
compatCols(options);
|
|
if (options.type) {
|
|
column.typeManual = options.type;
|
|
}
|
|
// `class` is a reserved word in JavaScript, so we need to provide
|
|
// the ability to use a valid name for the camel case input
|
|
if (options.className && !options.className) {
|
|
options.className = options.className;
|
|
}
|
|
var origClass = column.className;
|
|
util.object.assign(column, options);
|
|
map(column, options, 'width', 'widthOrig');
|
|
// Merge class from previously defined classes with this one, rather
|
|
// than just overwriting it in the extend above
|
|
if (origClass !== column.className) {
|
|
column.className = origClass + ' ' + column.className;
|
|
}
|
|
map(column, options, 'orderData');
|
|
// Search term specifically for this column
|
|
if (options.search) {
|
|
util.object.assign(settings.searches[colIdx], options.search);
|
|
}
|
|
}
|
|
/* Cache the data get and set functions for speed */
|
|
var dataSrc = column.data;
|
|
var dataFn = util.get(dataSrc);
|
|
// The `render` option can be given as an array to access the helper
|
|
// rendering methods. The first element is the rendering method to use, the
|
|
// rest are the parameters to pass
|
|
if (column.render && Array.isArray(column.render)) {
|
|
var copy = column.render.slice();
|
|
var name = copy.shift();
|
|
column.render = helpers[name].apply(window, copy);
|
|
}
|
|
column.renderer = column.render ? util.get(column.render) : null;
|
|
var attrTest = function (src) {
|
|
return typeof src === 'string' && src.indexOf('@') !== -1;
|
|
};
|
|
column.attrSrc =
|
|
!!dataSrc &&
|
|
util.is.plainObject(dataSrc) &&
|
|
(attrTest(dataSrc.sort) ||
|
|
attrTest(dataSrc.type) ||
|
|
attrTest(dataSrc.filter));
|
|
column.setter = null;
|
|
column.dataGet = function (rowData, type, meta) {
|
|
var innerData = dataFn(rowData, type, undefined, meta);
|
|
return column.renderer && type
|
|
? column.renderer(innerData, type, rowData, meta)
|
|
: innerData;
|
|
};
|
|
column.dataSet = function (rowData, val, meta) {
|
|
return util.set(dataSrc)(rowData, val, meta);
|
|
};
|
|
// Indicate if DataTables should read DOM data as an object or array
|
|
// Used in _fnGetRowElements
|
|
if (typeof dataSrc !== 'number' && !column._isArrayHost) {
|
|
settings.rowReadObject = true;
|
|
}
|
|
// Feature sorting overrides column specific when off
|
|
if (!settings.features.ordering) {
|
|
column.orderable = false;
|
|
}
|
|
}
|
|
/**
|
|
* Adjust the table column widths for new data. Note: you would probably want to
|
|
* do a redraw after calling this function!
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function adjustColumnSizing(settings) {
|
|
calculateColumnWidths(settings);
|
|
columnSizes(settings);
|
|
let scroll = settings.scroll;
|
|
if (scroll.y !== '' || scroll.x !== '') {
|
|
scrollDraw(settings);
|
|
}
|
|
callbackFire(settings, null, 'column-sizing', [settings]);
|
|
}
|
|
/**
|
|
* Apply column sizes
|
|
*
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function columnSizes(settings) {
|
|
let cols = settings.columns;
|
|
for (let i = 0; i < cols.length; i++) {
|
|
let width = columnsSumWidth(settings, [i], false);
|
|
if (width) {
|
|
cols[i].colEl.css('width', width);
|
|
if (settings.scroll.x) {
|
|
cols[i].colEl.css('min-width', width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Convert the index of a visible column to the index in the data array (take
|
|
* account of hidden columns)
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param visIdx Visible column index to lookup
|
|
* @returns i the data index
|
|
*/
|
|
function visibleToColumnIndex(settings, visIdx) {
|
|
let aiVis = getColumns(settings, 'visible');
|
|
return typeof aiVis[visIdx] === 'number' ? aiVis[visIdx] : null;
|
|
}
|
|
/**
|
|
* Convert the index of an index in the data array and convert it to the visible
|
|
* column index (take account of hidden columns)
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param match Column index to lookup
|
|
* @returns The data index
|
|
*/
|
|
function columnIndexToVisible(settings, match) {
|
|
let aiVis = getColumns(settings, 'visible');
|
|
let iPos = aiVis.indexOf(match);
|
|
return iPos !== -1 ? iPos : null;
|
|
}
|
|
/**
|
|
* Get the number of visible columns
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @returns i the number of visible columns
|
|
*/
|
|
function visibleColumns(settings) {
|
|
let layout = settings.header;
|
|
let columns = settings.columns;
|
|
let vis = 0;
|
|
if (layout.length) {
|
|
for (let i = 0, iLen = layout[0].length; i < iLen; i++) {
|
|
if (columns[i].visible &&
|
|
Dom.s(layout[0][i].cell).css('display') !== 'none') {
|
|
vis++;
|
|
}
|
|
}
|
|
}
|
|
return vis;
|
|
}
|
|
/**
|
|
* Get an array of column indexes that match a given property
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param param Parameter in the columns array to look for
|
|
* @returns Array of indexes with matched properties
|
|
*/
|
|
function getColumns(settings, param) {
|
|
let a = [];
|
|
settings.columns.map(function (val, i) {
|
|
if (val[param]) {
|
|
a.push(i);
|
|
}
|
|
});
|
|
return a;
|
|
}
|
|
/**
|
|
* Allow the result from a type detection function to be `true` while
|
|
* translating that into a string. Old type detection functions will return the
|
|
* type name if it passes. An object store would be better, but not backwards
|
|
* compatible.
|
|
*
|
|
* @param typeDetect Object or function for type detection
|
|
* @param res Result from the type detection function
|
|
* @returns Type name or false
|
|
*/
|
|
function _typeResult(typeDetect, res) {
|
|
return res === true ? typeDetect._name : res;
|
|
}
|
|
/**
|
|
* Calculate the 'type' of a column
|
|
* @param settings DataTables settings object
|
|
*/
|
|
function columnTypes(settings, originalTypes = '') {
|
|
var columns = settings.columns;
|
|
var data = settings.data;
|
|
var types = ext.type.detect;
|
|
var i, iLen, j, jen, k, ken;
|
|
var col, detectedType, cache;
|
|
if (!originalTypes) {
|
|
originalTypes = columns.map(c => c.type).join(',');
|
|
}
|
|
// For each column, spin over the data type detection functions, seeing if
|
|
// one matches
|
|
for (i = 0, iLen = columns.length; i < iLen; i++) {
|
|
col = columns[i];
|
|
cache = [];
|
|
if (!col.type && col.typeManual) {
|
|
col.type = col.typeManual;
|
|
}
|
|
else if (!col.type) {
|
|
// With SSP type detection can be unreliable and error prone, so we
|
|
// provide a way to turn it off.
|
|
if (!settings.typeDetect) {
|
|
return;
|
|
}
|
|
for (j = 0, jen = types.length; j < jen; j++) {
|
|
let typeDetect = types[j];
|
|
let oneOf;
|
|
let allOf;
|
|
let init;
|
|
let one = false;
|
|
// There can be either one, or three type detection functions
|
|
if (typeof typeDetect === 'function') {
|
|
allOf = typeDetect;
|
|
}
|
|
else {
|
|
oneOf = typeDetect.oneOf;
|
|
allOf = typeDetect.allOf;
|
|
init = typeDetect.init;
|
|
}
|
|
detectedType = null;
|
|
// Fast detect based on column assignment
|
|
if (init) {
|
|
detectedType = _typeResult(typeDetect, init(settings, col, i));
|
|
if (detectedType) {
|
|
col.type = detectedType;
|
|
break;
|
|
}
|
|
}
|
|
for (k = 0, ken = data.length; k < ken; k++) {
|
|
if (!data[k]) {
|
|
continue;
|
|
}
|
|
// Use a cache array so we only need to get the type data
|
|
// from the formatter once (when using multiple detectors)
|
|
if (cache[k] === undefined) {
|
|
cache[k] = getCellData(settings, k, i, 'type');
|
|
}
|
|
// Only one data point in the column needs to match this
|
|
// function
|
|
if (oneOf && !one) {
|
|
one = _typeResult(typeDetect, oneOf(cache[k], settings));
|
|
}
|
|
// All data points need to match this function
|
|
detectedType = _typeResult(typeDetect, allOf(cache[k], settings));
|
|
// If null, then this type can't apply to this column, so
|
|
// rather than testing all cells, break out. There is an
|
|
// exception for the last type which is `html`. We need to
|
|
// scan all rows since it is possible to mix string and HTML
|
|
// types
|
|
if (!detectedType && j !== types.length - 3) {
|
|
break;
|
|
}
|
|
// Only a single match is needed for html type since it is
|
|
// bottom of the pile and very similar to string - but it
|
|
// must not be empty
|
|
if (detectedType === 'html' && !util.is.empty(cache[k])) {
|
|
break;
|
|
}
|
|
}
|
|
// Type is valid for all data points in the column - use this
|
|
// type
|
|
if ((oneOf && one && detectedType) ||
|
|
(!oneOf && detectedType)) {
|
|
col.type = detectedType;
|
|
break;
|
|
}
|
|
}
|
|
// Fall back - if no type was detected, always use string
|
|
if (!col.type) {
|
|
col.type = 'string';
|
|
}
|
|
}
|
|
// Set class names for header / footer for auto type classes
|
|
var autoClass = ext.type.className[col.type];
|
|
if (autoClass) {
|
|
_columnAutoClass(settings.header, i, autoClass);
|
|
_columnAutoClass(settings.footer, i, autoClass);
|
|
}
|
|
var renderer = ext.type.render[col.type];
|
|
// This can only happen once! There is no way to remove
|
|
// a renderer. After the first time the renderer has
|
|
// already been set so createTr will run the renderer itself.
|
|
if (renderer && !col.renderer) {
|
|
col.renderer = util.get(renderer);
|
|
_columnAutoRender(settings, i);
|
|
}
|
|
}
|
|
var newTypes = columns.map(c => c.type).join(',');
|
|
if (newTypes !== originalTypes) {
|
|
callbackFire(settings, null, 'columnTypes', [settings], false);
|
|
}
|
|
}
|
|
/**
|
|
* Apply an auto detected renderer to data which doesn't yet have a renderer
|
|
*/
|
|
function _columnAutoRender(settings, colIdx) {
|
|
let data = settings.data;
|
|
for (let i = 0; i < data.length; i++) {
|
|
let d = data[i];
|
|
if (d && d.tr) {
|
|
// We have to update the display here since there is no invalidation
|
|
// check for the data
|
|
let display = getCellData(settings, i, colIdx, 'display');
|
|
d.displayData[colIdx] = display;
|
|
writeCell(d.cells[colIdx], display);
|
|
// No need to update sort / filter data since it has been
|
|
// invalidated and will be re-read with the renderer now applied
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Apply a class name to a column's header cells
|
|
*
|
|
* @param container The header / footer structure array
|
|
* @param colIdx Column index
|
|
* @param className Class name to apply
|
|
*/
|
|
function _columnAutoClass(container, colIdx, className) {
|
|
container.forEach(function (row) {
|
|
if (row[colIdx] && row[colIdx].unique) {
|
|
Dom.s(row[colIdx].cell).classAdd(className);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Take the column definitions and static columns arrays and calculate how they
|
|
* relate to column indexes. The callback function will then apply the
|
|
* definition found for a column to a suitable configuration object.
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param aoColDefs The aoColumnDefs array that is to be applied
|
|
* @param aoCols The aoColumns array that defines columns individually
|
|
* @param headerLayout Layout for header as it was loaded
|
|
* @param fn Callback function - takes two parameters, the calculated column
|
|
* index and the definition for that column.
|
|
*/
|
|
function applyColumnDefs(settings, aoColDefs, aoCols, headerLayout, fn) {
|
|
var i, iLen, j, jLen, k, kLen;
|
|
var columns = settings.columns;
|
|
if (aoCols) {
|
|
for (i = 0, iLen = aoCols.length; i < iLen; i++) {
|
|
// Compat
|
|
if (aoCols[i] && aoCols[i].name) {
|
|
columns[i].name = aoCols[i].name;
|
|
}
|
|
}
|
|
}
|
|
// Column definitions with aTargets
|
|
if (aoColDefs) {
|
|
// Loop over the definitions array - loop in reverse so first instance
|
|
// has priority
|
|
for (i = aoColDefs.length - 1; i >= 0; i--) {
|
|
let def = aoColDefs[i];
|
|
/* Each definition can target multiple columns, as it is an array */
|
|
let aTargets = def.target !== undefined
|
|
? def.target
|
|
: def.targets !== undefined
|
|
? def.targets
|
|
: def.aTargets; // legacy
|
|
if (!Array.isArray(aTargets)) {
|
|
aTargets = [aTargets];
|
|
}
|
|
for (j = 0, jLen = aTargets.length; j < jLen; j++) {
|
|
var target = aTargets[j];
|
|
if (typeof target === 'number' && target >= 0) {
|
|
/* Add columns that we don't yet know about */
|
|
while (columns.length <= target) {
|
|
addColumn(settings);
|
|
}
|
|
/* Integer, basic index */
|
|
fn(target, def);
|
|
}
|
|
else if (typeof target === 'number' && target < 0) {
|
|
/* Negative integer, right to left column counting */
|
|
fn(columns.length + target, def);
|
|
}
|
|
else if (typeof target === 'string') {
|
|
for (k = 0, kLen = columns.length; k < kLen; k++) {
|
|
if (target === '_all') {
|
|
// Apply to all columns
|
|
fn(k, def);
|
|
}
|
|
else if (target.indexOf(':name') !== -1) {
|
|
// Column selector
|
|
if (columns[k].name === target.replace(':name', '')) {
|
|
fn(k, def);
|
|
}
|
|
}
|
|
else {
|
|
// Cell selector
|
|
headerLayout.forEach(function (row) {
|
|
if (row[k]) {
|
|
var cell = row[k].cell;
|
|
// Legacy support. Note that it means that
|
|
// we don't support an element name selector
|
|
// only, since they are treated as class
|
|
// names for 1.x compat.
|
|
if (target.match(/^[a-z][\w-]*$/i)) {
|
|
target = '.' + target;
|
|
}
|
|
if (cell.matches(target)) {
|
|
fn(k, def);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Statically defined columns array
|
|
if (aoCols) {
|
|
for (i = 0, iLen = aoCols.length; i < iLen; i++) {
|
|
fn(i, aoCols[i]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get the width for a given set of columns
|
|
*
|
|
* @param settings DataTables settings object
|
|
* @param targets Columns - comma separated string or array of numbers
|
|
* @param original Use the original width (true) or calculated (false)
|
|
* @param incVisible Include visible columns (true) or not (false)
|
|
* @returns Combined CSS value
|
|
*/
|
|
function columnsSumWidth(settings, targets, original, incVisible) {
|
|
if (!Array.isArray(targets)) {
|
|
targets = columnsFromHeader(targets);
|
|
}
|
|
let sum = 0;
|
|
let unit = 'px';
|
|
let columns = settings.columns;
|
|
for (let i = 0, iLen = targets.length; i < iLen; i++) {
|
|
let column = columns[targets[i]];
|
|
let definedWidth = original ? column.widthOrig : column.width;
|
|
if (column.visible === false) {
|
|
continue;
|
|
}
|
|
if (definedWidth === null || definedWidth === undefined) {
|
|
return null; // can't determine a defined width - browser defined
|
|
}
|
|
else if (typeof definedWidth === 'number') {
|
|
sum += definedWidth;
|
|
}
|
|
else {
|
|
let matched = definedWidth.match(/([\d\.]+)([^\d]*)/);
|
|
if (matched) {
|
|
sum += parseFloat(matched[1]);
|
|
unit = matched.length === 3 ? matched[2] : 'px';
|
|
}
|
|
}
|
|
}
|
|
return sum + unit;
|
|
}
|
|
/**
|
|
* Determine what columns a header cell covers (can be multiple for colspan
|
|
* cases).
|
|
*
|
|
* @param cell The header cell in question
|
|
* @returns An array of column indexes
|
|
*/
|
|
function columnsFromHeader(cell) {
|
|
let attr = Dom.s(cell).closest('[data-dt-column]').attr('data-dt-column');
|
|
if (!attr) {
|
|
return [];
|
|
}
|
|
return attr.split(',').map(function (val) {
|
|
return parseInt(val);
|
|
});
|
|
}
|
|
/**
|
|
* Get cells from the header or footer, including a specific row and / or cell
|
|
*
|
|
* @param header The header or footer strcture
|
|
* @param row A specific row, or null
|
|
* @param column A specific column, or null
|
|
* @returns An array of all matching cells
|
|
*/
|
|
function columnCells(header, row = null, column = null) {
|
|
var out = [];
|
|
for (var i = 0; i < header.length; i++) {
|
|
if (row === null || row === i) {
|
|
for (var j = 0; j < header[i].length; j++) {
|
|
var cell = header[i][j].cell;
|
|
if ((column === null || column === j) && !out.includes(cell)) {
|
|
out.push(cell);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
/**
|
|
* Get cells that apply for ordering handler or icons
|
|
*
|
|
* @param settings Context
|
|
* @param notSelector DOM selector to exclude elements
|
|
* @returns Array of selected elements
|
|
*/
|
|
function columnOrderingCells(settings, notSelector) {
|
|
var cells = [];
|
|
var titleRow = settings.titleRow;
|
|
if (titleRow === true) {
|
|
// Top row (legacy `orderCellsTop`)
|
|
cells = columnCells(settings.header, 0);
|
|
}
|
|
else if (titleRow === false) {
|
|
// Bottom row (legacy `orderCellsTop`)
|
|
cells = columnCells(settings.header, settings.header.length - 1);
|
|
}
|
|
else if (titleRow !== null) {
|
|
// Specific row
|
|
cells = columnCells(settings.header, titleRow);
|
|
}
|
|
else {
|
|
// All
|
|
cells = columnCells(settings.header);
|
|
}
|
|
return Dom.s(cells)
|
|
.filter('th' + notSelector + ', td' + notSelector)
|
|
.filter(el => {
|
|
return (Dom.s(el)
|
|
.parent()
|
|
.filter(notSelector)
|
|
.length !== 0);
|
|
});
|
|
}
|
|
|
|
const footer = (settings, cell, classes) => {
|
|
cell.classAdd(classes.tfoot.cell);
|
|
};
|
|
const header = (settings, cell, classes) => {
|
|
cell.classAdd(classes.thead.cell);
|
|
if (!settings.features.ordering) {
|
|
cell.classAdd(classes.order.none);
|
|
}
|
|
// Conditions to not apply the ordering icons
|
|
if (!columnOrderingCells(settings, ':not([data-dt-order="disable"])')
|
|
.get()
|
|
.includes(cell[0])) {
|
|
return;
|
|
}
|
|
// No additional mark-up required. Attach a sort listener to update on sort
|
|
// - note that using the `DT` namespace will allow the event to be removed
|
|
// automatically on destroy, while the `dt` namespaced event is the one we
|
|
// are listening for
|
|
Dom.s(settings.table).on('order.dt.DT column-visibility.dt.DT', function (e, ctx, column) {
|
|
if (settings !== ctx) {
|
|
// need to check if this is the host
|
|
return; // table, not a nested one
|
|
}
|
|
var sorting = ctx.sortDetails;
|
|
if (!sorting) {
|
|
return;
|
|
}
|
|
var orderedColumns = pluck(sorting, 'col');
|
|
// This handler is only needed on column visibility if the column is
|
|
// part of the ordering. If it isn't, then we can bail out to save
|
|
// performance. It could be a separate event handler, but this is a
|
|
// balance between code reuse / size and performance console.log(e,
|
|
// e.name, column, orderedColumns, orderedColumns.includes(column))
|
|
if (e.type === 'column-visibility' &&
|
|
!orderedColumns.includes(column)) {
|
|
return;
|
|
}
|
|
var i;
|
|
var orderClasses = classes.order;
|
|
var columns = ctx.api.columns(cell);
|
|
var col = settings.columns[columns.flatten()[0]];
|
|
var orderable = columns.orderable().includes(true);
|
|
var ariaType = '';
|
|
var indexes = columns.indexes();
|
|
var sortDirs = columns.orderable(true).flatten();
|
|
var tabIndex = settings.tabIndex;
|
|
var canOrder = ctx.orderHandler && orderable;
|
|
cell.classRemove(orderClasses.isAsc + ' ' + orderClasses.isDesc)
|
|
.classToggle(orderClasses.none, !orderable)
|
|
.classToggle(orderClasses.canAsc, canOrder && sortDirs.includes('asc'))
|
|
.classToggle(orderClasses.canDesc, canOrder && sortDirs.includes('desc'));
|
|
// Determine if all of the columns that this cell covers are
|
|
// included in the current ordering
|
|
var isOrdering = true;
|
|
for (i = 0; i < indexes.length; i++) {
|
|
if (!orderedColumns.includes(indexes[i])) {
|
|
isOrdering = false;
|
|
}
|
|
}
|
|
if (isOrdering) {
|
|
// Get the ordering direction for the columns under this cell
|
|
// Note that it is possible for a cell to be asc and desc
|
|
// sorting (column spanning cells)
|
|
var orderDirs = columns.order();
|
|
cell.classAdd((orderDirs.includes('asc') ? orderClasses.isAsc : '') +
|
|
(orderDirs.includes('desc') ? orderClasses.isDesc : ''));
|
|
}
|
|
// Find the first visible column that has ordering applied to it -
|
|
// it get's the aria information, as the ARIA spec says that only
|
|
// one column should be marked with aria-sort
|
|
var firstVis = -1; // column index
|
|
for (i = 0; i < orderedColumns.length; i++) {
|
|
if (settings.columns[orderedColumns[i]].visible) {
|
|
firstVis = orderedColumns[i];
|
|
break;
|
|
}
|
|
}
|
|
if (indexes[0] == firstVis) {
|
|
var firstSort = sorting[0];
|
|
var sortOrder = col.orderSequence;
|
|
cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending');
|
|
// Determine if the next click will remove sorting or change the
|
|
// sort
|
|
ariaType =
|
|
sortOrder && !sortOrder[firstSort.index + 1]
|
|
? 'Remove'
|
|
: 'Reverse';
|
|
}
|
|
else {
|
|
cell.attrRemove('aria-sort');
|
|
}
|
|
// Make the headers tab-able for keyboard navigation
|
|
if (orderable) {
|
|
var orderSpan = cell.find('.dt-column-order');
|
|
orderSpan
|
|
.attr('role', 'button')
|
|
.attr('aria-label', orderable
|
|
? col.ariaTitle +
|
|
ctx.api.i18n('aria.orderable' + ariaType)
|
|
: col.ariaTitle);
|
|
if (tabIndex !== -1) {
|
|
orderSpan.attr('tabindex', tabIndex);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
const layout = (settings, container, items) => {
|
|
let classes = settings.classes.layout;
|
|
let row = Dom.c('div')
|
|
.attr('id', items.id || null)
|
|
.classAdd(items.className || classes.row)
|
|
.appendTo(container);
|
|
displayRowCells(items, function (key, val) {
|
|
var klass = '';
|
|
if (val.table) {
|
|
row.classAdd(classes.tableRow);
|
|
klass += classes.tableCell + ' ';
|
|
}
|
|
if (key === 'start') {
|
|
klass += classes.start;
|
|
}
|
|
else if (key === 'end') {
|
|
klass += classes.end;
|
|
}
|
|
else {
|
|
klass += classes.full;
|
|
}
|
|
Dom.c('div')
|
|
.attr({
|
|
id: val.id || null,
|
|
class: val.className
|
|
? val.className
|
|
: classes.cell + ' ' + klass
|
|
})
|
|
.append(val.contents)
|
|
.appendTo(row);
|
|
});
|
|
};
|
|
const pagingButton = (settings, buttonType, content, active, disabled) => {
|
|
var classes = settings.classes.paging;
|
|
var btnClasses = [classes.button];
|
|
var btn;
|
|
if (active) {
|
|
btnClasses.push(classes.active);
|
|
}
|
|
if (disabled) {
|
|
btnClasses.push(classes.disabled);
|
|
}
|
|
if (buttonType === 'ellipsis') {
|
|
btn = Dom.c('span').classAdd('ellipsis').html(content).get(0);
|
|
}
|
|
else {
|
|
btn = Dom.c('button')
|
|
.classAdd(btnClasses.join(' '))
|
|
.attr('role', 'link')
|
|
.attr('type', 'button')
|
|
.html(content)
|
|
.get(0);
|
|
}
|
|
return {
|
|
display: btn,
|
|
clicker: btn
|
|
};
|
|
};
|
|
const pagingContainer = (settings, buttons) => {
|
|
// No wrapping element - just append directly to the host
|
|
return buttons;
|
|
};
|
|
function displayRowCells(items, fn) {
|
|
if (items.start) {
|
|
fn('start', items.start);
|
|
}
|
|
if (items.end) {
|
|
fn('end', items.end);
|
|
}
|
|
if (items.full) {
|
|
fn('full', items.full);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DataTables extensions
|
|
*
|
|
* This namespace acts as a collection area for plug-ins that can be used to
|
|
* extend DataTables capabilities. Indeed many of the build in methods
|
|
* use this method to provide their own capabilities (sorting methods for
|
|
* example).
|
|
*
|
|
* Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy
|
|
* reasons
|
|
*/
|
|
const ext = {
|
|
/**
|
|
* DataTables build type (expanded by the download builder)
|
|
*/
|
|
builder: 'bs5/dt-3.0.4',
|
|
/**
|
|
* Buttons. For use with the Buttons extension for DataTables. This is
|
|
* defined here so other extensions can define buttons regardless of load
|
|
* order. It is _not_ used by DataTables core.
|
|
*/
|
|
buttons: {},
|
|
/**
|
|
* ColumnControl buttons and content
|
|
*/
|
|
ccContent: {},
|
|
/**
|
|
* Element class names
|
|
*/
|
|
classes: classes$1,
|
|
/**
|
|
* Error reporting.
|
|
*
|
|
* How should DataTables report an error. Can take the value 'alert',
|
|
* 'throw', 'none' or a function.
|
|
*/
|
|
errMode: 'alert',
|
|
/** HTML entity escaping */
|
|
escape: {
|
|
/** When reading data-* attributes for initialisation options */
|
|
attributes: false
|
|
},
|
|
/**
|
|
* Legacy so v1 plug-ins don't throw js errors on load
|
|
*/
|
|
feature: legacy,
|
|
/**
|
|
* Feature plug-ins.
|
|
*
|
|
* This is an object of callbacks which provide the features for DataTables
|
|
* to be initialised via the `layout` option.
|
|
*/
|
|
features: features,
|
|
/**
|
|
* Row searching.
|
|
*
|
|
* This method of searching is complimentary to the default type based
|
|
* searching, and a lot more comprehensive as it allows you complete control
|
|
* over the searching logic. Each element in this array is a function
|
|
* (parameters described below) that is called for every row in the table,
|
|
* and your logic decides if it should be included in the searching data set
|
|
* or not.
|
|
*/
|
|
search: [],
|
|
/**
|
|
* Selector extensions
|
|
*
|
|
* The `selector` option can be used to extend the options available for the
|
|
* selector modifier options (`selector-modifier` object data type) that
|
|
* each of the three built in selector types offer (row, column and cell +
|
|
* their plural counterparts). For example the Select extension uses this
|
|
* mechanism to provide an option to select only rows, columns and cells
|
|
* that have been marked as selected by the end user (`{selected: true}`),
|
|
* which can be used in conjunction with the existing built in selector
|
|
* options.
|
|
*/
|
|
selector: {
|
|
cell: [],
|
|
column: [],
|
|
row: []
|
|
},
|
|
settings: [],
|
|
/**
|
|
* Legacy configuration options. Enable and disable legacy options that
|
|
* are available in DataTables.
|
|
*
|
|
* @type object
|
|
*/
|
|
legacy: {
|
|
/**
|
|
* Enable / disable DataTables 1.9 compatible server-side processing
|
|
* requests
|
|
*/
|
|
ajax: null
|
|
},
|
|
/**
|
|
* Pagination plug-in methods.
|
|
*
|
|
* Each entry in this object is a function and defines which buttons should
|
|
* be shown by the pagination rendering method that is used for the table.
|
|
* The renderer addresses how the buttons are displayed in the document,
|
|
* while the functions here tell it what buttons to display. This is done by
|
|
* returning an array of button descriptions (what each button will do).
|
|
*/
|
|
pager: pager,
|
|
renderer: {
|
|
footer: {
|
|
_: footer
|
|
},
|
|
header: {
|
|
_: header
|
|
},
|
|
layout: {
|
|
_: layout
|
|
},
|
|
pagingButton: {
|
|
_: pagingButton
|
|
},
|
|
pagingContainer: {
|
|
_: pagingContainer
|
|
}
|
|
},
|
|
/**
|
|
* Rendering helper function exposed for use by the styling integrations.
|
|
*/
|
|
rendererDisplayRowCells: displayRowCells,
|
|
/**
|
|
* Ordering plug-ins - custom data source
|
|
*
|
|
* The extension options for ordering of data available here is
|
|
* complimentary to the default type based ordering that DataTables
|
|
* typically uses. It allows much greater control over the data that is
|
|
* being used to order a column, but is necessarily therefore more complex.
|
|
*/
|
|
order: {},
|
|
/**
|
|
* Type based plug-ins.
|
|
*
|
|
* Each column in DataTables has a type assigned to it, either by automatic
|
|
* detection or by direct assignment using the `type` option for the column.
|
|
* The type of a column will effect how it is ordering and search (plug-ins
|
|
* can also make use of the column type if required).
|
|
*/
|
|
type: store,
|
|
/**
|
|
* Unique DataTables instance counter
|
|
*
|
|
* @type int
|
|
* @private
|
|
*/
|
|
_unique: 0,
|
|
//
|
|
// Depreciated
|
|
// The following properties are retained for backwards compatibility only.
|
|
// The should not be used in new projects and will be removed in a future
|
|
// version
|
|
//
|
|
/**
|
|
* Software version
|
|
* @type string
|
|
*/
|
|
version: '3.0.4'
|
|
};
|
|
//
|
|
// Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts
|
|
//
|
|
Object.assign(ext, {
|
|
afnFiltering: ext.search,
|
|
aTypes: ext.type.detect,
|
|
ofnSearch: ext.type.search,
|
|
oSort: ext.type.order,
|
|
afnSortData: ext.order,
|
|
aoFeatures: ext.feature,
|
|
oStdClasses: ext.classes,
|
|
oPagination: ext.pager,
|
|
sVersion: ext.version,
|
|
fnVersionCheck: check$1
|
|
});
|
|
|
|
/**
|
|
* Common run function for selector types
|
|
*/
|
|
function selectorRun(type, selector, selectFn, settings, opts) {
|
|
var out = [], res, i, iLen, selectorType = typeof selector;
|
|
// If a Dom instance, then get the underlying elements
|
|
if (selector instanceof Dom) {
|
|
selector = selector.get();
|
|
}
|
|
// Can't just check for isArray here, as an API or jQuery instance might be
|
|
// given with their array like look
|
|
if (!selector ||
|
|
selectorType === 'string' ||
|
|
selectorType === 'function' ||
|
|
selector.length === undefined) {
|
|
selector = [selector];
|
|
}
|
|
for (i = 0, iLen = selector.length; i < iLen; i++) {
|
|
res = selectFn(typeof selector[i] === 'string' ? selector[i].trim() : selector[i]);
|
|
// Remove empty items
|
|
res = res.filter(function (item) {
|
|
return item !== null && item !== undefined;
|
|
});
|
|
if (res && res.length) {
|
|
out = out.concat(res);
|
|
}
|
|
}
|
|
// selector extensions
|
|
var extSelectors = ext.selector[type];
|
|
if (extSelectors.length) {
|
|
for (i = 0, iLen = extSelectors.length; i < iLen; i++) {
|
|
out = extSelectors[i](settings, opts, out);
|
|
}
|
|
}
|
|
return unique(out);
|
|
}
|
|
function selectorOpts(opts) {
|
|
if (!opts) {
|
|
opts = {};
|
|
}
|
|
// Backwards compatibility for 1.9- which used the terminology filter rather
|
|
// than search
|
|
if (opts.filter && opts.search === undefined) {
|
|
opts.search = opts.filter;
|
|
}
|
|
return assign({}, {
|
|
columnOrder: 'implied',
|
|
search: 'none',
|
|
order: 'current',
|
|
page: 'all'
|
|
}, opts);
|
|
}
|
|
// Reduce the API instance to the first item found
|
|
function selectorFirst(old) {
|
|
// Need to specify the target class as singular since `old` has the context
|
|
// of the plural
|
|
var inst = old.inst(old.context[0], null, old._newClass.replace(/s$/, ''));
|
|
// Use a push rather than passing to the constructor, since it will
|
|
// merge arrays down automatically, which isn't what is wanted here
|
|
if (old.length) {
|
|
inst.push(old[0]);
|
|
}
|
|
inst.selector = old.selector;
|
|
// Limit to a single row / column / cell
|
|
if (inst.length && inst[0].length > 1) {
|
|
inst[0].splice(1);
|
|
}
|
|
return inst;
|
|
}
|
|
function selectorRowIndexes(settings, opts) {
|
|
var i, iLen, tmp, a = [], displayFiltered = settings.display, displayMaster = settings.displayMaster;
|
|
var search = opts.search, // none, applied, removed
|
|
order = opts.order, // applied, current, index (original)
|
|
page = opts.page; // all, current
|
|
if (dataSource(settings) == 'ssp') {
|
|
// In server-side processing mode, most options are irrelevant since
|
|
// rows not shown don't exist and the index order is the applied order
|
|
// Removed is a special case - for consistency just return an empty
|
|
// array
|
|
return search === 'removed' ? [] : range(0, displayMaster.length);
|
|
}
|
|
if (page == 'current') {
|
|
// Current page implies that order=current and filter=applied, since it
|
|
// is fairly senseless otherwise, regardless of what order and search
|
|
// actually are
|
|
for (i = settings.displayStart, iLen = displayEnd(settings); i < iLen; i++) {
|
|
a.push(displayFiltered[i]);
|
|
}
|
|
}
|
|
else if (order == 'current' || order == 'applied') {
|
|
if (search == 'none') {
|
|
a = displayMaster.slice();
|
|
}
|
|
else if (search == 'applied') {
|
|
a = displayFiltered.slice();
|
|
}
|
|
else if (search == 'removed') {
|
|
// O(n+m) solution by creating a hash map
|
|
var displayFilteredMap = {};
|
|
for (i = 0, iLen = displayFiltered.length; i < iLen; i++) {
|
|
displayFilteredMap[displayFiltered[i]] = null;
|
|
}
|
|
displayMaster.forEach(function (item) {
|
|
if (!Object.prototype.hasOwnProperty.call(displayFilteredMap, item)) {
|
|
a.push(item);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else if (order == 'index' || order == 'original') {
|
|
for (i = 0, iLen = settings.data.length; i < iLen; i++) {
|
|
if (!settings.data[i]) {
|
|
continue;
|
|
}
|
|
if (search == 'none') {
|
|
a.push(i);
|
|
}
|
|
else {
|
|
// applied | removed
|
|
tmp = displayFiltered.indexOf(i);
|
|
if ((tmp === -1 && search == 'removed') ||
|
|
(tmp >= 0 && search == 'applied')) {
|
|
a.push(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (typeof order === 'number') {
|
|
// Order the rows by the given column
|
|
var ordered = sort(settings, order, 'asc');
|
|
if (search === 'none') {
|
|
a = ordered;
|
|
}
|
|
else {
|
|
// applied | removed
|
|
for (i = 0; i < ordered.length; i++) {
|
|
tmp = displayFiltered.indexOf(ordered[i]);
|
|
if ((tmp === -1 && search == 'removed') ||
|
|
(tmp >= 0 && search == 'applied')) {
|
|
a.push(ordered[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* `Array.prototype` reference as methods from it are used in the array-like
|
|
* methods of the API.
|
|
*/
|
|
const __arrayProto = Array.prototype;
|
|
const Api = function (context, data) {
|
|
// Allow the API to be initialised without specifying `new`
|
|
if (!(this instanceof Api)) {
|
|
return new Api(context, data);
|
|
}
|
|
this.context = toContextArray(context);
|
|
// Initial data
|
|
arrayApply(this, data);
|
|
// Add properties which will still execute in this scope
|
|
extendApi(this, 'Api');
|
|
};
|
|
// And the private parameters
|
|
util.object.assign(Api.prototype, {
|
|
_newClass: 'Api',
|
|
isDataTableApi: true,
|
|
any() {
|
|
return this.count() !== 0;
|
|
},
|
|
context: [], // array of table settings objects
|
|
count() {
|
|
return this.flatten().length;
|
|
},
|
|
each(fn) {
|
|
for (var i = 0, iLen = this.length; i < iLen; i++) {
|
|
fn.call(this, this[i], i, this);
|
|
}
|
|
return this;
|
|
},
|
|
eq(idx) {
|
|
var ctx = this.context;
|
|
// Note that `eq` returns an API instance, not a nested class instance
|
|
return ctx.length > idx ? this.inst(ctx[idx], this[idx], 'Api') : null;
|
|
},
|
|
filter(fn) {
|
|
var a = __arrayProto.filter.call(this, fn, this);
|
|
return this.inst(this.context, a);
|
|
},
|
|
flatten() {
|
|
var a = [];
|
|
return this.inst(this.context, a.concat.apply(a, this.toArray()));
|
|
},
|
|
get(idx) {
|
|
return this[idx];
|
|
},
|
|
join: __arrayProto.join,
|
|
includes(find) {
|
|
return this.indexOf(find) === -1 ? false : true;
|
|
},
|
|
indexOf: __arrayProto.indexOf,
|
|
inst(context, data, newClass) {
|
|
let name = newClass || this._newClass;
|
|
let inst = Api;
|
|
if (classes[name]) {
|
|
inst = classes[name];
|
|
}
|
|
return new inst(context, data);
|
|
},
|
|
iterator(flatten, type, fn, alwaysNew) {
|
|
var a = [], ret, i, iLen, j, jen, context = this.context, rows, items, item, selector = this.selector;
|
|
// Argument shifting
|
|
if (typeof flatten === 'string') {
|
|
alwaysNew = fn;
|
|
fn = type;
|
|
type = flatten;
|
|
flatten = false;
|
|
}
|
|
for (i = 0, iLen = context.length; i < iLen; i++) {
|
|
var apiInst = this.inst(context[i]);
|
|
if (type === 'table') {
|
|
ret = fn.call(apiInst, context[i], i);
|
|
if (ret !== undefined) {
|
|
a.push(ret);
|
|
}
|
|
}
|
|
else if (type === 'columns' || type === 'rows') {
|
|
// this has same length as context - one entry for each table
|
|
ret = fn.call(apiInst, context[i], this[i], i);
|
|
if (ret !== undefined) {
|
|
a.push(ret);
|
|
}
|
|
}
|
|
else if (type === 'every' ||
|
|
type === 'column' ||
|
|
type === 'column-rows' ||
|
|
type === 'row' ||
|
|
type === 'cell') {
|
|
// columns and rows share the same structure.
|
|
// 'this' is an array of column indexes for each context
|
|
items = this[i];
|
|
if (type === 'column-rows') {
|
|
rows = selectorRowIndexes(context[i], selector.opts);
|
|
}
|
|
for (j = 0, jen = items.length; j < jen; j++) {
|
|
item = items[j];
|
|
if (type === 'cell') {
|
|
ret = fn.call(apiInst, context[i], item.row, item.column, i, j);
|
|
}
|
|
else {
|
|
ret = fn.call(apiInst, context[i], item, i, j, rows);
|
|
}
|
|
if (ret !== undefined) {
|
|
a.push(ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (a.length || alwaysNew) {
|
|
var api = this.inst(context, flatten ? a.concat.apply([], a) : a);
|
|
var apiSelector = api.selector;
|
|
if (apiSelector) {
|
|
apiSelector.rows = selector.rows;
|
|
apiSelector.cols = selector.cols;
|
|
apiSelector.opts = selector.opts;
|
|
}
|
|
return api;
|
|
}
|
|
return this;
|
|
},
|
|
lastIndexOf: __arrayProto.lastIndexOf,
|
|
length: 0,
|
|
map(fn) {
|
|
var a = __arrayProto.map.call(this, fn, this);
|
|
return this.inst(this.context, a);
|
|
},
|
|
pluck(prop) {
|
|
var fn = util.get(prop);
|
|
return this.map((src) => fn(src));
|
|
},
|
|
pop: __arrayProto.pop,
|
|
push: __arrayProto.push,
|
|
reduce: __arrayProto.reduce,
|
|
reduceRight: __arrayProto.reduceRight,
|
|
reverse: __arrayProto.reverse,
|
|
// Object with rows, columns and opts
|
|
selector: {
|
|
rows: undefined,
|
|
cols: undefined,
|
|
opts: undefined
|
|
},
|
|
shift: __arrayProto.shift,
|
|
slice() {
|
|
return this.inst(this.context, this);
|
|
},
|
|
sort: __arrayProto.sort,
|
|
splice: __arrayProto.splice,
|
|
toArray() {
|
|
return __arrayProto.slice.call(this);
|
|
},
|
|
to$() {
|
|
let jq = util.external('jq');
|
|
return jq(this);
|
|
},
|
|
toDom() {
|
|
return new Dom(this.toArray());
|
|
},
|
|
toJQuery: function () {
|
|
let jq = util.external('jq');
|
|
return jq(this);
|
|
},
|
|
unique: function () {
|
|
return this.inst(this.context, util.array.unique(this.toArray()));
|
|
},
|
|
unshift: __arrayProto.unshift
|
|
});
|
|
function register(name, func) {
|
|
if (Array.isArray(name)) {
|
|
for (let i = 0; i < name.length; i++) {
|
|
Api.register(name[i], func);
|
|
}
|
|
return;
|
|
}
|
|
let names = getPrototypeNames(name);
|
|
// Has the parent already been defined or not?
|
|
if (!classes[names.hostClass]) {
|
|
// Create a new "class"
|
|
createApiClass(names.hostClass);
|
|
}
|
|
if (names.property) {
|
|
if (!properties[names.propertyHost]) {
|
|
properties[names.propertyHost] = [];
|
|
}
|
|
properties[names.propertyHost].push({
|
|
couldReturn: names.couldReturn,
|
|
property: names.property,
|
|
method: names.methodName,
|
|
fn: func
|
|
});
|
|
}
|
|
else {
|
|
let wrapped = function () {
|
|
// If a new instance (.inst) is created while the function is being
|
|
// executed, we want to allow it to return its target class. But we
|
|
// also need to keep hold of our own, so it can be used in the end.
|
|
let previousCould = this._newClass;
|
|
this._newClass = names.couldReturn;
|
|
let result = func.apply(this, arguments);
|
|
this._newClass = previousCould;
|
|
return result;
|
|
};
|
|
// Create the new method on the host class
|
|
classes[names.hostClass].prototype[names.methodName] = wrapped;
|
|
// If the method is on the top level, it needs to be applied to other
|
|
// classes which have already been defined to allow the circular
|
|
// chaining of the API (e.g. `row().data(...).draw())`.
|
|
if (names.hostClass === 'Api') {
|
|
util.object.each(classes, (className, klass) => {
|
|
if (!klass.prototype[names.methodName]) {
|
|
klass.prototype[names.methodName] = wrapped;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function registerPlural(pluralName, singularName, func) {
|
|
Api.register(pluralName, func);
|
|
Api.register(singularName, function () {
|
|
var ret = func.apply(this, arguments);
|
|
if (ret === this) {
|
|
// Returned item is the API instance that was passed in, return it
|
|
return this;
|
|
}
|
|
else if (ret && ret.isDataTableApi) {
|
|
// New API instance returned, want the value from the first item
|
|
// in the returned array for the singular result.
|
|
return ret.length
|
|
? Array.isArray(ret[0])
|
|
? this.inst(ret.context, ret[0]) // Array results are 'enhanced'
|
|
: ret[0]
|
|
: undefined;
|
|
}
|
|
// Non-API return - just fire it back
|
|
return ret;
|
|
});
|
|
}
|
|
Api.register = register;
|
|
Api.registerPlural = registerPlural;
|
|
/** A collection of properties to apply to the classes as they are constructed */
|
|
const properties = {};
|
|
/** Collection of API classes */
|
|
const classes = {
|
|
Api
|
|
};
|
|
// TODO debug
|
|
window.classes = classes;
|
|
window.properties = properties;
|
|
/**
|
|
* Create a new API "class" (function), used for nested levels of the API - e.g.
|
|
* `ApiRows` and `ApiColumn`.
|
|
*
|
|
* @param name
|
|
*/
|
|
function createApiClass(name) {
|
|
let newClass = function (context, data) {
|
|
// Same as the main API constructor
|
|
this.context = toContextArray(context);
|
|
arrayApply(this, data);
|
|
// Extend the API with properties that execute in this scope, both for
|
|
// this level and for the top level to allow looped chaining
|
|
extendApi(this, 'Api');
|
|
extendApi(this, this._newClass);
|
|
};
|
|
newClass.prototype = Object.create(Api.prototype);
|
|
Object.defineProperty(newClass, 'name', {
|
|
value: name,
|
|
writable: false
|
|
});
|
|
newClass.prototype._newClass = name;
|
|
classes[name] = newClass;
|
|
}
|
|
/**
|
|
* When an instance is created it needs to be extended with properties (since
|
|
* these cannot be given a scope from the prototype due to the nesting).
|
|
*
|
|
* @param api API instance to extend
|
|
* @param className The name of the instance to extend
|
|
* @returns void
|
|
*/
|
|
function extendApi(api, className) {
|
|
let props = properties[className];
|
|
if (!props) {
|
|
return;
|
|
}
|
|
for (let i = 0; i < props.length; i++) {
|
|
let def = props[i];
|
|
if (!api[def.property]) {
|
|
// Instance doesn't yet have this property, so need to create an
|
|
// object to hold the methods.
|
|
api[def.property] = {};
|
|
}
|
|
else if (!api.hasOwnProperty(def.property)) {
|
|
// Its a prototype function, which is a problem since it is shared
|
|
// between all instances, and thus scope is whichever is last setup.
|
|
// As such we need to make it an independent function and wrap it.
|
|
let fn = api[def.property];
|
|
api[def.property] = function () {
|
|
return fn.apply(api, arguments);
|
|
};
|
|
}
|
|
// Wrap the function so we can keep scope and set the return class
|
|
api[def.property][def.method] = function () {
|
|
let previousCould = api._newClass;
|
|
api._newClass = def.couldReturn;
|
|
let result = def.fn.apply(api, arguments);
|
|
api._newClass = previousCould;
|
|
return result;
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Based on an API method name, construct the class, property, etc names that
|
|
* are used to store and construct the API.
|
|
*
|
|
* @param name API function name
|
|
* @returns Name components
|
|
*/
|
|
function getPrototypeNames(name) {
|
|
let parts = name.split('.');
|
|
let property = null;
|
|
let hostClass = 'Api';
|
|
let returnClass = 'Api';
|
|
let methodName = '';
|
|
let propertyHost = '';
|
|
let lastPart = '';
|
|
for (let i = 0; i < parts.length; i++) {
|
|
let part = parts[i];
|
|
let partNoParen = part.replace('()', '');
|
|
hostClass = returnClass; // from previous loop
|
|
returnClass +=
|
|
partNoParen.charAt(0).toUpperCase() +
|
|
partNoParen.slice(1).toLowerCase();
|
|
if (part.includes('()')) {
|
|
methodName = partNoParen;
|
|
// If the previous part was a method rather than a property, then we
|
|
// remove the property host
|
|
if (lastPart.includes('()')) {
|
|
property = null;
|
|
propertyHost = '';
|
|
}
|
|
}
|
|
else {
|
|
// Is a property
|
|
property = part;
|
|
propertyHost = hostClass;
|
|
}
|
|
lastPart = part;
|
|
}
|
|
return {
|
|
couldReturn: returnClass,
|
|
hostClass,
|
|
property,
|
|
propertyHost,
|
|
methodName
|
|
};
|
|
}
|
|
/**
|
|
* Abstraction for `context` parameter of the `Api` constructor to allow it to
|
|
* take several different forms for ease of use.
|
|
*
|
|
* Each of the input parameter types will be converted to a DataTables settings
|
|
* object where possible.
|
|
*
|
|
* @param mixedIn DataTable identifier. Can be one of:
|
|
* * `string` - jQuery selector. Any DataTables' matching the given selector
|
|
* with be found and used.
|
|
* * `node` - `TABLE` node which has already been formed into a DataTable.
|
|
* * `jQuery` - A jQuery object of `TABLE` nodes.
|
|
* * `object` - DataTables settings object
|
|
* * `DataTables.Api` - API instance
|
|
* @return Matching DataTables settings objects. `null` or `undefined` is
|
|
* returned if no matching DataTable is found.
|
|
*/
|
|
function toContext(mixedIn) {
|
|
var mixed = mixedIn;
|
|
var idx, nodes = null;
|
|
var settings = ext.settings;
|
|
var tables = util.array.pluck(settings, 'table');
|
|
if (!mixed) {
|
|
return [];
|
|
}
|
|
else if (mixed.table && mixed.features) {
|
|
// DataTables settings object
|
|
return [mixed];
|
|
}
|
|
else if (mixed.nodeName && mixed.nodeName.toLowerCase() === 'table') {
|
|
// Table node
|
|
idx = tables.indexOf(mixed);
|
|
return idx !== -1 ? [settings[idx]] : null;
|
|
}
|
|
else if (mixed && typeof mixed.settings === 'function') {
|
|
return mixed.settings().toArray();
|
|
}
|
|
else if (typeof mixed === 'string') {
|
|
// jQuery selector
|
|
nodes = Dom.s(mixed).get();
|
|
}
|
|
else if (util.is.jquery(mixed)) {
|
|
// jQuery object
|
|
nodes = mixed.get();
|
|
}
|
|
else if (util.is.dom(mixed)) {
|
|
// DOM object
|
|
nodes = mixed.get();
|
|
}
|
|
if (nodes) {
|
|
return settings.filter(function (v, i) {
|
|
return nodes.includes(tables[i]);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Create the context array for an instance
|
|
*
|
|
* @param mixed The passed in options to convert to context
|
|
* @returns Context array
|
|
*/
|
|
function toContextArray(mixed) {
|
|
var i;
|
|
var settings = [];
|
|
var ctxSettings = function (o) {
|
|
var a = toContext(o);
|
|
if (a) {
|
|
settings.push.apply(settings, a);
|
|
}
|
|
};
|
|
if (Array.isArray(mixed)) {
|
|
for (i = 0; i < mixed.length; i++) {
|
|
ctxSettings(mixed[i]);
|
|
}
|
|
}
|
|
else {
|
|
ctxSettings(mixed);
|
|
}
|
|
// Remove duplicates
|
|
return settings.length > 1 ? util.unique(settings) : settings;
|
|
}
|
|
|
|
register('$()', function (selector, opts) {
|
|
let jq = util.external('jq');
|
|
if (!jq) {
|
|
log(this.context[0], 0, 'No jQuery available. Use `.dom()` or register jQuery');
|
|
}
|
|
let rows = this.rows(opts).nodes(), // Get all rows
|
|
jqRows = jq(rows);
|
|
return jq([].concat(jqRows.filter(selector).toArray(), jqRows.find(selector).toArray()));
|
|
});
|
|
// jQuery functions to operate on the tables
|
|
['on', 'one', 'off'].forEach(key => {
|
|
register(key + '()', function ( /* event, handler */) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
// Add the `dt` namespace automatically if it isn't already present
|
|
args[0] = args[0]
|
|
.split(/\s/)
|
|
.map(function (e) {
|
|
return !e.match(/\.dt\b/) ? e + '.dt' : e;
|
|
})
|
|
.join(' ');
|
|
var inst = Dom.s(this.tables().nodes());
|
|
inst[key].apply(inst, args);
|
|
return this;
|
|
});
|
|
});
|
|
register('clear()', function () {
|
|
return this.iterator('table', function (settings) {
|
|
clearTable(settings);
|
|
});
|
|
});
|
|
register('error()', function (msg) {
|
|
return this.iterator('table', function (settings) {
|
|
log(settings, 0, msg);
|
|
});
|
|
});
|
|
register('settings()', function () {
|
|
return new Api(this.context, this.context);
|
|
});
|
|
register('init()', function () {
|
|
var ctx = this.context;
|
|
return ctx.length ? ctx[0].init : null;
|
|
});
|
|
register('data()', function () {
|
|
return this.iterator('table', function (settings) {
|
|
return util.array.pluck(settings.data, 'data');
|
|
}).flatten();
|
|
});
|
|
register('trigger()', function (name, args, bubbles) {
|
|
return this.iterator('table', function (settings) {
|
|
return callbackFire(settings, null, name, args, bubbles);
|
|
}).flatten();
|
|
});
|
|
register('ready()', function (fn) {
|
|
var ctx = this.context;
|
|
// Get status of first table
|
|
if (!fn) {
|
|
return ctx.length ? ctx[0].initDone || false : false;
|
|
}
|
|
// Function to run either once the table becomes ready or
|
|
// immediately if it is already ready.
|
|
return this.tables().every(function () {
|
|
var api = this;
|
|
if (this.context[0].initDone) {
|
|
fn.call(api);
|
|
}
|
|
else {
|
|
this.on('init.dt.DT', function () {
|
|
fn.call(api);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
register('destroy()', function (remove) {
|
|
remove = remove || false;
|
|
return this.iterator('table', function (settings) {
|
|
var classes = settings.classes;
|
|
var table = settings.table;
|
|
var tbody = settings.tbody;
|
|
var thead = settings.thead;
|
|
var tfoot = settings.tfoot;
|
|
var jqTable = Dom.s(table);
|
|
var jqTbody = Dom.s(tbody);
|
|
var jqWrapper = Dom.s(settings.tableWrapper);
|
|
var rows = settings.data
|
|
.map(function (r) {
|
|
return r ? r.tr : null;
|
|
})
|
|
.filter(r => !!r);
|
|
var orderClasses = classes.order;
|
|
// Flag to note that the table is currently being destroyed - no action
|
|
// should be taken
|
|
settings.destroying = true;
|
|
// Fire off the destroy callbacks for plug-ins etc
|
|
callbackFire(settings, 'destroy', 'destroy', [settings], true);
|
|
// If not being removed from the document, make all columns visible
|
|
if (!remove) {
|
|
new Api(settings).columns().visible();
|
|
}
|
|
// Container width change listener
|
|
if (settings.resizeObserver) {
|
|
settings.resizeObserver.disconnect();
|
|
}
|
|
// Blitz all `DT` namespaced events (these are internal events, the
|
|
// lowercase, `dt` events are user subscribed and they are responsible
|
|
// for removing them
|
|
jqWrapper.off('.DT').find(':not(tbody *)').off('.DT');
|
|
if (settings.windowResizeCb) {
|
|
window.removeEventListener('resize', settings.windowResizeCb);
|
|
}
|
|
// When scrolling we had to break the table up - restore it
|
|
if (table != thead.parentNode) {
|
|
jqTable.children('thead').detach();
|
|
jqTable.append(thead);
|
|
}
|
|
if (tfoot && table != tfoot.parentNode) {
|
|
jqTable.children('tfoot').detach();
|
|
jqTable.append(tfoot);
|
|
}
|
|
// Clean up the header / footer
|
|
cleanHeader(thead, 'header');
|
|
cleanHeader(tfoot, 'footer');
|
|
settings.colgroup.remove();
|
|
settings.order = [];
|
|
settings.orderFixed = [];
|
|
sortingClasses(settings);
|
|
jqTable
|
|
.find('th, td')
|
|
.classRemove(Object.values(ext.type.className).join(' '));
|
|
Dom.s(thead)
|
|
.find('th, td')
|
|
.classRemove(orderClasses.none +
|
|
' ' +
|
|
orderClasses.canAsc +
|
|
' ' +
|
|
orderClasses.canDesc +
|
|
' ' +
|
|
orderClasses.isAsc +
|
|
' ' +
|
|
orderClasses.isDesc)
|
|
.css('width', '')
|
|
.attrRemove('aria-sort');
|
|
// Add the TR elements back into the table in their original order
|
|
jqTbody.children().detach();
|
|
jqTbody.append(rows);
|
|
var orig = settings.tableWrapper.parentNode;
|
|
var insertBefore = settings.tableWrapper.nextSibling;
|
|
// Remove the DataTables generated nodes, events and classes
|
|
var removedMethod = remove ? 'remove' : 'detach';
|
|
jqTable[removedMethod]();
|
|
jqWrapper[removedMethod]();
|
|
// If we need to reattach the table to the document
|
|
if (!remove && orig) {
|
|
// insertBefore acts like appendChild if !arg[1]
|
|
orig.insertBefore(table, insertBefore);
|
|
// Restore the width of the original table - was read from the style property,
|
|
// so we can restore directly to that
|
|
jqTable.css('width', settings + 'px').classRemove(classes.table);
|
|
}
|
|
/* Remove the settings object from the settings array */
|
|
var idx = ext.settings.indexOf(settings);
|
|
if (idx !== -1) {
|
|
ext.settings.splice(idx, 1);
|
|
}
|
|
});
|
|
});
|
|
// i18n method for extensions to be able to use the language object from the
|
|
// DataTable
|
|
register('i18n()', function (token, def, plural) {
|
|
var ctx = this.context[0];
|
|
var resolved = util.get(token)(ctx.language);
|
|
if (resolved === undefined) {
|
|
resolved = def;
|
|
}
|
|
if (util.is.plainObject(resolved)) {
|
|
if (plural !== false) {
|
|
resolved =
|
|
plural !== undefined && resolved[plural] !== undefined
|
|
? resolved[plural]
|
|
: resolved._;
|
|
}
|
|
}
|
|
return typeof resolved === 'string'
|
|
? resolved.replace('%d', plural) // nb: plural might be undefined,
|
|
: resolved;
|
|
});
|
|
// Needed for header and footer, so pulled into its own function
|
|
function cleanHeader(node, className) {
|
|
let headerCell = Dom.s(node);
|
|
headerCell.find('.dt-column-order').remove();
|
|
headerCell.find('.dt-column-title').each(function (el) {
|
|
let cell = Dom.s(el);
|
|
var title = cell.html();
|
|
cell.parent().parent().html(title);
|
|
cell.remove();
|
|
});
|
|
headerCell.find('div.dt-column-' + className).remove();
|
|
headerCell.find('th, td').attrRemove('data-dt-column');
|
|
}
|
|
|
|
const __reload = function (settings, holdPosition, callback) {
|
|
// Use the draw event to trigger a callback
|
|
if (callback) {
|
|
var api = new Api(settings);
|
|
api.one('draw', function () {
|
|
callback(api.ajax.json());
|
|
});
|
|
}
|
|
if (dataSource(settings) == 'ssp') {
|
|
reDraw(settings, holdPosition);
|
|
}
|
|
else {
|
|
processingDisplay(settings, true);
|
|
// Cancel an existing request
|
|
var xhr = settings.jqXHR;
|
|
if (xhr && xhr.readyState !== 4 && typeof xhr.abort === 'function') {
|
|
xhr.abort();
|
|
}
|
|
// Trigger xhr
|
|
buildAjax(settings, {}, function (json) {
|
|
clearTable(settings);
|
|
var data = ajaxDataSrc(settings, json, false);
|
|
for (var i = 0, iLen = data.length; i < iLen; i++) {
|
|
addData(settings, data[i]);
|
|
}
|
|
reDraw(settings, holdPosition);
|
|
initComplete(settings);
|
|
processingDisplay(settings, false);
|
|
});
|
|
}
|
|
};
|
|
register('ajax.json()', function () {
|
|
var ctx = this.context;
|
|
if (ctx.length > 0) {
|
|
return ctx[0].json;
|
|
}
|
|
// else return undefined;
|
|
});
|
|
register('ajax.params()', function () {
|
|
var ctx = this.context;
|
|
if (ctx.length > 0) {
|
|
return ctx[0].ajaxData;
|
|
}
|
|
// else return undefined;
|
|
});
|
|
register('ajax.reload()', function (callback, resetPaging) {
|
|
return this.iterator('table', function (settings) {
|
|
__reload(settings, resetPaging === false, callback);
|
|
});
|
|
});
|
|
register('ajax.url()', function (url) {
|
|
var ctx = this.context;
|
|
if (url === undefined) {
|
|
// get
|
|
if (ctx.length === 0) {
|
|
return undefined;
|
|
}
|
|
let context = ctx[0];
|
|
return util.is.plainObject(context.ajax)
|
|
? context.ajax.url
|
|
: context.ajax;
|
|
}
|
|
// set
|
|
return this.iterator('table', function (settings) {
|
|
if (util.is.plainObject(settings.ajax)) {
|
|
settings.ajax.url = url;
|
|
}
|
|
else {
|
|
settings.ajax = url;
|
|
}
|
|
}, true);
|
|
});
|
|
register('ajax.url().load()', function (callback, resetPaging) {
|
|
// Same as a reload, but makes sense to present it for easy access after
|
|
// a url change
|
|
return this.iterator('table', function (ctx) {
|
|
__reload(ctx, resetPaging === false, callback);
|
|
});
|
|
});
|
|
|
|
function selectCells(settings, selector, opts) {
|
|
var data = settings.data;
|
|
var rows = selectorRowIndexes(settings, opts);
|
|
var allCells;
|
|
var row;
|
|
var columns = settings.columns.length;
|
|
var a, i, iLen, j, o, host;
|
|
var run = function (s) {
|
|
var fnSelector = typeof s === 'function';
|
|
if (s === null || s === undefined || fnSelector) {
|
|
// All cells and function selectors
|
|
a = [];
|
|
for (i = 0, iLen = rows.length; i < iLen; i++) {
|
|
row = rows[i];
|
|
for (j = 0; j < columns; j++) {
|
|
o = {
|
|
row: row,
|
|
column: j
|
|
};
|
|
if (fnSelector) {
|
|
// Selector - function
|
|
host = data[row];
|
|
if (s(o, getCellData(settings, row, j), host && host.cells ? host.cells[j] : null)) {
|
|
a.push(o);
|
|
}
|
|
}
|
|
else {
|
|
// Selector - all
|
|
a.push(o);
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
// Selector - index
|
|
if (plainObject(s)) {
|
|
// Valid cell index and its in the array of selectable rows
|
|
return s.column !== undefined &&
|
|
s.row !== undefined &&
|
|
rows.indexOf(s.row) !== -1
|
|
? [s]
|
|
: [];
|
|
}
|
|
// Only get the nodes if we get these far in the selector and need to
|
|
// actually work with the cell nodes.
|
|
if (!allCells) {
|
|
let cells = removeEmpty(pluckOrder(data, rows, 'cells'));
|
|
allCells = Dom.s(flatten([], cells));
|
|
}
|
|
// Selector - jQuery filtered cells
|
|
let jqResult = allCells.filter(s).mapTo((el) => {
|
|
return {
|
|
// use a new object, in case someone changes the values
|
|
row: el._DT_CellIndex.row,
|
|
column: el._DT_CellIndex.column
|
|
};
|
|
});
|
|
if (jqResult.length || !s.nodeName) {
|
|
return jqResult;
|
|
}
|
|
// Otherwise the selector is a node, and there is one last option - the
|
|
// element might be a child of an element which has dt-row and dt-column
|
|
// data attributes
|
|
let rowHost = Dom.s(s).closest('*[data-dt-row]');
|
|
let columnHost = Dom.s(s).closest('*[data-dt-column]');
|
|
return rowHost.count()
|
|
? [
|
|
{
|
|
row: parseInt(rowHost.attr('data-dt-row')),
|
|
column: parseInt(columnHost.attr('data-dt-column'))
|
|
}
|
|
]
|
|
: [];
|
|
};
|
|
return selectorRun('cell', selector, run, settings, opts);
|
|
}
|
|
register('cells()', function (arg1, arg2, arg3) {
|
|
// // Argument shifting
|
|
let rowSelector = null;
|
|
let columnSelector = null;
|
|
let cellSelector;
|
|
let opts;
|
|
// Argument shifting
|
|
if (plainObject(arg1)) {
|
|
if (arg1.row === undefined) {
|
|
// Selector modifier only overload
|
|
opts = arg1;
|
|
}
|
|
else {
|
|
// Cell selector as an index object
|
|
cellSelector = arg1;
|
|
opts = arg2;
|
|
}
|
|
}
|
|
else if (plainObject(arg2) || arg2 === undefined) {
|
|
// Cell selector overload
|
|
cellSelector = arg1;
|
|
opts = arg2;
|
|
}
|
|
else if (arg1 !== undefined) {
|
|
// Row + column selector overload
|
|
rowSelector = arg1;
|
|
columnSelector = arg2;
|
|
opts = arg3;
|
|
}
|
|
// Cell selector (if there is no column selector, then it must be)
|
|
if (columnSelector === null) {
|
|
return this.iterator('table', function (settings) {
|
|
return selectCells(settings, cellSelector, selectorOpts(opts));
|
|
});
|
|
}
|
|
// The default built in options need to apply to row and columns
|
|
let internalOpts = opts
|
|
? {
|
|
page: opts.page,
|
|
order: opts.order,
|
|
search: opts.search
|
|
}
|
|
: {};
|
|
// Row + column selector
|
|
let columns = this.columns(columnSelector, internalOpts);
|
|
let rows = this.rows(rowSelector, internalOpts);
|
|
let i, iLen, j, jen;
|
|
let cellsNoOpts = this.iterator('table', function (settings, idx) {
|
|
let a = [];
|
|
for (i = 0, iLen = rows[idx].length; i < iLen; i++) {
|
|
for (j = 0, jen = columns[idx].length; j < jen; j++) {
|
|
a.push({
|
|
row: rows[idx][i],
|
|
column: columns[idx][j]
|
|
});
|
|
}
|
|
}
|
|
return a;
|
|
}, true);
|
|
// There is currently only one extension which uses a cell selector
|
|
// extension It is a _major_ performance drag to run this if it isn't
|
|
// needed, so this is an extension specific check at the moment
|
|
let cells = opts && opts.selected
|
|
? this.cells(cellsNoOpts.toArray(), opts)
|
|
: cellsNoOpts;
|
|
assign(cells.selector, {
|
|
cols: columnSelector,
|
|
rows: rowSelector,
|
|
opts: opts
|
|
});
|
|
return cells;
|
|
});
|
|
register('cells().every()', function (fn) {
|
|
var opts = this.selector.opts;
|
|
var counter = 0;
|
|
return this.iterator('every', (settings, selectedIdx, tableIdx) => {
|
|
let inst = this.cell(selectedIdx, opts);
|
|
fn.call(inst, inst[0][0].row, inst[0][0].column, tableIdx, counter);
|
|
counter++;
|
|
});
|
|
});
|
|
registerPlural('cells().nodes()', 'cell().node()', function () {
|
|
return this.iterator('cell', function (settings, row, column) {
|
|
var data = settings.data[row];
|
|
return data && data.cells ? data.cells[column] : undefined;
|
|
}, true);
|
|
});
|
|
register('cells().data()', function () {
|
|
return this.iterator('cell', function (settings, row, column) {
|
|
return getCellData(settings, row, column);
|
|
}, true);
|
|
});
|
|
registerPlural('cells().render()', 'cell().render()', function (type) {
|
|
return this.iterator('cell', function (settings, row, column) {
|
|
return getCellData(settings, row, column, type);
|
|
}, true);
|
|
});
|
|
registerPlural('cells().indexes()', 'cell().index()', function () {
|
|
return this.iterator('cell', function (settings, row, column) {
|
|
return {
|
|
row: row,
|
|
column: column,
|
|
columnVisible: columnIndexToVisible(settings, column)
|
|
};
|
|
}, true);
|
|
});
|
|
registerPlural('cells().invalidate()', 'cell().invalidate()', function (src) {
|
|
return this.iterator('cell', function (settings, row, column) {
|
|
invalidateRow(settings, row, src, column);
|
|
});
|
|
});
|
|
register('cell()', function (rowSelector, columnSelector, opts) {
|
|
return selectorFirst(this.cells(rowSelector, columnSelector, opts));
|
|
});
|
|
register('cell().data()', function (data) {
|
|
var ctx = this.context;
|
|
var cell = this[0];
|
|
if (data === undefined) {
|
|
// Get
|
|
return ctx.length && cell.length
|
|
? getCellData(ctx[0], cell[0].row, cell[0].column)
|
|
: undefined;
|
|
}
|
|
// Set
|
|
setCellData(ctx[0], cell[0].row, cell[0].column, data);
|
|
invalidateRow(ctx[0], cell[0].row, 'data', cell[0].column);
|
|
return this;
|
|
});
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* Columns
|
|
*
|
|
* {integer} - column index (>=0 count from left, <0 count from right)
|
|
* "{integer}:visIdx" - visible column index (i.e. translate to column index) (>=0 count from left, <0 count from right)
|
|
* "{integer}:visible" - alias for {integer}:visIdx (>=0 count from left, <0 count from right)
|
|
* "{string}:name" - column name
|
|
* "{string}" - jQuery selector on column header nodes
|
|
*
|
|
*/
|
|
// can be an array of these items, comma separated list, or an array of comma
|
|
// separated lists
|
|
const __re_column_selector = /^(.*?):(name|title|visIdx|visible)$/;
|
|
// r1 and r2 are redundant - but it means that the parameters match for the
|
|
// iterator callback in columns().data()
|
|
function columnData(settings, column, r1, r2, rows, type) {
|
|
let a = [];
|
|
for (let row = 0, iLen = rows.length; row < iLen; row++) {
|
|
a.push(getCellData(settings, rows[row], column, type));
|
|
}
|
|
return a;
|
|
}
|
|
function columnHeader(settings, column, row) {
|
|
var header = settings.header;
|
|
var titleRow = settings.titleRow;
|
|
var target = 0;
|
|
if (row !== undefined) {
|
|
target = row;
|
|
}
|
|
else if (titleRow === true) {
|
|
// legacy orderCellsTop support
|
|
target = 0;
|
|
}
|
|
else if (titleRow === false) {
|
|
target = header.length - 1;
|
|
}
|
|
else if (titleRow !== null) {
|
|
target = titleRow;
|
|
}
|
|
else {
|
|
// Automatic - find the _last_ unique cell from the top that is not empty (last for
|
|
// backwards compatibility)
|
|
for (var i = 0; i < header.length; i++) {
|
|
if (header[i][column].unique &&
|
|
Dom.s(header[i][column].cell).find('.dt-column-title').text()) {
|
|
target = i;
|
|
}
|
|
}
|
|
if (target === null) {
|
|
target = 0;
|
|
}
|
|
}
|
|
return header[target][column].cell;
|
|
}
|
|
function selectColumns(settings, selector, opts) {
|
|
var columns = settings.columns, names, titles;
|
|
var run = function (s) {
|
|
var selInt = intVal(s);
|
|
// Selector - all
|
|
if (s === '') {
|
|
return range(columns.length);
|
|
}
|
|
// Selector - index
|
|
if (selInt !== null) {
|
|
return [
|
|
selInt >= 0
|
|
? selInt // Count from left
|
|
: columns.length + selInt // Count from right (+ because its a negative value)
|
|
];
|
|
}
|
|
// Selector = function
|
|
if (typeof s === 'function') {
|
|
var rows = selectorRowIndexes(settings, opts);
|
|
return columns.map(function (col, idx) {
|
|
return s(idx, columnData(settings, idx, 0, 0, rows), columnHeader(settings, idx))
|
|
? idx
|
|
: null;
|
|
});
|
|
}
|
|
// String selector
|
|
var match = typeof s === 'string' ? s.match(__re_column_selector) : '';
|
|
if (match) {
|
|
switch (match[2]) {
|
|
case 'visIdx':
|
|
case 'visible':
|
|
// Selector is a column index
|
|
if (match[1] && match[1].match(/^\d+$/)) {
|
|
var idx = parseInt(match[1], 10);
|
|
// Visible index given, convert to column index
|
|
if (idx < 0) {
|
|
// Counting from the right
|
|
var visColumns = columns.map(function (col, i) {
|
|
return col.visible ? i : null;
|
|
});
|
|
return [visColumns[visColumns.length + idx]];
|
|
}
|
|
// Counting from the left
|
|
return [visibleToColumnIndex(settings, idx)];
|
|
}
|
|
return columns.map(function (col, mapIdx) {
|
|
// Not visible, can't match
|
|
if (!col.visible) {
|
|
return null;
|
|
}
|
|
if (col.responsiveVisible === false) {
|
|
return null;
|
|
}
|
|
// Selector
|
|
if (match && match[1]) {
|
|
let columnElements = columnCells(settings.header, null, col.idx);
|
|
return Dom.s(columnElements)
|
|
.filter(match[1])
|
|
.count() > 0
|
|
? mapIdx
|
|
: null;
|
|
}
|
|
// `:visible` on its own
|
|
return mapIdx;
|
|
});
|
|
case 'name':
|
|
// Don't get names, unless needed, and only get once if it is
|
|
if (!names) {
|
|
names = pluck(columns, 'name');
|
|
}
|
|
// match by name. `names` is column index complete and in
|
|
// order
|
|
return names.map(function (name, i) {
|
|
return match && name === match[1] ? i : null;
|
|
});
|
|
case 'title':
|
|
if (!titles) {
|
|
titles = pluck(columns, 'title');
|
|
}
|
|
// match by column title
|
|
return titles.map(function (title, i) {
|
|
return match && title === match[1] ? i : null;
|
|
});
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
// Cell in the table body
|
|
if (s.nodeName && s._DT_CellIndex) {
|
|
return [s._DT_CellIndex.column];
|
|
}
|
|
// Selector on the TH elements for the columns
|
|
var result = Dom.s(columnCells(settings.header))
|
|
.filter(s)
|
|
.mapTo(el => {
|
|
return columnsFromHeader(el);
|
|
})
|
|
.flat()
|
|
.sort(function (a, b) {
|
|
return a - b;
|
|
});
|
|
if (result.length || !s.nodeName) {
|
|
return result;
|
|
}
|
|
// Otherwise a node which might have a `dt-column` data attribute, or be
|
|
// a child or such an element
|
|
var host = Dom.s(s).closest('*[data-dt-column]');
|
|
return host.count() ? [parseInt(host.attr('data-dt-column'))] : [];
|
|
};
|
|
var selected = selectorRun('column', selector, run, settings, opts);
|
|
return opts.columnOrder && opts.columnOrder === 'index'
|
|
? selected.sort(function (a, b) {
|
|
return a - b;
|
|
})
|
|
: selected; // implied
|
|
}
|
|
function setColumnVis(settings, column, vis) {
|
|
var cols = settings.columns, col = cols[column], data = settings.data, cells, i, iLen, tr;
|
|
// Get
|
|
if (vis === undefined) {
|
|
return col.visible;
|
|
}
|
|
// Set
|
|
// No change
|
|
if (col.visible === vis) {
|
|
return false;
|
|
}
|
|
if (vis) {
|
|
// Insert column
|
|
// Need to decide if we should use appendChild or insertBefore
|
|
var insertBefore = pluck(cols, 'visible').indexOf(true, column + 1);
|
|
for (i = 0, iLen = data.length; i < iLen; i++) {
|
|
let row = data[i];
|
|
if (row) {
|
|
tr = row.tr;
|
|
cells = row.cells;
|
|
if (tr) {
|
|
// insertBefore can act like appendChild if 2nd arg is null
|
|
tr.insertBefore(cells[column], cells[insertBefore] || null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// Remove column
|
|
Dom.s(removeEmpty(pluck(settings.data, 'cells', column))).detach();
|
|
}
|
|
// Common actions
|
|
col.visible = vis;
|
|
colGroup(settings);
|
|
return true;
|
|
}
|
|
register('columns()', function (arg1, arg2) {
|
|
let selector;
|
|
let opts;
|
|
// argument shifting
|
|
if (arg1 === undefined) {
|
|
selector = '';
|
|
}
|
|
else if (plainObject(arg1)) {
|
|
selector = '';
|
|
arg2 = arg1;
|
|
}
|
|
else {
|
|
selector = arg1;
|
|
}
|
|
opts = selectorOpts(arg2);
|
|
let inst = this.iterator('table', settings => selectColumns(settings, selector, opts), true);
|
|
// Want argument shifting here and in _row_selector?
|
|
inst.selector.cols = selector;
|
|
inst.selector.opts = opts;
|
|
return inst;
|
|
});
|
|
register('columns().every()', function (fn) {
|
|
var opts = this.selector.opts;
|
|
var counter = 0;
|
|
return this.iterator('every', (settings, selectedIdx, tableIdx) => {
|
|
let inst = this.column(selectedIdx, opts);
|
|
fn.call(inst, selectedIdx, tableIdx, counter);
|
|
counter++;
|
|
});
|
|
});
|
|
registerPlural('columns().header()', 'column().header()', function (row) {
|
|
return this.iterator('column', function (settings, column) {
|
|
return columnHeader(settings, column, row);
|
|
}, true);
|
|
});
|
|
registerPlural('columns().footer()', 'column().footer()', function (row) {
|
|
return this.iterator('column', function (settings, column) {
|
|
var footer = settings.footer;
|
|
if (!footer.length) {
|
|
return null;
|
|
}
|
|
return settings.footer[row !== undefined ? row : 0][column]
|
|
.cell;
|
|
}, true);
|
|
});
|
|
registerPlural('columns().data()', 'column().data()', function () {
|
|
return this.iterator('column-rows', columnData, true);
|
|
});
|
|
registerPlural('columns().render()', 'column().render()', function (type) {
|
|
return this.iterator('column-rows', function (settings, column, i, j, rows) {
|
|
return columnData(settings, column, i, j, rows, type);
|
|
}, true);
|
|
});
|
|
registerPlural('columns().dataSrc()', 'column().dataSrc()', function () {
|
|
return this.iterator('column', function (settings, column) {
|
|
return settings.columns[column].data;
|
|
}, true);
|
|
});
|
|
registerPlural('columns().init()', 'column().init()', function () {
|
|
return this.iterator('column', function (settings, column) {
|
|
return settings.columns[column];
|
|
}, true);
|
|
});
|
|
registerPlural('columns().names()', 'column().name()', function () {
|
|
return this.iterator('column', function (settings, column) {
|
|
return settings.columns[column].name;
|
|
}, true);
|
|
});
|
|
registerPlural('columns().nodes()', 'column().nodes()', function () {
|
|
return this.iterator('column-rows', function (settings, column, i, j, rows) {
|
|
return removeEmpty(pluckOrder(settings.data, rows, 'cells', column));
|
|
}, true);
|
|
});
|
|
registerPlural('columns().titles()', 'column().title()', function (title, row) {
|
|
return this.iterator('column', function (settings, column) {
|
|
// Argument shifting
|
|
if (typeof title === 'number') {
|
|
row = title;
|
|
title = undefined;
|
|
}
|
|
var span = Dom.s(this.column(column).header(row)).find('.dt-column-title');
|
|
if (title !== undefined) {
|
|
span.html(title);
|
|
return this;
|
|
}
|
|
return span.html();
|
|
}, true);
|
|
});
|
|
registerPlural('columns().types()', 'column().type()', function () {
|
|
return this.iterator('column', function (settings, column) {
|
|
var colObj = settings.columns[column];
|
|
var type = colObj.type;
|
|
// If the type was invalidated, then resolve it. This actually
|
|
// does all columns at the moment. Would only happen once if
|
|
// getting all column's data types.
|
|
if (!type) {
|
|
columnTypes(settings);
|
|
type = colObj.type;
|
|
}
|
|
return type;
|
|
}, true);
|
|
});
|
|
registerPlural('columns().visible()', 'column().visible()', function (vis, calc) {
|
|
var that = this;
|
|
var changed = [];
|
|
var ret = this.iterator('column', function (settings, column) {
|
|
if (vis === undefined) {
|
|
return settings.columns[column].visible;
|
|
} // else
|
|
if (setColumnVis(settings, column, vis)) {
|
|
changed.push(column);
|
|
}
|
|
});
|
|
// Group the column visibility changes
|
|
if (vis !== undefined) {
|
|
this.iterator('table', function (settings) {
|
|
// Redraw the header after changes
|
|
drawHead(settings, settings.header);
|
|
drawHead(settings, settings.footer);
|
|
// Update colspan for no records display. Child rows and
|
|
// extensions will use their own listeners to do this - only
|
|
// need to update the empty table item here
|
|
if (!settings.display.length) {
|
|
Dom.s(settings.tbody)
|
|
.find('td[colspan]')
|
|
.attr('colspan', visibleColumns(settings));
|
|
}
|
|
saveState(settings);
|
|
// Second loop once the first is done for events
|
|
that.iterator('column', function (ctx, column) {
|
|
if (changed.includes(column)) {
|
|
callbackFire(ctx, null, 'column-visibility', [
|
|
ctx,
|
|
column,
|
|
vis,
|
|
calc
|
|
]);
|
|
}
|
|
});
|
|
if (changed.length && (calc === undefined || calc)) {
|
|
that.columns.adjust();
|
|
}
|
|
});
|
|
}
|
|
return ret;
|
|
});
|
|
registerPlural('columns().widths()', 'column().width()', function () {
|
|
// Injects a fake row into the table for just a moment so the widths can
|
|
// be read, regardless of colspan in the header and rows being present
|
|
// in the body
|
|
var columns = this.columns(':visible');
|
|
var row = Dom.c('tr').html('<td>' + Array(columns.count()).join('</td><td>') + '</td>');
|
|
Dom.s(this.table().body()).append(row);
|
|
var widths = [];
|
|
var indexes = columns.indexes();
|
|
row.children().each((el, idx) => {
|
|
widths[indexes[idx]] = Dom.s(el).width('outer');
|
|
});
|
|
row.remove();
|
|
return this.iterator('column', (settings, column) => {
|
|
return widths[column] || 0;
|
|
}, true);
|
|
});
|
|
registerPlural('columns().indexes()', 'column().index()', function (type) {
|
|
return this.iterator('column', function (settings, column) {
|
|
return type === 'visible'
|
|
? columnIndexToVisible(settings, column)
|
|
: column;
|
|
}, true);
|
|
});
|
|
register('columns.adjust()', function () {
|
|
return this.iterator('table', function (settings) {
|
|
// Force a column sizing to happen with a manual call - otherwise it
|
|
// can skip if the size hasn't changed
|
|
settings.containerWidth = -1;
|
|
adjustColumnSizing(settings);
|
|
}, true);
|
|
});
|
|
register('column.index()', function (type, idx) {
|
|
if (this.context.length !== 0) {
|
|
var ctx = this.context[0];
|
|
if (type === 'fromVisible' || type === 'toData') {
|
|
return visibleToColumnIndex(ctx, idx);
|
|
}
|
|
else if (type === 'fromData' || type === 'toVisible') {
|
|
return columnIndexToVisible(ctx, idx);
|
|
}
|
|
}
|
|
return -1;
|
|
});
|
|
register('column()', function (selector, opts) {
|
|
return selectorFirst(this.columns(selector, opts));
|
|
});
|
|
|
|
/**
|
|
* Redraw the tables in the current context.
|
|
*/
|
|
Api.register('draw()', function (paging) {
|
|
return this.iterator('table', function (settings) {
|
|
if (paging === 'page') {
|
|
draw(settings);
|
|
}
|
|
else {
|
|
if (typeof paging === 'string') {
|
|
paging = paging === 'full-hold' ? false : true;
|
|
}
|
|
reDraw(settings, paging === false);
|
|
}
|
|
});
|
|
});
|
|
|
|
register('order()', function (order, dir) {
|
|
let ctx = this.context;
|
|
let args = Array.prototype.slice.call(arguments);
|
|
if (order === undefined) {
|
|
// get
|
|
return ctx.length !== 0 ? ctx[0].order : undefined;
|
|
}
|
|
// set
|
|
if (typeof order === 'number' && typeof dir === 'string') {
|
|
// Simple column / direction passed in
|
|
order = [[order, dir]];
|
|
}
|
|
else if (args.length > 1) {
|
|
// Arguments passed in (list of 1D arrays)
|
|
order = args;
|
|
}
|
|
// otherwise a 2D array was passed in
|
|
return this.iterator('table', function (settings) {
|
|
let resolved = [];
|
|
sortResolve(settings, resolved, order);
|
|
settings.order = resolved;
|
|
});
|
|
});
|
|
register('order.listener()', function (node, column, callback) {
|
|
return this.iterator('table', function (settings) {
|
|
sortAttachListener(settings, node, '', column, callback);
|
|
});
|
|
});
|
|
register('order.fixed()', function (set) {
|
|
if (!set) {
|
|
var ctx = this.context;
|
|
var fixed = ctx.length ? ctx[0].orderFixed : undefined;
|
|
return Array.isArray(fixed) ? { pre: fixed } : fixed;
|
|
}
|
|
return this.iterator('table', function (settings) {
|
|
settings.orderFixed = assignDeep({}, set);
|
|
});
|
|
});
|
|
// Order by the selected column(s)
|
|
register(['columns().order()', 'column().order()'], function (dir) {
|
|
var that = this;
|
|
if (!dir) {
|
|
return this.iterator('column', function (settings, idx) {
|
|
var sort = sortFlatten(settings);
|
|
for (var i = 0, iLen = sort.length; i < iLen; i++) {
|
|
if (sort[i].col === idx) {
|
|
return sort[i].dir;
|
|
}
|
|
}
|
|
return null;
|
|
}, true);
|
|
}
|
|
else {
|
|
return this.iterator('table', function (settings, i) {
|
|
settings.order = that[i].map(function (col) {
|
|
return [col, dir];
|
|
});
|
|
});
|
|
}
|
|
});
|
|
registerPlural('columns().orderable()', 'column().orderable()', function (directions) {
|
|
return this.iterator('column', function (settings, idx) {
|
|
var col = settings.columns[idx];
|
|
return directions ? col.orderSequence : col.orderable;
|
|
}, true);
|
|
});
|
|
|
|
/**
|
|
* Set the page length
|
|
*
|
|
* @param ctx DataTables context
|
|
* @param val Value to change to
|
|
*/
|
|
function lengthChange(ctx, val) {
|
|
let len = typeof val === 'string' ? parseInt(val, 10) : val;
|
|
ctx.pageLength = len;
|
|
lengthOverflow(ctx);
|
|
// Fire length change event
|
|
callbackFire(ctx, null, 'length', [ctx, len]);
|
|
}
|
|
|
|
register('page()', function (action) {
|
|
if (action === undefined) {
|
|
return this.page.info().page; // not an expensive call
|
|
}
|
|
// else, have an action to take on all tables
|
|
return this.iterator('table', function (settings) {
|
|
pageChange(settings, action);
|
|
});
|
|
});
|
|
register('page.info()', function () {
|
|
var settings = this.context[0], start = settings.displayStart, len = settings.features.paging ? settings.pageLength : -1, visRecords = recordsDisplay(settings), all = len === -1;
|
|
return {
|
|
page: all ? 0 : Math.floor(start / len),
|
|
pages: all ? 1 : Math.ceil(visRecords / len),
|
|
start: start,
|
|
end: displayEnd(settings),
|
|
length: len,
|
|
recordsTotal: recordsTotal(settings),
|
|
recordsDisplay: visRecords,
|
|
serverSide: dataSource(settings) === 'ssp'
|
|
};
|
|
});
|
|
register('page.len()', function (len) {
|
|
// Note that we can't call this function 'length()' because `length` is a
|
|
// JavaScript property of functions which defines how many arguments the
|
|
// function expects.
|
|
if (len === undefined || len === null) {
|
|
return this.context.length !== 0
|
|
? this.context[0].pageLength
|
|
: undefined;
|
|
}
|
|
// else, set the page length
|
|
return this.iterator('table', function (settings) {
|
|
lengthChange(settings, len);
|
|
});
|
|
});
|
|
|
|
register('processing()', function (show) {
|
|
return this.iterator('table', ctx => processingDisplay(ctx, show));
|
|
});
|
|
|
|
// Add the state event handler in time for the initial draw to save state
|
|
Dom.s(document).on('preInit.dt', function (e, context) {
|
|
var api = new Api(context);
|
|
api.on('stateSaveParams.DT', function (ev, settings, d) {
|
|
// This could be more compact with the API, but it is a lot faster as a
|
|
// simple internal loop
|
|
var idFn = settings.rowIdFn;
|
|
var rows = settings.displayMaster;
|
|
var ids = [];
|
|
for (var i = 0; i < rows.length; i++) {
|
|
var rowIdx = rows[i];
|
|
var row = settings.data[rowIdx];
|
|
if (row.detailsShow) {
|
|
ids.push('#' + idFn(row.data));
|
|
}
|
|
}
|
|
d.childRows = ids;
|
|
});
|
|
// For future state loads (e.g. with StateRestore)
|
|
api.on('stateLoaded.DT', function (ev, settings, state) {
|
|
detailsStateLoad(api, state);
|
|
});
|
|
});
|
|
// But initial details can wait until the end
|
|
Dom.s(document).on('plugin-init.dt', function (e, context) {
|
|
var api = context.api;
|
|
// And the initial load state
|
|
detailsStateLoad(api, api.state.loaded());
|
|
});
|
|
function detailsStateLoad(api, state) {
|
|
if (state && state.childRows) {
|
|
api.rows(state.childRows.map(function (id) {
|
|
// Escape any `:` characters from the row id. Accounts for
|
|
// already escaped characters.
|
|
return id.replace(/([^:\\]*(?:\\.[^:\\]*)*):/g, '$1\\:');
|
|
})).every(function () {
|
|
callbackFire(api.settings()[0], null, 'requestChild', [this]);
|
|
});
|
|
}
|
|
}
|
|
function detailsAdd(ctx, row, data, klass) {
|
|
if (!row) {
|
|
return;
|
|
}
|
|
// Convert to array of TR elements
|
|
var rows = [];
|
|
var addRow = function (r, k) {
|
|
// Recursion to allow for arrays of jQuery objects
|
|
if (Array.isArray(r) || util.is.jquery(r)) {
|
|
for (var i = 0, iLen = r.length; i < iLen; i++) {
|
|
addRow(r[i], k);
|
|
}
|
|
return;
|
|
}
|
|
// If we get a TR element, then just add it directly - up to the dev
|
|
// to add the correct number of columns etc
|
|
if (r.nodeName && r.nodeName.toLowerCase() === 'tr') {
|
|
r.setAttribute('data-dt-row', row.idx);
|
|
rows.push(r);
|
|
}
|
|
else {
|
|
// Otherwise create a row with a wrapper
|
|
let td = Dom.c('td').classAdd(k);
|
|
let created = Dom
|
|
.c('tr')
|
|
.append(td)
|
|
.attr('data-dt-row', row.idx)
|
|
.classAdd(k);
|
|
if (r.nodeName) {
|
|
td.append(r);
|
|
}
|
|
else {
|
|
td.html(r);
|
|
}
|
|
td.get(0).colSpan = visibleColumns(ctx);
|
|
rows.push(created.get(0));
|
|
}
|
|
};
|
|
addRow(data, klass);
|
|
if (row.details) {
|
|
row.details.detach();
|
|
}
|
|
row.details = Dom.s(rows);
|
|
// If the children were already shown, that state should be retained
|
|
if (row.detailsShow && row.tr) {
|
|
row.details.insertAfter(row.tr);
|
|
}
|
|
}
|
|
// Make state saving of child row details async to allow them to be batch
|
|
// processed
|
|
var detailsState = util.throttle(function (ctx) {
|
|
saveState(ctx[0]);
|
|
}, 500);
|
|
function detailsRemove(api, idx) {
|
|
var ctx = api.context;
|
|
if (ctx.length) {
|
|
var row = ctx[0].data[idx !== undefined ? idx : api[0]];
|
|
if (row && row.details) {
|
|
row.details.detach();
|
|
row.detailsShow = undefined;
|
|
row.details = undefined;
|
|
Dom.s(row.tr).classRemove('dt-hasChild');
|
|
detailsState(ctx);
|
|
}
|
|
}
|
|
}
|
|
function detailsDisplay(api, show) {
|
|
var ctx = api.context;
|
|
if (ctx.length && api.length) {
|
|
var row = ctx[0].data[api[0]];
|
|
if (row && row.details) {
|
|
row.detailsShow = show;
|
|
if (show && row.tr) {
|
|
row.details.insertAfter(row.tr);
|
|
Dom.s(row.tr).classAdd('dt-hasChild');
|
|
}
|
|
else if (!show) {
|
|
row.details.detach();
|
|
Dom.s(row.tr).classRemove('dt-hasChild');
|
|
}
|
|
callbackFire(ctx[0], null, 'childRow', [show, api.row(api[0])]);
|
|
detailsEvents(ctx[0]);
|
|
detailsState(ctx);
|
|
}
|
|
}
|
|
}
|
|
function detailsEvents(settings) {
|
|
var api = new Api(settings);
|
|
var namespace = '.dt.DT_details';
|
|
var drawEvent = 'draw' + namespace;
|
|
var colvisEvent = 'column-sizing' + namespace;
|
|
var destroyEvent = 'destroy' + namespace;
|
|
var data = settings.data;
|
|
api.off(drawEvent + ' ' + colvisEvent + ' ' + destroyEvent);
|
|
if (util.array.pluck(data, 'details').length > 0) {
|
|
// On each draw, insert the required elements into the document
|
|
api.on(drawEvent, function (e, ctx) {
|
|
if (settings !== ctx) {
|
|
return;
|
|
}
|
|
api.rows({ page: 'current' })
|
|
.eq(0)
|
|
.each(function (idx) {
|
|
// Internal data grab
|
|
var row = data[idx];
|
|
if (row && row.detailsShow && row.details && row.tr) {
|
|
row.details.insertAfter(row.tr);
|
|
}
|
|
});
|
|
});
|
|
// Column visibility change - update the colspan
|
|
api.on(colvisEvent, function (e, ctx) {
|
|
if (settings !== ctx) {
|
|
return;
|
|
}
|
|
// Update the colspan for the details rows (note, only if it already
|
|
// has a colspan)
|
|
var row, visible = visibleColumns(ctx);
|
|
for (var i = 0, iLen = data.length; i < iLen; i++) {
|
|
row = data[i];
|
|
if (row && row.details) {
|
|
row.details.each(function (el) {
|
|
var td = Dom.s(el).children('td');
|
|
if (td.count() == 1) {
|
|
td.attr('colspan', visible);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
// Table destroyed - nuke any child rows
|
|
api.on(destroyEvent, function (e, ctx) {
|
|
if (settings !== ctx) {
|
|
return;
|
|
}
|
|
for (var i = 0, iLen = data.length; i < iLen; i++) {
|
|
let d = data[i];
|
|
if (d && d.details) {
|
|
detailsRemove(api, i);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Strings for the method names to help minification
|
|
var _emp = '';
|
|
var _child_obj = _emp + 'row().child';
|
|
var _child_mth = _child_obj + '()';
|
|
// data can be:
|
|
// tr
|
|
// string
|
|
// jQuery or array of any of the above
|
|
Api.register(_child_mth, function (data, klass) {
|
|
var _a;
|
|
var ctx = this.context;
|
|
if (data === undefined) {
|
|
// get
|
|
let details = ctx.length && this.length && ctx[0].data[this[0]]
|
|
? (_a = ctx[0].data[this[0]]) === null || _a === void 0 ? void 0 : _a.details
|
|
: undefined;
|
|
return details;
|
|
}
|
|
else if (data === true) {
|
|
// show
|
|
this.child.show();
|
|
}
|
|
else if (data === false) {
|
|
// remove
|
|
detailsRemove(this);
|
|
}
|
|
else if (ctx.length && this.length) {
|
|
// set
|
|
detailsAdd(ctx[0], ctx[0].data[this[0]], data, klass);
|
|
}
|
|
return this.inst(this.context, this);
|
|
});
|
|
Api.register([
|
|
_child_obj + '.show()',
|
|
_child_mth + '.show()' // only when `child()` was called with parameters
|
|
], function () {
|
|
// it returns an object and this method is not executed)
|
|
detailsDisplay(this, true);
|
|
return this;
|
|
});
|
|
Api.register([
|
|
_child_obj + '.hide()',
|
|
_child_mth + '.hide()' // only when `child()` was called with parameters
|
|
], function () {
|
|
// it returns an object and this method is not executed)
|
|
detailsDisplay(this, false);
|
|
return this;
|
|
});
|
|
Api.register([
|
|
_child_obj + '.remove()',
|
|
_child_mth + '.remove()' // only when `child()` was called with parameters
|
|
], function () {
|
|
// it returns an object and this method is not executed)
|
|
detailsRemove(this);
|
|
return this;
|
|
});
|
|
Api.register(_child_obj + '.isShown()', function () {
|
|
var ctx = this.context;
|
|
if (ctx.length && this.length && ctx[0].data[this[0]]) {
|
|
// detailsShown as false or undefined will fall through to return false
|
|
return ctx[0].data[this[0]].detailsShow || false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
* Rows
|
|
*
|
|
* {} - no selector - use all available rows
|
|
* {integer} - row data index
|
|
* {node} - TR node
|
|
* {string} - jQuery selector to apply to the TR elements
|
|
* {array} - jQuery array of nodes, or simply an array of TR nodes
|
|
*
|
|
*/
|
|
function selectRows(settings, selector, opts) {
|
|
var rows;
|
|
var run = function (sel) {
|
|
var selInt = util.conv.intVal(sel);
|
|
var data = settings.data;
|
|
// Short cut - selector is a number and no options provided (default is
|
|
// all records, so no need to check if the index is in there, since it
|
|
// must be - dev error if the index doesn't exist).
|
|
if (selInt !== null && !opts) {
|
|
return [selInt];
|
|
}
|
|
if (!rows) {
|
|
rows = selectorRowIndexes(settings, opts);
|
|
}
|
|
if (selInt !== null && rows.indexOf(selInt) !== -1) {
|
|
// Selector - integer
|
|
return [selInt];
|
|
}
|
|
else if (sel === null || sel === undefined || sel === '') {
|
|
// Selector - none
|
|
return rows;
|
|
}
|
|
// Selector - function
|
|
if (typeof sel === 'function') {
|
|
return rows.map(function (idx) {
|
|
var row = data[idx];
|
|
return row && sel(idx, row.data, row.tr) ? idx : null;
|
|
});
|
|
}
|
|
// Selector - node
|
|
if (sel.nodeName) {
|
|
var rowIdx = sel._DT_RowIndex; // Property added by DT for fast lookup
|
|
var cellIdx = sel._DT_CellIndex;
|
|
var row;
|
|
if (rowIdx !== undefined) {
|
|
// Make sure that the row is actually still present in the table
|
|
row = data[rowIdx];
|
|
return row && row.tr === sel ? [rowIdx] : [];
|
|
}
|
|
else if (cellIdx) {
|
|
row = data[cellIdx.row];
|
|
return row && row.tr === sel.parentNode ? [cellIdx.row] : [];
|
|
}
|
|
else {
|
|
var host = Dom.s(sel).closest('*[data-dt-row]');
|
|
return host.count() ? [parseInt(host.attr('data-dt-row'))] : [];
|
|
}
|
|
}
|
|
// ID selector. Want to always be able to select rows by id, regardless
|
|
// of if the tr element has been created or not, so can't rely upon
|
|
// jQuery here - hence a custom implementation. This does not match
|
|
// Sizzle's fast selector or HTML4 - in HTML5 the ID can be anything,
|
|
// but to select it using a CSS selector engine (like Sizzle or
|
|
// querySelect) it would need to need to be escaped for some characters.
|
|
// DataTables simplifies this for row selectors since you can select
|
|
// only a row. A # indicates an id any anything that follows is the id -
|
|
// unescaped.
|
|
if (typeof sel === 'string') {
|
|
if (sel.charAt(0) === '#') {
|
|
// get row index from id
|
|
var rowObj = settings.ids[sel.replace(/^#/, '')];
|
|
if (rowObj !== undefined) {
|
|
return [rowObj.idx];
|
|
}
|
|
// need to fall through to selector in case there is DOM id that
|
|
// matches
|
|
}
|
|
else if (sel.match(/^(tr)?:eq\(\d+\)$/)) {
|
|
// :eq() selector to get a row based on its position in the
|
|
// selectable rows.
|
|
let idx = parseInt(sel.replace(/[^\d]/g, ''));
|
|
return rows[idx] !== undefined ? [rows[idx]] : [];
|
|
}
|
|
}
|
|
// Get nodes in the order from the `rows` array with null values removed
|
|
var nodes = util.array.removeEmpty(util.array.pluckOrder(settings.data, rows, 'tr'));
|
|
// Selector - selector string, array of nodes or jQuery object.
|
|
return Dom.s(nodes)
|
|
.filter(sel)
|
|
.mapTo((el) => el._DT_RowIndex);
|
|
};
|
|
var matched = selectorRun('row', selector, run, settings, opts);
|
|
if (opts.order === 'current' || opts.order === 'applied') {
|
|
sortDisplay(settings, matched);
|
|
}
|
|
return matched;
|
|
}
|
|
register('rows()', function (arg1, arg2) {
|
|
let opts;
|
|
let selector;
|
|
// argument shifting
|
|
if (arg1 === undefined) {
|
|
// All rows - no selector or modifier
|
|
selector = '';
|
|
}
|
|
else if (util.is.plainObject(arg1)) {
|
|
// Arg1 is modifier overload
|
|
selector = '';
|
|
opts = arg1;
|
|
}
|
|
else {
|
|
selector = arg1;
|
|
opts = arg2;
|
|
}
|
|
opts = selectorOpts(opts);
|
|
var inst = this.iterator('table', function (settings) {
|
|
return selectRows(settings, selector, opts);
|
|
}, true);
|
|
// Want argument shifting here and in row_selector?
|
|
inst.selector.rows = selector;
|
|
inst.selector.opts = opts;
|
|
return inst;
|
|
});
|
|
register('rows().every()', function (fn) {
|
|
var opts = this.selector.opts;
|
|
var counter = 0;
|
|
return this.iterator('every', (settings, selectedIdx, tableIdx) => {
|
|
let inst = this.row(selectedIdx, opts);
|
|
fn.call(inst, selectedIdx, tableIdx, counter);
|
|
counter++;
|
|
});
|
|
});
|
|
register('rows().nodes()', function () {
|
|
return this.iterator('row', function (settings, row) {
|
|
var _a;
|
|
return ((_a = settings.data[row]) === null || _a === void 0 ? void 0 : _a.tr) || undefined;
|
|
}, true);
|
|
});
|
|
register('rows().data()', function () {
|
|
return this.iterator(true, 'rows', function (settings, rows) {
|
|
return util.array.pluckOrder(settings.data, rows, 'data');
|
|
}, true);
|
|
});
|
|
registerPlural('rows().invalidate()', 'row().invalidate()', function (src) {
|
|
return this.iterator('row', function (settings, row) {
|
|
invalidateRow(settings, row, src);
|
|
});
|
|
});
|
|
registerPlural('rows().indexes()', 'row().index()', function () {
|
|
return this.iterator('row', function (settings, row) {
|
|
return row;
|
|
}, true);
|
|
});
|
|
registerPlural('rows().ids()', 'row().id()', function (hash) {
|
|
var _a;
|
|
var a = [];
|
|
var context = this.context;
|
|
// `iterator` will drop undefined values, but in this case we want them
|
|
for (var i = 0, iLen = context.length; i < iLen; i++) {
|
|
for (var j = 0, jen = this[i].length; j < jen; j++) {
|
|
var id = context[i].rowIdFn((_a = context[i].data[this[i][j]]) === null || _a === void 0 ? void 0 : _a.data);
|
|
a.push((hash === true ? '#' : '') + id);
|
|
}
|
|
}
|
|
return this.inst(context, a);
|
|
});
|
|
registerPlural('rows().remove()', 'row().remove()', function () {
|
|
this.iterator('row', function (settings, row) {
|
|
var data = settings.data;
|
|
var rowData = data[row];
|
|
// Delete from the display arrays
|
|
var idx = settings.displayMaster.indexOf(row);
|
|
if (idx !== -1) {
|
|
settings.displayMaster.splice(idx, 1);
|
|
}
|
|
// For server-side processing tables - subtract the deleted row from
|
|
// the count
|
|
if (settings.recordsDisplay > 0) {
|
|
settings.recordsDisplay--;
|
|
}
|
|
// Check for an 'overflow' they case for displaying the table
|
|
lengthOverflow(settings);
|
|
// Remove the row's ID reference if there is one
|
|
var id = settings.rowIdFn(rowData === null || rowData === void 0 ? void 0 : rowData.data);
|
|
if (id !== undefined) {
|
|
delete settings.ids[id];
|
|
}
|
|
data[row] = null;
|
|
});
|
|
return this;
|
|
});
|
|
register('rows.add()', function (rows) {
|
|
var newRows = this.iterator('table', function (settings) {
|
|
var row, i, iLen;
|
|
var out = [];
|
|
for (i = 0, iLen = rows.length; i < iLen; i++) {
|
|
row = rows[i];
|
|
if (row.nodeName && row.nodeName.toUpperCase() === 'TR') {
|
|
out.push(addTr(settings, Dom.s(row))[0]);
|
|
}
|
|
else {
|
|
out.push(addData(settings, row));
|
|
}
|
|
}
|
|
return out;
|
|
}, true);
|
|
// Return an Api.rows() extended instance, so rows().nodes() etc can be used
|
|
var modRows = this.rows(-1);
|
|
modRows.pop();
|
|
arrayApply(modRows, newRows);
|
|
return modRows;
|
|
});
|
|
register('row()', function (selector, opts) {
|
|
return selectorFirst(this.rows(selector, opts));
|
|
});
|
|
register('row().data()', function (data) {
|
|
var _a;
|
|
var ctx = this.context;
|
|
if (data === undefined) {
|
|
// Get
|
|
return ctx.length && this.length && this[0].length
|
|
? (_a = ctx[0].data[this[0]]) === null || _a === void 0 ? void 0 : _a.data
|
|
: undefined;
|
|
}
|
|
// Set
|
|
var row = ctx[0].data[this[0]];
|
|
row.data = data;
|
|
// If the DOM has an id, and the data source is an array
|
|
if (Array.isArray(data) && row.tr && row.tr.id) {
|
|
util.set(ctx[0].rowId)(data, row.tr.id);
|
|
}
|
|
// Automatically invalidate
|
|
invalidateRow(ctx[0], this[0][0], 'data');
|
|
return this;
|
|
});
|
|
register('row().node()', function () {
|
|
var ctx = this.context;
|
|
if (ctx.length && this.length && this[0].length) {
|
|
var row = ctx[0].data[this[0]];
|
|
if (row && row.tr) {
|
|
return row.tr;
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
register('row.add()', function (row) {
|
|
// Allow an array-like object to be passed in - only a single row is added
|
|
// from it though - the first element in the set
|
|
if (row && row.fn && row.length) {
|
|
row = row[0];
|
|
}
|
|
var rows = this.iterator('table', function (settings) {
|
|
// New column could cause a change in the cached column properties such
|
|
// as type and width.
|
|
invalidColumn(settings);
|
|
if (row.nodeName && row.nodeName.toUpperCase() === 'TR') {
|
|
return addTr(settings, Dom.s(row))[0];
|
|
}
|
|
return addData(settings, row);
|
|
});
|
|
// Return an Api.rows() extended instance, with the newly added row selected
|
|
return this.row(rows[0]);
|
|
});
|
|
|
|
register('search()', function (input, regex, smart, caseInsen) {
|
|
if (input === undefined) {
|
|
let ctx = this.context;
|
|
// get
|
|
if (ctx.length === 0) {
|
|
return;
|
|
}
|
|
return ctx[0].searches['*'].search;
|
|
}
|
|
// set
|
|
return this.iterator('table', function (ctx) {
|
|
if (!ctx.features.searching) {
|
|
return;
|
|
}
|
|
let target = ctx.searches['*'];
|
|
if (!target) {
|
|
target = create$1();
|
|
}
|
|
if (typeof regex === 'object') {
|
|
// New style object of options
|
|
assign(target, regex);
|
|
}
|
|
else {
|
|
// Compat for the old options
|
|
assign(target, {
|
|
regex: regex === null ? false : regex,
|
|
smart: smart === null ? true : smart,
|
|
caseInsensitive: caseInsen === null ? true : caseInsen
|
|
});
|
|
}
|
|
target.search = input;
|
|
ctx.searches['*'] = target;
|
|
filterComplete(ctx);
|
|
});
|
|
});
|
|
register('search.fixed()', function (name, search, options) {
|
|
var ret = this.iterator(true, 'table', function (settings) {
|
|
var _a;
|
|
var fixed = settings.searchesFixed['*'];
|
|
if (!name) {
|
|
return Object.keys(fixed);
|
|
}
|
|
else if (search === undefined) {
|
|
return (_a = fixed[name]) === null || _a === void 0 ? void 0 : _a.search;
|
|
}
|
|
else if (search === null) {
|
|
delete fixed[name];
|
|
}
|
|
else {
|
|
let target = fixed[name];
|
|
if (!target || !util.is.plainObject(target)) {
|
|
target = create$1();
|
|
}
|
|
if (options) {
|
|
assign(target, options);
|
|
}
|
|
target.search = search;
|
|
fixed[name] = target;
|
|
}
|
|
return this;
|
|
});
|
|
return name !== undefined && search === undefined ? ret[0] : ret;
|
|
});
|
|
register(['columns().search()', 'column().search()'], function (input, regex, smart, caseInsen) {
|
|
var _a;
|
|
if (input === undefined) {
|
|
let name = this[0].join(',');
|
|
return this.context.length
|
|
? ((_a = this.context[0].searches[name]) === null || _a === void 0 ? void 0 : _a.search) || ''
|
|
: '';
|
|
}
|
|
return this.iterator('columns', function (ctx, columns) {
|
|
let colIdxs = columns.join(',');
|
|
let target = ctx.searches[colIdxs];
|
|
if (!target) {
|
|
target = create$1();
|
|
}
|
|
// Delete the search for custom grouping types if removing
|
|
if ((input === '' || input === null) && columns.length > 1) {
|
|
delete ctx.searches[colIdxs];
|
|
return;
|
|
}
|
|
if (typeof regex === 'object') {
|
|
// New style object of options
|
|
assign(target, regex);
|
|
}
|
|
else {
|
|
// Compat for the old options
|
|
assign(target, {
|
|
regex: regex === null ? false : regex,
|
|
smart: smart === null ? true : smart,
|
|
caseInsensitive: caseInsen === null ? true : caseInsen
|
|
});
|
|
}
|
|
target.search = input;
|
|
target.columns = columns.slice();
|
|
ctx.searches[colIdxs] = target;
|
|
filterComplete(ctx);
|
|
});
|
|
});
|
|
register(['columns().search.fixed()', 'column().search.fixed()'], function (name, search, options) {
|
|
// No name, just return the names of the fixed searches applied to these
|
|
// columns
|
|
if (!name) {
|
|
return this.iterator(true, 'columns', function (settings, columns) {
|
|
let colIdxs = columns.join(',');
|
|
let fixed = settings.searchesFixed[colIdxs];
|
|
return fixed ? Object.keys(fixed) : [];
|
|
});
|
|
}
|
|
// Get the value from an existing search
|
|
if (search === undefined) {
|
|
if (!this.context.length) {
|
|
return undefined;
|
|
}
|
|
else {
|
|
let colIdxs = this[0].join(',');
|
|
let fixed = this.context[0].searchesFixed[colIdxs];
|
|
return fixed && fixed[name] ? fixed[name].search : undefined;
|
|
}
|
|
}
|
|
// Set a search, possibly with options
|
|
return this.iterator(true, 'columns', function (settings, columns) {
|
|
let colIdxs = columns.join(',');
|
|
let fixed = settings.searchesFixed[colIdxs];
|
|
if (!fixed) {
|
|
fixed = {};
|
|
settings.searchesFixed[colIdxs] = fixed;
|
|
}
|
|
if (search === null) {
|
|
delete fixed[name];
|
|
}
|
|
else {
|
|
let target = fixed[name];
|
|
if (!target || !util.is.plainObject(target)) {
|
|
target = create$1();
|
|
}
|
|
if (options) {
|
|
assign(target, options);
|
|
}
|
|
target.search = search;
|
|
target.columns = columns;
|
|
fixed[name] = target;
|
|
}
|
|
return this;
|
|
});
|
|
});
|
|
|
|
register('state()', function (set, ignoreTime = true) {
|
|
// getter
|
|
if (!set) {
|
|
return this.context.length ? this.context[0].stateSaved : null;
|
|
}
|
|
let setMutate = assignDeep({}, set);
|
|
// setter
|
|
return this.iterator('table', function (settings) {
|
|
implementState(settings, setMutate, ignoreTime, function () { });
|
|
});
|
|
});
|
|
register('state.clear()', function () {
|
|
return this.iterator('table', function (settings) {
|
|
// Save an empty object
|
|
settings.stateSaveCallback.call(settings.instance, settings, {});
|
|
});
|
|
});
|
|
register('state.loaded()', function () {
|
|
return this.context.length ? this.context[0].stateLoaded : null;
|
|
});
|
|
register('state.save()', function () {
|
|
return this.iterator('table', function (settings) {
|
|
saveState(settings);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Selector for HTML tables. Apply the given selector to the give array of
|
|
* DataTables settings objects.
|
|
*
|
|
* @param selector Selector string or integer
|
|
* @param a Array of DataTables settings objects to be filtered
|
|
* @return Selected table notes
|
|
*/
|
|
function table_selector(selector, a) {
|
|
if (Array.isArray(selector)) {
|
|
var result = [];
|
|
selector.forEach(function (sel) {
|
|
var inner = table_selector(sel, a);
|
|
arrayApply(result, inner);
|
|
});
|
|
return result.filter(item => !!item);
|
|
}
|
|
// Integer is used to pick out a table by index
|
|
if (typeof selector === 'number') {
|
|
return [a[selector]];
|
|
}
|
|
// Perform a selector on the table nodes
|
|
var nodes = a.map(function (el) {
|
|
return el.table;
|
|
});
|
|
return Dom
|
|
.s(nodes)
|
|
.filter(selector)
|
|
.mapTo(el => {
|
|
// Need to translate back from the table node to the settings
|
|
var idx = nodes.indexOf(el);
|
|
return a[idx];
|
|
});
|
|
}
|
|
register('tables()', function (selector) {
|
|
// A new instance is created if there was a selector specified
|
|
return selector !== undefined && selector !== null
|
|
? this.inst(table_selector(selector, this.context))
|
|
: this.inst(this.context);
|
|
});
|
|
register('table()', function (selector) {
|
|
return selectorFirst(this.tables(selector));
|
|
});
|
|
// Common methods, combined to reduce size
|
|
[
|
|
['nodes', 'node', 'table'],
|
|
['body', 'body', 'tbody'],
|
|
['header', 'header', 'thead'],
|
|
['footer', 'footer', 'tfoot']
|
|
].forEach(function (item) {
|
|
registerPlural('tables().' + item[0] + '()', 'table().' + item[1] + '()', function () {
|
|
return this.iterator('table', ctx => ctx[item[2]], true);
|
|
});
|
|
});
|
|
// Structure methods
|
|
['header', 'footer'].forEach(function (item) {
|
|
register('table().' + item + '.structure()', function (selector) {
|
|
var indexes = this.columns(selector).indexes().flatten().toArray();
|
|
var ctx = this.context[0];
|
|
var structure = headerLayout(ctx, ctx[item], indexes);
|
|
// The structure is in column index order - but from this method we
|
|
// want the return to be in the columns() selector API order. In
|
|
// order to do that we need to map from one form to the other
|
|
var orderedIndexes = indexes.slice().sort(function (a, b) {
|
|
return a - b;
|
|
});
|
|
return structure.map(function (row) {
|
|
return indexes.map(function (colIdx) {
|
|
return row[orderedIndexes.indexOf(colIdx)];
|
|
});
|
|
});
|
|
});
|
|
});
|
|
registerPlural('tables().containers()', 'table().container()', function () {
|
|
return this.iterator('table', function (ctx) {
|
|
return ctx.tableWrapper;
|
|
}, true);
|
|
});
|
|
register('tables().every()', function (fn) {
|
|
return this.iterator('table', (s, i) => {
|
|
fn.call(this.table(i), i);
|
|
});
|
|
});
|
|
register('caption()', function (value, side) {
|
|
var context = this.context;
|
|
// Getter - return existing node's content
|
|
if (value === undefined) {
|
|
var node = context[0].captionNode;
|
|
return node && context.length ? node.innerHTML : null;
|
|
}
|
|
return this.iterator('table', function (ctx) {
|
|
var table = Dom.s(ctx.table);
|
|
var caption = Dom.s(ctx.captionNode);
|
|
var container = Dom.s(ctx.tableWrapper);
|
|
// Create the node if it doesn't exist yet
|
|
if (!caption.count()) {
|
|
caption = Dom.c('caption').html(value);
|
|
ctx.captionNode = caption.get(0);
|
|
// If side isn't set, we need to insert into the document to let
|
|
// the CSS decide so we can read it back, otherwise there is no
|
|
// way to know if the CSS would put it top or bottom for
|
|
// scrolling
|
|
if (!side) {
|
|
table.prepend(caption);
|
|
side = caption.css('caption-side');
|
|
}
|
|
}
|
|
caption.html(value);
|
|
if (side) {
|
|
caption.css('caption-side', side);
|
|
caption.get(0)._captionSide = side;
|
|
}
|
|
if (container.find('div.dt-scroll').count()) {
|
|
var selector = side === 'top' ? 'head' : 'foot';
|
|
container
|
|
.find('div.dt-scroll-' + selector + ' table')
|
|
.prepend(caption);
|
|
}
|
|
else {
|
|
table.prepend(caption);
|
|
}
|
|
}, true);
|
|
});
|
|
register('caption.node()', function () {
|
|
var ctx = this.context;
|
|
return ctx.length ? ctx[0].captionNode : null;
|
|
});
|
|
|
|
/**
|
|
* What's this!? "DataTables Plus" is a commercial set of extensions for
|
|
* DataTables, such as Editor, and the functions in this file allow a license
|
|
* key to be provided (`DataTable.key(...)`) to unlock those features.
|
|
*
|
|
* This is the modal that I've selected to make DataTables sustainable, open
|
|
* source core, with some commercial extensions available.
|
|
*
|
|
* Please support DataTables and open source by purchasing a Plus license from
|
|
* https://datatables.net/plus .
|
|
*/
|
|
let _ready = false;
|
|
let _notice;
|
|
let _processingKey = false;
|
|
let _delayedReleaseDate = null;
|
|
let _delayedSoftware = null;
|
|
const _licenseInfo = {
|
|
developers: 0,
|
|
type: null,
|
|
expires: null,
|
|
valid: null
|
|
};
|
|
const _wm = Dom.c('div');
|
|
const _publicKey = 'BE1A9w9D9U/4s4/TogY+1sW/dLJ8IquzK1PmV70J93ZTIvXMZ0eV2NAb52ntpgwVFySSB2fOI7geLNO737rQAyo=';
|
|
/**
|
|
* Convert a base64 string to a binary array
|
|
*
|
|
* @param b64 Source string
|
|
* @returns Array
|
|
*/
|
|
function b64ToBuf(b64) {
|
|
return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
}
|
|
/**
|
|
* Logic to check the trial and plus license expiry and display messages if
|
|
* needed. There is particular consideration for checking a release date of
|
|
* software, as the license for DataTables Plus is perpetual for the version
|
|
* purchased, and it shouldn't show a message for the purchased version ever.
|
|
*
|
|
* @param releaseDate The date the software was released on.
|
|
* @param software The software name being validated. Can be null for a general
|
|
* "Plus" check.
|
|
* @returns true if valid, false otherwise
|
|
*/
|
|
function check(releaseDate, software) {
|
|
let expires = _licenseInfo.expires;
|
|
if (!getSubtle()) {
|
|
noticePrep('Unable to validate license key');
|
|
noticeDisplay();
|
|
}
|
|
else if (_licenseInfo.valid === false) {
|
|
noticePrep('License key invalid');
|
|
noticeDisplay();
|
|
}
|
|
else if (_licenseInfo.type === 'trial') {
|
|
// Trail is for plus, so the software type isn't taken into account
|
|
let remaining = expires
|
|
? Math.ceil((expires.getTime() - new Date().getTime()) / 86400000)
|
|
: -1;
|
|
if (remaining < 0) {
|
|
// Trial expires
|
|
consoleMsg('Your trial has now expired - https://datatables.net/plus', 'warn');
|
|
noticePrep('Trial expired');
|
|
noticeDisplay();
|
|
return false;
|
|
}
|
|
else {
|
|
// Let the user know when it is going to expire with a console
|
|
// message.
|
|
consoleMsg('Your trial expires in ' +
|
|
remaining +
|
|
' day' +
|
|
(remaining === 1 ? '' : 's'));
|
|
return true;
|
|
}
|
|
}
|
|
else if (software === null) {
|
|
// Validating the key only - there hasn't been any specific software
|
|
// calling the `plus` parameter yet. The key is good, so carry on.
|
|
return true;
|
|
}
|
|
else if (
|
|
// Checking if the build version can be used with this key
|
|
_licenseInfo.type === 'plus' ||
|
|
(_licenseInfo.type === 'editor' && software === 'editor')) {
|
|
if (!expires || new Date(releaseDate) > expires) {
|
|
noticePrep('Upgrade required for this version');
|
|
noticeDisplay();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
else if (_licenseInfo.type === 'editor' && software !== 'editor') {
|
|
// Editor specific license
|
|
noticePrep('License for Editor only. Upgrade for Plus');
|
|
noticeDisplay();
|
|
return false;
|
|
}
|
|
noticePrep();
|
|
noticeDisplay();
|
|
return false;
|
|
}
|
|
/**
|
|
* Common log message handling
|
|
*
|
|
* @param msg Message to show
|
|
* @param level Log level
|
|
*/
|
|
function consoleMsg(msg, level = 'log') {
|
|
let fn = level === 'log' ? console.log : console.warn;
|
|
fn('%cDataTables Plus%c ' + msg, 'background: #007bff; color: #fff; padding: 2px 5px;', 'color: inherit;');
|
|
}
|
|
/**
|
|
* Set the DataTables Plus key to use
|
|
*
|
|
* @param key DataTables Plus key - obtain from https://datatables.net/account .
|
|
*/
|
|
const key = function (key) {
|
|
_processingKey = true;
|
|
// Run the verification of the key
|
|
verify(key)
|
|
.then(result => {
|
|
_processingKey = false;
|
|
check(_delayedReleaseDate, _delayedSoftware);
|
|
})
|
|
.catch(() => {
|
|
_processingKey = false;
|
|
check(_delayedReleaseDate, _delayedSoftware);
|
|
});
|
|
};
|
|
/**
|
|
* Build the notice
|
|
*
|
|
* @returns
|
|
*/
|
|
function noticePrep(text) {
|
|
if (!_ready) {
|
|
let shadow = _wm[0].attachShadow({ mode: 'closed' });
|
|
let notice = Dom.c('div').css({
|
|
position: 'fixed',
|
|
bottom: '1em',
|
|
right: '1em',
|
|
border: '1px solid #ffc107',
|
|
background: '#fff3cd',
|
|
color: '#856404',
|
|
padding: '0.5em 1em',
|
|
'font-family': 'sans-serif',
|
|
'font-size': '12px',
|
|
'border-radius': '4px',
|
|
'z-index': '10000',
|
|
'box-shadow': '0 2px 5px rgba(0,0,0,0.2)'
|
|
});
|
|
Dom.c('a')
|
|
.attr('href', 'https://datatables.net/tn/25')
|
|
.attr('target', '_blank')
|
|
.css({
|
|
color: 'inherit',
|
|
'text-decoration': 'none'
|
|
})
|
|
.appendTo(notice);
|
|
if (!text) {
|
|
text = 'License key required';
|
|
}
|
|
shadow.appendChild(notice[0]);
|
|
_notice = notice;
|
|
_ready = true;
|
|
}
|
|
if (text) {
|
|
_notice
|
|
.find('a')
|
|
.html('DataTables Plus: ' + text + ' - learn more »');
|
|
}
|
|
}
|
|
/**
|
|
* Display the license notice
|
|
*/
|
|
function noticeDisplay() {
|
|
if (!_processingKey && document.body && !document.body.contains(_wm[0])) {
|
|
document.body.appendChild(_wm[0]);
|
|
}
|
|
}
|
|
/**
|
|
* Validate the license string, which is in two parts - the first is a payload
|
|
* that provides a small amount of information about the license, and the second
|
|
* which is the license key.
|
|
*
|
|
* @param licenseString Key to validate
|
|
* @returns Promise with validation information
|
|
*/
|
|
function verify(licenseString) {
|
|
return new Promise(function (resolve) {
|
|
try {
|
|
var parts = licenseString.split(':');
|
|
if (parts.length !== 2) {
|
|
_licenseInfo.valid = false;
|
|
return resolve();
|
|
}
|
|
var payload = parts[0];
|
|
var signatureB64 = parts[1];
|
|
// Extract the payload to be useful
|
|
var payloadParts = payload.match(/(plus|trial|editor)_(\d+)_(\d{4})(\d{2})(\d{2})/);
|
|
if (!payloadParts || payloadParts.length !== 6) {
|
|
_licenseInfo.valid = false;
|
|
return resolve();
|
|
}
|
|
_licenseInfo.type = payloadParts[1];
|
|
_licenseInfo.developers = parseInt(payloadParts[2]);
|
|
_licenseInfo.expires = new Date(payloadParts[3] + '-' + payloadParts[4] + '-' + payloadParts[5]);
|
|
var subtle = getSubtle();
|
|
var rawKey = b64ToBuf(_publicKey);
|
|
var rawSig = b64ToBuf(signatureB64);
|
|
var data = new TextEncoder().encode(payload);
|
|
// Non-secure environments don't have cryptographic verification
|
|
// available.
|
|
if (!subtle) {
|
|
_licenseInfo.valid = false;
|
|
resolve();
|
|
return;
|
|
}
|
|
subtle
|
|
.importKey('raw', rawKey, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])
|
|
.then(function (key) {
|
|
return subtle.verify({ name: 'ECDSA', hash: { name: 'SHA-256' } }, key, rawSig, data);
|
|
})
|
|
.then(function (isValid) {
|
|
_licenseInfo.valid = isValid;
|
|
resolve();
|
|
})
|
|
.catch(function () {
|
|
_licenseInfo.valid = false;
|
|
resolve();
|
|
});
|
|
}
|
|
catch (e) {
|
|
_licenseInfo.valid = false;
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Create the `plus` function on `DataTable` which Plus extensions can call to
|
|
* determine if the license key is valid and in date for the release. The
|
|
* resulting function is called like this: `DataTable.plus('2026-12-25')` and
|
|
* will return `true` or `false` depending on the key that was given (or not).
|
|
*
|
|
* @param DataTable The DataTable host object
|
|
*/
|
|
function plus (DataTable) {
|
|
Object.defineProperty(DataTable, 'plus', {
|
|
value: function (releaseDate, software = '') {
|
|
// Unsecure sites are only useful for development, so allow there
|
|
// and on the site.
|
|
let host = window.location.hostname;
|
|
let isDev = host === '192.168.234.234' ||
|
|
host.endsWith('.datatables.net') ||
|
|
host === 'datatables.net';
|
|
if (isDev) {
|
|
return true;
|
|
}
|
|
if (_processingKey) {
|
|
// The validation of the key is async, so there is a chance that
|
|
// it could still be happening when this runs. We just queue the
|
|
// last one if that is the case.
|
|
_delayedReleaseDate = releaseDate;
|
|
_delayedSoftware = software;
|
|
return true;
|
|
}
|
|
return check(releaseDate, software);
|
|
},
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false
|
|
});
|
|
}
|
|
function getSubtle() {
|
|
// Backwards compat for old browsers
|
|
let cryptoObj = window.crypto || window.msCrypto;
|
|
let subtle = cryptoObj.subtle || cryptoObj.webkitSubtle;
|
|
return subtle;
|
|
}
|
|
|
|
/**
|
|
* CommonJS factory function pass through. This will check if the arguments
|
|
* given are a window object or a jQuery object. If so they are set accordingly.
|
|
*
|
|
* @param root Window
|
|
* @param jq jQuery
|
|
* @returns Indicator
|
|
*/
|
|
function factory(root, jq) {
|
|
var is = false;
|
|
// Test if the first parameter is a window object
|
|
if (root && root.document) {
|
|
window = root;
|
|
document = root.document;
|
|
}
|
|
// Test if the second parameter is a jQuery object
|
|
if (jq && jq.fn && jq.fn.jquery) {
|
|
is = true;
|
|
}
|
|
return is;
|
|
}
|
|
/**
|
|
* Check if a `<table>` node is a DataTable table already or not.
|
|
*
|
|
* @param table Table node or selector for the table to test. Note that if more
|
|
* than more than one table is passed on, only the first will be checked
|
|
* @returns true the table given is a DataTable, or false otherwise
|
|
*/
|
|
const isDataTable = function (table) {
|
|
if (table instanceof Api) {
|
|
return true;
|
|
}
|
|
else if (arrayLike(table)) {
|
|
// jQuery compatibility
|
|
table = Array.from(table);
|
|
}
|
|
var t = Dom.s(table).get(0);
|
|
var is = false;
|
|
for (let i = 0; i < ext.settings.length; i++) {
|
|
let ctx = ext.settings[i];
|
|
var head = ctx.scrollHead ? ctx.scrollHead.find('table').get(0) : null;
|
|
var foot = ctx.scrollFoot ? ctx.scrollFoot.find('table').get(0) : null;
|
|
if (ctx.table === t || head === t || foot === t) {
|
|
is = true;
|
|
}
|
|
}
|
|
return is;
|
|
};
|
|
/**
|
|
* Get all DataTable tables that have been initialised - optionally you can
|
|
* select to get only currently visible tables.
|
|
*
|
|
* @param visible Flag to indicate if you want all (default) or visible tables
|
|
* only.
|
|
* @returns Array of `table` nodes (not DataTable instances) which are
|
|
* DataTables
|
|
*/
|
|
const tables = function (visible) {
|
|
var api = false;
|
|
if (visible && typeof visible !== 'boolean') {
|
|
api = visible.api || false;
|
|
visible = visible.visible || false;
|
|
}
|
|
var a = ext.settings
|
|
.filter(function (o) {
|
|
return !visible || (visible && Dom.s(o.table).isVisible())
|
|
? true
|
|
: false;
|
|
})
|
|
.map(function (o) {
|
|
return o.table;
|
|
});
|
|
return api ? new Api(a) : a;
|
|
};
|
|
|
|
function _divProp(el, prop, val) {
|
|
if (val) {
|
|
el[prop] = val;
|
|
}
|
|
}
|
|
register$2('div', function (settings, opts) {
|
|
var n = document.createElement('div');
|
|
if (opts) {
|
|
_divProp(n, 'className', opts.className);
|
|
_divProp(n, 'id', opts.id);
|
|
_divProp(n, 'innerHTML', opts.html);
|
|
_divProp(n, 'textContent', opts.text);
|
|
}
|
|
return n;
|
|
});
|
|
|
|
register$2('info', function (settings, optsIn) {
|
|
// For compatibility with the legacy `info` top level option
|
|
if (!settings.features.info) {
|
|
return null;
|
|
}
|
|
let lang = settings.language, tid = settings.tableId, n = Dom.c('div').classAdd(settings.classes.info.container);
|
|
let opts = Object.assign({
|
|
callback: lang.infoCallback,
|
|
empty: lang.infoEmpty,
|
|
postfix: lang.infoPostFix,
|
|
search: lang.infoFiltered,
|
|
text: lang.info
|
|
}, optsIn);
|
|
// Update display on each draw
|
|
settings.callbacks.draw.push(function (s) {
|
|
updateInfo(s, opts, n);
|
|
});
|
|
// For the first info display in the table, we add a callback and aria
|
|
// information.
|
|
if (!settings.infoEl) {
|
|
n.attr({
|
|
'aria-live': 'polite',
|
|
id: tid + '_info',
|
|
role: 'status'
|
|
});
|
|
// Table is described by our info div
|
|
Dom.s(settings.table).attr('aria-describedby', tid + '_info');
|
|
settings.infoEl = n;
|
|
}
|
|
return n;
|
|
}, 'i');
|
|
/**
|
|
* Update the information elements in the display
|
|
* @param settings DataTables settings object
|
|
* @param opts
|
|
* @param node
|
|
*/
|
|
function updateInfo(settings, opts, node) {
|
|
var start = settings.displayStart + 1, end = displayEnd(settings), max = recordsTotal(settings), total = recordsDisplay(settings), out = total ? opts.text : opts.empty;
|
|
if (total !== max) {
|
|
// Record set after filtering
|
|
out += ' ' + opts.search;
|
|
}
|
|
// Convert the macros
|
|
out += opts.postfix;
|
|
out = macros(settings, out);
|
|
if (opts.callback) {
|
|
out = opts.callback.call(settings.instance, settings, start, end, max, total, out);
|
|
}
|
|
node.html(out);
|
|
callbackFire(settings, null, 'info', [settings, node.get(0), out]);
|
|
}
|
|
|
|
// opts
|
|
// - type - button configuration
|
|
// - buttons - number of buttons to show - must be odd
|
|
register$2('paging', function (settings, optsIn) {
|
|
// Don't show the paging input if the table doesn't have paging enabled
|
|
if (!settings.features.paging) {
|
|
return null;
|
|
}
|
|
let opts = Object.assign({
|
|
buttons: ext.pager.numbers_length,
|
|
type: settings.pagingType,
|
|
boundaryNumbers: true,
|
|
firstLast: true,
|
|
previousNext: true,
|
|
numbers: true
|
|
}, optsIn);
|
|
let host = Dom
|
|
.c('div')
|
|
.classAdd(settings.classes.paging.container +
|
|
(opts.type ? ' paging_' + opts.type : ''))
|
|
.append(Dom
|
|
.c('nav')
|
|
.attr('aria-label', 'pagination')
|
|
.classAdd(settings.classes.paging.nav));
|
|
let draw = function () {
|
|
_pagingDraw(settings, host.children(), opts);
|
|
};
|
|
settings.callbacks.draw.push(draw);
|
|
// Responsive redraw of paging control
|
|
Dom.s(settings.table).on('column-sizing.dt.DT', draw);
|
|
return host;
|
|
}, 'p');
|
|
/**
|
|
* Dynamically create the button type array based on the configuration options.
|
|
* This will only happen if the paging type is not defined.
|
|
*/
|
|
function _pagingDynamic(opts) {
|
|
let out = [];
|
|
if (opts.numbers) {
|
|
out.push('numbers');
|
|
}
|
|
if (opts.previousNext) {
|
|
out.unshift('previous');
|
|
out.push('next');
|
|
}
|
|
if (opts.firstLast) {
|
|
out.unshift('first');
|
|
out.push('last');
|
|
}
|
|
return out;
|
|
}
|
|
function _pagingDraw(settings, host, opts) {
|
|
if (!settings.initDone) {
|
|
return;
|
|
}
|
|
let plugin = opts.type ? ext.pager[opts.type] : _pagingDynamic, aria = settings.language.aria.paginate || {}, start = settings.displayStart, len = settings.pageLength, visRecords = recordsDisplay(settings), all = len === -1, page = all ? 0 : Math.ceil(start / len), pages = all ? (visRecords ? 1 : 0) : Math.ceil(visRecords / len), buttons = [], buttonEls = [], buttonsNested = plugin(opts).map(function (val) {
|
|
return val === 'numbers'
|
|
? pagingNumbers(page, pages, opts.buttons, opts.boundaryNumbers)
|
|
: val;
|
|
});
|
|
// .flat() would be better, but not supported in old Safari
|
|
buttons = buttons.concat.apply(buttons, buttonsNested);
|
|
for (let i = 0; i < buttons.length; i++) {
|
|
let button = buttons[i];
|
|
let btnInfo = _pagingButtonInfo(settings, button, page, pages);
|
|
let btn = renderer(settings, 'pagingButton')(settings, button, btnInfo.display, btnInfo.active, btnInfo.disabled);
|
|
let ariaLabel = typeof button === 'string'
|
|
? aria[button]
|
|
: aria.number
|
|
? aria.number + (button + 1)
|
|
: null;
|
|
// Common attributes
|
|
Dom.s(btn.clicker).attr({
|
|
'aria-controls': settings.tableId,
|
|
'aria-disabled': btnInfo.disabled ? 'true' : null,
|
|
'aria-current': btnInfo.active ? 'page' : null,
|
|
'aria-label': ariaLabel,
|
|
'data-dt-idx': button,
|
|
tabIndex: btnInfo.disabled
|
|
? -1
|
|
: settings.tabIndex &&
|
|
btn.clicker.nodeName.toLowerCase() !== 'span'
|
|
? settings.tabIndex
|
|
: null // `0` doesn't need a tabIndex since it is the default
|
|
});
|
|
if (typeof button !== 'number') {
|
|
Dom.s(btn.clicker).classAdd(button);
|
|
}
|
|
bindAction(btn.clicker, '', function (e) {
|
|
e.preventDefault();
|
|
pageChange(settings, button, true);
|
|
});
|
|
buttonEls.push(btn.display);
|
|
}
|
|
let wrapped = renderer(settings, 'pagingContainer')(settings, buttonEls);
|
|
let activeEl = host.find(document.activeElement).attr('data-dt-idx');
|
|
host.empty().append(wrapped);
|
|
if (activeEl) {
|
|
host.find('[data-dt-idx="' + activeEl + '"]').trigger('focus');
|
|
}
|
|
// Responsive - check if the buttons are over two lines based on the
|
|
// height of the buttons and the container.
|
|
if (buttonEls.length) {
|
|
let outerHeight = Dom.s(buttonEls[0]).height('withBorder');
|
|
if (opts.buttons > 1 && // prevent infinite
|
|
outerHeight > 0 && // will be 0 if hidden
|
|
host.height() >= outerHeight * 2 - 10) {
|
|
_pagingDraw(settings, host, Object.assign({}, opts, { buttons: opts.buttons - 2 }));
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get properties for a button based on the current paging state of the table
|
|
*
|
|
* @param settings DT settings object
|
|
* @param button The button type in question
|
|
* @param page Table's current page
|
|
* @param pages Number of pages
|
|
* @returns Info object
|
|
*/
|
|
function _pagingButtonInfo(settings, button, page, pages) {
|
|
let lang = settings.language.paginate;
|
|
let o = {
|
|
display: '',
|
|
active: false,
|
|
disabled: false
|
|
};
|
|
switch (button) {
|
|
case 'ellipsis':
|
|
o.display = '…';
|
|
break;
|
|
case 'first':
|
|
o.display = lang.first;
|
|
if (page === 0) {
|
|
o.disabled = true;
|
|
}
|
|
break;
|
|
case 'previous':
|
|
o.display = lang.previous;
|
|
if (page === 0) {
|
|
o.disabled = true;
|
|
}
|
|
break;
|
|
case 'next':
|
|
o.display = lang.next;
|
|
if (pages === 0 || page === pages - 1) {
|
|
o.disabled = true;
|
|
}
|
|
break;
|
|
case 'last':
|
|
o.display = lang.last;
|
|
if (pages === 0 || page === pages - 1) {
|
|
o.disabled = true;
|
|
}
|
|
break;
|
|
default:
|
|
if (typeof button === 'number') {
|
|
o.display = settings.formatNumber(button + 1, settings);
|
|
if (page === button) {
|
|
o.active = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return o;
|
|
}
|
|
|
|
var __lengthCounter = 0;
|
|
// opts
|
|
// - menu
|
|
// - text
|
|
register$2('pageLength', function (settings, optsIn) {
|
|
var features = settings.features;
|
|
// For compatibility with the legacy `pageLength` top level option
|
|
if (!features.paging || !features.lengthChange) {
|
|
return null;
|
|
}
|
|
let opts = Object.assign({
|
|
menu: settings.lengthMenu,
|
|
text: settings.language.lengthMenu
|
|
}, optsIn);
|
|
let classes = settings.classes.length, tableId = settings.tableId, menu = opts.menu, lengths = [], language = [], i;
|
|
// Options can be given in a number of ways
|
|
if (Array.isArray(menu[0])) {
|
|
// Old 1.x style - 2D array
|
|
lengths = menu[0];
|
|
language = menu[1];
|
|
}
|
|
else {
|
|
for (i = 0; i < menu.length; i++) {
|
|
// An object with different label and value
|
|
if (plainObject(menu[i])) {
|
|
lengths.push(menu[i].value);
|
|
language.push(menu[i].label);
|
|
}
|
|
else {
|
|
// Or just a number to display and use
|
|
lengths.push(menu[i]);
|
|
language.push(menu[i]);
|
|
}
|
|
}
|
|
}
|
|
// We can put the <select> outside of the label if it is at the start or
|
|
// end which helps improve accessability (not all screen readers like
|
|
// implicit for elements).
|
|
var end = opts.text.match(/_MENU_$/);
|
|
var start = opts.text.match(/^_MENU_/);
|
|
var removed = opts.text.replace(/_MENU_/, '');
|
|
var str = '<label>' + opts.text + '</label>';
|
|
if (start) {
|
|
str = '_MENU_<label>' + removed + '</label>';
|
|
}
|
|
else if (end) {
|
|
str = '<label>' + removed + '</label>_MENU_';
|
|
}
|
|
// Wrapper element - use a span as a holder for where the select will go
|
|
var tmpId = 'tmp-' + +new Date();
|
|
var div = Dom.c('div')
|
|
.classAdd(classes.container)
|
|
.html(str.replace('_MENU_', '<span id="' + tmpId + '"></span>'));
|
|
// Save text node content for macro updating
|
|
var textNodes = [];
|
|
Array.prototype.slice
|
|
.call(div.find('label').get(0).childNodes)
|
|
.forEach(function (el) {
|
|
if (el.nodeType === Node.TEXT_NODE) {
|
|
textNodes.push({
|
|
el: el,
|
|
text: el.textContent
|
|
});
|
|
}
|
|
});
|
|
// Update the label text in case it has an entries value
|
|
var updateEntries = function (len) {
|
|
textNodes.forEach(function (node) {
|
|
node.el.textContent = macros(settings, node.text, len);
|
|
});
|
|
};
|
|
// Next, the select itself, along with the options
|
|
var select = Dom.c('select')
|
|
.attr('aria-controls', tableId)
|
|
.attr('autocomplete', 'off')
|
|
.classAdd(classes.select);
|
|
for (i = 0; i < lengths.length; i++) {
|
|
// Attempt to look up the length from the i18n options
|
|
var label = settings.api.i18n('lengthLabels.' + lengths[i], null);
|
|
if (label === null) {
|
|
// If not present, fallback to old style
|
|
label =
|
|
typeof language[i] === 'number'
|
|
? settings.formatNumber(language[i], settings)
|
|
: language[i];
|
|
}
|
|
select.get(0)[i] = new Option(label, lengths[i]);
|
|
}
|
|
// Swap in the select list
|
|
div.find('#' + tmpId).replaceWith(select);
|
|
// Can't use `select` variable as user might provide their own and the
|
|
// reference is broken by the use of outerHTML
|
|
div.find('select')
|
|
.attr('id', 'dt-length-' + __lengthCounter)
|
|
.val(settings.pageLength)
|
|
.on('change.DT', function () {
|
|
lengthChange(settings, select.val());
|
|
draw(settings);
|
|
});
|
|
// add for and id to label and input
|
|
div.find('label').attr('for', 'dt-length-' + __lengthCounter);
|
|
__lengthCounter++;
|
|
// Update node value whenever anything changes the table's length
|
|
Dom.s(settings.table).on('length.dt.DT', function (e, s, len) {
|
|
if (settings === s) {
|
|
let localSelect = div.find('select');
|
|
// Remove any temporary values
|
|
localSelect.find('option[data-dt-len-tmp]').remove();
|
|
let option = localSelect.find('option[value="' + len + '"]');
|
|
// If the select list doesn't have the target value, then we
|
|
// need to add it for display.
|
|
if (!option.length) {
|
|
let after = findInsertBeforePoint(select, len);
|
|
let tempOption = Dom.c('option')
|
|
.val(len)
|
|
.text(len)
|
|
.attr('data-dt-len-tmp', true);
|
|
if (after && after.length) {
|
|
tempOption.insertBefore(after);
|
|
}
|
|
else {
|
|
localSelect.append(tempOption);
|
|
}
|
|
}
|
|
localSelect.val(len);
|
|
// Resolve plurals in the text for the new length
|
|
updateEntries(len);
|
|
}
|
|
});
|
|
updateEntries(settings.pageLength);
|
|
return div;
|
|
}, 'l');
|
|
/**
|
|
* Find the element to insert the temporary option before to keep the sequence.
|
|
*
|
|
* @param select Select element
|
|
* @param insertValue Page length value
|
|
* @returns Target option or null if not found
|
|
*/
|
|
function findInsertBeforePoint(select, insertValue) {
|
|
let options = select.find('option');
|
|
let values = options.mapTo(el => parseInt(el.value));
|
|
let idx = values.findIndex(val => val > insertValue);
|
|
return idx < -1 ? null : options.eq(idx);
|
|
}
|
|
|
|
let __searchCounter = 0;
|
|
register$2('search', function (settings, optsIn) {
|
|
// Don't show the input if filtering isn't available on the table
|
|
if (!settings.features.searching) {
|
|
return null;
|
|
}
|
|
let classes = settings.classes.search;
|
|
let tableId = settings.tableId;
|
|
let language = settings.language;
|
|
let input = '<input type="search" class="' +
|
|
classes.input +
|
|
'" autocomplete="off"/>';
|
|
let opts = util.object.assignDeep({
|
|
columns: '*',
|
|
placeholder: language.searchPlaceholder,
|
|
processing: false,
|
|
text: language.search
|
|
}, optsIn);
|
|
// The _INPUT_ is optional - is appended if not present
|
|
if (opts.text.indexOf('_INPUT_') === -1) {
|
|
opts.text += '_INPUT_';
|
|
}
|
|
opts.text = macros(settings, opts.text);
|
|
let indexes = settings.api.columns(opts.columns).indexes().toArray();
|
|
let searchName = opts.columns === '*' ? '*' : indexes.join(',');
|
|
let appliedSearch = settings.searches[searchName];
|
|
if (!appliedSearch) {
|
|
appliedSearch = create$1();
|
|
settings.searches[searchName] = appliedSearch;
|
|
}
|
|
appliedSearch.columns = indexes;
|
|
// We can put the <input> outside of the label if it is at the start or
|
|
// end which helps improve accessability (not all screen readers like
|
|
// implicit for elements).
|
|
let end = opts.text.match(/_INPUT_$/);
|
|
let start = opts.text.match(/^_INPUT_/);
|
|
let removed = opts.text.replace(/_INPUT_/, '');
|
|
let str = '<label>' + opts.text + '</label>';
|
|
if (start) {
|
|
str = '_INPUT_<label>' + removed + '</label>';
|
|
}
|
|
else if (end) {
|
|
str = '<label>' + removed + '</label>_INPUT_';
|
|
}
|
|
let filter = Dom.c('div')
|
|
.classAdd(classes.container)
|
|
.html(str.replace(/_INPUT_/, input));
|
|
// add for and id to label and input
|
|
filter.find('label').attr('for', 'dt-search-' + __searchCounter);
|
|
filter.find('input').attr('id', 'dt-search-' + __searchCounter);
|
|
__searchCounter++;
|
|
let searchFn = function (event) {
|
|
let val = this.value;
|
|
if (appliedSearch.return && event.key !== 'Enter') {
|
|
return;
|
|
}
|
|
/* Now do the filter */
|
|
if (val != appliedSearch.search) {
|
|
processingRun(settings, opts.processing, function () {
|
|
appliedSearch.search = val;
|
|
filterComplete(settings);
|
|
// Need to redraw, without resorting
|
|
settings.displayStart = 0;
|
|
draw(settings);
|
|
});
|
|
}
|
|
};
|
|
let searchDelay = settings.searchDelay;
|
|
let filterEl = filter
|
|
.find('input')
|
|
.val(textValue(appliedSearch.search))
|
|
.attr('placeholder', opts.placeholder)
|
|
.on('keyup.DT search.DT input.DT paste.DT cut.DT', searchDelay ? util.debounce(searchFn, searchDelay) : searchFn)
|
|
.on('mouseup.DT', function (e) {
|
|
// Edge fix! Edge 17 does not trigger anything other than mouse
|
|
// events when clicking on the clear icon (Edge bug 17584515).
|
|
// This is safe in other browsers as `searchFn` checks the value
|
|
// to see if it has changed. In other browsers it won't have.
|
|
setTimeout(function () {
|
|
searchFn.call(filterEl.get(0), e);
|
|
}, 10);
|
|
})
|
|
.on('keypress.DT', function (e) {
|
|
/* Prevent form submission */
|
|
if (e.keyCode == 13) {
|
|
return false;
|
|
}
|
|
})
|
|
.attr('aria-controls', tableId);
|
|
// Update the input elements whenever the table is filtered
|
|
Dom.s(settings.table).on('search.dt.DT', function (ev, s) {
|
|
if (settings === s && filterEl.get(0) !== document.activeElement) {
|
|
let host = settings.searches[searchName];
|
|
filterEl.val(textValue(host.search));
|
|
}
|
|
});
|
|
return filter;
|
|
}, 'f');
|
|
/**
|
|
* Convert a search input into a plain string value for display. This is needed
|
|
* as the value could be a function or regex, which can't be displayed in the
|
|
* input element.
|
|
*
|
|
* @param val Search term
|
|
* @returns String version
|
|
*/
|
|
function textValue(val) {
|
|
if (val instanceof RegExp) {
|
|
return val.toString();
|
|
}
|
|
else if (typeof val !== 'function') {
|
|
return val;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
const defaults$1 = {
|
|
ajax: null,
|
|
ajaxDataGet: false,
|
|
api: null,
|
|
browser: {
|
|
barWidth: 0,
|
|
scrollbarLeft: false
|
|
},
|
|
callbacks: {
|
|
destroy: [],
|
|
draw: [],
|
|
footer: [],
|
|
header: [],
|
|
init: [],
|
|
preDraw: [],
|
|
row: [],
|
|
rowCreated: [],
|
|
stateLoadParams: [],
|
|
stateLoaded: [],
|
|
stateSaveParams: []
|
|
},
|
|
caption: '',
|
|
captionNode: null,
|
|
classes: {},
|
|
columns: [],
|
|
containerWidth: -1,
|
|
data: [],
|
|
deferLoading: false,
|
|
destroyWidth: 0,
|
|
destroying: false,
|
|
display: [],
|
|
displayMaster: [],
|
|
displayStart: 0,
|
|
displayStartInit: -1,
|
|
doingDraw: false,
|
|
dom: null,
|
|
drawCount: 0,
|
|
drawError: -1,
|
|
drawHold: false,
|
|
features: {
|
|
autoWidth: false,
|
|
deferRender: false,
|
|
info: false,
|
|
lengthChange: false,
|
|
orderClasses: false,
|
|
orderMulti: false,
|
|
ordering: false,
|
|
paging: false,
|
|
processing: false,
|
|
searching: false,
|
|
serverSide: false,
|
|
stateSave: false
|
|
},
|
|
footer: [],
|
|
header: [],
|
|
ids: {},
|
|
init: {},
|
|
initDone: false,
|
|
initialised: false,
|
|
language: {
|
|
ajax: '',
|
|
aria: {
|
|
orderable: '',
|
|
orderableRemove: '',
|
|
orderableReverse: '',
|
|
paginate: {
|
|
first: '',
|
|
last: '',
|
|
next: '',
|
|
number: '',
|
|
previous: ''
|
|
}
|
|
},
|
|
decimal: '',
|
|
emptyTable: '',
|
|
entries: { _: '' },
|
|
info: '',
|
|
infoEmpty: '',
|
|
infoFiltered: '',
|
|
infoPostFix: '',
|
|
lengthMenu: '',
|
|
lengthLabels: {},
|
|
loadingRecords: '',
|
|
paginate: {
|
|
first: '',
|
|
last: '',
|
|
next: '',
|
|
previous: ''
|
|
},
|
|
processing: '',
|
|
search: '',
|
|
searchPlaceholder: '',
|
|
thousands: '',
|
|
url: '',
|
|
zeroRecords: ''
|
|
},
|
|
lastOrder: [],
|
|
layout: {},
|
|
loadingState: false,
|
|
order: [],
|
|
orderCellsTop: null,
|
|
orderDescReverse: false,
|
|
orderFixed: [],
|
|
orderHandler: true,
|
|
orderIndicators: true,
|
|
pageLength: 10,
|
|
pagingControls: 0,
|
|
pagingType: 'two_button',
|
|
searchCols: [],
|
|
recordsDisplay: 0,
|
|
recordsTotal: 0,
|
|
renderer: null,
|
|
resizeObserver: null,
|
|
reszEvt: false,
|
|
rowId: '',
|
|
rowReadObject: false,
|
|
scroll: {
|
|
barWidth: 0,
|
|
collapse: null,
|
|
x: '',
|
|
xInner: '',
|
|
y: ''
|
|
},
|
|
scrollBarVis: false,
|
|
searchDelay: 0,
|
|
searches: {},
|
|
searchesFixed: {
|
|
'*': {}
|
|
},
|
|
serverMethod: null,
|
|
sortDetails: [],
|
|
stateDuration: 0,
|
|
stateLoadCallback: () => {
|
|
return {};
|
|
},
|
|
stateLoaded: null,
|
|
stateSaveCallback: () => { },
|
|
stateSaved: null,
|
|
tabIndex: 0,
|
|
tableId: '',
|
|
titleRow: null,
|
|
typeDetect: true,
|
|
unique: '',
|
|
wasFiltered: false,
|
|
wasOrdered: false,
|
|
windowResizeCb: () => { }
|
|
};
|
|
/**
|
|
* Create a new context object
|
|
*
|
|
* @param parts Values to assign, otherwise the defaults will be used
|
|
* @returns New object
|
|
*/
|
|
function create(parts = {}) {
|
|
return util.object.assignDeep({}, defaults$1, parts);
|
|
}
|
|
|
|
var models = {
|
|
Column: Settings,
|
|
Row: create$2,
|
|
Search: create$1,
|
|
Settings: create
|
|
};
|
|
|
|
/**
|
|
* Initialisation options that can be given to DataTables at initialisation
|
|
* time.
|
|
*/
|
|
const defaults = {
|
|
ajax: null,
|
|
autoWidth: true,
|
|
caption: '',
|
|
classes: {},
|
|
column: defaults$2,
|
|
columnDefs: null,
|
|
columns: null,
|
|
createdRow: null,
|
|
data: null,
|
|
deferLoading: null,
|
|
deferRender: true,
|
|
destroy: false,
|
|
displayStart: 0,
|
|
dom: null,
|
|
drawCallback: null,
|
|
footerCallback: null,
|
|
formatNumber: function (toFormat, ctx) {
|
|
return toFormat
|
|
.toString()
|
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ctx.language.thousands);
|
|
},
|
|
headerCallback: null,
|
|
info: true,
|
|
infoCallback: null,
|
|
initComplete: null,
|
|
language: {
|
|
ajax: '',
|
|
aria: {
|
|
orderable: ': Activate to sort',
|
|
orderableRemove: ': Activate to remove sorting',
|
|
orderableReverse: ': Activate to invert sorting',
|
|
paginate: {
|
|
first: 'First',
|
|
last: 'Last',
|
|
next: 'Next',
|
|
number: '',
|
|
previous: 'Previous'
|
|
}
|
|
},
|
|
decimal: '',
|
|
emptyTable: 'No data available in table',
|
|
entries: {
|
|
_: 'entries',
|
|
1: 'entry'
|
|
},
|
|
info: 'Showing _START_ to _END_ of _TOTAL_ _ENTRIES-TOTAL_',
|
|
infoEmpty: 'Showing 0 to 0 of 0 _ENTRIES-TOTAL_',
|
|
infoFiltered: '(filtered from _MAX_ total _ENTRIES-MAX_)',
|
|
infoPostFix: '',
|
|
lengthLabels: {
|
|
'-1': 'All'
|
|
},
|
|
lengthMenu: '_MENU_ _ENTRIES_ per page',
|
|
loadingRecords: 'Loading...',
|
|
paginate: {
|
|
first: '\u00AB',
|
|
last: '\u00BB',
|
|
next: '\u203A',
|
|
previous: '\u2039'
|
|
},
|
|
processing: '',
|
|
search: 'Search:',
|
|
searchPlaceholder: '',
|
|
thousands: ',',
|
|
url: '',
|
|
zeroRecords: 'No matching records found'
|
|
},
|
|
layout: {
|
|
bottomEnd: 'paging',
|
|
bottomStart: 'info',
|
|
topEnd: 'search',
|
|
topStart: 'pageLength'
|
|
},
|
|
lengthChange: true,
|
|
lengthMenu: [10, 25, 50, 100],
|
|
on: {},
|
|
order: [[0, 'asc']],
|
|
orderCellsTop: null,
|
|
orderClasses: true,
|
|
orderDescReverse: true,
|
|
orderFixed: [],
|
|
orderMulti: true,
|
|
ordering: true,
|
|
pageLength: 10,
|
|
paging: true,
|
|
pagingType: '',
|
|
preDrawCallback: null,
|
|
processing: false,
|
|
renderer: null,
|
|
retrieve: false,
|
|
rowCallback: null,
|
|
rowId: 'DT_RowId',
|
|
scrollCollapse: false,
|
|
scrollX: '',
|
|
scrollY: '',
|
|
search: defaults$3,
|
|
searchCols: [],
|
|
searchDelay: 0,
|
|
searching: true,
|
|
serverMethod: 'GET',
|
|
serverSide: false,
|
|
stateDuration: 7200,
|
|
stateLoadCallback: function (settings) {
|
|
try {
|
|
const state = (settings.stateDuration === -1 ? sessionStorage : localStorage).getItem('DataTables_' + settings.unique + '_' + location.pathname);
|
|
return state ? JSON.parse(state) : {};
|
|
}
|
|
catch (e) {
|
|
return {};
|
|
}
|
|
},
|
|
stateLoadParams: null,
|
|
stateLoaded: null,
|
|
stateSave: false,
|
|
stateSaveCallback: function (settings, data) {
|
|
try {
|
|
(settings.stateDuration === -1
|
|
? sessionStorage
|
|
: localStorage).setItem('DataTables_' + settings.unique + '_' + location.pathname, JSON.stringify(data));
|
|
}
|
|
catch (e) {
|
|
// noop
|
|
}
|
|
},
|
|
stateSaveParams: null,
|
|
tabIndex: 0,
|
|
titleRow: null,
|
|
typeDetect: true
|
|
};
|
|
|
|
const DataTable = function (selector, options) {
|
|
// Check if called with a window or jQuery object for DOM less applications
|
|
// This is for backwards compatibility
|
|
if (factory(selector, options)) {
|
|
return DataTable;
|
|
}
|
|
// Allow access to the API from the core class
|
|
this.api = () => {
|
|
return new Api(selector);
|
|
};
|
|
// Backwards compatibility with this "class" being exposed as
|
|
// `$().dataTable()`. We can't simply provide that as a wrapper, as there
|
|
// are properties on this class which are expected to be exposed.
|
|
if (typeof this.jquery === 'string') {
|
|
// Typescript doesn't like the `return api` from the constructor, but is
|
|
// it valid Javascript, and allows backwards compatibility, hence the any
|
|
new DataTable(this.toArray(), selector); // note argument shift
|
|
return this;
|
|
}
|
|
var emptyInit = options === undefined;
|
|
let tableEls = Dom.s(selector);
|
|
let len = tableEls.count();
|
|
if (emptyInit) {
|
|
options = {};
|
|
}
|
|
tableEls.each(tableEl => {
|
|
// For each initialisation we want to give it a clean initialisation
|
|
// object that can be bashed around
|
|
var o = {};
|
|
var init = len > 1 // optimisation for single table case
|
|
? util.object.assignDeepObjects(o, options, true)
|
|
: options;
|
|
var i = 0, iLen;
|
|
var id = tableEl.getAttribute('id');
|
|
var table = Dom.s(tableEl);
|
|
// Sanity check
|
|
if (tableEl.nodeName.toLowerCase() != 'table') {
|
|
log(null, 0, 'Non-table node initialisation (' + tableEl.nodeName + ')', 2);
|
|
return;
|
|
}
|
|
// Special case for options
|
|
if (init.on && init.on.options) {
|
|
listener(table, 'options', init.on.options);
|
|
}
|
|
table.trigger('options.dt', true, [init]);
|
|
// Backwards compatibility parameter mapping
|
|
compatOpts(defaults, true);
|
|
compatCols(defaults$2);
|
|
// Allow data properties on the table element to be used as
|
|
// initialisation options
|
|
util.object.assign(init, escapeObject(table.data()));
|
|
compatOpts(init);
|
|
/* Check to see if we are re-initialising a table */
|
|
var allSettings = ext.settings;
|
|
for (i = 0, iLen = allSettings.length; i < iLen; i++) {
|
|
var s = allSettings[i];
|
|
/* Base check on table node */
|
|
if (s.table == tableEl ||
|
|
(s.thead && s.thead.parentNode == tableEl) ||
|
|
(s.tfoot && s.tfoot.parentNode == tableEl)) {
|
|
var retrieve = init.retrieve || false;
|
|
var destroy = init.destroy || false;
|
|
if (emptyInit || retrieve) {
|
|
return s.instance;
|
|
}
|
|
else if (destroy) {
|
|
new Api(s).destroy();
|
|
break;
|
|
}
|
|
else {
|
|
log(s, 0, 'Cannot reinitialise DataTable', 3);
|
|
return;
|
|
}
|
|
}
|
|
/* If the element we are initialising has the same ID as a table
|
|
* which was previously initialised, but the table nodes don't match
|
|
* (from before) then we destroy the old instance by simply deleting
|
|
* it. This is under the assumption that the table has been
|
|
* destroyed by other methods. Anyone using non-id selectors will
|
|
* need to do this manually
|
|
*/
|
|
if (s.tableId == tableEl.id) {
|
|
allSettings.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
/* Ensure the table has an ID - required for accessibility */
|
|
if (id === null || id === '') {
|
|
id = 'DataTables_Table_' + ext._unique++;
|
|
tableEl.id = id;
|
|
}
|
|
// Replacing an existing colgroup with our own. Not ideal, but a merge
|
|
// could take a lot of code
|
|
table.children('colgroup').remove();
|
|
// Create the settings object for this table and set some of the default
|
|
// parameters
|
|
var settings = create({
|
|
destroyWidth: table.width(),
|
|
unique: id,
|
|
tableId: id,
|
|
colgroup: Dom.c('colgroup'),
|
|
fastData: function (row, column, type) {
|
|
return getCellData(settings, row, column, type);
|
|
}
|
|
});
|
|
settings.table = tableEl;
|
|
settings.init = init;
|
|
allSettings.push(settings);
|
|
// Make a single API instance available for internal handling
|
|
settings.api = new Api(settings);
|
|
// Need to add the instance after the instance after the settings object
|
|
// has been added to the settings array, so we can self reference the
|
|
// table instance if more than one
|
|
settings.instance = Dom.s(tableEl); // any until we add the api
|
|
settings.instance.api = () => settings.api;
|
|
// If the length menu is given, but the init display length is not, use
|
|
// the length menu
|
|
if (init.lengthMenu && !init.pageLength) {
|
|
init.pageLength =
|
|
typeof init.lengthMenu[0] === 'number'
|
|
? init.lengthMenu[0]
|
|
: Array.isArray(init.lengthMenu[0])
|
|
? init.lengthMenu[0][0]
|
|
: init.lengthMenu[0].value;
|
|
}
|
|
// Apply the defaults and init options to make a single init object will
|
|
// all options defined from defaults and instance options.
|
|
let config = util.object.assignDeepObjects(util.object.assignDeep({}, defaults), init);
|
|
// Map the initialisation options onto the context object
|
|
map(settings.features, config, [
|
|
'autoWidth',
|
|
'deferRender',
|
|
'info',
|
|
'lengthChange',
|
|
'orderClasses',
|
|
'ordering',
|
|
'orderMulti',
|
|
'paging',
|
|
'processing',
|
|
'searching',
|
|
'serverSide'
|
|
]);
|
|
map(settings, config, [
|
|
'ajax',
|
|
'formatNumber',
|
|
'serverMethod',
|
|
'order',
|
|
'orderFixed',
|
|
'lengthMenu',
|
|
'pagingType',
|
|
'stateDuration',
|
|
'orderCellsTop',
|
|
'tabIndex',
|
|
'dom',
|
|
'stateLoadCallback',
|
|
'stateSaveCallback',
|
|
'renderer',
|
|
'searchDelay',
|
|
'rowId',
|
|
'caption',
|
|
'layout',
|
|
'orderDescReverse',
|
|
'orderIndicators',
|
|
'orderHandler',
|
|
'titleRow',
|
|
'typeDetect',
|
|
'pageLength',
|
|
'searchCols'
|
|
]);
|
|
map(settings.scroll, config, [
|
|
['scrollX', 'x'],
|
|
['scrollY', 'y'],
|
|
['scrollCollapse', 'collapse']
|
|
]);
|
|
map(settings.language, config, 'infoCallback');
|
|
// Setup global search
|
|
settings.searches['*'] = create$1(config.search);
|
|
/* Callback functions which are array driven */
|
|
callbackReg(settings, 'draw', config.drawCallback);
|
|
callbackReg(settings, 'stateSaveParams', config.stateSaveParams);
|
|
callbackReg(settings, 'stateLoadParams', config.stateLoadParams);
|
|
callbackReg(settings, 'stateLoaded', config.stateLoaded);
|
|
callbackReg(settings, 'row', config.rowCallback);
|
|
callbackReg(settings, 'rowCreated', config.createdRow);
|
|
callbackReg(settings, 'header', config.headerCallback);
|
|
callbackReg(settings, 'footer', config.footerCallback);
|
|
callbackReg(settings, 'init', config.initComplete);
|
|
callbackReg(settings, 'preDraw', config.preDrawCallback);
|
|
settings.rowIdFn = util.get(settings.rowId);
|
|
// Add event listeners
|
|
if (config.on) {
|
|
Object.keys(config.on).forEach(function (key) {
|
|
listener(table, key, config.on[key]);
|
|
});
|
|
}
|
|
// Browser support detection
|
|
browserDetect(settings);
|
|
var classes = settings.classes;
|
|
util.object.assignDeep(classes, ext.classes, config.classes);
|
|
table.classAdd(classes.table);
|
|
if (!settings.features.paging) {
|
|
config.displayStart = 0;
|
|
}
|
|
if (settings.displayStartInit === -1) {
|
|
// Display start point, taking into account the save saving
|
|
settings.displayStartInit = config.displayStart;
|
|
settings.displayStart = config.displayStart; // TODO remove !
|
|
}
|
|
var defer = config.deferLoading;
|
|
if (defer !== null) {
|
|
settings.deferLoading = true;
|
|
if (Array.isArray(defer)) {
|
|
settings.recordsDisplay = defer[0];
|
|
settings.recordsTotal = defer[1];
|
|
}
|
|
else {
|
|
settings.recordsDisplay = defer; // TODO remove !
|
|
settings.recordsTotal = defer;
|
|
}
|
|
}
|
|
/*
|
|
* Columns
|
|
* See if we should load columns automatically or use defined ones
|
|
*/
|
|
var columnsInit = [];
|
|
var thead = table.children('thead');
|
|
var initHeaderLayout = detectHeader(settings, thead.get(0), false);
|
|
// If we don't have a columns array, then generate one with nulls
|
|
if (config.columns) {
|
|
columnsInit = config.columns;
|
|
}
|
|
else if (initHeaderLayout.length) {
|
|
for (i = 0, iLen = initHeaderLayout[0].length; i < iLen; i++) {
|
|
columnsInit.push(null);
|
|
}
|
|
}
|
|
// Add the columns
|
|
for (i = 0, iLen = columnsInit.length; i < iLen; i++) {
|
|
addColumn(settings);
|
|
}
|
|
// Apply the column definitions
|
|
applyColumnDefs(settings, config.columnDefs, columnsInit, initHeaderLayout, function (idx, def) {
|
|
columnOptions(settings, idx, def);
|
|
});
|
|
/* HTML5 attribute detection - build an mData object automatically if
|
|
* the attributes are found
|
|
*/
|
|
var rowOne = table.children('tbody').find('tr:first-child').eq(0);
|
|
if (rowOne.count()) {
|
|
var a = function (cell, name) {
|
|
return cell.getAttribute('data-' + name) !== null ? name : null;
|
|
};
|
|
rowOne
|
|
.eq(0)
|
|
.children('th, td')
|
|
.each(function (cell, loop) {
|
|
var col = settings.columns[loop];
|
|
if (!col) {
|
|
log(settings, 0, 'Incorrect column count', 18);
|
|
}
|
|
if (col.data === loop) {
|
|
var sort = a(cell, 'sort') || a(cell, 'order');
|
|
var filter = a(cell, 'filter') || a(cell, 'search');
|
|
if (sort !== null || filter !== null) {
|
|
col.data = {
|
|
_: loop + '.display',
|
|
sort: sort !== null
|
|
? loop + '.@data-' + sort
|
|
: undefined,
|
|
type: sort !== null
|
|
? loop + '.@data-' + sort
|
|
: undefined,
|
|
filter: filter !== null
|
|
? loop + '.@data-' + filter
|
|
: undefined
|
|
};
|
|
col._isArrayHost = true;
|
|
columnOptions(settings, loop);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// Must be done after everything which can be overridden by the state
|
|
// saving!
|
|
callbackReg(settings, 'draw', saveState);
|
|
var features = settings.features;
|
|
if (config.stateSave) {
|
|
features.stateSave = true;
|
|
}
|
|
// If aaSorting is not defined, then we use the first indicator in
|
|
// asSorting in case that has been altered, so the default sort reflects
|
|
// that option
|
|
if (config.order === undefined) {
|
|
var sorting = settings.order;
|
|
for (i = 0, iLen = sorting.length; i < iLen; i++) {
|
|
sorting[i][1] = settings.columns[i].orderSequence[0];
|
|
}
|
|
}
|
|
// Do a first pass on the sorting classes (allows any size changes to be
|
|
// taken into account, and also will apply sorting disabled classes if
|
|
// disabled
|
|
sortingClasses(settings);
|
|
callbackReg(settings, 'draw', function () {
|
|
if (settings.wasOrdered ||
|
|
dataSource(settings) === 'ssp' ||
|
|
features.deferRender) {
|
|
sortingClasses(settings);
|
|
}
|
|
});
|
|
/*
|
|
* Table HTML init Cache the header, body and footer as required,
|
|
* creating them if needed
|
|
*/
|
|
var caption = table.children('caption');
|
|
if (settings.caption) {
|
|
if (caption.count() === 0) {
|
|
caption = Dom.c('caption').prependTo(table);
|
|
}
|
|
caption.html(settings.caption);
|
|
}
|
|
// Store the caption side, so we can remove the element from the
|
|
// document when creating the element
|
|
if (caption.count()) {
|
|
caption.get(0)._captionSide = caption.css('caption-side');
|
|
settings.captionNode = caption.get(0);
|
|
}
|
|
// Place the colgroup element in the correct location for the HTML
|
|
// structure
|
|
if (caption.count()) {
|
|
settings.colgroup.insertAfter(caption.get(0));
|
|
}
|
|
else {
|
|
settings.colgroup.prependTo(tableEl);
|
|
}
|
|
if (thead.count() === 0) {
|
|
thead = Dom.c('thead').appendTo(table);
|
|
}
|
|
settings.thead = thead.get(0);
|
|
var tbody = table.children('tbody');
|
|
if (tbody.count() === 0) {
|
|
tbody = Dom.c('tbody').insertAfter(thead.get(0));
|
|
}
|
|
settings.tbody = tbody.get(0);
|
|
var tfoot = table.children('tfoot');
|
|
if (tfoot.count() === 0) {
|
|
// If we are a scrolling table, and no footer has been given, then
|
|
// we need to create a tfoot element for the caption element to be
|
|
// appended to
|
|
tfoot = Dom.c('tfoot').appendTo(tableEl);
|
|
}
|
|
settings.tfoot = tfoot.get(0);
|
|
// Copy the data index array
|
|
settings.display = settings.displayMaster.slice();
|
|
// Initialisation complete - table can be drawn
|
|
settings.initialised = true;
|
|
// Language definitions
|
|
var language = settings.language;
|
|
if (config.language) {
|
|
util.object.assignDeep(language, config.language);
|
|
}
|
|
if (language.ajax) {
|
|
let languageLoaded = function (json) {
|
|
hungarianToCamel(json);
|
|
util.object.assignDeep(language, json, settings.init.language);
|
|
callbackFire(settings, null, 'i18n', [settings], true);
|
|
initialise(settings);
|
|
};
|
|
// Get the language definitions from a remote
|
|
if (typeof language.ajax === 'function') {
|
|
language.ajax(settings, languageLoaded);
|
|
}
|
|
else {
|
|
let ajaxBase = {
|
|
dataType: 'json',
|
|
url: '',
|
|
success: languageLoaded,
|
|
error: function () {
|
|
// Error occurred loading language file
|
|
log(settings, 0, 'i18n file loading error', 21);
|
|
// Continue on as best we can
|
|
initialise(settings);
|
|
}
|
|
};
|
|
if (typeof language.ajax === 'string') {
|
|
ajaxBase.url = language.ajax;
|
|
}
|
|
else {
|
|
ajaxBase = util.object.assign(ajaxBase, language.ajax);
|
|
}
|
|
util.ajax(ajaxBase);
|
|
}
|
|
}
|
|
else {
|
|
callbackFire(settings, null, 'i18n', [settings], true);
|
|
initialise(settings);
|
|
}
|
|
});
|
|
// This is unusual, but we want the return from the exposed `DataTable`
|
|
// function to be an API instance, rather than the core, which is not
|
|
// publicly exposed. This is also the reason for the `as unknown` below - TS
|
|
// doesn't like the return with a different object.
|
|
return this.api();
|
|
};
|
|
DataTable.type = register$1;
|
|
DataTable.types = types;
|
|
DataTable.render = helpers;
|
|
DataTable.ext = ext;
|
|
DataTable.use = util.external;
|
|
DataTable.factory = factory;
|
|
DataTable.versionCheck = util.version.check;
|
|
DataTable.version = ext.version;
|
|
DataTable.isDataTable = isDataTable;
|
|
DataTable.tables = tables;
|
|
DataTable.util = util;
|
|
DataTable.Api = Api;
|
|
DataTable.datetime = datetime;
|
|
DataTable.__browser = browser;
|
|
DataTable.Dom = Dom;
|
|
DataTable.ajax = util.ajax;
|
|
DataTable.key = key;
|
|
plus(DataTable);
|
|
/**
|
|
* Private data store, containing all of the settings objects that are created
|
|
* for the tables on a given page.
|
|
*/
|
|
DataTable.settings = ext.settings;
|
|
/**
|
|
* Object models container, for the various models that DataTables has available
|
|
* to it. These models define the objects that are used to hold the active state
|
|
* and configuration of the table.
|
|
*/
|
|
DataTable.models = models;
|
|
DataTable.defaults = defaults;
|
|
DataTable.feature = {
|
|
register: register$2
|
|
};
|
|
// Register the libraries
|
|
util.external(DataTable);
|
|
if (window.jQuery) {
|
|
util.external(window.jQuery);
|
|
}
|
|
|
|
|
|
return DataTable;
|
|
}));
|
|
|
|
|
|
/*! DataTables Bootstrap 5 integration
|
|
* © SpryMedia Ltd - datatables.net/license
|
|
*/
|
|
|
|
(function(factory){
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD
|
|
define(['datatables.net'], function (dt) {
|
|
return factory(window, document, dt);
|
|
});
|
|
}
|
|
else if (typeof exports === 'object') {
|
|
// CommonJS
|
|
var cjsRequires = function (root) {
|
|
if (! root.DataTable) {
|
|
require('datatables.net')(root);
|
|
}
|
|
};
|
|
|
|
if (typeof window === 'undefined') {
|
|
module.exports = function (root) {
|
|
if (! root) {
|
|
// CommonJS environments without a window global must pass a
|
|
// root. This will give an error otherwise
|
|
root = window;
|
|
}
|
|
|
|
cjsRequires(root);
|
|
return factory(root, root.document, root.DataTable);
|
|
};
|
|
}
|
|
else {
|
|
cjsRequires(window);
|
|
module.exports = factory(window, window.document, window.DataTable);
|
|
}
|
|
}
|
|
else {
|
|
// Browser
|
|
factory(window, document, window.DataTable);
|
|
}
|
|
}(function(window, document, DataTable) {
|
|
'use strict';
|
|
|
|
|
|
/* Set the defaults for DataTables initialisation */
|
|
DataTable.util.object.assignDeep(DataTable.defaults, {
|
|
renderer: 'bootstrap'
|
|
});
|
|
/* Default class modification */
|
|
DataTable.util.object.assignDeep(DataTable.ext.classes, {
|
|
container: 'dt-container dt-bootstrap5',
|
|
search: {
|
|
input: 'form-control form-control-sm'
|
|
},
|
|
length: {
|
|
select: 'form-select form-select-sm'
|
|
},
|
|
processing: {
|
|
container: 'dt-processing card'
|
|
},
|
|
layout: {
|
|
row: 'row mt-2 justify-content-between',
|
|
cell: 'd-md-flex justify-content-between align-items-center',
|
|
tableCell: 'col-12',
|
|
start: 'dt-layout-start col-md-auto me-auto',
|
|
end: 'dt-layout-end col-md-auto ms-auto',
|
|
full: 'dt-layout-full col-md'
|
|
}
|
|
});
|
|
/* Bootstrap paging button renderer */
|
|
DataTable.ext.renderer.pagingButton.bootstrap = function (settings, buttonType, content, active, disabled) {
|
|
var btnClasses = ['dt-paging-button', 'page-item'];
|
|
if (active) {
|
|
btnClasses.push('active');
|
|
}
|
|
if (disabled) {
|
|
btnClasses.push('disabled');
|
|
}
|
|
var li = DataTable.Dom.c('li').classAdd(btnClasses.join(' '));
|
|
var a = DataTable.Dom
|
|
.c('button')
|
|
.classAdd('page-link')
|
|
.attr('role', 'link')
|
|
.attr('type', 'button')
|
|
.html(content)
|
|
.appendTo(li);
|
|
return {
|
|
display: li.get(0),
|
|
clicker: a.get(0)
|
|
};
|
|
};
|
|
DataTable.ext.renderer.pagingContainer.bootstrap = function (settings, buttonEls) {
|
|
return DataTable.Dom
|
|
.c('ul')
|
|
.classAdd('pagination')
|
|
.append(buttonEls)
|
|
.get(0);
|
|
};
|
|
|
|
|
|
return DataTable;
|
|
}));
|
|
|
|
|