src/extensions/sort/adapterSortabletable.js
import {Feature} from '../../feature';
import {isUndef, isObj, EMPTY_FN} from '../../types';
import {createElm, elm, tag} from '../../dom';
import {addEvt, bound} from '../../event';
import {parse as parseNb} from '../../number';
import {
NONE, CELL_TAG, HEADER_TAG, STRING, NUMBER, DATE, FORMATTED_NUMBER,
IP_ADDRESS
} from '../../const';
import {defaultsStr, defaultsFn, defaultsArr} from '../../settings';
/**
* SortableTable Adapter module
*/
export default class AdapterSortableTable extends Feature {
/**
* Creates an instance of AdapterSortableTable
* @param {TableFilter} tf TableFilter instance
* @param {Object} opts Configuration object
*/
constructor(tf, opts) {
super(tf, AdapterSortableTable);
/**
* Module name
* @type {String}
*/
this.name = opts.name;
/**
* Module description
* @type {String}
*/
this.desc = defaultsStr(opts.description, 'Sortable table');
/**
* Indicate whether table previously sorted
* @type {Boolean}
* @private
*/
this.sorted = false;
/**
* List of sort type per column basis
* @type {Array}
*/
this.sortTypes = defaultsArr(opts.types, tf.colTypes);
/**
* Column to be sorted at initialization, ie:
* sort_col_at_start: [1, true]
* @type {Array}
*/
this.sortColAtStart = defaultsArr(opts.sort_col_at_start, null);
/**
* Enable asynchronous sort, if triggers are external
* @type {Boolean}
*/
this.asyncSort = Boolean(opts.async_sort);
/**
* List of element IDs triggering sort on a per column basis
* @type {Array}
*/
this.triggerIds = defaultsArr(opts.trigger_ids, []);
// edit .sort-arrow.descending / .sort-arrow.ascending in
// tablefilter.css to reflect any path change
/**
* Path to images
* @type {String}
*/
this.imgPath = defaultsStr(opts.images_path, tf.themesPath);
/**
* Blank image file name
* @type {String}
*/
this.imgBlank = defaultsStr(opts.image_blank, 'blank.png');
/**
* Css class for sort indicator image
* @type {String}
*/
this.imgClassName = defaultsStr(opts.image_class_name, 'sort-arrow');
/**
* Css class for ascending sort indicator image
* @type {String}
*/
this.imgAscClassName = defaultsStr(opts.image_asc_class_name,
'ascending');
/**
* Css class for descending sort indicator image
* @type {String}
*/
this.imgDescClassName = defaultsStr(opts.image_desc_class_name,
'descending');
/**
* Cell attribute key storing custom value used for sorting
* @type {String}
*/
this.customKey = defaultsStr(opts.custom_key, 'data-tf-sortKey');
/**
* Callback fired when sort extension is instanciated
* @type {Function}
*/
this.onSortLoaded = defaultsFn(opts.on_sort_loaded, EMPTY_FN);
/**
* Callback fired before a table column is sorted
* @type {Function}
*/
this.onBeforeSort = defaultsFn(opts.on_before_sort, EMPTY_FN);
/**
* Callback fired after a table column is sorted
* @type {Function}
*/
this.onAfterSort = defaultsFn(opts.on_after_sort, EMPTY_FN);
/**
* SortableTable instance
* @private
*/
this.stt = null;
this.enable();
}
/**
* Initializes AdapterSortableTable instance
*/
init() {
if (this.initialized) {
return;
}
let tf = this.tf;
let adpt = this;
// SortableTable class sanity check (sortabletable.js)
if (isUndef(SortableTable)) {
throw new Error('SortableTable class not found.');
}
// Add any date format if needed
this.emitter.emit('add-date-type-formats', this.tf, this.sortTypes);
this.overrideSortableTable();
this.setSortTypes();
this.onSortLoaded(tf, this);
/*** SortableTable callbacks ***/
this.stt.onbeforesort = function () {
adpt.onBeforeSort(tf, adpt.stt.sortColumn);
/*** sort behaviour for paging ***/
if (tf.paging) {
tf.feature('paging').disable();
}
};
this.stt.onsort = function () {
adpt.sorted = true;
//sort behaviour for paging
if (tf.paging) {
let paginator = tf.feature('paging');
// recalculate valid rows index as sorting may have change it
tf.getValidRows(true);
paginator.enable();
paginator.setPage(paginator.getPage());
}
adpt.onAfterSort(tf, adpt.stt.sortColumn, adpt.stt.descending);
adpt.emitter.emit('column-sorted', tf, adpt.stt.sortColumn,
adpt.stt.descending);
};
// Column sort at start
let sortColAtStart = adpt.sortColAtStart;
if (sortColAtStart) {
this.stt.sort(sortColAtStart[0], sortColAtStart[1]);
}
this.emitter.on(['sort'], bound(this.sortByColumnIndexHandler, this));
/** @inherited */
this.initialized = true;
this.emitter.emit('sort-initialized', tf, this);
}
/**
* Sort specified column
* @param {Number} colIdx Column index
* @param {Boolean} desc Optional: descending manner
*/
sortByColumnIndex(colIdx, desc) {
this.stt.sort(colIdx, desc);
}
/** @private */
sortByColumnIndexHandler(tf, colIdx, desc) {
this.sortByColumnIndex(colIdx, desc);
}
/**
* Set SortableTable overrides for TableFilter integration
*/
overrideSortableTable() {
let adpt = this,
tf = this.tf;
/**
* Overrides headerOnclick method in order to handle th event
* @param {Object} e [description]
*/
SortableTable.prototype.headerOnclick = function (evt) {
if (!adpt.initialized) {
return;
}
// find Header element
let el = evt.target || evt.srcElement;
while (el.tagName !== CELL_TAG && el.tagName !== HEADER_TAG) {
el = el.parentNode;
}
this.sort(
SortableTable.msie ?
SortableTable.getCellIndex(el) : el.cellIndex
);
};
/**
* Overrides getCellIndex IE returns wrong cellIndex when columns are
* hidden
* @param {Object} oTd TD element
* @return {Number} Cell index
*/
SortableTable.getCellIndex = function (oTd) {
let cells = oTd.parentNode.cells,
l = cells.length, i;
for (i = 0; cells[i] !== oTd && i < l; i++) { }
return i;
};
/**
* Overrides initHeader in order to handle filters row position
* @param {Array} oSortTypes
*/
SortableTable.prototype.initHeader = function (oSortTypes) {
let stt = this;
if (!stt.tHead) {
if (tf.gridLayout) {
stt.tHead = tf.feature('gridLayout').headTbl.tHead;
} else {
return;
}
}
stt.headersRow = tf.headersRow;
let cells = stt.tHead.rows[stt.headersRow].cells;
stt.sortTypes = oSortTypes || [];
let l = cells.length;
let img, c;
for (let i = 0; i < l; i++) {
c = cells[i];
if (stt.sortTypes[i] !== null && stt.sortTypes[i] !== 'None') {
c.style.cursor = 'pointer';
img = createElm('img',
['src', adpt.imgPath + adpt.imgBlank]);
c.appendChild(img);
if (stt.sortTypes[i] !== null) {
c.setAttribute('_sortType', stt.sortTypes[i]);
}
addEvt(c, 'click', stt._headerOnclick);
} else {
c.setAttribute('_sortType', oSortTypes[i]);
c._sortType = 'None';
}
}
stt.updateHeaderArrows();
};
/**
* Overrides updateHeaderArrows in order to handle arrows indicators
*/
SortableTable.prototype.updateHeaderArrows = function () {
let stt = this;
let cells, l, img;
// external headers
if (adpt.asyncSort && adpt.triggerIds.length > 0) {
let triggers = adpt.triggerIds;
cells = [];
l = triggers.length;
for (let j = 0; j < l; j++) {
cells.push(elm(triggers[j]));
}
} else {
if (!this.tHead) {
return;
}
cells = stt.tHead.rows[stt.headersRow].cells;
l = cells.length;
}
for (let i = 0; i < l; i++) {
let cell = cells[i];
if (!cell) {
continue;
}
let cellAttr = cell.getAttribute('_sortType');
if (cellAttr !== null && cellAttr !== 'None') {
img = cell.lastChild || cell;
if (img.nodeName.toLowerCase() !== 'img') {
img = createElm('img',
['src', adpt.imgPath + adpt.imgBlank]);
cell.appendChild(img);
}
if (i === stt.sortColumn) {
img.className = adpt.imgClassName + ' ' +
(this.descending ?
adpt.imgDescClassName :
adpt.imgAscClassName);
} else {
img.className = adpt.imgClassName;
}
}
}
};
/**
* Overrides getRowValue for custom key value feature
* @param {Object} oRow Row element
* @param {String} sType
* @param {Number} nColumn
* @return {String}
*/
SortableTable.prototype.getRowValue = function (oRow, sType, nColumn) {
let stt = this;
// if we have defined a custom getRowValue use that
let sortTypeInfo = stt._sortTypeInfo[sType];
if (sortTypeInfo && sortTypeInfo.getRowValue) {
return sortTypeInfo.getRowValue(oRow, nColumn);
}
let c = oRow.cells[nColumn];
let s = SortableTable.getInnerText(c);
return stt.getValueFromString(s, sType);
};
/**
* Overrides getInnerText in order to avoid Firefox unexpected sorting
* behaviour with untrimmed text elements
* @param {Object} cell DOM element
* @return {String} DOM element inner text
*/
SortableTable.getInnerText = function (cell) {
if (!cell) {
return;
}
if (cell.getAttribute(adpt.customKey)) {
return cell.getAttribute(adpt.customKey);
} else {
return tf.getCellValue(cell);
}
};
}
/**
* Adds a sort type
*/
addSortType(...args) {
// Extract the arguments
let [id, caster, sorter, getRowValue] = args;
SortableTable.prototype.addSortType(id, caster, sorter, getRowValue);
}
/**
* Sets the sort types on a column basis
* @private
*/
setSortTypes() {
let tf = this.tf,
sortTypes = this.sortTypes,
_sortTypes = [];
tf.eachCol((i) => {
let colType;
if (sortTypes[i]) {
colType = sortTypes[i];
if (isObj(colType)) {
if (colType.type === DATE) {
colType = this._addDateType(i, sortTypes);
}
else if (colType.type === FORMATTED_NUMBER) {
let decimal = colType.decimal || tf.decimalSeparator;
colType = this._addNumberType(i, decimal);
}
} else {
colType = colType.toLowerCase();
if (colType === DATE) {
colType = this._addDateType(i, sortTypes);
}
else if (colType === FORMATTED_NUMBER ||
colType === NUMBER) {
colType = this._addNumberType(i, tf.decimalSeparator);
}
else if (colType === NONE) {
// TODO: normalise 'none' vs 'None'
colType = 'None';
}
}
} else {
colType = STRING;
}
_sortTypes.push(colType);
});
//Public TF method to add sort type
//Custom sort types
this.addSortType('caseinsensitivestring', SortableTable.toUpperCase);
this.addSortType(STRING);
this.addSortType(IP_ADDRESS, ipAddress, sortIP);
this.stt = new SortableTable(tf.dom(), _sortTypes);
/*** external table headers adapter ***/
if (this.asyncSort && this.triggerIds.length > 0) {
let triggers = this.triggerIds;
for (let j = 0; j < triggers.length; j++) {
if (triggers[j] === null) {
continue;
}
let trigger = elm(triggers[j]);
if (trigger) {
trigger.style.cursor = 'pointer';
addEvt(trigger, 'click', (evt) => {
let elm = evt.target;
if (!this.tf.sort) {
return;
}
this.stt.asyncSort(triggers.indexOf(elm.id));
});
trigger.setAttribute('_sortType', _sortTypes[j]);
}
}
}
}
_addDateType(colIndex, types) {
let tf = this.tf;
let dateType = tf.feature('dateType');
let locale = dateType.getOptions(colIndex, types).locale || tf.locale;
let colType = `${DATE}-${locale}`;
this.addSortType(colType, (value) => {
let parsedDate = dateType.parse(value, locale);
// Invalid date defaults to Wed Feb 04 -768 11:00:00
return isNaN(+parsedDate) ? new Date(-86400000000000) : parsedDate;
});
return colType;
}
_addNumberType(colIndex, decimal) {
let colType = `${FORMATTED_NUMBER}${decimal === '.' ? '' : '-custom'}`;
this.addSortType(colType, (value) => {
return parseNb(value, decimal);
});
return colType;
}
/**
* Remove extension
*/
destroy() {
if (!this.initialized) {
return;
}
let tf = this.tf;
this.emitter.off(['sort'], bound(this.sortByColumnIndexHandler, this));
this.sorted = false;
this.stt.destroy();
let ids = tf.getFiltersId();
for (let idx = 0; idx < ids.length; idx++) {
let header = tf.getHeaderElement(idx);
let img = tag(header, 'img');
if (img.length === 1) {
header.removeChild(img[0]);
}
}
this.initialized = false;
}
}
AdapterSortableTable.meta = {altName: 'sort'};
//Converters
function ipAddress(value) {
let vals = value.split('.');
// eslint-disable-next-line no-unused-vars
for (let x in vals) {
let val = vals[x];
while (3 > val.length) {
val = '0' + val;
}
vals[x] = val;
}
return vals.join('.');
}
function sortIP(a, b) {
let aa = ipAddress(a.value.toLowerCase());
let bb = ipAddress(b.value.toLowerCase());
if (aa === bb) {
return 0;
} else if (aa < bb) {
return -1;
} else {
return 1;
}
}