var DataTable = (function (Sortable,Clusterize) { 'use strict'; Sortable = Sortable && Sortable.hasOwnProperty('default') ? Sortable['default'] : Sortable; Clusterize = Clusterize && Clusterize.hasOwnProperty('default') ? Clusterize['default'] : Clusterize; function $(expr, con) { return typeof expr === 'string' ? (con || document).querySelector(expr) : expr || null; } $.each = (expr, con) => { return typeof expr === 'string' ? Array.from((con || document).querySelectorAll(expr)) : expr || null; }; $.create = (tag, o) => { let element = document.createElement(tag); for (let i in o) { let val = o[i]; if (i === 'inside') { $(val).appendChild(element); } else if (i === 'around') { let ref = $(val); ref.parentNode.insertBefore(element, ref); element.appendChild(ref); } else if (i === 'styles') { if (typeof val === 'object') { Object.keys(val).map(prop => { element.style[prop] = val[prop]; }); } } else if (i in element) { element[i] = val; } else { element.setAttribute(i, val); } } return element; }; $.on = (element, event, selector, callback) => { if (!callback) { callback = selector; $.bind(element, event, callback); } else { $.delegate(element, event, selector, callback); } }; $.off = (element, event, handler) => { element.removeEventListener(event, handler); }; $.bind = (element, event, callback) => { event.split(/\s+/).forEach(function (event) { element.addEventListener(event, callback); }); }; $.delegate = (element, event, selector, callback) => { element.addEventListener(event, function (e) { const delegatedTarget = e.target.closest(selector); if (delegatedTarget) { e.delegatedTarget = delegatedTarget; callback.call(this, e, delegatedTarget); } }); }; $.unbind = (element, o) => { if (element) { for (let event in o) { let callback = o[event]; event.split(/\s+/).forEach(function (event) { element.removeEventListener(event, callback); }); } } }; $.fire = (target, type, properties) => { let evt = document.createEvent('HTMLEvents'); evt.initEvent(type, true, true); for (let j in properties) { evt[j] = properties[j]; } return target.dispatchEvent(evt); }; $.data = (element, attrs) => { // eslint-disable-line if (!attrs) { return element.dataset; } for (const attr in attrs) { element.dataset[attr] = attrs[attr]; } }; $.style = (elements, styleMap) => { // eslint-disable-line if (typeof styleMap === 'string') { return $.getStyle(elements, styleMap); } if (!Array.isArray(elements)) { elements = [elements]; } elements.map(element => { for (const prop in styleMap) { element.style[prop] = styleMap[prop]; } }); }; $.removeStyle = (elements, styleProps) => { if (!Array.isArray(elements)) { elements = [elements]; } if (!Array.isArray(styleProps)) { styleProps = [styleProps]; } elements.map(element => { for (const prop of styleProps) { element.style[prop] = ''; } }); }; $.getStyle = (element, prop) => { let val = getComputedStyle(element)[prop]; if (['width', 'height'].includes(prop)) { val = parseFloat(val); } return val; }; $.closest = (selector, element) => { if (!element) return null; if (element.matches(selector)) { return element; } return $.closest(selector, element.parentNode); }; $.inViewport = (el, parentEl) => { const { top, left, bottom, right } = el.getBoundingClientRect(); const { top: pTop, left: pLeft, bottom: pBottom, right: pRight } = parentEl.getBoundingClientRect(); return top >= pTop && left >= pLeft && bottom <= pBottom && right <= pRight; }; $.scrollTop = function scrollTop(element, pixels) { requestAnimationFrame(() => { element.scrollTop = pixels; }); }; /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * _.isObject({}); * // => true * * _.isObject([1, 2, 3]); * // => true * * _.isObject(_.noop); * // => true * * _.isObject(null); * // => false */ function isObject(value) { var type = typeof value; return value != null && (type == 'object' || type == 'function'); } var isObject_1 = isObject; var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; var _freeGlobal = freeGlobal; var freeSelf = typeof self == 'object' && self && self.Object === Object && self; /** Used as a reference to the global object. */ var root = _freeGlobal || freeSelf || Function('return this')(); var _root = root; var now = function() { return _root.Date.now(); }; var now_1 = now; var Symbol = _root.Symbol; var _Symbol = Symbol; var objectProto = Object.prototype; /** Used to check objects for own properties. */ var hasOwnProperty = objectProto.hasOwnProperty; /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var nativeObjectToString = objectProto.toString; /** Built-in value references. */ var symToStringTag = _Symbol ? _Symbol.toStringTag : undefined; /** * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. * * @private * @param {*} value The value to query. * @returns {string} Returns the raw `toStringTag`. */ function getRawTag(value) { var isOwn = hasOwnProperty.call(value, symToStringTag), tag = value[symToStringTag]; try { value[symToStringTag] = undefined; var unmasked = true; } catch (e) {} var result = nativeObjectToString.call(value); if (unmasked) { if (isOwn) { value[symToStringTag] = tag; } else { delete value[symToStringTag]; } } return result; } var _getRawTag = getRawTag; /** Used for built-in method references. */ var objectProto$1 = Object.prototype; /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var nativeObjectToString$1 = objectProto$1.toString; /** * Converts `value` to a string using `Object.prototype.toString`. * * @private * @param {*} value The value to convert. * @returns {string} Returns the converted string. */ function objectToString(value) { return nativeObjectToString$1.call(value); } var _objectToString = objectToString; var nullTag = '[object Null]'; var undefinedTag = '[object Undefined]'; /** Built-in value references. */ var symToStringTag$1 = _Symbol ? _Symbol.toStringTag : undefined; /** * The base implementation of `getTag` without fallbacks for buggy environments. * * @private * @param {*} value The value to query. * @returns {string} Returns the `toStringTag`. */ function baseGetTag(value) { if (value == null) { return value === undefined ? undefinedTag : nullTag; } return (symToStringTag$1 && symToStringTag$1 in Object(value)) ? _getRawTag(value) : _objectToString(value); } var _baseGetTag = baseGetTag; /** * Checks if `value` is object-like. A value is object-like if it's not `null` * and has a `typeof` result of "object". * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. * @example * * _.isObjectLike({}); * // => true * * _.isObjectLike([1, 2, 3]); * // => true * * _.isObjectLike(_.noop); * // => false * * _.isObjectLike(null); * // => false */ function isObjectLike(value) { return value != null && typeof value == 'object'; } var isObjectLike_1 = isObjectLike; var symbolTag = '[object Symbol]'; /** * Checks if `value` is classified as a `Symbol` primitive or object. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. * @example * * _.isSymbol(Symbol.iterator); * // => true * * _.isSymbol('abc'); * // => false */ function isSymbol(value) { return typeof value == 'symbol' || (isObjectLike_1(value) && _baseGetTag(value) == symbolTag); } var isSymbol_1 = isSymbol; var NAN = 0 / 0; /** Used to match leading and trailing whitespace. */ var reTrim = /^\s+|\s+$/g; /** Used to detect bad signed hexadecimal string values. */ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; /** Used to detect binary string values. */ var reIsBinary = /^0b[01]+$/i; /** Used to detect octal string values. */ var reIsOctal = /^0o[0-7]+$/i; /** Built-in method references without a dependency on `root`. */ var freeParseInt = parseInt; /** * Converts `value` to a number. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to process. * @returns {number} Returns the number. * @example * * _.toNumber(3.2); * // => 3.2 * * _.toNumber(Number.MIN_VALUE); * // => 5e-324 * * _.toNumber(Infinity); * // => Infinity * * _.toNumber('3.2'); * // => 3.2 */ function toNumber(value) { if (typeof value == 'number') { return value; } if (isSymbol_1(value)) { return NAN; } if (isObject_1(value)) { var other = typeof value.valueOf == 'function' ? value.valueOf() : value; value = isObject_1(other) ? (other + '') : other; } if (typeof value != 'string') { return value === 0 ? value : +value; } value = value.replace(reTrim, ''); var isBinary = reIsBinary.test(value); return (isBinary || reIsOctal.test(value)) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : (reIsBadHex.test(value) ? NAN : +value); } var toNumber_1 = toNumber; var FUNC_ERROR_TEXT = 'Expected a function'; /* Built-in method references for those with the same name as other `lodash` methods. */ var nativeMax = Math.max; var nativeMin = Math.min; /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. The debounced function comes with a `cancel` method to cancel * delayed `func` invocations and a `flush` method to immediately invoke them. * Provide `options` to indicate whether `func` should be invoked on the * leading and/or trailing edge of the `wait` timeout. The `func` is invoked * with the last arguments provided to the debounced function. Subsequent * calls to the debounced function return the result of the last `func` * invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.debounce` and `_.throttle`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] The number of milliseconds to delay. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=false] * Specify invoking on the leading edge of the timeout. * @param {number} [options.maxWait] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', _.debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })); * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); * var source = new EventSource('/stream'); * jQuery(source).on('message', debounced); * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel); */ function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } wait = toNumber_1(wait) || 0; if (isObject_1(options)) { leading = !!options.leading; maxing = 'maxWait' in options; maxWait = maxing ? nativeMax(toNumber_1(options.maxWait) || 0, wait) : maxWait; trailing = 'trailing' in options ? !!options.trailing : trailing; } function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { var time = now_1(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now_1()); } function debounced() { var time = now_1(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; } var debounce_1 = debounce; var FUNC_ERROR_TEXT$1 = 'Expected a function'; /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds. The throttled function comes with a `cancel` * method to cancel delayed `func` invocations and a `flush` method to * immediately invoke them. Provide `options` to indicate whether `func` * should be invoked on the leading and/or trailing edge of the `wait` * timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.throttle` and `_.debounce`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] The number of milliseconds to throttle invocations to. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=true] * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); * jQuery(element).on('click', throttled); * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT$1); } if (isObject_1(options)) { leading = 'leading' in options ? !!options.leading : leading; trailing = 'trailing' in options ? !!options.trailing : trailing; } return debounce_1(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); } var throttle_1 = throttle; function camelCaseToDash(str) { return str.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); } function makeDataAttributeString(props) { const keys = Object.keys(props); return keys .map((key) => { const _key = camelCaseToDash(key); const val = props[key]; if (val === undefined) return ''; return `data-${_key}="${val}" `; }) .join('') .trim(); } function getDefault(a, b) { return a !== undefined ? a : b; } function copyTextToClipboard(text) { // https://stackoverflow.com/a/30810322/5353542 var textArea = document.createElement('textarea'); // // *** This styling is an extra step which is likely not required. *** // // Why is it here? To ensure: // 1. the element is able to have focus and selection. // 2. if element was to flash render it has minimal visual impact. // 3. less flakyness with selection and copying which **might** occur if // the textarea element is not visible. // // The likelihood is the element won't even render, not even a flash, // so some of these are just precautions. However in IE the element // is visible whilst the popup box asking the user for permission for // the web page to copy to the clipboard. // // Place in top-left corner of screen regardless of scroll position. textArea.style.position = 'fixed'; textArea.style.top = 0; textArea.style.left = 0; // Ensure it has a small width and height. Setting to 1px / 1em // doesn't work as this gives a negative w/h on some browsers. textArea.style.width = '2em'; textArea.style.height = '2em'; // We don't need padding, reducing the size if it does flash render. textArea.style.padding = 0; // Clean up any borders. textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; // Avoid flash of white box if rendered for any reason. textArea.style.background = 'transparent'; textArea.value = text; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); } catch (err) { console.log('Oops, unable to copy'); } document.body.removeChild(textArea); } function isNumeric(val) { return !isNaN(val); } let throttle$1 = throttle_1; let debounce$2 = debounce_1; function promisify(fn, context = null) { return (...args) => { return new Promise(resolve => { setTimeout(() => { const out = fn.apply(context, args); resolve(out); }, 0); }); }; } function linkProperties(target, source, properties) { const props = properties.reduce((acc, prop) => { acc[prop] = { get() { return source[prop]; } }; return acc; }, {}); Object.defineProperties(target, props); } class DataManager { constructor(options) { this.options = options; this.sortRows = promisify(this.sortRows, this); this.switchColumn = promisify(this.switchColumn, this); this.removeColumn = promisify(this.removeColumn, this); this.filterRows = promisify(this.filterRows, this); } init(data) { if (!data) { data = this.options.data; } this.data = data; this.rowCount = 0; this.columns = []; this.rows = []; this.prepareColumns(); this.prepareRows(); this.prepareNumericColumns(); } // computed property get currentSort() { const col = this.columns.find(col => col.sortOrder !== 'none'); return col || { colIndex: -1, sortOrder: 'none' }; } prepareColumns() { this.columns = []; this.validateColumns(); this.prepareDefaultColumns(); this.prepareHeader(); } prepareDefaultColumns() { if (this.options.addCheckboxColumn && !this.hasColumnById('_checkbox')) { const cell = { id: '_checkbox', content: this.getCheckboxHTML(), editable: false, resizable: false, sortable: false, focusable: false, dropdown: false, width: 25 }; this.columns.push(cell); } if (this.options.addSerialNoColumn && !this.hasColumnById('_rowIndex')) { let cell = { id: '_rowIndex', content: '', align: 'center', editable: false, resizable: false, focusable: false, dropdown: false, width: 30 }; this.columns.push(cell); } } prepareRow(row, i) { const baseRowCell = { rowIndex: i }; return row .map((cell, i) => this.prepareCell(cell, i)) .map(cell => Object.assign({}, baseRowCell, cell)); } prepareHeader() { let columns = this.columns.concat(this.options.columns); const baseCell = { isHeader: 1, editable: true, sortable: true, resizable: true, focusable: true, dropdown: true, width: null, format: (value) => { if (value === null || value === undefined) { return ''; } return value + ''; } }; this.columns = columns .map((cell, i) => this.prepareCell(cell, i)) .map(col => Object.assign({}, baseCell, col)) .map(col => { col.id = col.id || col.content; return col; }); } prepareCell(content, i) { const cell = { content: '', align: 'left', sortOrder: 'none', colIndex: i, column: this.columns[i] }; if (content !== null && typeof content === 'object') { // passed as column/header Object.assign(cell, content); } else { cell.content = content; } return cell; } prepareNumericColumns() { const row0 = this.getRow(0); if (!row0) return; this.columns = this.columns.map((column, i) => { const cellValue = row0[i].content; if (!column.align && cellValue && isNumeric(cellValue)) { column.align = 'right'; } return column; }); } prepareRows() { this.validateData(this.data); this.rows = this.data.map((d, i) => { const index = this._getNextRowCount(); let row = []; if (Array.isArray(d)) { // row is an array if (this.options.addCheckboxColumn) { row.push(this.getCheckboxHTML()); } if (this.options.addSerialNoColumn) { row.push((index + 1) + ''); } row = row.concat(d); while (row.length < this.columns.length) { row.push(''); } } else { // row is a dict for (let col of this.columns) { if (col.id === '_checkbox') { row.push(this.getCheckboxHTML()); } else if (col.id === '_rowIndex') { row.push((index + 1) + ''); } else { row.push(col.format(d[col.id])); } } } return this.prepareRow(row, index); }); } validateColumns() { const columns = this.options.columns; if (!Array.isArray(columns)) { throw new DataError('`columns` must be an array'); } columns.forEach((column, i) => { if (typeof column !== 'string' && typeof column !== 'object') { throw new DataError(`column "${i}" must be a string or an object`); } }); } validateData(data) { if (Array.isArray(data) && (data.length === 0 || Array.isArray(data[0]) || typeof data[0] === 'object')) { return true; } throw new DataError('`data` must be an array of arrays or objects'); } appendRows(rows) { this.validateData(rows); this.rows = this.rows.concat(this.prepareRows(rows)); } sortRows(colIndex, sortOrder = 'none') { colIndex = +colIndex; // reset sortOrder and update for colIndex this.getColumns() .map(col => { if (col.colIndex === colIndex) { col.sortOrder = sortOrder; } else { col.sortOrder = 'none'; } }); this._sortRows(colIndex, sortOrder); } _sortRows(colIndex, sortOrder) { if (this.currentSort.colIndex === colIndex) { // reverse the array if only sortOrder changed if ( (this.currentSort.sortOrder === 'asc' && sortOrder === 'desc') || (this.currentSort.sortOrder === 'desc' && sortOrder === 'asc') ) { this.reverseArray(this.rows); this.currentSort.sortOrder = sortOrder; return; } } this.rows.sort((a, b) => { const _aIndex = a[0].rowIndex; const _bIndex = b[0].rowIndex; const _a = a[colIndex].content; const _b = b[colIndex].content; if (sortOrder === 'none') { return _aIndex - _bIndex; } else if (sortOrder === 'asc') { if (_a < _b) return -1; if (_a > _b) return 1; if (_a === _b) return 0; } else if (sortOrder === 'desc') { if (_a < _b) return 1; if (_a > _b) return -1; if (_a === _b) return 0; } return 0; }); if (this.hasColumnById('_rowIndex')) { // update row index const srNoColIndex = this.getColumnIndexById('_rowIndex'); this.rows = this.rows.map((row, index) => { return row.map(cell => { if (cell.colIndex === srNoColIndex) { cell.content = (index + 1) + ''; } return cell; }); }); } } reverseArray(array) { let left = null; let right = null; let length = array.length; for (left = 0, right = length - 1; left < right; left += 1, right -= 1) { const temporary = array[left]; array[left] = array[right]; array[right] = temporary; } } switchColumn(index1, index2) { // update columns const temp = this.columns[index1]; this.columns[index1] = this.columns[index2]; this.columns[index2] = temp; this.columns[index1].colIndex = index1; this.columns[index2].colIndex = index2; // update rows this.rows = this.rows.map(row => { const newCell1 = Object.assign({}, row[index1], { colIndex: index2 }); const newCell2 = Object.assign({}, row[index2], { colIndex: index1 }); let newRow = row.map(cell => { // make object copy return Object.assign({}, cell); }); newRow[index2] = newCell1; newRow[index1] = newCell2; return newRow; }); } removeColumn(index) { index = +index; const filter = cell => cell.colIndex !== index; const map = (cell, i) => Object.assign({}, cell, { colIndex: i }); // update columns this.columns = this.columns .filter(filter) .map(map); // update rows this.rows = this.rows.map(row => { const newRow = row .filter(filter) .map(map); return newRow; }); } updateRow(row, rowIndex) { if (row.length < this.columns.length) { if (this.hasColumnById('_rowIndex')) { const val = (rowIndex + 1) + ''; row = [val].concat(row); } if (this.hasColumnById('_checkbox')) { const val = ''; row = [val].concat(row); } } const _row = this.prepareRow(row, rowIndex); const index = this.rows.findIndex(row => row[0].rowIndex === rowIndex); this.rows[index] = _row; return _row; } updateCell(colIndex, rowIndex, options) { let cell; if (typeof colIndex === 'object') { // cell object was passed, // must have colIndex, rowIndex cell = colIndex; colIndex = cell.colIndex; rowIndex = cell.rowIndex; // the object passed must be merged with original cell options = cell; } cell = this.getCell(colIndex, rowIndex); // mutate object directly for (let key in options) { const newVal = options[key]; if (newVal !== undefined) { cell[key] = newVal; } } return cell; } updateColumn(colIndex, keyValPairs) { const column = this.getColumn(colIndex); for (let key in keyValPairs) { const newVal = keyValPairs[key]; if (newVal !== undefined) { column[key] = newVal; } } return column; } filterRows(keyword, colIndex) { let rowsToHide = []; let rowsToShow = []; const cells = this.rows.map(row => row[colIndex]); cells.forEach(cell => { const hay = cell.content.toLowerCase(); const needle = (keyword || '').toLowerCase(); if (!needle || hay.includes(needle)) { rowsToShow.push(cell.rowIndex); } else { rowsToHide.push(cell.rowIndex); } }); return {rowsToHide, rowsToShow}; } getRowCount() { return this.rowCount; } _getNextRowCount() { const val = this.rowCount; this.rowCount++; return val; } getRows(start, end) { return this.rows.slice(start, end); } getColumns(skipStandardColumns) { let columns = this.columns; if (skipStandardColumns) { columns = columns.slice(this.getStandardColumnCount()); } return columns; } getStandardColumnCount() { if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { return 2; } if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { return 1; } return 0; } getColumnCount(skipStandardColumns) { let val = this.columns.length; if (skipStandardColumns) { val = val - this.getStandardColumnCount(); } return val; } getColumn(colIndex) { colIndex = +colIndex; return this.columns.find(col => col.colIndex === colIndex); } getRow(rowIndex) { rowIndex = +rowIndex; return this.rows.find(row => row[0].rowIndex === rowIndex); } getCell(colIndex, rowIndex) { rowIndex = +rowIndex; colIndex = +colIndex; return this.rows.find(row => row[0].rowIndex === rowIndex)[colIndex]; } get() { return { columns: this.columns, rows: this.rows }; } hasColumn(name) { return Boolean(this.columns.find(col => col.content === name)); } hasColumnById(id) { return Boolean(this.columns.find(col => col.id === id)); } getColumnIndex(name) { return this.columns.findIndex(col => col.content === name); } getColumnIndexById(id) { return this.columns.findIndex(col => col.id === id); } getCheckboxHTML() { return ''; } } // Custom Errors class DataError extends TypeError {} class ColumnManager { constructor(instance) { this.instance = instance; linkProperties(this, this.instance, [ 'options', 'fireEvent', 'header', 'datamanager', 'style', 'wrapper', 'rowmanager', 'bodyScrollable' ]); this.bindEvents(); getDropdownHTML = getDropdownHTML.bind(this, this.options.dropdownButton); } renderHeader() { this.header.innerHTML = ''; this.refreshHeader(); } refreshHeader() { const columns = this.datamanager.getColumns(); if (!$('.data-table-col', this.header)) { // insert html let html = this.rowmanager.getRowHTML(columns, { isHeader: 1 }); if (this.options.enableInlineFilters) { html += this.rowmanager.getRowHTML(columns, { isFilter: 1 }); } $('thead', this.header).innerHTML = html; this.$filterRow = $('.data-table-row[data-is-filter]', this.header); if (this.$filterRow) { // hide filter row immediately, so it doesn't disturb layout $.style(this.$filterRow, { display: 'none' }); } } else { // refresh dom state const $cols = $.each('.data-table-col', this.header); if (columns.length < $cols.length) { // deleted column $('thead', this.header).innerHTML = this.rowmanager.getRowHTML(columns, { isHeader: 1 }); return; } $cols.map(($col, i) => { const column = columns[i]; // column sorted or order changed // update colIndex of each header cell $.data($col, { colIndex: column.colIndex }); // refresh sort indicator const sortIndicator = $('.sort-indicator', $col); if (sortIndicator) { sortIndicator.innerHTML = this.options.sortIndicator[column.sortOrder]; } }); } // reset columnMap this.$columnMap = []; } bindEvents() { this.bindDropdown(); this.bindResizeColumn(); this.bindMoveColumn(); this.bindFilter(); } bindDropdown() { let $activeDropdown; $.on(this.header, 'click', '.data-table-dropdown-toggle', (e, $button) => { const $dropdown = $.closest('.data-table-dropdown', $button); if (!$dropdown.classList.contains('is-active')) { deactivateDropdown(); $dropdown.classList.add('is-active'); $activeDropdown = $dropdown; } else { deactivateDropdown(); } }); $.on(document.body, 'click', (e) => { if (e.target.matches('.data-table-dropdown-toggle')) return; deactivateDropdown(); }); const dropdownItems = this.options.headerDropdown; $.on(this.header, 'click', '.data-table-dropdown-list > div', (e, $item) => { const $col = $.closest('.data-table-col', $item); const { index } = $.data($item); const { colIndex } = $.data($col); let callback = dropdownItems[index].action; callback && callback.call(this.instance, this.getColumn(colIndex)); }); function deactivateDropdown(e) { $activeDropdown && $activeDropdown.classList.remove('is-active'); $activeDropdown = null; } } bindResizeColumn() { let isDragging = false; let $resizingCell, startWidth, startX; $.on(this.header, 'mousedown', '.data-table-col .column-resizer', (e, $handle) => { document.body.classList.add('data-table-resize'); const $cell = $handle.parentNode.parentNode; $resizingCell = $cell; const { colIndex } = $.data($resizingCell); const col = this.getColumn(colIndex); if (col && col.resizable === false) { return; } isDragging = true; startWidth = $.style($('.content', $resizingCell), 'width'); startX = e.pageX; }); $.on(document.body, 'mouseup', (e) => { document.body.classList.remove('data-table-resize'); if (!$resizingCell) return; isDragging = false; const { colIndex } = $.data($resizingCell); this.setColumnWidth(colIndex); this.style.setBodyStyle(); $resizingCell = null; }); $.on(document.body, 'mousemove', (e) => { if (!isDragging) return; const finalWidth = startWidth + (e.pageX - startX); const { colIndex } = $.data($resizingCell); if (this.getColumnMinWidth(colIndex) > finalWidth) { // don't resize past minWidth return; } this.datamanager.updateColumn(colIndex, { width: finalWidth }); this.setColumnHeaderWidth(colIndex); }); } bindMoveColumn() { let initialized; const initialize = () => { if (initialized) { $.off(document.body, 'mousemove', initialize); return; } const ready = $('.data-table-col', this.header); if (!ready) return; const $parent = $('.data-table-row', this.header); this.sortable = Sortable.create($parent, { onEnd: (e) => { const { oldIndex, newIndex } = e; const $draggedCell = e.item; const { colIndex } = $.data($draggedCell); if (+colIndex === newIndex) return; this.switchColumn(oldIndex, newIndex); }, preventOnFilter: false, filter: '.column-resizer, .data-table-dropdown', animation: 150 }); }; $.on(document.body, 'mousemove', initialize); } bindSortColumn() { $.on(this.header, 'click', '.data-table-col .column-title', (e, span) => { const $cell = span.closest('.data-table-col'); let { colIndex, sortOrder } = $.data($cell); sortOrder = getDefault(sortOrder, 'none'); const col = this.getColumn(colIndex); if (col && col.sortable === false) { return; } // reset sort indicator $('.sort-indicator', this.header).textContent = ''; $.each('.data-table-col', this.header).map($cell => { $.data($cell, { sortOrder: 'none' }); }); let nextSortOrder, textContent; if (sortOrder === 'none') { nextSortOrder = 'asc'; textContent = '▲'; } else if (sortOrder === 'asc') { nextSortOrder = 'desc'; textContent = '▼'; } else if (sortOrder === 'desc') { nextSortOrder = 'none'; textContent = ''; } $.data($cell, { sortOrder: nextSortOrder }); $('.sort-indicator', $cell).textContent = textContent; this.sortColumn(colIndex, nextSortOrder); }); } sortColumn(colIndex, nextSortOrder) { this.instance.freeze(); this.sortRows(colIndex, nextSortOrder) .then(() => { this.refreshHeader(); return this.rowmanager.refreshRows(); }) .then(() => this.instance.unfreeze()) .then(() => { this.fireEvent('onSortColumn', this.getColumn(colIndex)); }); } removeColumn(colIndex) { const removedCol = this.getColumn(colIndex); this.instance.freeze(); this.datamanager.removeColumn(colIndex) .then(() => { this.refreshHeader(); return this.rowmanager.refreshRows(); }) .then(() => this.instance.unfreeze()) .then(() => { this.fireEvent('onRemoveColumn', removedCol); }); } switchColumn(oldIndex, newIndex) { this.instance.freeze(); this.datamanager.switchColumn(oldIndex, newIndex) .then(() => { this.refreshHeader(); return this.rowmanager.refreshRows(); }) .then(() => { this.setColumnWidth(oldIndex); this.setColumnWidth(newIndex); this.instance.unfreeze(); }) .then(() => { this.fireEvent('onSwitchColumn', this.getColumn(oldIndex), this.getColumn(newIndex) ); }); } toggleFilter() { this.isFilterShown = this.isFilterShown || false; if (this.isFilterShown) { $.style(this.$filterRow, { display: 'none' }); } else { $.style(this.$filterRow, { display: '' }); } this.isFilterShown = !this.isFilterShown; this.style.setBodyStyle(); } focusFilter(colIndex) { if (!this.isFilterShown) return; const $filterInput = $(`[data-col-index="${colIndex}"] .data-table-filter`, this.$filterRow); $filterInput.focus(); } bindFilter() { if (!this.options.enableInlineFilters) return; const handler = e => { const $filterCell = $.closest('.data-table-col', e.target); const { colIndex } = $.data($filterCell); const keyword = e.target.value; this.datamanager.filterRows(keyword, colIndex) .then(({ rowsToHide, rowsToShow }) => { rowsToHide.map(rowIndex => { const $tr = $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); $tr.classList.add('hide'); }); rowsToShow.map(rowIndex => { const $tr = $(`.data-table-row[data-row-index="${rowIndex}"]`, this.bodyScrollable); $tr.classList.remove('hide'); }); }); }; $.on(this.header, 'keydown', '.data-table-filter', debounce$2(handler, 300)); } sortRows(colIndex, sortOrder) { return this.datamanager.sortRows(colIndex, sortOrder); } getColumn(colIndex) { return this.datamanager.getColumn(colIndex); } getColumns() { return this.datamanager.getColumns(); } setColumnWidth(colIndex) { colIndex = +colIndex; this._columnWidthMap = this._columnWidthMap || []; const { width } = this.getColumn(colIndex); let index = this._columnWidthMap[colIndex]; const selector = `[data-col-index="${colIndex}"] .content, [data-col-index="${colIndex}"] .edit-cell`; const styles = { width: width + 'px' }; index = this.style.setStyle(selector, styles, index); this._columnWidthMap[colIndex] = index; } setColumnHeaderWidth(colIndex) { colIndex = +colIndex; this.$columnMap = this.$columnMap || []; const selector = `.data-table-header [data-col-index="${colIndex}"] .content`; const { width } = this.getColumn(colIndex); let $column = this.$columnMap[colIndex]; if (!$column) { $column = this.header.querySelector(selector); this.$columnMap[colIndex] = $column; } $column.style.width = width + 'px'; } getColumnMinWidth(colIndex) { colIndex = +colIndex; return this.getColumn(colIndex).minWidth || 24; } getFirstColumnIndex() { if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { return 2; } if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { return 1; } return 0; } getHeaderCell$(colIndex) { return $(`.data-table-col[data-col-index="${colIndex}"]`, this.header); } getLastColumnIndex() { return this.datamanager.getColumnCount() - 1; } getSerialColumnIndex() { const columns = this.datamanager.getColumns(); return columns.findIndex(column => column.content.includes('Sr. No')); } } // eslint-disable-next-line var getDropdownHTML = function getDropdownHTML(dropdownButton = 'v') { // add dropdown buttons const dropdownItems = this.options.headerDropdown; return `