/*
* Copyright 2020 Martin F. Schlegel Jr. | MIT AND BSD-3-Clause
*/
// Virtual Interfaces
// SortDescriptor
/**
*
* @interface SortDescriptor
* @classdesc
*
* Describes the information necessary to sort a table based upon a column.
*/
/**
* Optional property indicating the index of the column this `SortDescriptor` describes how to sort. If this property
* is not a positive number, it will be assumed this `SortDescriptor` describes how to sort entire rows.
*
* @member {number} SortDescriptor#columnIndex
*/
/**
* Callback comparator function that compares one cell or row to another. If {@link SortDescriptor#columnIndex} is defined, and
* a postiive number, `HTMLTableCellElement`s in the approprite position within the rows being compared will be passed to this
* function corresponding to this `SortDescriptor`'s {@link SortDescriptor#columnIndex} property, otherwise the `HTMLTableRowElement`s
* undergoing comparison will be passed.
*
* If this function returns a value greater than 0, the relevant row corresponding to the first given item will be sorted below the second,
* if a value less than 0, the first will be sorted above the second, and if 0, no preference will be applied.
*
* @function SortDescriptor#compare
* @param {HTMLCellElement|HTMLTableRowElement} itemA Reference cell or row.
* @param {HTMLCellElement|HTMLTableRowElement} itemB Compare cell or row.
* @returns {number}
* A value greater than 0 if `itemA` should be sorted below `itemB`, less than 0 for above, 0 for no preference.
*/
// FilterDescriptor
/**
* @interface FilterDescriptor
* @classdesc
*
* Describes the information necessary to filter a table based upon a column.
*/
/**
* Optional property indicating the index of the column this `FilterDescriptor` describes how to filter. If this property
* is not a positive number, it will be assumed this `FilterDescriptor` describes how to filter entire rows.
*
* @member {number} FilterDescriptor#columnIndex
*/
/**
* Callback function to determine whether the given row or cell's parent row should be filtered. If {@link FilterDescriptor#columnIndex}
* is defined, and a postiive number, the `HTMLTableCellElement` in the approprite position within the row being considered will be passed
* to this function corresponding to this `FilterDescriptor`'s {@link FilterDescriptor#columnIndex} property, otherwise the `HTMLTableRowElement`
* being considered will be passed directly.
*
* If this function returns `false`, the relevant row will be filtered.
*
* @function FilterDescriptor#include
* @param {HTMLCellElement|HTMLTableRowElement} item Cell or Row to be considered for inclusion.
* @returns {boolean} `false` if the given `item` should be filtered.
*/
// Constructor
/**
*
* @constructor
* @param {HTMLTableElement} table Table element this `HTMLTableWrapper` is to process.
* @throws {ReferenceError} If `table` is not defined, or does not have any table body sections.
* @classdesc
*
* Wrapper for `HTMLTableElement`s that provides a limited set of extended functionality, most notably the capibility of {@link HTMLTableWrapper#sort sorting} and
* {@link HTMLTableWrapper#filter filtering} the first table body section.
*
* As the description implies, the given `table` must define at least one table body section, and that section should contain the table's primary data set
* (any subsequent table body sections are ignored). It is also assumed all the cells for each column are aligned (i.e. they all define the same `colSpan`;
* any misaligned rows will likely result in `RangeError`s).
*
*/
function HTMLTableWrapper(table) {
'use strict';
if (!table || !table.tBodies || !table.tBodies.length) {
throw new ReferenceError('Table must be an defined and have a body.');
}
/**
* Backing `HTMLTableElement`.
*
* @private
* @type {HTMLTableElement}
*/
this.table = table;
/**
* Cache of the initial state of the table's rows. Used when sort parameters are {@link HTMLTableWrapper#clearSort cleared}.
*
* @private
* @type {Array}
*/
this.initialOrder = HTMLTableWrapper.copy(table.tBodies[0].rows);
}
// Static Fields
/**
* Class name added to the class list of filtered elements. Default value is `'data-table-filtered'`.
*
* @type {string}
*/
HTMLTableWrapper.filteredClassName = 'data-table-filtered';
// Static methods
/**
* Utility function to copy the elements from the given `src` {@link MinimalList} into a new `Array`.
*
* @private
* @param {MinimalList} src List to be copied.
* @return {Array} An `Array` containing the same elements of the given `src`.
*/
HTMLTableWrapper.copy = function (src) {
var result, i;
result = [];
for (i = 0; i < src.length; ++i) {
result.push(src[i]);
}
return result;
};
// Instance methods
/**
* Sorts the first table body section of the backing table according to the given {@link SortDescriptor}s. This function can be called with a single
* `Array` of {@link SortDescriptor}s or in a variadic manner. If no arguments are provided, or a zero-length `Array` is provided for argument 0,
* {@link HTMLTableWrapper#clearSort} is implicitly called.
*
* @param {...SortDescriptor} args
* {@link SortDescriptor}s to process. If the first argument is an `Array`, it will be used and subsequent arguments
* will be ignored.
*/
HTMLTableWrapper.prototype.sort = function () {
'use strict';
var sortDescriptors, sortDescriptor, i, tbody, rows, copy, _this;
// Pre-Validation Initialization.
sortDescriptors = arguments[0] instanceof Array ? arguments[0] : arguments;
if (!sortDescriptors.length) {
this.clearSort();
return;
}
// Validation.
for (i = 0; i < sortDescriptors.length; ++i) {
sortDescriptor = sortDescriptors[i];
if (!sortDescriptor) {
throw new ReferenceError('Invalid reference supplied for sort descriptor at index ' + i + ': ' + sortDescriptor);
}
if (typeof sortDescriptor.compare !== 'function') {
throw new TypeError('Sort descriptor does not define define the compare property (of type function) at index ' + i);
}
}
// Post-Validation Initialization.
tbody = this.table.tBodies[0];
rows = tbody.rows;
copy = HTMLTableWrapper.copy(rows);
// Perform sort.
_this = this;
copy.sort(function (rowA, rowB) {
var sortDescriptor, columnIndex, cellA, cellB, compareValue, i;
for (i = 0; i < sortDescriptors.length; ++i) {
sortDescriptor = sortDescriptors[i];
columnIndex = sortDescriptor.columnIndex;
if (typeof columnIndex === 'number' && columnIndex >= 0) {
cellA = rowA.cells[columnIndex];
cellB = rowB.cells[columnIndex];
compareValue = sortDescriptor.compare(cellA, cellB);
} else {
compareValue = sortDescriptor.compare(rowA, rowB);
}
// Allowing type coercion if (for whatever reason) the compare function does not return an integer.
if (compareValue != 0) {
return compareValue;
}
}
return 0;
});
// Update table.
while (rows.length) {
tbody.removeChild(rows[0]);
}
for (i = 0; i < copy.length; ++i) {
tbody.appendChild(copy[i]);
}
};
/**
* Filters the first table body section of the backing table according to the given {@link FilterDescriptor}s. This function can be called with a single
* `Array` of {@link FilterDescriptor}s or in a variadic manner. If no arguments are provided or a zero-length `Array` is provided for argument 0,
* {@link HTMLTableWrapper#clearFilter} is implicitly called.
*
* @param {...FilterDescriptor} args
* {@link FilterDescriptor}s to process. If the first argument is an `Array`, it will be used and subsequent arguments
* will be ignored.
*/
HTMLTableWrapper.prototype.filter = function () {
'use strict';
var filterDescriptors, filterDescriptor, i, rows, row, filter, j, cells, columnIndex, shouldInclude;
// Initialization.
filterDescriptors = arguments[0] instanceof Array ? arguments[0] : arguments;
if (!filterDescriptors.length) {
this.clearFilter();
return;
}
// Validation.
for (i = 0; i < filterDescriptors.length; ++i) {
filterDescriptor = filterDescriptors[i];
if (!filterDescriptor) {
throw new ReferenceError('Invalid reference supplied for filter descriptor at index ' + i);
}
if (typeof filterDescriptor.include !== 'function') {
throw new TypeError('Filter descriptor does not define the include property (of type function) at index ' + i);
}
}
// Perform filtering.
rows = this.table.tBodies[0].rows;
for (i = 0; i < rows.length; ++i) {
row = rows[i];
cells = row.cells;
filter = false;
for (j = 0; j < filterDescriptors.length; ++j) {
filterDescriptor = filterDescriptors[j];
columnIndex = filterDescriptor.columnIndex;
if (typeof columnIndex === 'number' && columnIndex >= 0) {
shouldInclude = filterDescriptor.include(cells[columnIndex]);
} else {
shouldInclude = filterDescriptor.include(row);
}
if (!shouldInclude) {
filter = true;
break;
}
}
if (filter) {
IE8Compatibility.addClass(row, HTMLTableWrapper.filteredClassName);
} else {
IE8Compatibility.removeClass(row, HTMLTableWrapper.filteredClassName);
}
}
};
/**
* Clears all filters.
*/
HTMLTableWrapper.prototype.clearFilter = function () {
'use strict';
var i, rows;
rows = this.table.tBodies[0].rows;
for (i = 0; i < rows.length; ++i) {
IE8Compatibility.removeClass(rows[i], HTMLTableWrapper.filteredClassName);
}
};
/**
* Clears the sorting for all columns. The original order for all rows (at the time this `HTMLTableWrapper` was constructed) is restored.
*/
HTMLTableWrapper.prototype.clearSort = function () {
'use strict';
var initialOrder, tbody, rows, i;
tbody = this.table.tBodies[0];
rows = tbody.rows;
initialOrder = this.initialOrder;
while (rows.length) {
tbody.removeChild(rows[0]);
}
for (i = 0; i < initialOrder.length; ++i) {
tbody.appendChild(initialOrder[i]);
}
};
/**
* Returns the `HTMLTableElement` backing this `HTMLTableWrapper`.
*
* @returns {HTMLTableElement} The `HTMLTableElement` backing this `HTMLTableWrapper`.
*/
HTMLTableWrapper.prototype.getTableElement = function () {
'use strict';
return this.table;
};
/**
* Returns the `HTMLTableRowElement`s of the first table body section of the backing table. Rows that have been filtered are excluded unless
* `includeFiltered` is `true`.
*
* *IMPLEMENTATION NOTE:* Callers to this function should only rely on the interface defined by {@link MinimalList}, as this method may return
* either an `Array` or a `NodeList`.
*
* @param {boolean} [includeFiltered=false] Whether to include rows that are filtered in the result.
* @returns {MinimalList} `HTMLTableRowElement`s of the first table body section of the backing table.
*/
HTMLTableWrapper.prototype.getRows = function (includeFiltered) {
'use strict';
var rows, row, i, result;
rows = this.table.tBodies[0].rows;
if (includeFiltered) {
return rows;
}
result = [];
for (i = 0; i < rows.length; ++i) {
row = rows[i];
if (!IE8Compatibility.hasClass(row, HTMLTableWrapper.filteredClassName)) {
result.push(row);
}
}
return result;
};